mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-22 03:14:57 +00:00
fix: Hetzner Claude Code installation + add --debug mode (#1198)
Fixed Hetzner installation issue where curl to claude.ai/install.sh was returning 403 errors. Added fallback to use bun (already installed by cloud-init) to install Claude Code. Also added --debug flag to enable verbose bash output (set -x) for easier troubleshooting. Changes: - hetzner/claude.sh: Use bun fallback installation method - CLI: Added --debug flag support (v0.2.86) - shared/common.sh: Enable set -x when SPAWN_DEBUG=1 Co-authored-by: lab <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6c8ab77bd0
commit
01ed74ba95
6 changed files with 50 additions and 24 deletions
|
|
@ -285,6 +285,7 @@ Without these, the cloud has **no test coverage**, body validation is missing, m
|
|||
- **Major** (X.0.0 → X+1.0.0): Breaking changes
|
||||
- The CLI has auto-update enabled — users get new versions immediately on next run
|
||||
- Version bumps ensure users always have the latest fixes and features
|
||||
- **NEVER commit `cli/cli.js`** — it is a build artifact (already in `.gitignore`). It is produced during releases, not checked into the repo. Do NOT use `git add -f cli/cli.js`.
|
||||
|
||||
## Autonomous Loops
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.2.83",
|
||||
"version": "0.2.86",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -656,7 +656,7 @@ export async function preflightCredentialCheck(manifest: Manifest, cloud: string
|
|||
}
|
||||
}
|
||||
|
||||
export async function cmdRun(agent: string, cloud: string, prompt?: string, dryRun?: boolean): Promise<void> {
|
||||
export async function cmdRun(agent: string, cloud: string, prompt?: string, dryRun?: boolean, debug?: boolean): Promise<void> {
|
||||
const manifest = await loadManifestWithSpinner();
|
||||
({ agent, cloud } = resolveAndLog(manifest, agent, cloud));
|
||||
|
||||
|
|
@ -676,7 +676,7 @@ export async function cmdRun(agent: string, cloud: string, prompt?: string, dryR
|
|||
const suffix = prompt ? " with prompt..." : "...";
|
||||
p.log.step(`Launching ${pc.bold(agentName)} on ${pc.bold(cloudName)}${suffix}`);
|
||||
|
||||
await execScript(cloud, agent, prompt, getAuthHint(manifest, cloud), manifest.clouds[cloud].url);
|
||||
await execScript(cloud, agent, prompt, getAuthHint(manifest, cloud), manifest.clouds[cloud].url, debug);
|
||||
}
|
||||
|
||||
export function getStatusDescription(status: number): string {
|
||||
|
|
@ -1070,10 +1070,10 @@ function handleUserInterrupt(errMsg: string, dashboardUrl?: string): void {
|
|||
process.exit(130);
|
||||
}
|
||||
|
||||
async function runWithRetries(script: string, prompt?: string, dashboardUrl?: string): Promise<string | undefined> {
|
||||
async function runWithRetries(script: string, prompt?: string, dashboardUrl?: string, debug?: boolean): Promise<string | undefined> {
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES + 1; attempt++) {
|
||||
try {
|
||||
await runBash(script, prompt);
|
||||
await runBash(script, prompt, debug);
|
||||
return undefined; // success
|
||||
} catch (err) {
|
||||
const errMsg = getErrorMessage(err);
|
||||
|
|
@ -1092,7 +1092,7 @@ async function runWithRetries(script: string, prompt?: string, dashboardUrl?: st
|
|||
return "Script failed after all retries";
|
||||
}
|
||||
|
||||
async function execScript(cloud: string, agent: string, prompt?: string, authHint?: string, dashboardUrl?: string): Promise<void> {
|
||||
async function execScript(cloud: string, agent: string, prompt?: string, authHint?: string, dashboardUrl?: string, debug?: boolean): Promise<void> {
|
||||
const url = `https://openrouter.ai/labs/spawn/${cloud}/${agent}.sh`;
|
||||
const ghUrl = `${RAW_BASE}/${cloud}/${agent}.sh`;
|
||||
|
||||
|
|
@ -1116,13 +1116,13 @@ async function execScript(cloud: string, agent: string, prompt?: string, authHin
|
|||
// Non-fatal: don't block the spawn if history write fails
|
||||
}
|
||||
|
||||
const lastErr = await runWithRetries(scriptContent, prompt, dashboardUrl);
|
||||
const lastErr = await runWithRetries(scriptContent, prompt, dashboardUrl, debug);
|
||||
if (lastErr) {
|
||||
reportScriptFailure(lastErr, cloud, agent, authHint, prompt, dashboardUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function runBash(script: string, prompt?: string): Promise<void> {
|
||||
function runBash(script: string, prompt?: string, debug?: boolean): Promise<void> {
|
||||
// SECURITY: Validate script content before execution
|
||||
validateScriptContent(script);
|
||||
|
||||
|
|
@ -1132,6 +1132,9 @@ function runBash(script: string, prompt?: string): Promise<void> {
|
|||
env.SPAWN_PROMPT = prompt;
|
||||
env.SPAWN_MODE = "non-interactive";
|
||||
}
|
||||
if (debug) {
|
||||
env.SPAWN_DEBUG = "1";
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const child = spawn("bash", ["-c", script], {
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ const KNOWN_FLAGS = new Set([
|
|||
"--version", "-v", "-V",
|
||||
"--prompt", "-p", "--prompt-file", "-f",
|
||||
"--dry-run", "-n",
|
||||
"--debug",
|
||||
"-a", "-c", "--agent", "--cloud",
|
||||
"--clear",
|
||||
]);
|
||||
|
|
@ -93,6 +94,7 @@ function checkUnknownFlags(args: string[]): void {
|
|||
console.error(` ${pc.cyan("--prompt, -p")} Provide a prompt for non-interactive execution`);
|
||||
console.error(` ${pc.cyan("--prompt-file, -f")} Read prompt from a file`);
|
||||
console.error(` ${pc.cyan("--dry-run, -n")} Preview what would be provisioned`);
|
||||
console.error(` ${pc.cyan("--debug")} Show all commands being executed`);
|
||||
console.error(` ${pc.cyan("--help, -h")} Show help information`);
|
||||
console.error(` ${pc.cyan("--version, -v")} Show version`);
|
||||
console.error();
|
||||
|
|
@ -143,13 +145,13 @@ async function showInfoOrError(name: string): Promise<void> {
|
|||
showUnknownCommandError(name, manifest);
|
||||
}
|
||||
|
||||
async function handleDefaultCommand(agent: string, cloud: string | undefined, prompt?: string, dryRun?: boolean): Promise<void> {
|
||||
async function handleDefaultCommand(agent: string, cloud: string | undefined, prompt?: string, dryRun?: boolean, debug?: boolean): Promise<void> {
|
||||
if (cloud && HELP_FLAGS.includes(cloud)) {
|
||||
await showInfoOrError(agent);
|
||||
return;
|
||||
}
|
||||
if (cloud) {
|
||||
await cmdRun(agent, cloud, prompt, dryRun);
|
||||
await cmdRun(agent, cloud, prompt, dryRun, debug);
|
||||
return;
|
||||
}
|
||||
if (dryRun) {
|
||||
|
|
@ -428,11 +430,11 @@ async function dispatchSubcommand(cmd: string, filteredArgs: string[]): Promise<
|
|||
}
|
||||
|
||||
/** Handle verb aliases like "spawn run claude sprite" -> "spawn claude sprite" */
|
||||
async function dispatchVerbAlias(cmd: string, filteredArgs: string[], prompt: string | undefined, dryRun: boolean): Promise<void> {
|
||||
async function dispatchVerbAlias(cmd: string, filteredArgs: string[], prompt: string | undefined, dryRun: boolean, debug: boolean): Promise<void> {
|
||||
if (filteredArgs.length > 1) {
|
||||
const remaining = filteredArgs.slice(1);
|
||||
warnExtraArgs(remaining, 2);
|
||||
await handleDefaultCommand(remaining[0], remaining[1], prompt, dryRun);
|
||||
await handleDefaultCommand(remaining[0], remaining[1], prompt, dryRun, debug);
|
||||
return;
|
||||
}
|
||||
console.error(pc.red(`Error: ${pc.bold(cmd)} requires an agent and cloud`));
|
||||
|
|
@ -442,19 +444,19 @@ async function dispatchVerbAlias(cmd: string, filteredArgs: string[], prompt: st
|
|||
}
|
||||
|
||||
/** Handle slash notation: "spawn claude/hetzner" -> "spawn claude hetzner" */
|
||||
async function dispatchSlashNotation(cmd: string, prompt: string | undefined, dryRun: boolean): Promise<boolean> {
|
||||
async function dispatchSlashNotation(cmd: string, prompt: string | undefined, dryRun: boolean, debug: boolean): Promise<boolean> {
|
||||
const parts = cmd.split("/");
|
||||
if (parts.length === 2 && parts[0] && parts[1]) {
|
||||
console.error(pc.dim(`Tip: use a space instead of slash: ${pc.cyan(`spawn ${parts[0]} ${parts[1]}`)}`));
|
||||
console.error();
|
||||
await handleDefaultCommand(parts[0], parts[1], prompt, dryRun);
|
||||
await handleDefaultCommand(parts[0], parts[1], prompt, dryRun, debug);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Dispatch a named command or fall through to agent/cloud handling */
|
||||
async function dispatchCommand(cmd: string, filteredArgs: string[], prompt: string | undefined, dryRun: boolean): Promise<void> {
|
||||
async function dispatchCommand(cmd: string, filteredArgs: string[], prompt: string | undefined, dryRun: boolean, debug: boolean): Promise<void> {
|
||||
if (IMMEDIATE_COMMANDS[cmd]) {
|
||||
warnExtraArgs(filteredArgs, 1);
|
||||
IMMEDIATE_COMMANDS[cmd]();
|
||||
|
|
@ -463,14 +465,14 @@ async function dispatchCommand(cmd: string, filteredArgs: string[], prompt: stri
|
|||
|
||||
if (LIST_COMMANDS.has(cmd)) { await dispatchListCommand(filteredArgs); return; }
|
||||
if (SUBCOMMANDS[cmd]) { await dispatchSubcommand(cmd, filteredArgs); return; }
|
||||
if (VERB_ALIASES.has(cmd)) { await dispatchVerbAlias(cmd, filteredArgs, prompt, dryRun); return; }
|
||||
if (VERB_ALIASES.has(cmd)) { await dispatchVerbAlias(cmd, filteredArgs, prompt, dryRun, debug); return; }
|
||||
|
||||
if (filteredArgs.length === 1 && cmd.includes("/")) {
|
||||
if (await dispatchSlashNotation(cmd, prompt, dryRun)) return;
|
||||
if (await dispatchSlashNotation(cmd, prompt, dryRun, debug)) return;
|
||||
}
|
||||
|
||||
warnExtraArgs(filteredArgs, 2);
|
||||
await handleDefaultCommand(filteredArgs[0], filteredArgs[1], prompt, dryRun);
|
||||
await handleDefaultCommand(filteredArgs[0], filteredArgs[1], prompt, dryRun, debug);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
|
|
@ -485,6 +487,11 @@ async function main(): Promise<void> {
|
|||
const dryRun = dryRunIdx !== -1;
|
||||
if (dryRun) filteredArgs.splice(dryRunIdx, 1);
|
||||
|
||||
// Extract --debug boolean flag
|
||||
const debugIdx = filteredArgs.findIndex(a => a === "--debug");
|
||||
const debug = debugIdx !== -1;
|
||||
if (debug) filteredArgs.splice(debugIdx, 1);
|
||||
|
||||
checkUnknownFlags(filteredArgs);
|
||||
|
||||
const cmd = filteredArgs[0];
|
||||
|
|
@ -493,7 +500,7 @@ async function main(): Promise<void> {
|
|||
if (!cmd) {
|
||||
await handleNoCommand(prompt, dryRun);
|
||||
} else {
|
||||
await dispatchCommand(cmd, filteredArgs, prompt, dryRun);
|
||||
await dispatchCommand(cmd, filteredArgs, prompt, dryRun, debug);
|
||||
}
|
||||
} catch (err) {
|
||||
handleError(err);
|
||||
|
|
|
|||
|
|
@ -27,15 +27,18 @@ create_server "${SERVER_NAME}"
|
|||
verify_server_connectivity "${HETZNER_SERVER_IP}"
|
||||
wait_for_cloud_init "${HETZNER_SERVER_IP}" 60
|
||||
|
||||
# 5. Verify Claude Code is installed (fallback to manual install)
|
||||
# 5. Verify Claude Code is installed (try curl first, then bun fallback)
|
||||
log_step "Verifying Claude Code installation..."
|
||||
if ! run_server "${HETZNER_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
|
||||
log_step "Claude Code not found, installing manually..."
|
||||
run_server "${HETZNER_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"
|
||||
if ! run_server "${HETZNER_SERVER_IP}" "export PATH=\$HOME/.claude/local/bin:\$HOME/.local/bin:\$PATH && command -v claude" >/dev/null 2>&1; then
|
||||
log_step "Claude Code not found, installing..."
|
||||
if ! run_server "${HETZNER_SERVER_IP}" "curl -fsSL https://claude.ai/install.sh | bash"; then
|
||||
log_warn "curl install failed, falling back to bun..."
|
||||
run_server "${HETZNER_SERVER_IP}" "export PATH=\$HOME/.bun/bin:\$HOME/.local/bin:\$PATH && bun add -g @anthropic-ai/claude-code && claude install"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Verify installation succeeded
|
||||
if ! run_server "${HETZNER_SERVER_IP}" "export PATH=\$HOME/.local/bin:\$PATH && command -v claude &> /dev/null && claude --version &> /dev/null"; then
|
||||
if ! run_server "${HETZNER_SERVER_IP}" "export PATH=\$HOME/.claude/local/bin:\$HOME/.local/bin:\$PATH && command -v claude &> /dev/null && claude --version &> /dev/null"; then
|
||||
log_install_failed "Claude Code" "curl -fsSL https://claude.ai/install.sh | bash" "${HETZNER_SERVER_IP}"
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -7,6 +7,15 @@
|
|||
# It does not set bash flags (like set -eo pipefail) as those should be set
|
||||
# by the scripts that source this file.
|
||||
|
||||
# ============================================================
|
||||
# Debug mode
|
||||
# ============================================================
|
||||
|
||||
# Enable debug output if SPAWN_DEBUG is set
|
||||
if [[ -n "${SPAWN_DEBUG:-}" ]]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
# ============================================================
|
||||
# Color definitions and logging
|
||||
# ============================================================
|
||||
|
|
@ -1759,6 +1768,9 @@ wait_for_cloud_init() {
|
|||
ssh_run_server() {
|
||||
local ip="${1}"
|
||||
local cmd="${2}"
|
||||
if [[ -n "${SPAWN_DEBUG:-}" ]]; then
|
||||
cmd="set -x; ${cmd}"
|
||||
fi
|
||||
# shellcheck disable=SC2086
|
||||
ssh $SSH_OPTS "${SSH_USER:-root}@${ip}" -- "${cmd}"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue