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: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The result event detection in refactor.sh, discovery.sh, and security.sh
was killing the entire process tree 30s after the team lead's session
ended. In team-based workflows, the team lead's "result" event fires
after spawning teammates — while the actual work is still running as
child processes.
Instead of immediately killing on result detection, monitor the claude
process's child processes via pgrep. While teammates are running, reset
the idle counter to prevent false timeouts. Only shut down once all
teammate processes have completed (or the hard timeout fires).
Co-authored-by: Claude Opus 4.6 <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>
The tagline claimed "149 combinations" without context. Users might think
this is a limitation or wonder why 15×10=150. The matrix table shows the
blank cell for local/opencode, but the tagline should clarify this upfront.
Agent: ux-engineer
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Remove verbose fields (dropdowns, use cases, environment, proposed UX) from
all issue templates. Humans just need to say what they want; the refactor
team handles enrichment and triage.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add missing commands to README commands table for consistency with CLI help:
- spawn <cloud> (show available agents)
- spawn list <filter> (filter by agent/cloud name)
- spawn list -a/-c (explicit filters)
- spawn list --clear (clear history)
- spawn last (rerun most recent)
- spawn help (show help)
- spawn version (show version)
Updated descriptions to match CLI help output exactly.
Agent: ux-engineer
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Three reliability improvements:
1. OAuth session cleanup: Verify PID still exists before killing to prevent
accidentally killing unrelated processes if PID is reused by the OS.
Uses kill -0 check before sending SIGTERM.
2. Float arithmetic fallback: Check for python3 availability before using it
for fractional POLL_INTERVAL support. Falls back to integer seconds with
explicit comment about potential early timeout.
3. Exit code preservation: Add clarifying comment about exit code capture
timing in refactor.sh cleanup trap (already correct, now documented).
Agent: code-health
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <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>
Prevents potential code injection if malicious parameters containing
single quotes are passed to _generate_oauth_server_script(). The
function embeds bash variables directly into a Node.js script string
using single-quoted JS strings. Without escaping, a crafted parameter
like "foo'; malicious(); '" could break out of the string context.
While current callers use safe values (randomUUID, tempfile paths,
HTML constants), defense-in-depth requires sanitizing at the point
of use to prevent future regressions if callers change.
Fixes: CWE-94 (Code Injection)
Severity: HIGH
Impact: Remote code execution if attacker controls OAuth state token,
file paths, or HTML content
Agent: security-auditor
Co-authored-by: spawn-bot <bot@openrouter.ai>
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>
Improves codebase reliability by adding critical safety validations:
1. **cleanup_oauth_session**: Added path validation before rm -rf
- Prevents accidental deletion if oauth_dir is empty, /, or /tmp
- Validates path starts with /tmp/ and is not just /tmp itself
- Prevents catastrophic system damage from failed mktemp
2. **_init_oauth_session**: Added mktemp failure detection
- Checks if mktemp -d succeeded before using oauth_dir
- Returns error with actionable message if temp dir creation fails
- Prevents empty oauth_dir from propagating to rm -rf
3. **refactor.sh SPAWN_ISSUE validation**: Strengthened regex
- Changed from ^[0-9]+$ to ^[1-9][0-9]*$
- Prevents SPAWN_ISSUE="0" from creating issue-0 worktrees
- Ensures issue numbers are positive integers (>= 1)
These fixes prevent potential data loss from edge cases in OAuth
cleanup and refactor service issue handling.
Agent: code-health
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
HIGH severity: Three functions used hardcoded /tmp/env_config for uploading
API keys, creating a TOCTOU race condition where attackers on multi-user
systems could create symlinks to exfiltrate OPENROUTER_API_KEY and other
credentials.
Fixed by using unpredictable temp file names with mktemp-derived randomness,
matching the secure pattern in write_remote_file_via_callback().
Affected functions:
- inject_env_vars_with_ssh() (line 1094)
- inject_env_vars_local() (line 1128)
- inject_env_vars_cb() (line 1363)
Agent: security-auditor
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds destroy_server() wrapper functions to OVH and Sprite cloud libraries
to match the standardized function name used by 8 other clouds.
Before:
- OVH used destroy_ovh_instance()
- Sprite had no destroy function
- Cross-cloud scripts couldn't use a uniform destroy_server() call
After:
- OVH: destroy_server() wraps destroy_ovh_instance()
- Sprite: destroy_server() wraps "sprite destroy <name>" CLI command
- Cross-cloud scripts can now call destroy_server() uniformly
This fixes the blocker for PR #1217 which hardcodes destroy_server() calls
that would silently fail for OVH and Sprite users.
Fixes#1178
Agent: ux-engineer
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Fix race condition in cleanup_oauth_session: Kill process group to prevent zombie OAuth server processes
- Add mktemp failure handling in _init_oauth_session: Prevents undefined behavior when /tmp is full or inaccessible
- Add env var name validation in generate_env_config: Prevents shell injection via malformed KEY=value pairs
Agent: code-health
Co-authored-by: test-engineer <agent@spawn.local>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add team lead pre-approval gate: teammates spawn in plan mode and must
get approval before creating any PR (hard gate, not just prompt rules)
- Add diminishing returns rule: default posture is "code is good, shut down"
- Add dedup rule: check for existing open/closed PRs before creating new ones
- Require concrete PR justification (what breaks without this change)
- Add off-limits files list (.github/workflows, .claude/skills, CLAUDE.md)
- Use git pathspec exclusions in refactor.sh to never stage protected files
- Constrain pr-maintainer to only act on approved or feedback PRs
- Reduce refactor cron from every 5 minutes to every 2 hours
Co-authored-by: Claude Opus 4.6 <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>
The README incorrectly showed SPRITE_API_KEY as an environment variable,
but Sprite uses 'sprite login' for authentication (no API key needed).
Changes:
- Remove SPRITE_API_KEY example from non-interactive mode section
- Add clarifying note that Sprite uses 'sprite login'
- Change example command from 'spawn claude sprite' to 'spawn claude hetzner'
since Hetzner actually uses an API token (HCLOUD_TOKEN)
This prevents user confusion when trying to authenticate with Sprite.
Agent: ux-engineer
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Prevent silent failures when cloud API responses don't contain expected
server/instance IDs or IPs. Without these checks, scripts would continue
with empty variables, leading to cryptic failures downstream (e.g., "ssh
root@" or API calls with empty IDs).
Changes:
- fly: Check FLY_MACHINE_ID after extraction, fail fast with clear error
- ovh: Check OVH_INSTANCE_ID after extraction, fail fast with clear error
- hetzner: Check HETZNER_SERVER_ID and HETZNER_SERVER_IP (+ null check for jq)
- digitalocean: Check DO_DROPLET_ID after extraction, fail fast with clear error
Impact: Improves reliability by catching API response parsing failures
immediately rather than propagating empty values to SSH/API calls.
Agent: code-health
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Replace PID-based temp path with cryptographically random generation
to prevent symlink attacks on remote servers.
Severity: MEDIUM
Finding: sprite/lib/common.sh:237 used $$ (PID) for temp file naming,
which is predictable and allows symlink race attacks.
Fix: Use openssl rand or /dev/urandom for 8-byte random suffix,
matching the hardened pattern from PR #1039 for shared/common.sh.
Related: #763 (security batch tracking issue)
Agent: security-auditor
Co-authored-by: spawn-bot <bot@openrouter.ai>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Add a new Troubleshooting section to the README with three subsections:
- Installation issues: bun version checks, manual installation, PATH setup
- Agent launch failures: credential checks, cloud alternatives, dry-run usage
- Getting help: command history, version checks, bug reporting
This helps users quickly resolve common issues without searching through
issues or documentation. Positioned right after usage examples where users
are most likely to encounter problems.
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>
Ubuntu's default .bashrc has an interactive-shell guard that exits
early in non-interactive contexts. When SSH runs a command string
(ssh -t user@host -- "cmd"), the shell is non-interactive, so
env vars appended to .bashrc are never loaded — causing Claude Code
to start without OpenRouter credentials and get rejected.
Fix: write env vars to ~/.spawnrc and have .bashrc/.zshrc source it.
Launch commands source ~/.spawnrc directly, bypassing the guard.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Env vars (OPENROUTER_API_KEY, ANTHROPIC_BASE_URL, etc.) are written to
~/.bashrc by inject_env_vars_* functions, but launch commands only
exported PATH inline — they never sourced .bashrc. This meant Claude
started without API keys.
Previously `source ~/.bashrc` was removed because fnm's eval corrupted
PATH. fnm has been completely removed from the codebase, so it's now
safe to source .bashrc again.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The npm/fnm fallback was causing multiple issues:
- bun installed claude but verification ran `claude --version` which
needs node (bun-installed claude has #!/usr/bin/env node shebang)
- fnm's `eval "$(fnm env)"` corrupts PATH when written to rc files
- fnm installs node in a dir that requires eval to access
Simplified to two methods:
1. curl installer (standalone binary, no runtime needed)
2. bun i -g (installs to ~/.bun/bin/)
Removed: npm method, fnm/nodesource node installers, fnm PATH logic.
Changed verification from `command -v claude && claude --version` to
just `command -v claude` (avoids needing node just to verify).
Also: cleaned up claude_path (removed fnm references), kept stale
.bash_profile cleanup.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Root cause: the launch command did `source ~/.bashrc; source ~/.zshrc; claude`.
The .zshrc contains `eval "$(fnm env)"` which outputs PATH with literal
"$PATH" in quotes instead of expanding it, destroying the entire PATH.
Confirmed via debugging:
- `ssh -t ... 'export PATH=...; which claude'` → works (/root/.bun/bin/claude)
- `ssh -t ... 'export PATH=...; source ~/.zshrc; which claude'` → "command not found"
- `source ~/.zshrc; echo $PATH` → `"/run/user/0/fnm_multishells/...":"$PATH"` (broken)
Fix:
- Remove `source ~/.bashrc` and `source ~/.zshrc` from ALL launch commands
- ssh -t creates a pseudo-terminal, so bash auto-sources .bashrc for env vars
- Explicit PATH export is all we need for finding the claude binary
- Remove fnm eval snippet from _finalize_claude_install (it poisoned rc files)
- Also: clean up stale ~/.bash_profile, fix cloud-init PATH, move node
install after bun attempt
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <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>
After installing Claude Code (via any method), run `claude install --force`
to set up shell integration, then ensure fnm bootstrap is persisted to both
.bashrc and .zshrc so interactive sessions can find node.
Also simplify all launch commands across 9 clouds: instead of hardcoding
PATH entries that may miss fnm, source the rc files which now contain all
the necessary PATH entries from both inject_env_vars and _finalize_claude_install.
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>
Previously offer_github_auth prompted interactively inside inject_env_vars_*,
which runs after the server is already provisioned. This means the user sits
through provisioning before being asked a simple yes/no question.
Split into two phases:
- prompt_github_auth: asks the question early (before create_server)
- offer_github_auth: executes the install later (after server is up),
using the stored answer without re-prompting
Falls back to interactive prompt if prompt_github_auth was never called,
so non-claude scripts and older clouds keep working unchanged.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All cloud claude.sh scripts had inline curl-only installs with no fallback.
When the curl installer failed (transient outage, rate limit), installation
failed with no recovery. Additionally, fnm-installed Node.js was invisible
to subsequent SSH sessions because each SSH command runs in a non-interactive
shell that doesn't source .bashrc/.zshrc.
Changes:
- Migrate 8 cloud scripts to use shared install_claude_code (curl → npm → bun)
- Move _ensure_node_runtime before npm/bun install attempts (not after)
- Add fnm paths to claude_path so node is discoverable across SSH sessions
- Prefix npm/bun install commands with claude_path for PATH visibility
- Update test assertion to match new install_claude_code behavior
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add 5 composable helper functions to shared/common.sh (install_agent,
verify_agent, get_or_prompt_api_key, inject_env_vars_cb, launch_session)
using the same callback pattern as offer_github_auth and
setup_claude_code_config. Refactor all 15 hetzner scripts to use them,
reducing total line count from 868 to 579 (-33%).
Add install_claude_code helper with 3-method fallback (curl → npm → bun)
and per-step error logging. When npm/bun fallback needs node, installs it
via fnm (platform-agnostic) with nodesource as Debian/Ubuntu fallback.
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>