Commit graph

472 commits

Author SHA1 Message Date
A
0f4df7be71
feat: pre-built Docker image for OpenClaw on Fly.io (#1686)
Eliminates the slow waitForCloudInit() + bun install phase by booting
a pre-built image with Node.js, bun, and openclaw already installed.
The image is rebuilt daily via GitHub Actions to pick up new releases.

Other agents are unaffected — they still use ubuntu:24.04 + cloud-init.

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 02:50:46 -05:00
A
e381ca2412
fix: replace require() with ESM imports in bun eval scripts (#1682)
* fix: replace require() with ESM imports in bun eval scripts (#1669)

Fixes #1669

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

* fix: update test assertion to match ESM import pattern

The test expected require('http') but the PR changed shared/common.sh
to use ESM imports. Update assertion to expect import http from 'http'.

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

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-22 01:50:08 -05:00
A
9f43113452
fix: pin zeroclaw installer to commit SHA instead of mutable main branch (#1683)
Replaces all references to zeroclaw-labs/zeroclaw/main/scripts/install.sh
with a pinned commit SHA (a117be64). This prevents supply chain attacks via
the mutable 'main' branch reference in curl|bash installer patterns.

Other curl|bash patterns (bun.sh, claude.ai, sprites.dev) use HTTPS to
vendor-controlled domains with no stable commit SHA to pin to -- these
follow industry-standard installer patterns and are left as-is.

Fixes #1670

-- refactor/ux-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 01:49:44 -05:00
A
4cec25c6b7
fix: pass spawn name through cmdRun and headless flows (#1674)
cmdRun (spawn <agent> <cloud>) was not collecting or passing the spawn
name, so SPAWN_NAME was never set in the script environment and the
history record lacked a name. cmdRunHeadless had the same gap.

- Add promptSpawnName() call to cmdRun and pass result to execScript
- Wire spawnName through HeadlessOptions to runBashHeadless
- Add --name CLI flag to set SPAWN_NAME from the command line
- Skip interactive name prompt when SPAWN_NAME is already set
- Bump CLI to 0.5.33

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 21:52:52 -08:00
A
2c3d12b22a
fix: add build-essential to Fly.io base packages (#1668)
Native npm packages (node-gyp, etc.) need gcc/make/libc-dev to compile.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 20:53:51 -08:00
A
aed93ef6c2
fix: align Fly.io Node.js install with other clouds (apt + n) (#1667)
Replace direct nodejs.org tarball download with the same apt + n
approach used by all other clouds: install nodejs/npm via apt, then
upgrade to v22 LTS via `n`. Also adds zsh to base packages (matching
cloud-init userdata) and removes xz-utils (no longer needed).

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 20:40:42 -08:00
A
461d945212
chore: bump CLI version to 0.5.32 (#1666)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 20:30:14 -08:00
A
26f689a0e1
fix: install OpenClaw via bun on Fly.io instead of curl installer (#1665)
Use `bun install -g openclaw` (consistent with Hetzner and other VM
clouds) instead of the curl installer script. Bun and Node.js are
already available from the cloud-init phase.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 20:29:11 -08:00
A
96df2a2a52
feat: add dedicated CPU VM tiers for Fly.io (#1664)
Add performance-1x/2x/4x (dedicated vCPU) options alongside existing
shared CPU tiers. Thread cpuKind through to the Machines API cpu_kind
field so users can provision dedicated VMs for consistent performance.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 19:59:18 -08:00
A
459f97ee47
fix: remove volume provisioning prompt from Fly.io setup (#1663)
The interactive volume prompt added unnecessary friction to the
provisioning flow. Volume support remains in fly.ts for programmatic
use via ServerOptions.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 19:45:20 -08:00
A
8eedcd8553
feat: add "Enter agent" option to spawn ls (#1662)
When selecting a previous spawn from `spawn ls`, the first option is now
"Enter <agent>" which SSHes into the VM and launches the agent directly,
instead of just opening a plain SSH shell.

The exact launch command is captured at spawn time and stored in the
connection record, so dynamic state (PATH setup, env sourcing) is
preserved for reconnection.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 19:40:05 -08:00
A
ef7b67752e
fix: prevent GitHub token exposure via process listing (#1661)
Write GITHUB_TOKEN to a temp file with 0600 permissions instead of
inlining in the command string, preventing exposure via ps aux and
/proc/*/cmdline.

Fixes #1659

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-21 22:11:55 -05:00
A
2f0f705387
feat: add spawn_agent and spawn_cloud params to OAuth auth URL (#1660)
Thread agent and cloud slugs through to the OpenRouter OAuth URL so
OpenRouter knows which agent/cloud combination the user is deploying.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 18:31:57 -08:00
A
760fa22dea
fix: bump fly default VM to 4GB, add 10GB volume, hide keepalive dots (#1654)
- Default VM memory: 1024MB → 4096MB (all agents except ZeroClaw
  which stays at 1024MB). Prevents OOM kills during native installs.
- Attach a 10GB persistent volume at /data with a /root/work symlink
  so agents have enough storage to clone repos and work.
  Configurable via FLY_VOLUME_SIZE env var.
- Keepalive: changed dots to spaces so they're invisible in terminal.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 17:56:02 -08:00
Ahmed Abushagur
20ca94bddb
fix: add keepalive to Fly installAgent to prevent SSH idle timeout (#1658)
fly machine exec drops the session when there's no output for too long.
OpenClaw's installer runs silently in non-TTY mode, producing no output
for minutes while npm builds native deps — triggering "ssh shell: session
forcibly closed".

Fix: run the install command in the background and print a dot every 5s
to keep the SSH session alive. Applies to all agents on Fly, not just
OpenClaw.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 20:45:14 -05:00
A
08a51843f8
fix: replace require("path") with ESM import in security.ts (#1657)
The validatePromptFilePath function used CJS require("path") inline,
violating the project's ESM-only rule. This could trigger Bun
compatibility issues since the project is "type": "module".

Replace with a top-level `import { resolve } from "path"` statement.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-21 20:44:57 -05:00
A
0e1740e869
fix: PowerShell 5.1 compatibility in install.ps1 (#1653)
- Replace Join-Path 3-arg with nested Join-Path calls (PS 5.1 compat)
- Wrap bun pm bin -g in try/catch to suppress NativeCommandError
- Fix build failure detection using $LASTEXITCODE instead of try/catch
- Replace non-ASCII em dashes and box drawing chars with ASCII equivalents

Fixes #1649, #1650, #1651, #1652

Agent: ps1-bug-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-21 20:42:37 -05:00
A
8650ad15d8
feat: interactive VM size and volume prompts for Fly.io (#1655)
Users can now choose VM size (1x/2x/4x shared CPU tiers) and opt into
persistent volumes during provisioning instead of getting hardcoded defaults.
FLY_VM_MEMORY env var still works for CI/headless mode.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 17:19:09 -08:00
A
0827866a95
fix: pass app name (not machine ID) to flyDestroyServer (#1647)
`conn.server_id` holds the Fly machine ID (e.g. "d8d91a0c4e1783")
while `conn.server_name` holds the app name (e.g. "spawn-abc123").
`flyDestroyServer()` calls `/apps/${name}/machines` — it expects the
app name, not a machine ID.

The `server_id || server_name` precedence meant every `spawn delete`
for Fly.io passed a machine ID, causing the Fly API to return
"Could not find App" and leaving the VM running and accumulating charges.

Fix: swap precedence to `server_name || server_id` for the Fly.io path.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 16:22:07 -08:00
A
b43d3f1b70
fix: combine gateway start + port wait into single SSH session (#1642)
The old flow opened up to 60+ separate fly ssh console sessions to
poll port 18789 after starting the gateway daemon. Each session opens
a new WireGuard tunnel which is slow and flaky.

Now: one SSH session starts the daemon, then polls the port in-band
with a simple for loop. Output from the loop also serves as a
keepalive for flyctl.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 16:16:08 -08:00
A
a2dfddec3d
fix: add keepalive to fly ssh + stop suppressing install output (#1641)
Two fixes for "session forcibly closed" during openclaw install:

1. The openclaw install command piped all output to /dev/null, so
   flyctl saw zero bytes flowing and killed the session. Removed
   the >/dev/null 2>&1 redirect.

2. Added a background keepalive to runServer that prints a dot to
   stderr every 10s. This prevents flyctl from tearing down silent
   SSH sessions even if the command itself produces no output for
   a while.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 16:01:47 -08:00
A
09d9f597ac
fix: use openclaw curl installer to prevent fly ssh hang (#1640)
bun install -g openclaw spawns child processes that keep stdout/stderr
FDs open, preventing fly ssh console from detecting EOF. Replace with
the official curl installer (--no-onboard) which handles Node detection
and cleanup without leaving orphan processes on the pipe.

See: https://docs.openclaw.ai/install

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 15:58:05 -08:00
A
3b27a1d5d8
fix: fly CLI not found after install and token not passed to subprocesses (#1634)
Two bugs in fly.ts:

1. getCmd() only uses `which` in a subprocess, but Bun.spawnSync inherits
   the original PATH — not process.env mutations from ensureFlyCli(). After
   installing flyctl to ~/.fly/bin, getCmd() still can't find it. Fix: add
   a filesystem fallback that checks ~/.fly/bin directly.

2. ensureFlyToken() resolves the token and saves it to config, but never
   writes it to process.env.FLY_API_TOKEN. When fly ssh console runs as a
   subprocess, it has no token and can't authenticate. Fix: add
   syncTokenToEnv() and call it on every successful token resolution path.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:45:19 -08:00
A
ea9bb2bee5
fix: use direct Node.js binary tarball on Fly instead of apt/npm/n (#1637)
Replace the Node.js install chain (apt nodejs+npm → npm install -g n →
n 22 → symlinks) with a single curl of the v22 binary tarball from
nodejs.org. Eliminates python3 dependency, npm bloat, and the n version
manager. Bun is installed first as the primary package manager.

Fly-only change — other clouds unchanged pending validation.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 15:44:39 -08:00
A
42f2b66b55
fix: keep stdin pipe open during fly ssh to prevent session teardown (#1638)
flyctl tears down the WireGuard transport when stdin closes ("session
forcibly closed; the remote process may still be running"). This
killed long-running commands like `bun install -g openclaw`.

Instead of calling stdin.end() immediately, keep the pipe open for
the duration of the command and close it after the process exits.
The pipe still prevents interactive prompts from hanging (no data
flows through it), but flyctl no longer interprets the closed fd
as a signal to kill the session.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 15:44:14 -08:00
A
c0c6f896b9
fix: use TypeScript module for fly spawn delete (bash script sourced missing file) (#1635)
After PR #1602 converted fly/ from bash to TypeScript, fly/lib/common.sh was
removed. However, buildDeleteScript() still generated a bash script that tried
to source it via curl, causing spawn delete to always fail for Fly.io servers
with a curl exit 22 (404). Users were left with orphaned apps incurring charges.

Fix: add fly-specific path in execDeleteServer() that calls ensureFlyCli(),
ensureFlyToken(), and destroyServer() directly from the TypeScript fly module,
bypassing the bash script path entirely. Remove the dead case "fly" from
buildDeleteScript().

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-21 15:28:51 -08:00
A
4bd373f1aa
fix: replace NodeSource with n for Fly Node.js install (#1633)
NodeSource's setup_22.x script adds an APT repo, pulls in python3 as a
dependency, and runs apt-get update twice — slow and heavyweight. Switch
to the same approach used by GCP/Hetzner: install apt's bundled nodejs,
then upgrade to v22 via n with symlinks.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 15:16:11 -08:00
A
b4be9b9d2f
fix: use pipe+close for fly ssh stdin instead of ignore (#1632)
fly ssh console with stdin as /dev/null ("ignore") can cause the
connection to hang — flyctl doesn't get a clean EOF signal to know
when to close the transport. Switch to "pipe" and immediately call
stdin.end() so flyctl receives a proper EOF.

Applied to runServer, runServerCapture, and uploadFile.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 15:12:44 -08:00
A
576fc05c6e
fix: prevent openclaw install hang on fly by closing inherited FDs (#1630)
`bun install -g openclaw` spawns child processes that keep stdout/stderr
FDs open, preventing `fly ssh console -C` from detecting EOF and returning.
Wrap in subshell with redirected output so children inherit closed FDs.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 15:04:35 -08:00
A
246351874e
fix: skip unnecessary apt recommends (python3) during fly VM setup (#1629)
git on Ubuntu 24.04 pulls in python3 via recommended packages. Use
--no-install-recommends to install only direct dependencies. Also
added ca-certificates explicitly since it's needed for HTTPS but
won't be auto-pulled without recommends.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 15:02:55 -08:00
A
cbd8c87a6d
fix: prevent terminal hang during fly agent install + fatal preLaunch (#1628)
Two fixes:

1. runServer() was inheriting stdin, so commands like `claude install`
   that try to read input would hang the terminal indefinitely. Changed
   stdin to "ignore" (/dev/null) for non-interactive remote commands.

2. preLaunch failures (e.g. OpenClaw gateway) were silently swallowed,
   dropping users into a broken TUI with no gateway. Now preLaunch
   errors propagate — users get a clear error instead of a mystery hang.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 14:53:07 -08:00
A
79da7298f9
test: remove 11 more theater/duplicate test files (825 tests) (#1627)
Removed files fall into two categories:

1. Replica files (7) — define inline copies of functions and test the
   copies instead of real code:
   - resolve-list-filters.test.ts (75 tests, explicit "Exact replica" comment)
   - credential-display-lines.test.ts (44 tests, "test via exact replicas")
   - cli-pipeline.test.ts (43 tests, replica extractFlagValue)
   - index-parsing.test.ts (38 tests, replica expandEqualsFlags/extractPromptArgs)
   - list-prompt-display.test.ts (32 tests, replica suggestCloudsForPrompt)
   - prompt-file-errors.test.ts (34 tests, replica handlePromptFileError)
   - commands-helpers.test.ts (64 tests, replica calculateColumnWidth/validateNonEmptyString)

2. Duplicate coverage (4) — import real functions but test the exact
   same helpers already covered in commands-exported-utils.test.ts:
   - credential-prioritization.test.ts (57 tests, also has replica functions)
   - list-output-helpers.test.ts (97 tests, same 10+ functions)
   - time-auth-record-helpers.test.ts (70 tests, same 9 functions)
   - manifest-real-data.test.ts (44 tests, same functions against real manifest)

Before: 92 files, 4,469 tests
After:  81 files, 3,644 tests (0 failures)

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 14:40:37 -08:00
A
cbecb9cbea
fix: suppress interactive dpkg prompts during fly VM setup (#1626)
tzdata (pulled in as a Node.js dependency) tries to run
dpkg-reconfigure interactively, which fails on headless Fly
machines. Set DEBIAN_FRONTEND=noninteractive so apt silently
accepts defaults.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 14:35:27 -08:00
A
bd78f6dc1f
fix: replace fly machine exec with fly ssh console to fix 408 timeouts (#1623)
fly machine exec uses Fly's HTTP exec API which randomly returns 408
deadline_exceeded on commands >30s. Switch all non-interactive remote
execution (runServer, runServerCapture, uploadFile) to fly ssh console -C
which uses WireGuard tunneling and is reliable for long-running commands.

Also batch ~25 individual remote calls into ~4 combined shell scripts:
- waitForCloudInit: 8 calls → 1 (apt, node, bun, PATH setup)
- installClaudeCode: 8 calls → 1 (cleanup, install, finalize)
- setupClaudeCodeConfig: 5 calls → 1 (inline base64 file writes)
- env setup in main.ts: 4 calls → 1 (inline base64 + shell hooks)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
2026-02-21 14:25:05 -08:00
A
9c0ebcba63
test: remove unicode token test (contradicts security validation) (#1625)
_load_token_from_config intentionally rejects non-ASCII tokens via
regex validation to prevent curl injection. The test expected unicode
tokens to pass, contradicting the code's security design.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 14:22:53 -08:00
A
978221c673
test: remove sandbox-verification.test.ts (always skipped in CI) (#1621)
CI runs `bun test` from the repo root, not `cli/`, so the
bunfig.toml preload that sets up the sandbox never loads. All 17
tests skip silently — they verify preload infrastructure, not
application code.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 14:12:15 -08:00
A
175b1a798b
test: remove 38 replica/duplicate test files (2,497 fake tests) (#1620)
These test files were auto-generated by an AI agent and test copy-pasted
"replica" functions defined inline — not the real source code. They pass
even when the actual code is broken, providing false confidence.

Two categories removed:

1. Replica-only files (34 files, ~1,482 tests): Define inline copies of
   functions and test those copies instead of importing from source.
   Examples: key-server.test.ts, trigger-server.test.ts,
   index-dispatch-routing.test.ts, verb-aliases.test.ts

2. Duplicate-with-imports files (4 files, ~631 tests): Import real
   functions but duplicate coverage already in
   commands-exported-utils.test.ts. Examples:
   commands-credential-display-internals.test.ts (178 tests),
   cli-core-edge-cases.test.ts (237 tests)

Before: 131 files, 6,966 tests (5 failing)
After:   93 files, 4,469 tests (1 pre-existing failure)

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 14:09:06 -08:00
A
0f59f0e844
fix: fly machine wait timeout exceeds API max of 60s (#1619)
The Fly Machines API enforces a [1s, 1m0s] range on
WaitMachineRequest.Timeout. We were passing 90s, which caused an
invalid_argument error and prevented machines from starting.

Lower the default to 60s (the API maximum) and retry up to 3 times
so slow-starting machines still have a full 3-minute window.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 14:00:13 -08:00
A
0569c86ba5
fix: strip ANSI codes in tests that compare plain text output (#1617)
On CI (GitHub Actions), `CI=true` causes picocolors to enable ANSI
output. Tests comparing against plain text (e.g., `toContain("--prompt
requires a value")`) fail because the actual output wraps text in bold/
dim ANSI codes.

Fixes:
- Subprocess tests (runCli): add NO_COLOR=1 to child env
- Mock capture tests: add stripAnsi() helper to output getters
- Bash subprocess tests: add NO_COLOR=1 to execSync env

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 13:52:30 -08:00
A
d69c4f0f02
fix: reject non-2xx responses in Fly.io token validation (#1614)
testFlyToken() fallback to /v1/user accepted 404 plain text responses
because hasError() only checks for JSON "error"/"errors" keys. Adding
resp.ok check ensures non-2xx responses are correctly rejected.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 13:47:40 -08:00
A
01e5fa842d
fix: update tests for fly TypeScript shim scripts and env injection (#1609)
After the fly provider was converted to TypeScript (PR #1602), the bash
shim scripts no longer source lib/common.sh or reference OPENROUTER_API_KEY
directly -- that logic moved to TypeScript. Skip TypeScript shim scripts
in bash-specific convention checks.

Also fixes:
- URL regex in cloud-error-guidance to exclude backticks/commas from
  template literals in heredocs
- aws added to skipProviders for destroy_server error check (uses set -e
  and internal process.exit, not explicit return 1)
- inject_env_vars_local test regex updated to match semicolon separator
  instead of && (matches actual shared/common.sh implementation)

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 12:45:21 -08:00
A
262d081756
refactor: move fly TS into cli/src/fly/, add build-clouds.sh (#1604)
Move all fly TypeScript files from fly/lib/*.ts and fly/main.ts into
cli/src/fly/. This gives them access to cli/node_modules (@clack/prompts),
biome linting, and the existing bun:test infrastructure — no symlinks or
NODE_PATH hacks needed.

The org picker now uses @clack/prompts select() directly (static import,
bundled at build time).

New: cli/build-clouds.sh — auto-discovers cli/src/*/main.ts and bundles
each into {cloud}.js. Scalable to future cloud TS migrations:
  bash cli/build-clouds.sh        # build all
  bash cli/build-clouds.sh fly    # build one

Shims now check for cli/src/fly/main.ts (local) or download fly.js from
GitHub releases (remote curl|bash).

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 12:34:09 -08:00
L
14fb352b52
feat: add Windows PowerShell installer (install.ps1) (#1610)
The only existing installer (install.sh) is bash-only and fails silently
on Windows PowerShell — 'curl ... | bash' errors because bash.exe is not
available outside WSL.

install.ps1 implements the same logic as install.sh for PowerShell:
- Checks bun >= 1.2.0; installs via bun.sh/install.ps1 if missing
- Downloads CLI source via git sparse-checkout or GitHub API fallback
- Builds with 'bun install && bun run build'; falls back to pre-built binary
- Installs to %USERPROFILE%\.local\bin (or SPAWN_INSTALL_DIR override)
- Creates spawn.cmd wrapper for cmd.exe compatibility
- Adds install dir to the user's persistent PATH if not already present

Usage:
  irm https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/cli/install.ps1 | iex

README updated with Windows PowerShell install instructions alongside
the existing macOS/Linux/WSL command.

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 12:31:23 -08:00
A
70dd127edc
fix: aws destroy_server silently swallows errors, fix URL regex in test (#1606)
The aws destroy_server function had conditional logic (if/else for CLI
vs REST mode) but no error handling - failures were silently ignored and
"Instance destroyed" was logged even on failure. This could leave
instances running and incurring charges without the user knowing.

Also fix the URL extraction regex in cloud-error-guidance.test.ts to
exclude backtick characters, preventing false positives from template
literals in embedded TypeScript code.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-21 15:09:44 -05:00
A
eff99caefe
fix: apply default spawn name when user presses Enter without typing (#1605)
promptSpawnName() used `placeholder` (visual hint only) without `defaultValue`,
so pressing Enter returned an empty string instead of applying the placeholder.
Now generates a unique default like `spawn-a3f2` with a random suffix to avoid
Fly.io global name collisions.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 11:46:59 -08:00
A
d7ff0739a2
fix: fly auth token deprecated + org picker + macaroon tokens (#1603)
* fix: fly auth token deprecated + org picker + macaroon discharge tokens

Three fixes for the fly/ TypeScript provider:

1. `fly auth token` is deprecated — newer flyctl outputs a message, not
   a token. Now tries `fly tokens create org --expiry 24h` first, with
   `fly auth token` as fallback. Uses org tokens (not deploy) since
   spawn needs to create new apps.

2. Token sanitization stripped macaroon discharge tokens at commas
   (`fm2_[^ ,]*` → `fm2_\S+`). The full composite token
   `fm2_xxx,fm2_yyy,fo1_zzz` is now preserved.

3. Org picker upgraded from numbered 1/2 input to arrow-key interactive
   selector with cursor navigation, scroll windowing, and fallback to
   numbered list when TTY is unavailable.

Also fixes: testFlyToken fallback sent `Bearer FlyV1 ...` (double prefix)
for macaroon tokens — now dispatches FlyV1 vs Bearer correctly.

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

* docs: never run test/mock.sh locally — opens browser, CI only

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

---------

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 11:06:19 -08:00
A
2ef621cc69
refactor: convert fly/ cloud provider from bash to TypeScript (#1601) (#1602)
Replace fly/lib/common.sh (741 lines of bash) with a TypeScript
implementation using Bun runtime. The fly/ provider was the most
complex bash code in the project — recent fixes (#1597, #1599, #1600)
highlight the pain of debugging HTTP calls, JSON parsing, and multi-step
auth flows in shell.

New TypeScript modules:
- fly/lib/ui.ts — logging, prompts, validation (zero deps)
- fly/lib/fly.ts — API client (fetch), auth chain, org listing, provisioning
- fly/lib/oauth.ts — OpenRouter OAuth via Bun.serve(), key management
- fly/lib/agents.ts — typed agent configs for all 6 agents
- fly/main.ts — orchestrator entry point

Agent .sh files become thin shims (~30 lines) that install bun if needed,
download TS sources for curl|bash execution, and delegate to main.ts.

Test coverage:
- 44 TypeScript unit tests (bun test) for pure logic
- 4 fly failure-mode tests (mock.sh) for error scenarios
- All existing test suites pass (110 run.sh, 76 mock.sh)

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-21 10:41:34 -08:00
A
9acc239001
fix: validate token characters in _load_token_from_config to prevent curl injection (#1547)
* fix: validate token characters in _load_token_from_config to prevent curl injection

Tokens loaded from ~/.config/spawn/{cloud}.json were exported without
character validation. A tampered config file containing a token with
embedded newlines could exploit the _curl_api function's -K - (stdin
config) mechanism to inject arbitrary curl directives (e.g., output,
url), since curl interprets newlines in the config format as directive
separators.

Add allowlist validation (^[a-zA-Z0-9._/@:-]+$) matching the pattern
already used in key-request.sh _try_load_env_var and validate_api_token,
making all three token-loading paths consistent.

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

* fix: address review feedback on token validation PR

- Update backslash test to expect validation failure (backslashes not
  valid in any known API token format; the old expectation was wrong
  after validation was added)
- Fix test so exit code comes from _load_token_from_config directly,
  not the trailing echo which always exits 0
- Add comment in shared/common.sh explaining why the pattern includes
  colon vs key-request.sh pattern (Fly.io FlyV1 tokens use colons)

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

* fix: address review feedback — widen token charset for base64 segments

The original regex rejected + and = which are valid base64 characters
found in API tokens (e.g. sk-or-v1-abc/def+ghi==). This caused a
pre-existing test to fail. Widen the allowlist to include + and =
while keeping the security comment documenting the pattern difference
with key-request.sh.

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>
2026-02-21 01:18:34 -05:00
A
53e6de7f55
fix: validate mock-curl-script.sh stays in sync with mock.sh in test-infra-sync (#1550)
The test-infra-sync test validates that mock.sh's _strip_api_base() and
_validate_body() cover all clouds with fixtures. However, the actual
runtime mock used by tests is mock-curl-script.sh, which has its own
copies of these functions. Nothing enforced these copies staying in sync,
so a contributor could update mock.sh to pass validation while the
runtime mock silently fails to handle new cloud URLs.

Add cross-file sync tests that verify both files handle the same cloud
patterns for _strip_api_base() and _validate_body(). Also refactor
helpers to accept content as a parameter for reuse across both files.

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-21 01:18:15 -05:00
A
3c4d92cc9f
test: fix 25 test failures from mock.module global pollution and sandbox env (#1539)
Add autocomplete mock to 38 @clack/prompts mock.module declarations
that were missing it. Bun's mock.module is process-global, so when any
other test file's mock wins the race, p.autocomplete was undefined,
causing 17 cmd-interactive tests to fail non-deterministically.

Also guard sandbox-verification tests with describe.skipIf(!isSandboxed)
so the 8 meta-tests skip cleanly when running from repo root (where
bunfig.toml preload is not active) instead of failing.

Result: 6995 pass, 0 fail from cli/; 6978 pass, 0 fail, 17 skip from root.

Agent: test-engineer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 19:36:20 -05:00