diff --git a/README.md b/README.md index 3c925389..8bcb0cf2 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,6 @@ Work with code and text, run processes, and automate tasks, going far beyond oth - [Features](#features) - [How to install](#how-to-install) - [Usage](#usage) -- [Docker Support](#docker-support) - [Handling Long-Running Commands](#handling-long-running-commands) - [Work in Progress and TODOs](#roadmap) - [Sponsors and Supporters](#support-desktop-commander) @@ -70,11 +69,12 @@ Execute long-running terminal commands on your computer and manage processes thr - Detailed timestamps and arguments ## How to install -First, ensure you've downloaded and installed the [Claude Desktop app](https://claude.ai/download) and you have [npm installed](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm). -> **📋 Update & Uninstall Information:** Before choosing an installation option, note that **only Options 1 and 3 have automatic updates**. Options 2, 4, and 5 require manual updates. See the sections below for update and uninstall instructions for each option. +Desktop Commander offers multiple installation methods to fit different user needs and technical requirements. -### Option 1: Install through npx ⭐ **Auto-Updates** +> **📋 Update & Uninstall Information:** Before choosing an installation option, note that **only Options 1, 2, 3, and 6 have automatic updates**. Options 4 and 5 require manual updates. See the sections below for update and uninstall instructions for each option. + +### Option 1: Install through npx ⭐ **Auto-Updates** **Requires Node.js** Just run this in terminal: ``` npx @wonderwhy-er/desktop-commander@latest setup @@ -90,7 +90,7 @@ Restart Claude if running. **🔄 Manual Update:** Run the setup command again **đŸ—‘ī¸ Uninstall:** Run `npx @wonderwhy-er/desktop-commander@latest remove` -### Option 2: Using bash script installer (macOS) ⭐ **Auto-Updates** +### Option 2: Using bash script installer (macOS) ⭐ **Auto-Updates** **Installs Node.js if needed** For macOS users, you can use our automated bash installer which will check your Node.js version, install it if needed, and automatically configure Desktop Commander: ``` curl -fsSL https://raw.githubusercontent.com/wonderwhy-er/DesktopCommanderMCP/refs/heads/main/install.sh | bash @@ -101,7 +101,7 @@ This script handles all dependencies and configuration automatically for a seaml **🔄 Manual Update:** Re-run the bash installer command above **đŸ—‘ī¸ Uninstall:** Run `npx @wonderwhy-er/desktop-commander@latest remove` -### Option 3: Installing via Smithery ⭐ **Auto-Updates** +### Option 3: Installing via Smithery ⭐ **Auto-Updates** **Requires Node.js** To install Desktop Commander for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@wonderwhy-er/desktop-commander): @@ -113,7 +113,7 @@ npx -y @smithery/cli install @wonderwhy-er/desktop-commander --client claude **🔄 Manual Update:** Re-run the Smithery install command **đŸ—‘ī¸ Uninstall:** `npx -y @smithery/cli uninstall @wonderwhy-er/desktop-commander --client claude` -### Option 4: Add to claude_desktop_config manually ⭐ **Auto-Updates** +### Option 4: Add to claude_desktop_config manually ⭐ **Auto-Updates** **Requires Node.js** Add this entry to your claude_desktop_config.json: - On Mac: `~/Library/Application\ Support/Claude/claude_desktop_config.json` @@ -139,7 +139,7 @@ Restart Claude if running. **🔄 Manual Update:** Run the setup command again **đŸ—‘ī¸ Uninstall:** Run `npx @wonderwhy-er/desktop-commander@latest remove` or remove the "desktop-commander" entry from your claude_desktop_config.json file -### Option 5: Checkout locally ❌ **Manual Updates** +### ### Option 5: Checkout locally ❌ **Manual Updates** **Requires Node.js** ❌ **Manual Updates** **Requires Node.js** 1. Clone and build: ```bash git clone https://github.com/wonderwhy-er/DesktopCommanderMCP.git @@ -158,16 +158,168 @@ The setup command will: **🔄 Manual Update:** `cd DesktopCommanderMCP && git pull && npm run setup` **đŸ—‘ī¸ Uninstall:** Run `npx @wonderwhy-er/desktop-commander@latest remove` or remove the cloned directory and remove MCP server entry from Claude config +### Option 6: Docker Installation đŸŗ ⭐ **Auto-Updates** **No Node.js Required** + +Perfect for users who want complete or partial isolation or don't have Node.js installed. Desktop Commander runs in a sandboxed Docker container with a persistent work environment. + +#### Prerequisites +- [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed **and running** +- Claude Desktop app installed + +**Important:** Make sure Docker Desktop is fully started before running the installer. + +#### Automated Installation (Recommended) + +**macOS/Linux:** +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/wonderwhy-er/DesktopCommanderMCP/refs/heads/main/install-docker.sh) +``` + +**Windows PowerShell (Run as Administrator):** +```powershell +# Download and run the installer (one-liner) +iex ((New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/wonderwhy-er/DesktopCommanderMCP/refs/heads/main/install-docker.ps1')) +``` + +The automated installer will: +- Check Docker installation +- Pull the latest Docker image +- Prompt you to select folders for mounting +- Configure Claude Desktop automatically +- Restart Claude if possible + +#### How Docker Persistence Works +Desktop Commander creates a persistent work environment that remembers everything between sessions: +- **Your development tools**: Any software you install (Node.js, Python, databases, etc.) stays installed +- **Your configurations**: Git settings, SSH keys, shell preferences, and other personal configs are preserved +- **Your work files**: Projects and files in the workspace area persist across restarts +- **Package caches**: Downloaded packages and dependencies are cached for faster future installs + +Think of it like having your own dedicated development computer that never loses your setup, but runs safely isolated from your main system. + +#### Manual Docker Configuration + +If you prefer manual setup, add this to your claude_desktop_config.json: + +**Basic setup (no file access):** +```json +{ + "mcpServers": { + "desktop-commander-in-docker": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "mcp/desktop-commander:latest" + ] + } + } +} +``` + +**With folder mounting:** +```json +{ + "mcpServers": { + "desktop-commander-in-docker": { + "command": "docker", + "args": [ + "run", + "-i", + "--rm", + "-v", "/Users/username/Desktop:/mnt/desktop", + "-v", "/Users/username/Documents:/mnt/documents", + "mcp/desktop-commander:latest" + ] + } + } +} +``` + +**Advanced folder mounting:** +```json +{ + "mcpServers": { + "desktop-commander-in-docker": { + "command": "docker", + "args": [ + "run", "-i", "--rm", + "-v", "dc-system:/usr", + "-v", "dc-home:/root", + "-v", "dc-workspace:/workspace", + "-v", "dc-packages:/var", + "-v", "/Users/username/Projects:/mnt/Projects", + "-v", "/Users/username/Downloads:/mnt/Downloads", + "mcp/desktop-commander:latest" + ] + } + } +} +``` + +#### Docker Benefits +✅ **Controlled Isolation:** Runs in sandboxed environment with persistent development state +✅ **No Node.js Required:** Everything included in the container +✅ **Cross-Platform:** Same experience on all operating systems +✅ **Persistent Environment:** Your tools, files, configs, and work survives restarts + +**✅ Auto-Updates:** Yes - `latest` tag automatically gets newer versions +**🔄 Manual Update:** `docker pull mcp/desktop-commander:latest` then restart Claude + +#### Docker Management Commands + +**macOS/Linux:** + +Check installation status: +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/wonderwhy-er/DesktopCommanderMCP/refs/heads/main/install-docker.sh) --status +``` + +Reset all persistent data (removes all installed tools and configs): +```bash +bash <(curl -fsSL https://raw.githubusercontent.com/wonderwhy-er/DesktopCommanderMCP/refs/heads/main/install-docker.sh) --reset +``` + +**Windows PowerShell:** + +Check status: +```powershell +$script = (New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/wonderwhy-er/DesktopCommanderMCP/refs/heads/main/install-docker.ps1'); & ([ScriptBlock]::Create("$script")) -Status +``` + +Reset all data: +```powershell +$script = (New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/wonderwhy-er/DesktopCommanderMCP/refs/heads/main/install-docker.ps1'); & ([ScriptBlock]::Create("$script")) -Reset +``` + +Show help: +```powershell +$script = (New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/wonderwhy-er/DesktopCommanderMCP/refs/heads/main/install-docker.ps1'); & ([ScriptBlock]::Create("$script")) -Help +``` + +Verbose output: +```powershell +$script = (New-Object System.Net.WebClient).DownloadString('https://raw.githubusercontent.com/wonderwhy-er/DesktopCommanderMCP/refs/heads/main/install-docker.ps1'); & ([ScriptBlock]::Create("$script")) -VerboseOutput +``` + +#### Troubleshooting Docker Installation +If you broke the Docker container or need a fresh start: +```bash +# Reset and reinstall from scratch +bash <(curl -fsSL https://raw.githubusercontent.com/wonderwhy-er/DesktopCommanderMCP/refs/heads/main/install-docker.sh) --reset && bash <(curl -fsSL https://raw.githubusercontent.com/wonderwhy-er/DesktopCommanderMCP/refs/heads/main/install-docker.sh) +``` +This will completely reset your persistent environment and reinstall everything fresh with exception of not touching mounted folders + ## Updating & Uninstalling Desktop Commander -### Automatic Updates (Options 1, 2, 3 & 4) -**Options 1 (npx), Option 2 (bash installer), 3 (Smithery) and Option 4 (manual config)** automatically update to the latest version whenever you restart Claude. No manual intervention needed. +### Automatic Updates (Options 1, 2, 3, 4 & 6) +**Options 1 (npx), Option 2 (bash installer), 3 (Smithery), 4 (manual config), and 6 (Docker)** automatically update to the latest version whenever you restart Claude. No manual intervention needed. ### Manual Updates (Option 5) - **Option 5 (local checkout):** `cd DesktopCommanderMCP && git pull && npm run setup` ### Uninstalling Desktop Commander - #### 🤖 Automatic Uninstallation (Recommended) The easiest way to completely remove Desktop Commander: diff --git a/install-docker.ps1 b/install-docker.ps1 new file mode 100644 index 00000000..5a7e1d0d --- /dev/null +++ b/install-docker.ps1 @@ -0,0 +1,894 @@ +#!/usr/bin/env powershell +param( + [string]$Option = "", + [switch]$Help, + [switch]$Status, + [switch]$Reset, + [switch]$VerboseOutput +) + +# Script-level variables for folder and Docker args +$script:Folders = @() +$script:DockerArgs = @() + +# Colors and output functions +function Write-Success { param($Message) Write-Host "[SUCCESS] $Message" -ForegroundColor Green } +function Write-Error { param($Message) Write-Host "[ERROR] $Message" -ForegroundColor Red } +function Write-Warning { param($Message) Write-Host "[WARNING] $Message" -ForegroundColor Yellow } +function Write-Info { param($Message) Write-Host "[INFO] $Message" -ForegroundColor Blue } + +function Write-Header { + Write-Host "" + Write-Host "================================================================" -ForegroundColor Blue + Write-Host " CLAUDE " -ForegroundColor Blue + Write-Host " SERVER COMMANDER " -ForegroundColor Blue + Write-Host " Docker Installer " -ForegroundColor Blue + Write-Host "================================================================" -ForegroundColor Blue + Write-Host "" + Write-Info "Experiment with AI in secure sandbox environment that won't mess up your main computer" + Write-Host "" +} + +function Test-Docker { + while ($true) { + try { + $null = Get-Command docker -ErrorAction Stop + } catch { + Write-Error "Docker is not installed or not found" + Write-Host "" + Write-Error "Please install Docker first:" + Write-Error "Download Docker Desktop: https://www.docker.com/products/docker-desktop/" + Write-Host "" + $null = Read-Host "Press Enter when Docker Desktop is installed or Ctrl+C to exit" + continue + } + + Write-Info "Checking Docker installation and daemon status..." + try { + $null = docker info 2>$null + if ($LASTEXITCODE -eq 0) { + Write-Success "Docker is installed and running" + break + } else { + throw "Docker daemon not running" + } + } catch { + Write-Error "Docker is installed but not running" + Write-Host "" + Write-Error "Please start Docker Desktop and try again" + Write-Info "Make sure Docker Desktop is fully started (check system tray)" + Write-Host "" + $null = Read-Host "Press Enter when Docker Desktop is running or Ctrl+C to exit" + continue + } + } +} + +function Get-DockerImage { + Write-Info "Pulling latest Docker image (this may take a moment)..." + try { + docker pull mcp/desktop-commander:latest + if ($LASTEXITCODE -eq 0) { + Write-Success "Docker image ready: mcp/desktop-commander:latest" + } else { + Write-Error "Failed to pull Docker image" + Write-Info "Check your internet connection and Docker Hub access" + exit 1 + } + } catch { + Write-Error "Failed to get Docker image" + Write-Info "This could be a network issue or Docker Hub being unavailable" + exit 1 + } +}function Ask-ForFolders { + Write-Host "" + Write-Host "Folder Access Setup" -ForegroundColor Blue + Write-Info "By default, Desktop Commander will have access to your user folder:" + Write-Info "Folder: $env:USERPROFILE" + Write-Host "" + $response = Read-Host "Press Enter to accept user folder access or 'y' to customize" + + $script:Folders = @() + + if ($response -match "^[Yy]$") { + Write-Host "" + Write-Info "Custom folder selection:" + $homeResponse = Read-Host "Mount your complete home directory ($env:USERPROFILE)? [Y/n]" + + switch ($homeResponse.ToLower()) { + { $_ -in @("n", "no") } { + Write-Info "Skipping home directory" + } + default { + $script:Folders += $env:USERPROFILE + Write-Success "Added home directory access" + } + } + + Write-Host "" + Write-Info "Add extra folders outside home directory (optional):" + + while ($true) { + $customDir = Read-Host "Enter folder path (or Enter to finish)" + + if ([string]::IsNullOrEmpty($customDir)) { + break + } + + $customDir = [System.Environment]::ExpandEnvironmentVariables($customDir) + $customDir = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($customDir) + + if (Test-Path $customDir -PathType Container) { + $script:Folders += $customDir + Write-Success "Added: $customDir" + } else { + $addAnyway = Read-Host "Folder doesn't exist. Add anyway? [y/N]" + if ($addAnyway -match "^[Yy]$") { + $script:Folders += $customDir + Write-Info "Added: $customDir (will create if needed)" + } + } + } + + if ($script:Folders.Count -eq 0) { + Write-Host "" + Write-Warning "WARNING: No folders selected - Desktop Commander will have NO file access" + Write-Host "" + Write-Info "This means:" + Write-Host " - Desktop Commander cannot read or write any files on your computer" + Write-Host " - It cannot help with coding projects, file management, or document editing" + Write-Host " - It will only work for system commands and package installation" + Write-Host " - This makes Desktop Commander much less useful than intended" + Write-Host "" + Write-Info "You probably want to share at least some folder to work with files" + Write-Info "Most users share their home directory: $env:USERPROFILE" + Write-Host "" + $confirm = Read-Host "Continue with NO file access? [y/N]" + if ($confirm -notmatch "^[Yy]$") { + Write-Info "Restarting folder selection..." + Ask-ForFolders + return + } + Write-Warning "Proceeding with no file access - Desktop Commander will be limited" + } + } else { + $script:Folders += $env:USERPROFILE + Write-Success "Using default access to your user folder" + } +} + +function Initialize-Volumes { + Write-Info "Setting up persistent development environment" + Write-Host "" + Write-Info "Creating essential volumes for development persistence:" + Write-Info "- dc-system: All system packages, binaries, libraries" + Write-Info "- dc-home: User configs, dotfiles, SSH keys, git config" + Write-Info "- dc-workspace: Development files and projects" + Write-Info "- dc-packages: Package databases, caches, logs" + Write-Host "" + + $volumes = @("dc-system", "dc-home", "dc-workspace", "dc-packages") + $volumesCreated = 0 + + foreach ($volume in $volumes) { + try { + $null = docker volume inspect $volume 2>$null + if ($LASTEXITCODE -ne 0) { + docker volume create $volume | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Success "Created volume: $volume" + $volumesCreated++ + } else { + Write-Warning "Failed to create volume: $volume" + } + } else { + Write-Info "Volume already exists: $volume" + } + } catch { + Write-Warning "Could not manage volume: $volume" + } + } + + if ($volumesCreated -gt 0) { + Write-Host "" + Write-Success "Created $volumesCreated new volume(s)" + } + Write-Success "Persistent environment ready - your tools will survive restarts!" +} + +function Build-DockerArgs { + Write-Info "Building Docker configuration..." + + $script:DockerArgs = @("run", "-i", "--rm") + + $essentialVolumes = @( + "dc-system:/usr", + "dc-home:/root", + "dc-workspace:/workspace", + "dc-packages:/var" + ) + + foreach ($volume in $essentialVolumes) { + $script:DockerArgs += "-v" + $script:DockerArgs += $volume + } + + foreach ($folder in $script:Folders) { + # Get the full path structure after drive letter for absolute mounting + if ($folder -match '^([A-Za-z]):(.+)$') { + $absolutePath = $matches[2].TrimStart('\').Replace('\', '/') + $script:DockerArgs += "-v" + $script:DockerArgs += "${folder}:/home/$absolutePath" + } else { + # Fallback for non-standard paths - use basename + $folderName = Split-Path $folder -Leaf + $script:DockerArgs += "-v" + $script:DockerArgs += "${folder}:/home/${folderName}" + } + } + + $script:DockerArgs += "mcp/desktop-commander:latest" + + if ($VerboseOutput) { + Write-Info "Docker configuration ready" + Write-Info "Essential volumes: 4 volumes" + Write-Info "Mounted folders: $($script:Folders.Count) folders" + Write-Info "Container mode: Auto-remove after each use (--rm)" + } +}function Find-ClaudeProcess { + Write-Info "Looking for Claude Desktop processes..." + + # Try different process name patterns for Claude + $processNames = @("Claude", "claude", "Claude Desktop", "claude-desktop", "ClaudeDesktop") + $foundProcesses = @() + + foreach ($processName in $processNames) { + if ($VerboseOutput) { Write-Info "Checking for process pattern: $processName" } + try { + $processes = Get-Process -Name $processName -ErrorAction SilentlyContinue + if ($processes) { + foreach ($proc in $processes) { + $procPath = try { $proc.Path } catch { "Unknown" } + $procWindowTitle = try { $proc.MainWindowTitle } catch { "N/A" } + + $foundProcesses += @{ + Name = $proc.ProcessName + Id = $proc.Id + Path = $procPath + WindowTitle = $procWindowTitle + } + if ($VerboseOutput) { Write-Info "Found: $($proc.ProcessName) (PID: $($proc.Id))" } + } + } + } catch { + if ($VerboseOutput) { Write-Info "No processes found for pattern: $processName" } + } + } + + # Also try WMI for more comprehensive search + if ($VerboseOutput) { Write-Info "Searching with WMI for Claude-related processes..." } + try { + $wmiProcesses = Get-WmiObject Win32_Process | Where-Object { + $_.Name -like "*claude*" -or + $_.CommandLine -like "*claude*" -or + $_.ExecutablePath -like "*claude*" + } + + foreach ($proc in $wmiProcesses) { + $foundProcesses += @{ + Name = $proc.Name + Id = $proc.ProcessId + Path = $proc.ExecutablePath + CommandLine = $proc.CommandLine + } + if ($VerboseOutput) { Write-Info "WMI Found: $($proc.Name) (PID: $($proc.ProcessId))" } + } + } catch { + if ($VerboseOutput) { Write-Info "WMI search failed or no additional processes found" } + } + + return $foundProcesses +} + +function Stop-ClaudeProcess { + Write-Info "Attempting to stop Claude Desktop..." + + $processes = Find-ClaudeProcess + + if ($processes.Count -eq 0) { + Write-Info "No Claude Desktop processes found running" + return $true + } + + Write-Info "Found $($processes.Count) Claude-related process(es)" + foreach ($proc in $processes) { + Write-Info " - $($proc.Name) (PID: $($proc.Id))" + if ($proc.Path -and $proc.Path -ne "Unknown" -and $VerboseOutput) { + Write-Info " Path: $($proc.Path)" + } + } + + $stoppedCount = 0 + + # Method 1: Graceful shutdown using Stop-Process + Write-Info "Attempting graceful shutdown..." + foreach ($proc in $processes) { + try { + Stop-Process -Id $proc.Id -ErrorAction Stop + Write-Success "Gracefully stopped: $($proc.Name) (PID: $($proc.Id))" + $stoppedCount++ + } catch { + Write-Warning "Could not gracefully stop: $($proc.Name) (PID: $($proc.Id)) - $($_.Exception.Message)" + } + } + + # Wait a moment for graceful shutdown + Start-Sleep -Seconds 2 + + # Method 2: Force kill remaining processes + $remainingProcesses = Find-ClaudeProcess + if ($remainingProcesses.Count -gt 0) { + Write-Info "Force stopping remaining processes..." + foreach ($proc in $remainingProcesses) { + try { + Stop-Process -Id $proc.Id -Force -ErrorAction Stop + Write-Success "Force stopped: $($proc.Name) (PID: $($proc.Id))" + $stoppedCount++ + } catch { + Write-Error "Could not force stop: $($proc.Name) (PID: $($proc.Id)) - $($_.Exception.Message)" + } + } + } + + # Final verification + Start-Sleep -Seconds 1 + $finalProcesses = Find-ClaudeProcess + if ($finalProcesses.Count -eq 0) { + Write-Success "All Claude processes stopped successfully" + return $true + } else { + Write-Warning "Some Claude processes may still be running" + return $false + } +} + +function Find-ClaudeExecutable { + if ($VerboseOutput) { Write-Info "Searching for Claude Desktop executable..." } + + # Common installation paths for Claude Desktop + $claudePaths = @( + "$env:LOCALAPPDATA\Programs\Claude\Claude.exe", + "$env:PROGRAMFILES\Claude\Claude.exe", + "$env:PROGRAMFILES(x86)\Claude\Claude.exe", + "$env:APPDATA\Claude\Claude.exe", + "$env:USERPROFILE\AppData\Local\Programs\Claude\Claude.exe" + ) + + if ($VerboseOutput) { Write-Info "Checking standard installation paths..." } + foreach ($path in $claudePaths) { + if ($VerboseOutput) { Write-Info "Checking: $path" } + if (Test-Path $path) { + if ($VerboseOutput) { Write-Info "Found Claude executable: $path" } + return $path + } + } + + # Check registry for installation location + if ($VerboseOutput) { Write-Info "Searching Windows registry for Claude installation..." } + try { + $uninstallKeys = @( + "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*", + "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" + ) + + foreach ($keyPath in $uninstallKeys) { + try { + $entries = Get-ItemProperty $keyPath -ErrorAction SilentlyContinue | Where-Object { + $_.DisplayName -like "*Claude*" + } + foreach ($entry in $entries) { + if ($entry.InstallLocation) { + $possibleExe = Join-Path $entry.InstallLocation "Claude.exe" + if ($VerboseOutput) { Write-Info "Registry found: $($entry.DisplayName) at $possibleExe" } + if (Test-Path $possibleExe) { + if ($VerboseOutput) { Write-Info "Found Claude executable via registry: $possibleExe" } + return $possibleExe + } + } + if ($entry.UninstallString) { + $uninstallDir = Split-Path $entry.UninstallString -Parent + $possibleExe = Join-Path $uninstallDir "Claude.exe" + if ($VerboseOutput) { Write-Info "Trying uninstall directory: $possibleExe" } + if (Test-Path $possibleExe) { + if ($VerboseOutput) { Write-Info "Found Claude executable via uninstall path: $possibleExe" } + return $possibleExe + } + } + } + } catch { + if ($VerboseOutput) { Write-Info "Could not check registry key: $keyPath" } + } + } + } catch { + if ($VerboseOutput) { Write-Info "Registry search failed" } + } + + # Last resort: search common program directories + if ($VerboseOutput) { Write-Info "Performing broader search in program directories..." } + $searchDirs = @( + "$env:LOCALAPPDATA\Programs", + "$env:PROGRAMFILES", + "$env:PROGRAMFILES(x86)" + ) + + foreach ($dir in $searchDirs) { + if (Test-Path $dir) { + if ($VerboseOutput) { Write-Info "Searching in: $dir" } + try { + $found = Get-ChildItem -Path $dir -Recurse -Name "Claude.exe" -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($found) { + $fullPath = Join-Path $dir $found + if ($VerboseOutput) { Write-Info "Found Claude executable via search: $fullPath" } + return $fullPath + } + } catch { + if ($VerboseOutput) { Write-Info "Could not search in: $dir" } + } + } + } + + return $null +} + +function Start-ClaudeProcess { + Write-Info "Attempting to start Claude Desktop..." + + $claudePath = Find-ClaudeExecutable + + if (-not $claudePath) { + Write-Warning "Could not find Claude Desktop executable" + Write-Info "Claude may not be installed or may be in a non-standard location" + Write-Info "You can start Claude Desktop manually after installation completes" + return $false + } + + Write-Success "Found Claude executable: $claudePath" + + try { + Write-Info "Starting Claude Desktop..." + if ($VerboseOutput) { Write-Info "Executing: Start-Process -FilePath '$claudePath' -PassThru" } + $process = Start-Process -FilePath $claudePath -PassThru -ErrorAction Stop + + if ($process) { + Write-Success "Process started with PID: $($process.Id)" + + # Wait for the process to initialize + Write-Info "Waiting 5 seconds for Claude to initialize..." + Start-Sleep -Seconds 5 + + # Check if the process is still running + try { + $runningProcess = Get-Process -Id $process.Id -ErrorAction Stop + Write-Success "Claude Desktop is running and stable (PID: $($runningProcess.Id))" + Write-Success "Process name: $($runningProcess.ProcessName)" + if ($runningProcess.MainWindowTitle -and $VerboseOutput) { + Write-Info "Window title: $($runningProcess.MainWindowTitle)" + } + return $true + } catch { + Write-Warning "Process started but exited quickly (PID: $($process.Id))" + Write-Info "This might indicate a configuration issue" + return $false + } + } else { + Write-Error "Failed to start process - Start-Process returned null" + return $false + } + } catch { + Write-Error "Exception starting Claude Desktop: $($_.Exception.Message)" + if ($VerboseOutput) { Write-Info "Full exception: $($_.Exception)" } + return $false + } +} + +function Restart-Claude { + Write-Info "Attempting to restart Claude Desktop..." + + # Step 1: Stop Claude + $stopSuccess = Stop-ClaudeProcess + + if (-not $stopSuccess) { + Write-Warning "Failed to stop Claude completely, but continuing with startup attempt" + } + + # Step 2: Start Claude + $startSuccess = Start-ClaudeProcess + + # Step 3: Final verification + Write-Info "Verifying Claude restart..." + Start-Sleep -Seconds 2 + $finalProcesses = Find-ClaudeProcess + + if ($startSuccess -and $finalProcesses.Count -gt 0) { + Write-Success "Claude Desktop restart: SUCCESSFUL" + Write-Info "Claude Desktop is now running with updated configuration" + return $true + } else { + Write-Warning "Claude Desktop restart: PARTIAL or FAILED" + Write-Info "You may need to start Claude Desktop manually" + Write-Info "The configuration has been updated and will work when Claude starts" + return $false + } +} + +function Update-ClaudeConfig { + Write-Info "Updating Claude Desktop configuration..." + + $configPath = "$env:APPDATA\Claude\claude_desktop_config.json" + Write-Info "Config location: $configPath" + + # No backup needed - direct config update + + $configDir = Split-Path $configPath -Parent + if (!(Test-Path $configDir)) { + New-Item -ItemType Directory -Path $configDir -Force | Out-Null + Write-Info "Created config directory" + } + + # Read existing config or create new + $config = @{} + if (Test-Path $configPath) { + try { + # Read as JSON object first, then convert to hashtable if needed + $jsonContent = Get-Content $configPath -Raw | ConvertFrom-Json + + # Convert PSCustomObject to hashtable for easier manipulation + $config = @{} + foreach ($property in $jsonContent.PSObject.Properties) { + if ($property.Name -eq "mcpServers" -and $property.Value) { + # Preserve existing MCP servers + $config.mcpServers = @{} + foreach ($serverProperty in $property.Value.PSObject.Properties) { + $serverConfig = @{ + command = $serverProperty.Value.command + } + if ($serverProperty.Value.args) { + $serverConfig.args = @($serverProperty.Value.args) + } + if ($serverProperty.Value.env) { + $serverConfig.env = @{} + foreach ($envProperty in $serverProperty.Value.env.PSObject.Properties) { + $serverConfig.env[$envProperty.Name] = $envProperty.Value + } + } + $config.mcpServers[$serverProperty.Name] = $serverConfig + } + Write-Info "Preserved $($config.mcpServers.Count) existing MCP server(s)" + } else { + $config[$property.Name] = $property.Value + } + } + } catch { + Write-Warning "Could not parse existing config, creating new one" + Write-Warning "Error: $($_.Exception.Message)" + $config = @{} + } + } else { + Write-Info "Creating new Claude configuration" + } + + # Ensure mcpServers section exists + if (!$config.mcpServers) { + $config.mcpServers = @{} + Write-Info "Created new mcpServers section" + } + + # Check if our server already exists + if ($config.mcpServers.ContainsKey("desktop-commander")) { + Write-Info "Updating existing Desktop Commander configuration" + } else { + Write-Info "Adding new Desktop Commander configuration" + } + + # Convert PowerShell array to proper format for JSON + $argsArray = @() + foreach ($arg in $script:DockerArgs) { + $argsArray += $arg + } + + # Add/update our server configuration (this preserves all other servers) + $config.mcpServers["desktop-commander"] = @{ + command = "docker" + args = $argsArray + } + + # Save configuration + try { + $jsonConfig = $config | ConvertTo-Json -Depth 10 + [System.IO.File]::WriteAllText($configPath, $jsonConfig, [System.Text.UTF8Encoding]::new($false)) + Write-Success "Claude configuration updated successfully" + Write-Info "Server 'desktop-commander' added to MCP servers" + Write-Info "Total MCP servers configured: $($config.mcpServers.Count)" + + # List all configured servers + if ($config.mcpServers.Count -gt 1) { + Write-Host "" + Write-Info "All configured MCP servers:" + foreach ($serverName in $config.mcpServers.Keys) { + if ($serverName -eq "desktop-commander") { + Write-Info " * $serverName (Desktop Commander) - UPDATED" + } else { + Write-Info " * $serverName (preserved)" + } + } + } + + # Show what folders are mounted for our server + if ($script:Folders.Count -gt 0) { + Write-Host "" + Write-Info "Folders accessible to Desktop Commander:" + foreach ($folder in $script:Folders) { + $folderName = Split-Path $folder -Leaf + Write-Info " Folder: $folder" + Write-Info " -> /home/$folderName (home-style path)" + + # Show Windows-style path too + $windowsPath = $folder.Replace('\', '/') + if ($windowsPath -match '^([A-Za-z]):(.*)') { + $windowsStylePath = "/$($matches[1].ToLower())$($matches[2])" + Write-Info " -> $windowsStylePath (windows-style path)" + } + } + } else { + Write-Warning "No folders mounted - limited file access" + } + } catch { + Write-Error "Failed to save Claude configuration" + Write-Error "Error: $($_.Exception.Message)" + if (Test-Path "$configPath.backup-*") { + Write-Info "You can restore from backup if needed" + } + exit 1 + } +} + +function Show-Status { + Write-Header + Write-Info "Checking installation status..." + Write-Host "" + + try { + $null = docker info 2>$null + if ($LASTEXITCODE -eq 0) { + Write-Success "Docker daemon: Running" + } else { + Write-Warning "Docker daemon: Not running" + } + } catch { + Write-Warning "Docker: Not available" + } + + try { + $null = docker image inspect desktop-commander:latest 2>$null + if ($LASTEXITCODE -eq 0) { + Write-Success "Docker image: Available" + } else { + Write-Warning "Docker image: Missing" + } + } catch { + Write-Warning "Docker image: Cannot check" + } + + $volumes = @("dc-system", "dc-home", "dc-workspace", "dc-packages") + $volumesFound = 0 + + Write-Host "" + Write-Info "Persistent Volumes Status:" + + foreach ($volume in $volumes) { + try { + $null = docker volume inspect $volume 2>$null + if ($LASTEXITCODE -eq 0) { + $volumesFound++ + Write-Success " OK: $volume" + } else { + Write-Warning " MISSING: $volume" + } + } catch { + Write-Warning " UNKNOWN: $volume (cannot check)" + } + } + + $configPath = "$env:APPDATA\Claude\claude_desktop_config.json" + if (Test-Path $configPath) { + try { + $config = Get-Content $configPath | ConvertFrom-Json + if ($config.mcpServers."desktop-commander") { + Write-Success "Claude config: Desktop Commander configured" + } else { + Write-Warning "Claude config: Missing Desktop Commander server" + } + } catch { + Write-Warning "Claude config: Cannot parse" + } + } else { + Write-Warning "Claude config: File not found" + } + + Write-Host "" + Write-Host "Status Summary:" -ForegroundColor Yellow + Write-Host " Essential volumes: $volumesFound/4 found" + Write-Host " Container mode: Auto-remove (fresh containers)" + Write-Host " Persistence: Data stored in volumes" + + Write-Host "" + if ($volumesFound -eq 4) { + Write-Success "Ready to use with Claude!" + Write-Info "Each command creates a fresh container that uses your persistent volumes." + } elseif ($volumesFound -gt 0) { + Write-Warning "Some volumes missing - may need to reinstall" + Write-Info "Run reset and reinstall to fix this" + } else { + Write-Error "No volumes found - please run full installation" + Write-Info "Run: .\install-docker-clean.ps1" + } +}function Reset-Installation { + Write-Header + Write-Warning "This will remove ALL persistent container data!" + Write-Info "This includes:" + Write-Info " - All installed packages and software" + Write-Info " - All user configurations and settings" + Write-Info " - All development projects in /workspace" + Write-Info " - All package caches and databases" + Write-Host "" + Write-Info "Your mounted folders will NOT be affected." + Write-Host "" + + $confirm = Read-Host "Are you sure you want to reset everything? [y/N]" + if ($confirm -match "^[yY]") { + Write-Info "Cleaning up containers and volumes..." + + try { + $containers = docker ps -q --filter "ancestor=mcp/desktop-commander:latest" 2>$null + if ($containers -and $LASTEXITCODE -eq 0) { + docker stop $containers 2>$null | Out-Null + docker rm $containers 2>$null | Out-Null + Write-Info "Stopped running containers" + } + } catch { + # Ignore errors here + } + + Write-Info "Removing persistent volumes..." + $volumes = @("dc-system", "dc-home", "dc-workspace", "dc-packages") + $removedCount = 0 + + foreach ($volume in $volumes) { + try { + docker volume rm $volume 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { + Write-Success "Removed volume: $volume" + $removedCount++ + } else { + Write-Warning "Volume $volume is still in use or doesn't exist" + } + } catch { + Write-Warning "Error removing volume: $volume" + } + } + + Write-Host "" + Write-Success "Persistent data reset complete!" + if ($removedCount -gt 0) { + Write-Success "Successfully removed $removedCount volume(s)" + } + Write-Host "" + Write-Info "To reinstall after reset:" + Write-Info "Run: .\install-docker-clean.ps1" + } else { + Write-Info "Reset cancelled" + } +} + +function Show-Help { + Write-Host "Desktop Commander Docker Installation (Enhanced)" -ForegroundColor Blue + Write-Host "" + Write-Host "Usage:" + Write-Host " .\install-docker-clean.ps1 - Interactive installation with folder selection" + Write-Host " .\install-docker-clean.ps1 -Status - Check installation status" + Write-Host " .\install-docker-clean.ps1 -Reset - Reset all data" + Write-Host " .\install-docker-clean.ps1 -VerboseOutput - Show detailed output" + Write-Host " .\install-docker-clean.ps1 -Help - Show this help" + Write-Host "" + Write-Host "Features:" + Write-Host " - Interactive folder selection (like Mac version)" + Write-Host " - Custom folder mounting outside home directory" + Write-Host " - Persistent development environment" + Write-Host " - Enhanced configuration options" + Write-Host "" + Write-Host "Troubleshooting:" + Write-Host "If you broke the Docker container or need a fresh start:" + Write-Host " .\install-docker-clean.ps1 -Reset" + Write-Host " .\install-docker-clean.ps1" + Write-Host "" + Write-Host "This will completely reset your persistent environment and reinstall everything fresh." +} + +function Start-Installation { + Write-Header + + if ($Help) { + Show-Help + return + } + + if ($Status) { + Show-Status + return + } + + if ($Reset) { + Reset-Installation + return + } + + Test-Docker + Get-DockerImage + Ask-ForFolders + Initialize-Volumes + Build-DockerArgs + Update-ClaudeConfig + Restart-Claude + + Write-Host "" + Write-Success "Setup complete!" + Write-Host "" + Write-Info "How it works:" + Write-Info "- Desktop Commander runs in isolated containers" + Write-Info "- Your development tools and configs persist between uses" + Write-Info "- Each command creates a fresh, clean container" + + if ($script:Folders.Count -gt 0) { + Write-Host "" + Write-Info "Your accessible folders (dual mount paths):" + foreach ($folder in $script:Folders) { + $folderName = Split-Path $folder -Leaf + Write-Info " Folder: $folder" + Write-Info " -> /home/$folderName (home-style)" + + # Show Windows-style path too + $windowsPath = $folder.Replace('\', '/') + if ($windowsPath -match '^([A-Za-z]):(.*)') { + $windowsStylePath = "/$($matches[1].ToLower())$($matches[2])" + Write-Info " -> $windowsStylePath (windows-style)" + } + } + Write-Host "" + Write-Info "💡 Path Translation Examples:" + Write-Info " Windows: C:\Users\wonde\projects\file.txt" + Write-Info " Docker: /c/users/wonde/projects/file.txt (windows-style)" + Write-Info " Docker: /home/wonde/projects/file.txt (home-style)" + } + + Write-Host "" + Write-Info "To refresh/reset your persistent environment:" + Write-Info "- Run: .\install-docker-clean.ps1 -Reset" + Write-Info "- This removes all installed packages and resets everything" + Write-Host "" + Write-Info "If you broke the Docker container or need a fresh start:" + Write-Info "- Run: .\install-docker-clean.ps1 -Reset" + Write-Info "- Then: .\install-docker-clean.ps1" + Write-Info "- This will reset everything and reinstall from scratch" + Write-Host "" + Write-Info "Claude Desktop has been automatically restarted (if possible)" + Write-Success "Desktop Commander is available as 'desktop-commander' in Claude" + + Write-Host "" + Write-Info "Next steps: Install anything you want - it will persist!" + Write-Info "- Global packages: npm install -g typescript" + Write-Info "- User configs: git config, SSH keys, .bashrc" +} + +# Run installation +Start-Installation \ No newline at end of file diff --git a/install-docker.sh b/install-docker.sh new file mode 100755 index 00000000..0d05a375 --- /dev/null +++ b/install-docker.sh @@ -0,0 +1,601 @@ +#!/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 "🎉 Setup complete!" + 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 "💡 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 +show_management_info + +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" diff --git a/src/tools/config.ts b/src/tools/config.ts index aa4e4ba4..09617cf0 100644 --- a/src/tools/config.ts +++ b/src/tools/config.ts @@ -24,6 +24,7 @@ export async function getConfig() { isWindows: systemInfo.isWindows, isMacOS: systemInfo.isMacOS, isLinux: systemInfo.isLinux, + docker: systemInfo.docker, examplePaths: systemInfo.examplePaths } }; diff --git a/src/utils/system-info.ts b/src/utils/system-info.ts index 8ac3c54d..357f9dd0 100644 --- a/src/utils/system-info.ts +++ b/src/utils/system-info.ts @@ -1,4 +1,24 @@ import os from 'os'; +import fs from 'fs'; +import path from 'path'; + +export interface DockerMount { + hostPath: string; + containerPath: string; + type: 'bind' | 'volume'; + readOnly: boolean; + description: string; +} + +export interface DockerInfo { + isDocker: boolean; + mountPoints: DockerMount[]; + containerEnvironment?: { + dockerImage?: string; + containerName?: string; + hostPlatform?: string; + }; +} export interface SystemInfo { platform: string; @@ -8,13 +28,189 @@ export interface SystemInfo { isWindows: boolean; isMacOS: boolean; isLinux: boolean; + docker: DockerInfo; examplePaths: { home: string; temp: string; absolute: string; + accessible?: string[]; }; } +/** + * Detect if running inside Docker container + */ +function isRunningInDocker(): boolean { + // Method 1: MCP_CLIENT_DOCKER environment variable (set in Dockerfile) + if (process.env.MCP_CLIENT_DOCKER === 'true') { + return true; + } + + // Method 2: Check for .dockerenv file + if (fs.existsSync('/.dockerenv')) { + return true; + } + + // Method 3: Check /proc/1/cgroup for container indicators (Linux only) + if (os.platform() === 'linux') { + try { + const cgroup = fs.readFileSync('/proc/1/cgroup', 'utf8'); + if (cgroup.includes('docker') || cgroup.includes('containerd')) { + return true; + } + } catch (error) { + // /proc/1/cgroup might not exist + } + } + + return false; +} + +/** + * Discover Docker mount points + */ +function discoverDockerMounts(): DockerMount[] { + const mounts: DockerMount[] = []; + + if (!isRunningInDocker()) { + return mounts; + } + + // Method 1: Parse /proc/mounts (Linux only) + if (os.platform() === 'linux') { + try { + const mountsContent = fs.readFileSync('/proc/mounts', 'utf8'); + const mountLines = mountsContent.split('\n'); + + for (const line of mountLines) { + const parts = line.split(' '); + if (parts.length >= 4) { + const device = parts[0]; + const mountPoint = parts[1]; + const options = parts[3]; + + // Look for user mounts (skip system mounts) + if (mountPoint.startsWith('/mnt/') || + mountPoint.startsWith('/workspace') || + mountPoint.startsWith('/data/') || + (mountPoint.startsWith('/home/') && !mountPoint.startsWith('/home/root'))) { + + const isReadOnly = options.includes('ro'); + + mounts.push({ + hostPath: device, + containerPath: mountPoint, + type: 'bind', + readOnly: isReadOnly, + description: `Mounted directory: ${path.basename(mountPoint)}` + }); + } + } + } + } catch (error) { + // /proc/mounts might not be available + } + } + + // Method 2: Check /mnt directory contents + try { + if (fs.existsSync('/mnt')) { + const contents = fs.readdirSync('/mnt'); + for (const item of contents) { + const itemPath = `/mnt/${item}`; + try { + const stats = fs.statSync(itemPath); + if (stats.isDirectory()) { + // Check if we already have this mount + const exists = mounts.some(m => m.containerPath === itemPath); + if (!exists) { + mounts.push({ + hostPath: `/${item}`, + containerPath: itemPath, + type: 'bind', + readOnly: false, + description: `Mounted folder: ${item}` + }); + } + } + } catch (itemError) { + // Skip items we can't stat + } + } + } + } catch (error) { + // /mnt directory doesn't exist or not accessible + } + + // Method 3: Check /home directory for user-mounted folders (Desktop Commander Docker installer pattern) + try { + if (fs.existsSync('/home')) { + const contents = fs.readdirSync('/home'); + for (const item of contents) { + // Skip the root user directory and common system directories + if (item === 'root' || item === 'node' || item === 'bin' || item === 'sbin' || + item === 'usr' || item === 'lib' || item === 'lib64' || item === 'var' || + item === 'tmp' || item === 'opt' || item === 'sys' || item === 'proc') { + continue; + } + + const itemPath = `/home/${item}`; + try { + const stats = fs.statSync(itemPath); + if (stats.isDirectory()) { + // Check if we already have this mount + const exists = mounts.some(m => m.containerPath === itemPath); + if (!exists) { + mounts.push({ + hostPath: `/${item}`, + containerPath: itemPath, + type: 'bind', + readOnly: false, + description: `Host folder: ${item}` + }); + } + } + } catch (itemError) { + // Skip items we can't stat + } + } + } + } catch (error) { + // /home directory doesn't exist or not accessible + } + + return mounts; +} + +/** + * Get container environment information + */ +function getContainerEnvironment(): DockerInfo['containerEnvironment'] { + const env: DockerInfo['containerEnvironment'] = {}; + + // Try to get container name from hostname (often set to container ID/name) + try { + const hostname = os.hostname(); + if (hostname && hostname !== 'localhost') { + env.containerName = hostname; + } + } catch (error) { + // Hostname not available + } + + // Try to get Docker image from environment variables + if (process.env.DOCKER_IMAGE) { + env.dockerImage = process.env.DOCKER_IMAGE; + } + + // Try to detect host platform + if (process.env.HOST_PLATFORM) { + env.hostPlatform = process.env.HOST_PLATFORM; + } + + return Object.keys(env).length > 0 ? env : undefined; +} + /** * Get comprehensive system information for tool prompts */ @@ -24,6 +220,10 @@ export function getSystemInfo(): SystemInfo { const isMacOS = platform === 'darwin'; const isLinux = platform === 'linux'; + // Docker detection + const dockerDetected = isRunningInDocker(); + const mountPoints = dockerDetected ? discoverDockerMounts() : []; + let platformName: string; let defaultShell: string; let pathSeparator: string; @@ -68,6 +268,16 @@ export function getSystemInfo(): SystemInfo { }; } + // Adjust platform name for Docker + if (dockerDetected) { + platformName = `${platformName} (Docker)`; + + // Add accessible paths from mounts + if (mountPoints.length > 0) { + examplePaths.accessible = mountPoints.map(mount => mount.containerPath); + } + } + return { platform, platformName, @@ -76,6 +286,11 @@ export function getSystemInfo(): SystemInfo { isWindows, isMacOS, isLinux, + docker: { + isDocker: dockerDetected, + mountPoints, + containerEnvironment: getContainerEnvironment() + }, examplePaths }; } @@ -84,10 +299,57 @@ export function getSystemInfo(): SystemInfo { * Generate OS-specific guidance for tool prompts */ export function getOSSpecificGuidance(systemInfo: SystemInfo): string { - const { platformName, defaultShell, isWindows } = systemInfo; + const { platformName, defaultShell, isWindows, docker } = systemInfo; let guidance = `Running on ${platformName}. Default shell: ${defaultShell}.`; + // Docker-specific guidance + if (docker.isDocker) { + guidance += ` + +đŸŗ DOCKER ENVIRONMENT DETECTED: +This Desktop Commander instance is running inside a Docker container.`; + + if (docker.mountPoints.length > 0) { + guidance += ` + +AVAILABLE MOUNTED DIRECTORIES:`; + for (const mount of docker.mountPoints) { + const access = mount.readOnly ? '(read-only)' : '(read-write)'; + guidance += ` +- ${mount.containerPath} ${access} - ${mount.description}`; + } + + guidance += ` + +IMPORTANT: When users ask about files, FIRST check mounted directories above. +Files outside these paths will be lost when the container stops. +Always suggest using mounted directories for file operations. + +PATH TRANSLATION IN DOCKER: +When users provide host paths, translate to container paths: + +Windows: "C:\\projects\\data\\file.txt" → "/home/projects/data/file.txt" +Linux/Mac: "/Users/john/projects/data/file.txt" → "/home/projects/data/file.txt" + +Rules: Remove drive letter/user prefix, keep full folder structure, mount to /home/ + +NOTE: Desktop Commander Docker installer mounts host folders to /home/[folder-name].`; + } else { + guidance += ` + +âš ī¸ WARNING: No mounted directories detected. +Files created outside mounted volumes will be lost when the container stops. +Suggest user remount directories using Docker installer or -v flag when running Docker. +Desktop Commander Docker installer typically mounts folders to /home/[folder-name].`; + } + + if (docker.containerEnvironment?.containerName) { + guidance += ` +Container: ${docker.containerEnvironment.containerName}`; + } + } + if (isWindows) { guidance += ` @@ -161,5 +423,14 @@ COMMON LINUX DEVELOPMENT TOOLS: * Get path guidance (simplified since paths are normalized) */ export function getPathGuidance(systemInfo: SystemInfo): string { - return `Always use absolute paths for reliability. Paths are automatically normalized regardless of slash direction.`; + let guidance = `Always use absolute paths for reliability. Paths are automatically normalized regardless of slash direction.`; + + if (systemInfo.docker.isDocker && systemInfo.docker.mountPoints.length > 0) { + guidance += ` + +đŸŗ DOCKER: Prefer paths within mounted directories: ${systemInfo.docker.mountPoints.map(m => m.containerPath).join(', ')}. +When users ask about file locations, check these mounted paths first.`; + } + + return guidance; } \ No newline at end of file