@nifrajs/cli 2.13.0 → 2.14.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 (69) hide show
  1. package/dist/capabilities-tool.d.ts +54 -1
  2. package/dist/capabilities-tool.d.ts.map +1 -1
  3. package/dist/capabilities-tool.js +146 -18
  4. package/dist/capabilities-tool.js.map +1 -1
  5. package/dist/check.d.ts +11 -11
  6. package/dist/check.d.ts.map +1 -1
  7. package/dist/check.js +234 -43
  8. package/dist/check.js.map +1 -1
  9. package/dist/cli.d.ts +11 -0
  10. package/dist/cli.d.ts.map +1 -1
  11. package/dist/cli.js +91 -413
  12. package/dist/cli.js.map +1 -1
  13. package/dist/command-catalog.d.ts +244 -0
  14. package/dist/command-catalog.d.ts.map +1 -0
  15. package/dist/command-catalog.js +1014 -0
  16. package/dist/command-catalog.js.map +1 -0
  17. package/dist/contracts.d.ts +9 -0
  18. package/dist/contracts.d.ts.map +1 -1
  19. package/dist/contracts.js +22 -6
  20. package/dist/contracts.js.map +1 -1
  21. package/dist/doctor.d.ts +13 -0
  22. package/dist/doctor.d.ts.map +1 -1
  23. package/dist/doctor.js +18 -2
  24. package/dist/doctor.js.map +1 -1
  25. package/dist/load.d.ts.map +1 -1
  26. package/dist/load.js +22 -5
  27. package/dist/load.js.map +1 -1
  28. package/dist/manifest-tool.d.ts +15 -1
  29. package/dist/manifest-tool.d.ts.map +1 -1
  30. package/dist/manifest-tool.js +23 -9
  31. package/dist/manifest-tool.js.map +1 -1
  32. package/dist/mcp-http.js +1 -1
  33. package/dist/mcp.d.ts +8 -0
  34. package/dist/mcp.d.ts.map +1 -1
  35. package/dist/mcp.js +59 -349
  36. package/dist/mcp.js.map +1 -1
  37. package/dist/project-facts.d.ts +40 -0
  38. package/dist/project-facts.d.ts.map +1 -0
  39. package/dist/project-facts.js +36 -0
  40. package/dist/project-facts.js.map +1 -0
  41. package/dist/rules/comment-markers.d.ts +8 -0
  42. package/dist/rules/comment-markers.d.ts.map +1 -1
  43. package/dist/rules/comment-markers.js +36 -0
  44. package/dist/rules/comment-markers.js.map +1 -1
  45. package/dist/rules/index.d.ts +2 -3
  46. package/dist/rules/index.d.ts.map +1 -1
  47. package/dist/rules/index.js.map +1 -1
  48. package/dist/rules/routes.d.ts.map +1 -1
  49. package/dist/rules/routes.js +12 -14
  50. package/dist/rules/routes.js.map +1 -1
  51. package/dist/rules/security.js +14 -14
  52. package/dist/rules/security.js.map +1 -1
  53. package/docs/types.json +27 -14
  54. package/package.json +9 -9
  55. package/src/capabilities-tool.ts +229 -21
  56. package/src/check.ts +285 -59
  57. package/src/cli.ts +100 -404
  58. package/src/command-catalog.ts +1415 -0
  59. package/src/contracts.ts +28 -7
  60. package/src/doctor.ts +35 -4
  61. package/src/load.ts +25 -6
  62. package/src/manifest-tool.ts +43 -12
  63. package/src/mcp-http.ts +1 -1
  64. package/src/mcp.ts +82 -414
  65. package/src/project-facts.ts +86 -0
  66. package/src/rules/comment-markers.ts +36 -0
  67. package/src/rules/index.ts +3 -4
  68. package/src/rules/routes.ts +13 -14
  69. package/src/rules/security.ts +14 -14
