The block comment in run-path-credential-display.test.ts listed five
functions it claimed to test, but the file only tests two:
- prioritizeCloudsByCredentials
- isRetryableExitCode
Functions buildCredentialStatusLines, formatAuthVarLine, validateRunSecurity,
and validateEntities were never imported or exercised in this file. Removed
the misleading entries so the comment accurately reflects test coverage.
-- qa/code-quality
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
When the TCP probe succeeds on the final attempt, `attempt` equals
`maxAttempts` after the loop increments it. The previous guard
`attempt >= maxAttempts` then incorrectly threw a timeout error even
though the port was open.
Fix by tracking TCP success with a `tcpOpen` boolean flag and checking
that instead of the attempt counter.
Fixes#2155
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* refactor: Remove redundant loadTokenFromConfig wrappers in hetzner, daytona, digitalocean
The previous PR (#2151) introduced shared loadApiToken() in shared/ui.ts and
updated hetzner/daytona to delegate to it via thin wrapper functions. This
commit removes the now-unnecessary wrapper functions entirely, inlining the
loadApiToken() calls directly at the callsite.
Also removes the 16-line duplicate loadTokenFromConfig() implementation in
digitalocean.ts (which replicates the same api_key/token field reading and
regex validation logic as loadApiToken) and replaces it with a direct call to
loadApiToken("digitalocean").
-- qa/code-quality
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* bump version to 0.12.12 (main already has 0.12.11)
---------
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>
The 'promptBundle should skip prompt without --custom' test expected
promptBundle() to return immediately when SPAWN_CUSTOM is unset. But
promptBundle() has no SPAWN_CUSTOM guard — it always shows an interactive
selection prompt unless LIGHTSAIL_BUNDLE or SPAWN_NON_INTERACTIVE=1 is set.
Without SPAWN_NON_INTERACTIVE=1, the test blocks on stdin input and hits
the 5-second bun:test timeout. When run in the full test suite it
appeared to pass due to module import caching from previous tests, making
it a flaky, non-deterministic test.
Remove the test entirely since it tests non-existent behavior.
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>
Fixes#2156
The spinner was stopped with a success message before the HTTP response
body stream was fully consumed. If the stream failed mid-transfer (network
drop, truncation), users saw "Script downloaded" followed by a confusing
downstream error. Now both the primary and fallback paths await res.text()
before calling s.stop().
Agent: issue-fixer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Extract duplicate loadTokenFromConfig helper (hetzner + daytona) into
shared loadApiToken() in shared/ui.ts, eliminating 24 lines of
duplicate validation logic across two cloud modules
- Move misplaced FETCH_TIMEOUT and UPDATE_BACKOFF_MS constants in
update-check.ts from the Schemas section into the Constants section
where they belong (stale empty section header fix)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): validate env values in cloud_headless_env parser
Reject values containing shell metacharacters ($, backtick, ;, &, |, <, >)
to prevent potential command injection if a cloud driver returns malicious output.
Fixes#2139
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): replace env value blacklist with whitelist regex
The blacklist approach missed dangerous characters like (), quotes,
backslash, newlines, {}, and !. Switch to a whitelist that only allows
[A-Za-z0-9@%+=:,./_-] — a strict safe set sufficient for env values.
Agent: security-auditor
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.6 <noreply@anthropic.com>
The "install commands should be strings" test in the "Agent launch
command consistency" describe block was a full duplicate of the
per-agent "install should be a non-empty string" test already
present in the "Agent required field types" loop.
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>
The fallback .spawnrc construction (used when provision times out before
.spawnrc is written) had two bugs:
1. zeroclaw case wrongly included OPENAI_API_KEY and OPENAI_BASE_URL —
these are hermes env vars, not zeroclaw's. zeroclaw only needs
ZEROCLAW_PROVIDER=openrouter (plus the base OPENROUTER_API_KEY).
2. hermes and kilocode were missing from the case statement entirely.
- hermes needs OPENAI_BASE_URL and OPENAI_API_KEY (verify_hermes
checks for OPENAI_BASE_URL in .spawnrc)
- kilocode needs KILO_PROVIDER_TYPE=openrouter and
KILO_OPEN_ROUTER_API_KEY (verify_kilocode checks KILO_PROVIDER_TYPE)
Without these fixes, hermes and kilocode would fail verification whenever
provisioning timed out before the normal .spawnrc was written.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
When credentials expire during server deletion, the spinner was running
simultaneously with interactive credential prompts, creating confusing
overlapping UI. Extract ensureDeleteCredentials() to run all credential
checks (which may prompt the user) before starting the deletion spinner.
All 6 cloud providers are covered: AWS, Hetzner, DigitalOcean, GCP,
Daytona, and Sprite.
Fixes#2141
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Distinguish between 'no local credentials' and 'using spawn-cached credentials'
so users understand why an initial failure message is followed by a success.
Fixes#2142
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): add --proto '=https' to all curl bun installer calls
Fixes#2134
All _ensure_bun() functions across aws, hetzner, gcp, local, daytona,
and sprite scripts now enforce HTTPS-only downloads via --proto '=https'.
This prevents MITM attacks during bun installation on remote VMs.
DigitalOcean scripts were already correct and are not changed.
Agent: security-auditor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): add --proto '=https' to bun installer in TS files
Address security reviewer feedback: the same MITM vulnerability
existed in 5 TypeScript programmatic provisioning files.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(security): quote --proto '=https' in su -c curl calls
The aws.ts and gcp.ts files had --proto =https without quotes inside
su -c '...' blocks. Uses double quotes ("=https") to properly nest
inside the single-quoted su -c argument while maintaining protocol
restriction.
Agent: security-auditor
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.6 <noreply@anthropic.com>
The DigitalOcean OAuth flow printed two near-identical fallback URL
messages: one manually before calling openBrowser(), and one from
openBrowser() itself. Remove the manual one since openBrowser()
already handles the fallback.
Fixes#2140
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
After showing post-session messages, the local process now exits cleanly
instead of requiring an extra Ctrl+C. The root cause was that after main()
resolved, lingering event loop handles (from @clack/prompts stdin listeners,
fetch connections, etc.) prevented Node/Bun from exiting naturally.
The fix adds process.exit(0) on successful main() completion, which covers
all session paths (bash script execution via execScript, SSH reconnection
via cmdConnect, and agent re-entry via cmdEnterAgent).
Fixes#2145
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* test: add coverage for 6 untested pure utility functions in shared/ui.ts
Adds tests for validateServerName, validateRegionName, validateModelId,
toKebabCase, sanitizeTermValue (security-critical), and jsonEscape.
These exported functions previously had zero test coverage.
Agent: test-engineer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: apply biome formatting to ui-utils test file
Address formatting review feedback: reformats destructuring import
to match project style.
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove `cleanup_stale_apps()` in `sh/e2e/lib/cleanup.sh` which was dead
code — defined but never called. The E2E orchestrator (`e2e.sh`) invokes
`cloud_cleanup_stale` directly on the active cloud driver; the wrapper
function and its file served no purpose.
Also remove the corresponding `source` call in `e2e.sh`.
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>
- Remove cmdlast "should not call cmdRun when no history exists" test which
admitted in its own comment that it could not verify its stated intent and
simply duplicated the assertion from the previous test in the same describe block.
- Fix always-pass risk in manifest-type-contracts: "Interactive prompts
structure" and "Config files structure" tests iterated over optional agent
fields with a bare continue when the field was absent, meaning both tests
would vacuously pass if no agents had those fields. Added guard assertions
(expect(length).toBeGreaterThan(0)) matching the pattern used by sibling
tests in the same file.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The "should use fresh disk cache without calling fetch" test only checked
toHaveProperty("agents"), which would pass even if fetch was called again.
Renamed to reflect actual behavior (in-memory cache path) and added
assertions: expect(m2).toBe(m1) and fetch call count unchanged.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address 4 reliability issues across codebase
1. sprite.ts: add --force to destroy command (stdin is "ignore" so
interactive prompts would hang until 60s timeout)
2. verify.sh: replace /dev/tcp port checks with ss -tln primary
(Debian/Ubuntu bash compiled without /dev/tcp support)
3. verify.sh: make _openclaw_restart_gateway a hard failure instead
of log_warn (matching _openclaw_ensure_gateway behavior)
4. agent-setup.ts: add ss -tln port check + "already running" early
exit + increase timeout from 120s to 300s (gateway takes ~3min
to initialize on AWS medium instances)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: biome format - use consistent double quotes in portCheck
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
- sprite/sprite.ts: Replace duplicate saveVmConnection implementation
with a call to the shared saveVmConnection from history.ts. The local
version duplicated the mkdir + writeFileSync logic already provided by
the shared function, just with Sprite-specific hardcoded values.
Remove now-unused writeFileSync, mkdirSync, and getSpawnDir imports.
- Bump CLI version 0.12.5 → 0.12.6 (patch)
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The "should use disk cache when fresh" test in manifest.test.ts set up
a mock fetch with a comment saying it "should not be called" but never
asserted expect(global.fetch).not.toHaveBeenCalled(). The test passed
whether or not the cache was actually used, providing no signal.
-- qa/dedup-scanner
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(e2e): improve openclaw reliability on AWS and other clouds
Three changes to make openclaw e2e tests more robust:
1. Increase PROVISION_TIMEOUT from 480s to 720s — AWS cloud-init
for "full" tier (Node.js + Bun + build-essential) can exceed 480s,
causing the CLI to be killed before .spawnrc is written.
2. Add .spawnrc manual fallback in provision.sh — if the CLI is killed
before writing .spawnrc, construct it via SSH using OPENROUTER_API_KEY
with agent-specific env vars (openclaw, zeroclaw).
3. Add retry logic to openclaw gateway input test — the gateway can
crash with 1006 websocket closure on resource-constrained instances.
Now retries once after killing and restarting the gateway process.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(security): fix command injection in e2e provision scripts
- Use printf %q and temp file for api_key handling in provision.sh to
prevent shell metachar injection (single quotes, backticks, $)
- Double-quote env_b64 interpolation in cloud_exec call to prevent
word splitting
- Replace echo with printf in bashrc append to avoid portability issues
- Replace overbroad pkill -f 'openclaw gateway' in verify.sh with
PID-targeted kill via lsof/fuser on port 18789
Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
* test: Remove duplicate and theatrical tests
Remove 18 duplicate tests from run-path-credential-display.test.ts
that repeated coverage already provided by dedicated test files:
- "entity validation for run path" (7 tests) duplicated check-entity.test.ts
- "key resolution for run path" (6 tests) duplicated fuzzy-key-matching.test.ts
- "run-path validation sequence integration" (5 tests) duplicated
check-entity.test.ts, fuzzy-key-matching.test.ts, and script-failure-guidance.test.ts
Replace the three duplicate describe blocks with a focused 2-test
describe("isRetryableExitCode") block that covers the only unique
assertions in that section. Also remove unused spyOn import and
unused mockExit variable.
Bump version 0.12.4 → 0.12.5.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(fmt): collapse import to single line for biome format compliance
Agent: team-lead
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
The SC2154 (referenced but not assigned) comment was leftover from a
prior version of the script. No such external variable is referenced in
the current implementation, making the suppression comment stale.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Add logStepInline/logStepDone helpers to ui.ts and convert all 9
polling loops (DO droplet, DO cloud-init, AWS instance, AWS cloud-init,
Hetzner cloud-init, Daytona SSH, Sprite connectivity, GCP startup,
shared SSH port) from multi-line spam to a single line that updates
in place.
Signed-off-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
14 agent shim scripts in sh/aws/ and sh/hetzner/ were missing error
handlers on the curl command that downloads the JS bundle from GitHub
releases. If the download failed (network issue, 404, etc.), the script
would silently proceed to exec an empty/corrupt file via bun, producing
a confusing error instead of a clear "Failed to download" message.
All other clouds (GCP, Daytona, DigitalOcean, Sprite) already had this
error handling pattern. This brings AWS and Hetzner into consistency.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Adds validateRegionName() check immediately wherever awsRegion is
assigned from environment variables, rather than waiting until
createInstance(). Prevents malicious region values from being used
in SigV4 signing and shell commands.
Fixes#2113
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Removes eval-based function creation pattern in e2e/lib/common.sh.
Uses variable indirection (ACTIVE_CLOUD global + wrapper functions)
instead of eval to reduce attack surface.
Fixes#2118
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Removes underscore and hyphen from the OAuth authorization code
validation regex, restricting it to alphanumeric characters only.
Defense in depth: if the code is ever used in logging or other
contexts, special characters won't create injection opportunities.
Fixes#2114
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes unquoted ${timeout} in _sprite_exec_long that could allow
command injection if timeout contained shell metacharacters.
Adds numeric validation before use.
Fixes#2117
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(digitalocean): throw on non-2xx in doApi() wrapper
Make doApi() throw on non-2xx responses, matching hetznerApi and daytonaApi.
5/7 call sites were silently swallowing 401/403/404/422 errors by only
destructuring text and ignoring status.
Agent: code-health
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* style: fix biome formatting in doApi() signature
Function signature needed multi-line format to match biome expectations.
Agent: code-health
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.6 <noreply@anthropic.com>
Removed `getState()` from hetzner, gcp, daytona, sprite, and digitalocean
modules. These functions were exported but never called from production code
or tests. The aws module retains its `getState()` which is tested in
custom-flag.test.ts to verify region state mutation.
Also bumps CLI patch version (0.12.2 → 0.12.3) as required per project rules.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The outer cloud_exec_long already enforces a timeout via
INPUT_TEST_TIMEOUT. The inner --timeout 60 was redundant and could
cause premature kills before the outer timeout expired.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
* fix: add 30s AbortSignal.timeout to all cloud API fetch wrappers
All four cloud provider API client wrapper functions (lightsailRest,
hetznerApi, doFetch, daytonaApi) were missing fetch timeouts, while
every other fetch call in the codebase already used AbortSignal.timeout.
A stalled TCP connection to any cloud provider would cause the CLI to
hang indefinitely with no user feedback or recovery path.
Agent: team-lead
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: apply biome formatting to fetch timeout changes
---------
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
The gateway startup was silently swallowed with log_warn, masking
real failures. Now tracks whether the port came up and fails the
test with the gateway log contents if it didn't.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
The script content tests had `if (existsSync(scriptPath))` guards
that silently skipped files if they weren't found on disk. Since
the prior test in the same describe block already asserts that all
implemented entries have script files, these guards were dead code
that could mask failures. Remove them so every sampled script is
unconditionally read and validated.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The destroyServer function only checked for status 204 (success) and
responses containing a `message` field. Any other non-204 response
(e.g., 403 with no message, 500 with HTML body) fell through to the
success path, logging "Droplet destroyed" and returning normally.
This caused `spawn delete` to mark the droplet as deleted in history
while it was still running and incurring charges.
Now all non-204 responses unconditionally throw, matching the pattern
established in Hetzner (#2105) and Daytona (#2102).
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
hetznerApi() was returning raw response text on non-2xx final attempts
instead of throwing, identical to the bug fixed in daytonaApi() by PR #2102.
Impact: a 5xx when fetching SSH keys caused createServer() to receive null
from parseJsonObj(), fall back to an empty SSH key list, and provision a
server the user cannot SSH into -- with no error or warning.
Fix matches the pattern from lightsailRest() (AWS) and PR #2102 (Daytona):
throw with the HTTP status code after retries exhaust on any !resp.ok response.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Update stale comments referencing the monolithic commands.ts (replaced
by commands/ directory in #2095). The shim at commands.ts still exists
for backward compat but all internal code paths now live under commands/.
- Fix test file comments: point checkEntity to commands/shared.ts,
cmdInteractive to commands/interactive.ts, cmdRun paths to commands/run.ts,
cmdUpdate to commands/update.ts, cmdCloudInfo to commands/info.ts,
cmdListClear to commands/list.ts
- Fix guidance-data.ts: update stale extraction comment and circular-dep
notes to reference commands/run.ts instead of commands.ts
- Fix CLAUDE.md file structure diagram: show commands/ directory and
note commands.ts is a compatibility shim
- Fix packages/cli/README.md: update directory structure diagram and
"Adding a New Command" guide to reflect the per-module layout
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
daytonaApi() returned the raw response body on all final attempts regardless
of HTTP status. destroyServer() checked hasApiError() which only matched 4xx
patterns, so persistent 500/502/503 responses were silently treated as
success — users were told "Sandbox destroyed" when billing continued.
Fix: throw on !resp.ok after retries exhaust, consistent with other cloud
modules (aws, gcp). destroyServer() now uses try/catch. testDaytonaToken()
already had try/catch so the hasApiError() check was redundant.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* test: remove Bun.spawnSync subprocess calls from ssh-keys tests
Replace Bun.spawnSync calls to ssh-keygen in createFakeKeyPair helper
with plain file writes, and mock Bun.spawnSync via spyOn for all tests
that exercise getKeyType, generateSshKey, and getSshFingerprint.
Cuts test runtime from 1212ms to ~47ms (25x speedup) and brings the
test file into compliance with the CLAUDE.md no-subprocess-spawning
policy.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: apply biome formatting to ssh-keys test
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
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>
Without --force, sprite destroy prompts for confirmation in
non-interactive E2E mode and silently fails ("Ok, come back later!"),
leaving stale instances running indefinitely.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
The sprite destroy command can hang indefinitely when the Sprite API
is unresponsive. Add a 60s timeout using the existing killWithTimeout
utility (same pattern as runSprite).
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
Co-authored-by: A <258483684+la14-1@users.noreply.github.com>
Rebased fix/issue-2083 onto main after commands.ts split (PR #2095).
Key resolutions:
- commands.ts: kept HEAD shim (re-exports from ./commands/index.ts)
- package.json: kept PR version 0.12.0 without @openrouter/spawn-shared dep
- Fixed @openrouter/spawn-shared imports in commands/shared.ts, commands/update.ts,
and __tests__/orchestrate.test.ts that were added after the PR branched
All 1390 tests pass, biome lint clean.
Agent: pr-maintainer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The README was referencing commands.test.ts and integration.test.ts which
no longer exist (split into 20+ specialized files), and incorrectly stated
the test runner was vitest (banned — project uses bun:test). Rewrote to
accurately document all 44 test files with their coverage scope.
Co-authored-by: spawn-qa-bot <qa@openrouter.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- hetzner/hermes.sh: add thin-shim header comment, blank line after
_ensure_bun definition, and section comments (Local checkout, Remote)
to match the canonical pattern used by aws/gcp/sprite/daytona
- digitalocean/hermes.sh: add detailed _run_with_restart comment block
and inline section comments (Normal exit, SIGTERM, Other failure) to
match digitalocean/claude.sh
Both scripts now produce identical output to their cloud's reference
script (e.g. aws/hermes.sh, digitalocean/claude.sh) when the agent
name is substituted.
Fixes#2082
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>