#!/bin/bash # Desktop Commander Docker Installation Script set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Docker image - can be changed to latest DOCKER_IMAGE="mcp/desktop-commander:latest" CONTAINER_NAME="desktop-commander" # Global flag for verbose output VERBOSE=false print_header() { echo echo -e "${BLUE}██████╗ ███████╗███████╗██╗ ██╗████████╗ ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ███╗███╗ ███╗ █████╗ ███╗ ██╗██████╗ ███████╗██████╗${NC}" echo -e "${BLUE}██╔══██╗██╔════╝██╔════╝██║ ██╔╝╚══██╔══╝██╔═══██╗██╔══██╗ ██╔════╝██╔═══██╗████╗ ████║████╗ ████║██╔══██╗████╗ ██║██╔══██╗██╔════╝██╔══██╗${NC}" echo -e "${BLUE}██║ ██║█████╗ ███████╗█████╔╝ ██║ ██║ ██║██████╔╝ ██║ ██║ ██║██╔████╔██║██╔████╔██║███████║██╔██╗ ██║██║ ██║█████╗ ██████╔╝${NC}" echo -e "${BLUE}██║ ██║██╔══╝ ╚════██║██╔═██╗ ██║ ██║ ██║██╔═══╝ ██║ ██║ ██║██║╚██╔╝██║██║╚██╔╝██║██╔══██║██║╚██╗██║██║ ██║██╔══╝ ██╔══██╗${NC}" echo -e "${BLUE}██████╔╝███████╗███████║██║ ██╗ ██║ ╚██████╔╝██║ ╚██████╗╚██████╔╝██║ ╚═╝ ██║██║ ╚═╝ ██║██║ ██║██║ ╚████║██████╔╝███████╗██║ ██║${NC}" echo -e "${BLUE}╚═════╝ ╚══════╝╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ╚═╝${NC}" echo echo -e "${BLUE}🐳 Docker Installation${NC}" echo print_info "Experiment with AI in secure sandbox environment that won't mess up your main computer" echo } print_success() { echo -e "${GREEN}✅ $1${NC}" } print_error() { echo -e "${RED}❌ Error: $1${NC}" >&2 } print_warning() { echo -e "${YELLOW}⚠️ Warning: $1${NC}" } print_info() { echo -e "${BLUE}ℹ️ $1${NC}" } print_verbose() { if [ "$VERBOSE" = true ]; then echo -e "${BLUE}ℹ️ $1${NC}" fi } # Detect OS detect_os() { case "$OSTYPE" in darwin*) OS="macos" ;; linux*) OS="linux" ;; *) print_error "Unsupported OS: $OSTYPE" ; exit 1 ;; esac } # Get Claude config path based on OS get_claude_config_path() { case "$OS" in "macos") CLAUDE_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json" ;; "linux") CLAUDE_CONFIG="$HOME/.config/claude/claude_desktop_config.json" ;; esac } # Check if Docker is available check_docker() { while true; do if ! command -v docker >/dev/null 2>&1; then print_error "Docker is not installed or not found" echo print_error "Please install Docker first:" case "$OS" in "macos") print_error "• Download Docker Desktop: https://www.docker.com/products/docker-desktop/" ;; "linux") print_error "• Install Docker Engine: https://docs.docker.com/engine/install/" ;; esac echo echo -n "Press Enter when Docker Desktop is running or Ctrl+C to exit: " read -r continue fi if ! docker info >/dev/null 2>&1; then print_error "Docker is installed but not running" echo print_error "Please start Docker Desktop and try again" echo echo -n "Press Enter when Docker Desktop is running or Ctrl+C to exit: " read -r continue fi # If we get here, Docker is working break done print_success "Docker is available and running" } # Pull the Docker image pull_docker_image() { print_info "Pulling latest Docker image (this may take a moment)..." if docker pull "$DOCKER_IMAGE"; then print_success "Docker image ready: $DOCKER_IMAGE" else print_error "Failed to pull Docker image" print_info "Check your internet connection and Docker Hub access" exit 1 fi } # Ask user which folders to mount ask_for_folders() { echo echo -e "${BLUE}📁 Folder Access Setup${NC}" print_info "By default, Desktop Commander will have access to your user folder:" print_info "📂 $HOME" echo echo -n "Press Enter to accept user folder access or 'y' to customize: " read -r response FOLDERS=() if [[ $response =~ ^[Yy]$ ]]; then # Custom folder selection echo print_info "Custom folder selection:" echo -n "Mount your complete home directory ($HOME)? [Y/n]: " read -r home_response case "$home_response" in [nN]|[nN][oO]) print_info "Skipping home directory" ;; *) FOLDERS+=("$HOME") print_success "Added home directory access" ;; esac # Ask for additional folders echo print_info "Add extra folders outside home directory (optional):" while true; do echo -n "Enter folder path (or Enter to finish): " read -r custom_dir if [ -z "$custom_dir" ]; then break fi custom_dir="${custom_dir/#\~/$HOME}" if [ -d "$custom_dir" ]; then FOLDERS+=("$custom_dir") print_success "Added: $custom_dir" else echo -n "Folder doesn't exist. Add anyway? [y/N]: " read -r add_anyway if [[ $add_anyway =~ ^[Yy]$ ]]; then FOLDERS+=("$custom_dir") print_info "Added: $custom_dir (will create if needed)" fi fi done if [ ${#FOLDERS[@]} -eq 0 ]; then echo print_warning "⚠️ No folders selected - Desktop Commander will have NO file access" echo print_info "This means:" echo " • Desktop Commander cannot read or write any files on your computer" echo " • It cannot help with coding projects, file management, or document editing" echo " • It will only work for system commands and package installation" echo " • This makes Desktop Commander much less useful than intended" echo print_info "You probably want to share at least some folder to work with files" print_info "Most users share their home directory: $HOME" echo echo -n "Continue with NO file access? [y/N]: " read -r confirm if [[ ! $confirm =~ ^[Yy]$ ]]; then print_info "Restarting folder selection..." ask_for_folders return fi print_warning "Proceeding with no file access - Desktop Commander will be limited" fi else # Default: use home directory FOLDERS+=("$HOME") print_success "Using default access to your user folder" fi } # Setup essential volumes for maximum persistence setup_persistent_volumes() { print_verbose "🔧 Setting up persistent development environment" # Essential volumes that cover everything a developer needs ESSENTIAL_VOLUMES=( "dc-system:/usr" # All system packages, binaries, libraries "dc-home:/root" # User configs, dotfiles, SSH keys, git config "dc-workspace:/workspace" # Development files and projects "dc-packages:/var" # Package databases, caches, logs ) for volume in "${ESSENTIAL_VOLUMES[@]}"; do volume_name=$(echo "$volume" | cut -d':' -f1) if ! docker volume inspect "$volume_name" >/dev/null 2>&1; then docker volume create "$volume_name" >/dev/null 2>&1 fi done print_verbose "Persistent environment ready - your tools will survive restarts" } # Build Docker run arguments build_docker_args() { print_verbose "Building Docker configuration..." # Start with base arguments (use --rm so containers auto-remove after each use) DOCKER_ARGS=("run" "-i" "--rm") # Add essential persistent volumes for volume in "${ESSENTIAL_VOLUMES[@]}"; do DOCKER_ARGS+=("-v" "$volume") done # Add user folder mounts with absolute path structure for folder in "${FOLDERS[@]}"; do # Remove leading /Users/username or /home/username and keep absolute structure if [[ "$folder" =~ ^/Users/[^/]+(/.+)$ ]]; then # Mac: /Users/john/projects/data → /home/projects/data absolute_path="${BASH_REMATCH[1]}" DOCKER_ARGS+=("-v" "$folder:/home$absolute_path") elif [[ "$folder" =~ ^/home/[^/]+(/.+)$ ]]; then # Linux: /home/john/projects/data → /home/projects/data absolute_path="${BASH_REMATCH[1]}" DOCKER_ARGS+=("-v" "$folder:/home$absolute_path") else # Fallback for other paths - use basename folder_name=$(basename "$folder") DOCKER_ARGS+=("-v" "$folder:/home/$folder_name") fi done # Add the image DOCKER_ARGS+=("$DOCKER_IMAGE") print_verbose "Docker configuration ready" print_verbose "Essential volumes: ${#ESSENTIAL_VOLUMES[@]} volumes" print_verbose "Mounted folders: ${#FOLDERS[@]} folders" print_verbose "Container mode: Auto-remove after each use (--rm)" } # Update Claude desktop config update_claude_config() { print_verbose "Updating Claude Desktop configuration..." # Create config directory if it doesn't exist CONFIG_DIR=$(dirname "$CLAUDE_CONFIG") if [[ ! -d "$CONFIG_DIR" ]]; then mkdir -p "$CONFIG_DIR" print_verbose "Created config directory: $CONFIG_DIR" fi # Create config if it doesn't exist if [[ ! -f "$CLAUDE_CONFIG" ]]; then echo '{"mcpServers": {}}' > "$CLAUDE_CONFIG" print_verbose "Created new Claude config file" fi # Convert DOCKER_ARGS array to JSON format ARGS_JSON="[" for i in "${!DOCKER_ARGS[@]}"; do if [[ $i -gt 0 ]]; then ARGS_JSON+=", " fi ARGS_JSON+="\"${DOCKER_ARGS[$i]}\"" done ARGS_JSON+="]" # Use Python to update JSON (preserves existing MCP servers) python3 -c " import json import sys config_path = '$CLAUDE_CONFIG' docker_args = $ARGS_JSON try: with open(config_path, 'r') as f: config = json.load(f) except: config = {'mcpServers': {}} if 'mcpServers' not in config: config['mcpServers'] = {} # Configure to use docker run with essential volumes config['mcpServers']['desktop-commander'] = { 'command': 'docker', 'args': docker_args } with open(config_path, 'w') as f: json.dump(config, f, indent=2) print('Successfully updated Claude config') " || { print_error "Failed to update Claude config with Python" exit 1 } print_verbose "Updated Claude config: $CLAUDE_CONFIG" print_verbose "Desktop Commander will be available as 'desktop-commander' in Claude" } # Test the persistent setup test_persistence() { print_verbose "Testing persistent container setup..." print_verbose "Testing essential volumes with a temporary container..." # Test that essential paths are available for persistence if docker "${DOCKER_ARGS[@]}" /bin/bash -c " echo 'Testing persistence paths...' mkdir -p /workspace/test echo 'test-data' > /workspace/test/file.txt && echo 'Workspace persistence: OK' touch /root/.test_config && echo 'Home persistence: OK' echo 'Container test completed successfully' " >/dev/null 2>&1; then print_verbose "Essential persistence test passed" print_verbose "Volumes are working correctly" else print_verbose "Some persistence tests had issues (might still work)" fi } # Show container management commands show_management_info() { echo print_success "🎉 Installation successfully completed! Thank you for using Desktop Commander!" echo print_info "How it works:" echo "• Desktop Commander runs in isolated containers" echo "• Your development tools and configs persist between uses" echo "• Each command creates a fresh, clean container" echo print_info "🤔 Need help or have feedback? Happy to jump on a quick call:" echo " https://calendar.app.google/SHMNZN5MJznJWC5A7" echo print_info "💬 Join our community: https://discord.com/invite/kQ27sNnZr7" echo print_info "💡 If you broke the Docker container or need a fresh start:" echo "• Run: $0 --reset && $0" echo "• This will reset everything and reinstall from scratch" } # Reset all persistent data reset_persistence() { echo print_warning "This will remove ALL persistent container data!" echo "This includes:" echo " • All installed packages and software" echo " • All user configurations and settings" echo " • All development projects in /workspace" echo " • All package caches and databases" echo print_info "Your mounted folders will NOT be affected." echo read -p "Are you sure you want to reset everything? [y/N]: " -r case "$REPLY" in [yY]|[yY][eE][sS]) print_info "Cleaning up containers and volumes..." # Stop and remove any containers that might be using our volumes print_verbose "Stopping any running Desktop Commander containers..." docker ps -q --filter "ancestor=$DOCKER_IMAGE" | xargs -r docker stop >/dev/null 2>&1 || true docker ps -a -q --filter "ancestor=$DOCKER_IMAGE" | xargs -r docker rm >/dev/null 2>&1 || true # Also try by container name if it exists docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true docker rm "$CONTAINER_NAME" >/dev/null 2>&1 || true print_info "Removing persistent volumes..." local volumes=("dc-system" "dc-home" "dc-workspace" "dc-packages") local failed_volumes=() for volume in "${volumes[@]}"; do if docker volume rm "$volume" >/dev/null 2>&1; then print_success "✅ Removed volume: $volume" else failed_volumes+=("$volume") print_warning "⚠️ Volume $volume is still in use or doesn't exist" fi done # If some volumes failed, try harder cleanup if [ ${#failed_volumes[@]} -gt 0 ]; then print_info "Attempting force cleanup of remaining volumes..." # Remove ALL containers that might be holding references (more aggressive) docker container prune -f >/dev/null 2>&1 || true for volume in "${failed_volumes[@]}"; do if docker volume rm "$volume" >/dev/null 2>&1; then print_success "✅ Force removed volume: $volume" else print_error "❌ Could not remove volume: $volume" print_info "Manual cleanup needed: docker volume rm $volume" fi done fi print_success "🎉 Persistent data reset complete!" echo print_info "Run the installer again to create a fresh environment" ;; *) print_info "Reset cancelled" ;; esac } # Show status of current setup show_status() { echo print_header # Check essential volumes local volumes=("dc-system" "dc-home" "dc-workspace" "dc-packages") local volumes_found=0 echo "Essential volumes status:" for volume in "${volumes[@]}"; do if docker volume inspect "$volume" >/dev/null 2>&1; then local mountpoint mountpoint=$(docker volume inspect "$volume" --format '{{.Mountpoint}}' 2>/dev/null || echo "unknown") local size size=$(sudo du -sh "$mountpoint" 2>/dev/null | cut -f1 || echo "unknown") echo " ✅ $volume ($size)" ((volumes_found++)) else echo " ❌ $volume (missing)" fi done echo echo "Status Summary:" echo " Essential volumes: $volumes_found/4 found" echo " Container mode: Auto-remove (--rm)" echo " Persistence: Data stored in volumes" echo if [ "$volumes_found" -eq 4 ]; then echo "✅ Ready to use with Claude!" echo "Each command creates a fresh container that uses your persistent volumes." elif [ "$volumes_found" -gt 0 ]; then echo "⚠️ Some volumes missing - may need to reinstall" else echo "🚀 Run the installer to create your persistent volumes" fi } # Try to restart Claude automatically restart_claude() { print_info "Attempting to restart Claude..." case "$OS" in macos) # Kill Claude if running if pgrep -f "Claude" > /dev/null; then killall "Claude" 2>/dev/null || true sleep 2 print_info "Stopped Claude" fi # Try to start Claude if command -v open &> /dev/null; then if open -a "Claude" 2>/dev/null; then print_success "Claude restarted successfully" else print_warning "Could not auto-start Claude. Please start it manually." fi else print_warning "Could not auto-restart Claude. Please start it manually." fi ;; linux) # Kill Claude if running if pgrep -f "claude" > /dev/null; then pkill -f "claude" 2>/dev/null || true sleep 2 print_info "Stopped Claude" fi # Try to start Claude if command -v claude &> /dev/null; then if claude &>/dev/null & disown; then print_success "Claude restarted successfully" else print_warning "Could not auto-start Claude. Please start it manually." fi else print_warning "Could not auto-restart Claude. Please start it manually." fi ;; esac } # Help message show_help() { print_header echo "Usage: $0 [OPTION]" echo echo "Options:" echo " (no args) Interactive installation" echo " --verbose Show detailed technical output" echo " --reset Remove all persistent data" echo " --status Show current status" echo " --help Show this help" echo echo "Creates a persistent development container using 4 essential volumes:" echo " • dc-system: System packages and binaries (/usr)" echo " • dc-home: User configurations (/root)" echo " • dc-workspace: Development projects (/workspace)" echo " • dc-packages: Package databases and caches (/var)" echo echo "This covers 99% of development persistence needs with simple management." echo } # Main execution logic case "${1:-}" in --reset) print_header reset_persistence exit 0 ;; --status) show_status exit 0 ;; --help) show_help exit 0 ;; --verbose) VERBOSE=true # Continue to main installation flow ;; ""|--install) # Main installation flow ;; *) print_error "Unknown option: $1" echo "Use --help for usage information" exit 1 ;; esac # Main installation flow print_header detect_os print_success "Detected OS: $OS" get_claude_config_path print_info "Claude config path: $CLAUDE_CONFIG" check_docker pull_docker_image ask_for_folders setup_persistent_volumes build_docker_args update_claude_config test_persistence restart_claude echo print_success "✅ Claude has been restarted (if possible)" print_info "Desktop Commander is available as 'desktop-commander' in Claude" echo print_info "Next steps: Install anything you want - it will persist!" echo "• Global packages: npm install -g typescript" echo "• User configs: git config, SSH keys, .bashrc" show_management_info