Fixes#1801 - chmod 600 on gh hosts.yml after token login
Fixes#1798 - remove 2>&1 from bun install curl across agent scripts
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Fixes#1797 - heredoc for curl POST body in request_missing_cloud_keys
Fixes#1799 - strip whitespace and reject newlines/tabs in _try_load_env_var
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: reduce SSH interactive lag with GSSAPIAuthentication=no and TCPKeepAlive=no
GSSAPIAuthentication causes latency on every SSH interaction when
the server doesn't support Kerberos (i.e. always for our VMs).
TCPKeepAlive is redundant with ServerAliveInterval and can cause
retransmission issues through NAT/firewalls.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use SSH_INTERACTIVE_OPTS for all interactive sessions
The reconnect (cmdConnect) and agent launch (cmdEnterAgent) paths
were using bare SSH with only StrictHostKeyChecking, missing all
performance flags. Now they use SSH_INTERACTIVE_OPTS which includes:
- GSSAPIAuthentication=no (skip Kerberos timeout)
- TCPKeepAlive=no (avoid NAT retransmission issues)
- ServerAliveInterval=15 (encrypted keepalives)
- Compression=yes (reduce latency on slow/distant links)
- IPQoS=lowdelay (mark packets for low-latency treatment)
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>
Add `lint/no-typeof-string-number.grit` plugin that bans raw
`typeof x === "string"` and `typeof x === "number"` checks. All
occurrences replaced with `isString(x)` / `isNumber(x)` from
`shared/type-guards.ts`.
This makes narrowing patterns consistent and scannable — every
type check uses the same vocabulary project-wide.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
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>
* docs: enforce PR workflow for ALL changes, no exceptions
Rewrites the "Draft PR First" section to be unambiguous:
- Every file edit (including CLAUDE.md itself) requires a PR
- Explicit list of change types that are NOT exempt
- Step-by-step workflow: branch → change → commit → draft PR → merge
- Finished PRs must be converted from draft and merged immediately
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* remove unnecessary "don't commit to main" warning
Branch protection already prevents direct commits — no need
to restate it as a rule.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: add branch-check-first workflow and stash recovery
Agents must check their branch before editing files. If on main,
branch first. If they already have uncommitted changes on main,
stash → branch → unstash.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add PreToolUse hook to block edits on main branch
- Adds a PreToolUse hook that exits 2 (blocks) any Write/Edit when
the current branch is main, with a clear error message telling the
agent to create a branch first
- Updates CLAUDE.md to reference the hook and use cherry-pick
(not stash) for recovering commits made on main by mistake
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>
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>
After provisioning, @clack/prompts and readline leave stdin with stale
listeners, raw mode, and buffered input. This causes flaky keyboard input
in the interactive SSH session. Add prepareStdinForHandoff() that closes
the shared readline, removes all stdin listeners, resets raw mode, and
pauses stdin before launching the child process.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
wrapSshCall (agent-setup.ts) and spriteRetry (sprite.ts) used `instanceof
Error` to extract error messages — an anti-pattern explicitly avoided
throughout the rest of the codebase (consistent with comments in index.ts,
commands.ts, manifest.ts, etc.). When errors cross module or bundling
boundaries, instanceof returns false even for real Error objects, causing
err.message to fall back to String(err) and producing `[object Object]` in
the retry logs. Uses `hasMessage()` from shared/type-guards for consistent
duck-typed narrowing.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
DigitalOcean SSH was failing with "Permission denied (publickey)" because
the SSH client was not explicitly told which identity file to use. When
users have multiple SSH keys or an SSH agent with different keys loaded,
SSH may try the wrong key first and fail — especially with BatchMode=yes
which suppresses interactive fallbacks.
The fix adds `-i ~/.ssh/id_ed25519` to SSH_OPTS (matching AWS's approach)
and passes sshKeyPath to the shared waitForSsh utility, ensuring the
correct key is always used for both the handshake wait and all subsequent
SSH/SCP commands.
Fixes#1783
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
openBrowser() never checked the exitCode from Bun.spawnSync, so it silently
returned success even when the browser command failed (headless VMs, no
DISPLAY). Now checks exitCode and always shows the URL as fallback.
selectFromList() uses @clack/prompts which creates/destroys its own readline
on stdin. After it finishes, the shared readline in ui.ts can be corrupted
(Bun #1707). Now explicitly closes and nulls the shared readline after
@clack/prompts returns so the next prompt() call gets a fresh one.
Fixes#1770
Agent: ux-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Daytona was writing raw sshHost/sshToken as ip/user in last-connection.json.
history.ts:mergeLastConnection() calls validateUsername() on the user field,
rejecting SSH tokens (>32 chars) and deleting the connection file. This meant
spawn list/delete/resume never showed Daytona sandboxes.
Replace with the "daytona-sandbox" sentinel (already in CONNECTION_SENTINELS
in security.ts:31 and checked by all relevant handlers in commands.ts) — the
same pattern Fly.io and Sprite use for their provider-managed SSH.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Replace inline $(...) in sed replacement string with intermediate variable
to eliminate shell expansion risk. Fixes#1767.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Entries in `spawn ls` and `spawn delete` now display as two lines:
- Line 1: spawn name (bold)
- Line 2: Agent · Cloud · relative time
Removes SSH connection info and prompt previews from the list display
to keep it clean and scannable.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The `installClaudeCode()` SSH step already handles Node.js and Claude Code
installation with retries and fallbacks, making the cloud-init Node/npm
install redundant. Switch to "minimal" so cloud-init only installs
curl/unzip/git/ca-certificates — finishing faster and eliminating the
duplicate install path.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
macOS ships a /usr/bin/git shim that triggers a ~1.5GB Xcode CLT download
when invoked. The install script's `command -v git` check was fooled by
this shim, causing the script to hang or silently fail on fresh macOS.
Removes the git clone path entirely — the curl-based download is fast,
reliable, and has zero external dependencies beyond curl and bun.
Closes#1768
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: Result monad for retry logic — prevent duplicate server creation
SSH exit 255 after an interactive session caused runWithRetries to retry
the entire bash script, creating duplicate servers. The old withRetry
also blindly retried all errors including timeouts where the remote
command may have already completed.
Introduces a Result<T> monad (Ok/Err) so callers explicitly signal
whether a failure is retryable (return Err) or fatal (throw). Adds
wrapSshCall() that classifies SSH errors: transient connection failures
are retryable, timeouts are not. Removes retry loop from the top-level
script runner entirely since it spans server creation + interactive
session.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs: mandate draft-PR-first workflow for all changes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add biome lint to CI and pre-commit hook, fix lint violations
- Add Biome lint job to .github/workflows/lint.yml
- Add TypeScript lint check to .githooks/pre-commit
- Fix useBlockStatements violations in ui.ts and tests
- Add biome lint to CLAUDE.md "After Each Change" checklist
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: rename Result.value to Result.data
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: clean up stale pre-commit hook
- Remove dead check for deleted functions (write_oauth_response_file,
create_oauth_response_html) — they no longer exist in the codebase
- Fix early exit skipping Biome lint when no .sh files are staged
- Replace echo -e with printf (the hook was using the pattern it bans)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve biome lint errors blocking CI
- Fix useImportType: import { type Result } → import type { Result }
- Fix noUnusedImports: remove unused KNOWN_FLAGS import
- Fix noUnusedTemplateLiteral: template literal → string literal
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>
Long labels (e.g. "Claude Code on GCP Compute Engine -- spawn-trial-000-ahmed")
wrap to multiple rows, but the redraw logic uses a fixed line count to cursor-up.
This causes old content to pile up on every arrow-key press.
Query terminal width via `stty size` and truncate all lines to fit within
a single row, with a 1-char margin to prevent auto-wrap edge cases.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
discovery.sh used python3 for manifest parsing and string replacement,
violating the CLAUDE.md rule to use jq/bun instead. Replace all three
python3 call sites with jq equivalents and sed.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Pressing `d` in the server picker now shows a sub-menu:
- Destroy server: hard delete (destroys cloud VM + marks deleted)
- Remove from history: soft delete (removes entry, no cloud API call)
- Cancel: go back to picker
Also adds `kill` as an alias for `spawn delete`.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Agent installation, config upload, env setup, and agent configuration
can all fail transiently due to network flakiness or SSH instability
on fresh VMs. Add a shared withRetry() helper and wrap these operations
with 2-attempt retries to improve reliability without over-engineering.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Both `spawn list` and `spawn delete` now share a single interactive
picker (`activeServerPicker`) backed by `getActiveServers()`. Pressing
`d` in the picker triggers inline delete-and-refresh without leaving
the list. Failed deletions now mark entries as deleted so users aren't
stuck with phantom servers they can't clear.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All 6 other cloud providers (Fly, Hetzner, DigitalOcean, AWS, Sprite, Daytona)
validate remotePath with an allowlist regex before passing it to scp. GCP's
uploadFile had no validation at all, breaking the defense-in-depth pattern.
Adds the same allowlist check (^[a-zA-Z0-9/_.~$-]+$) plus dotdot check.
The regex includes $ to allow $HOME prefix paths used by agent-setup.ts.
Agent: code-health
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
KNOWN_FLAGS in unknown-flags.test.ts was copy-pasted from index.ts and
was missing the --name flag, causing silent test gaps. Extract
KNOWN_FLAGS, findUnknownFlag, and expandEqualsFlags into a new flags.ts
module so tests import the real source of truth.
- Create cli/src/flags.ts with KNOWN_FLAGS, findUnknownFlag, expandEqualsFlags
- Update index.ts to import from flags.ts (checkUnknownFlags now uses findUnknownFlag)
- Update unknown-flags.test.ts to import from flags.ts instead of copy-pasting
- Add tests for --name flag, KNOWN_FLAGS completeness, and expandEqualsFlags
- Bump CLI version to 0.6.15
Fixes#1744
Agent: test-engineer
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- 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>
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>
* 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>
clone_cli() now uses rm -rf "${canonical_repo}" (the resolved real
path) instead of "${repo_dir}" for safer cleanup. Test assertion
updated to match.
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Replace blocklist filename validation with a strict allowlist regex
(^[a-zA-Z0-9_-]+\.ts$) to prevent path traversal via encoding tricks
in GitHub API responses (#1749).
Use pwd -P for symlink-resolving canonicalization and delete via the
canonical path instead of the original variable to close the TOCTOU
gap in cleanup logic (#1750).
Agent: security-auditor
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
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>
* fix: add ~/.bun/bin to shell rc files so spawn finds bun after install
The install script was only adding ~/.local/bin to shell profile files
(bashrc/zshrc/bash_profile), but not ~/.bun/bin. Since the spawn binary
uses #!/usr/bin/env bun as its shebang, bun must be in PATH for spawn
to work. After exec $SHELL, only dirs in rc files are available.
Now ensure_in_path() patches shell rc files for both ~/.local/bin (for
spawn) and ~/.bun/bin (for bun), and correctly checks both when deciding
whether to show "Run spawn" vs "exec $SHELL" instructions.
Fixes#1747
Agent: code-health
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: quote dir in fish_add_path to prevent command injection
Address security review feedback on PR #1748 — unquoted ${dir} in
fish command string could allow injection if HOME/BUN_INSTALL env
vars contain metacharacters.
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.5 <noreply@anthropic.com>
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>
spawn uses #!/usr/bin/env bun shebang, so bun must be in PATH for it
to run. When ~/.local/bin isn't in PATH and we symlink spawn into
/usr/local/bin, bun (typically at ~/.bun/bin/bun) isn't reachable,
causing "bun: not found" on first run. Now symlink bun too.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: add build-essential to node cloud-init tier
The "node" tier (used by claude, codex, kilocode) was missing
build-essential. Native npm packages that compile C/C++ addons
fail without it. The "full" tier had it but no agent uses "full".
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: upgrade openclaw to full cloud-init tier
Openclaw needs the most dependencies (build-essential, nodejs, npm,
bun) but was on the "bun" tier which only installed curl/unzip/git/zsh.
Switch to "full" which includes everything.
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>
`spawn delete` was broken for all non-Fly clouds (hetzner, digitalocean,
gcp, aws, daytona, sprite) because buildDeleteScript sourced
{cloud}/lib/common.sh files that were removed in the TypeScript rewrite.
Fix by removing buildDeleteScript entirely and rewriting execDeleteServer
to call each cloud's TypeScript destroyServer/destroyInstance directly,
following the same pattern already used for Fly.io. All clouds now use
their native TS module for auth + deletion with no shell dependency.
Fixes#1729
Agent: issue-fixer
Signed-off-by: L <6723574+louisgv@users.noreply.github.com>
Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: skip /usr/local/bin symlink when ~/.local/bin is already in PATH
Only symlink when actually needed, and never prompt for a password:
- If ~/.local/bin is in PATH, skip symlink (not needed)
- If not, try direct write to /usr/local/bin first
- Fall back to sudo only if passwordless (NOPASSWD, cached creds,
or macOS Touch ID via pam_tid)
- Otherwise patch rc files and show `exec $SHELL`
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: prompt for password as last resort when symlinking spawn
Symlink priority: direct write → passwordless sudo (NOPASSWD/Touch ID)
→ prompt for password → fall back to exec $SHELL.
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>
* fix: make spawn immediately available after install on fresh machines
The install script modified PATH internally to find bun, then checked
that same modified PATH to decide where to install and whether to warn.
On a fresh machine, ~/.local/bin wasn't in the user's real PATH, but
the script thought it was — so spawn was installed there with no
warning, and `spawn` was not found after install.
Fix: always install to ~/.local/bin, then symlink into /usr/local/bin
(which is universally in PATH). Also patch shell rc files for future
sessions. If symlinking fails, fall back to showing `exec $SHELL`.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: update install.sh tests for new symlink-based install
Remove find_install_dir tests (function removed), update ensure_in_path
tests for symlink + rc-patching behavior, fix section header reference.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: replace [[ ]] with case for macOS bash 3.x compatibility
Addresses security review: [[ ... == */pattern ]] is bash 4.x syntax
that fails on macOS's default bash 3.2. Use POSIX case statements.
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>
DigitalOcean's cloud-init environment doesn't set HOME. Combined
with set -e, any $HOME or ~ reference (bun install, .bashrc writes)
fails with "HOME: unbound variable" and cloud-init silently aborts.
Fixed in both DigitalOcean and Hetzner (same pattern). AWS doesn't
use set -e so is unaffected.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
.join("; ") produced invalid bash: &; after background command,
do; after for, then; after if. Use newline-joined string instead.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace 60×5s blind poll loop ("Cloud-init in progress N/60") with
real-time streaming of /var/log/cloud-init-output.log via tail -f
over SSH. Users now see every apt-get, curl, and error as it happens.
Background checker exits as soon as .cloud-init-complete marker
appears. 5min timeout. Brief 30s fallback poll if streaming fails.
Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>