Repository navigation
fix(cli): stop with a clear message on a Node older than engines.node #689
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+449
−21
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@taskless/cli": patch | ||
| --- | ||
|
|
||
| On a Node older than the CLI needs (`>=22.22.0`), Taskless now stops with `Taskless needs Node >=22.22.0, you have 20.19.0. Install a newer Node and run it again.` and exits 1, instead of crashing with an unrelated `SyntaxError` about `globSync`. This holds for `npx @taskless/cli` and `pnpm dlx`, whose `bin` is now a small shim that checks the version before loading the CLI, and for the project launcher (`.taskless/bin/cli.mjs`), which checks before installing a version and before running one. Launchers pick this up when `init` rewrites them. The `ci` recipe now says the job needs Node 22.22 or later. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
26 changes: 26 additions & 0 deletions
26
openspec/changes/archive/2026-11-11-node-version-guard/proposal.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| ## Why | ||
|
|
||
| The CLI requires Node `>=22.22.0` (`engines.node`), but on an older Node it fails with an error that never mentions the version. Two runs in the CLI agent survey hit it: `init` on Node 20.19 crashed with a `SyntaxError` because `node:fs` has no `globSync`, after only npm's `EBADENGINE` warning. The crash is a link error in the CLI's static imports, so a check inside the CLI never runs. | ||
|
|
||
| ## What Changes | ||
|
|
||
| - The package `bin` becomes a small shim (`dist/bin.js`) that compares the running Node against `engines.node`, then dynamically imports the CLI (`dist/index.js`). On an older Node it prints `taskless: Taskless needs Node >=22.22.0, you have 20.19.0. Install a newer Node and run it again.` and exits 1, loading nothing else. This covers `npx @taskless/cli` and `pnpm dlx`. | ||
| - The project launcher checks the same thing before it installs (from `engines.node` in its lockfile) and before it starts an installed version (from that version's `package.json`), with the same message and exit 1. | ||
| - The `ci` recipe says the job needs Node 22.22 or later. | ||
|
|
||
| ## Capabilities | ||
|
|
||
| ### New Capabilities | ||
|
|
||
| None. | ||
|
|
||
| ### Modified Capabilities | ||
|
|
||
| - `cli`: a new requirement that the `bin` checks the Node version before loading the CLI. | ||
| - `cli-project-launcher`: a new requirement that the launcher refuses a Node older than the package's `engines.node`. | ||
|
|
||
| ## Impact | ||
|
|
||
| - `packages/cli/src/bin.ts`, `packages/cli/src/node-version.ts`, `packages/cli/vite.config.ts` (a second program entry and the `__TASKLESS_NODE_ENGINE__` define), `packages/cli/package.json` (`bin`), `packages/cli/assets/launcher/launcher.mjs`, and the `ci` recipe. | ||
| - `dist/index.js` stays the CLI and keeps its shebang, so `pnpm cli`, CI and the `self` build run it as before. | ||
| - Delivery shape: **single PR**. Two small guards, their tests, and a recipe line. |
16 changes: 16 additions & 0 deletions
16
...hanges/archive/2026-11-11-node-version-guard/specs/cli-project-launcher/spec.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| ## ADDED Requirements | ||
|
|
||
| ### Requirement: A launcher refuses a Node older than its package needs | ||
|
|
||
| Before it installs a version, a launcher SHALL compare the running Node against the `engines.node` its lockfile records for the package, and before it runs an installed version, against the `engines.node` in that version's `package.json`. When either is a `>=` lower bound the running Node does not meet, the launcher SHALL write `taskless: Taskless needs Node <range>, you have <version>. Install a newer Node and run it again.` to standard error and exit with code 1, without installing and without starting the CLI. A range other than a `>=` lower bound, or none, SHALL NOT stop it. | ||
|
|
||
| #### Scenario: First run on Node 20 | ||
|
|
||
| - **WHEN** a launcher whose lockfile records `engines.node` `>=22.22.0` runs on Node 20.19.0 with nothing installed | ||
| - **THEN** it SHALL print the message naming `>=22.22.0` and `20.19.0` and exit with code 1 | ||
| - **AND** it SHALL NOT run npm | ||
|
|
||
| #### Scenario: An installed version on an older Node | ||
|
|
||
| - **WHEN** a version installed under a newer Node is run by the launcher on a Node older than its `engines.node` | ||
| - **THEN** the launcher SHALL print the message and exit with code 1 without starting the CLI |
16 changes: 16 additions & 0 deletions
16
openspec/changes/archive/2026-11-11-node-version-guard/specs/cli/spec.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| ## ADDED Requirements | ||
|
|
||
| ### Requirement: The bin checks the Node version before loading the CLI | ||
|
|
||
| The package `bin` SHALL be a separate built file from the CLI entry. It SHALL compare the running Node version against the package's `engines.node` before it loads any of the CLI, and SHALL load the CLI with a dynamic `import()` only when the version satisfies it, so that an older Node never links the CLI's module graph. When the version does not satisfy it, the `bin` SHALL write one line to standard error naming the required range and the running version, write nothing to standard output, and exit with code 1. A range other than a `>=` lower bound SHALL NOT stop the CLI. | ||
|
|
||
| #### Scenario: npx on Node 20 | ||
|
|
||
| - **WHEN** a user runs `npx @taskless/cli init` on Node 20.19.0 and `engines.node` is `>=22.22.0` | ||
| - **THEN** standard error SHALL be `taskless: Taskless needs Node >=22.22.0, you have 20.19.0. Install a newer Node and run it again.` | ||
| - **AND** the process SHALL exit with code 1 without loading the CLI | ||
|
|
||
| #### Scenario: A satisfying Node runs the CLI | ||
|
|
||
| - **WHEN** the running Node satisfies `engines.node` | ||
| - **THEN** the `bin` SHALL run the CLI with every argument unchanged |
19 changes: 19 additions & 0 deletions
19
openspec/changes/archive/2026-11-11-node-version-guard/tasks.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| ## 1. Package bin | ||
|
|
||
| - [x] 1.1 Add `src/node-version.ts`, the `>=` lower-bound comparison and its message | ||
| - [x] 1.2 Add `src/bin.ts`, which checks `__TASKLESS_NODE_ENGINE__` and then dynamically imports the CLI | ||
| - [x] 1.3 Build it as a second program entry with a shebang, define `__TASKLESS_NODE_ENGINE__` from `engines.node`, and point `bin` at `dist/bin.js` | ||
|
|
||
| ## 2. Project launcher | ||
|
|
||
| - [x] 2.1 Check the lockfile's `engines.node` before installing, and the installed `package.json`'s before running | ||
|
|
||
| ## 3. Docs | ||
|
|
||
| - [x] 3.1 Say in the `ci` recipe that the job needs Node 22.22 or later, and bump its topic version | ||
|
|
||
| ## 4. Tests | ||
|
|
||
| - [x] 4.1 `nodeVersionProblem`: ranges it admits, ranges it refuses, ranges it does not judge | ||
| - [x] 4.2 The built `bin` on a spoofed Node 20.19.0 prints the message, exits 1, and loads nothing else | ||
| - [x] 4.3 The launcher on a spoofed older Node refuses before installing and refuses an already installed version |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import process from "node:process"; | ||
|
|
||
| import { nodeVersionProblem } from "./node-version"; | ||
|
|
||
| /** | ||
| * The package's `bin`: check the running Node against `engines.node`, then | ||
| * load the CLI. | ||
| * | ||
| * The CLI is loaded with a dynamic `import()` on purpose. A static import | ||
| * would link the CLI's whole module graph before this file's first line runs, | ||
| * and on an older Node that link is what fails, with an error about a missing | ||
| * export rather than about the Node version. Nothing this file imports | ||
| * statically may need a newer Node than the oldest one it is meant to turn | ||
| * away. | ||
| */ | ||
| const problem = nodeVersionProblem( | ||
| __TASKLESS_NODE_ENGINE__, | ||
| process.versions.node | ||
| ); | ||
| if (problem === undefined) { | ||
| await import("./index"); | ||
| } else { | ||
| process.stderr.write(`taskless: ${problem}\n`); | ||
| process.exitCode = 1; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| /** | ||
| * The Node version check the package's `bin` runs before it loads the CLI. | ||
| * | ||
| * It lives apart from the CLI because the CLI cannot run it: on an older Node | ||
| * the CLI's module graph fails to link (`globSync` from `node:fs`, for one) | ||
| * before any of its code executes, so a check inside it never gets the chance | ||
| * to speak. This module and the shim that calls it (`src/bin.ts`) import | ||
| * nothing that an old Node lacks. | ||
| * | ||
| * The project launcher (`assets/launcher/launcher.mjs`) carries its own copy | ||
| * of the same comparison and prints the same message. It is a standalone | ||
| * script written into a project, so it cannot import this one. A change to | ||
| * the accepted range syntax or the message belongs in both. | ||
| */ | ||
|
|
||
| /** `>=MAJOR[.MINOR[.PATCH]]`, the only `engines.node` shape this package uses. */ | ||
| const MINIMUM = /^\s*>=\s*v?(\d+)(?:\.(\d+))?(?:\.(\d+))?\s*$/; | ||
|
|
||
| /** The numeric `[major, minor, patch]` of a version, ignoring any prerelease. */ | ||
| function parts(version: string): number[] | undefined { | ||
| const match = /^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?/.exec(version.trim()); | ||
| if (match === null) return undefined; | ||
| return [match[1], match[2], match[3]].map((part) => Number(part ?? 0)); | ||
| } | ||
|
|
||
| /** | ||
| * The message to print when `running` does not satisfy `required`, or | ||
| * undefined when it does. | ||
| * | ||
| * Only a `>=` lower bound is understood. Any other range, or none, passes: | ||
| * a check that cannot read the requirement must not be the thing that stops | ||
| * the CLI from running. | ||
| */ | ||
| export function nodeVersionProblem( | ||
| required: string | null | undefined, | ||
| running: string | ||
| ): string | undefined { | ||
| if (typeof required !== "string") return undefined; | ||
| const match = MINIMUM.exec(required); | ||
| const have = parts(running); | ||
| if (match === null || have === undefined) return undefined; | ||
| const need = [match[1], match[2], match[3]].map((part) => Number(part ?? 0)); | ||
| for (const [index, wanted] of need.entries()) { | ||
| const actual = have[index] ?? 0; | ||
| if (actual > wanted) return undefined; | ||
| if (actual < wanted) { | ||
| return ( | ||
| `Taskless needs Node ${required.trim()}, you have ${running}. ` + | ||
| `Install a newer Node and run it again.` | ||
| ); | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.