fix(loader): add startup checks for Node version and git availability (#2463)
Closes #2461
This commit is contained in:
parent
43aca75b98
commit
bf54012d1f
4 changed files with 99 additions and 1 deletions
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
engine-strict=true
|
||||||
|
|
@ -30,6 +30,46 @@ if (firstArg === '--help' || firstArg === '-h') {
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Runtime dependency checks — fail fast with clear diagnostics before any
|
||||||
|
// heavy imports. Reads minimum Node version from the engines field in
|
||||||
|
// package.json (already parsed above) and verifies git is available.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
{
|
||||||
|
const MIN_NODE_MAJOR = 22
|
||||||
|
const red = '\x1b[31m'
|
||||||
|
const bold = '\x1b[1m'
|
||||||
|
const dim = '\x1b[2m'
|
||||||
|
const reset = '\x1b[0m'
|
||||||
|
|
||||||
|
// -- Node version --
|
||||||
|
const nodeMajor = parseInt(process.versions.node.split('.')[0], 10)
|
||||||
|
if (nodeMajor < MIN_NODE_MAJOR) {
|
||||||
|
process.stderr.write(
|
||||||
|
`\n${red}${bold}Error:${reset} GSD requires Node.js >= ${MIN_NODE_MAJOR}.0.0\n` +
|
||||||
|
` You are running Node.js ${process.versions.node}\n\n` +
|
||||||
|
`${dim}Install a supported version:${reset}\n` +
|
||||||
|
` nvm install ${MIN_NODE_MAJOR} ${dim}# if using nvm${reset}\n` +
|
||||||
|
` fnm install ${MIN_NODE_MAJOR} ${dim}# if using fnm${reset}\n` +
|
||||||
|
` brew install node@${MIN_NODE_MAJOR} ${dim}# macOS Homebrew${reset}\n\n`
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- git --
|
||||||
|
try {
|
||||||
|
const { execFileSync } = await import('child_process')
|
||||||
|
execFileSync('git', ['--version'], { stdio: 'ignore' })
|
||||||
|
} catch {
|
||||||
|
process.stderr.write(
|
||||||
|
`\n${red}${bold}Error:${reset} GSD requires git but it was not found on PATH.\n\n` +
|
||||||
|
`${dim}Install git:${reset}\n` +
|
||||||
|
` https://git-scm.com/downloads\n\n`
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
import { agentDir, appRoot } from './app-paths.js'
|
import { agentDir, appRoot } from './app-paths.js'
|
||||||
import { serializeBundledExtensionPaths } from './bundled-extension-paths.js'
|
import { serializeBundledExtensionPaths } from './bundled-extension-paths.js'
|
||||||
import { discoverExtensionEntryPaths } from './extension-discovery.js'
|
import { discoverExtensionEntryPaths } from './extension-discovery.js'
|
||||||
|
|
|
||||||
|
|
@ -78,8 +78,12 @@ function loadProvider(): void {
|
||||||
// unavailable
|
// unavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nodeMajor = parseInt(process.versions.node.split(".")[0], 10);
|
||||||
|
const versionHint = nodeMajor < 22
|
||||||
|
? ` GSD requires Node >= 22.0.0 (current: v${process.versions.node}). Upgrade Node to fix this.`
|
||||||
|
: "";
|
||||||
process.stderr.write(
|
process.stderr.write(
|
||||||
"gsd-db: No SQLite provider available (tried node:sqlite, better-sqlite3)\n",
|
`gsd-db: No SQLite provider available (tried node:sqlite, better-sqlite3).${versionHint}\n`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,59 @@ test("loader sets all 4 GSD_ env vars and PI_PACKAGE_DIR", async (t) => {
|
||||||
rmSync(tmp, { recursive: true, force: true });
|
rmSync(tmp, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 2b. loader runtime dependency checks
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
test("loader source contains Node version check with MIN_NODE_MAJOR", () => {
|
||||||
|
const loaderSrc = readFileSync(join(projectRoot, "src", "loader.ts"), "utf-8");
|
||||||
|
assert.ok(loaderSrc.includes("MIN_NODE_MAJOR"), "loader defines MIN_NODE_MAJOR constant");
|
||||||
|
assert.ok(loaderSrc.includes("process.versions.node"), "loader checks process.versions.node");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("loader source contains git availability check", () => {
|
||||||
|
const loaderSrc = readFileSync(join(projectRoot, "src", "loader.ts"), "utf-8");
|
||||||
|
assert.ok(loaderSrc.includes("git"), "loader checks for git");
|
||||||
|
assert.ok(loaderSrc.includes("execFileSync"), "loader uses execFileSync for git check");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("loader exits with error on unsupported Node version", () => {
|
||||||
|
// Spawn a subprocess that simulates the loader's version check logic
|
||||||
|
// with a deliberately high minimum to force the failure path
|
||||||
|
const script = [
|
||||||
|
"const major = parseInt(process.versions.node.split('.')[0], 10);",
|
||||||
|
"const MIN = 99;",
|
||||||
|
"if (major < MIN) { process.stderr.write('WOULD_EXIT'); process.exit(1); }",
|
||||||
|
"process.stdout.write('OK');",
|
||||||
|
].join(" ");
|
||||||
|
try {
|
||||||
|
execSync(`node -e "${script}"`, { encoding: "utf-8", stdio: "pipe" });
|
||||||
|
// Node >= 99 would reach here — acceptable no-op
|
||||||
|
} catch (err: unknown) {
|
||||||
|
const e = err as { status?: number; stderr?: string };
|
||||||
|
assert.strictEqual(e.status, 1, "exits with code 1 for unsupported Node");
|
||||||
|
assert.ok((e.stderr || "").includes("WOULD_EXIT"), "stderr contains version error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("loader MIN_NODE_MAJOR matches package.json engines field", () => {
|
||||||
|
const loaderSrc = readFileSync(join(projectRoot, "src", "loader.ts"), "utf-8");
|
||||||
|
const pkg = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf-8"));
|
||||||
|
|
||||||
|
// Extract MIN_NODE_MAJOR value from loader source
|
||||||
|
const match = loaderSrc.match(/MIN_NODE_MAJOR\s*=\s*(\d+)/);
|
||||||
|
assert.ok(match, "MIN_NODE_MAJOR is defined with a numeric value");
|
||||||
|
const loaderMin = parseInt(match![1], 10);
|
||||||
|
|
||||||
|
// Extract major version from engines.node (e.g. ">=22.0.0" → 22)
|
||||||
|
const engineMatch = (pkg.engines?.node || "").match(/(\d+)/);
|
||||||
|
assert.ok(engineMatch, "package.json engines.node is defined");
|
||||||
|
const engineMin = parseInt(engineMatch![1], 10);
|
||||||
|
|
||||||
|
assert.strictEqual(loaderMin, engineMin,
|
||||||
|
`loader MIN_NODE_MAJOR (${loaderMin}) must match package.json engines.node (>=${engineMin}.0.0)`);
|
||||||
|
});
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// 3. resource-loader syncs bundled resources
|
// 3. resource-loader syncs bundled resources
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue