Commit graph

23 commits

Author SHA1 Message Date
A
ac7fa14c61
fix: cache AWS credentials to avoid re-prompting on retry (#1841) (#1852)
After a successful interactive credential entry, credentials are now
saved to ~/.config/spawn/aws.json (chmod 600). On the next run, cached
credentials are loaded and validated before prompting again.

Supports --reauth flag / SPAWN_REAUTH=1 to force fresh credential entry.

Fixes #1841

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 00:53:34 -05:00
A
a96a396e79
fix: add Lightsail activation prerequisite to docs and error messages (#1850)
- Adds a Prerequisites section to sh/aws/README.md (updated path after
  shell scripts reorganization in #1843) with the Lightsail activation
  URL as the first step
- Surfaces the Lightsail activation URL in createInstance error hints
  for both CLI and REST paths so users get actionable guidance on failure
- Bumps CLI to 0.9.1

Fixes #1838

Agent: issue-fixer

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-24 00:53:28 -05:00
A
18a7f7e96a
fix: improve AWS Lightsail error message for new accounts (#1826)
* fix: improve AWS Lightsail error message for new accounts

Fixes #1824

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

* style: fix biome formatting in AWS Lightsail error message

---------

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 22:05:50 -05:00
A
463c7a2efb
feat: add --custom flag for machine type/region selection (#1810)
* feat: add --custom flag for interactive machine type/region selection

By default, all clouds now skip size/region prompts and use sensible
defaults for faster provisioning. The --custom flag enables interactive
pickers on all clouds, unifying the previously inconsistent behavior
where some clouds always prompted and others never did.

- AWS: promptRegion/promptBundle gated on SPAWN_CUSTOM
- GCP: promptMachineType/promptZone gated on SPAWN_CUSTOM
- Fly: promptVmOptions gated on SPAWN_CUSTOM
- Hetzner: new promptServerType/promptLocation with type/location arrays
- DigitalOcean: new promptDropletSize/promptDoRegion with size/region arrays
- Daytona: new promptSandboxSize with cpu/memory/disk presets
- Sprite: no change (managed platform, no meaningful size options)
- --custom + --headless is an error (incompatible modes)
- Version bump to 0.8.0 (new feature)

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

* style: fix biome format violations in --custom flag code

Auto-format object literals in arrays (expand to multi-line), wrap
long console.error line, and expand inline array in test assertion.

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-23 12:29:51 -05:00
A
493ca7a1cd
fix: use SSH_INTERACTIVE_OPTS in interactiveSession() for hetzner/do/aws/gcp (#1805)
BatchMode=yes in SSH_BASE_OPTS actively blocks TTY prompts in interactive
sessions. Four cloud providers (Hetzner, DigitalOcean, AWS, GCP) were
using SSH_BASE_OPTS in their interactiveSession() functions despite
SSH_INTERACTIVE_OPTS being purpose-built for this (added in PR #1795).
This also adds Compression=yes, IPQoS=lowdelay, StrictHostKeyChecking=accept-new,
and the -t flag (already included), aligning with the commands.ts reconnect 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-23 09:25:26 -05:00
A
a26d27f139
style: enforce biome format across codebase, add CI check (#1794)
Run `biome format --write` on all 98 source files (38 needed fixes).
The main change: object literals and long argument lists are now expanded
onto separate lines per Biome's `"expand": "always"` setting, making
code much easier to scan on narrow screens.

Add `biome format` check step to CI lint workflow so formatting
regressions are caught on every PR.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-22 23:32:12 -08:00
A
86cae8ee32
feat: add SSH key discovery & selection across all providers (#1792)
All 4 providers (Hetzner, DO, AWS, GCP) hardcoded ~/.ssh/id_ed25519 and
duplicated key generation logic. Users with id_rsa or custom-named keys
got unwanted new keys generated. This adds a shared ssh-keys module that:

- Scans ~/.ssh/ for all valid key pairs (matching pub + private files)
- With 0 keys: generates id_ed25519 (same as before)
- With 1 key: uses it silently
- With 2+ keys: prompts multiselect (all selected by default)
- Caches the result at module level for the session
- Centralizes getSshFingerprint() (was duplicated in Hetzner + DO)
- All providers now pass -i flags for selected keys to SSH commands

Net -152 lines of duplicated code across providers.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-22 23:22:50 -08:00
A
b802dfbc16
refactor: extract saveLaunchCmd to history.ts (#1789)
Eliminates copy-paste of saveLaunchCmd across 8 cloud provider files.
The local/local.ts copy had already diverged (using Bun.write() instead
of writeFileSync()), confirming the maintenance risk.

Fixes #1786

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-22 23:11:14 -08:00
A
4a45a2c9c1
refactor: extract saveVmConnection to history.ts (#1788)
Eliminates copy-paste of saveVmConnection across 6 cloud provider files.
Fixes #1787

Agent: complexity-hunter

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-23 01:56:48 -05:00
A
16c8a2b90b
fix: use getSpawnDir()/getConnectionPath() in all cloud providers (#1774)
Fixes #1769

All 8 cloud providers hard-coded `${process.env.HOME}/.spawn` for
connection data, bypassing the SPAWN_HOME env var support in history.ts.
Replaced all 16 occurrences with getSpawnDir() and getConnectionPath().

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-22 19:27:21 -08:00
A
ef2748069f
fix: use child_process.spawn for interactive sessions to fix TTY passthrough (#1780)
Bun.spawn() doesn't properly restore TTY state after @clack/prompts
manipulates stdin raw mode during provisioning. This causes laggy/broken
keyboard input in SSH sessions launched via `spawn run`. Node's
child_process.spawn() with stdio: "inherit" does a clean FD handoff,
matching the already-working pattern in runInteractiveCommand() used by
`spawn ls` resume.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-22 19:22:17 -08:00
A
0843c5e708
feat: shared SSH wait utility with TCP pre-check and stderr capture (#1779)
Replace 5 duplicated SSH wait implementations (AWS, DO, Hetzner, GCP,
Sprite) with a shared two-phase utility in cli/src/shared/ssh.ts:

- Phase 1: cheap TCP probe (2s intervals) until port 22 opens
- Phase 2: full SSH handshake (3s intervals) with stderr capture
- Adds BatchMode=yes to prevent interactive prompt hangs
- Removes ~220 lines of duplicated sleep/SSH_OPTS/waitForSsh code

Daytona (token auth) and Fly (WireGuard) left unchanged — too different.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-22 19:17:09 -08:00
A
b62dc1af33
feat: ban as type assertions, add runtime schema validation with valibot (#1775)
* fix: resolve all biome lint warnings across the codebase

- Replace all noExplicitAny with proper types (unknown, Record<string, unknown>)
- Fix useBlockStatements in picker.ts (braceless if)
- Fix useNumberNamespace in picker.ts (parseInt → Number.parseInt)
- Codebase now passes biome lint with 0 errors and 0 warnings

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

* feat: ban `as` type assertions, add runtime schema validation with valibot

Replace all ~170 unsafe `as` type assertions across the entire codebase
(production + tests) with runtime-validated alternatives:

- Add GritQL biome plugin (`no-type-assertion.grit`) that bans all `as`
  casts except `as const`
- Add valibot for schema-validated JSON parsing (`parseJsonWith`)
- Add shared utilities: `parse.ts` (schema parsing), `type-guards.ts`
- Replace `as` casts in all 5 cloud modules (aws, daytona, hetzner,
  digitalocean, fly) with valibot schemas + type guards
- Replace `as` casts in shared modules (manifest, update-check, oauth,
  commands, history, ui)
- Replace `as any` in all 26 test files with proper `new Response()`
  mocks and typed variables
- Add 13 tests for parseJsonWith/parseJsonRaw
- Add "Embrace Bold Changes" culture rule to CLAUDE.md
- Bump version 0.6.19 → 0.7.0

1859 tests pass, 0 lint errors across 95 files, bundle +6KB from valibot.

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

* refactor: move GritQL plugin into cli/lint/ directory

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-22 18:50:53 -08:00
A
63bce1bd04
security: sanitize TERM env var in interactiveSession to prevent shell injection (#1763)
All 6 cloud providers interpolated process.env.TERM directly into shell
commands without validation. A malicious TERM value (e.g., containing
$(cmd)) would execute on the remote server, potentially exfiltrating
OPENROUTER_API_KEY and other credentials.

Add sanitizeTermValue() allowlist (alphanumeric, dots, hyphens, underscores)
to cli/src/shared/ui.ts and apply it in all interactiveSession functions.

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-22 18:11:09 -05:00
A
7e7d4aa3d7
fix: add SSH keepalives, increase cloud-init patience, simplify openclaw launch (#1761)
- Add ServerAliveInterval=15 + ServerAliveCountMax=3 to SSH_OPTS on all
  clouds (DO, Hetzner, AWS, GCP) to prevent silent TCP drops during long
  idle periods (e.g. waiting on slow LLM API calls). Daytona already had
  these.
- Increase DigitalOcean cloud-init fallback poll from 6×5s (30s) to
  20×5s (100s) so full-tier installs (build-essential + bun + node)
  have time to finish when the streaming tail path fails.
- Replace `source ~/.zshrc` with explicit PATH export in openclaw launch
  command to avoid side effects from zshrc inside bash -l.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-22 14:54:35 -08:00
A
fdd6a9b6c3
chore: harden biome lint rules and auto-fix codebase (#1759)
* chore: harden biome lint rules and auto-fix codebase

Add strict biome rules for better TypeScript code quality:
- useBlockStatements: enforce braces on all control flow
- useConst: prefer const over let
- useNodejsImportProtocol: require node: prefix for builtins
- noUnusedImports/Variables: error (warn in tests)
- noExplicitAny: warn in source, off in tests
- noDoubleEquals, noAssignInExpressions, noFallthroughSwitchClause
- useNumberNamespace (Number.isNaN over isNaN)
- noImplicitAnyLet, noInferrableTypes, noUselessElse

Auto-fixed 55 files. Tests relaxed for any/unused patterns.

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

* chore: enable biome formatter with expand: always for brace newlines

Enable biome formatter with:
- expand: "always" — braces on their own lines
- indentStyle: space, indentWidth: 2
- lineWidth: 120
- arrowParentheses: always
- trailingCommas: all
- semicolons: always

82 files reformatted. All 1819 tests pass.

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>
2026-02-22 14:37:47 -08:00
A
f3a2b85b5b
fix: always confirm cloud resource name with user, even when SPAWN_NAME is set (#1758)
When the CLI collects a display name (SPAWN_NAME), each cloud now shows
the kebab-case derivative as the default in the resource name prompt
instead of silently accepting it. Users can hit Enter to accept or type
an override. Non-interactive mode still skips the prompt.

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-22 14:25:34 -08:00
A
7c37a793de
fix: eliminate duplicate name prompts, use cloud-native terminology (#1755)
* fix: eliminate duplicate name prompts, use cloud-native terminology

Users were prompted for a name up to 4 times per spawn. Now each cloud
has a single prompt using its native resource terminology (e.g. "Hetzner
server name", "Fly machine name") and getServerName() returns the
already-collected name silently instead of re-prompting.

Closes #1753

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

* fix: never use bare "spawn" as default name, always append random suffix

Extract defaultSpawnName() helper to shared/ui.ts that generates
"spawn-xxxx" with a random suffix. All cloud modules now use it
instead of bare "spawn" for every fallback path.

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-22 14:20:47 -08:00
A
7b021fb1f5
fix: set TERM and use login shell for interactive SSH sessions (#1754)
SSH interactive sessions ran the agent command in a non-login,
non-interactive shell — .bashrc/.profile weren't sourced and TERM
wasn't always set, making the shell feel broken (no colors, bad
line editing, missing env).

Fix for all 6 SSH-based clouds (DO, Hetzner, AWS, GCP, Fly, Daytona):
- Forward local TERM (default xterm-256color) to the remote
- Use `exec bash -l -c` for a proper login shell

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-22 14:14:13 -08:00
A
c5e2790ea0
fix: symlink bun to /usr/local/bin in cloud-init for all providers (#1752)
After installing bun via curl in cloud-init userdata, bun lives in
~/.bun/bin/bun which isn't on the system PATH. Agent scripts use
#!/usr/bin/env bun and fail with "bun: not found". Symlink it into
/usr/local/bin so it's immediately available system-wide.

Applies to: AWS, DigitalOcean, GCP, Hetzner

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-22 17:09:28 -05:00
A
57d4ee7eeb
fix: drop apt nodejs/npm, install Node 22 directly via n (#1746)
apt-get install nodejs npm pulls in hundreds of node-* packages
(libhwasan, node-jsonify, node-eslint-utils, etc.) adding 60-90s
to cloud-init. We immediately replace it with Node 22 via n anyway.

Fix: bootstrap n directly from curl and install Node 22 in one step.
No apt nodejs/npm needed.

Before: apt install nodejs npm → npm install -g n → n 22 (slow)
After:  curl n | bash -s install 22 (fast, no apt bloat)

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-22 12:40:22 -08:00
A
ac5e8495b1
feat: customize cloud-init per agent to fix boot timeouts (#1733)
Agents declare their dependency tier (minimal/node/bun/full), and
cloud-init only installs what's needed. Lightweight agents like
OpenCode and ZeroClaw skip Node.js upgrade, Bun install, and
build-essential — saving 60-90s on boot and eliminating the
DigitalOcean cloud-init timeout.

- Add CloudInitTier type + cloudInitTier field to AgentConfig
- Add shared/cloud-init.ts: tier-to-packages mapping
- Update all 6 clouds (DO, Hetzner, AWS, GCP, Fly, Daytona)
- Bump CLI version to 0.6.8

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-02-22 11:43:45 -08:00
A
850327c29d
feat: convert aws/ cloud provider from Bash to TypeScript (#1693)
Migrates AWS Lightsail from 609-line bash (aws/lib/common.sh) to TypeScript,
following the established Fly.io/local provider patterns. Type safety eliminates
SigV4 signing bugs, @clack/prompts provides interactive bundle/region pickers,
and error handling is explicit.

- cli/src/aws/aws.ts — Core: AWS CLI wrapper, SigV4 REST API, auth, provisioning, SSH
- cli/src/aws/agents.ts — Agent configs and install helpers
- cli/src/aws/main.ts — Orchestrator
- aws/*.sh — Converted to thin bun shims with bash fallback (curl|bash compatible)
- cli/package.json — Version bump to 0.6.0

Fixes #1675

Agent: complexity-hunter

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-22 07:50:54 -05:00