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>
This commit is contained in:
A 2026-02-22 12:40:22 -08:00 committed by GitHub
parent 0a8e6a30ad
commit 57d4ee7eeb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 29 additions and 31 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.6.12",
"version": "0.6.13",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -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",

View file

@ -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<vo
`apt-get update -y`,
`apt-get install -y --no-install-recommends ${packages.join(" ")}`,
];
if (needsNodeUpgrade(tier)) {
parts.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`,
);
if (needsNode(tier)) {
parts.push(NODE_INSTALL_CMD);
}
if (needsBun(tier)) {
parts.push(`curl -fsSL https://bun.sh/install | bash`);

View file

@ -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 DO_API_BASE = "https://api.digitalocean.com/v2";
const DO_DASHBOARD_URL = "https://cloud.digitalocean.com/droplets";
@ -624,8 +624,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('if ! command -v bun >/dev/null 2>&1; then curl -fsSL https://bun.sh/install | bash; fi');

View file

@ -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<vo
`echo "==> 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..."`,

View file

@ -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",

View file

@ -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');

View file

@ -82,7 +82,7 @@ export async function installClaudeCode(runner: CloudRunner): Promise<void> {
`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"`,

View file

@ -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";
}