Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/e2e-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
'

Expand Down Expand Up @@ -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"
'

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions scripts/linux/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
48 changes: 48 additions & 0 deletions scripts/linux/tools/squad-cli.sh
Original file line number Diff line number Diff line change
@@ -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}"
2 changes: 2 additions & 0 deletions scripts/windows/setup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -60,6 +61,7 @@ function Main {
Install-Vim
Install-Psmux
Install-CopilotCli
Install-SquadCli
Install-Dotfiles
Write-PowerShellProfile
Install-GitHook
Expand Down
44 changes: 44 additions & 0 deletions scripts/windows/tools/squad-cli.ps1
Original file line number Diff line number Diff line change
@@ -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"
}
26 changes: 26 additions & 0 deletions tests/test_nvm_bootstrap.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down
59 changes: 59 additions & 0 deletions tests/test_windows_setup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down
Loading