The interactive flows asked users to name their spawn before they had
selected an agent or cloud, which was confusing since they didn't know
what they were naming. Move promptSpawnName() to after agent/cloud
selection and credential preflight so users have full context.
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Export isInteractiveTTY from commands.ts and import in index.ts,
removing the duplicate definition that was missing !! boolean coercion
- Remove unused inject_env_vars_ovh function from ovh/lib/common.sh
(all OVH scripts use spawn_agent which calls _spawn_inject_env_vars)
- Bump CLI version to 0.5.6
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Tests were failing due to code changes that were not reflected in test
assertions:
- env injection uses mktemp paths (/tmp/spawn_env_*) not /tmp/env_config
- Oracle Cloud removal reduced cloud count from 10 to 9 and SSH clouds from 6 to 5
- install.sh clone_cli uses safe canonical path rm (${repo_dir}) not ${dest}/repo
- Fly.io fixture coverage requires api.machines.dev in URL pattern map
- spawn_agent calls get_or_prompt_api_key internally for API key acquisition
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
_multi_creds_validate tests in two files were missing the required
help_url parameter (3rd positional arg), causing env vars intended as
the 4th+ args to be consumed as help_url. This meant unset-on-failure
tests only unset 1 of N vars instead of all N.
inject_env_vars_ssh/local tests expected the old hardcoded path
/tmp/env_config but the code now uses randomized /tmp/spawn_env_*
names (a prior security fix to prevent symlink race conditions).
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The buildDeleteScript function in commands.ts interpolated connection.metadata.zone
and connection.metadata.project directly into a bash script string without validation.
A tampered history file could inject arbitrary shell commands via these fields
(e.g., zone='"; rm -rf /; echo "' would escape the double quotes).
Add validateMetadataValue() to security.ts and call it before interpolating
GCP zone and project values into the delete script.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Add --default to KNOWN_FLAGS so it is recognized even if the `spawn pick`
early-return path is bypassed (e.g. due to Bun kqueue/TTY errors on certain
platforms). Also wrap cmdPick in a try/catch so TTY errors produce a clean
error message instead of an unhandled rejection.
Sync test copies of KNOWN_FLAGS that had drifted: unknown-flags.test.ts was
missing --debug, --headless, --output, --clear, -a, -c, --agent, --cloud;
index-dispatch-routing.test.ts had the same gaps. Fix an incorrect test that
expected --output to be flagged as unknown (it has been a known flag since
--headless/--output were added).
Fixes#1447
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes GitHub CLI authentication on remote VMs by passing local token through to remote installation script. Uses printf '%q' for safe shell escaping to prevent command injection.
- New cli/src/picker.ts: modular picker module with pickToTTY() that renders
an arrow-key UI directly to /dev/tty, works even when stdout is captured by
bash $() subshell substitution and stdin is piped with options.
- New spawn pick subcommand: reads options from stdin as tab-separated lines
(value\tLabel\tHint), shows clack-style picker via /dev/tty, writes selected
value to stdout. Falls back to a numbered list when no TTY is available.
Usage from bash:
zone=$(printf 'us-central1-a\tIowa\nus-east1-b\tVirginia\n' \
| spawn pick --prompt "Select zone" --default "us-central1-a")
- gcp/lib/common.sh: interactive project, zone, and machine-type pickers for
all GCP agent scripts. Each picker respects env var overrides (GCP_PROJECT,
GCP_ZONE, GCP_MACHINE_TYPE) and skips the prompt when already set. Uses
spawn pick for a nice arrow-key UI when available; falls back to
_display_and_select (fzf or numbered list) from shared/common.sh.
- _gcp_machine_type_options(): curated list of 8 popular instance types
- _gcp_zone_options(): 12 curated zones across US / EU / APAC / AU
- _gcp_project_options(): live list via gcloud projects list
- _gcp_pick_{machine_type,zone,project}(): picker wrappers
- _gcp_resolve_project(): now prompts interactively instead of erroring when
no project is configured
- create_server(): now calls pickers before provisioning instead of silently
using defaults
- cli version bump 0.5.2 to 0.5.3
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: add missing text and autocomplete mocks to cmd-interactive tests
17 tests in cmd-interactive.test.ts were failing with
"p.text is not a function" because the @clack/prompts mock didn't
include the text() prompt (added for spawn name input) or
autocomplete() (used for agent selection). Adds both mocks to restore
full test coverage of cmdInteractive.
Agent: code-health
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: update stale version strings in update-check tests
The update-check tests mock "latest" version as 0.3.0, but the current
CLI version is 0.5.2. Since 0.3.0 < 0.5.2, compareVersions returns
false and the auto-update logic never fires, causing 5 tests to fail.
Replace mock version with 99.0.0 to future-proof against further bumps.
Agent: test-engineer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
aider-chat on Python 3.13 fails with `ImportError: cannot import name
'_imaging' from 'PIL'` when an old Pillow version (pre-10.4) is resolved
— those releases have no Python 3.13 binary wheels, so the C extension
is missing at runtime.
Replace `--with 'Pillow>=10.2.0'` (which was silently broken — the `>`
and single quotes get mangled by `printf '%q'` in run_server before the
command reaches the remote machine) with `--upgrade`, which forces all
transitive deps including Pillow to their latest compatible versions.
Also adds a plain-text echo before the install so users see progress
instead of a silent hang during the 2-4 minute install.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: clean up test directories after cmdlist integration tests
The cmdlist-integration.test.ts was creating temporary directories in
beforeEach but never cleaning them up in afterEach, leaving 1,560
test directories in /root (spawn-cmdlist-test-*).
Added rmSync cleanup in afterEach to remove the test directory after
each test run. Bumped CLI version to 0.5.2.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat: add update-team skill for managing agent team services
This skill automates updating and restarting agent team services
(discovery, refactor, security, qa-cycle) with the latest configuration
from setup-agent-team.
Features:
- Reads latest setup-agent-team SKILL.md for best practices
- Identifies all deployed services via wrapper scripts
- Validates wrapper scripts have required env vars and correct paths
- Validates systemd service files for compliance
- Updates wrapper scripts and systemd configs as needed
- Restarts services and verifies health
- Supports --check-only for dry-run mode
- Can target specific services or update all
Usage:
- claude /update-team # Update all services
- claude /update-team discovery # Update specific service
- claude /update-team --check-only # Check without changes
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Oracle Cloud is removed as a supported provider. Each agent now has a
`featured_cloud` field in manifest.json that controls cloud sort order
in the CLI picker — featured clouds appear after credential-detected
clouds but before CLI-installed ones, with a "recommended" hint.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add headless SDK mode for programmatic provisioning (#1181)
Add --headless and --output json flags to enable non-interactive
provisioning with structured JSON output on stdout.
- --headless: disables prompts, OAuth browser flows, and SSH sessions
- --output json: outputs structured SpawnResult JSON on stdout
- Exit code contract: 0=success, 1=execution, 2=download, 3=validation
- Upfront credential validation (fail-fast before provisioning)
- Script stdout piped to stderr to keep JSON output clean
- SPAWN_HEADLESS=1 env var set for bash scripts
Closes#1181
-- refactor/ux-engineer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: restore critical test mocks for fly SSH readiness checks
The PR inadvertently removed essential mock logic:
- fly ssh mock no longer responded to 'echo ok' commands
- timeout/gtimeout mocks were removed (needed for SSH polling)
- python3 mock was removed (needed for JSON parsing)
- /tmp/spawn_* cleanup was removed from test teardown
This caused 29 fly/* test failures with 'SSH connectivity failed'.
Restores the exact mock implementations from main branch.
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* security: fix path traversal risk in SPAWN_HOME validation
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: add missing join import and update tests for SPAWN_HOME security validation
Addresses security review feedback on PR #1402:
- Add missing 'join' import to cli-version-and-dispatch.test.ts
- Update all test files to use homedir() instead of tmpdir() for SPAWN_HOME
The security fix in history.ts now enforces that SPAWN_HOME must be within
the user's home directory. All tests have been updated to use home-based
test directories instead of /tmp paths.
Changes:
- cli/src/__tests__/cli-version-and-dispatch.test.ts: Add join to path imports
- All test files: Replace tmpdir() with homedir() and /tmp/spawn- with /.spawn-test-
Tests:
- bun test history.test.ts: ✅ 69 pass
- bun test clear-history.test.ts: ✅ 27 pass
- bun test cli-version-and-dispatch.test.ts: ✅ 62 pass
- bun test list-table-rendering.test.ts: ✅ 8 pass
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* security: fix incomplete command injection detection in prompt validation
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: refine command injection patterns to avoid false positives
Addresses changes requested in PR review:
- Updated && and || patterns to only match when followed by common shell commands
- Added context-aware check to exclude programming expressions like "a > b && c < d"
- Maintains security by still catching shell command chaining attempts
- All security tests pass including new edge case tests
Fixes false positive rejection of legitimate programming expressions
while still detecting shell injection attempts from issue #1400.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixed readonly property assignments in commands-compact-list.test.ts by using the existing setTerminalWidth() helper instead of direct Object.defineProperty() calls. This makes the code more maintainable and consistent.
Updated oracle-provider-patterns.test.ts to check for install_claude_code function instead of the outdated claude.ai/install.sh reference, matching the current oracle/claude.sh implementation.
Changes:
- Replaced 4 inline Object.defineProperty() calls with setTerminalWidth() helper
- Updated oracle claude.sh test to check for install_claude_code instead of claude.ai/install.sh
- All compact list tests passing (20/20)
Fixes#1366
Agent: complexity-hunter
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit addresses issue #1373 by improving the test sandbox to prevent
accidental writes to the real user environment.
Changes:
1. Enhanced preload.ts:
- Added .ssh directory creation in sandboxed HOME
- Expanded documentation explaining sandboxing strategy
- Clarified safety guarantees for filesystem operations
2. Added sandbox-verification.test.ts:
- Comprehensive test suite verifying sandbox isolation
- Tests environment variable sandboxing (HOME, XDG_*)
- Tests pre-created directories (.config, .ssh, .claude, .cache)
- Tests filesystem isolation (writes stay in temp directory)
- Tests subprocess isolation (bash inherits sandboxed env)
- Tests safety guarantees (no exposure of /root paths)
The existing preload.ts already prevented writes to real home directory
by redirecting process.env.HOME and XDG variables to temp directories.
This commit strengthens that sandboxing with the .ssh directory and adds
comprehensive verification tests to ensure the sandbox works correctly.
Fixes#1373
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Implements spawn name feature (#1372) to improve UX:
- Add optional spawn name prompt in interactive mode
- Pass spawn name via SPAWN_NAME env var to shell scripts
- Shell scripts use spawn name as default for resource names
- Store spawn name in history for future reference
- Bump CLI version to 0.4.0
The spawn name is prompted before agent/cloud selection and
automatically used as the default for platform-specific resource
names (server name on Hetzner, sprite name on Sprite, etc.).
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Applied the test fixes from PR #1358:
1. Fixed process.stdout.columns mutation in commands-compact-list.test.ts
- Replaced direct property assignments with Object.defineProperty
- Created setColumns() helper function for strict mode compatibility
- Removed duplicate setTerminalWidth() function
2. Updated oracle-provider-patterns.test.ts assertion
- Changed from checking for "claude.ai/install.sh" URL
- Now checks for "install_claude_code" function name
- Matches current oracle/claude.sh implementation
Note: Shell scripts (aws/gptme.sh, gcp/gptme.sh) already have
set -eo pipefail from previous commits - no changes needed.
Fixes#1365
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace select prompts with autocomplete for improved UX when
choosing agents and clouds. Users can now type to filter the list,
significantly reducing time to find desired options in long lists.
- Replace p.select with p.autocomplete for agent selection
- Replace p.select with p.autocomplete for cloud selection
- Add "type to filter" messaging and placeholder text
- Update CLI version 0.3.2 → 0.3.3
Fixes#1367
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Add input validation for SSH connection parameters (IP, username, server_name)
and server identifiers used in delete operations. This prevents command injection
attacks if ~/.spawn/history.json is corrupted or tampered with.
Changes:
- Add validateConnectionIP() - validates IPv4/IPv6 addresses and sentinels
- Add validateUsername() - validates Unix username format
- Add validateServerIdentifier() - validates server names/IDs
- Update cmdConnect() to validate all connection params before use
- Update buildDeleteScript() to validate server IDs before interpolation
- Update mergeLastConnection() to validate data from bash scripts
- Add comprehensive test coverage for all validation functions
- Bump CLI version to 0.3.3 (security patch)
Security impact:
- Prevents HIGH severity command injection via history.ip/user (issue #1381)
- Prevents MEDIUM severity command injection via server_id (issue #1380)
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes path traversal vulnerability where unvalidated filenames from
GitHub API could write files outside intended directory.
Attack vector: MITM attack or DNS hijacking could inject filenames
like "../../../../../../tmp/evil.ts" to write arbitrary files.
Fix: Validate filenames before download - block "..", "/", and "\\"
to ensure files are written only within ${dest}/cli/src/
Severity: HIGH/CRITICAL
Affects: All users running installer via curl|bash
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#1376 - HIGH severity path traversal in CLI installer
Fixes#1377 - MEDIUM severity unquoted variable in hetzner token extraction
Changes:
- cli/install.sh: Replace string prefix matching with canonicalized path
comparison to prevent path traversal in rm -rf cleanup. The previous
check could be bypassed with sequences like "/tmp/../../home/user".
- hetzner/lib/common.sh: Quote xargs placeholder variable to prevent
unexpected behavior if hcloud context name contains shell metacharacters.
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: auto-run gcloud auth login on expired GCP tokens
Instead of telling users to run `gcloud auth login` manually, just
run it automatically when auth check fails or instance creation hits
a reauthentication error, then retry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: prioritize clouds with CLI installed + hcloud CLI integration
When selecting a cloud provider, clouds are now sorted in 3 tiers:
1. Credentials detected (env vars set) — top priority
2. CLI installed (e.g., gcloud, hcloud, aws) — middle priority
3. Neither — default order
Also adds hcloud CLI-first support for Hetzner operations (server
create/delete/list, SSH key management, auth) with automatic fallback
to the existing REST API when hcloud is not available.
Closes#1370
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: rename aws-lightsail to aws across the project
Simplifies the cloud key from "aws-lightsail" to "aws" — AWS should
have a single entry regardless of the underlying service used.
Renames the directory, updates manifest.json matrix keys, CLI map,
test fixtures, README, and all agent scripts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extracted EXIT_CODE_GUIDANCE and SIGNAL_GUIDANCE from commands.ts into a
new guidance-data.ts module. This reduces commands.ts complexity by 100+ lines,
making error handling logic more maintainable and focused.
Changes:
- New file: cli/src/guidance-data.ts (116 lines) with error/signal guidance data
- Refactored: commands.ts now 100 lines shorter, imports guidance data
- Improved: Exit code 1 handling to avoid circular dependency with credentialHints
The extracted module is a pure data file focused on error messages and guidance,
separate from the command execution logic.
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Redirects HOME and XDG dirs to a temp directory before tests run,
preventing any test from accidentally writing to the real user's
home directory (e.g. ~/.claude/settings.json, ~/.zshrc).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Fixes#1197 by checking for saved credentials in ~/.config/spawn/{cloud}.json files.
This prevents false-positive credential warnings when cloud-specific credentials are saved via config files (as done by cloud setup scripts).
Advantages over PR #1288:
- Works with all credential key names (not just api_key/token)
- Handles multi-credential clouds correctly (OVH, Contabo)
- Generic approach checks for any non-empty credential value
Security review: ✅ No vulnerabilities detected
- Path traversal protected
- Safe JSON parsing
- No information disclosure
- Correct multi-cloud credential logic
UX improvements:
- Replace outdated cloud references (vultr/linode) with existing clouds (ovh/gcp) in help examples
- Add missing --debug flag to README commands table
- Ensure all documented examples reference clouds that exist in the matrix
These changes prevent user confusion when following examples in help text
and documentation.
Agent: ux-engineer
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: add error logging to empty catch blocks in test helpers
Previously, test helper functions had 14 empty catch blocks that
silently swallowed all errors during cleanup operations (reading and
deleting temporary stderr files).
This change adds error logging that:
- Allows expected errors (ENOENT for missing files, exit code 1 for cat)
- Logs unexpected errors to console for debugging
This improves test reliability by surfacing unexpected filesystem or
permission errors that could indicate real problems, while still
allowing the intended best-effort cleanup behavior.
Fixes: Empty catch blocks in 6 test files
Impact: Better test debugging and error visibility
Agent: code-health
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: improve error handling in Python fallback and directory deletion
1. Python arithmetic fallback (shared/common.sh:713):
- Changed from: || echo "$((elapsed + 1))"
- Changed to: explicit if/else with error detection
- Impact: Python errors are now properly caught instead of masked by ||
2. Unvalidated directory deletion (cli/install.sh:142):
- Added path validation before rm -rf
- Checks: path is within dest directory AND directory exists
- Impact: Prevents accidental deletion if variables are malformed
Both changes improve safety and error visibility without breaking
existing functionality.
Agent: code-health
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes 256 failing tests that spawn bun subprocesses. These tests were
failing because bun was not in the child process PATH. Ensures all
CLI test helpers pass PATH with $HOME/.bun/bin included.
Also corrects two gptme.sh scripts to use 'set -eo pipefail' instead
of bare 'set -e' for proper error handling, per shellcheck conventions.
Changes:
- 7 CLI test files: add PATH=$HOME/.bun/bin to execSync/spawnSync env
- 2 shell scripts: use set -eo pipefail for proper error handling
Results: 256 tests now passing, 0 failures in subprocess CLI tests.
Co-authored-by: test-engineer <agent@spawn.local>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace empty catch blocks with explicit error parameters for better debugging
and potential future error logging. Changes include:
- Add error parameter to all catch blocks (currently 7 instances)
- Enable conditional debug logging for non-fatal history write failures
- Maintain backward compatibility - no behavior changes
- Improve code maintainability and debugging capability
This addresses code health issue where errors were silently swallowed without
any reference, making debugging difficult.
Agent: code-health
Co-authored-by: test-engineer <agent@spawn.local>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Refactored two high-complexity functions to improve maintainability:
1. shared/common.sh: Extract install_claude_code() into 5 focused helpers:
- _finalize_claude_install: Setup shell integration
- _verify_claude_installed: Check if installation succeeded
- _install_via_curl: Curl installer method
- _ensure_nodejs_runtime: Node.js runtime setup
- _install_via_bun: Bun installer method
Main function now reads as a clear sequence of steps.
2. cli/src/commands.ts: Simplify credential checking in printQuickStart:
- Extract checkAllCredentialsReady() for clarity
- Extract printAuthVariableStatus() to handle auth var display
- Extract buildCloudCommandHint() for cloud hint formatting
Reduces complexity and improves readability.
All 80 tests pass. No functional changes.
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The tests were failing because process.stdout.columns is a readonly property in Bun's test environment. Changed all direct assignments to use Object.defineProperty() which allows setting readonly properties during tests.
Changes:
- Added setTerminalWidth() helper in commands-compact-list.test.ts
- Updated all test cases to use Object.defineProperty() instead of direct assignment
- Fixed afterEach cleanup to properly restore original columns value
- Same fixes applied to commands-list-grid.test.ts
This ensures tests pass in Bun runtime while maintaining the same test coverage.
Agent: ux-engineer
Co-authored-by: test-engineer <agent@spawn.local>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Wire up connection tracking across all 10 clouds so users can reconnect
to and delete previously spawned servers via `spawn list` and `spawn delete`.
Phase 1 - Connection tracking:
- Extend save_vm_connection() with cloud and metadata params
- Add save_vm_connection to create_server() in all cloud libs
- Extend VMConnection with cloud, deleted, deleted_at, metadata fields
Phase 2 - Delete via interactive picker:
- Add "Delete this server" option to spawn list picker
- Build delete scripts that reuse each cloud's destroy_server()
- Confirmation UX with spinner feedback
- Soft-delete marking in history (deleted records show [deleted])
Phase 3 - Standalone delete command:
- spawn delete (aliases: rm, destroy) with interactive picker
- Filter support: spawn delete -a <agent> -c <cloud>
Also improves reconnect hints for Fly (fly ssh console) and
Daytona (daytona ssh) connections.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Stop writing env vars to ~/.profile and ~/.bash_profile — only write to
.bashrc and .zshrc. The .profile approach caused issues because login
shells source it inconsistently across distros, and creating .bash_profile
makes bash -l skip .profile entirely.
Replace `bash -lc claude` launch commands with explicit PATH export +
source pattern across all cloud providers. This ensures claude is found
regardless of shell initialization quirks.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On Ubuntu/Debian, ~/.bash_profile doesn't exist by default. When bash
starts as a login shell (bash -l), it sources the FIRST file it finds
from: ~/.bash_profile, ~/.bash_login, ~/.profile. Since only ~/.profile
exists, that's what gets sourced — and ~/.profile sets up the standard
PATH (/usr/bin, /bin, etc.) and sources ~/.bashrc.
Our inject_env_vars_* functions and _finalize_claude_install were writing
to ~/.bash_profile and ~/.zprofile (either via touch+append or via
for-loop over all rc files). Creating ~/.bash_profile caused bash -l to
source it INSTEAD of ~/.profile, completely losing the standard PATH
setup. After deployment, even basic commands like `ls` would fail.
Fix: Only write to ~/.profile, ~/.bashrc, ~/.zshrc across all clouds
(shared, fly, sprite). These are the standard files that work correctly
on all Linux distros without breaking the shell initialization chain.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: bash -l sources the FIRST of ~/.bash_profile, ~/.bash_login,
~/.profile. If ~/.bash_profile exists (e.g. from cloud-init), ~/.profile
is never read and our claude PATH exports are invisible.
Additionally, .bashrc has a non-interactive guard that skips exports when
sourced from non-interactive shells like `ssh host "cmd"` or `bash -lc`.
Fix: write env config and PATH entries to ALL shell startup files:
~/.profile, ~/.bash_profile, ~/.bashrc, ~/.zshrc, ~/.zprofile.
This ensures both login and interactive shells on any platform find claude.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two fixes:
1. Swap fallback order from curl → npm → bun to curl → bun → npm.
Bun is faster and typically pre-installed. Use `bun i -g`.
2. Fix "claude: command not found" at launch. The default .bashrc has
a non-interactive guard (`case $- in *i*) ;; *) return;; esac`)
that skips PATH exports when sourced from SSH command strings.
Fix: write env config to ~/.profile (always sourced by login shells)
in addition to .bashrc/.zshrc, and launch with `bash -lc claude`
which starts a login shell that sources ~/.profile.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restyle the OAuth success/error pages to match openrouter.ai's minimal
aesthetic: system-ui font, clean white/near-black backgrounds, muted
secondary text, and proper light/dark mode via prefers-color-scheme.
- Light mode: white background (#fff), dark text (#090a0b)
- Dark mode: near-black background (#090a0b), light text (#fafafa)
- Use simple checkmark/cross icons instead of colored headings for status
- Add viewport meta tag for mobile
- Update tests to match new markup
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: wire shared/github-auth.sh into all agent flows
Add offer_github_auth() to shared/common.sh and call it from the
inject_env_vars_* functions so all agent flows automatically offer
GitHub CLI setup after env var injection — no per-script changes needed.
Changes:
- shared/common.sh: add offer_github_auth() function, call it from
inject_env_vars_ssh() and inject_env_vars_local()
- sprite/lib/common.sh: call offer_github_auth() from
inject_env_vars_sprite()
- OVH is covered automatically (inject_env_vars_ovh delegates to
inject_env_vars_ssh)
Behavior:
- Prompts "Set up GitHub CLI (gh) on this machine? (y/N):"
- Defaults to No (non-blocking for users who don't need it)
- Skippable via SPAWN_SKIP_GITHUB_AUTH=1 env var for CI/automation
- Uses safe_read for curl|bash compatibility
- Downloads and runs shared/github-auth.sh on the remote VM
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: add shared agent setup helpers, deduplicate hetzner scripts (#1236)
Add 5 composable helper functions to shared/common.sh (install_agent,
verify_agent, get_or_prompt_api_key, inject_env_vars_cb, launch_session)
that use the same callback pattern as offer_github_auth and
setup_claude_code_config. Refactor all 15 hetzner agent scripts to use
them, reducing total line count from 868 to 579 (-33%).
Phase 1 of multi-phase rollout — remaining clouds to follow.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Tests calling loadManifest(true) with mocked fetch were writing test
manifests (only 2 agents) to the real ~/.cache/spawn/manifest.json.
This caused `spawn` to show only "Claude Code" and "Aider" instead
of all 15 agents.
Root cause: CACHE_DIR/CACHE_FILE were computed once at import time,
so tests setting XDG_CACHE_HOME in beforeEach() had no effect.
Fix:
- Make CACHE_DIR/CACHE_FILE dynamic via getter functions so test
isolation via XDG_CACHE_HOME actually works
- Skip disk writes in test environments unless XDG_CACHE_HOME is
explicitly set (tests that need disk cache use setupTestEnvironment
which sets XDG_CACHE_HOME to a temp dir)
- Bump CLI version to 0.2.88
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Fixes#1180
When running `spawn <agent>` (e.g., `spawn claude`), now shows an interactive
cloud picker instead of requiring the full command or showing agent info.
- Add cmdAgentInteractive() function for agent-first cloud selection
- Route `spawn <agent>` to interactive picker when in TTY mode
- Fall back to agent info display in non-interactive contexts
- Update help text to reflect new interactive behavior
- Version bump 0.2.83 → 0.2.84
Agent: ux-engineer
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixed Hetzner installation issue where curl to claude.ai/install.sh
was returning 403 errors. Added fallback to use bun (already installed
by cloud-init) to install Claude Code.
Also added --debug flag to enable verbose bash output (set -x) for
easier troubleshooting.
Changes:
- hetzner/claude.sh: Use bun fallback installation method
- CLI: Added --debug flag support (v0.2.86)
- shared/common.sh: Enable set -x when SPAWN_DEBUG=1
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Extract helper functions to reduce nesting and duplication:
1. cmdConnect (54 → 28 lines): Extract runInteractiveCommand() helper to
eliminate duplicate spawn/Promise handling for Sprite and SSH connections
2. interactiveListPicker (48 → 21 lines): Extract handleRecordAction() helper
to reduce nesting in reconnect/rerun logic
3. setup_claude_code_config (46 → 40 lines): Extract _generate_claude_code_settings()
and _generate_claude_code_state() helpers to clarify JSON generation and
make the main function focus on orchestration
All changes preserve existing behavior and pass existing tests.
Agent: complexity-hunter
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
All 40 tests in agent-config-setup.test.ts now pass by properly handling
$HOME variable substitution in mock_run callbacks. Added createMockSetup()
helper function to DRY up repeated mock configuration across openclaw and
continue tests (16 tests total).
Changes:
- Fix mock_run() to replace $HOME before evaluating commands
- Add createMockSetup(tempDir, configDir) helper to reduce code duplication
- Update all setup_openclaw_config and setup_continue_config tests to use helper
- Ensures /tmp/spawn_config_* temp files are redirected to temp test directory
Agent: test-engineer
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Reduce from 41 cloud providers to 10 (9 + local) curated for launch:
- local (free), oracle (free tier), hetzner (~€3.29/mo), ovh (~€3.50/mo),
fly (free tier), aws-lightsail ($3.50/mo), daytona (pay-per-second),
digitalocean ($4/mo), gcp ($7.11/mo), sprite (Fly.io VMs)
Changes:
- Remove 30 cloud directories, test fixtures, and provider-specific tests
- Slim manifest.json from 600 to 150 matrix entries, sorted by price
- Update CLAUDE.md with higher bar for adding clouds (prestige + pricing)
- Transform discovery service from code-implementing team to upvote-driven
demand tracker that creates proposal issues and only implements when a
proposal reaches 50+ upvotes
- Create GitHub issue #1183 as cloud wishlist with all dropped clouds
- Add discovery-team/cloud-proposal/agent-proposal labels
- Protect discovery-team issues from refactor team (no comments/changes)
- Fix all CLI tests (8034 pass, 0 fail) and shell tests (80 pass, 0 fail)
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add VM reconnect functionality to spawn list (#1144)
Implements ability to reconnect to previously spawned VMs instead of
always creating new instances. Changes include:
- Add VMConnection interface to track IP, user, and server metadata
- Add save_vm_connection() bash function for scripts to persist connection info
- Modify spawn list to show connection status and offer reconnect option
- Support both SSH (cloud providers) and sprite console reconnection
- Update digitalocean/claude.sh and sprite/claude.sh as reference implementations
Fixes#1144
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* improve: add helpful error message when VM reconnect fails
Show user-friendly message suggesting to spawn a new VM if
reconnection fails, rather than just showing raw SSH error.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: spawn-refactor-bot <refactor@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>