@@ -0,0 +1,1014 @@
1
+ /**
2
+ * The public command contract for the stable, project-facing nifra commands.
3
+ *
4
+ * This module is deliberately descriptive at the projection boundary: the catalog entry does not run
5
+ * a command, validate permissions, or decide whether a result is safe. Those policies remain in the
6
+ * command implementations and in the adapters (the same split used by `@nifrajs/core/tool-catalog`).
7
+ */
8
+ import { resolve } from "node:path";
9
+ import { formatAssuranceReport } from "./assure.js";
10
+ import { renderCheckReport } from "./check.js";
11
+ import { collectPortResult, renderReport } from "./port.js";
12
+ import { collectProjectWorkGraph, renderWorkGraphText, } from "./work-graph.js";
13
+ function freezeJson(value) {
14
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
15
+ for (const child of Object.values(value))
16
+ freezeJson(child);
17
+ Object.freeze(value);
18
+ }
19
+ return value;
20
+ }
21
+ /** Build the single descriptive projection shared by CLI help, MCP tools, and generated cards. */
22
+ export function toCommandCatalogEntry(spec) {
23
+ return Object.freeze({
24
+ name: spec.name,
25
+ summary: spec.summary,
26
+ inputSchema: freezeJson({ ...spec.input.jsonSchema }),
27
+ outputVersion: spec.output.version,
28
+ transports: Object.freeze([...spec.transports]),
29
+ stability: spec.stability,
30
+ });
31
+ }
32
+ function record(value) {
33
+ if (typeof value !== "object" || value === null || Array.isArray(value))
34
+ throw new TypeError("command input must be an object");
35
+ return value;
36
+ }
37
+ function optionalString(value, field) {
38
+ if (value === undefined)
39
+ return undefined;
40
+ if (typeof value !== "string")
41
+ throw new TypeError(`${field} must be a string`);
42
+ return value;
43
+ }
44
+ function optionalBoolean(value, field) {
45
+ if (value === undefined)
46
+ return undefined;
47
+ if (typeof value !== "boolean")
48
+ throw new TypeError(`${field} must be a boolean`);
49
+ return value;
50
+ }
51
+ function optionalNumber(value, field) {
52
+ if (value === undefined)
53
+ return undefined;
54
+ if (typeof value !== "number" || !Number.isFinite(value))
55
+ throw new TypeError(`${field} must be a finite number`);
56
+ return value;
57
+ }
58
+ function objectSchema(properties, required = []) {
59
+ return {
60
+ type: "object",
61
+ properties,
62
+ ...(required.length > 0 ? { required } : {}),
63
+ additionalProperties: false,
64
+ };
65
+ }
66
+ function input(jsonSchema, parse) {
67
+ return Object.freeze({ jsonSchema: freezeJson({ ...jsonSchema }), parse });
68
+ }
69
+ function output(jsonSchema, parse = (value) => value) {
70
+ return Object.freeze({ version: 1, jsonSchema: freezeJson({ ...jsonSchema }), parse });
71
+ }
72
+ function withDir(value) {
73
+ const raw = record(value);
74
+ const dir = optionalString(raw.dir, "dir");
75
+ return { ...value, ...(dir === undefined ? {} : { dir }) };
76
+ }
77
+ function valueAfterFlag(argv, index, flag) {
78
+ const value = argv[index + 1];
79
+ if (value === undefined || value.startsWith("-"))
80
+ throw new Error(`${flag} needs a value`);
81
+ return value;
82
+ }
83
+ /** Bind CLI positionals and flags to the JSON object passed to a command's schema parser. */
84
+ export function bindCommandArgv(spec, argv) {
85
+ const raw = {};
86
+ const binding = spec.argv;
87
+ const flags = binding?.flags ?? [];
88
+ const byName = new Map();
89
+ for (const flag of flags) {
90
+ byName.set(`--${flag.name}`, flag);
91
+ for (const alias of flag.aliases ?? [])
92
+ byName.set(alias, flag);
93
+ }
94
+ const positionals = binding?.positionals ?? [];
95
+ let positionalIndex = 0;
96
+ for (let index = 0; index < argv.length; index++) {
97
+ const token = argv[index];
98
+ if (token.startsWith("-")) {
99
+ const equals = token.indexOf("=");
100
+ const flagName = equals === -1 ? token : token.slice(0, equals);
101
+ const flag = byName.get(flagName);
102
+ if (flag === undefined)
103
+ throw new Error(`unknown option: ${token}`);
104
+ if (flag.type === "boolean") {
105
+ const value = equals === -1 ? undefined : token.slice(equals + 1);
106
+ if (value !== undefined && value !== "true" && value !== "false")
107
+ throw new Error(`${flagName} must be a boolean`);
108
+ raw[flag.field] = value !== "false";
109
+ }
110
+ else {
111
+ const value = equals === -1 ? valueAfterFlag(argv, index, flagName) : token.slice(equals + 1);
112
+ if (equals === -1)
113
+ index++;
114
+ if (flag.type === "number") {
115
+ const number = Number(value);
116
+ if (!Number.isFinite(number))
117
+ throw new Error(`${flagName} must be a number`);
118
+ raw[flag.field] = number;
119
+ }
120
+ else if (flag.type === "string[]") {
121
+ const values = raw[flag.field] ?? [];
122
+ values.push(value);
123
+ raw[flag.field] = values;
124
+ }
125
+ else
126
+ raw[flag.field] = value;
127
+ }
128
+ continue;
129
+ }
130
+ const field = positionals[positionalIndex++];
131
+ if (field === undefined)
132
+ throw new Error(`unexpected positional argument: ${token}`);
133
+ raw[field] = token;
134
+ }
135
+ return spec.input.parse(raw);
136
+ }
137
+ function parseBooleanFlags(raw, fields) {
138
+ const out = {};
139
+ for (const field of fields)
140
+ out[field] = optionalBoolean(raw[field], field);
141
+ return out;
142
+ }
143
+ const CHECK_SCHEMA = input(objectSchema({
144
+ lintsOnly: { type: "boolean" },
145
+ json: { type: "boolean" },
146
+ maxDiagnostics: { type: "integer", minimum: 1 },
147
+ dir: { type: "string" },
148
+ }), (value) => {
149
+ const raw = withDir(record(value));
150
+ const maxDiagnostics = optionalNumber(raw.maxDiagnostics, "maxDiagnostics");
151
+ if (maxDiagnostics !== undefined && (!Number.isInteger(maxDiagnostics) || maxDiagnostics < 1))
152
+ throw new TypeError("maxDiagnostics must be a positive integer");
153
+ return {
154
+ ...parseBooleanFlags(raw, ["lintsOnly", "json"]),
155
+ ...(maxDiagnostics === undefined ? {} : { maxDiagnostics }),
156
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
157
+ };
158
+ });
159
+ const ASSURE_SCHEMA = input(objectSchema({
160
+ config: { type: "string" },
161
+ bundle: { type: "boolean" },
162
+ strict: { type: "boolean" },
163
+ hydration: { type: "boolean" },
164
+ interact: { type: "boolean" },
165
+ out: { type: "string" },
166
+ json: { type: "boolean" },
167
+ dir: { type: "string" },
168
+ }), (value) => {
169
+ const raw = withDir(record(value));
170
+ return {
171
+ ...parseBooleanFlags(raw, ["bundle", "strict", "hydration", "interact", "json"]),
172
+ config: optionalString(raw.config, "config"),
173
+ out: optionalString(raw.out, "out"),
174
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
175
+ };
176
+ });
177
+ const LEVELS_SCHEMA = input(objectSchema({
178
+ config: { type: "string" },
179
+ seed: { type: "number" },
180
+ min: { type: "integer", minimum: 0, maximum: 4 },
181
+ json: { type: "boolean" },
182
+ dir: { type: "string" },
183
+ }), (value) => {
184
+ const raw = withDir(record(value));
185
+ const seed = optionalNumber(raw.seed, "seed");
186
+ const min = optionalNumber(raw.min, "min");
187
+ if (seed !== undefined && !Number.isSafeInteger(seed))
188
+ throw new TypeError("seed must be a safe integer");
189
+ if (min !== undefined && (!Number.isInteger(min) || min < 0 || min > 4))
190
+ throw new TypeError("min must be an integer from 0 to 4");
191
+ return {
192
+ ...parseBooleanFlags(raw, ["json"]),
193
+ config: optionalString(raw.config, "config"),
194
+ ...(seed === undefined ? {} : { seed }),
195
+ ...(min === undefined ? {} : { min }),
196
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
197
+ };
198
+ });
199
+ const CAPABILITIES_SCHEMA = input(objectSchema({
200
+ action: { type: "string", enum: ["snapshot", "check", "explain"] },
201
+ config: { type: "string" },
202
+ out: { type: "string" },
203
+ lockfile: { type: "string" },
204
+ method: { type: "string" },
205
+ path: { type: "string" },
206
+ json: { type: "boolean" },
207
+ dir: { type: "string" },
208
+ }, ["action"]), (value) => {
209
+ const raw = withDir(record(value));
210
+ if (raw.action !== "snapshot" && raw.action !== "check" && raw.action !== "explain")
211
+ throw new TypeError("action must be snapshot, check, or explain");
212
+ return {
213
+ action: raw.action,
214
+ config: optionalString(raw.config, "config"),
215
+ out: optionalString(raw.out, "out"),
216
+ lockfile: optionalString(raw.lockfile, "lockfile"),
217
+ method: optionalString(raw.method, "method"),
218
+ path: optionalString(raw.path, "path"),
219
+ ...parseBooleanFlags(raw, ["json"]),
220
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
221
+ };
222
+ });
223
+ const MANIFEST_SCHEMA = input(objectSchema({
224
+ action: { type: "string", enum: ["emit", "diff"] },
225
+ config: { type: "string" },
226
+ out: { type: "string" },
227
+ sign: { type: "string" },
228
+ before: { type: "string" },
229
+ after: { type: "string" },
230
+ json: { type: "boolean" },
231
+ dir: { type: "string" },
232
+ }, ["action"]), (value) => {
233
+ const raw = withDir(record(value));
234
+ if (raw.action !== "emit" && raw.action !== "diff")
235
+ throw new TypeError("action must be emit or diff");
236
+ return {
237
+ action: raw.action,
238
+ config: optionalString(raw.config, "config"),
239
+ out: optionalString(raw.out, "out"),
240
+ sign: optionalString(raw.sign, "sign"),
241
+ before: optionalString(raw.before, "before"),
242
+ after: optionalString(raw.after, "after"),
243
+ ...parseBooleanFlags(raw, ["json"]),
244
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
245
+ };
246
+ });
247
+ const ROUTES_SCHEMA = input(objectSchema({
248
+ json: { type: "boolean" },
249
+ graph: { type: "boolean" },
250
+ modes: { type: "boolean" },
251
+ target: { type: "string" },
252
+ dir: { type: "string" },
253
+ }), (value) => {
254
+ const raw = withDir(record(value));
255
+ if (raw.graph === true && raw.modes === true)
256
+ throw new TypeError("graph and modes cannot be combined");
257
+ return {
258
+ ...parseBooleanFlags(raw, ["json", "graph", "modes"]),
259
+ target: optionalString(raw.target, "target"),
260
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
261
+ };
262
+ });
263
+ const CONTEXT_SCHEMA = input(objectSchema({
264
+ path: { type: "string" },
265
+ kind: { type: "string", enum: ["api", "pages"] },
266
+ dir: { type: "string" },
267
+ }), (value) => {
268
+ const raw = withDir(record(value));
269
+ if (raw.kind !== undefined && raw.kind !== "api" && raw.kind !== "pages")
270
+ throw new TypeError("kind must be api or pages");
271
+ return {
272
+ path: optionalString(raw.path, "path"),
273
+ kind: raw.kind,
274
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
275
+ };
276
+ });
277
+ const DOCTOR_SCHEMA = input(objectSchema({
278
+ json: { type: "boolean" },
279
+ autoFix: { type: "boolean" },
280
+ strict: { type: "boolean" },
281
+ target: { type: "string" },
282
+ dir: { type: "string" },
283
+ }), (value) => {
284
+ const raw = withDir(record(value));
285
+ return {
286
+ ...parseBooleanFlags(raw, ["json", "autoFix", "strict"]),
287
+ target: optionalString(raw.target, "target"),
288
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
289
+ };
290
+ });
291
+ const FIX_SCHEMA = input(objectSchema({ code: { type: "string" }, json: { type: "boolean" }, dir: { type: "string" } }), (value) => {
292
+ const raw = withDir(record(value));
293
+ return {
294
+ code: optionalString(raw.code, "code"),
295
+ ...parseBooleanFlags(raw, ["json"]),
296
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
297
+ };
298
+ });
299
+ const SNAPSHOT_SCHEMA = input(objectSchema({ out: { type: "string" }, json: { type: "boolean" }, dir: { type: "string" } }), (value) => {
300
+ const raw = withDir(record(value));
301
+ return {
302
+ out: optionalString(raw.out, "out"),
303
+ ...parseBooleanFlags(raw, ["json"]),
304
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
305
+ };
306
+ });
307
+ const DIFF_SCHEMA = input(objectSchema({
308
+ baseline: { type: "string" },
309
+ json: { type: "boolean" },
310
+ dir: { type: "string" },
311
+ }), (value) => {
312
+ const raw = withDir(record(value));
313
+ return {
314
+ baseline: optionalString(raw.baseline, "baseline"),
315
+ ...parseBooleanFlags(raw, ["json"]),
316
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
317
+ };
318
+ });
319
+ const CONTRACTS_SCHEMA = input(objectSchema({
320
+ action: { type: "string", enum: ["snapshot", "check"] },
321
+ out: { type: "string" },
322
+ json: { type: "boolean" },
323
+ dir: { type: "string" },
324
+ }, ["action"]), (value) => {
325
+ const raw = withDir(record(value));
326
+ if (raw.action !== "snapshot" && raw.action !== "check")
327
+ throw new TypeError("action must be snapshot or check");
328
+ return {
329
+ action: raw.action,
330
+ out: optionalString(raw.out, "out"),
331
+ ...parseBooleanFlags(raw, ["json"]),
332
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
333
+ };
334
+ });
335
+ const SYNC_SCHEMA = input(objectSchema({ json: { type: "boolean" }, dir: { type: "string" } }), (value) => {
336
+ const raw = withDir(record(value));
337
+ return {
338
+ ...parseBooleanFlags(raw, ["json"]),
339
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
340
+ };
341
+ });
342
+ const PROVE_SCHEMA = input(objectSchema({
343
+ files: { type: "array", items: { type: "string" } },
344
+ minLevel: { type: "integer", minimum: 0, maximum: 4 },
345
+ json: { type: "boolean" },
346
+ dir: { type: "string" },
347
+ }), (value) => {
348
+ const raw = withDir(record(value));
349
+ let files;
350
+ if (raw.files !== undefined) {
351
+ if (!Array.isArray(raw.files) || raw.files.some((entry) => typeof entry !== "string"))
352
+ throw new TypeError("files must be an array of strings");
353
+ files = raw.files;
354
+ }
355
+ const minLevel = optionalNumber(raw.minLevel, "minLevel");
356
+ if (minLevel !== undefined && (!Number.isInteger(minLevel) || minLevel < 0 || minLevel > 4))
357
+ throw new TypeError("minLevel must be an integer from 0 to 4");
358
+ return {
359
+ ...(files === undefined ? {} : { files }),
360
+ ...(minLevel === undefined ? {} : { minLevel }),
361
+ ...parseBooleanFlags(raw, ["json"]),
362
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
363
+ };
364
+ });
365
+ const REPLAY_SCHEMA = input(objectSchema({ file: { type: "string" }, dir: { type: "string" } }, ["file"]), (value) => {
366
+ const raw = withDir(record(value));
367
+ const file = optionalString(raw.file, "file");
368
+ if (file === undefined || file.trim() === "")
369
+ throw new TypeError("file is required");
370
+ return { file, ...(raw.dir === undefined ? {} : { dir: raw.dir }) };
371
+ });
372
+ const PORT_SCHEMA = input(objectSchema({
373
+ target: { type: "string" },
374
+ json: { type: "boolean" },
375
+ ci: { type: "boolean" },
376
+ strict: { type: "boolean" },
377
+ dir: { type: "string" },
378
+ }), (value) => {
379
+ const raw = withDir(record(value));
380
+ return {
381
+ target: optionalString(raw.target, "target"),
382
+ ...parseBooleanFlags(raw, ["json", "ci", "strict"]),
383
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
384
+ };
385
+ });
386
+ function loadAppFor(ctx) {
387
+ if (ctx.loadApp !== undefined)
388
+ return ctx.loadApp();
389
+ return import("./load.js").then(({ loadApp }) => loadApp(ctx.cwd));
390
+ }
391
+ function checkJson(out) {
392
+ return out.structuredDiagnostics === undefined
393
+ ? out
394
+ : { ...out, diagnostics: out.structuredDiagnostics };
395
+ }
396
+ const checkSpec = {
397
+ name: "check",
398
+ summary: "Run the typed-contract, source-lint, and dependency drift gate.",
399
+ input: CHECK_SCHEMA,
400
+ output: output({
401
+ type: "object",
402
+ properties: { ok: { type: "boolean" }, diagnostics: { type: "array" } },
403
+ required: ["ok", "diagnostics"],
404
+ }),
405
+ transports: ["cli", "mcp"],
406
+ stability: "stable",
407
+ argv: {
408
+ flags: [
409
+ { name: "lints-only", field: "lintsOnly", type: "boolean" },
410
+ { name: "json", field: "json", type: "boolean" },
411
+ ],
412
+ },
413
+ async run(value, ctx) {
414
+ const { collectProjectVerification } = await import("./verification.js");
415
+ return (await collectProjectVerification(ctx.cwd, {
416
+ lintsOnly: value.lintsOnly === true,
417
+ ...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
418
+ ...(value.maxDiagnostics === undefined ? {} : { maxDiagnostics: value.maxDiagnostics }),
419
+ })).check();
420
+ },
421
+ render: (out) => renderCheckReport(out),
422
+ success: (out) => out.ok,
423
+ json: (out) => checkJson(out),
424
+ };
425
+ const assureSpec = {
426
+ name: "assure",
427
+ summary: "Evaluate route classification and fail-closed enforcement evidence.",
428
+ input: ASSURE_SCHEMA,
429
+ output: output({ type: "object" }),
430
+ transports: ["cli", "mcp"],
431
+ stability: "stable",
432
+ argv: {
433
+ flags: [
434
+ { name: "config", field: "config", type: "string" },
435
+ { name: "out", field: "out", type: "string" },
436
+ { name: "json", field: "json", type: "boolean" },
437
+ { name: "bundle", field: "bundle", type: "boolean" },
438
+ { name: "strict", field: "strict", type: "boolean" },
439
+ { name: "hydration", field: "hydration", type: "boolean" },
440
+ { name: "interact", field: "interact", type: "boolean" },
441
+ ],
442
+ },
443
+ async run(value, ctx) {
444
+ const { collectAssuranceReport, collectAssureBundle } = await import("./assure.js");
445
+ const bundle = value.bundle === true ||
446
+ value.strict === true ||
447
+ value.hydration === true ||
448
+ value.interact === true;
449
+ if (bundle) {
450
+ const result = await collectAssureBundle(ctx.cwd, {
451
+ ...(value.config === undefined ? {} : { config: value.config }),
452
+ ...(value.strict === undefined ? {} : { strict: value.strict }),
453
+ ...(value.hydration === undefined ? {} : { hydration: value.hydration }),
454
+ ...(value.interact === undefined ? {} : { interact: value.interact }),
455
+ });
456
+ if (value.out !== undefined)
457
+ await Bun.write(resolve(ctx.cwd, value.out), `${JSON.stringify(result, null, 2)}\n`);
458
+ return { bundle: result, ok: result.verdict === "green" };
459
+ }
460
+ const report = await collectAssuranceReport(ctx.cwd, value.config);
461
+ return { report, ok: report.ok };
462
+ },
463
+ render: (out) => [
464
+ out.bundle === undefined
465
+ ? formatAssuranceReport(out.report)
466
+ : JSON.stringify(out.bundle, null, 2),
467
+ ],
468
+ success: (out) => out.ok,
469
+ json: (out) => out.bundle ?? out.report,
470
+ };
471
+ const levelsSpec = {
472
+ name: "levels",
473
+ summary: "Compute the cumulative L0 typed-contract through L4 invariant verification ladder.",
474
+ input: LEVELS_SCHEMA,
475
+ output: output({
476
+ type: "object",
477
+ properties: { achieved: { type: "integer" }, levels: { type: "array" } },
478
+ required: ["achieved", "levels"],
479
+ }),
480
+ transports: ["cli", "mcp"],
481
+ stability: "stable",
482
+ argv: {
483
+ flags: [
484
+ { name: "config", field: "config", type: "string" },
485
+ { name: "seed", field: "seed", type: "number" },
486
+ { name: "min", field: "min", type: "number" },
487
+ { name: "json", field: "json", type: "boolean" },
488
+ ],
489
+ },
490
+ async run(value, ctx) {
491
+ return import("./levels-tool.js").then(({ collectVerificationLevels }) => collectVerificationLevels(ctx.cwd, {
492
+ ...(value.config === undefined ? {} : { config: value.config }),
493
+ ...(value.seed === undefined ? {} : { seed: value.seed }),
494
+ }));
495
+ },
496
+ render: (out) => [
497
+ ...out.levels.flatMap((s) => [
498
+ `${s.ok ? "✓" : "✖"} L${s.level} ${s.name}`,
499
+ ...s.reasons.map((r) => ` - ${r}`),
500
+ ]),
501
+ out.achieved < 0
502
+ ? "[nifra] verification level: none (L0 failing)"
503
+ : `[nifra] verification level: L${out.achieved}`,
504
+ ],
505
+ success: (out, value) => out.achieved >= (value.min ?? 0),
506
+ };
507
+ const capabilitiesSpec = {
508
+ name: "capabilities",
509
+ summary: "Snapshot, check, or explain token-only capability provenance and lockfile drift.",
510
+ input: CAPABILITIES_SCHEMA,
511
+ output: output({ type: "object" }),
512
+ transports: ["cli", "mcp"],
513
+ stability: "stable",
514
+ argv: {
515
+ positionals: ["action", "method", "path"],
516
+ flags: [
517
+ { name: "config", field: "config", type: "string" },
518
+ { name: "out", field: "out", type: "string" },
519
+ { name: "lockfile", field: "lockfile", type: "string" },
520
+ { name: "json", field: "json", type: "boolean" },
521
+ ],
522
+ },
523
+ async run(value, ctx) {
524
+ if (value.action === "snapshot")
525
+ return import("./capabilities-tool.js").then(({ collectCapabilitySnapshot }) => collectCapabilitySnapshot(ctx.cwd, {
526
+ ...(value.config === undefined ? {} : { config: value.config }),
527
+ ...(value.out === undefined ? {} : { out: value.out }),
528
+ }));
529
+ if (value.action === "check")
530
+ return import("./capabilities-tool.js").then(({ collectCapabilityCheck }) => collectCapabilityCheck(ctx.cwd, {
531
+ ...(value.config === undefined ? {} : { config: value.config }),
532
+ ...(value.lockfile === undefined ? {} : { lockfile: value.lockfile }),
533
+ }));
534
+ if (value.method === undefined || value.path === undefined)
535
+ throw new Error("capabilities explain needs <METHOD> <path>");
536
+ return import("./capabilities-tool.js").then(({ collectCapabilityExplanation }) => collectCapabilityExplanation(ctx.cwd, value.method, value.path, value.config === undefined ? {} : { config: value.config }));
537
+ },
538
+ render: (out) => [JSON.stringify(out, null, 2)],
539
+ success: (out) => out.ok,
540
+ };
541
+ const manifestSpec = {
542
+ name: "manifest",
543
+ summary: "Emit or diff the hash-verified route trust manifest.",
544
+ input: MANIFEST_SCHEMA,
545
+ output: output({ type: "object" }),
546
+ transports: ["cli", "mcp"],
547
+ stability: "stable",
548
+ argv: {
549
+ positionals: ["action", "before", "after"],
550
+ flags: [
551
+ { name: "config", field: "config", type: "string" },
552
+ { name: "out", field: "out", type: "string" },
553
+ { name: "sign", field: "sign", type: "string" },
554
+ { name: "json", field: "json", type: "boolean" },
555
+ ],
556
+ },
557
+ async run(value, ctx) {
558
+ if (value.action === "emit")
559
+ return import("./manifest-tool.js").then(({ collectManifestEmit }) => collectManifestEmit(ctx.cwd, {
560
+ ...(value.config === undefined ? {} : { config: value.config }),
561
+ ...(value.out === undefined ? {} : { out: value.out }),
562
+ ...(value.sign === undefined ? {} : { sign: value.sign }),
563
+ }));
564
+ if (value.before === undefined || value.after === undefined)
565
+ throw new Error("manifest diff needs <before> <after>");
566
+ const diff = await import("./manifest-tool.js").then(({ collectManifestDiff }) => collectManifestDiff(ctx.cwd, value.before, value.after));
567
+ return { ok: !diff.hasBreaking, hasBreaking: diff.hasBreaking, changes: diff.changes };
568
+ },
569
+ render: (out) => "changes" in out
570
+ ? [
571
+ out.ok ? "✓ no breaking manifest changes" : "✖ breaking manifest changes",
572
+ ...out.changes.map((c) => `${c.method} ${c.path}: ${c.message}`),
573
+ ]
574
+ : [
575
+ out.ok
576
+ ? `✓ wrote manifest to ${out.path}`
577
+ : "✖ refusing to emit a manifest from failing assurance",
578
+ ],
579
+ success: (out) => out.ok,
580
+ json: (out) => out,
581
+ };
582
+ const routesSpec = {
583
+ name: "routes",
584
+ summary: "List, graph, or target-check every page and API route the app serves.",
585
+ input: ROUTES_SCHEMA,
586
+ output: output({
587
+ type: "object",
588
+ properties: { ok: { type: "boolean" }, mode: { type: "string" }, data: {} },
589
+ required: ["ok", "mode", "data"],
590
+ }),
591
+ transports: ["cli", "mcp"],
592
+ stability: "stable",
593
+ argv: {
594
+ flags: [
595
+ { name: "json", field: "json", type: "boolean" },
596
+ { name: "graph", field: "graph", type: "boolean" },
597
+ { name: "modes", field: "modes", type: "boolean" },
598
+ { name: "target", field: "target", type: "string" },
599
+ ],
600
+ },
601
+ async run(value, ctx) {
602
+ const app = await loadAppFor(ctx);
603
+ if (value.modes === true) {
604
+ const { buildRouteManifest, renderRouteManifest } = await import("@nifrajs/web/route-manifest");
605
+ const { discoverRoutes } = await import("@nifrajs/web/fs");
606
+ const data = await buildRouteManifest(discoverRoutes(app.routesDir), value.target === undefined ? {} : { target: value.target });
607
+ return {
608
+ ok: data.conflicts.length === 0,
609
+ mode: "modes",
610
+ data,
611
+ text: renderRouteManifest(data),
612
+ };
613
+ }
614
+ const { describeRoutes, describeRouteGraph } = await import("./introspect.js");
615
+ const mode = value.graph === true ? "graph" : "default";
616
+ const text = value.graph === true ? await describeRouteGraph(app) : await describeRoutes(app);
617
+ const jsonText = value.graph === true
618
+ ? await describeRouteGraph(app, { json: true })
619
+ : await describeRoutes(app, { json: true });
620
+ return { ok: true, mode, data: JSON.parse(jsonText), text };
621
+ },
622
+ render: (out) => [out.text],
623
+ success: (out) => out.ok,
624
+ json: (out) => out.data,
625
+ };
626
+ const contextSpec = {
627
+ name: "context",
628
+ summary: "Print the project route index and framework conventions for an agent.",
629
+ input: CONTEXT_SCHEMA,
630
+ output: output({
631
+ type: "object",
632
+ properties: { ok: { type: "boolean" }, text: { type: "string" } },
633
+ required: ["ok", "text"],
634
+ }),
635
+ transports: ["cli", "mcp"],
636
+ stability: "stable",
637
+ argv: {
638
+ flags: [
639
+ { name: "path", field: "path", type: "string" },
640
+ { name: "kind", field: "kind", type: "string" },
641
+ ],
642
+ },
643
+ async run(value, ctx) {
644
+ return {
645
+ ok: true,
646
+ text: (await import("./introspect.js")).describeProject(await loadAppFor(ctx), {
647
+ ...(value.path === undefined ? {} : { path: value.path }),
648
+ ...(value.kind === undefined ? {} : { kind: value.kind }),
649
+ }),
650
+ };
651
+ },
652
+ render: (out) => [out.text],
653
+ };
654
+ const doctorSpec = {
655
+ name: "doctor",
656
+ summary: "Find undeclared imports, duplicate identity installs, and pipeline readiness drift.",
657
+ input: DOCTOR_SCHEMA,
658
+ output: output({
659
+ type: "object",
660
+ properties: { ok: { type: "boolean" }, findings: { type: "array" } },
661
+ required: ["ok"],
662
+ }),
663
+ transports: ["cli", "mcp"],
664
+ stability: "stable",
665
+ argv: {
666
+ flags: [
667
+ { name: "json", field: "json", type: "boolean" },
668
+ { name: "auto-fix", field: "autoFix", type: "boolean" },
669
+ { name: "fix", field: "autoFix", type: "boolean" },
670
+ { name: "strict", field: "strict", type: "boolean" },
671
+ { name: "target", field: "target", type: "string" },
672
+ ],
673
+ },
674
+ async run(value, ctx) {
675
+ const mod = await import("./doctor.js");
676
+ const options = {
677
+ ...(value.target === undefined ? {} : { target: value.target }),
678
+ ...(value.strict === undefined ? {} : { strict: value.strict }),
679
+ ...(ctx.cliVersion === undefined ? {} : { cliVersion: ctx.cliVersion }),
680
+ };
681
+ return value.autoFix === true
682
+ ? mod.applyDoctorAutoFix(ctx.cwd, options)
683
+ : mod.collectDoctorResult(ctx.cwd, options);
684
+ },
685
+ render: (out) => [
686
+ out.ok ? "✓ doctor passed" : "✖ doctor found issues",
687
+ ...out.findings.map((f) => ` ${f.file}:${f.line} ${f.package}`),
688
+ ],
689
+ success: (out) => out.ok,
690
+ };
691
+ const fixSpec = {
692
+ name: "fix",
693
+ summary: "Apply registered mechanical diagnostic recipes and return remaining findings.",
694
+ input: FIX_SCHEMA,
695
+ output: output({
696
+ type: "object",
697
+ properties: {
698
+ ok: { type: "boolean" },
699
+ changed: { type: "array" },
700
+ diagnostics: { type: "array" },
701
+ },
702
+ required: ["ok", "diagnostics"],
703
+ }),
704
+ transports: ["cli", "mcp"],
705
+ stability: "stable",
706
+ argv: {
707
+ flags: [
708
+ { name: "code", field: "code", type: "string" },
709
+ { name: "json", field: "json", type: "boolean" },
710
+ ],
711
+ },
712
+ async run(value, ctx) {
713
+ const { collectCheckResult } = await import("./check.js");
714
+ const { applyDiagnosticRecipe } = await import("./fix-recipes.js");
715
+ const result = await collectCheckResult(ctx.cwd, {
716
+ lintsOnly: true,
717
+ ...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
718
+ });
719
+ const changed = [];
720
+ for (const diagnostic of result.structuredDiagnostics ?? [])
721
+ if (value.code === undefined || diagnostic.code === value.code)
722
+ changed.push(...(await applyDiagnosticRecipe(ctx.cwd, diagnostic)));
723
+ const final = await collectCheckResult(ctx.cwd, {
724
+ lintsOnly: true,
725
+ ...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
726
+ });
727
+ return { ok: final.ok, changed, diagnostics: final.structuredDiagnostics ?? final.diagnostics };
728
+ },
729
+ render: (out) => [
730
+ out.ok ? "✓ fixes applied" : "✖ unresolved diagnostics remain",
731
+ ...out.changed.map((file) => ` changed ${file}`),
732
+ ...out.diagnostics.map((d) => ` ${String(d.code ?? "diagnostic")}`),
733
+ ],
734
+ success: (out) => out.ok,
735
+ };
736
+ const snapshotSpec = {
737
+ name: "snapshot",
738
+ summary: "Write the backend API contract as a versioned JSON baseline.",
739
+ input: SNAPSHOT_SCHEMA,
740
+ output: output({
741
+ type: "object",
742
+ properties: {
743
+ ok: { type: "boolean" },
744
+ file: { type: "string" },
745
+ routes: { type: "integer" },
746
+ snapshot: { type: "object" },
747
+ },
748
+ required: ["ok", "file", "routes"],
749
+ }),
750
+ transports: ["cli", "mcp"],
751
+ stability: "stable",
752
+ argv: {
753
+ flags: [
754
+ { name: "out", field: "out", type: "string" },
755
+ { name: "json", field: "json", type: "boolean" },
756
+ ],
757
+ },
758
+ async run(value, ctx) {
759
+ const mod = await import("./diff-tool.js");
760
+ const routes = await mod.snapshotBackend(ctx.cwd);
761
+ const file = value.out ?? mod.DEFAULT_SNAPSHOT_FILE;
762
+ const snapshot = { nifraSnapshot: 1, routes };
763
+ await Bun.write(resolve(ctx.cwd, file), `${JSON.stringify(snapshot, null, 2)}\n`);
764
+ return { ok: true, file, routes: routes.length, snapshot };
765
+ },
766
+ render: (out) => [
767
+ `[nifra] wrote ${out.routes} route${out.routes === 1 ? "" : "s"} to ${out.file}`,
768
+ ],
769
+ };
770
+ const diffSpec = {
771
+ name: "diff",
772
+ summary: "Compare the current backend contract with a baseline and fail on breaking changes.",
773
+ input: DIFF_SCHEMA,
774
+ output: output({
775
+ type: "object",
776
+ properties: {
777
+ ok: { type: "boolean" },
778
+ hasBreaking: { type: "boolean" },
779
+ changes: { type: "array" },
780
+ },
781
+ required: ["ok", "hasBreaking", "changes"],
782
+ }),
783
+ transports: ["cli", "mcp"],
784
+ stability: "stable",
785
+ argv: { positionals: ["baseline"], flags: [{ name: "json", field: "json", type: "boolean" }] },
786
+ async run(value, ctx) {
787
+ const mod = await import("./diff-tool.js");
788
+ const baseline = value.baseline ?? mod.DEFAULT_SNAPSHOT_FILE;
789
+ const file = await Bun.file(resolve(ctx.cwd, baseline)).text();
790
+ const before = mod.parseSnapshotFile(file, baseline);
791
+ const current = await mod.snapshotBackend(ctx.cwd);
792
+ const { diffRouteSnapshots } = await import("@nifrajs/core/diff");
793
+ const diff = diffRouteSnapshots(before.routes, current);
794
+ return { ok: !diff.hasBreaking, hasBreaking: diff.hasBreaking, changes: diff.changes };
795
+ },
796
+ render: (out) => [
797
+ out.ok ? "✓ no breaking changes" : "✖ breaking changes",
798
+ ...out.changes.map((c) => `${c.method} ${c.path}: ${c.message}`),
799
+ ],
800
+ success: (out) => out.ok,
801
+ };
802
+ const contractsSpec = {
803
+ name: "contracts",
804
+ summary: "Snapshot or check the deterministic route contract lock.",
805
+ input: CONTRACTS_SCHEMA,
806
+ output: output({ type: "object" }),
807
+ transports: ["cli", "mcp"],
808
+ stability: "stable",
809
+ argv: {
810
+ positionals: ["action"],
811
+ flags: [
812
+ { name: "out", field: "out", type: "string" },
813
+ { name: "json", field: "json", type: "boolean" },
814
+ ],
815
+ },
816
+ async run(value, ctx) {
817
+ const mod = await import("./contracts.js");
818
+ if (value.action === "snapshot")
819
+ return { ok: true, lock: await mod.snapshotContracts(ctx.cwd, value.out) };
820
+ const result = await mod.checkContractsLock(ctx.cwd);
821
+ return { ok: result.present && result.diagnostics.length === 0, ...result };
822
+ },
823
+ render: (out) => [
824
+ out.ok ? "✓ contracts are current" : "✖ contract drift",
825
+ ...(out.diagnostics ?? []).map((d) => ` ${d.message}`),
826
+ ],
827
+ success: (out) => out.ok,
828
+ };
829
+ const syncManifestSpec = {
830
+ name: "sync-manifest",
831
+ summary: "Regenerate generated server-manifest route tables without a full build.",
832
+ input: SYNC_SCHEMA,
833
+ output: output({
834
+ type: "object",
835
+ properties: { ok: { type: "boolean" }, results: { type: "array" } },
836
+ required: ["ok", "results"],
837
+ }),
838
+ transports: ["cli", "mcp"],
839
+ stability: "stable",
840
+ argv: { flags: [{ name: "json", field: "json", type: "boolean" }] },
841
+ async run(_value, ctx) {
842
+ return {
843
+ ok: true,
844
+ results: await (await import("./sync-manifest.js")).syncServerManifests(ctx.cwd),
845
+ };
846
+ },
847
+ render: (out) => [
848
+ out.results.length === 0
849
+ ? "nifra sync-manifest: no generated server-manifest.ts found under this directory."
850
+ : `✓ checked ${out.results.length} generated server manifest${out.results.length === 1 ? "" : "s"}`,
851
+ ],
852
+ };
853
+ const syncRoutesSpec = {
854
+ name: "sync-routes",
855
+ summary: "Regenerate nifra-routes.d.ts so typed navigation follows route search schemas.",
856
+ input: SYNC_SCHEMA,
857
+ output: output({
858
+ type: "object",
859
+ properties: { ok: { type: "boolean" }, results: { type: "array" } },
860
+ required: ["ok", "results"],
861
+ }),
862
+ transports: ["cli", "mcp"],
863
+ stability: "stable",
864
+ argv: { flags: [{ name: "json", field: "json", type: "boolean" }] },
865
+ async run(_value, ctx) {
866
+ const result = await (await import("./sync-routes.js")).syncRouteTypes(ctx.cwd);
867
+ return { ok: true, results: result === null ? [] : [result] };
868
+ },
869
+ render: (out) => [
870
+ out.results.length === 0
871
+ ? "nifra sync-routes: no routes/ directory found. Run from your project root."
872
+ : `✓ checked ${out.results.length} route type output`,
873
+ ],
874
+ };
875
+ const proveSpec = {
876
+ name: "prove",
877
+ summary: "Build the static verification work graph, plan the cheapest proofs for the changed files, and report a machine-checkable stop condition.",
878
+ input: PROVE_SCHEMA,
879
+ output: output({ type: "object" }),
880
+ transports: ["cli", "mcp"],
881
+ stability: "stable",
882
+ argv: {
883
+ flags: [
884
+ { name: "file", field: "files", type: "string[]" },
885
+ { name: "min", field: "minLevel", type: "number" },
886
+ { name: "json", field: "json", type: "boolean" },
887
+ ],
888
+ },
889
+ async run(value, ctx) {
890
+ return collectProjectWorkGraph(ctx.cwd, {
891
+ ...(value.files === undefined ? {} : { changedFiles: value.files }),
892
+ ...(value.minLevel === undefined ? {} : { minLevel: value.minLevel }),
893
+ });
894
+ },
895
+ render: (out) => [renderWorkGraphText(out)],
896
+ success: (out) => out.evidence.stop.done,
897
+ };
898
+ const replaySpec = {
899
+ name: "replay",
900
+ summary: "Validate a token-only verification metadata file and dispatch it against its gate.",
901
+ input: REPLAY_SCHEMA,
902
+ output: output({ type: "object" }),
903
+ transports: ["cli", "mcp"],
904
+ stability: "stable",
905
+ argv: { positionals: ["file"] },
906
+ async run(value, ctx) {
907
+ const { runReplay } = await import("./replay.js");
908
+ return runReplay(ctx.cwd, value.file);
909
+ },
910
+ render: (out) => [JSON.stringify(out, null, 2)],
911
+ success: (out) => out.ok,
912
+ };
913
+ const portSpec = {
914
+ name: "port",
915
+ summary: "Print a feature by deploy-target portability matrix with file:line evidence and gate against an unsupported target.",
916
+ input: PORT_SCHEMA,
917
+ output: output({ type: "object" }),
918
+ transports: ["cli", "mcp"],
919
+ stability: "stable",
920
+ argv: {
921
+ flags: [
922
+ { name: "target", field: "target", type: "string" },
923
+ { name: "json", field: "json", type: "boolean" },
924
+ { name: "ci", field: "ci", type: "boolean" },
925
+ { name: "strict", field: "strict", type: "boolean" },
926
+ ],
927
+ },
928
+ async run(value, ctx) {
929
+ return collectPortResult(ctx.cwd, {
930
+ ...(value.target === undefined ? {} : { target: value.target }),
931
+ ...(value.strict === true ? { strict: true } : {}),
932
+ });
933
+ },
934
+ render: (out, input) => {
935
+ const report = renderReport(out, { strict: input?.strict === true });
936
+ const gating = input?.ci === true || input?.target !== undefined;
937
+ if (gating && out.resolved === undefined)
938
+ return [
939
+ report,
940
+ "",
941
+ "[nifra] --ci needs a deploy target to gate against, and none was detected. Pass --target <bun|node|deno|cf-pages|vercel>.",
942
+ ];
943
+ return [report];
944
+ },
945
+ success: (out, input) => {
946
+ const gating = input.ci === true || input.target !== undefined;
947
+ if (!gating)
948
+ return true;
949
+ if (out.resolved === undefined)
950
+ return false;
951
+ return out.json.blocked.length === 0;
952
+ },
953
+ json: (out) => out.json,
954
+ };
955
+ export const commandSpecs = Object.freeze([
956
+ checkSpec,
957
+ assureSpec,
958
+ levelsSpec,
959
+ capabilitiesSpec,
960
+ manifestSpec,
961
+ routesSpec,
962
+ contextSpec,
963
+ doctorSpec,
964
+ fixSpec,
965
+ snapshotSpec,
966
+ diffSpec,
967
+ contractsSpec,
968
+ syncManifestSpec,
969
+ syncRoutesSpec,
970
+ proveSpec,
971
+ replaySpec,
972
+ portSpec,
973
+ ]);
974
+ const commandByName = new Map(commandSpecs.map((spec) => [spec.name, spec]));
975
+ export function findCommandSpec(name) {
976
+ return commandByName.get(name);
977
+ }
978
+ export const commandCatalog = Object.freeze(commandSpecs.map((spec) => toCommandCatalogEntry(spec)));
979
+ export function commandMcpName(name) {
980
+ return `nifra_${name.replaceAll("-", "_")}`;
981
+ }
982
+ export function commandUsage(spec) {
983
+ const positionals = (spec.argv?.positionals ?? []).map((field) => `<${field}>`).join(" ");
984
+ const flags = (spec.argv?.flags ?? [])
985
+ .filter((flag, index, all) => all.findIndex((x) => x.field === flag.field) === index)
986
+ .map((flag) => (flag.type === "boolean" ? `[--${flag.name}]` : `[--${flag.name} <value>]`))
987
+ .join(" ");
988
+ return [positionals, flags].filter(Boolean).join(" ");
989
+ }
990
+ /** Stable one-line help/card projection; command-specific prose lives only in the catalog. */
991
+ export function renderCommandCatalogLines(prefix = "nifra") {
992
+ return commandCatalog.map((entry) => {
993
+ const spec = findCommandSpec(entry.name);
994
+ const usage = commandUsage(spec);
995
+ return `${prefix} ${entry.name}${usage === "" ? "" : ` ${usage}`} - ${entry.summary}`;
996
+ });
997
+ }
998
+ export function renderCommandCatalogHelp() {
999
+ return ["", "Stable project command catalog:", ...renderCommandCatalogLines(" nifra")].join("\n");
1000
+ }
1001
+ export function parseCommandOutput(spec, value) {
1002
+ const raw = record(value);
1003
+ if (raw.nifraCommand !== undefined && raw.version !== undefined && raw.result !== undefined) {
1004
+ if (raw.version !== spec.output.version)
1005
+ throw new Error(`unsupported ${spec.name} output version: ${String(raw.version)}`);
1006
+ return spec.output.parse(raw.result);
1007
+ }
1008
+ return spec.output.parse(value);
1009
+ }
1010
+ /** Serialize a result as an envelope without forcing existing raw --json consumers to migrate. */
1011
+ export function envelopeCommandOutput(spec, value) {
1012
+ return { nifraCommand: spec.name, version: spec.output.version, result: value };
1013
+ }
1014
+ //# sourceMappingURL=command-catalog.js.map