@ekanos/cli 0.1.10 → 0.2.0
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/dist/banner.d.ts +5 -1
- package/dist/banner.js +7 -2
- package/dist/banner.js.map +1 -1
- package/dist/commands/init.d.ts +13 -1
- package/dist/commands/init.js +70 -13
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/login.d.ts +13 -0
- package/dist/commands/login.js +41 -10
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/publish.js +55 -3
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/status.d.ts +68 -2
- package/dist/commands/status.js +312 -43
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/test.d.ts +27 -5
- package/dist/commands/test.js +85 -9
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/validate.js +115 -3
- package/dist/commands/validate.js.map +1 -1
- package/dist/context.d.ts +37 -1
- package/dist/context.js +27 -1
- package/dist/context.js.map +1 -1
- package/dist/did-you-mean.d.ts +36 -0
- package/dist/did-you-mean.js +192 -0
- package/dist/did-you-mean.js.map +1 -0
- package/dist/findings-renderer.d.ts +59 -0
- package/dist/findings-renderer.js +271 -0
- package/dist/findings-renderer.js.map +1 -0
- package/dist/index.d.ts +11 -4
- package/dist/index.js +23 -5
- package/dist/index.js.map +1 -1
- package/dist/project-checks.d.ts +30 -0
- package/dist/project-checks.js +162 -0
- package/dist/project-checks.js.map +1 -1
- package/dist/publish-api.d.ts +13 -0
- package/dist/publish-api.js.map +1 -1
- package/dist/source-locator.d.ts +67 -0
- package/dist/source-locator.js +284 -0
- package/dist/source-locator.js.map +1 -0
- package/dist/test-report.d.ts +32 -0
- package/dist/test-report.js +255 -0
- package/dist/test-report.js.map +1 -0
- package/dist/ui.d.ts +75 -0
- package/dist/ui.js +132 -0
- package/dist/ui.js.map +1 -0
- package/dist/validate-findings.d.ts +22 -0
- package/dist/validate-findings.js +57 -1
- package/dist/validate-findings.js.map +1 -1
- package/dist/wizard.d.ts +80 -0
- package/dist/wizard.js +74 -0
- package/dist/wizard.js.map +1 -0
- package/package.json +4 -2
- package/templates/package.json.tmpl +3 -1
- package/templates/scripts/setup-hooks.mjs.tmpl +45 -0
- package/templates/tsconfig.json.tmpl +2 -0
package/dist/commands/test.d.ts
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
|
+
import { type spawn as nodeSpawn } from 'node:child_process';
|
|
1
2
|
import type { CliContext } from '../context.js';
|
|
2
3
|
export interface TestArgs {
|
|
3
4
|
dir: string;
|
|
4
5
|
/** argv after `--`, forwarded verbatim to the runner. */
|
|
5
6
|
passthrough: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Escape hatch: skips reporter injection and the curated failures
|
|
9
|
+
* appendix entirely, reproducing exactly the pre-appendix raw-passthrough
|
|
10
|
+
* behavior. Also the sanctioned bypass when the JSON-reporter detection
|
|
11
|
+
* (below) ever guesses wrong for a project.
|
|
12
|
+
*/
|
|
13
|
+
raw?: boolean;
|
|
14
|
+
/** Injectable so tests never fork a real process; defaults to the real `node:child_process` spawn. */
|
|
15
|
+
spawnFn?: typeof nodeSpawn;
|
|
6
16
|
}
|
|
7
17
|
/**
|
|
8
18
|
* `test` — delegate to the partner's own test script; do NOT reimplement or
|
|
9
|
-
* parse the runner's output
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
19
|
+
* parse the runner's output ourselves beyond the OPTIONAL structured
|
|
20
|
+
* appendix below. We detect the package manager from the lockfile, spawn
|
|
21
|
+
* `<pm> run test` with any post-`--` args appended, stream the child's
|
|
22
|
+
* output to OUR stderr (keeping stdout pure for the single JSON envelope),
|
|
23
|
+
* and report `{ command, exitCode, durationMs }`.
|
|
13
24
|
*
|
|
14
|
-
*
|
|
25
|
+
* When the project's OWN `test` script names vitest (and `--raw` wasn't
|
|
26
|
+
* passed), we ALSO ask vitest for a `--reporter=json` report alongside its
|
|
27
|
+
* normal terminal output — mirroring how the server-side gate grades a
|
|
28
|
+
* submission's test run, so local triage has the same per-test intelligence
|
|
29
|
+
* as CI. On failure, that report drives a curated "FAILURES" appendix
|
|
30
|
+
* (`../findings-renderer.ts`) rendered AFTER the child's own live stream
|
|
31
|
+
* ends; on success, it drives a one-line pass count. Reading/parsing the
|
|
32
|
+
* report is entirely best-effort (`test-report.ts`): anything wrong with it
|
|
33
|
+
* silently drops the appendix, never fails the command.
|
|
34
|
+
*
|
|
35
|
+
* The CLI's exit code is always a transparent passthrough of the runner's
|
|
36
|
+
* exit code, with or without the appendix.
|
|
15
37
|
*/
|
|
16
38
|
export declare function runTest(ctx: CliContext, args: TestArgs): Promise<number>;
|
package/dist/commands/test.js
CHANGED
|
@@ -1,25 +1,54 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import { renderFindings } from '../findings-renderer.js';
|
|
2
4
|
import { detectPackageManager } from '../package-manager.js';
|
|
3
5
|
import { loadProject } from '../project.js';
|
|
6
|
+
import { newReportPath, parseTestReport, projectTestScriptUsesVitest, reporterArgs, } from '../test-report.js';
|
|
4
7
|
/**
|
|
5
8
|
* `test` — delegate to the partner's own test script; do NOT reimplement or
|
|
6
|
-
* parse the runner's output
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
* parse the runner's output ourselves beyond the OPTIONAL structured
|
|
10
|
+
* appendix below. We detect the package manager from the lockfile, spawn
|
|
11
|
+
* `<pm> run test` with any post-`--` args appended, stream the child's
|
|
12
|
+
* output to OUR stderr (keeping stdout pure for the single JSON envelope),
|
|
13
|
+
* and report `{ command, exitCode, durationMs }`.
|
|
10
14
|
*
|
|
11
|
-
*
|
|
15
|
+
* When the project's OWN `test` script names vitest (and `--raw` wasn't
|
|
16
|
+
* passed), we ALSO ask vitest for a `--reporter=json` report alongside its
|
|
17
|
+
* normal terminal output — mirroring how the server-side gate grades a
|
|
18
|
+
* submission's test run, so local triage has the same per-test intelligence
|
|
19
|
+
* as CI. On failure, that report drives a curated "FAILURES" appendix
|
|
20
|
+
* (`../findings-renderer.ts`) rendered AFTER the child's own live stream
|
|
21
|
+
* ends; on success, it drives a one-line pass count. Reading/parsing the
|
|
22
|
+
* report is entirely best-effort (`test-report.ts`): anything wrong with it
|
|
23
|
+
* silently drops the appendix, never fails the command.
|
|
24
|
+
*
|
|
25
|
+
* The CLI's exit code is always a transparent passthrough of the runner's
|
|
26
|
+
* exit code, with or without the appendix.
|
|
12
27
|
*/
|
|
13
28
|
export async function runTest(ctx, args) {
|
|
29
|
+
var _a;
|
|
14
30
|
const loaded = loadProject(args.dir);
|
|
15
31
|
const pm = detectPackageManager(loaded.projectDir);
|
|
16
|
-
const
|
|
17
|
-
const
|
|
32
|
+
const spawnFn = (_a = args.spawnFn) !== null && _a !== void 0 ? _a : spawn;
|
|
33
|
+
const useReporter = args.raw !== true && projectTestScriptUsesVitest(loaded.projectDir);
|
|
34
|
+
const reportPath = useReporter ? newReportPath() : null;
|
|
35
|
+
const baseRunnerArgs = ['run', 'test', ...args.passthrough];
|
|
36
|
+
// A bare `--` before the injected reporter flags — npm treats anything
|
|
37
|
+
// after `run <script>` as ITS OWN CLI config unless told otherwise (it
|
|
38
|
+
// warns and silently drops `--reporter`/`--outputFile` rather than
|
|
39
|
+
// forwarding them to vitest); pnpm/yarn/bun already forward unrecognized
|
|
40
|
+
// args either way, so the extra separator is a harmless no-op for them.
|
|
41
|
+
// This only protects OUR OWN appended flags — a caller's own passthrough
|
|
42
|
+
// args keep today's exact (unseparated) behavior.
|
|
43
|
+
const runnerArgs = reportPath
|
|
44
|
+
? [...baseRunnerArgs, '--', ...reporterArgs(reportPath)]
|
|
45
|
+
: baseRunnerArgs;
|
|
46
|
+
const command = `${pm.bin} ${baseRunnerArgs.join(' ')}`;
|
|
18
47
|
ctx.log(`Running "${command}" in ${loaded.projectDir}…`);
|
|
19
48
|
const started = Date.now();
|
|
20
49
|
const exitCode = await new Promise((resolve, reject) => {
|
|
21
50
|
var _a, _b;
|
|
22
|
-
const child =
|
|
51
|
+
const child = spawnFn(pm.bin, runnerArgs, {
|
|
23
52
|
cwd: loaded.projectDir,
|
|
24
53
|
// stdin inherited; the child's stdout/stderr are piped so we can route
|
|
25
54
|
// them to OUR stderr and keep stdout reserved for the JSON envelope.
|
|
@@ -37,9 +66,56 @@ export async function runTest(ctx, args) {
|
|
|
37
66
|
child.on('close', (code) => resolve(code !== null && code !== void 0 ? code : 1));
|
|
38
67
|
});
|
|
39
68
|
const durationMs = Date.now() - started;
|
|
69
|
+
const report = reportPath
|
|
70
|
+
? parseTestReport(reportPath, loaded.projectDir)
|
|
71
|
+
: null;
|
|
72
|
+
if (reportPath) {
|
|
73
|
+
try {
|
|
74
|
+
fs.rmSync(reportPath, { force: true });
|
|
75
|
+
}
|
|
76
|
+
catch (_b) {
|
|
77
|
+
// Best-effort cleanup only — a leftover temp file is not worth failing
|
|
78
|
+
// (or even flagging) the command over.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
40
81
|
const data = { command, exitCode, durationMs };
|
|
82
|
+
if (report === null || report === void 0 ? void 0 : report.summary)
|
|
83
|
+
data.summary = report.summary;
|
|
41
84
|
if (exitCode === 0) {
|
|
42
|
-
|
|
85
|
+
const summary = report === null || report === void 0 ? void 0 : report.summary;
|
|
86
|
+
const humanLine = summary
|
|
87
|
+
? ctx.ui.ok(`${ctx.ui.glyphs.ok} test: ${summary.passed} test` +
|
|
88
|
+
`${summary.passed === 1 ? '' : 's'} passed in ${durationMs}ms.`)
|
|
89
|
+
: `test: OK — "${command}" passed in ${durationMs}ms.`;
|
|
90
|
+
return ctx.succeed(data, humanLine);
|
|
91
|
+
}
|
|
92
|
+
const hasAppendix = report !== null && report.findings.length > 0;
|
|
93
|
+
if (hasAppendix)
|
|
94
|
+
data.failures = report.findings;
|
|
95
|
+
if (!ctx.jsonMode && args.raw !== true) {
|
|
96
|
+
if (hasAppendix && report) {
|
|
97
|
+
ctx.log('');
|
|
98
|
+
ctx.log(ctx.ui.section('FAILURES'));
|
|
99
|
+
renderFindings(ctx, report.findings, { cwd: loaded.projectDir });
|
|
100
|
+
if (report.failingFiles.length === 1) {
|
|
101
|
+
ctx.log(ctx.ui.nextAction(`ekanos test -- run ${report.failingFiles[0]}`, 'fix, then re-run just this file'));
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
ctx.log(ctx.ui.dim(`Failing files:\n${report.failingFiles
|
|
105
|
+
.map((file) => ` ${file}`)
|
|
106
|
+
.join('\n')}`));
|
|
107
|
+
ctx.log(ctx.ui.nextAction('ekanos test', 'fix the above, then re-run'));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (useReporter) {
|
|
111
|
+
// Reporter was injected but the report never parsed (vitest crashed
|
|
112
|
+
// before writing it, or wrote something unrecognizable) — say so
|
|
113
|
+
// quietly and fall back to a generic re-run rather than staying silent
|
|
114
|
+
// or failing over a decoration.
|
|
115
|
+
ctx.log('');
|
|
116
|
+
ctx.log(ctx.ui.dim('(Structured failure details were unavailable for this run — see the output above.)'));
|
|
117
|
+
ctx.log(ctx.ui.nextAction('ekanos test', 'fix the above, then re-run'));
|
|
118
|
+
}
|
|
43
119
|
}
|
|
44
120
|
return ctx.failWith({
|
|
45
121
|
code: 'test_failed',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test.js","sourceRoot":"","sources":["../../src/commands/test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAG3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAQzC;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,GAAe,EACf,IAAc;IAEd,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,EAAE,GAAG,oBAAoB,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAEnD,MAAM,UAAU,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;IACxD,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,GAAG,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACpD,GAAG,CAAC,GAAG,CAAC,YAAY,OAAO,QAAQ,MAAM,CAAC,UAAU,GAAG,CAAC,CAAC;IAEzD,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC3B,MAAM,QAAQ,GAAG,MAAM,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;;QAC7D,MAAM,KAAK,GAAG,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,UAAU,EAAE;YACtC,GAAG,EAAE,MAAM,CAAC,UAAU;YACtB,uEAAuE;YACvE,qEAAqE;YACrE,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC;SACnC,CAAC,CAAC;QAEH,4EAA4E;QAC5E,sCAAsC;QACtC,MAAA,KAAK,CAAC,MAAM,0CAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACzC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;QACH,MAAA,KAAK,CAAC,MAAM,0CAAE,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACzC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5C,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,aAAJ,IAAI,cAAJ,IAAI,GAAI,CAAC,CAAC,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;IACxC,MAAM,IAAI,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;IAE/C,IAAI,QAAQ,KAAK,CAAC,EAAE,CAAC;QACnB,OAAO,GAAG,CAAC,OAAO,CAChB,IAAI,EACJ,eAAe,OAAO,eAAe,UAAU,KAAK,CACrD,CAAC;IACJ,CAAC;IAED,OAAO,GAAG,CAAC,QAAQ,CAAC;QAClB,IAAI,EAAE,aAAa;QACnB,OAAO,EAAE,IAAI,OAAO,sBAAsB,QAAQ,GAAG;QACrD,IAAI,EACF,mEAAmE;YACnE,uBAAuB;QACzB,yDAAyD;QACzD,QAAQ;QACR,IAAI;KACL,CAAC,CAAC;AACL,CAAC","sourcesContent":["import { spawn } from 'node:child_process';\n\nimport type { CliContext } from '../context';\nimport { detectPackageManager } from '../package-manager';\nimport { loadProject } from '../project';\n\nexport interface TestArgs {\n dir: string;\n /** argv after `--`, forwarded verbatim to the runner. */\n passthrough: string[];\n}\n\n/**\n * `test` — delegate to the partner's own test script; do NOT reimplement or\n * parse the runner's output. We detect the package manager from the lockfile,\n * spawn `<pm> run test` with any post-`--` args appended, stream the child's\n * output to OUR stderr (keeping stdout pure for the single JSON envelope), and\n * report `{ command, exitCode, durationMs }`.\n *\n * The CLI's exit code is a transparent passthrough of the runner's exit code.\n */\nexport async function runTest(\n ctx: CliContext,\n args: TestArgs,\n): Promise<number> {\n const loaded = loadProject(args.dir);\n const pm = detectPackageManager(loaded.projectDir);\n\n const runnerArgs = ['run', 'test', ...args.passthrough];\n const command = `${pm.bin} ${runnerArgs.join(' ')}`;\n ctx.log(`Running \"${command}\" in ${loaded.projectDir}…`);\n\n const started = Date.now();\n const exitCode = await new Promise<number>((resolve, reject) => {\n const child = spawn(pm.bin, runnerArgs, {\n cwd: loaded.projectDir,\n // stdin inherited; the child's stdout/stderr are piped so we can route\n // them to OUR stderr and keep stdout reserved for the JSON envelope.\n stdio: ['inherit', 'pipe', 'pipe'],\n });\n\n // In JSON mode every byte of runner output goes to stderr; in human mode we\n // keep the child's stream separation.\n child.stdout?.on('data', (chunk: Buffer) => {\n (ctx.jsonMode ? process.stderr : process.stdout).write(chunk);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n process.stderr.write(chunk);\n });\n\n child.on('error', (error) => reject(error));\n child.on('close', (code) => resolve(code ?? 1));\n });\n\n const durationMs = Date.now() - started;\n const data = { command, exitCode, durationMs };\n\n if (exitCode === 0) {\n return ctx.succeed(\n data,\n `test: OK — \"${command}\" passed in ${durationMs}ms.`,\n );\n }\n\n return ctx.failWith({\n code: 'test_failed',\n message: `\"${command}\" exited with code ${exitCode}.`,\n hint:\n 'Read the runner output on stderr and fix the failing tests, then ' +\n 're-run \"ekanos test\".',\n // Transparent passthrough of the runner's own exit code.\n exitCode,\n data,\n });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"test.js","sourceRoot":"","sources":["../../src/commands/test.ts"],"names":[],"mappings":"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","sourcesContent":["import { type spawn as nodeSpawn, spawn } from 'node:child_process';\nimport * as fs from 'node:fs';\n\nimport type { CliContext } from '../context';\nimport { type RenderableFinding, renderFindings } from '../findings-renderer';\nimport { detectPackageManager } from '../package-manager';\nimport { loadProject } from '../project';\nimport {\n newReportPath,\n parseTestReport,\n projectTestScriptUsesVitest,\n reporterArgs,\n} from '../test-report';\n\nexport interface TestArgs {\n dir: string;\n /** argv after `--`, forwarded verbatim to the runner. */\n passthrough: string[];\n /**\n * Escape hatch: skips reporter injection and the curated failures\n * appendix entirely, reproducing exactly the pre-appendix raw-passthrough\n * behavior. Also the sanctioned bypass when the JSON-reporter detection\n * (below) ever guesses wrong for a project.\n */\n raw?: boolean;\n /** Injectable so tests never fork a real process; defaults to the real `node:child_process` spawn. */\n spawnFn?: typeof nodeSpawn;\n}\n\n/**\n * `test` — delegate to the partner's own test script; do NOT reimplement or\n * parse the runner's output ourselves beyond the OPTIONAL structured\n * appendix below. We detect the package manager from the lockfile, spawn\n * `<pm> run test` with any post-`--` args appended, stream the child's\n * output to OUR stderr (keeping stdout pure for the single JSON envelope),\n * and report `{ command, exitCode, durationMs }`.\n *\n * When the project's OWN `test` script names vitest (and `--raw` wasn't\n * passed), we ALSO ask vitest for a `--reporter=json` report alongside its\n * normal terminal output — mirroring how the server-side gate grades a\n * submission's test run, so local triage has the same per-test intelligence\n * as CI. On failure, that report drives a curated \"FAILURES\" appendix\n * (`../findings-renderer.ts`) rendered AFTER the child's own live stream\n * ends; on success, it drives a one-line pass count. Reading/parsing the\n * report is entirely best-effort (`test-report.ts`): anything wrong with it\n * silently drops the appendix, never fails the command.\n *\n * The CLI's exit code is always a transparent passthrough of the runner's\n * exit code, with or without the appendix.\n */\nexport async function runTest(\n ctx: CliContext,\n args: TestArgs,\n): Promise<number> {\n const loaded = loadProject(args.dir);\n const pm = detectPackageManager(loaded.projectDir);\n const spawnFn = args.spawnFn ?? spawn;\n\n const useReporter =\n args.raw !== true && projectTestScriptUsesVitest(loaded.projectDir);\n const reportPath = useReporter ? newReportPath() : null;\n\n const baseRunnerArgs = ['run', 'test', ...args.passthrough];\n // A bare `--` before the injected reporter flags — npm treats anything\n // after `run <script>` as ITS OWN CLI config unless told otherwise (it\n // warns and silently drops `--reporter`/`--outputFile` rather than\n // forwarding them to vitest); pnpm/yarn/bun already forward unrecognized\n // args either way, so the extra separator is a harmless no-op for them.\n // This only protects OUR OWN appended flags — a caller's own passthrough\n // args keep today's exact (unseparated) behavior.\n const runnerArgs = reportPath\n ? [...baseRunnerArgs, '--', ...reporterArgs(reportPath)]\n : baseRunnerArgs;\n const command = `${pm.bin} ${baseRunnerArgs.join(' ')}`;\n ctx.log(`Running \"${command}\" in ${loaded.projectDir}…`);\n\n const started = Date.now();\n const exitCode = await new Promise<number>((resolve, reject) => {\n const child = spawnFn(pm.bin, runnerArgs, {\n cwd: loaded.projectDir,\n // stdin inherited; the child's stdout/stderr are piped so we can route\n // them to OUR stderr and keep stdout reserved for the JSON envelope.\n stdio: ['inherit', 'pipe', 'pipe'],\n });\n\n // In JSON mode every byte of runner output goes to stderr; in human mode we\n // keep the child's stream separation.\n child.stdout?.on('data', (chunk: Buffer) => {\n (ctx.jsonMode ? process.stderr : process.stdout).write(chunk);\n });\n child.stderr?.on('data', (chunk: Buffer) => {\n process.stderr.write(chunk);\n });\n\n child.on('error', (error) => reject(error));\n child.on('close', (code) => resolve(code ?? 1));\n });\n\n const durationMs = Date.now() - started;\n\n const report = reportPath\n ? parseTestReport(reportPath, loaded.projectDir)\n : null;\n if (reportPath) {\n try {\n fs.rmSync(reportPath, { force: true });\n } catch {\n // Best-effort cleanup only — a leftover temp file is not worth failing\n // (or even flagging) the command over.\n }\n }\n\n const data: {\n command: string;\n exitCode: number;\n durationMs: number;\n summary?: { passed: number; failed: number; files: number };\n failures?: RenderableFinding[];\n } = { command, exitCode, durationMs };\n if (report?.summary) data.summary = report.summary;\n\n if (exitCode === 0) {\n const summary = report?.summary;\n const humanLine = summary\n ? ctx.ui.ok(\n `${ctx.ui.glyphs.ok} test: ${summary.passed} test` +\n `${summary.passed === 1 ? '' : 's'} passed in ${durationMs}ms.`,\n )\n : `test: OK — \"${command}\" passed in ${durationMs}ms.`;\n return ctx.succeed(data, humanLine);\n }\n\n const hasAppendix = report !== null && report.findings.length > 0;\n if (hasAppendix) data.failures = report.findings;\n\n if (!ctx.jsonMode && args.raw !== true) {\n if (hasAppendix && report) {\n ctx.log('');\n ctx.log(ctx.ui.section('FAILURES'));\n renderFindings(ctx, report.findings, { cwd: loaded.projectDir });\n\n if (report.failingFiles.length === 1) {\n ctx.log(\n ctx.ui.nextAction(\n `ekanos test -- run ${report.failingFiles[0]}`,\n 'fix, then re-run just this file',\n ),\n );\n } else {\n ctx.log(\n ctx.ui.dim(\n `Failing files:\\n${report.failingFiles\n .map((file) => ` ${file}`)\n .join('\\n')}`,\n ),\n );\n ctx.log(ctx.ui.nextAction('ekanos test', 'fix the above, then re-run'));\n }\n } else if (useReporter) {\n // Reporter was injected but the report never parsed (vitest crashed\n // before writing it, or wrote something unrecognizable) — say so\n // quietly and fall back to a generic re-run rather than staying silent\n // or failing over a decoration.\n ctx.log('');\n ctx.log(\n ctx.ui.dim(\n '(Structured failure details were unavailable for this run — see the output above.)',\n ),\n );\n ctx.log(ctx.ui.nextAction('ekanos test', 'fix the above, then re-run'));\n }\n }\n\n return ctx.failWith({\n code: 'test_failed',\n message: `\"${command}\" exited with code ${exitCode}.`,\n hint:\n 'Read the runner output on stderr and fix the failing tests, then ' +\n 're-run \"ekanos test\".',\n // Transparent passthrough of the runner's own exit code.\n exitCode,\n data,\n });\n}\n"]}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { acceptedKeysAt, extractUnrecognizedKeys, suggestKey, } from '../did-you-mean.js';
|
|
1
4
|
import { validationError } from '../errors.js';
|
|
5
|
+
import { renderFindings } from '../findings-renderer.js';
|
|
2
6
|
import { loadProject } from '../project.js';
|
|
3
7
|
import { assertSchemaNotSkewed } from '../schema-skew.js';
|
|
8
|
+
import { ENCLOSING_NOTE, extractPathFromMessage, isRootPath, locateDotPath, } from '../source-locator.js';
|
|
4
9
|
import { announceUpdate, checkForUpdate } from '../update-notice.js';
|
|
5
10
|
import { collectAllFindings } from '../validate-findings.js';
|
|
6
11
|
/**
|
|
@@ -27,6 +32,7 @@ export async function runValidate(ctx, args) {
|
|
|
27
32
|
ctx.log(`Validating ${loaded.integrations.length} integration` +
|
|
28
33
|
`${loaded.integrations.length === 1 ? '' : 's'} (${slugs})…`);
|
|
29
34
|
const findings = await collectAllFindings(loaded);
|
|
35
|
+
enrichSchemaFindings(findings, loaded.projectDir);
|
|
30
36
|
const errorCount = findings.filter((f) => f.severity === 'error').length;
|
|
31
37
|
const data = Object.assign(Object.assign({ slug: loaded.primary.slug, integrations: loaded.integrations.map((i) => ({
|
|
32
38
|
slug: i.slug,
|
|
@@ -41,9 +47,115 @@ export async function runValidate(ctx, args) {
|
|
|
41
47
|
}
|
|
42
48
|
: {})), (notice ? { toolchain: notice } : {}));
|
|
43
49
|
if (errorCount > 0) {
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
// Render the shared findings block to stderr BEFORE the envelope
|
|
51
|
+
// settles, human mode only — JSON mode already carries the exact same
|
|
52
|
+
// findings in `data.findings` and must stay byte-identical. The closing
|
|
53
|
+
// order is findings block -> short summary (via `ctx.fail`'s
|
|
54
|
+
// `findingsSummary` option) -> next action: `log` doesn't care that the
|
|
55
|
+
// context already settled, so it's safe to call after `fail`.
|
|
56
|
+
if (!ctx.jsonMode) {
|
|
57
|
+
renderFindings(ctx, findings, { cwd: loaded.projectDir });
|
|
58
|
+
}
|
|
59
|
+
const exitCode = ctx.fail(validationError(`${errorCount} validation finding${errorCount === 1 ? '' : 's'}.`, 'Resolve the error-severity findings in data.findings, then re-run ' +
|
|
60
|
+
'"ekanos validate".'), data, !ctx.jsonMode
|
|
61
|
+
? { findingsSummary: { command: 'ekanos validate', errorCount } }
|
|
62
|
+
: undefined);
|
|
63
|
+
if (!ctx.jsonMode) {
|
|
64
|
+
ctx.log(ctx.ui.nextAction('ekanos validate', 'after fixing, re-check'));
|
|
65
|
+
}
|
|
66
|
+
return exitCode;
|
|
67
|
+
}
|
|
68
|
+
const successLine = ctx.jsonMode
|
|
69
|
+
? `validate: OK — no findings for ${slugs}.`
|
|
70
|
+
: ctx.ui.ok(`${ctx.ui.glyphs.ok} validate: no findings for ${slugs}.`);
|
|
71
|
+
return ctx.succeed(data, successLine);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Best-effort dot-path → `file:line` + "did you mean?" enrichment for
|
|
75
|
+
* `definition.schema` findings — the ones `collectDefinitionFindings` builds
|
|
76
|
+
* directly from a zod issue (see its doc comment in
|
|
77
|
+
* `@ekanos/integration-schema`). Mutates `line` (already part of `Finding`'s
|
|
78
|
+
* shape, just usually absent) and, for an unrecognized key with a close
|
|
79
|
+
* match, `hint`; for a resolved-but-missing key it appends a plain-text (no
|
|
80
|
+
* ANSI — `findings-renderer.ts` styles it at render time) note to `message`.
|
|
81
|
+
* These are additive changes to `data.findings` in `--json` mode too — the
|
|
82
|
+
* envelope's existing fields never change shape or disappear across a minor
|
|
83
|
+
* release, so a partner's own JSON parsing keeps working; only new optional
|
|
84
|
+
* fields are ever added.
|
|
85
|
+
*
|
|
86
|
+
* Every other finding, and every locator miss, passes through unchanged:
|
|
87
|
+
* `source-locator.ts`'s whole design is to never guess, so a miss here is
|
|
88
|
+
* silent, not an error.
|
|
89
|
+
*/
|
|
90
|
+
function enrichSchemaFindings(findings, projectDir) {
|
|
91
|
+
const sourceCache = new Map();
|
|
92
|
+
const readSource = (file) => {
|
|
93
|
+
const cached = sourceCache.get(file);
|
|
94
|
+
if (cached !== undefined)
|
|
95
|
+
return cached;
|
|
96
|
+
let text;
|
|
97
|
+
try {
|
|
98
|
+
text = fs.readFileSync(path.resolve(projectDir, file), 'utf8');
|
|
99
|
+
}
|
|
100
|
+
catch (_a) {
|
|
101
|
+
// Best-effort — an unreadable file (deleted, permissions, a race with
|
|
102
|
+
// a concurrent edit) just means no enrichment, never a validate
|
|
103
|
+
// failure.
|
|
104
|
+
text = null;
|
|
105
|
+
}
|
|
106
|
+
sourceCache.set(file, text);
|
|
107
|
+
return text;
|
|
108
|
+
};
|
|
109
|
+
for (const finding of findings) {
|
|
110
|
+
if (finding.check !== 'definition.schema')
|
|
111
|
+
continue;
|
|
112
|
+
if (finding.line !== undefined || finding.file === undefined)
|
|
113
|
+
continue;
|
|
114
|
+
const parsed = extractPathFromMessage(finding.message);
|
|
115
|
+
if (!parsed)
|
|
116
|
+
continue;
|
|
117
|
+
const source = readSource(finding.file);
|
|
118
|
+
if (source === null)
|
|
119
|
+
continue;
|
|
120
|
+
const unrecognizedKeys = extractUnrecognizedKeys(parsed.rest);
|
|
121
|
+
if (unrecognizedKeys) {
|
|
122
|
+
applyUnrecognizedKeyEnrichment(finding, source, parsed.path, unrecognizedKeys);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
applyLocation(finding, locateDotPath(source, parsed.path));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/** Sets `line`/`message` from a locate result — shared by the plain schema-path case and the unrecognized-key case below. A `'none'` result changes nothing. */
|
|
129
|
+
function applyLocation(finding, result) {
|
|
130
|
+
if (result.confidence === 'none')
|
|
131
|
+
return;
|
|
132
|
+
finding.line = result.line;
|
|
133
|
+
if (result.confidence === 'enclosing') {
|
|
134
|
+
finding.message = `${finding.message} ${ENCLOSING_NOTE}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function applyUnrecognizedKeyEnrichment(finding, source, parentPath, badKeys) {
|
|
138
|
+
const candidates = acceptedKeysAt(parentPath);
|
|
139
|
+
const suggestions = badKeys
|
|
140
|
+
.map((bad) => {
|
|
141
|
+
const suggestion = suggestKey(bad, candidates);
|
|
142
|
+
return suggestion ? { bad, suggestion } : null;
|
|
143
|
+
})
|
|
144
|
+
.filter((entry) => entry !== null);
|
|
145
|
+
const [onlySuggestion] = suggestions;
|
|
146
|
+
if (suggestions.length === 1 && badKeys.length === 1 && onlySuggestion) {
|
|
147
|
+
finding.hint = `did you mean "${onlySuggestion.suggestion}"?`;
|
|
148
|
+
}
|
|
149
|
+
else if (suggestions.length > 0) {
|
|
150
|
+
finding.hint =
|
|
151
|
+
suggestions.map((s) => `"${s.bad}" → "${s.suggestion}"`).join(', ') + '?';
|
|
46
152
|
}
|
|
47
|
-
|
|
153
|
+
const [firstBadKey] = badKeys;
|
|
154
|
+
if (firstBadKey === undefined)
|
|
155
|
+
return;
|
|
156
|
+
const fullPath = isRootPath(parentPath)
|
|
157
|
+
? firstBadKey
|
|
158
|
+
: `${parentPath}.${firstBadKey}`;
|
|
159
|
+
applyLocation(finding, locateDotPath(source, fullPath));
|
|
48
160
|
}
|
|
49
161
|
//# sourceMappingURL=validate.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAE5C,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClE,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAQ1D;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,GAAe,EACf,IAAkB;;IAElB,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAErC,MAAM,IAAI,GAAG,qBAAqB,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IAE3D,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,EAAE,GAAG,EAAE,MAAA,IAAI,CAAC,GAAG,mCAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACtE,cAAc,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAE5B,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,GAAG,CAAC,GAAG,CACL,cAAc,MAAM,CAAC,YAAY,CAAC,MAAM,cAAc;QACpD,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,KAAK,IAAI,CAC/D,CAAC;IAEF,MAAM,QAAQ,GAAG,MAAM,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAElD,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;IACzE,MAAM,IAAI,iCACR,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,EACzB,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC5C,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,KAAK,EAAE,CAAC,CAAC,KAAK;SACf,CAAC,CAAC,EACH,QAAQ,IACL,CAAC,IAAI,CAAC,MAAM,KAAK,eAAe;QACjC,CAAC,CAAC;YACE,UAAU,EAAE;gBACV,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;gBACvC,oBAAoB,EAAE,IAAI,CAAC,oBAAoB;aAChD;SACF;QACH,CAAC,CAAC,EAAE,CAAC,GACJ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CACzC,CAAC;IAEF,IAAI,UAAU,GAAG,CAAC,EAAE,CAAC;QACnB,OAAO,GAAG,CAAC,IAAI,CACb,eAAe,CACb,GAAG,UAAU,sBAAsB,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,GAAG,EACjE,oEAAoE;YAClE,oBAAoB,CACvB,EACD,IAAI,CACL,CAAC;IACJ,CAAC;IAED,OAAO,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,kCAAkC,KAAK,GAAG,CAAC,CAAC;AACvE,CAAC","sourcesContent":["import type { CliContext } from '../context';\nimport { validationError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { loadProject } from '../project';\nimport { assertSchemaNotSkewed } from '../schema-skew';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\nimport { collectAllFindings } from '../validate-findings';\n\nexport interface ValidateArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n env?: Record<string, string | undefined>;\n}\n\n/**\n * `validate` — parse the declaration with the REAL zod schemas from\n * `@ekanos/integration-schema` (never a reimplementation), gather structured\n * findings, and add project-level integrity checks (the vitest inline-SDK\n * check). Exit 0 with `findings: []` when clean; exit 3 when any error-severity\n * finding is present, with the findings carried in the envelope's `data`.\n *\n * The findings pass itself lives in `validate-findings.ts`, shared with\n * `publish` so the publish gate cannot drift from what validate checks.\n *\n * Runs the schema-skew gate FIRST, before anything else — see\n * `schema-skew.ts` for why a newer project schema than the CLI bundles makes\n * every finding below it unreliable.\n */\nexport async function runValidate(\n ctx: CliContext,\n args: ValidateArgs,\n): Promise<ExitCode> {\n const loaded = loadProject(args.dir);\n\n const skew = assertSchemaNotSkewed(ctx, loaded.projectDir);\n\n const notice = await checkForUpdate({ env: args.env ?? process.env });\n announceUpdate(ctx, notice);\n\n const slugs = loaded.integrations.map((i) => i.slug).join(', ');\n ctx.log(\n `Validating ${loaded.integrations.length} integration` +\n `${loaded.integrations.length === 1 ? '' : 's'} (${slugs})…`,\n );\n\n const findings = await collectAllFindings(loaded);\n\n const errorCount = findings.filter((f) => f.severity === 'error').length;\n const data = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => ({\n slug: i.slug,\n entry: i.entry,\n })),\n findings,\n ...(skew.status === 'project-older'\n ? {\n schemaSkew: {\n status: skew.status,\n cliSchemaVersion: skew.cliSchemaVersion,\n projectSchemaVersion: skew.projectSchemaVersion,\n },\n }\n : {}),\n ...(notice ? { toolchain: notice } : {}),\n };\n\n if (errorCount > 0) {\n return ctx.fail(\n validationError(\n `${errorCount} validation finding${errorCount === 1 ? '' : 's'}.`,\n 'Resolve the error-severity findings in data.findings, then re-run ' +\n '\"ekanos validate\".',\n ),\n data,\n );\n }\n\n return ctx.succeed(data, `validate: OK — no findings for ${slugs}.`);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/commands/validate.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Finding } from '@ekanos/integration-schema';\nimport * as fs from 'node:fs';\nimport * as path from 'node:path';\n\nimport type { CliContext } from '../context';\nimport {\n acceptedKeysAt,\n extractUnrecognizedKeys,\n suggestKey,\n} from '../did-you-mean';\nimport { validationError } from '../errors';\nimport type { ExitCode } from '../exit-codes';\nimport { renderFindings } from '../findings-renderer';\nimport { loadProject } from '../project';\nimport { assertSchemaNotSkewed } from '../schema-skew';\nimport {\n ENCLOSING_NOTE,\n extractPathFromMessage,\n isRootPath,\n locateDotPath,\n} from '../source-locator';\nimport { announceUpdate, checkForUpdate } from '../update-notice';\nimport { collectAllFindings } from '../validate-findings';\n\nexport interface ValidateArgs {\n /** Project directory (contains ekanos.json). Defaults to cwd. */\n dir: string;\n env?: Record<string, string | undefined>;\n}\n\n/**\n * `validate` — parse the declaration with the REAL zod schemas from\n * `@ekanos/integration-schema` (never a reimplementation), gather structured\n * findings, and add project-level integrity checks (the vitest inline-SDK\n * check). Exit 0 with `findings: []` when clean; exit 3 when any error-severity\n * finding is present, with the findings carried in the envelope's `data`.\n *\n * The findings pass itself lives in `validate-findings.ts`, shared with\n * `publish` so the publish gate cannot drift from what validate checks.\n *\n * Runs the schema-skew gate FIRST, before anything else — see\n * `schema-skew.ts` for why a newer project schema than the CLI bundles makes\n * every finding below it unreliable.\n */\nexport async function runValidate(\n ctx: CliContext,\n args: ValidateArgs,\n): Promise<ExitCode> {\n const loaded = loadProject(args.dir);\n\n const skew = assertSchemaNotSkewed(ctx, loaded.projectDir);\n\n const notice = await checkForUpdate({ env: args.env ?? process.env });\n announceUpdate(ctx, notice);\n\n const slugs = loaded.integrations.map((i) => i.slug).join(', ');\n ctx.log(\n `Validating ${loaded.integrations.length} integration` +\n `${loaded.integrations.length === 1 ? '' : 's'} (${slugs})…`,\n );\n\n const findings = await collectAllFindings(loaded);\n enrichSchemaFindings(findings, loaded.projectDir);\n\n const errorCount = findings.filter((f) => f.severity === 'error').length;\n const data = {\n slug: loaded.primary.slug,\n integrations: loaded.integrations.map((i) => ({\n slug: i.slug,\n entry: i.entry,\n })),\n findings,\n ...(skew.status === 'project-older'\n ? {\n schemaSkew: {\n status: skew.status,\n cliSchemaVersion: skew.cliSchemaVersion,\n projectSchemaVersion: skew.projectSchemaVersion,\n },\n }\n : {}),\n ...(notice ? { toolchain: notice } : {}),\n };\n\n if (errorCount > 0) {\n // Render the shared findings block to stderr BEFORE the envelope\n // settles, human mode only — JSON mode already carries the exact same\n // findings in `data.findings` and must stay byte-identical. The closing\n // order is findings block -> short summary (via `ctx.fail`'s\n // `findingsSummary` option) -> next action: `log` doesn't care that the\n // context already settled, so it's safe to call after `fail`.\n if (!ctx.jsonMode) {\n renderFindings(ctx, findings, { cwd: loaded.projectDir });\n }\n\n const exitCode = ctx.fail(\n validationError(\n `${errorCount} validation finding${errorCount === 1 ? '' : 's'}.`,\n 'Resolve the error-severity findings in data.findings, then re-run ' +\n '\"ekanos validate\".',\n ),\n data,\n !ctx.jsonMode\n ? { findingsSummary: { command: 'ekanos validate', errorCount } }\n : undefined,\n );\n\n if (!ctx.jsonMode) {\n ctx.log(ctx.ui.nextAction('ekanos validate', 'after fixing, re-check'));\n }\n\n return exitCode;\n }\n\n const successLine = ctx.jsonMode\n ? `validate: OK — no findings for ${slugs}.`\n : ctx.ui.ok(`${ctx.ui.glyphs.ok} validate: no findings for ${slugs}.`);\n\n return ctx.succeed(data, successLine);\n}\n\n/**\n * Best-effort dot-path → `file:line` + \"did you mean?\" enrichment for\n * `definition.schema` findings — the ones `collectDefinitionFindings` builds\n * directly from a zod issue (see its doc comment in\n * `@ekanos/integration-schema`). Mutates `line` (already part of `Finding`'s\n * shape, just usually absent) and, for an unrecognized key with a close\n * match, `hint`; for a resolved-but-missing key it appends a plain-text (no\n * ANSI — `findings-renderer.ts` styles it at render time) note to `message`.\n * These are additive changes to `data.findings` in `--json` mode too — the\n * envelope's existing fields never change shape or disappear across a minor\n * release, so a partner's own JSON parsing keeps working; only new optional\n * fields are ever added.\n *\n * Every other finding, and every locator miss, passes through unchanged:\n * `source-locator.ts`'s whole design is to never guess, so a miss here is\n * silent, not an error.\n */\nfunction enrichSchemaFindings(findings: Finding[], projectDir: string): void {\n const sourceCache = new Map<string, string | null>();\n\n const readSource = (file: string): string | null => {\n const cached = sourceCache.get(file);\n if (cached !== undefined) return cached;\n\n let text: string | null;\n try {\n text = fs.readFileSync(path.resolve(projectDir, file), 'utf8');\n } catch {\n // Best-effort — an unreadable file (deleted, permissions, a race with\n // a concurrent edit) just means no enrichment, never a validate\n // failure.\n text = null;\n }\n sourceCache.set(file, text);\n return text;\n };\n\n for (const finding of findings) {\n if (finding.check !== 'definition.schema') continue;\n if (finding.line !== undefined || finding.file === undefined) continue;\n\n const parsed = extractPathFromMessage(finding.message);\n if (!parsed) continue;\n\n const source = readSource(finding.file);\n if (source === null) continue;\n\n const unrecognizedKeys = extractUnrecognizedKeys(parsed.rest);\n if (unrecognizedKeys) {\n applyUnrecognizedKeyEnrichment(\n finding,\n source,\n parsed.path,\n unrecognizedKeys,\n );\n continue;\n }\n\n applyLocation(finding, locateDotPath(source, parsed.path));\n }\n}\n\n/** Sets `line`/`message` from a locate result — shared by the plain schema-path case and the unrecognized-key case below. A `'none'` result changes nothing. */\nfunction applyLocation(\n finding: Finding,\n result: ReturnType<typeof locateDotPath>,\n): void {\n if (result.confidence === 'none') return;\n\n finding.line = result.line;\n if (result.confidence === 'enclosing') {\n finding.message = `${finding.message} ${ENCLOSING_NOTE}`;\n }\n}\n\nfunction applyUnrecognizedKeyEnrichment(\n finding: Finding,\n source: string,\n parentPath: string,\n badKeys: string[],\n): void {\n const candidates = acceptedKeysAt(parentPath);\n const suggestions = badKeys\n .map((bad) => {\n const suggestion = suggestKey(bad, candidates);\n return suggestion ? { bad, suggestion } : null;\n })\n .filter(\n (entry): entry is { bad: string; suggestion: string } => entry !== null,\n );\n\n const [onlySuggestion] = suggestions;\n if (suggestions.length === 1 && badKeys.length === 1 && onlySuggestion) {\n finding.hint = `did you mean \"${onlySuggestion.suggestion}\"?`;\n } else if (suggestions.length > 0) {\n finding.hint =\n suggestions.map((s) => `\"${s.bad}\" → \"${s.suggestion}\"`).join(', ') + '?';\n }\n\n const [firstBadKey] = badKeys;\n if (firstBadKey === undefined) return;\n\n const fullPath = isRootPath(parentPath)\n ? firstBadKey\n : `${parentPath}.${firstBadKey}`;\n applyLocation(finding, locateDotPath(source, fullPath));\n}\n"]}
|
package/dist/context.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CliError } from './errors.js';
|
|
2
2
|
import { type ExitCode } from './exit-codes.js';
|
|
3
|
+
import { type Ui } from './ui.js';
|
|
3
4
|
/**
|
|
4
5
|
* A single writable sink. Real streams satisfy this; tests pass a capturing
|
|
5
6
|
* buffer so the stdout-purity invariant can be asserted byte-for-byte.
|
|
@@ -20,6 +21,15 @@ export interface ContextInput {
|
|
|
20
21
|
/** Overrides process.env for CLAUDECODE detection (tests). */
|
|
21
22
|
env?: Record<string, string | undefined>;
|
|
22
23
|
}
|
|
24
|
+
/** See `fail`'s doc comment — the human-mode epilogue seam for a command that already rendered a findings block. */
|
|
25
|
+
interface FindingsSummary {
|
|
26
|
+
/** The command name to name in the summary line, e.g. "ekanos validate". */
|
|
27
|
+
command: string;
|
|
28
|
+
errorCount: number;
|
|
29
|
+
}
|
|
30
|
+
export interface FailOptions {
|
|
31
|
+
findingsSummary?: FindingsSummary;
|
|
32
|
+
}
|
|
23
33
|
/**
|
|
24
34
|
* `data` is deliberately `unknown` above — each verb owns its own shape — but
|
|
25
35
|
* one convention spans several of them: `validate`, `dev`, `publish` and
|
|
@@ -49,12 +59,26 @@ export declare class CliContext {
|
|
|
49
59
|
* every call site threading `process.env` through by hand.
|
|
50
60
|
*/
|
|
51
61
|
readonly env: Readonly<Record<string, string | undefined>>;
|
|
62
|
+
/**
|
|
63
|
+
* The style layer (`ui.ts`), resolved once against stderr — the stream
|
|
64
|
+
* every human-mode render target uses — so every command shares one color
|
|
65
|
+
* decision instead of each re-deriving it. See `ui.ts` for the tokens and
|
|
66
|
+
* components this carries.
|
|
67
|
+
*/
|
|
68
|
+
readonly ui: Ui;
|
|
52
69
|
private readonly stdout;
|
|
53
70
|
private readonly stderr;
|
|
54
71
|
private settled;
|
|
55
72
|
constructor(input?: ContextInput);
|
|
56
73
|
/** Whether stderr is a real terminal — decoration (the banner) gates on this. */
|
|
57
74
|
get stderrIsTTY(): boolean;
|
|
75
|
+
/**
|
|
76
|
+
* Whether stdout is a real terminal — `wizard.ts`'s `canWizard` gates on
|
|
77
|
+
* this (clack renders to stdout, unlike the rest of this CLI's human
|
|
78
|
+
* output). `ctx.jsonMode` being false does not by itself guarantee this:
|
|
79
|
+
* `--no-json` forces human mode even against a piped stdout.
|
|
80
|
+
*/
|
|
81
|
+
get stdoutIsTTY(): boolean;
|
|
58
82
|
/** Human/progress output — ALWAYS stderr, so stdout stays pure in JSON mode. */
|
|
59
83
|
log(message: string): void;
|
|
60
84
|
/** A warning — stderr, no ANSI. */
|
|
@@ -78,8 +102,18 @@ export declare class CliContext {
|
|
|
78
102
|
* `error.hint` is guaranteed non-empty by CliError's constructor, so the
|
|
79
103
|
* envelope's "every error carries a hint" invariant holds for every path
|
|
80
104
|
* that reaches here.
|
|
105
|
+
*
|
|
106
|
+
* `options.findingsSummary` is the seam for a command that has ALREADY
|
|
107
|
+
* rendered the shared findings block to stderr (human mode) before calling
|
|
108
|
+
* this — `validate`'s and `publish`'s gate-refusal paths. Passing it swaps
|
|
109
|
+
* the default two-line `Error: … / Hint: …` epilogue (whose hint always
|
|
110
|
+
* points at the JSON-only `data.findings`) for one short summary line,
|
|
111
|
+
* `✖ <command> failed: <n> error(s)`, so a partner never reads the same
|
|
112
|
+
* finding jargon twice. JSON mode is completely unaffected either way —
|
|
113
|
+
* `error.hint` always carries the original, JSON-only-appropriate text in
|
|
114
|
+
* the envelope.
|
|
81
115
|
*/
|
|
82
|
-
fail(error: CliError, data?: unknown): ExitCode;
|
|
116
|
+
fail(error: CliError, data?: unknown, options?: FailOptions): ExitCode;
|
|
83
117
|
/**
|
|
84
118
|
* The low-level failure emitter. Used directly by `test`, whose exit code is
|
|
85
119
|
* a TRANSPARENT passthrough of the delegated runner's exit code (which is not
|
|
@@ -93,6 +127,8 @@ export declare class CliContext {
|
|
|
93
127
|
hint: string;
|
|
94
128
|
exitCode: number;
|
|
95
129
|
data?: unknown;
|
|
130
|
+
findingsSummary?: FindingsSummary;
|
|
96
131
|
}): number;
|
|
97
132
|
private settle;
|
|
98
133
|
}
|
|
134
|
+
export {};
|
package/dist/context.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EXIT_CODES } from './exit-codes.js';
|
|
2
|
+
import { createUi } from './ui.js';
|
|
2
3
|
/**
|
|
3
4
|
* `data` is deliberately `unknown` above — each verb owns its own shape — but
|
|
4
5
|
* one convention spans several of them: `validate`, `dev`, `publish` and
|
|
@@ -30,6 +31,7 @@ export class CliContext {
|
|
|
30
31
|
this.stdout = stdout;
|
|
31
32
|
this.stderr = stderr;
|
|
32
33
|
this.env = env;
|
|
34
|
+
this.ui = createUi({ isTTY: stderr.isTTY === true, env });
|
|
33
35
|
// Resolve JSON mode once. An EXPLICIT flag always wins: `--json` forces the
|
|
34
36
|
// envelope on even at a terminal, `--no-json` forces the human form even
|
|
35
37
|
// when piped. Only when the user expressed no preference is the mode
|
|
@@ -42,6 +44,15 @@ export class CliContext {
|
|
|
42
44
|
get stderrIsTTY() {
|
|
43
45
|
return this.stderr.isTTY === true;
|
|
44
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Whether stdout is a real terminal — `wizard.ts`'s `canWizard` gates on
|
|
49
|
+
* this (clack renders to stdout, unlike the rest of this CLI's human
|
|
50
|
+
* output). `ctx.jsonMode` being false does not by itself guarantee this:
|
|
51
|
+
* `--no-json` forces human mode even against a piped stdout.
|
|
52
|
+
*/
|
|
53
|
+
get stdoutIsTTY() {
|
|
54
|
+
return this.stdout.isTTY === true;
|
|
55
|
+
}
|
|
45
56
|
/** Human/progress output — ALWAYS stderr, so stdout stays pure in JSON mode. */
|
|
46
57
|
log(message) {
|
|
47
58
|
this.stderr.write(`${message}\n`);
|
|
@@ -81,14 +92,25 @@ export class CliContext {
|
|
|
81
92
|
* `error.hint` is guaranteed non-empty by CliError's constructor, so the
|
|
82
93
|
* envelope's "every error carries a hint" invariant holds for every path
|
|
83
94
|
* that reaches here.
|
|
95
|
+
*
|
|
96
|
+
* `options.findingsSummary` is the seam for a command that has ALREADY
|
|
97
|
+
* rendered the shared findings block to stderr (human mode) before calling
|
|
98
|
+
* this — `validate`'s and `publish`'s gate-refusal paths. Passing it swaps
|
|
99
|
+
* the default two-line `Error: … / Hint: …` epilogue (whose hint always
|
|
100
|
+
* points at the JSON-only `data.findings`) for one short summary line,
|
|
101
|
+
* `✖ <command> failed: <n> error(s)`, so a partner never reads the same
|
|
102
|
+
* finding jargon twice. JSON mode is completely unaffected either way —
|
|
103
|
+
* `error.hint` always carries the original, JSON-only-appropriate text in
|
|
104
|
+
* the envelope.
|
|
84
105
|
*/
|
|
85
|
-
fail(error, data) {
|
|
106
|
+
fail(error, data, options) {
|
|
86
107
|
this.failWith({
|
|
87
108
|
code: error.code,
|
|
88
109
|
message: error.message,
|
|
89
110
|
hint: error.hint,
|
|
90
111
|
exitCode: error.exitCode,
|
|
91
112
|
data,
|
|
113
|
+
findingsSummary: options === null || options === void 0 ? void 0 : options.findingsSummary,
|
|
92
114
|
});
|
|
93
115
|
return error.exitCode;
|
|
94
116
|
}
|
|
@@ -113,6 +135,10 @@ export class CliContext {
|
|
|
113
135
|
} });
|
|
114
136
|
this.stdout.write(`${JSON.stringify(envelope)}\n`);
|
|
115
137
|
}
|
|
138
|
+
else if (params.findingsSummary) {
|
|
139
|
+
const { command, errorCount } = params.findingsSummary;
|
|
140
|
+
this.stderr.write(`${this.ui.err(`${this.ui.glyphs.err} ${command} failed: ${errorCount} error${errorCount === 1 ? '' : 's'}`)}\n`);
|
|
141
|
+
}
|
|
116
142
|
else {
|
|
117
143
|
this.stderr.write(`Error: ${params.message}\n`);
|
|
118
144
|
this.stderr.write(`Hint: ${params.hint}\n`);
|
package/dist/context.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAiB,MAAM,cAAc,CAAC;AAqCzD;;;;;;;;GAQG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,UAAU;IAYrB,YAAY,QAAsB,EAAE;;QAF5B,YAAO,GAAG,KAAK,CAAC;QAGtB,MAAM,MAAM,GAAG,MAAA,KAAK,CAAC,MAAM,mCAAI,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAG,MAAA,KAAK,CAAC,MAAM,mCAAI,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC7D,MAAM,GAAG,GAAG,MAAA,KAAK,CAAC,GAAG,mCAAI,OAAO,CAAC,GAAG,CAAC;QAErC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QAEf,4EAA4E;QAC5E,yEAAyE;QACzE,qEAAqE;QACrE,4EAA4E;QAC5E,4BAA4B;QAC5B,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAG,MAAA,KAAK,CAAC,QAAQ,mCAAI,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,KAAK,GAAG,CAAC,CAAC;IACvE,CAAC;IAED,iFAAiF;IACjF,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC;IACpC,CAAC;IAED,gFAAgF;IAChF,GAAG,CAAC,OAAe;QACjB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,mCAAmC;IACnC,IAAI,CAAC,OAAe;QAClB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC;IACpC,CAAC;IAED;;;;OAIG;IACH,kBAAkB,CAAC,OAAgC;QACjD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,IAAa,EAAE,YAAqB;QAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,QAAQ,GAAe,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YAChD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACrD,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,YAAY,aAAZ,YAAY,cAAZ,YAAY,GAAI,IAAI,IAAI,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,UAAU,CAAC,EAAE,CAAC;IACvB,CAAC;IAED;;;;;;;OAOG;IACH,IAAI,CAAC,KAAe,EAAE,IAAc;QAClC,IAAI,CAAC,QAAQ,CAAC;YACZ,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,IAAI;SACL,CAAC,CAAC;QACH,OAAO,KAAK,CAAC,QAAQ,CAAC;IACxB,CAAC;IAED;;;;;;OAMG;IACH,QAAQ,CAAC,MAMR;QACC,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,KAAK,CACb,iBAAiB,MAAM,CAAC,OAAO,uCAAuC;gBACpE,6CAA6C,CAChD,CAAC;QACJ,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,QAAQ,iCACZ,EAAE,EAAE,KAAK,IACN,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAC3D,KAAK,EAAE;oBACL,IAAI,EAAE,MAAM,CAAC,IAAI;oBACjB,OAAO,EAAE,MAAM,CAAC,OAAO;oBACvB,IAAI,EAAE,MAAM,CAAC,IAAI;iBAClB,GACF,CAAC;YACF,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACrD,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;YAChD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC;QAC9C,CAAC;QACD,OAAO,MAAM,CAAC,QAAQ,CAAC;IACzB,CAAC;IAEO,MAAM;QACZ,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CACb,8DAA8D;gBAC5D,mEAAmE;gBACnE,wCAAwC,CAC3C,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;IACtB,CAAC;CACF;AAED,SAAS,aAAa,CAAC,MAA0B;IAC/C,OAAO;QACL,KAAK,EAAE,CAAC,KAAa,EAAE,EAAE;YACvB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;QACD,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC;AACJ,CAAC","sourcesContent":["import { CliError } from './errors';\nimport { EXIT_CODES, type ExitCode } from './exit-codes';\n\n/**\n * A single writable sink. Real streams satisfy this; tests pass a capturing\n * buffer so the stdout-purity invariant can be asserted byte-for-byte.\n */\nexport interface WriteStream {\n write(chunk: string): void;\n isTTY?: boolean;\n}\n\nexport interface ContextInput {\n /**\n * The user's EXPLICIT preference, already parsed: `true` for `--json`,\n * `false` for `--no-json`, `undefined` when neither was passed. The\n * undefined case is what allows the mode to be inferred instead.\n */\n jsonFlag?: boolean;\n stdout?: WriteStream;\n stderr?: WriteStream;\n /** Overrides process.env for CLAUDECODE detection (tests). */\n env?: Record<string, string | undefined>;\n}\n\n/** The success envelope. */\ninterface OkEnvelope {\n ok: true;\n data: unknown;\n}\n\n/** The error envelope. `data` is optional — validate carries its findings here. */\ninterface ErrEnvelope {\n ok: false;\n data?: unknown;\n error: { code: string; message: string; hint: string };\n}\n\n/**\n * `data` is deliberately `unknown` above — each verb owns its own shape — but\n * one convention spans several of them: `validate`, `dev`, `publish` and\n * `status` all add an OPTIONAL `toolchain` field to `data` (never to the\n * envelope's top level) when the proactive update check in `update-notice.ts`\n * finds this CLI is behind. See that file's `ToolchainNoticeData` for the\n * shape, and `README.md`'s \"Keeping the toolchain current\" section for the\n * full contract. It is absent, not `null`, when there is nothing to report.\n */\n\n/**\n * The agent-native output envelope, resolved ONCE at startup and threaded\n * through every command.\n *\n * The load-bearing invariant: in JSON mode, stdout receives EXACTLY ONE JSON\n * object for the whole process — nothing else, ever. Every progress line, warn,\n * and scrap of human prose goes to stderr. This is the single most common way\n * an agent's tool call breaks (a stray log line on stdout makes the JSON\n * unparseable), so the context enforces it structurally: `succeed`/`fail` are\n * the only writers of stdout, and a `settled` latch makes a second call throw\n * rather than emit a second object.\n */\nexport class CliContext {\n readonly jsonMode: boolean;\n /**\n * The environment the context resolved against — exposed so decoration\n * (the gradient banner) can honour `NO_COLOR` / `EKANOS_NO_BANNER` without\n * every call site threading `process.env` through by hand.\n */\n readonly env: Readonly<Record<string, string | undefined>>;\n private readonly stdout: WriteStream;\n private readonly stderr: WriteStream;\n private settled = false;\n\n constructor(input: ContextInput = {}) {\n const stdout = input.stdout ?? defaultStream(process.stdout);\n const stderr = input.stderr ?? defaultStream(process.stderr);\n const env = input.env ?? process.env;\n\n this.stdout = stdout;\n this.stderr = stderr;\n this.env = env;\n\n // Resolve JSON mode once. An EXPLICIT flag always wins: `--json` forces the\n // envelope on even at a terminal, `--no-json` forces the human form even\n // when piped. Only when the user expressed no preference is the mode\n // inferred — from a non-TTY stdout (piped into another program or an agent)\n // or the CLAUDECODE marker.\n const nonTty = stdout.isTTY !== true;\n this.jsonMode = input.jsonFlag ?? (nonTty || env.CLAUDECODE === '1');\n }\n\n /** Whether stderr is a real terminal — decoration (the banner) gates on this. */\n get stderrIsTTY(): boolean {\n return this.stderr.isTTY === true;\n }\n\n /** Human/progress output — ALWAYS stderr, so stdout stays pure in JSON mode. */\n log(message: string): void {\n this.stderr.write(`${message}\\n`);\n }\n\n /** A warning — stderr, no ANSI. */\n warn(message: string): void {\n this.stderr.write(`${message}\\n`);\n }\n\n /**\n * Emit the `claude-code-hint` protocol line to STDERR. Used on --help and on\n * an unknown command so an agent reading stderr is told what to do next\n * without the line ever touching stdout.\n */\n emitClaudeCodeHint(payload: Record<string, unknown>): void {\n this.stderr.write(`claude-code-hint: ${JSON.stringify(payload)}\\n`);\n }\n\n /**\n * Terminal success. Writes the single stdout object (JSON mode) or a concise\n * human line (human mode). Returns the OK exit code so the caller can end the\n * process with it. Idempotent-by-latch: a second settle throws.\n */\n succeed(data: unknown, humanSummary?: string): ExitCode {\n this.settle();\n if (this.jsonMode) {\n const envelope: OkEnvelope = { ok: true, data };\n this.stdout.write(`${JSON.stringify(envelope)}\\n`);\n } else {\n this.stdout.write(`${humanSummary ?? 'OK'}\\n`);\n }\n return EXIT_CODES.OK;\n }\n\n /**\n * Terminal failure. Writes the single stdout error object (JSON mode) or the\n * message + hint to stderr (human mode). Returns the error's exit code.\n *\n * `error.hint` is guaranteed non-empty by CliError's constructor, so the\n * envelope's \"every error carries a hint\" invariant holds for every path\n * that reaches here.\n */\n fail(error: CliError, data?: unknown): ExitCode {\n this.failWith({\n code: error.code,\n message: error.message,\n hint: error.hint,\n exitCode: error.exitCode,\n data,\n });\n return error.exitCode;\n }\n\n /**\n * The low-level failure emitter. Used directly by `test`, whose exit code is\n * a TRANSPARENT passthrough of the delegated runner's exit code (which is not\n * one of the CLI's own frozen numeric conditions) and whose error `code` is\n * an out-of-taxonomy string. Enforces the same non-empty-hint invariant as\n * CliError so no failure path can emit an empty hint.\n */\n failWith(params: {\n code: string;\n message: string;\n hint: string;\n exitCode: number;\n data?: unknown;\n }): number {\n this.settle();\n if (!params.hint || params.hint.trim().length === 0) {\n throw new Error(\n `failWith for \"${params.message}\" was given an empty hint. Every CLI ` +\n 'error must carry an imperative remediation.',\n );\n }\n if (this.jsonMode) {\n const envelope: ErrEnvelope = {\n ok: false,\n ...(params.data !== undefined ? { data: params.data } : {}),\n error: {\n code: params.code,\n message: params.message,\n hint: params.hint,\n },\n };\n this.stdout.write(`${JSON.stringify(envelope)}\\n`);\n } else {\n this.stderr.write(`Error: ${params.message}\\n`);\n this.stderr.write(`Hint: ${params.hint}\\n`);\n }\n return params.exitCode;\n }\n\n private settle(): void {\n if (this.settled) {\n throw new Error(\n 'CliContext settled twice — a command tried to emit a second ' +\n 'terminal result. In JSON mode this would corrupt stdout with two ' +\n 'objects. This is a bug in the command.',\n );\n }\n this.settled = true;\n }\n}\n\nfunction defaultStream(stream: NodeJS.WriteStream): WriteStream {\n return {\n write: (chunk: string) => {\n stream.write(chunk);\n },\n isTTY: stream.isTTY,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAiB,MAAM,cAAc,CAAC;AACzD,OAAO,EAAW,QAAQ,EAAE,MAAM,MAAM,CAAC;AAgDzC;;;;;;;;GAQG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,UAAU;IAmBrB,YAAY,QAAsB,EAAE;;QAF5B,YAAO,GAAG,KAAK,CAAC;QAGtB,MAAM,MAAM,GAAG,MAAA,KAAK,CAAC,MAAM,mCAAI,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC7D,MAAM,MAAM,GAAG,MAAA,KAAK,CAAC,MAAM,mCAAI,aAAa,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAC7D,MAAM,GAAG,GAAG,MAAA,KAAK,CAAC,GAAG,mCAAI,OAAO,CAAC,GAAG,CAAC;QAErC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,EAAE,GAAG,QAAQ,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,KAAK,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;QAE1D,4EAA4E;QAC5E,yEAAyE;QACzE,qEAAqE;QACrE,4EAA4E;QAC5E,4BAA4B;QAC5B,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC;QACrC,IAAI,CAAC,QAAQ,GAAG,MAAA,KAAK,CAAC,QAAQ,mCAAI,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,KAAK,GAAG,CAAC,CAAC;IACvE,CAAC;IAED,iFAAiF;IACjF,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC;IACpC,CAAC;IAED;;;;;OAKG;IACH,IAAI,WAAW;QACb,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC;IACpC,CAAC;IAED,gFAAgF;IAChF,GAAG,CAAC,OAAe;QACjB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC;IACpC,CAAC;IAED,mCAAmC;IACnC,IAAI,CAAC,OAAe;QAClB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,OAAO,IAAI,CAAC,CAAC;IACpC,CAAC;IAED;;;;OAIG;IACH,kBAAkB,CAAC,OAAgC;QACjD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,qBAAqB,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACtE,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,IAAa,EAAE,YAAqB;QAC1C,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,QAAQ,GAAe,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;YAChD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACrD,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,YAAY,aAAZ,YAAY,cAAZ,YAAY,GAAI,IAAI,IAAI,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,UAAU,CAAC,EAAE,CAAC;IACvB,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,IAAI,CAAC,KAAe,EAAE,IAAc,EAAE,OAAqB;QACzD,IAAI,CAAC,QAAQ,CAAC;YACZ,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,OAAO,EAAE,KAAK,CAAC,OAAO;YACtB,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,IAAI;YACJ,eAAe,EAAE,OAAO,aAAP,OAAO,uBAAP,OAAO,CAAE,eAAe;SAC1C,CAAC,CAAC;QACH,OAAO,KAAK,CAAC,QAAQ,CAAC;IACxB,CAAC;IAED;;;;;;OAMG;IACH,QAAQ,CAAC,MAOR;QACC,IAAI,CAAC,MAAM,EAAE,CAAC;QACd,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,KAAK,CACb,iBAAiB,MAAM,CAAC,OAAO,uCAAuC;gBACpE,6CAA6C,CAChD,CAAC;QACJ,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,MAAM,QAAQ,iCACZ,EAAE,EAAE,KAAK,IACN,CAAC,MAAM,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAC3D,KAAK,EAAE;oBACL,IAAI,EAAE,MAAM,CAAC,IAAI;oBACjB,OAAO,EAAE,MAAM,CAAC,OAAO;oBACvB,IAAI,EAAE,MAAM,CAAC,IAAI;iBAClB,GACF,CAAC;YACF,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACrD,CAAC;aAAM,IAAI,MAAM,CAAC,eAAe,EAAE,CAAC;YAClC,MAAM,EAAE,OAAO,EAAE,UAAU,EAAE,GAAG,MAAM,CAAC,eAAe,CAAC;YACvD,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,GAAG,IAAI,CAAC,EAAE,CAAC,GAAG,CACZ,GAAG,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,GAAG,IAAI,OAAO,YAAY,UAAU,SACpD,UAAU,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAC1B,EAAE,CACH,IAAI,CACN,CAAC;QACJ,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,MAAM,CAAC,OAAO,IAAI,CAAC,CAAC;YAChD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,MAAM,CAAC,IAAI,IAAI,CAAC,CAAC;QAC9C,CAAC;QACD,OAAO,MAAM,CAAC,QAAQ,CAAC;IACzB,CAAC;IAEO,MAAM;QACZ,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CACb,8DAA8D;gBAC5D,mEAAmE;gBACnE,wCAAwC,CAC3C,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;IACtB,CAAC;CACF;AAED,SAAS,aAAa,CAAC,MAA0B;IAC/C,OAAO;QACL,KAAK,EAAE,CAAC,KAAa,EAAE,EAAE;YACvB,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QACtB,CAAC;QACD,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC;AACJ,CAAC","sourcesContent":["import { CliError } from './errors';\nimport { EXIT_CODES, type ExitCode } from './exit-codes';\nimport { type Ui, createUi } from './ui';\n\n/**\n * A single writable sink. Real streams satisfy this; tests pass a capturing\n * buffer so the stdout-purity invariant can be asserted byte-for-byte.\n */\nexport interface WriteStream {\n write(chunk: string): void;\n isTTY?: boolean;\n}\n\nexport interface ContextInput {\n /**\n * The user's EXPLICIT preference, already parsed: `true` for `--json`,\n * `false` for `--no-json`, `undefined` when neither was passed. The\n * undefined case is what allows the mode to be inferred instead.\n */\n jsonFlag?: boolean;\n stdout?: WriteStream;\n stderr?: WriteStream;\n /** Overrides process.env for CLAUDECODE detection (tests). */\n env?: Record<string, string | undefined>;\n}\n\n/** The success envelope. */\ninterface OkEnvelope {\n ok: true;\n data: unknown;\n}\n\n/** The error envelope. `data` is optional — validate carries its findings here. */\ninterface ErrEnvelope {\n ok: false;\n data?: unknown;\n error: { code: string; message: string; hint: string };\n}\n\n/** See `fail`'s doc comment — the human-mode epilogue seam for a command that already rendered a findings block. */\ninterface FindingsSummary {\n /** The command name to name in the summary line, e.g. \"ekanos validate\". */\n command: string;\n errorCount: number;\n}\n\nexport interface FailOptions {\n findingsSummary?: FindingsSummary;\n}\n\n/**\n * `data` is deliberately `unknown` above — each verb owns its own shape — but\n * one convention spans several of them: `validate`, `dev`, `publish` and\n * `status` all add an OPTIONAL `toolchain` field to `data` (never to the\n * envelope's top level) when the proactive update check in `update-notice.ts`\n * finds this CLI is behind. See that file's `ToolchainNoticeData` for the\n * shape, and `README.md`'s \"Keeping the toolchain current\" section for the\n * full contract. It is absent, not `null`, when there is nothing to report.\n */\n\n/**\n * The agent-native output envelope, resolved ONCE at startup and threaded\n * through every command.\n *\n * The load-bearing invariant: in JSON mode, stdout receives EXACTLY ONE JSON\n * object for the whole process — nothing else, ever. Every progress line, warn,\n * and scrap of human prose goes to stderr. This is the single most common way\n * an agent's tool call breaks (a stray log line on stdout makes the JSON\n * unparseable), so the context enforces it structurally: `succeed`/`fail` are\n * the only writers of stdout, and a `settled` latch makes a second call throw\n * rather than emit a second object.\n */\nexport class CliContext {\n readonly jsonMode: boolean;\n /**\n * The environment the context resolved against — exposed so decoration\n * (the gradient banner) can honour `NO_COLOR` / `EKANOS_NO_BANNER` without\n * every call site threading `process.env` through by hand.\n */\n readonly env: Readonly<Record<string, string | undefined>>;\n /**\n * The style layer (`ui.ts`), resolved once against stderr — the stream\n * every human-mode render target uses — so every command shares one color\n * decision instead of each re-deriving it. See `ui.ts` for the tokens and\n * components this carries.\n */\n readonly ui: Ui;\n private readonly stdout: WriteStream;\n private readonly stderr: WriteStream;\n private settled = false;\n\n constructor(input: ContextInput = {}) {\n const stdout = input.stdout ?? defaultStream(process.stdout);\n const stderr = input.stderr ?? defaultStream(process.stderr);\n const env = input.env ?? process.env;\n\n this.stdout = stdout;\n this.stderr = stderr;\n this.env = env;\n this.ui = createUi({ isTTY: stderr.isTTY === true, env });\n\n // Resolve JSON mode once. An EXPLICIT flag always wins: `--json` forces the\n // envelope on even at a terminal, `--no-json` forces the human form even\n // when piped. Only when the user expressed no preference is the mode\n // inferred — from a non-TTY stdout (piped into another program or an agent)\n // or the CLAUDECODE marker.\n const nonTty = stdout.isTTY !== true;\n this.jsonMode = input.jsonFlag ?? (nonTty || env.CLAUDECODE === '1');\n }\n\n /** Whether stderr is a real terminal — decoration (the banner) gates on this. */\n get stderrIsTTY(): boolean {\n return this.stderr.isTTY === true;\n }\n\n /**\n * Whether stdout is a real terminal — `wizard.ts`'s `canWizard` gates on\n * this (clack renders to stdout, unlike the rest of this CLI's human\n * output). `ctx.jsonMode` being false does not by itself guarantee this:\n * `--no-json` forces human mode even against a piped stdout.\n */\n get stdoutIsTTY(): boolean {\n return this.stdout.isTTY === true;\n }\n\n /** Human/progress output — ALWAYS stderr, so stdout stays pure in JSON mode. */\n log(message: string): void {\n this.stderr.write(`${message}\\n`);\n }\n\n /** A warning — stderr, no ANSI. */\n warn(message: string): void {\n this.stderr.write(`${message}\\n`);\n }\n\n /**\n * Emit the `claude-code-hint` protocol line to STDERR. Used on --help and on\n * an unknown command so an agent reading stderr is told what to do next\n * without the line ever touching stdout.\n */\n emitClaudeCodeHint(payload: Record<string, unknown>): void {\n this.stderr.write(`claude-code-hint: ${JSON.stringify(payload)}\\n`);\n }\n\n /**\n * Terminal success. Writes the single stdout object (JSON mode) or a concise\n * human line (human mode). Returns the OK exit code so the caller can end the\n * process with it. Idempotent-by-latch: a second settle throws.\n */\n succeed(data: unknown, humanSummary?: string): ExitCode {\n this.settle();\n if (this.jsonMode) {\n const envelope: OkEnvelope = { ok: true, data };\n this.stdout.write(`${JSON.stringify(envelope)}\\n`);\n } else {\n this.stdout.write(`${humanSummary ?? 'OK'}\\n`);\n }\n return EXIT_CODES.OK;\n }\n\n /**\n * Terminal failure. Writes the single stdout error object (JSON mode) or the\n * message + hint to stderr (human mode). Returns the error's exit code.\n *\n * `error.hint` is guaranteed non-empty by CliError's constructor, so the\n * envelope's \"every error carries a hint\" invariant holds for every path\n * that reaches here.\n *\n * `options.findingsSummary` is the seam for a command that has ALREADY\n * rendered the shared findings block to stderr (human mode) before calling\n * this — `validate`'s and `publish`'s gate-refusal paths. Passing it swaps\n * the default two-line `Error: … / Hint: …` epilogue (whose hint always\n * points at the JSON-only `data.findings`) for one short summary line,\n * `✖ <command> failed: <n> error(s)`, so a partner never reads the same\n * finding jargon twice. JSON mode is completely unaffected either way —\n * `error.hint` always carries the original, JSON-only-appropriate text in\n * the envelope.\n */\n fail(error: CliError, data?: unknown, options?: FailOptions): ExitCode {\n this.failWith({\n code: error.code,\n message: error.message,\n hint: error.hint,\n exitCode: error.exitCode,\n data,\n findingsSummary: options?.findingsSummary,\n });\n return error.exitCode;\n }\n\n /**\n * The low-level failure emitter. Used directly by `test`, whose exit code is\n * a TRANSPARENT passthrough of the delegated runner's exit code (which is not\n * one of the CLI's own frozen numeric conditions) and whose error `code` is\n * an out-of-taxonomy string. Enforces the same non-empty-hint invariant as\n * CliError so no failure path can emit an empty hint.\n */\n failWith(params: {\n code: string;\n message: string;\n hint: string;\n exitCode: number;\n data?: unknown;\n findingsSummary?: FindingsSummary;\n }): number {\n this.settle();\n if (!params.hint || params.hint.trim().length === 0) {\n throw new Error(\n `failWith for \"${params.message}\" was given an empty hint. Every CLI ` +\n 'error must carry an imperative remediation.',\n );\n }\n if (this.jsonMode) {\n const envelope: ErrEnvelope = {\n ok: false,\n ...(params.data !== undefined ? { data: params.data } : {}),\n error: {\n code: params.code,\n message: params.message,\n hint: params.hint,\n },\n };\n this.stdout.write(`${JSON.stringify(envelope)}\\n`);\n } else if (params.findingsSummary) {\n const { command, errorCount } = params.findingsSummary;\n this.stderr.write(\n `${this.ui.err(\n `${this.ui.glyphs.err} ${command} failed: ${errorCount} error${\n errorCount === 1 ? '' : 's'\n }`,\n )}\\n`,\n );\n } else {\n this.stderr.write(`Error: ${params.message}\\n`);\n this.stderr.write(`Hint: ${params.hint}\\n`);\n }\n return params.exitCode;\n }\n\n private settle(): void {\n if (this.settled) {\n throw new Error(\n 'CliContext settled twice — a command tried to emit a second ' +\n 'terminal result. In JSON mode this would corrupt stdout with two ' +\n 'objects. This is a bug in the command.',\n );\n }\n this.settled = true;\n }\n}\n\nfunction defaultStream(stream: NodeJS.WriteStream): WriteStream {\n return {\n write: (chunk: string) => {\n stream.write(chunk);\n },\n isTTY: stream.isTTY,\n };\n}\n"]}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "Did you mean?" for a zod `unrecognized_keys` finding — the typo case
|
|
3
|
+
* (`entrypiont` instead of `entrypoint`). Two pieces: a hand-rolled
|
|
4
|
+
* Levenshtein distance (no new dependency — this package deliberately keeps
|
|
5
|
+
* its runtime dependency set to picocolors + @clack/prompts only) and a
|
|
6
|
+
* structural walk of the REAL `IntegrationDefinitionSchema` to find the
|
|
7
|
+
* sibling keys a given path actually accepts.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Classic single-rolling-row DP, O(a.length * b.length). Fine at CLI scale —
|
|
11
|
+
* this compares key names, never paragraphs.
|
|
12
|
+
*/
|
|
13
|
+
export declare function levenshteinDistance(a: string, b: string): number;
|
|
14
|
+
/**
|
|
15
|
+
* The single closest candidate within `MAX_SUGGESTION_DISTANCE`, or `null`
|
|
16
|
+
* when nothing is close enough. Never a wrong-confident guess for a key that
|
|
17
|
+
* isn't really a typo of anything this schema accepts.
|
|
18
|
+
*/
|
|
19
|
+
export declare function suggestKey(key: string, candidates: readonly string[]): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Parses the literal, comma-separated, single-quoted key list zod's
|
|
22
|
+
* `unrecognized_keys` issue produces (`Unrecognized key(s) in object: 'a',
|
|
23
|
+
* 'b'`) — this exact wording is what `collectDefinitionFindings` bakes into
|
|
24
|
+
* `Finding.message` verbatim (see its doc comment), so this is a text match
|
|
25
|
+
* against a stable, committed third-party format, not a guess at prose.
|
|
26
|
+
* `null` for a message tail that isn't this issue.
|
|
27
|
+
*/
|
|
28
|
+
export declare function extractUnrecognizedKeys(rest: string): string[] | null;
|
|
29
|
+
/**
|
|
30
|
+
* The keys `IntegrationDefinitionSchema` actually accepts at
|
|
31
|
+
* `parentDotPath` — the zod issue's own `path` for an `unrecognized_keys`
|
|
32
|
+
* finding, so this always names the OBJECT the extra key was found on, never
|
|
33
|
+
* the extra key itself. Structural introspection first, the curated list
|
|
34
|
+
* second, `[]` (no suggestion offered — never a wrong-confident one) last.
|
|
35
|
+
*/
|
|
36
|
+
export declare function acceptedKeysAt(parentDotPath: string): readonly string[];
|