@ekanos/cli 0.1.10 → 0.2.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.
Files changed (55) hide show
  1. package/dist/banner.d.ts +5 -1
  2. package/dist/banner.js +7 -2
  3. package/dist/banner.js.map +1 -1
  4. package/dist/commands/init.d.ts +13 -1
  5. package/dist/commands/init.js +70 -13
  6. package/dist/commands/init.js.map +1 -1
  7. package/dist/commands/login.d.ts +13 -0
  8. package/dist/commands/login.js +41 -10
  9. package/dist/commands/login.js.map +1 -1
  10. package/dist/commands/publish.js +55 -3
  11. package/dist/commands/publish.js.map +1 -1
  12. package/dist/commands/status.d.ts +68 -2
  13. package/dist/commands/status.js +312 -43
  14. package/dist/commands/status.js.map +1 -1
  15. package/dist/commands/test.d.ts +27 -5
  16. package/dist/commands/test.js +85 -9
  17. package/dist/commands/test.js.map +1 -1
  18. package/dist/commands/validate.js +115 -3
  19. package/dist/commands/validate.js.map +1 -1
  20. package/dist/context.d.ts +37 -1
  21. package/dist/context.js +27 -1
  22. package/dist/context.js.map +1 -1
  23. package/dist/did-you-mean.d.ts +36 -0
  24. package/dist/did-you-mean.js +192 -0
  25. package/dist/did-you-mean.js.map +1 -0
  26. package/dist/findings-renderer.d.ts +59 -0
  27. package/dist/findings-renderer.js +271 -0
  28. package/dist/findings-renderer.js.map +1 -0
  29. package/dist/index.d.ts +11 -4
  30. package/dist/index.js +23 -5
  31. package/dist/index.js.map +1 -1
  32. package/dist/project-checks.d.ts +30 -0
  33. package/dist/project-checks.js +162 -0
  34. package/dist/project-checks.js.map +1 -1
  35. package/dist/publish-api.d.ts +13 -0
  36. package/dist/publish-api.js.map +1 -1
  37. package/dist/source-locator.d.ts +67 -0
  38. package/dist/source-locator.js +284 -0
  39. package/dist/source-locator.js.map +1 -0
  40. package/dist/test-report.d.ts +32 -0
  41. package/dist/test-report.js +255 -0
  42. package/dist/test-report.js.map +1 -0
  43. package/dist/ui.d.ts +75 -0
  44. package/dist/ui.js +132 -0
  45. package/dist/ui.js.map +1 -0
  46. package/dist/validate-findings.d.ts +22 -0
  47. package/dist/validate-findings.js +57 -1
  48. package/dist/validate-findings.js.map +1 -1
  49. package/dist/wizard.d.ts +80 -0
  50. package/dist/wizard.js +74 -0
  51. package/dist/wizard.js.map +1 -0
  52. package/package.json +5 -3
  53. package/templates/package.json.tmpl +3 -1
  54. package/templates/scripts/setup-hooks.mjs.tmpl +45 -0
  55. package/templates/tsconfig.json.tmpl +2 -0
@@ -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. We detect the package manager from the lockfile,
10
- * spawn `<pm> run test` with any post-`--` args appended, stream the child's
11
- * output to OUR stderr (keeping stdout pure for the single JSON envelope), and
12
- * report `{ command, exitCode, durationMs }`.
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
- * The CLI's exit code is a transparent passthrough of the runner's exit code.
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>;
@@ -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. We detect the package manager from the lockfile,
7
- * spawn `<pm> run test` with any post-`--` args appended, stream the child's
8
- * output to OUR stderr (keeping stdout pure for the single JSON envelope), and
9
- * report `{ command, exitCode, durationMs }`.
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
- * The CLI's exit code is a transparent passthrough of the runner's exit code.
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 runnerArgs = ['run', 'test', ...args.passthrough];
17
- const command = `${pm.bin} ${runnerArgs.join(' ')}`;
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 = spawn(pm.bin, runnerArgs, {
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
- return ctx.succeed(data, `test: OK — "${command}" passed in ${durationMs}ms.`);
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
- return ctx.fail(validationError(`${errorCount} validation finding${errorCount === 1 ? '' : 's'}.`, 'Resolve the error-severity findings in data.findings, then re-run ' +
45
- '"ekanos validate".'), data);
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
- return ctx.succeed(data, `validate: OK — no findings for ${slugs}.`);
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`);
@@ -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":"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","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[];