diff --git a/cli/package.json b/cli/package.json index 275aed4c..6e13682b 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.6.12", + "version": "0.6.13", "type": "module", "bin": { "spawn": "cli.js" diff --git a/cli/src/aws/aws.ts b/cli/src/aws/aws.ts index 3e5bda6d..fd7a127e 100644 --- a/cli/src/aws/aws.ts +++ b/cli/src/aws/aws.ts @@ -15,7 +15,7 @@ import { toKebabCase, } from "../shared/ui"; import type { CloudInitTier } from "../shared/agents"; -import { getPackagesForTier, needsNodeUpgrade, needsBun } from "../shared/cloud-init"; +import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init"; const DASHBOARD_URL = "https://lightsail.aws.amazon.com/"; @@ -461,10 +461,10 @@ function getCloudInitUserdata(tier: CloudInitTier = "full"): string { "apt-get update -y", `apt-get install -y --no-install-recommends ${packages.join(" ")}`, ]; - if (needsNodeUpgrade(tier)) { + if (needsNode(tier)) { lines.push( - "# Upgrade Node.js to v22 LTS", - "npm install -g n && n 22 && ln -sf /usr/local/bin/node /usr/bin/node && ln -sf /usr/local/bin/npm /usr/bin/npm && ln -sf /usr/local/bin/npx /usr/bin/npx", + "# Install Node.js 22 via n", + `su - ubuntu -c '${NODE_INSTALL_CMD}'`, "# Install Claude Code", "su - ubuntu -c 'curl -fsSL https://claude.ai/install.sh | bash'", "# Configure npm global prefix", diff --git a/cli/src/daytona/daytona.ts b/cli/src/daytona/daytona.ts index ca16a1dc..a3dc2f18 100644 --- a/cli/src/daytona/daytona.ts +++ b/cli/src/daytona/daytona.ts @@ -12,7 +12,7 @@ import { toKebabCase, } from "../shared/ui"; import type { CloudInitTier } from "../shared/agents"; -import { getPackagesForTier, needsNodeUpgrade, needsBun } from "../shared/cloud-init"; +import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init"; const DAYTONA_API_BASE = "https://app.daytona.io/api"; const DAYTONA_DASHBOARD_URL = "https://app.daytona.io/"; @@ -484,13 +484,8 @@ export async function waitForCloudInit(tier: CloudInitTier = "full"): Promise/dev/null 2>&1; then curl -fsSL https://bun.sh/install | bash; fi'); diff --git a/cli/src/fly/fly.ts b/cli/src/fly/fly.ts index 0e0adaf6..82f94610 100644 --- a/cli/src/fly/fly.ts +++ b/cli/src/fly/fly.ts @@ -14,7 +14,7 @@ import { toKebabCase, } from "../shared/ui"; import type { CloudInitTier } from "../shared/agents"; -import { getPackagesForTier, needsNodeUpgrade, needsBun } from "../shared/cloud-init"; +import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init"; const FLY_API_BASE = "https://api.machines.dev/v1"; const FLY_DASHBOARD_URL = "https://fly.io/dashboard"; @@ -874,9 +874,9 @@ export async function waitForCloudInit(tier: CloudInitTier = "full"): Promise Installing base packages..."`, `export DEBIAN_FRONTEND=noninteractive`, `apt-get update -y && apt-get install -y --no-install-recommends ${packages.join(" ")} || true`, - ...(needsNodeUpgrade(tier) ? [ - `echo "==> Upgrading Node.js to v22 LTS..."`, - `npm install -g n && n 22 && ln -sf /usr/local/bin/node /usr/bin/node && ln -sf /usr/local/bin/npm /usr/bin/npm && ln -sf /usr/local/bin/npx /usr/bin/npx || true`, + ...(needsNode(tier) ? [ + `echo "==> Installing Node.js 22..."`, + `${NODE_INSTALL_CMD} || true`, ] : []), ...(needsBun(tier) ? [ `echo "==> Checking bun..."`, diff --git a/cli/src/gcp/gcp.ts b/cli/src/gcp/gcp.ts index b531cc2f..5b2f4de1 100644 --- a/cli/src/gcp/gcp.ts +++ b/cli/src/gcp/gcp.ts @@ -13,7 +13,7 @@ import { toKebabCase, } from "../shared/ui"; import type { CloudInitTier } from "../shared/agents"; -import { getPackagesForTier, needsNodeUpgrade, needsBun } from "../shared/cloud-init"; +import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init"; const DASHBOARD_URL = "https://console.cloud.google.com/compute/instances"; @@ -435,10 +435,10 @@ function getStartupScript(username: string, tier: CloudInitTier = "full"): strin "apt-get update -y", `apt-get install -y --no-install-recommends ${packages.join(" ")}`, ]; - if (needsNodeUpgrade(tier)) { + if (needsNode(tier)) { lines.push( - "# Upgrade Node.js to v22 LTS", - "npm install -g n && n 22 && ln -sf /usr/local/bin/node /usr/bin/node && ln -sf /usr/local/bin/npm /usr/bin/npm && ln -sf /usr/local/bin/npx /usr/bin/npx", + "# Install Node.js 22 via n", + `su - "${username}" -c '${NODE_INSTALL_CMD}'`, `# Install Claude Code as the login user`, `su - "${username}" -c 'curl -fsSL https://claude.ai/install.sh | bash' || true`, "# Configure npm global prefix", diff --git a/cli/src/hetzner/hetzner.ts b/cli/src/hetzner/hetzner.ts index 75f9a62e..78198b4f 100644 --- a/cli/src/hetzner/hetzner.ts +++ b/cli/src/hetzner/hetzner.ts @@ -13,7 +13,7 @@ import { toKebabCase, } from "../shared/ui"; import type { CloudInitTier } from "../shared/agents"; -import { getPackagesForTier, needsNodeUpgrade, needsBun } from "../shared/cloud-init"; +import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init"; const HETZNER_API_BASE = "https://api.hetzner.cloud/v1"; const HETZNER_DASHBOARD_URL = "https://console.hetzner.cloud/"; @@ -295,8 +295,8 @@ function getCloudInitUserdata(tier: CloudInitTier = "full"): string { "apt-get update -y", `apt-get install -y --no-install-recommends ${packages.join(" ")}`, ]; - if (needsNodeUpgrade(tier)) { - lines.push("npm install -g n && n 22 && ln -sf /usr/local/bin/node /usr/bin/node && ln -sf /usr/local/bin/npm /usr/bin/npm && ln -sf /usr/local/bin/npx /usr/bin/npx || true"); + if (needsNode(tier)) { + lines.push(`${NODE_INSTALL_CMD} || true`); } if (needsBun(tier)) { lines.push('curl -fsSL https://bun.sh/install | bash || true'); diff --git a/cli/src/shared/agent-setup.ts b/cli/src/shared/agent-setup.ts index 645207e5..f02ecddb 100644 --- a/cli/src/shared/agent-setup.ts +++ b/cli/src/shared/agent-setup.ts @@ -82,7 +82,7 @@ export async function installClaudeCode(runner: CloudRunner): Promise { `curl -fsSL https://claude.ai/install.sh | bash || true`, `export PATH="${claudePath}:$PATH"`, `if command -v claude >/dev/null 2>&1; then ${finalize}; exit 0; fi`, - `if ! command -v node >/dev/null 2>&1; then apt-get update -y && apt-get install -y --no-install-recommends nodejs npm && npm install -g n && n 22 && ln -sf /usr/local/bin/node /usr/bin/node && ln -sf /usr/local/bin/npm /usr/bin/npm && ln -sf /usr/local/bin/npx /usr/bin/npx || true; fi`, + `if ! command -v node >/dev/null 2>&1; then curl -fsSL https://raw.githubusercontent.com/tj/n/master/bin/n | bash -s install 22 || true; fi`, `echo "==> Installing Claude Code (method 2/2: npm)..."`, `npm install -g @anthropic-ai/claude-code || true`, `export PATH="${claudePath}:$PATH"`, diff --git a/cli/src/shared/cloud-init.ts b/cli/src/shared/cloud-init.ts index 7cf6e448..5371e4a9 100644 --- a/cli/src/shared/cloud-init.ts +++ b/cli/src/shared/cloud-init.ts @@ -7,13 +7,16 @@ const MINIMAL = ["curl", "unzip", "git", "ca-certificates"]; export function getPackagesForTier(tier: CloudInitTier = "full"): string[] { switch (tier) { case "minimal": return [...MINIMAL]; - case "node": return [...MINIMAL, "zsh", "nodejs", "npm", "build-essential"]; + case "node": return [...MINIMAL, "zsh", "build-essential"]; case "bun": return [...MINIMAL, "zsh"]; - case "full": return [...MINIMAL, "zsh", "nodejs", "npm", "build-essential"]; + case "full": return [...MINIMAL, "zsh", "build-essential"]; } } -export function needsNodeUpgrade(tier: CloudInitTier = "full"): boolean { +/** Node 22 install via `n` bootstrapped directly from curl (no apt nodejs/npm). */ +export const NODE_INSTALL_CMD = 'curl -fsSL https://raw.githubusercontent.com/tj/n/master/bin/n | bash -s install 22'; + +export function needsNode(tier: CloudInitTier = "full"): boolean { return tier === "node" || tier === "full"; }