diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f579eefd..3063a8a2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -11,8 +11,7 @@ updates: dependency-type: development ignore: # commander is version-synced with cli/package.json (a test enforces - # it), and the cli supports Node >=20.12 while commander 15 requires - # Node >=22.12. Major bumps are a deliberate cli decision. + # it). Major bumps are a deliberate cli decision. - dependency-name: commander update-types: ["version-update:semver-major"] # typescript is aliased to @typescript/typescript6 (the JS compiler diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4a4ce019..60922106 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,14 +66,22 @@ jobs: - run: npm run build - # The cli package targets older runtimes than the server (engines >=20.12), + # The cli package targets older runtimes than the server (engines >=22.12), # but the repo's @types/node tracks the server's Node version — so only a # runtime smoke test catches accidental use of too-new APIs in the CLI. + # The floor row pins 22.12 so the oldest supported API surface actually + # runs; label keeps the required-check name at the major, so the ruleset + # contexts survive future floor bumps. cli-smoke: + name: cli-smoke (${{ matrix.label }}) runs-on: ubuntu-latest strategy: matrix: - node-version: [20, 22, 24] + include: + - node-version: "22.12" + label: "22" + - node-version: "24" + label: "24" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -98,3 +106,22 @@ jobs: mkdir -p /tmp/smoke-vault/.obsidian node cli/dist/bin.js init --yes --vault-path /tmp/smoke-vault --dir /tmp/smoke-out grep -q "VAULT_PATH=/tmp/smoke-vault" /tmp/smoke-out/.env + + # The refusal branch never executes on supported majors, so the floor + # row re-runs the built CLI on EOL Node 20 and asserts the refusal + # shape: nonzero exit, the floor in the message, both setup pointers. + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + if: matrix.label == '22' + with: + node-version: 20 + + - name: Refusal smoke on Node 20 + if: matrix.label == '22' + run: | + if node cli/dist/bin.js --help > refusal-output.txt 2>&1; then + echo "expected the CLI to refuse Node 20" + exit 1 + fi + grep -F "requires Node.js >= 22.12.0" refusal-output.txt + grep -F "deploy/local/README.md" refusal-output.txt + grep -F "deploy/remote/README.md" refusal-output.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e4f6ecc9..c14602d7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -140,12 +140,13 @@ truth. Key points: ``` 4. **Fill out the PR template** — the checklist mirrors CI -5. **Required checks must pass** — the `main` ruleset requires all eight; +5. **Required checks must pass** — the `main` ruleset requires all seven; each blocks the merge and the finding details are in its job log: - `checks` — prettier, lint, markdownlint, knip, test, and build - - `cli-smoke (20)` / `cli-smoke (22)` / `cli-smoke (24)` — builds the - CLI and runs `init` on each supported Node major, catching APIs too - new for the CLI's `engines` range + - `cli-smoke (22)` / `cli-smoke (24)` — builds the + CLI and runs `init` on the engines floor (22.12) and the newest + major (24), catching APIs too new for the CLI's `engines` range; + the floor row also asserts the too-old refusal on Node 20 - `arch-smoke (amd64)` / `arch-smoke (arm64)` — builds the Docker image and boots it on a native runner for each architecture, then boots the remote image with a stubbed Sync client to run its init chain diff --git a/README.md b/README.md index 883b8b53..8032f900 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ ### Local (2 minutes — Docker + your vault folder) -**Prerequisites:** [Docker](https://docs.docker.com/get-docker/) (or a Docker-compatible runtime, e.g. OrbStack, Colima, Podman), Node.js >= 20.12 (only for the CLI — the server itself runs in Docker), and an Obsidian vault (or any folder of `.md` files). +**Prerequisites:** [Docker](https://docs.docker.com/get-docker/) (or a Docker-compatible runtime, e.g. OrbStack, Colima, Podman), Node.js >= 22.12 (only for the CLI — the server itself runs in Docker), and an Obsidian vault (or any folder of `.md` files). ```bash npx vault-cortex@latest init @@ -111,7 +111,7 @@ All three need an [Obsidian Sync](https://obsidian.md/sync) subscription. Whiche #### Self-hosted: your own VPS -The [vault-cortex CLI](./cli/) sets up the same container on any Linux box you run — you manage the server, the image, and updates. You need Node.js >= 20.12 for the CLI itself; the server runs in Docker. +The [vault-cortex CLI](./cli/) sets up the same container on any Linux box you run — you manage the server, the image, and updates. You need Node.js >= 22.12 for the CLI itself; the server runs in Docker. ```bash # On your VPS: diff --git a/cli/README.md b/cli/README.md index d340fcf7..f98166a0 100644 --- a/cli/README.md +++ b/cli/README.md @@ -206,7 +206,7 @@ During `init --mode remote`, this flow is offered automatically. ## Requirements -- Node.js >= 20.12 (only for this CLI — the server itself runs in Docker) +- Node.js >= 22.12 (only for this CLI — the server itself runs in Docker) - [Docker](https://docs.docker.com/get-docker/) or a Docker-compatible runtime (e.g. OrbStack, Colima, Podman) to run the server — the CLI manages the container through the `docker` command (on Linux, see diff --git a/cli/package.json b/cli/package.json index 91ade722..803b7e40 100644 --- a/cli/package.json +++ b/cli/package.json @@ -12,7 +12,7 @@ "README.md" ], "engines": { - "node": ">=20.12.0" + "node": ">=22.12.0" }, "repository": { "type": "git", diff --git a/cli/src/__tests__/node-version.test.ts b/cli/src/__tests__/node-version.test.ts index 6f7bc5a6..1f51cf5a 100644 --- a/cli/src/__tests__/node-version.test.ts +++ b/cli/src/__tests__/node-version.test.ts @@ -2,7 +2,11 @@ import { readFileSync } from "node:fs" import { fileURLToPath } from "node:url" import { describe, expect, it } from "vitest" -import { minimumNodeVersion, satisfiesMinimum } from "../node-version.js" +import { + minimumNodeVersion, + nodeVersionRefusalMessage, + satisfiesMinimum, +} from "../node-version.js" describe("minimumNodeVersion", () => { it("extracts the floor from a >= range", () => { @@ -27,12 +31,12 @@ describe("minimumNodeVersion", () => { const minimum = minimumNodeVersion(manifest.engines.node) - expect(minimum).toBe("20.12.0") + expect(minimum).toBe("22.12.0") }) it("throws on a range with no version in it", () => { expect(() => minimumNodeVersion("latest")).toThrow( - "Cannot parse engines range", + "Cannot parse engines range: latest", ) }) }) @@ -95,3 +99,19 @@ describe("satisfiesMinimum", () => { expect(satisfied).toBe(expected) }) }) + +describe("nodeVersionRefusalMessage", () => { + it("names the floor, the running version, and both no-Node setup guides", () => { + const message = nodeVersionRefusalMessage({ + minimum: "22.12.0", + current: "20.20.2", + }) + + expect(message).toBe( + "vault-cortex requires Node.js >= 22.12.0 (you have 20.20.2).\n" + + "Upgrade at https://nodejs.org — or use a no-Node manual setup:\n" + + " local: https://github.com/aliasunder/vault-cortex/blob/main/deploy/local/README.md\n" + + " remote: https://github.com/aliasunder/vault-cortex/blob/main/deploy/remote/README.md", + ) + }) +}) diff --git a/cli/src/bin.ts b/cli/src/bin.ts index 5f7ae2bb..4ca90444 100644 --- a/cli/src/bin.ts +++ b/cli/src/bin.ts @@ -2,7 +2,11 @@ import { readFileSync } from "node:fs" import { fileURLToPath } from "node:url" -import { minimumNodeVersion, satisfiesMinimum } from "./node-version.js" +import { + minimumNodeVersion, + nodeVersionRefusalMessage, + satisfiesMinimum, +} from "./node-version.js" const pkg: { version: string; engines: { node: string } } = JSON.parse( readFileSync( @@ -19,9 +23,10 @@ const { version, engines } = pkg const requiredNodeVersion = minimumNodeVersion(engines.node) if (!satisfiesMinimum(process.versions.node, requiredNodeVersion)) { console.error( - `vault-cortex requires Node.js >= ${requiredNodeVersion} (you have ${process.versions.node}).\n` + - `Upgrade at https://nodejs.org — or use the no-Node manual setup:\n` + - `https://github.com/aliasunder/vault-cortex/blob/main/deploy/local/README.md`, + nodeVersionRefusalMessage({ + minimum: requiredNodeVersion, + current: process.versions.node, + }), ) process.exit(1) } diff --git a/cli/src/node-version.ts b/cli/src/node-version.ts index 90a67ea9..81ba5e9b 100644 --- a/cli/src/node-version.ts +++ b/cli/src/node-version.ts @@ -1,19 +1,38 @@ -/** Matches the first dotted version number in an engines range like ">=20.12.0". */ +/** Matches the first dotted version number in an engines range like ">=22.12.0". */ const VERSION_IN_RANGE = /(\d+)\.(\d+)(?:\.(\d+))?/ /** - * Extracts the minimum version from a simple engines range (">=20.12.0"). + * Extracts the minimum version from a simple engines range (">=22.12.0"). * The CLI only ever declares a floor, so the first version in the string * is the minimum. */ export const minimumNodeVersion = (enginesRange: string): string => { const match = VERSION_IN_RANGE.exec(enginesRange) - if (match === null) - throw new Error(`Cannot parse engines range: ${enginesRange}`) + if (!match) throw new Error(`Cannot parse engines range: ${enginesRange}`) const [, major, minor, patch] = match return `${major}.${minor}.${patch ?? "0"}` } +/** + * The refusal printed when the running Node is below the engines floor. + * Lives in this zero-import module so bin.ts can build it before any + * dependency-laden import runs on an unsupported runtime. + */ +export const nodeVersionRefusalMessage = ({ + minimum, + current, +}: { + minimum: string + current: string +}): string => { + return ( + `vault-cortex requires Node.js >= ${minimum} (you have ${current}).\n` + + `Upgrade at https://nodejs.org — or use a no-Node manual setup:\n` + + ` local: https://github.com/aliasunder/vault-cortex/blob/main/deploy/local/README.md\n` + + ` remote: https://github.com/aliasunder/vault-cortex/blob/main/deploy/remote/README.md` + ) +} + /** * Numeric major.minor.patch comparison: is current >= minimum? * The most significant differing segment decides; equal versions satisfy. diff --git a/deploy/local/README.md b/deploy/local/README.md index 73556968..18d7c760 100644 --- a/deploy/local/README.md +++ b/deploy/local/README.md @@ -8,7 +8,7 @@ Obsidian Sync — just Docker and a folder of `.md` files. ## Prerequisites - [Docker](https://docs.docker.com/get-docker/) (v20.10+) -- Node.js >= 20.12 — only for the CLI setup below; the +- Node.js >= 22.12 — only for the CLI setup below; the [manual setup](#setup) needs just Docker - An Obsidian vault (or any folder of Markdown files) diff --git a/deploy/remote/README.md b/deploy/remote/README.md index 54c3ac99..bfae64fb 100644 --- a/deploy/remote/README.md +++ b/deploy/remote/README.md @@ -39,7 +39,7 @@ starts the server and prints the connection details for your MCP client
Don't have Node.js installed? -The CLI needs Node.js >= 20.12 (the server itself runs in Docker). On Ubuntu/Debian: +The CLI needs Node.js >= 22.12 (the server itself runs in Docker). On Ubuntu/Debian: ```bash curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash - @@ -68,7 +68,7 @@ Or clone the repo and `cd deploy/remote`. **3. Generate your Obsidian Sync auth token** (one-time): -If you have Node.js >= 20.12 on this machine, the CLI signs in to your +If you have Node.js >= 22.12 on this machine, the CLI signs in to your Obsidian account and captures the token: ```bash