@yagni-app/code-staging 1.1.1-staging.1352.1 → 1.1.1-staging.1355.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/bin.d.ts +24 -0
- package/dist/bin.js +73 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +11 -3
- package/dist/crashReport.d.ts +7 -0
- package/dist/crashReport.js +10 -1
- package/dist/doctor.d.ts +9 -0
- package/dist/doctor.js +21 -0
- package/dist/extension/crashReport.d.ts +7 -0
- package/dist/extension/crashReport.js +11 -2
- package/dist/nodeVersion.d.ts +53 -0
- package/dist/nodeVersion.js +79 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -23,6 +23,9 @@ correct, autonomous work than a coding agent that starts blank.
|
|
|
23
23
|
npm install -g @yagni-app/code
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
+
Requires Node.js 22.19 or newer (`node --version`); an older Node stops at
|
|
27
|
+
launch with an upgrade message instead of crashing mid-session.
|
|
28
|
+
|
|
26
29
|
The command it installs is `yagni`:
|
|
27
30
|
|
|
28
31
|
```bash
|
package/dist/bin.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* yagni — the published bin entry.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately tiny: the only static imports are the dependency-free Node
|
|
6
|
+
* version gate and the (equally dependency-free) distribution record that
|
|
7
|
+
* names the installed channel. Everything else (`cli.js` and the module graph
|
|
8
|
+
* behind it) is loaded dynamically AFTER the check passes, so an old Node
|
|
9
|
+
* prints one clear message and exits instead of blowing up inside a
|
|
10
|
+
* dependency — or failing to parse one. `cli.js` stays directly runnable
|
|
11
|
+
* (`node dist/cli.js`) for the e2e lanes; its own entrypoint guard is false
|
|
12
|
+
* when this shim is argv[1], so the shim calls `runAsEntrypoint()` explicitly.
|
|
13
|
+
*
|
|
14
|
+
* Both failure paths set `process.exitCode` and return instead of calling
|
|
15
|
+
* `process.exit()`: stderr on a pipe is asynchronous on Windows, and an
|
|
16
|
+
* immediate exit can truncate the very diagnostic this shim exists to print.
|
|
17
|
+
* Letting the event loop drain flushes it.
|
|
18
|
+
*
|
|
19
|
+
* Behavioral coverage lives in `test/bin.test.ts`, which spawns this file
|
|
20
|
+
* under a faked-old `process.versions.node` and against a stub launcher that
|
|
21
|
+
* fails to load.
|
|
22
|
+
*/
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=bin.d.ts.map
|
package/dist/bin.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* yagni — the published bin entry.
|
|
4
|
+
*
|
|
5
|
+
* Deliberately tiny: the only static imports are the dependency-free Node
|
|
6
|
+
* version gate and the (equally dependency-free) distribution record that
|
|
7
|
+
* names the installed channel. Everything else (`cli.js` and the module graph
|
|
8
|
+
* behind it) is loaded dynamically AFTER the check passes, so an old Node
|
|
9
|
+
* prints one clear message and exits instead of blowing up inside a
|
|
10
|
+
* dependency — or failing to parse one. `cli.js` stays directly runnable
|
|
11
|
+
* (`node dist/cli.js`) for the e2e lanes; its own entrypoint guard is false
|
|
12
|
+
* when this shim is argv[1], so the shim calls `runAsEntrypoint()` explicitly.
|
|
13
|
+
*
|
|
14
|
+
* Both failure paths set `process.exitCode` and return instead of calling
|
|
15
|
+
* `process.exit()`: stderr on a pipe is asynchronous on Windows, and an
|
|
16
|
+
* immediate exit can truncate the very diagnostic this shim exists to print.
|
|
17
|
+
* Letting the event loop drain flushes it.
|
|
18
|
+
*
|
|
19
|
+
* Behavioral coverage lives in `test/bin.test.ts`, which spawns this file
|
|
20
|
+
* under a faked-old `process.versions.node` and against a stub launcher that
|
|
21
|
+
* fails to load.
|
|
22
|
+
*/
|
|
23
|
+
import { DISTRIBUTION } from "./distribution.js";
|
|
24
|
+
import { nodeCheckSkipped, nodeVersionProblem } from "./nodeVersion.js";
|
|
25
|
+
function debugEnabled() {
|
|
26
|
+
const value = process.env.YAGNI_DEBUG;
|
|
27
|
+
return value !== undefined && value !== "" && value !== "0";
|
|
28
|
+
}
|
|
29
|
+
async function main() {
|
|
30
|
+
const problem = nodeCheckSkipped() ? null : nodeVersionProblem(process.versions.node, DISTRIBUTION);
|
|
31
|
+
if (problem !== null) {
|
|
32
|
+
process.stderr.write(`${problem}\n`);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// A launcher that fails to LOAD (corrupt or partial install, a dependency
|
|
37
|
+
// missing from node_modules) would otherwise die as a raw unhandled
|
|
38
|
+
// rejection before the launcher's crash handlers exist — the same opaque
|
|
39
|
+
// failure this shim is here to prevent. No crash report is possible at this
|
|
40
|
+
// point (the reporter is part of what failed to load), so say what to do
|
|
41
|
+
// instead. The message carries the error class and message; YAGNI_DEBUG=1
|
|
42
|
+
// adds the full stack (resolution chain, parse location) for a support thread.
|
|
43
|
+
let launcher;
|
|
44
|
+
try {
|
|
45
|
+
launcher = await import("./cli.js");
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const name = err instanceof Error && err.name ? err.name : "Error";
|
|
49
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
50
|
+
const lines = [
|
|
51
|
+
`${DISTRIBUTION.displayName} failed to load: ${name}: ${message}`,
|
|
52
|
+
"",
|
|
53
|
+
"The install looks incomplete or corrupt. Reinstall it:",
|
|
54
|
+
"",
|
|
55
|
+
` npm install -g ${DISTRIBUTION.packageName}`,
|
|
56
|
+
"",
|
|
57
|
+
];
|
|
58
|
+
if (debugEnabled()) {
|
|
59
|
+
const stack = err instanceof Error && err.stack ? err.stack : "(no stack)";
|
|
60
|
+
const cause = err instanceof Error && err.cause !== undefined ? `\ncause: ${String(err.cause)}` : "";
|
|
61
|
+
lines.push(`${stack}${cause}`, "");
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
lines.push("(YAGNI_DEBUG=1 prints the full stack.)", "");
|
|
65
|
+
}
|
|
66
|
+
process.stderr.write(lines.join("\n"));
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
launcher.runAsEntrypoint();
|
|
71
|
+
}
|
|
72
|
+
await main();
|
|
73
|
+
//# sourceMappingURL=bin.js.map
|
package/dist/cli.d.ts
CHANGED
|
@@ -166,5 +166,11 @@ export declare function main(argv: string[]): Promise<number>;
|
|
|
166
166
|
* test import (argv[1] points at the test runner) does not.
|
|
167
167
|
*/
|
|
168
168
|
export declare function isEntrypoint(argv1: string | undefined, moduleUrl: string): boolean;
|
|
169
|
+
/**
|
|
170
|
+
* Run the launcher as the process entrypoint. Called by the published bin
|
|
171
|
+
* shim (`bin.js`, after the Node version gate) and by the guard below when
|
|
172
|
+
* this module is executed directly (`node dist/cli.js`, the e2e lanes).
|
|
173
|
+
*/
|
|
174
|
+
export declare function runAsEntrypoint(): void;
|
|
169
175
|
export {};
|
|
170
176
|
//# sourceMappingURL=cli.d.ts.map
|
package/dist/cli.js
CHANGED
|
@@ -889,9 +889,12 @@ export function isEntrypoint(argv1, moduleUrl) {
|
|
|
889
889
|
};
|
|
890
890
|
return resolve(argv1) === resolve(fileURLToPath(moduleUrl));
|
|
891
891
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
892
|
+
/**
|
|
893
|
+
* Run the launcher as the process entrypoint. Called by the published bin
|
|
894
|
+
* shim (`bin.js`, after the Node version gate) and by the guard below when
|
|
895
|
+
* this module is executed directly (`node dist/cli.js`, the e2e lanes).
|
|
896
|
+
*/
|
|
897
|
+
export function runAsEntrypoint() {
|
|
895
898
|
// Crash reporting for the LAUNCHER process only (pi runs as a child and the
|
|
896
899
|
// extension covers the session side). Fire-and-forget, sanitized, bounded;
|
|
897
900
|
// YAGNI_DISABLE_CRASH_REPORTS=1 turns it off. Registered before main() so a
|
|
@@ -904,4 +907,9 @@ if (isEntrypoint(process.argv[1], import.meta.url)) {
|
|
|
904
907
|
process.exit(1);
|
|
905
908
|
});
|
|
906
909
|
}
|
|
910
|
+
// Only auto-run when invoked as the CLI entry, so tests can import this module
|
|
911
|
+
// (e.g. to exercise wantsHelp) without spawning the agent.
|
|
912
|
+
if (isEntrypoint(process.argv[1], import.meta.url)) {
|
|
913
|
+
runAsEntrypoint();
|
|
914
|
+
}
|
|
907
915
|
//# sourceMappingURL=cli.js.map
|
package/dist/crashReport.d.ts
CHANGED
|
@@ -77,6 +77,13 @@ export interface SanitizedCrash {
|
|
|
77
77
|
* payloads…) are never touched.
|
|
78
78
|
*/
|
|
79
79
|
export declare function sanitizeCrashError(err: unknown, opts?: SanitizeCrashOptions): SanitizedCrash;
|
|
80
|
+
/**
|
|
81
|
+
* OS, arch AND the Node version. The runtime is a first-class crash cause
|
|
82
|
+
* (an old Node dies inside undici on `zlib.createZstdDecompress`), and it
|
|
83
|
+
* rides the existing `platform` field so the backend and its Sentry tag need
|
|
84
|
+
* no change. Mirrored in `pi-extension-yagni/src/crashReport.ts`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function platformLabel(): string;
|
|
80
87
|
export type CrashClient = "cli" | "desktop" | "desktop-driver";
|
|
81
88
|
export interface CrashReportInput {
|
|
82
89
|
client: CrashClient;
|
package/dist/crashReport.js
CHANGED
|
@@ -177,6 +177,15 @@ export function sanitizeCrashError(err, opts = {}) {
|
|
|
177
177
|
...(cappedStack !== undefined ? { stack: cappedStack } : {}),
|
|
178
178
|
};
|
|
179
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* OS, arch AND the Node version. The runtime is a first-class crash cause
|
|
182
|
+
* (an old Node dies inside undici on `zlib.createZstdDecompress`), and it
|
|
183
|
+
* rides the existing `platform` field so the backend and its Sentry tag need
|
|
184
|
+
* no change. Mirrored in `pi-extension-yagni/src/crashReport.ts`.
|
|
185
|
+
*/
|
|
186
|
+
export function platformLabel() {
|
|
187
|
+
return `${process.platform} ${process.arch} node ${process.version}`;
|
|
188
|
+
}
|
|
180
189
|
/**
|
|
181
190
|
* Sanitize + POST one crash report from pre-extracted fields. Resolves on
|
|
182
191
|
* every outcome — timeout, network error, non-2xx, disabled — and never
|
|
@@ -199,7 +208,7 @@ export async function sendCrashReport(input) {
|
|
|
199
208
|
const payload = {
|
|
200
209
|
client: input.client,
|
|
201
210
|
clientVersion: input.clientVersion,
|
|
202
|
-
platform:
|
|
211
|
+
platform: platformLabel(),
|
|
203
212
|
errorClass: sanitizeCrashText(input.errorClass, opts).slice(0, MAX_CRASH_ERROR_CLASS),
|
|
204
213
|
message: sanitizeCrashText(input.message, opts).slice(0, MAX_CRASH_MESSAGE),
|
|
205
214
|
...(stack !== undefined ? { stack } : {}),
|
package/dist/doctor.d.ts
CHANGED
|
@@ -50,6 +50,13 @@ export type BackendProbe = {
|
|
|
50
50
|
} | {
|
|
51
51
|
kind: "network";
|
|
52
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* The Node floor, first in the list because every other check is moot
|
|
55
|
+
* without it: an old Node dies inside pi's HTTP client mid-request (Sentry
|
|
56
|
+
* YAGNI-BACKEND-4S: `zlib.createZstdDecompress is not a function`). The bin
|
|
57
|
+
* shim refuses to launch below the floor; doctor explains it in the same terms.
|
|
58
|
+
*/
|
|
59
|
+
export declare function checkNodeVersion(version: string): CheckResult;
|
|
53
60
|
export declare function checkPiEngine(probe: PiEngineProbe): CheckResult;
|
|
54
61
|
export declare function checkExtension(probe: ExtensionProbe): CheckResult;
|
|
55
62
|
export declare function checkProfileToken(profile: Pick<Profile, "name" | "token">): CheckResult;
|
|
@@ -115,6 +122,8 @@ export declare function buildDoctorReport(checks: CheckResult[]): DoctorReport;
|
|
|
115
122
|
export declare function formatDoctorReport(report: DoctorReport): string;
|
|
116
123
|
export interface DoctorDeps {
|
|
117
124
|
now?: () => number;
|
|
125
|
+
/** Running Node version (defaults to process.versions.node). */
|
|
126
|
+
nodeVersion?: string;
|
|
118
127
|
probePiEngine?: () => PiEngineProbe;
|
|
119
128
|
probeExtension?: () => ExtensionProbe;
|
|
120
129
|
readActiveProfile?: () => Promise<Profile>;
|
package/dist/doctor.js
CHANGED
|
@@ -21,7 +21,27 @@ import { otelChildEnv, resolveOtelLaunchWithWorkspace } from "./otel.js";
|
|
|
21
21
|
import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir, resolveTelemetryProbePath } from "./paths.js";
|
|
22
22
|
import { readActiveProfile } from "./profiles.js";
|
|
23
23
|
import { resolveMcpConfigPath } from "./mcpCommand.js";
|
|
24
|
+
import { MIN_NODE_VERSION, nodeVersionSatisfies } from "./nodeVersion.js";
|
|
24
25
|
// ── Pure check builders ─────────────────────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* The Node floor, first in the list because every other check is moot
|
|
28
|
+
* without it: an old Node dies inside pi's HTTP client mid-request (Sentry
|
|
29
|
+
* YAGNI-BACKEND-4S: `zlib.createZstdDecompress is not a function`). The bin
|
|
30
|
+
* shim refuses to launch below the floor; doctor explains it in the same terms.
|
|
31
|
+
*/
|
|
32
|
+
export function checkNodeVersion(version) {
|
|
33
|
+
const shown = version.startsWith("v") ? version : `v${version}`;
|
|
34
|
+
if (!nodeVersionSatisfies(version, MIN_NODE_VERSION)) {
|
|
35
|
+
return {
|
|
36
|
+
name: "node",
|
|
37
|
+
status: "fail",
|
|
38
|
+
detail: `${shown} is older than the ${MIN_NODE_VERSION} floor`,
|
|
39
|
+
hint: "upgrade Node.js (https://nodejs.org or `nvm install 22`); older Node crashes mid-request on missing zlib APIs",
|
|
40
|
+
required: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return { name: "node", status: "ok", detail: `${shown} (needs ${MIN_NODE_VERSION}+)`, required: true };
|
|
44
|
+
}
|
|
25
45
|
export function checkPiEngine(probe) {
|
|
26
46
|
if (!probe.binPath || !probe.binExists) {
|
|
27
47
|
return {
|
|
@@ -512,6 +532,7 @@ export async function gatherChecks(deps = {}) {
|
|
|
512
532
|
const probeBash = deps.probeBash ?? (() => bashOnWindowsDefault());
|
|
513
533
|
const probeLatestVersion = deps.probeLatestVersion ?? (() => fetchLatestVersion());
|
|
514
534
|
const checks = [];
|
|
535
|
+
checks.push(checkNodeVersion(deps.nodeVersion ?? process.versions.node));
|
|
515
536
|
checks.push(checkPiEngine(probePiEngine()));
|
|
516
537
|
checks.push(checkExtension(probeExtension()));
|
|
517
538
|
// win32 only, and skipped means NOT SHOWN: on macOS/Linux there is nothing
|
|
@@ -72,6 +72,13 @@ export interface CrashReporterOpts {
|
|
|
72
72
|
timeoutMs?: number;
|
|
73
73
|
}
|
|
74
74
|
export type CrashReporter = (error: unknown, context?: string, repoRoot?: string) => Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* OS, arch AND the Node version: the runtime is a first-class crash cause (an
|
|
77
|
+
* old Node dies inside undici on `zlib.createZstdDecompress`), and it rides
|
|
78
|
+
* the existing `platform` field so the backend and its Sentry tag need no
|
|
79
|
+
* change. Mirrored in `yagni-code-cli/src/crashReport.ts`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function platformLabel(): string;
|
|
75
82
|
/**
|
|
76
83
|
* Build the fail-soft reporter. The extension runs inside pi's process, so
|
|
77
84
|
* the client label follows the surface: `desktop` under the desktop shell
|
|
@@ -145,6 +145,15 @@ export function sanitizeCrashError(err, opts = {}) {
|
|
|
145
145
|
...(cappedStack !== undefined ? { stack: cappedStack } : {}),
|
|
146
146
|
};
|
|
147
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* OS, arch AND the Node version: the runtime is a first-class crash cause (an
|
|
150
|
+
* old Node dies inside undici on `zlib.createZstdDecompress`), and it rides
|
|
151
|
+
* the existing `platform` field so the backend and its Sentry tag need no
|
|
152
|
+
* change. Mirrored in `yagni-code-cli/src/crashReport.ts`.
|
|
153
|
+
*/
|
|
154
|
+
export function platformLabel() {
|
|
155
|
+
return `${process.platform} ${process.arch} node ${process.version}`;
|
|
156
|
+
}
|
|
148
157
|
/**
|
|
149
158
|
* Build the fail-soft reporter. The extension runs inside pi's process, so
|
|
150
159
|
* the client label follows the surface: `desktop` under the desktop shell
|
|
@@ -163,7 +172,7 @@ export function makeCrashReporter(opts) {
|
|
|
163
172
|
const payload = {
|
|
164
173
|
client: isDesktopSurface() ? "desktop" : "cli",
|
|
165
174
|
clientVersion: env.YAGNI_CODE_VERSION?.trim() || "unknown",
|
|
166
|
-
platform:
|
|
175
|
+
platform: platformLabel(),
|
|
167
176
|
...sanitized,
|
|
168
177
|
...(context !== undefined ? { context } : {}),
|
|
169
178
|
timestamp: new Date().toISOString(),
|
|
@@ -234,7 +243,7 @@ export function reportFatalCrash(error, opts, context) {
|
|
|
234
243
|
const payload = {
|
|
235
244
|
client: isDesktopSurface() ? "desktop" : "cli",
|
|
236
245
|
clientVersion: env.YAGNI_CODE_VERSION?.trim() || "unknown",
|
|
237
|
-
platform:
|
|
246
|
+
platform: platformLabel(),
|
|
238
247
|
...sanitized,
|
|
239
248
|
...(context !== undefined ? { context } : {}),
|
|
240
249
|
timestamp: new Date().toISOString(),
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node.js floor gate.
|
|
3
|
+
*
|
|
4
|
+
* `engines.node` in package.json is advisory: npm prints an EBADENGINE
|
|
5
|
+
* warning on a global install and carries on, so a machine running an older
|
|
6
|
+
* Node ends up with a working `yagni` command that dies deep inside a
|
|
7
|
+
* dependency instead of failing cleanly. The first real report (Sentry
|
|
8
|
+
* YAGNI-BACKEND-4S, 2026-09-09) was pi's bundled undici 8 calling
|
|
9
|
+
* `zlib.createZstdDecompress` — added in Node 22.15 — the moment the backend
|
|
10
|
+
* answered a request with `content-encoding: zstd`. Node's own default fatal
|
|
11
|
+
* printer showed an undici stack trace, and nothing said "your Node is too
|
|
12
|
+
* old".
|
|
13
|
+
*
|
|
14
|
+
* `MIN_NODE_VERSION` mirrors `engines.node` (the manifest test pins the two
|
|
15
|
+
* together). This module is imported by `bin.ts` BEFORE anything else loads,
|
|
16
|
+
* so it must stay dependency-free and use only syntax every Node this could
|
|
17
|
+
* plausibly run under can parse.
|
|
18
|
+
*/
|
|
19
|
+
export declare const MIN_NODE_VERSION = "22.19.0";
|
|
20
|
+
/**
|
|
21
|
+
* Escape hatch for the preflight (`YAGNI_SKIP_NODE_CHECK=1`). Same
|
|
22
|
+
* truthiness rule as the other switches: set and not "" / "0".
|
|
23
|
+
*/
|
|
24
|
+
export declare const NODE_CHECK_SKIP_ENV = "YAGNI_SKIP_NODE_CHECK";
|
|
25
|
+
export declare function nodeCheckSkipped(env?: NodeJS.ProcessEnv): boolean;
|
|
26
|
+
/** `"v22.19.0"` / `"22.19.0-nightly..."` → `[22, 19, 0]`; anything else → null. */
|
|
27
|
+
export declare function parseNodeVersion(raw: string): [number, number, number] | null;
|
|
28
|
+
/**
|
|
29
|
+
* Whether `actual` meets the floor. An unparseable version passes: the gate
|
|
30
|
+
* exists to stop a KNOWN-old Node with a clear message, never to lock out a
|
|
31
|
+
* runtime whose version string we merely failed to read.
|
|
32
|
+
*/
|
|
33
|
+
export declare function nodeVersionSatisfies(actual: string, min?: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The names the message speaks in. The same shim ships on two channels
|
|
36
|
+
* (`@yagni-app/code` as `yagni`, `@yagni-app/code-staging` as
|
|
37
|
+
* `yagni-staging`), so the caller passes the installed distribution
|
|
38
|
+
* (`distribution.ts` owns the names; a `CodeDistribution` satisfies this
|
|
39
|
+
* shape) rather than this module keeping a copy that could drift.
|
|
40
|
+
*/
|
|
41
|
+
export interface NodeGateNames {
|
|
42
|
+
packageName: string;
|
|
43
|
+
commandName: string;
|
|
44
|
+
displayName: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The user-facing explanation when the floor is not met, or null when it is.
|
|
48
|
+
* Names the symptom the gate prevents so someone who already hit the crash
|
|
49
|
+
* recognizes it, and spells out the nvm gotcha (global packages live per
|
|
50
|
+
* Node version there, so the CLI needs a reinstall after switching).
|
|
51
|
+
*/
|
|
52
|
+
export declare function nodeVersionProblem(actual: string, names: NodeGateNames, min?: string): string | null;
|
|
53
|
+
//# sourceMappingURL=nodeVersion.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node.js floor gate.
|
|
3
|
+
*
|
|
4
|
+
* `engines.node` in package.json is advisory: npm prints an EBADENGINE
|
|
5
|
+
* warning on a global install and carries on, so a machine running an older
|
|
6
|
+
* Node ends up with a working `yagni` command that dies deep inside a
|
|
7
|
+
* dependency instead of failing cleanly. The first real report (Sentry
|
|
8
|
+
* YAGNI-BACKEND-4S, 2026-09-09) was pi's bundled undici 8 calling
|
|
9
|
+
* `zlib.createZstdDecompress` — added in Node 22.15 — the moment the backend
|
|
10
|
+
* answered a request with `content-encoding: zstd`. Node's own default fatal
|
|
11
|
+
* printer showed an undici stack trace, and nothing said "your Node is too
|
|
12
|
+
* old".
|
|
13
|
+
*
|
|
14
|
+
* `MIN_NODE_VERSION` mirrors `engines.node` (the manifest test pins the two
|
|
15
|
+
* together). This module is imported by `bin.ts` BEFORE anything else loads,
|
|
16
|
+
* so it must stay dependency-free and use only syntax every Node this could
|
|
17
|
+
* plausibly run under can parse.
|
|
18
|
+
*/
|
|
19
|
+
export const MIN_NODE_VERSION = "22.19.0";
|
|
20
|
+
/**
|
|
21
|
+
* Escape hatch for the preflight (`YAGNI_SKIP_NODE_CHECK=1`). Same
|
|
22
|
+
* truthiness rule as the other switches: set and not "" / "0".
|
|
23
|
+
*/
|
|
24
|
+
export const NODE_CHECK_SKIP_ENV = "YAGNI_SKIP_NODE_CHECK";
|
|
25
|
+
export function nodeCheckSkipped(env = process.env) {
|
|
26
|
+
const value = env[NODE_CHECK_SKIP_ENV];
|
|
27
|
+
return value !== undefined && value !== "" && value !== "0";
|
|
28
|
+
}
|
|
29
|
+
/** `"v22.19.0"` / `"22.19.0-nightly..."` → `[22, 19, 0]`; anything else → null. */
|
|
30
|
+
export function parseNodeVersion(raw) {
|
|
31
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(raw.trim());
|
|
32
|
+
if (!m)
|
|
33
|
+
return null;
|
|
34
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Whether `actual` meets the floor. An unparseable version passes: the gate
|
|
38
|
+
* exists to stop a KNOWN-old Node with a clear message, never to lock out a
|
|
39
|
+
* runtime whose version string we merely failed to read.
|
|
40
|
+
*/
|
|
41
|
+
export function nodeVersionSatisfies(actual, min = MIN_NODE_VERSION) {
|
|
42
|
+
const a = parseNodeVersion(actual);
|
|
43
|
+
const b = parseNodeVersion(min);
|
|
44
|
+
if (!a || !b)
|
|
45
|
+
return true;
|
|
46
|
+
for (let i = 0; i < 3; i++) {
|
|
47
|
+
if (a[i] !== b[i])
|
|
48
|
+
return a[i] > b[i];
|
|
49
|
+
}
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The user-facing explanation when the floor is not met, or null when it is.
|
|
54
|
+
* Names the symptom the gate prevents so someone who already hit the crash
|
|
55
|
+
* recognizes it, and spells out the nvm gotcha (global packages live per
|
|
56
|
+
* Node version there, so the CLI needs a reinstall after switching).
|
|
57
|
+
*/
|
|
58
|
+
export function nodeVersionProblem(actual, names, min = MIN_NODE_VERSION) {
|
|
59
|
+
if (nodeVersionSatisfies(actual, min))
|
|
60
|
+
return null;
|
|
61
|
+
const shown = actual.startsWith("v") ? actual : `v${actual}`;
|
|
62
|
+
return [
|
|
63
|
+
`${names.displayName} needs Node.js ${min} or newer, but this is Node ${shown}.`,
|
|
64
|
+
"",
|
|
65
|
+
"Older Node is missing APIs the agent's HTTP client relies on (zstd",
|
|
66
|
+
"decompression, for one), so a session would crash mid-request with an",
|
|
67
|
+
'error like "zlib.createZstdDecompress is not a function" instead of',
|
|
68
|
+
"failing cleanly.",
|
|
69
|
+
"",
|
|
70
|
+
"Upgrade Node (https://nodejs.org, or `nvm install 22 && nvm use 22`),",
|
|
71
|
+
`then run \`${names.commandName}\` again. If your Node comes from nvm, reinstall the CLI`,
|
|
72
|
+
"afterwards so it lives under the new version:",
|
|
73
|
+
"",
|
|
74
|
+
` npm install -g ${names.packageName}`,
|
|
75
|
+
"",
|
|
76
|
+
`(${NODE_CHECK_SKIP_ENV}=1 bypasses this check at your own risk.)`,
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=nodeVersion.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.1.1-staging.
|
|
3
|
+
"version": "1.1.1-staging.1355.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"node": ">=22.19.0"
|
|
26
26
|
},
|
|
27
27
|
"bin": {
|
|
28
|
-
"yagni-staging": "dist/
|
|
28
|
+
"yagni-staging": "dist/bin.js"
|
|
29
29
|
},
|
|
30
30
|
"files": [
|
|
31
31
|
"dist",
|
|
@@ -58,5 +58,5 @@
|
|
|
58
58
|
"turndown": "^7.2.4",
|
|
59
59
|
"typebox": "^1.3.15"
|
|
60
60
|
},
|
|
61
|
-
"yagniSourceSha": "
|
|
61
|
+
"yagniSourceSha": "e319781c1b81806a857f6a846d15b7e9ace2447e"
|
|
62
62
|
}
|