From 007a1ca2849a6d95e5aebf4a7a22307448b0b2da Mon Sep 17 00:00:00 2001 From: LinYushen Date: Mon, 13 Apr 2026 18:05:22 +0800 Subject: [PATCH] feat(cli): add Windows installation support (#854) * feat(cli): add Windows installation support (MUL-689) Add PowerShell install script and Windows binary builds so Windows users can install the CLI without WSL. Co-Authored-By: Claude Opus 4.6 (1M context) * fix(cli): address PR review for Windows install script - Use GitHub REST API for Get-LatestVersion (PS 5.1 compatible) - Add SHA256 checksum verification after download - Use [System.Version] for proper semantic version comparison - Refactor $arch assignment for readability - Warn before git reset --hard in Install-Server Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .goreleaser.yml | 3 + CLI_INSTALL.md | 27 ++- README.md | 6 + README.zh-CN.md | 6 + scripts/install.ps1 | 403 ++++++++++++++++++++++++++++++++++++++++++++ scripts/install.sh | 5 +- 6 files changed, 447 insertions(+), 3 deletions(-) create mode 100644 scripts/install.ps1 diff --git a/.goreleaser.yml b/.goreleaser.yml index e61f09489..5737ae059 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -21,6 +21,9 @@ builds: goarch: - amd64 - arm64 + ignore: + - goos: windows + goarch: arm64 archives: - id: default diff --git a/CLI_INSTALL.md b/CLI_INSTALL.md index 6bb8a3718..fcefb154f 100644 --- a/CLI_INSTALL.md +++ b/CLI_INSTALL.md @@ -27,7 +27,9 @@ multica version ## Step 2: Install the Multica CLI -### Option A: Homebrew (preferred) +> **Windows users:** Skip to [Option C: Windows (PowerShell)](#option-c-windows-powershell) below. + +### Option A: Homebrew (preferred — macOS/Linux) Check if Homebrew is available: @@ -49,7 +51,7 @@ multica version If the version prints successfully, skip to **Step 3**. -### Option B: Download from GitHub Releases (no Homebrew) +### Option B: Download from GitHub Releases (macOS/Linux, no Homebrew) If Homebrew is not available, download the binary directly. @@ -85,6 +87,27 @@ multica version - On Linux, you may need `chmod +x /usr/local/bin/multica`. - If `sudo` is not available, install to a user-writable directory: `mv /tmp/multica ~/.local/bin/multica` and ensure `~/.local/bin` is in `$PATH`. +### Option C: Windows (PowerShell) + +Run in PowerShell (no admin required): + +```powershell +irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex +``` + +This downloads the latest Windows binary from GitHub Releases, installs it to `%USERPROFILE%\.multica\bin\`, and adds it to your user PATH. + +Verify: + +```powershell +multica version +``` + +**If this fails:** +- Restart your terminal so the updated PATH takes effect. +- If you use Scoop, the installer will use it automatically: `scoop bucket add multica https://github.com/multica-ai/scoop-bucket.git && scoop install multica` +- If your execution policy blocks the script: `Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned` then re-run. + --- ## Step 3: Log in diff --git a/README.md b/README.md index e9ca4b634..a5cbf67e0 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,12 @@ curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/ins Installs the Multica CLI on macOS and Linux. Works with Homebrew or downloads the binary directly. +**Windows (PowerShell):** + +```powershell +irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex +``` + After installation: ```bash diff --git a/README.zh-CN.md b/README.zh-CN.md index 6ba7b3028..03de6d0a4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -56,6 +56,12 @@ curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/ins 安装 Multica CLI,支持 macOS 和 Linux。有 Homebrew 用 Homebrew,没有则直接下载二进制。 +**Windows (PowerShell):** + +```powershell +irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex +``` + 安装完成后: ```bash diff --git a/scripts/install.ps1 b/scripts/install.ps1 new file mode 100644 index 000000000..4195468c6 --- /dev/null +++ b/scripts/install.ps1 @@ -0,0 +1,403 @@ +# Multica installer for Windows — one command to get started. +# +# Install CLI (default): connects to multica.ai +# irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex +# +# Self-host: starts a local Multica server + installs CLI + configures +# $env:MULTICA_MODE="local"; irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex +# + +$ErrorActionPreference = "Stop" + +# --------------------------------------------------------------------------- +# Configuration +# --------------------------------------------------------------------------- +$RepoUrl = "https://github.com/multica-ai/multica.git" +$RepoWebUrl = "https://github.com/multica-ai/multica" +$DefaultInstallDir = Join-Path $env:USERPROFILE ".multica\server" +$InstallDir = if ($env:MULTICA_INSTALL_DIR) { $env:MULTICA_INSTALL_DIR } else { $DefaultInstallDir } + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- +function Write-Info { param([string]$Msg) Write-Host "==> $Msg" -ForegroundColor Cyan } +function Write-Ok { param([string]$Msg) Write-Host "[OK] $Msg" -ForegroundColor Green } +function Write-Warn { param([string]$Msg) Write-Warning $Msg } +function Write-Fail { param([string]$Msg) Write-Host "[ERROR] $Msg" -ForegroundColor Red; exit 1 } + +function Test-CommandExists { + param([string]$Name) + $null -ne (Get-Command $Name -ErrorAction SilentlyContinue) +} + +function Get-LatestVersion { + try { + $release = Invoke-RestMethod -Uri "https://api.github.com/repos/multica-ai/multica/releases/latest" -ErrorAction Stop + return $release.tag_name + } catch { + return $null + } +} + +# --------------------------------------------------------------------------- +# CLI Installation +# --------------------------------------------------------------------------- +function Install-CliBinary { + Write-Info "Installing Multica CLI from GitHub Releases..." + + if (-not [Environment]::Is64BitOperatingSystem) { + Write-Fail "Multica requires a 64-bit Windows installation." + } + $arch = "amd64" + + $latest = Get-LatestVersion + if (-not $latest) { + Write-Fail "Could not determine latest release. Check your network connection." + } + + $url = "https://github.com/multica-ai/multica/releases/download/$latest/multica_windows_$arch.zip" + $tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "multica-install" + + if (Test-Path $tmpDir) { Remove-Item $tmpDir -Recurse -Force } + New-Item -ItemType Directory -Path $tmpDir | Out-Null + + Write-Info "Downloading $url ..." + try { + Invoke-WebRequest -Uri $url -OutFile (Join-Path $tmpDir "multica.zip") -UseBasicParsing + } catch { + Remove-Item $tmpDir -Recurse -Force + Write-Fail "Failed to download CLI binary: $_" + } + + # Verify SHA256 checksum + $checksumUrl = "https://github.com/multica-ai/multica/releases/download/$latest/checksums.txt" + try { + $checksums = Invoke-WebRequest -Uri $checksumUrl -UseBasicParsing -ErrorAction Stop + $zipFile = Join-Path $tmpDir "multica.zip" + $actualHash = (Get-FileHash -Path $zipFile -Algorithm SHA256).Hash.ToLower() + $expectedLine = ($checksums.Content -split "`n") | Where-Object { $_ -match "multica_windows_$arch\.zip" } | Select-Object -First 1 + if ($expectedLine) { + $expectedHash = ($expectedLine -split "\s+")[0].ToLower() + if ($actualHash -ne $expectedHash) { + Remove-Item $tmpDir -Recurse -Force + Write-Fail "Checksum verification failed. Expected: $expectedHash, Got: $actualHash" + } + Write-Ok "Checksum verified" + } else { + Write-Warn "Could not find checksum entry for windows_$arch — skipping verification." + } + } catch { + Write-Warn "Could not download checksums.txt — skipping verification." + } + + Expand-Archive -Path (Join-Path $tmpDir "multica.zip") -DestinationPath $tmpDir -Force + + $binDir = Join-Path $env:USERPROFILE ".multica\bin" + if (-not (Test-Path $binDir)) { + New-Item -ItemType Directory -Path $binDir -Force | Out-Null + } + + $exeSrc = Join-Path $tmpDir "multica.exe" + if (-not (Test-Path $exeSrc)) { + $exeSrc = Get-ChildItem -Path $tmpDir -Filter "multica.exe" -Recurse | Select-Object -First 1 -ExpandProperty FullName + } + if (-not $exeSrc -or -not (Test-Path $exeSrc)) { + Remove-Item $tmpDir -Recurse -Force + Write-Fail "multica.exe not found in downloaded archive." + } + + Copy-Item $exeSrc (Join-Path $binDir "multica.exe") -Force + Remove-Item $tmpDir -Recurse -Force + + Add-ToUserPath $binDir + Write-Ok "Multica CLI installed to $binDir\multica.exe" +} + +function Add-ToUserPath { + param([string]$Dir) + $currentPath = [Environment]::GetEnvironmentVariable("Path", "User") + if ($currentPath -and $currentPath.Split(";") -contains $Dir) { + return + } + $newPath = if ($currentPath) { "$currentPath;$Dir" } else { $Dir } + [Environment]::SetEnvironmentVariable("Path", $newPath, "User") + # Also update current session + if ($env:Path -notlike "*$Dir*") { + $env:Path = "$Dir;$env:Path" + } + Write-Info "Added $Dir to user PATH (restart your terminal for other sessions to pick it up)." +} + +function Install-CliScoop { + Write-Info "Installing Multica CLI via Scoop..." + try { + scoop bucket add multica https://github.com/multica-ai/scoop-bucket.git 2>$null + scoop install multica + Write-Ok "Multica CLI installed via Scoop" + } catch { + Write-Warn "Scoop install failed, falling back to direct download." + Install-CliBinary + } +} + +function Install-Cli { + if (Test-CommandExists "multica") { + $currentVer = (multica version 2>$null) -replace '.*?(v[\d.]+).*','$1' + $latestVer = Get-LatestVersion + + $currentCmp = $currentVer -replace '^v','' + $latestCmp = if ($latestVer) { $latestVer -replace '^v','' } else { $null } + + $isUpToDate = -not $latestCmp + if (-not $isUpToDate) { + try { + $isUpToDate = [System.Version]$currentCmp -ge [System.Version]$latestCmp + } catch { + $isUpToDate = $currentCmp -eq $latestCmp + } + } + + if ($isUpToDate) { + Write-Ok "Multica CLI is up to date ($currentVer)" + return + } + + Write-Info "Multica CLI $currentVer installed, latest is $latestVer - upgrading..." + Install-CliBinary + + $newVer = (multica version 2>$null) -replace '.*?(v[\d.]+).*','$1' + Write-Ok "Multica CLI upgraded ($currentVer -> $newVer)" + return + } + + if (Test-CommandExists "scoop") { + Install-CliScoop + } else { + Install-CliBinary + } + + if (-not (Test-CommandExists "multica")) { + Write-Fail "CLI installed but 'multica' not found on PATH. Restart your terminal and try again." + } +} + +# --------------------------------------------------------------------------- +# Docker check +# --------------------------------------------------------------------------- +function Test-Docker { + if (-not (Test-CommandExists "docker")) { + Write-Fail @" +Docker is not installed. Multica self-hosting requires Docker and Docker Compose. + +Install Docker Desktop for Windows: + https://docs.docker.com/desktop/install/windows-install/ + +After installing Docker, re-run this script with `$env:MULTICA_MODE="local"`. +"@ + } + + try { + docker info 2>$null | Out-Null + } catch { + Write-Fail "Docker is installed but not running. Please start Docker Desktop and re-run this script." + } + + Write-Ok "Docker is available" +} + +# --------------------------------------------------------------------------- +# Server setup (self-host / local) +# --------------------------------------------------------------------------- +function Install-Server { + Write-Info "Setting up Multica server..." + + if (Test-Path (Join-Path $InstallDir ".git")) { + Write-Info "Updating existing installation at $InstallDir..." + Write-Warn "Any local changes in $InstallDir will be overwritten." + Push-Location $InstallDir + git fetch origin main --depth 1 2>$null + git reset --hard origin/main 2>$null + Pop-Location + } else { + Write-Info "Cloning Multica repository..." + if (-not (Test-CommandExists "git")) { + Write-Fail "Git is not installed. Please install git and re-run." + } + if (Test-Path $InstallDir) { + Write-Warn "Removing incomplete installation at $InstallDir..." + Remove-Item $InstallDir -Recurse -Force + } + $parentDir = Split-Path $InstallDir -Parent + if (-not (Test-Path $parentDir)) { + New-Item -ItemType Directory -Path $parentDir -Force | Out-Null + } + git clone --depth 1 $RepoUrl $InstallDir + } + + Write-Ok "Repository ready at $InstallDir" + + Push-Location $InstallDir + + if (-not (Test-Path ".env")) { + Write-Info "Creating .env with random JWT_SECRET..." + Copy-Item ".env.example" ".env" + $jwt = -join ((1..32) | ForEach-Object { "{0:x2}" -f (Get-Random -Maximum 256) }) + (Get-Content ".env") -replace '^JWT_SECRET=.*', "JWT_SECRET=$jwt" | Set-Content ".env" + Write-Ok "Generated .env with random JWT_SECRET" + } else { + Write-Ok "Using existing .env" + } + + Write-Info "Starting Multica services (this may take a few minutes on first run)..." + docker compose -f docker-compose.selfhost.yml up -d --build + + Write-Info "Waiting for backend to be ready..." + $ready = $false + for ($i = 1; $i -le 45; $i++) { + try { + $null = Invoke-WebRequest -Uri "http://localhost:8080/health" -UseBasicParsing -TimeoutSec 2 + $ready = $true + break + } catch { + Start-Sleep -Seconds 2 + } + } + + if ($ready) { + Write-Ok "Multica server is running" + } else { + Write-Warn "Server is still starting. Check logs with:" + Write-Host " cd $InstallDir; docker compose -f docker-compose.selfhost.yml logs" + } + + Pop-Location +} + +# --------------------------------------------------------------------------- +# Configure CLI +# --------------------------------------------------------------------------- +function Set-ConfigLocal { + Write-Info "Configuring CLI for local server..." + try { + multica config local 2>$null + } catch { + multica config set app_url http://localhost:3000 2>$null + multica config set server_url http://localhost:8080 2>$null + } + Write-Ok "CLI configured for localhost (backend :8080, frontend :3000)" +} + +function Set-ConfigCloud { + Write-Info "Configuring CLI for Multica Cloud..." + multica config set server_url https://api.multica.ai 2>$null + multica config set app_url https://multica.ai 2>$null + Write-Ok "CLI configured for multica.ai" +} + +# --------------------------------------------------------------------------- +# Main: Default mode (cloud) +# --------------------------------------------------------------------------- +function Start-DefaultInstall { + Write-Host "" + Write-Host " Multica - Installer" -ForegroundColor White + Write-Host " Installing the CLI to connect to multica.ai" -ForegroundColor Cyan + Write-Host "" + + Install-Cli + Set-ConfigCloud + + Write-Host "" + Write-Host " ============================================" -ForegroundColor Green + Write-Host " [OK] Multica CLI is installed!" -ForegroundColor Green + Write-Host " ============================================" -ForegroundColor Green + Write-Host "" + Write-Host " Next steps:" + Write-Host "" + Write-Host " multica login " -NoNewline; Write-Host "# Authenticate with multica.ai" -ForegroundColor DarkGray + Write-Host " multica daemon start " -NoNewline; Write-Host "# Start the agent daemon" -ForegroundColor DarkGray + Write-Host "" + Write-Host " Or do it all in one command:" + Write-Host "" + Write-Host " multica setup" -ForegroundColor Cyan + Write-Host "" + Write-Host " Self-hosting? Re-run with:" + Write-Host ' $env:MULTICA_MODE="local"; irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex' + Write-Host "" +} + +# --------------------------------------------------------------------------- +# Main: Local mode (self-host) +# --------------------------------------------------------------------------- +function Start-LocalInstall { + Write-Host "" + Write-Host " Multica - Self-Host Installer" -ForegroundColor White + Write-Host " Setting up a local Multica server + CLI" + Write-Host "" + + Test-Docker + Install-Server + Install-Cli + Set-ConfigLocal + + Write-Host "" + Write-Host " ============================================" -ForegroundColor Green + Write-Host " [OK] Multica is installed and running!" -ForegroundColor Green + Write-Host " ============================================" -ForegroundColor Green + Write-Host "" + Write-Host " Frontend: http://localhost:3000" + Write-Host " Backend: http://localhost:8080" + Write-Host " Server at: $InstallDir" + Write-Host "" + Write-Host " Next steps:" + Write-Host " 1. Open http://localhost:3000 in your browser" + Write-Host " 2. Log in with any email + verification code: 888888" + Write-Host " 3. Then run:" + Write-Host "" + Write-Host " multica login " -NoNewline; Write-Host "# Authenticate (opens browser)" -ForegroundColor DarkGray + Write-Host " multica daemon start " -NoNewline; Write-Host "# Start the agent daemon" -ForegroundColor DarkGray + Write-Host "" + Write-Host " To stop all services:" + Write-Host ' $env:MULTICA_MODE="stop"; irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex' + Write-Host "" +} + +# --------------------------------------------------------------------------- +# Stop: shut down a self-hosted installation +# --------------------------------------------------------------------------- +function Start-Stop { + Write-Host "" + Write-Info "Stopping Multica services..." + + if (Test-Path $InstallDir) { + Push-Location $InstallDir + if (Test-Path "docker-compose.selfhost.yml") { + docker compose -f docker-compose.selfhost.yml down + Write-Ok "Docker services stopped" + } else { + Write-Warn "No docker-compose.selfhost.yml found at $InstallDir" + } + Pop-Location + } else { + Write-Warn "No Multica installation found at $InstallDir" + } + + if (Test-CommandExists "multica") { + try { + multica daemon stop 2>$null + Write-Ok "Daemon stopped" + } catch {} + } + + Write-Host "" +} + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- +$mode = if ($env:MULTICA_MODE) { $env:MULTICA_MODE.ToLower() } else { "default" } + +switch ($mode) { + "local" { Start-LocalInstall } + "stop" { Start-Stop } + default { Start-DefaultInstall } +} diff --git a/scripts/install.sh b/scripts/install.sh index 8bf16cbda..3e333a830 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -43,7 +43,10 @@ detect_os() { case "$(uname -s)" in Darwin) OS="darwin" ;; Linux) OS="linux" ;; - *) fail "Unsupported operating system: $(uname -s). Multica supports macOS and Linux." ;; + MINGW*|MSYS*|CYGWIN*) + fail "This script does not support Windows. Use the PowerShell installer instead: + irm https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.ps1 | iex" ;; + *) fail "Unsupported operating system: $(uname -s). Multica supports macOS, Linux, and Windows." ;; esac ARCH="$(uname -m)"