diff --git a/.github/workflows/e2e-install.yml b/.github/workflows/e2e-install.yml index f69c222..e650fbe 100644 --- a/.github/workflows/e2e-install.yml +++ b/.github/workflows/e2e-install.yml @@ -89,6 +89,11 @@ jobs: exit 1 fi + echo "--- squad-cli ---" + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + squad --version | head -1 + echo "All Linux assertions passed" ' @@ -225,6 +230,11 @@ jobs: exit 1 fi + echo "--- squad-cli ---" + export NVM_DIR="$HOME/.nvm" + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + squad --version | head -1 + echo "All macOS assertions passed" ' @@ -373,6 +383,9 @@ jobs: $failures += 'profile' } + Write-Host "--- squad-cli ---" + Assert-Command 'squad' '--version' + if ($failures.Count -gt 0) { Write-Host "`nFailed assertions: $($failures -join ', ')" exit 1 diff --git a/.tool-versions b/.tool-versions index 541bb76..ac7553f 100644 --- a/.tool-versions +++ b/.tool-versions @@ -3,4 +3,5 @@ nvm 0.39.7 nvm-windows 1.2.2 uv 0.4.18 copilot-cli 1.0.48 +squad-cli 0.10.0 gh 2.92.0 diff --git a/README.md b/README.md index 9478c38..0dea8d5 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ | `nvm` + Node.js LTS | Node Version Manager + latest Node LTS | | `gh` | GitHub CLI | | GitHub Copilot CLI | AI pair programmer in your terminal (`gh copilot`) | +| `squad` CLI | Multi-agent copilot orchestrator (`gosquad` launcher) | | `vim` | Modal text editor -- installed on all platforms | | `tmux` | Terminal multiplexer (Linux/macOS) | | `psmux` | Terminal multiplexer (Windows) | @@ -93,7 +94,8 @@ dev-setup/ | | \-- tools/ -- Individual tool install scripts | | +-- auth.sh -- GitHub CLI authentication (interactive) | | +-- copilot-cli.sh -| | +-- gh.sh +| | +-- squad-cli.sh +| +-- gh.sh | | +-- nvm.sh | | +-- uv.sh | | \-- zsh.sh @@ -103,9 +105,9 @@ dev-setup/ | \-- tools/ -- Per-tool install scripts | +-- auth.ps1 -- GitHub CLI authentication (interactive) | +-- copilot.ps1, dotfiles.ps1, gh.ps1, git.ps1, nvm.ps1 -| +-- profile.ps1, psmux.ps1 +| +-- profile.ps1, psmux.ps1, squad-cli.ps1 | +-- uv.ps1, vim.ps1 -| \-- (9 files total) +| \-- (10 files total) +-- config/ | \-- dotfiles/ -- Dotfile templates (.aliases, .gitconfig, .editorconfig, etc.) | \-- install.sh -- Dotfile installer diff --git a/scripts/linux/setup.sh b/scripts/linux/setup.sh index 0e7a0d9..5460544 100755 --- a/scripts/linux/setup.sh +++ b/scripts/linux/setup.sh @@ -85,6 +85,7 @@ main() { run_tool "gh" run_tool "auth" run_tool "copilot-cli" + run_tool "squad-cli" # Apply dotfiles if installer script exists local dotfiles_script="${REPO_ROOT}/config/dotfiles/install.sh" diff --git a/scripts/linux/tools/squad-cli.sh b/scripts/linux/tools/squad-cli.sh new file mode 100644 index 0000000..66553af --- /dev/null +++ b/scripts/linux/tools/squad-cli.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# scripts/linux/tools/squad-cli.sh -- Install squad-cli globally via npm at pinned version +# +# Called by: scripts/linux/setup.sh +# Idempotent: yes -- version-aware; upgrades if installed version != pinned version. + +set -euo pipefail + +# shellcheck disable=SC1091 +. "$(dirname "${BASH_SOURCE[0]}")/../lib/log.sh" + +# Source nvm if available, to get node/npm on PATH in this subshell. +export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" +# shellcheck source=/dev/null +if [ -s "$NVM_DIR/nvm.sh" ]; then + . "$NVM_DIR/nvm.sh" --no-use + nvm use default 2>/dev/null || true +fi + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SQUAD_CLI_VERSION="$(sh "${SCRIPT_DIR}/../../lib/read-tool-version.sh" squad-cli)" + +log_info "Pinned squad-cli version: ${SQUAD_CLI_VERSION}" + +# Detect installed version; use timeout to prevent a network-dependent binary from hanging +INSTALLED_VERSION="" +if command -v squad &>/dev/null; then + INSTALLED_VERSION="$(timeout 10 squad --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)" +fi + +if [ "${INSTALLED_VERSION}" = "${SQUAD_CLI_VERSION}" ]; then + log_ok "squad-cli already at pinned version ${SQUAD_CLI_VERSION}" + exit 0 +fi + +if [ -n "${INSTALLED_VERSION}" ]; then + log_info "squad-cli ${INSTALLED_VERSION} installed; upgrading to pinned ${SQUAD_CLI_VERSION}..." +else + log_info "Installing squad-cli ${SQUAD_CLI_VERSION}..." +fi + +if ! command -v npm &>/dev/null; then + log_warn "npm not found -- cannot install squad-cli; run 'npm install -g @bradygaster/squad-cli@${SQUAD_CLI_VERSION}' once Node is available" + exit 0 +fi + +npm install -g --no-fund --no-audit "@bradygaster/squad-cli@${SQUAD_CLI_VERSION}" +log_ok "squad-cli installed at ${SQUAD_CLI_VERSION}" diff --git a/scripts/windows/setup.ps1 b/scripts/windows/setup.ps1 index 6a16797..4b5ea2f 100644 --- a/scripts/windows/setup.ps1 +++ b/scripts/windows/setup.ps1 @@ -27,6 +27,7 @@ function Test-WingetAvailable { . "$PSScriptRoot\tools\vim.ps1" . "$PSScriptRoot\tools\psmux.ps1" . "$PSScriptRoot\tools\copilot.ps1" +. "$PSScriptRoot\tools\squad-cli.ps1" . "$PSScriptRoot\tools\dotfiles.ps1" . "$PSScriptRoot\tools\profile.ps1" . "$PSScriptRoot\tools\auth.ps1" @@ -60,6 +61,7 @@ function Main { Install-Vim Install-Psmux Install-CopilotCli + Install-SquadCli Install-Dotfiles Write-PowerShellProfile Install-GitHook diff --git a/scripts/windows/tools/squad-cli.ps1 b/scripts/windows/tools/squad-cli.ps1 new file mode 100644 index 0000000..e9560f1 --- /dev/null +++ b/scripts/windows/tools/squad-cli.ps1 @@ -0,0 +1,44 @@ +# scripts/windows/tools/squad-cli.ps1 - squad-cli installer +# +# Installs squad-cli globally via npm at pinned version from .tool-versions. +# Version-aware: upgrades if installed version != pinned version. + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +. "$PSScriptRoot\..\lib\logging.ps1" +. "$PSScriptRoot\..\lib\path.ps1" +. "$PSScriptRoot\..\..\lib\Read-ToolVersion.ps1" + +function Install-SquadCli { + $SquadCliVersion = Get-ToolVersion -Name 'squad-cli' + + # Detect installed version (squad --version may emit a warning before the semver) + $InstalledVersion = '' + if (Get-Command squad -ErrorAction SilentlyContinue) { + $raw = (squad --version 2>&1) | Out-String + $m = [regex]::Match($raw, '[0-9]+\.[0-9]+\.[0-9]+') + if ($m.Success) { $InstalledVersion = $m.Value } + } + + if ($InstalledVersion -eq $SquadCliVersion) { + Write-Ok "squad-cli already at pinned version $SquadCliVersion" + return + } + + if ($InstalledVersion) { + Write-Info "squad-cli $InstalledVersion installed; upgrading to pinned $SquadCliVersion..." + } else { + Write-Info "Installing squad-cli $SquadCliVersion..." + } + + Refresh-SessionPath + if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { + Write-Warn "npm not found -- cannot install squad-cli via npm; run 'npm install -g `"@bradygaster/squad-cli@$SquadCliVersion`"' once Node is available" + return + } + + npm install -g "@bradygaster/squad-cli@$SquadCliVersion" + Assert-LastExit -ToolName "squad-cli" + Write-Ok "squad-cli installed at $SquadCliVersion" +} diff --git a/tests/test_nvm_bootstrap.sh b/tests/test_nvm_bootstrap.sh index fffd1ea..d624cf2 100644 --- a/tests/test_nvm_bootstrap.sh +++ b/tests/test_nvm_bootstrap.sh @@ -73,6 +73,32 @@ else fail "copilot-cli.sh still uses bare binary-exists guard (no version comparison)" fi +# --- squad-cli bootstrap tests --- + +SQUAD_CLI_SCRIPT="${REPO_ROOT}/scripts/linux/tools/squad-cli.sh" + +# T7: squad-cli.sh sources nvm in-script before npm checks +# shellcheck disable=SC2016 +if grep -q '\. "\$NVM_DIR/nvm.sh" --no-use' "$SQUAD_CLI_SCRIPT" && grep -q 'nvm use default 2>/dev/null || true' "$SQUAD_CLI_SCRIPT"; then + pass "squad-cli.sh sources nvm and activates default alias in-script" +else + fail "squad-cli.sh does not source nvm/default alias before npm checks" +fi + +# T8: squad-cli.sh reads pinned version from .tool-versions +if grep -q 'read-tool-version.sh.*squad-cli' "$SQUAD_CLI_SCRIPT"; then + pass "squad-cli.sh reads squad-cli version from .tool-versions" +else + fail "squad-cli.sh does not read squad-cli version from .tool-versions" +fi + +# T9: squad-cli.sh performs version-aware idempotency check +if grep -q 'INSTALLED_VERSION' "$SQUAD_CLI_SCRIPT" && grep -q 'SQUAD_CLI_VERSION' "$SQUAD_CLI_SCRIPT"; then + pass "squad-cli.sh performs version-aware idempotency check" +else + fail "squad-cli.sh still uses bare binary-exists guard (no version comparison)" +fi + # --- Summary --- echo "" diff --git a/tests/test_windows_setup.ps1 b/tests/test_windows_setup.ps1 index 043e6d0..02cfc41 100644 --- a/tests/test_windows_setup.ps1 +++ b/tests/test_windows_setup.ps1 @@ -2391,6 +2391,65 @@ Test-Scenario "GG-7: Non-zero host exit -- fallback path returned" { } $global:LASTEXITCODE = 0 # v5-H2: reset after native-command contamination +# --------------------------------------------------------------------------- +# Group HH: squad-cli installer (Issue #487) +# --------------------------------------------------------------------------- + +Write-Host "`n========================================================" -ForegroundColor Cyan +Write-Host " Group HH: squad-cli installer (#487)" -ForegroundColor Cyan +Write-Host "========================================================" -ForegroundColor Cyan + +$squadCliToolPath = Join-Path $RepoRoot 'scripts\windows\tools\squad-cli.ps1' +$squadCliToolContent = Get-Content $squadCliToolPath -Raw + +Test-Scenario "HH-1: Install-SquadCli function defined in squad-cli.ps1" { + $found = Select-String -Path $squadCliToolPath -Pattern 'function Install-SquadCli' -Quiet + if (-not $found) { + throw "Install-SquadCli function not found in scripts/windows/tools/squad-cli.ps1" + } +} + +Test-Scenario "HH-2: squad-cli.ps1 reads pinned version via Get-ToolVersion" { + if ($squadCliToolContent -notmatch "Get-ToolVersion.*-Name\s+'squad-cli'") { + throw "squad-cli.ps1 does not call Get-ToolVersion -Name 'squad-cli'" + } +} + +Test-Scenario "HH-3: Install-SquadCli called in setup.ps1 Main() after Install-CopilotCli" { + $setupPath = Join-Path $RepoRoot 'scripts\windows\setup.ps1' + $tokens = $null; $errors = $null + $ast = [System.Management.Automation.Language.Parser]::ParseFile($setupPath, [ref]$tokens, [ref]$errors) + $mainFn = $ast.FindAll({ param($n) $n -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $n.Name -eq 'Main' }, $true) + if ($mainFn.Count -eq 0) { throw "Main function not found in setup.ps1" } + $mainBody = $mainFn[0].Body.Extent.Text + if ($mainBody -notmatch 'Install-SquadCli') { + throw "Install-SquadCli is not called in Main" + } + # Verify ordering: Install-CopilotCli must appear before Install-SquadCli + $copilotIdx = $mainBody.IndexOf('Install-CopilotCli') + $squadCliIdx = $mainBody.IndexOf('Install-SquadCli') + if ($copilotIdx -lt 0) { throw "Install-CopilotCli not found in Main" } + if ($squadCliIdx -le $copilotIdx) { + throw "Install-SquadCli does not appear after Install-CopilotCli in Main" + } +} + +Test-Scenario "HH-4: Missing npm guard uses Write-Warn + return (soft-fail, not exit 1)" { + if ($squadCliToolContent -notmatch 'Write-Warn.*npm not found') { + throw "squad-cli.ps1 does not use Write-Warn for missing npm (soft-fail pattern missing)" + } + # Must NOT contain exit 1 for the npm-absent path + $lines = $squadCliToolContent -split "`n" + $npmAbsentSection = $false + foreach ($line in $lines) { + if ($line -match 'npm not found') { $npmAbsentSection = $true } + if ($npmAbsentSection -and $line -match '\bexit 1\b') { + throw "squad-cli.ps1 uses exit 1 for npm-absent path -- must use return (soft-fail)" + } + if ($npmAbsentSection -and $line -match '\breturn\b') { break } + } +} + # --------------------------------------------------------------------------- # Results # ---------------------------------------------------------------------------