Commit graph

551 commits

Author SHA1 Message Date
A
0f3cb8b2eb
docs(tests): add missing test entries to __tests__/README (#2949)
Two test files (do-min-size.test.ts, docker-cloudinit-skip.test.ts) existed
on disk but were not documented in the README. Add entries for both.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 15:51:43 +07:00
A
e9cbab5b7f
fix(sprite): add retry for list failures, increase timeout, refresh auth on expiry (#2936)
Three fixes for Sprite E2E failures in long-running batches (73+ min):

1. Retry `_sprite_provision_verify`: list failures now retry 3x with
   exponential backoff (5s, 10s, 20s) instead of failing immediately.
   Fixes kilocode batch 6 "Could not list Sprite instances" errors.

2. Increase `CREATE_TIMEOUT_SECS` default from 300s to 600s and add
   `Client.Timeout`, `request canceled`, and `authentication failed`
   to the transient error retry pattern in `spriteRetry`. Also uses
   linear backoff (3s * attempt) instead of fixed 3s delay.
   Fixes hermes batch 7 HTTP timeout errors.

3. Add `_sprite_refresh_auth` + `cloud_refresh_auth` interface. The
   E2E orchestrator calls `cloud_refresh_auth` before each provisioning
   batch. For Sprite, this re-validates the token via `sprite org list`
   and attempts `sprite auth refresh` if expired.
   Fixes junie batch 8 "authentication failed" errors.

Fixes #2934

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 21:47:58 -07:00
A
50319e0d39
fix(hetzner): clean up orphaned primary IPs before provisioning to avoid quota exceeded (#2935)
Hetzner E2E runs fail with `resource_limit_exceeded` when stale primary
IPs from previous test runs consume the account quota. This adds proactive
cleanup at two levels:

1. E2E shell driver: `_hetzner_cleanup_orphaned_ips()` deletes unattached
   primary IPs during pre-batch stale cleanup, freeing quota before any
   new servers are provisioned.

2. TypeScript CLI: `hetzner/main.ts` calls `cleanupOrphanedPrimaryIps()`
   before `createServer()` in headless/non-interactive mode, ensuring
   each agent provisioning attempt starts with a clean IP quota.

The existing reactive cleanup (retry after failure) in `hetzner.ts`
remains as a fallback.

Fixes #2933

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 11:20:30 +07:00
Ahmed Abushagur
3b150eabd8
fix: skip cloud-init wait in Hetzner Docker mode (#2924)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
Hetzner's waitForReady() was missing the useDocker check that GCP
already has. Non-minimal agents (openclaw, codex) with --beta docker
waited 5 minutes for a cloud-init marker that never appears on Docker
CE app images.

Adds useDocker to the condition and a source-level regression test
verifying both Hetzner and GCP include the check.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:36:37 -07:00
Ahmed Abushagur
659fd1c6da
fix: use POSIX normalize for remote Linux paths in validateRemotePath (#2929)
node:path.normalize() is platform-dependent — on Windows it converts
forward slashes to backslashes, which then fail the character allowlist
regex. Remote paths are always Linux paths regardless of the client OS.

Switch to node:path/posix so normalization always uses forward slashes.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 19:34:49 -07:00
Ahmed Abushagur
56f7840f0c
fix: fail fast when GCP delete is missing project metadata (#2925)
When history metadata lacks a project ID, spawn delete silently fell
back to the gcloud default project, attempting deletion in the wrong
project (404) while the instance kept running and billing.

Now fails fast with a clear error and link to GCP Console. Also adds
a defensive check in destroyInstance() to reject empty project.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-24 08:42:47 +07:00
Ahmed Abushagur
2f4fef049a
fix: enforce minimum droplet size for any undersized selection (#2931)
The min-size check only triggered when the exact default slug was
selected (s-2vcpu-2gb). Users who chose s-1vcpu-1gb or s-1vcpu-2gb
bypassed the check and got OOM crashes on openclaw.

Now parses RAM from the DO slug and compares GB values, so any size
below the agent's minimum gets upgraded.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-24 07:34:44 +07:00
Ahmed Abushagur
42df6f753a
fix: prevent uninstall from truncating RC files with missing end marker (#2927)
If the end marker (# <<< spawn <<<) is missing from .bashrc/.zshrc,
cleanRcFile dropped all content after the start marker. Now detects
unclosed blocks and skips the file with a warning instead of writing
a truncated version.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-24 06:54:10 +07:00
Ahmed Abushagur
9651e029df
fix: handle missing ssh-keygen in getSshFingerprint (#2926)
getSshFingerprint called Bun.spawnSync without error handling, crashing
the CLI if ssh-keygen is not in PATH. Wrapped with unwrapOr(tryCatch())
to return empty string on failure, matching getKeyType's pattern.

Also added empty fingerprint handling to Hetzner SSH key registration
(matching DigitalOcean's existing pattern) to skip keys that can't be
fingerprinted instead of attempting re-registration.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-24 06:50:45 +07:00
Ahmed Abushagur
fd2d661e27
fix: validate manifest fields are plain objects, not just truthy (#2921)
* fix: validate manifest fields are plain objects, not just truthy

isValidManifest used !!data.agents/clouds/matrix which accepts strings,
numbers, and arrays. Downstream Object.keys() then silently returns
character indices or array indices instead of real agent/cloud names.
Replace with isPlainObject() checks to reject non-object values.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: add validation tests for non-object manifest fields

Tests that loadManifest rejects manifests where agents/clouds/matrix
are strings, arrays, or numbers instead of plain objects.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-24 06:48:54 +07:00
Ahmed Abushagur
472b315762
fix: prevent permanent history lock when PID file write fails (#2928)
Two bugs in acquireLock:
1. PID write failure was ignored — process returned success but left a
   lock dir without a PID file. If it crashed, no other process could
   detect the lock as stale, making it permanent.
2. Lock dirs without PID files were not treated as stale — other
   processes waited until timeout instead of cleaning up immediately.

Fix: retry on PID write failure (clean up dir first), and treat
lock dirs without PID files as broken/stale (force remove).

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 06:47:10 +07:00
Ahmed Abushagur
6a6ca87969
fix: add sudo to tarball mirror commands for non-root SSH users (#2922)
* fix: add sudo to tarball mirror commands for non-root SSH users

The mirror step copies files from /root/ to $HOME/ for non-root users
(e.g. ubuntu on AWS Lightsail), but cp and chown ran without sudo.
A non-root user can't read /root/ or chown root-owned files, so the
mirror silently failed (errors suppressed by 2>/dev/null || true).

Adds sudo to cp/chown in both mirror blocks (tryTarballInstall and
uploadAndExtractTarball) and removes error suppression so failures
propagate to the caller.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: verify sudo in tarball mirror commands for both install paths

Adds tests for tryTarballInstall and uploadAndExtractTarball that assert:
- cp and chown use sudo (needed to read /root/ as non-root user)
- error suppression (2>/dev/null || true) is not present

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 05:47:39 +07:00
A
18b1a5f50f
fix(install): force IPv4 DNS for npm installs and add junie binary verify (#2920)
* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* chore: update agent GitHub star counts

* fix(install): force IPv4 DNS for npm installs and add junie binary verify

On Sprite VMs (and potentially other clouds with flaky IPv6 routing), npm
install of packages with native-binary postinstall scripts (kilocode, junie)
fails with i/o timeout when connecting to the npm registry over IPv6.

Changes:
- Add NODE_OPTIONS=--dns-result-order=ipv4first to NPM_PREFIX_SETUP so all
  npm installs prefer IPv4, preventing the IPv6 timeout on first attempt
- Add cd ~ before postinstall re-run in KILOCODE_BINARY_VERIFY to avoid
  "current working directory was deleted" errors in bun/node on retry
- Add JUNIE_BINARY_VERIFY snippet (analogous to kilocode) that detects and
  recovers from a failed junie postinstall by re-running it from $HOME
- Apply JUNIE_BINARY_VERIFY to the junie install command

Fixes sprite kilocode and junie failures seen in E2E run 2026-03-23.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 05:13:12 +07:00
A
e0db833307
fix(update-check): redirect install script stdout to stderr in --output json mode (#2919)
When --output json is requested, the auto-update install script was
running with stdio: "inherit", causing [spawn] install messages to
pollute stdout before the JSON result, breaking JSON consumers.

Fix:
- Pre-scan process.argv for --output json before checkForUpdates()
  is called in index.ts (formal flag parsing happens later at line 944)
- Pass jsonOutput flag through checkForUpdates() -> performAutoUpdate()
- When jsonOutput=true, use stdio: ["pipe", stderr, stderr] for the
  install script execution so all output goes to stderr only
- Set SPAWN_CLI_UPDATED=1 env var on re-exec so JSON consumers can
  detect the update via cli_updated: true in SpawnResult
- Add cli_updated?: boolean to SpawnResult interface in commands/run.ts
- Add tests covering both json and non-json stdio behavior

Fixes #2918

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-24 03:18:50 +07:00
A
f38ae693de
fix: set SPAWN_NON_INTERACTIVE in headless mode to prevent prompt hangs (#2916)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
Headless mode set SPAWN_HEADLESS and SPAWN_MODE but not
SPAWN_NON_INTERACTIVE, which all cloud modules check before prompting.
This caused GCP (and potentially other clouds) to prompt for project
confirmation when stdin was closed, resulting in a fatal error.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 01:22:47 +07:00
A
a959a6db83
fix(types): remove as type assertions from test mocks (#2913)
Add missing fields (signalCode, resourceUsage, pid, killed) to
Bun.spawnSync and Bun.spawn mock return values so they satisfy the
full return types without needing `as` casts or biome-ignore comments.

Agent: style-reviewer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-24 00:24:49 +07:00
A
69a0d476a0
test: remove duplicate and theatrical tests (#2912)
Remove 8 tests that checked constant equality (DEFAULT_DROPLET_SIZE,
DEFAULT_DO_REGION, DEFAULT_MACHINE_TYPE, DEFAULT_ZONE, DEFAULT_SERVER_TYPE,
DEFAULT_LOCATION) across digitalocean/gcp/hetzner cov files — these tests
just hardcode the same string twice and break if the default is changed for
a valid reason.

Also remove 2 sleep() tests from ssh-cov.test.ts: sleep() is a trivial
setTimeout wrapper with no logic, and the timing test added 50ms of real
wall time per run.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-24 00:22:49 +07:00
A
0e17461fcd
test: remove duplicate cmdFix tests from cmd-fix-cov.test.ts (#2910)
Three tests in the `cmdFix (additional coverage)` describe block were
exact duplicates of tests already in cmd-fix.test.ts:

- "fixes directly when only one server" = "directly fixes when only one active server"
- "finds record by name when spawnId matches name" = "fixes by spawn name"
- "shows no active spawns when history is empty" = "shows message when no active spawns"

Removed the duplicate describe block and its now-unused imports.
Unique fixSpawn coverage (security validation, manifest failure, label
fallbacks, success message) is preserved.

Agent: pr-maintainer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-23 21:35:44 +07:00
A
f8e23317c9
fix(cli): fix openclaw DO size and kilocode CWD install failures (#2909)
- digitalocean: change openclaw min size from s-2vcpu-4gb-intel to
  s-2vcpu-4gb (intel variant no longer available in nyc3)
- agent-setup: add cd "$HOME" before kilocode npm install to prevent
  postinstall failure when CWD is deleted during npm global install
- bump version to 0.25.19

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 20:37:48 +07:00
A
59dea5fc09
refactor: remove dead code and stale references (#2908)
- remove `export` from `LocalTarball` interface in `shared/agent-tarball.ts`
  — the type is only used internally as the return type of `downloadTarballLocally`;
  it was never imported from outside the module.

- remove `getTerminalWidth` re-export from `commands/index.ts`
  — `getTerminalWidth` is only called inside `commands/info.ts` itself;
  it was re-exported through the barrel but never imported from there by any consumer or test.

bump CLI version patch: 0.25.18 → 0.25.19

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 19:51:41 +07:00
A
f296544c1c
fix(cli): bump version to 0.25.18 for security fix in #2904 (#2906)
Commit 97b6424 (fix(security): add cmd validation to Sprite
runSprite() and runSpriteSilent()) changed production CLI code without
a corresponding version bump. The CLI has auto-update — without this
bump users won't receive the null-byte injection guard.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-23 18:50:00 +07:00
A
97b6424ebe
fix(security): add cmd validation to Sprite runSprite() and runSpriteSilent() (#2904)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
Mirrors the guard already in interactiveSession() and all other clouds.
Null bytes in cmd could truncate commands at the C level.

Fixes #2903

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 17:30:25 +07:00
A
5392ff2d7a
fix: detect and recover from Hetzner primary_ip_limit exceeded error (#2905)
When parallel E2E runs exhaust Hetzner's Primary IP quota, the CLI now
detects the `resource_limit_exceeded` / `primary_ip_limit` error, automatically
cleans up orphaned Primary IPs (unattached to any server), and retries once.
If cleanup doesn't free quota, a clear message guides users to delete stale
resources or request a quota increase.

Fixes #2902

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 17:26:32 +07:00
A
d2f11bbf06
test: remove duplicate and theatrical tests (#2901)
cmd-pick-cov.test.ts: remove 8 theatrical flag-parsing tests that all hit
the same early-exit code path (no stdin options → exit 1). Each test
passed a different flag combination but all verified only that exit(1) was
thrown — no flag-specific behavior was actually exercised. Keep the one
meaningful test: "exits with error when no options provided".

ssh-cov.test.ts: consolidate 5 single-assertion constant-check tests into
2 tests (one per constant). All 5 previously tested string membership in
SSH_BASE_OPTS / SSH_INTERACTIVE_OPTS in separate it() blocks.

Before: 1868 tests, 4454 expect() calls
After:  1857 tests, 4446 expect() calls (-11 tests, -8 expects)

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 16:28:30 +07:00
A
7aba20e327
fix(ux): deduplicate install messages, add newlines to SSH polling, clarify completion messages (#2900)
- Suppress stdout+stderr from `claude install --force` to prevent duplicate
  "successfully installed" messages (was printed up to 4x)
- Make logStepInline fall back to newline-separated output when stderr is not
  a TTY, so SSH port polling status is readable in piped/captured contexts
- Consolidate post-install completion messages into a single clear milestone:
  "Agent setup complete -- {agent} is ready on {cloud}"
- Bump CLI version to 0.25.16

Fixes #2899

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 15:26:34 +07:00
A
e7e3b327a1
test: remove duplicate saveSpawnRecord describe block (#2896)
The saveSpawnRecord tests in history-trimming.test.ts duplicated the
describe block already in history.test.ts. Moved the two unique test
cases ("no cap" 200-record retention and "assign id when missing") into
history.test.ts and removed the duplicate block from history-trimming.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-23 12:14:49 +07:00
A
f1f2667cb0
fix: skip interactive session in headless mode (#2895)
* fix: skip interactive session in headless mode (#2892)

When SPAWN_HEADLESS=1, the orchestrator now exits with code 0 after
provisioning completes instead of attempting to launch the agent
interactively. This fixes Claude Code (and other agents) failing with
"Input must be provided through stdin or --prompt" when spawned via
`--headless --output json` without a prompt.

The VM is fully provisioned and ready — callers can SSH in or use
`spawn connect` to start the agent manually.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: clean up SPAWN_HEADLESS env in test afterEach to prevent leaks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-22 21:38:53 -07:00
A
b0593952df
fix(security): validate cmd parameter in sprite interactiveSession (#2888)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
Add empty-string and null-byte validation to sprite's interactiveSession,
matching the guards already present in aws, hetzner, digitalocean, and gcp.
Without this check, a raw cmd string is passed directly to bash -c.

Fixes #2881

Agent: ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:53:28 -07:00
A
da07fd4031
fix(security): prevent command injection in sprite uploadFile (#2889)
Replace shell string interpolation with array-based exec arguments in
uploadFileSprite. Previously, remotePath and tempRemote were interpolated
into a bash -c string (`mkdir -p $(dirname '${normalizedRemote}') && mv
'${tempRemote}' '${normalizedRemote}'`), which is inherently unsafe
even with regex validation.

Now uses two separate sprite exec calls with paths passed as discrete
array arguments after `--`, and computes dirname in TypeScript using
node:path/posix instead of shell command substitution. Also fixes the
mockBunSpawn test helper to return fresh ReadableStream instances per
call, preventing "ReadableStream already used" errors.

Fixes #2880

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:51:51 -07:00
A
0224b56a4d
fix(digitalocean): detect droplet limit before creation, clear error on 422 (#2891)
checkAccountStatus() now queries the account's droplet_limit and
current droplet count. When at capacity it warns interactively and
throws immediately in headless/E2E mode with a clear message instead
of attempting creation and getting a cryptic 422.

Also adds specific detection of droplet limit 422 errors in
createServer() with actionable guidance (limit increase URL).

Bump CLI to 0.25.14.

Fixes #2865

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:49:17 -07:00
A
83cd6bc6df
test: remove duplicate generateCodeVerifier/generateCodeChallenge tests from oauth-cov (#2885)
These two describe blocks in oauth-cov.test.ts were redundant subsets of the more
comprehensive coverage already in oauth-pkce.test.ts (which includes RFC 7636 test
vectors, uniqueness checks, padding validation, and base64url character checks).

Duplicates found: 1 function pair (generateCodeVerifier + generateCodeChallenge)
Tests removed: 2
Tests rewritten: 0

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 08:43:14 +07:00
A
054a740e5a
refactor: remove stale Packer comment in hetzner.ts (#2878)
The reference to "Hetzner Packer" was removed in #2869.
Updated the comment to accurately describe the snapshot naming convention.

-- qa/code-quality

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-23 04:14:00 +07:00
A
76afe9546b
test: add missing assertions to no-op smoke tests (#2879)
19 tests across 7 files were calling functions with no expect() calls —
they verified "does not throw" implicitly but provided zero signal on
side effects or return values.

Added assertions to each:
- agent-setup-cov: expect runServer called after graceful failure
- auto-update: expect runServer called on non-fatal SSH error
- aws-cov: assert state.awsRegion set by promptRegion env var paths,
  spawnSync call counts for ensureAwsCli, fetch called for destroyServer
- do-cov: assert SPAWN_NAME_KEBAB preserved on early return,
  fetch NOT called when no token in checkAccountStatus
- gcp-cov: assert spy call counts for authenticate, destroyInstance,
  ensureGcloudCli; spawnSync NOT called when GCP_PROJECT env set;
  fetch NOT called when no project in checkBillingEnabled
- hetzner-cov: assert fetch called for ensureHcloudToken validation
  and for destroyServer REST calls
- ssh-cov: assert connectSpy and bunSpawnSpy called in waitForSsh

All 1925 tests pass. expect() calls increased from 4555 to 4575.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 04:12:18 +07:00
Ahmed Abushagur
baf03ce47b
fix: prevent sprite idle shutdown during agent install (#2874)
The sprite was going idle and shutting down during long npm install
operations because the remote keep-alive script wasn't installed yet
and sprite exec alone doesn't count as activity.

- Add local keep-alive that pings the sprite's public URL every 30s
  from the client machine during provisioning and agent install
- Stop it when the interactive session starts (remote script takes over)
- Add i/o timeout to spriteRetry's transient error regex so connection
  timeouts are retried instead of failing immediately

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-23 02:13:07 +07:00
A
87f49eba48
test: remove duplicate and theatrical tests (#2873)
Remove 7 redundant tests that test the same code paths as existing tests:

- history.test.ts: consolidate 4 separate "unrecognized JSON value" tests
  (non-array object, JSON string, null, number) into one data-driven test.
  All 4 hit the identical parseHistoryData "Unrecognized format" branch.

- cmd-link-cov.test.ts: remove "exits with error when no IP provided" —
  duplicate of the same test in cmd-link.test.ts with identical behavior.

- update-check-cov.test.ts: remove "skips in test environment" and "skips
  when SPAWN_NO_UPDATE_CHECK=1" — both already covered in update-check.test.ts.

- orchestrate-cov.test.ts: remove "calls preLaunch when defined" — identical
  to the same test in orchestrate.test.ts (same mock setup, same assertion).

All 1866 remaining tests pass. Lint clean.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 20:22:47 +07:00
A
c25594cf09
test: Remove duplicate killWithTimeout tests (#2870)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
* test: remove duplicate and theatrical tests

- cmd-fix-cov.test.ts: remove 6 duplicate fixSpawn tests already covered
  in cmd-fix.test.ts; keep only the unique success message assertion
- icon-integrity.test.ts: consolidate 54 per-entity it() blocks into 4
  data-driven tests (same 67 expect() calls, 50 fewer test cases)
- manifest-type-contracts.test.ts: consolidate per-field for-loop it()
  blocks into 3 grouped tests (same 662 expect() calls, 15 fewer cases)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: remove duplicate killWithTimeout tests from ssh-cov.test.ts

The `killWithTimeout additional` describe block in ssh-cov.test.ts
duplicated scenarios already covered in kill-with-timeout.test.ts:
- "sends SIGTERM then SIGKILL" == kill-with-timeout's SIGKILL grace test
- "does nothing when first kill throws" == kill-with-timeout's SIGTERM throw test

Removed the 2 duplicate tests from ssh-cov.test.ts. The dedicated
kill-with-timeout.test.ts file is the canonical location for
killWithTimeout coverage.

---------

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-22 16:47:59 +07:00
A
cc8b6601ec
refactor: remove stale references and add missing entries to test README (#2871)
- remove stale reference to `commands-update-download.test.ts` (renamed to `cmd-update-cov.test.ts`)
- remove stale reference to `picker.test.ts` (renamed to `picker-cov.test.ts`)
- add 25 missing `-cov.test.ts` files that exist on disk but were undocumented

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-22 15:47:58 +07:00
A
7e56e1839b
test: remove duplicate and theatrical tests (#2868)
- cmd-fix-cov.test.ts: remove 6 duplicate fixSpawn tests already covered
  in cmd-fix.test.ts; keep only the unique success message assertion
- icon-integrity.test.ts: consolidate 54 per-entity it() blocks into 4
  data-driven tests (same 67 expect() calls, 50 fewer test cases)
- manifest-type-contracts.test.ts: consolidate per-field for-loop it()
  blocks into 3 grouped tests (same 662 expect() calls, 15 fewer cases)

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 12:06:55 +07:00
A
c1363b138c
feat(gcp): default boot disk to 40 GB, configurable via GCP_DISK_SIZE (#2867)
GCP's default 10 GB boot disk is insufficient for coding agents — node_modules,
apt packages, and build caches easily exceed it. Default to 40 GB and allow
override via GCP_DISK_SIZE env var.

Closes #2866

Co-authored-by: Claude <claude@anthropic.com>
2026-03-22 11:21:05 +07:00
A
92f2de4036
test: remove theatrical tests — replace no-op assertions with real signal (#2863)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
preflight-credentials.test.ts: all 7 tests had zero expect() calls with
comments like "// No crash = pass". Rewrote to capture logWarn mock calls
from mockClackPrompts() and assert on warning presence and credential names.

sprite-cov.test.ts: 13 out of 23 tests had no expect/rejects calls (just
called functions and discarded results). Added assertions on Bun.spawn call
counts to verify: authenticated paths skip login, unauthenticated paths
trigger login, createSprite reuses vs creates based on list output,
verifySpriteConnectivity calls sprite twice, setupShellEnvironment runs
multiple exec commands.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 08:38:39 +07:00
A
300e2fc221
fix(security): shellQuote cmd in runServer() across all cloud providers (#2862)
Defense-in-depth: explicitly shellQuote(cmd) inside runServer() so the
cmd parameter is always protected by single-quote escaping, regardless
of how the surrounding command string is constructed.

Previously, cmd was interpolated raw into fullCmd before the outer
shellQuote() wrapper. While the outer wrapper did protect it, this
made the safety non-obvious and fragile against future refactors.
The new pattern matches interactiveSession() where cmd gets its own
shellQuote() call.

Fixes #2859

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 14:48:37 -07:00
A
3f12cb9ee8
refactor: remove duplicate docker constants into shared orchestrate module (#2860)
Consolidate DOCKER_CONTAINER_NAME and DOCKER_REGISTRY constants from
gcp/main.ts and hetzner/main.ts into shared/orchestrate.ts. Both files
defined identical values ("spawn-agent" and "ghcr.io/openrouterteam"); they
now import the shared exports instead.

Bumps CLI patch version to 0.25.11.

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-03-21 14:27:21 -07:00
A
d480a7fec4
test: remove duplicate and theatrical tests (#2861)
- manifest.test.ts: remove 4 duplicate loadManifest error/fallback tests
  (HTTP 500 stale-cache, no-cache-HTTP500-throws, invalid-manifest-throws,
  network-error-throws) — all covered more thoroughly by
  manifest-cache-lifecycle.test.ts

- ssh-keys.test.ts: remove 2-key sorting test superseded by ssh-keys-cov.test.ts
  which validates the full 3-way sort order (ED25519 > RSA > ECDSA)

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 03:43:47 +07:00
A
7ab6c693d3
fix: add --beta docker to help output and update description (#2857)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
The --beta docker feature (PR #2854) was missing from `spawn help`
output, and its error description said "Hetzner" only but it also
works on GCP.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-21 06:20:35 -07:00
A
2f329684e0
test: remove duplicate and theatrical tests (#2858)
- aws-cov.test.ts: remove aws/BUNDLES (3 tests) and aws/credential-persistence
  (6 tests) — all scenarios already covered by aws.test.ts with stronger
  assertions (>= 5 tiers vs >= 3, pricing format, naming convention, etc.)

- cmd-run-cov.test.ts: remove "cmdRun dry run" and "cmdRun validation" (3 tests)
  — dry-run is covered more thoroughly in cmdrun-happy-path.test.ts;
  validation tests duplicate commands-error-paths.test.ts exactly

- agent-setup-cov.test.ts: remove "agents return non-empty launch commands"
  (weaker duplicate of "all agents have launchCmd") and "agents have configure
  functions" (no expect() calls — theatrical)

Total: 5 tests removed, 162 lines deleted, 0 regressions (1951 pass)

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 19:49:27 +07:00
Ahmed Abushagur
6d2c4746f5
feat: add --beta docker for Hetzner Docker CE app image (#2854)
* feat: add --beta docker for Hetzner Docker CE app image

Uses Hetzner's pre-built docker-ce app image when --beta docker
(or --fast) is active, giving faster boot times similar to DO
marketplace images. Snapshots still take priority when available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: pull and run pre-built agent Docker images on Hetzner

When --beta docker (or --fast) is active, boots Hetzner with docker-ce
app image, then pulls ghcr.io/openrouterteam/spawn-{agent}:latest and
runs it. All runServer commands are routed through docker exec into
the container, and the interactive session uses docker exec -it.
Skips agent install since the agent is pre-baked in the image.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add --beta docker support for GCP with Container-Optimized OS

When --beta docker (or --fast) is active on GCP, uses cos-stable
from cos-cloud (Docker pre-installed, read-only OS). Skips cloud-init
startup script (incompatible with COS), pulls the pre-built agent
image from ghcr.io, and routes all commands through docker exec.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: correct import path for logInfo/logStep (shared/log.js -> shared/ui.js)

The log.js module does not exist; these functions are exported from ui.ts.
Also merge duplicate ui.js imports per biome organizeImports.

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
2026-03-21 17:10:19 +07:00
A
bfe9fb9808
test: remove duplicate and theatrical tests (#2856)
Some checks are pending
CLI Release / Build and release CLI (push) Waiting to run
Lint / ShellCheck (push) Waiting to run
Lint / Biome Lint (push) Waiting to run
Lint / macOS Compatibility (push) Waiting to run
- Replace 10x `expect(true).toBe(true)` in update-check-cov.test.ts with
  meaningful assertions: skip-condition tests now verify fetch was NOT called,
  fetch-failure tests use `resolves.toBeUndefined()`, backoff edge-case tests
  verify fetch WAS called (proving the skip was bypassed)
- Remove theatrical executor existence check (`typeof executor.execFileSync === "function"`)
  that proved nothing about behavior
- Replace structural `typeof agent.install/envVars/launchCmd === "function"` checks in
  agent-setup-cov.test.ts with assertion that agent names are non-empty strings;
  the downstream tests already prove the functions work by calling them

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
2026-03-21 15:48:44 +07:00
Ahmed Abushagur
8c7a381375
fix: auto-reconnect on Sprite connection drops (#2855)
Sprite CLI exits with code 1 on "connection closed" (not 255 like SSH).
The reconnect loop now treats exit code 1 on Sprite as a connection
drop, retrying up to 5 times with a 3s delay between attempts.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:13:14 +07:00
A
a3e0dbd4dd
test: remove duplicate and theatrical tests (#2853)
- Remove `digitalocean/findSpawnSnapshot` describe from do-cov.test.ts
  (3 basic tests) — fully superseded by do-snapshot.test.ts (7 thorough
  tests covering name filtering, invalid IDs, network failure, etc.)

- Remove `setupAutoUpdate` describe from agent-setup-cov.test.ts
  (2 shallow tests checking only "systemd" string presence) — fully
  superseded by auto-update.test.ts which verifies exact systemd unit
  content, base64-encoded scripts, timer schedules, and error handling

Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 12:24:00 +07:00
Ahmed Abushagur
26332afa56
fix: prevent silent exit in --fast mode on Sprite (#2852)
In fast mode, Promise.allSettled runs server boot, OAuth, and tarball
download concurrently. When all operations complete — especially after
Bun.serve.stop(true) in the OAuth flow removes its event loop handle —
the event loop can appear empty before the await continuation starts
new I/O operations. This causes Bun to exit silently with code 0,
dropping the user back to their shell after "Successfully obtained
OpenRouter API key via OAuth!" with no error.

Fix: keep a dummy setInterval handle alive during the fast-mode
concurrent section so the event loop never drains prematurely.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 20:51:02 -07:00