diff --git a/AGENTS.md b/AGENTS.md index a4ca6b8..8885797 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,8 @@ A CLI (published as `@stackguardian/mcp-wizard`, command `add-sg-mcp`) that conn - `src/sg/` — everything StackGuardian-specific: `preset.ts` (URLs, header, validation), `credentials.ts` (credential store), `auth.ts` (pieces the browser sign-in shares: state, callback page, URL opener), `oauth.ts` (grant sign-in through the OAuth broker), `skills.ts` (skills installer), `commands.ts` (Commander wiring for the default connect flow, `login`, `logout`, `status`, `remove`), `types.ts`. - `src/agents.ts`, `src/installer.ts`, `src/formats/*`, `src/schema.ts`, `src/reader.ts`, `src/opencode-config.ts`, `src/source-parser.ts`, `src/template.ts` — upstream, kept byte-identical so `git merge upstream/main` stays cheap. Do not put StackGuardian logic there. -- `src/index.ts` — the bin. Upstream's `main()` install flow plus the registration of the `sg` commands (`registerSgCommands`, dependencies injected to avoid an import cycle). +- `src/cli.ts` — the npm bin: refuses Node.js older than `engines`, then loads `src/index.ts`. The standalone executable builds from `src/index.ts` directly, since it brings its own runtime. +- `src/index.ts` — the CLI. Upstream's `main()` install flow plus the registration of the `sg` commands (`registerSgCommands`, dependencies injected to avoid an import cycle). - `skills/` — the bundled `SKILL.md` folders shipped in the npm package. - `install.sh`, `install.ps1` — installers for the standalone executables that `.github/workflows/binaries.yml` builds (`bun run build:binary`), uploads to `dl.stackguardian.io` and attaches to each GitHub release. - `docs/superpowers/specs/` and `docs/superpowers/plans/` — design and implementation plan. diff --git a/CHANGELOG.md b/CHANGELOG.md index d75024a..30aba8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 1.3.1 + +- Node.js older than 18 now gets a clear error as soon as the tool starts, with the version you are running and the commands to install the standalone executable, which does not need Node.js. Before, npm printed an easy-to-miss engine warning and the tool carried on: the browser sign-in then failed with `fetch is not defined`, after you had already approved access. This also stops `--token` installs on those versions, which happened to work but were never supported. Node.js 18 and 20 keep working. + ## 1.3.0 - The connect flow and `login` now send usage events to StackGuardian when an install starts, when you sign in and when it completes, so we can see where installs stop. They are tied to a random ID on your machine and carry your organization and region, never the key or the grant token. Sending adds at most two seconds to a run. Set `DO_NOT_TRACK=1` or `SG_NO_TELEMETRY=1` to turn them off. See "Usage analytics" in the README. diff --git a/package.json b/package.json index 5057154..7d7a564 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@stackguardian/mcp-wizard", - "version": "1.3.0", + "version": "1.3.1", "description": "Connect your coding agents to StackGuardian: installs the StackGuardian MCP server and skills into Claude Code, Codex, Cursor, VS Code, Gemini CLI, OpenCode and more with a single command.", "author": "StackGuardian ", "license": "Apache-2.0", @@ -15,7 +15,7 @@ "./package.json": "./package.json" }, "bin": { - "add-sg-mcp": "dist/index.js" + "add-sg-mcp": "dist/cli.js" }, "files": [ "dist", @@ -24,7 +24,7 @@ ], "scripts": { "fmt": "prettier --write .", - "build": "tsup src/index.ts src/lib.ts --format esm --dts --clean", + "build": "tsup src/cli.ts src/index.ts src/lib.ts --format esm --dts --clean", "build:binary": "bun build src/index.ts skills/*/SKILL.md --compile --minify --loader .md:file --asset-naming '[dir]/[name].[ext]'", "dev": "tsx src/index.ts", "test": "node tests/run.mjs all", diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 0000000..82da39d --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +// The npm bin. npm only warns about `engines`, so check the version here, before +// index.ts and its dependencies load: below Node 18 the sign-in fails on the missing +// global fetch, and only after the user has approved access in the browser. +import { nodeVersionError } from "./sg/node-version.js"; + +const versionError = nodeVersionError(process.versions.node); +if (versionError) { + console.error(versionError); + process.exit(1); +} + +import("./index.js").catch((error: unknown) => { + console.error(error); + process.exit(1); +}); diff --git a/src/sg/node-version.ts b/src/sg/node-version.ts new file mode 100644 index 0000000..a18e329 --- /dev/null +++ b/src/sg/node-version.ts @@ -0,0 +1,19 @@ +/** Oldest Node.js major the npm package runs on; `engines` in package.json says the same. */ +export const MIN_NODE_MAJOR = 18; + +/** What to tell the user when `version` (`process.versions.node`) is too old, otherwise null. */ +export function nodeVersionError(version: string): string | null { + if (Number.parseInt(version, 10) >= MIN_NODE_MAJOR) return null; + return [ + `add-sg-mcp needs Node.js ${MIN_NODE_MAJOR} or later, and this is Node.js ${version}.`, + "Upgrade Node.js, or install the standalone executable, which does not need Node.js:", + "", + " # macOS and Linux", + " curl -fsSL https://dl.stackguardian.io/add-sg-mcp/install.sh | sh", + "", + " # Windows (PowerShell)", + " irm https://dl.stackguardian.io/add-sg-mcp/install.ps1 | iex", + "", + "More: https://github.com/StackGuardian/stackguardian-mcp#install", + ].join("\n"); +} diff --git a/tests/run.mjs b/tests/run.mjs index 42b1fcd..5e12d70 100644 --- a/tests/run.mjs +++ b/tests/run.mjs @@ -26,6 +26,7 @@ const UNIT = [ "tests/sg-oauth.test.ts", "tests/sg-skills.test.ts", "tests/sg-telemetry.test.ts", + "tests/sg-node-version.test.ts", ]; const E2E = [ "tests/e2e/install.test.ts", diff --git a/tests/sg-node-version.test.ts b/tests/sg-node-version.test.ts new file mode 100644 index 0000000..56c6cd0 --- /dev/null +++ b/tests/sg-node-version.test.ts @@ -0,0 +1,71 @@ +#!/usr/bin/env tsx +import assert from "node:assert"; +import { readFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { MIN_NODE_MAJOR, nodeVersionError } from "../src/sg/node-version.js"; + +let passed = 0; +let failed = 0; + +function test(name: string, fn: () => void) { + try { + fn(); + console.log(`✓ ${name}`); + passed++; + } catch (err) { + console.log(`✗ ${name}`); + console.error(` ${(err as Error).message}`); + failed++; + } +} + +const packageJson = JSON.parse( + readFileSync( + join(dirname(fileURLToPath(import.meta.url)), "..", "package.json"), + "utf-8", + ), +); + +test("Node versions below the minimum get an error naming both versions", () => { + for (const version of ["14.21.3", "16.20.2", "17.9.1"]) { + const error = nodeVersionError(version); + assert.ok(error, `expected an error for ${version}`); + assert.ok(error.includes(version)); + assert.ok(error.includes(`Node.js ${MIN_NODE_MAJOR} or later`)); + } +}); + +test("the error gives the standalone install commands from the README", () => { + const error = nodeVersionError("16.20.2"); + assert.ok(error); + const readme = readFileSync( + join(dirname(fileURLToPath(import.meta.url)), "..", "README.md"), + "utf-8", + ); + for (const command of [ + "curl -fsSL https://dl.stackguardian.io/add-sg-mcp/install.sh | sh", + "irm https://dl.stackguardian.io/add-sg-mcp/install.ps1 | iex", + ]) { + assert.ok(error.includes(command), `error is missing: ${command}`); + assert.ok(readme.includes(command), `README is missing: ${command}`); + } +}); + +test("Node 18 and later pass, including the EOL 18 and 20", () => { + for (const version of ["18.0.0", "18.20.8", "20.20.2", "22.23.4", "26.0.0"]) { + assert.strictEqual(nodeVersionError(version), null); + } +}); + +test("the minimum matches engines in package.json", () => { + assert.strictEqual(packageJson.engines.node, `>=${MIN_NODE_MAJOR}`); +}); + +test("the npm bin is the entry that checks the version", () => { + assert.strictEqual(packageJson.bin["add-sg-mcp"], "dist/cli.js"); + assert.ok(packageJson.scripts.build.includes("src/cli.ts")); +}); + +console.log(`\n${passed} passed, ${failed} failed`); +process.exit(failed > 0 ? 1 : 0);