@nifrajs/cli 2.13.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) 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 +260 -49
  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 +130 -414
  12. package/dist/cli.js.map +1 -1
  13. package/dist/command-catalog.d.ts +250 -0
  14. package/dist/command-catalog.d.ts.map +1 -0
  15. package/dist/command-catalog.js +1032 -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 +55 -0
  22. package/dist/doctor.d.ts.map +1 -1
  23. package/dist/doctor.js +82 -9
  24. package/dist/doctor.js.map +1 -1
  25. package/dist/env-file.d.ts +17 -0
  26. package/dist/env-file.d.ts.map +1 -0
  27. package/dist/env-file.js +113 -0
  28. package/dist/env-file.js.map +1 -0
  29. package/dist/fix-recipes.d.ts.map +1 -1
  30. package/dist/fix-recipes.js +91 -15
  31. package/dist/fix-recipes.js.map +1 -1
  32. package/dist/internal/reserved-segment-codemod.d.ts +36 -0
  33. package/dist/internal/reserved-segment-codemod.d.ts.map +1 -0
  34. package/dist/internal/reserved-segment-codemod.js +134 -0
  35. package/dist/internal/reserved-segment-codemod.js.map +1 -0
  36. package/dist/internal/typescript-import.d.ts +1 -1
  37. package/dist/internal/typescript-import.d.ts.map +1 -1
  38. package/dist/internal/typescript-import.js +62 -11
  39. package/dist/internal/typescript-import.js.map +1 -1
  40. package/dist/introspect.d.ts +26 -1
  41. package/dist/introspect.d.ts.map +1 -1
  42. package/dist/introspect.js +74 -24
  43. package/dist/introspect.js.map +1 -1
  44. package/dist/load.d.ts.map +1 -1
  45. package/dist/load.js +22 -5
  46. package/dist/load.js.map +1 -1
  47. package/dist/manifest-tool.d.ts +15 -1
  48. package/dist/manifest-tool.d.ts.map +1 -1
  49. package/dist/manifest-tool.js +23 -9
  50. package/dist/manifest-tool.js.map +1 -1
  51. package/dist/mcp-http.js +1 -1
  52. package/dist/mcp-http.js.map +1 -1
  53. package/dist/mcp.d.ts +16 -0
  54. package/dist/mcp.d.ts.map +1 -1
  55. package/dist/mcp.js +127 -355
  56. package/dist/mcp.js.map +1 -1
  57. package/dist/project-facts.d.ts +40 -0
  58. package/dist/project-facts.d.ts.map +1 -0
  59. package/dist/project-facts.js +36 -0
  60. package/dist/project-facts.js.map +1 -0
  61. package/dist/recipes/3.0.0.d.ts +10 -0
  62. package/dist/recipes/3.0.0.d.ts.map +1 -0
  63. package/dist/recipes/3.0.0.js +23 -0
  64. package/dist/recipes/3.0.0.js.map +1 -0
  65. package/dist/recipes/index.d.ts.map +1 -1
  66. package/dist/recipes/index.js +2 -0
  67. package/dist/recipes/index.js.map +1 -1
  68. package/dist/release-verification.d.ts.map +1 -1
  69. package/dist/release-verification.js +15 -0
  70. package/dist/release-verification.js.map +1 -1
  71. package/dist/rules/comment-markers.d.ts +8 -0
  72. package/dist/rules/comment-markers.d.ts.map +1 -1
  73. package/dist/rules/comment-markers.js +36 -0
  74. package/dist/rules/comment-markers.js.map +1 -1
  75. package/dist/rules/index.d.ts +2 -3
  76. package/dist/rules/index.d.ts.map +1 -1
  77. package/dist/rules/index.js.map +1 -1
  78. package/dist/rules/routes.d.ts.map +1 -1
  79. package/dist/rules/routes.js +29 -44
  80. package/dist/rules/routes.js.map +1 -1
  81. package/dist/rules/security.js +14 -14
  82. package/dist/rules/security.js.map +1 -1
  83. package/dist/workspace-link.d.ts +37 -0
  84. package/dist/workspace-link.d.ts.map +1 -0
  85. package/dist/workspace-link.js +72 -0
  86. package/dist/workspace-link.js.map +1 -0
  87. package/docs/examples.json +1 -1
  88. package/docs/llms-full.txt +362 -20
  89. package/docs/types.json +956 -56
  90. package/package.json +9 -9
  91. package/src/capabilities-tool.ts +229 -21
  92. package/src/check.ts +312 -65
  93. package/src/cli.ts +140 -405
  94. package/src/command-catalog.ts +1435 -0
  95. package/src/contracts.ts +28 -7
  96. package/src/doctor.ts +134 -12
  97. package/src/env-file.ts +121 -0
  98. package/src/fix-recipes.ts +101 -15
  99. package/src/internal/reserved-segment-codemod.ts +150 -0
  100. package/src/internal/typescript-import.ts +61 -11
  101. package/src/introspect.ts +82 -21
  102. package/src/load.ts +25 -6
  103. package/src/manifest-tool.ts +43 -12
  104. package/src/mcp-http.ts +1 -1
  105. package/src/mcp.ts +163 -420
  106. package/src/project-facts.ts +86 -0
  107. package/src/recipes/3.0.0.ts +24 -0
  108. package/src/recipes/index.ts +2 -0
  109. package/src/release-verification.ts +17 -0
  110. package/src/rules/comment-markers.ts +36 -0
  111. package/src/rules/index.ts +3 -4
  112. package/src/rules/routes.ts +30 -44
  113. package/src/rules/security.ts +14 -14
  114. package/src/workspace-link.ts +80 -0
package/dist/cli.js CHANGED
@@ -17,12 +17,13 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
17
17
  *
18
18
  * Bun-only (it runs the framework's TS + Bun plugins directly). The *output* runs anywhere.
19
19
  */
20
- import { existsSync } from "node:fs";
20
+ import { existsSync, readFileSync } from "node:fs";
21
21
  import { resolve } from "node:path";
22
22
  import { inProcessClient } from "@nifrajs/client";
23
23
  import { createWebApp, DEFAULT_DEV_PORT, } from "@nifrajs/web";
24
24
  import { discoverRoutes } from "@nifrajs/web/fs";
25
- import { describeProject, describeRouteGraph, describeRoutes } from "./introspect.js";
25
+ import { bindCommandArgv, findCommandSpec, renderCommandCatalogHelp } from "./command-catalog.js";
26
+ import { applyEnvFiles, takeEnvFileFlags } from "./env-file.js";
26
27
  import { loadApp } from "./load.js";
27
28
  import { chooseBuildPipeline, describePipeline } from "./pipeline-guard.js";
28
29
  const HELP = `nifra - zero-config dev/build/start for a nifra app
@@ -36,6 +37,10 @@ Usage:
36
37
  preserved), no Vite in the process. CSS Modules, plain CSS,
37
38
  Tailwind and the app's own \`clientPlugins\`/\`serverPlugins\` run
38
39
  through the same Bun transforms production uses.
40
+ [--allow-duplicate-identity] Downgrade the Vite dev server's identity-parity check to a
41
+ loud warning instead of a hard failure, so a duplicate React/
42
+ adapter copy from a linked sibling repo doesn't take dev down
43
+ while you fix the resolution. Dev only - \`nifra build\` still fails.
39
44
  nifra build [--out <dir>] [--report] Emit a complete deploy directory.
40
45
  [--target <t>] Target a FULL deploy dir for <t>:
41
46
  bun | node | deno | cf-pages | vercel | static. Packages
@@ -106,11 +111,13 @@ Usage:
106
111
  closed. Exits non-zero on any breaking change - run it in CI.
107
112
  nifra sdk --lang <python|go> [--out <file>]
108
113
  Generate a deterministic non-TypeScript SDK from backend.ts.
109
- nifra assure [--config <file>] [--json] [--strict] [--out <file>] [--hydration] [--interact]
110
- Emit one structured assurance bundle. The bundle records every
111
- applicable gate as pass, fail, or skip and exits non-zero unless
112
- its verdict is green. Without these bundle flags, the legacy
113
- route-assurance report remains available.
114
+ nifra assure [--config <file>] [--json] Route-assurance report. Human table by default; --json emits
115
+ the {ok, routes, findings} report for agents.
116
+ nifra assure --bundle [--json] [--strict] [--out <file>] [--hydration] [--interact]
117
+ Emit one structured assurance {version, gates, verdict} bundle
118
+ (always JSON). Records every applicable gate as pass, fail, or
119
+ skip and exits non-zero unless its verdict is green. --strict,
120
+ --hydration, --interact and --out imply --bundle.
114
121
  nifra contracts snapshot [--out <file>] Write the deterministic route contract lock.
115
122
  nifra contracts check [--json] Check the current routes against contracts.lock.json.
116
123
  nifra replay <file> Replay a token-only verification metadata file.
@@ -120,6 +127,8 @@ Usage:
120
127
  nifra capabilities check [--lockfile <file>] [--config <file>] [--json]
121
128
  CI gate: fail on raw effect-import bypasses, declaration/evidence
122
129
  drift, unsafe GET/HEAD writes, idempotency gaps, or lockfile drift.
130
+ nifra capabilities explain <METHOD> <path> [--config <file>] [--json]
131
+ Explain one route's declared capability tokens and static provenance.
123
132
  nifra manifest emit [--out <file>] [--config <file>] [--sign <key-ref>]
124
133
  Emit a deterministic route trust artifact after assurance passes;
125
134
  optionally write an Ed25519 signature via the configured KMS signer.
@@ -157,9 +166,17 @@ Reads nifra.config.ts (adapter + clientModule + plugins; or framework.ts), backe
157
166
  routes/ from the current directory. Run from your project root.
158
167
 
159
168
  Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with \`--port <n>\` (alias \`-p\`) or the
160
- \`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).`;
169
+ \`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).
170
+
171
+ Env: \`--env-file <path>\` works on EVERY command and may be repeated (later files win). Commands that
172
+ reflect your app import it, so an app that validates its environment at module scope needs that
173
+ environment present - without it the app aborts before nifra runs a single check. A variable already
174
+ set in the process environment is never overwritten by a file.
175
+ e.g. \`nifra check --env-file .env.local\`
176
+
177
+ ${renderCommandCatalogHelp()}`;
161
178
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
162
- const CLI_VERSION = "2.13.0";
179
+ const CLI_VERSION = "3.1.0";
163
180
  // A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
164
181
  const asAdapter = (v) => v;
165
182
  const asBunPlugins = (v) => v;
@@ -354,6 +371,7 @@ async function dev(app, flags) {
354
371
  ...(fw.publicEnvPrefix !== undefined ? { publicEnvPrefix: fw.publicEnvPrefix } : {}),
355
372
  poll: flags.poll,
356
373
  port: flags.port,
374
+ ...(flags.allowDuplicateIdentity ? { allowDuplicateIdentity: true } : {}),
357
375
  ...(fw.conditions ? { conditions: fw.conditions } : {}),
358
376
  ...(fw.define ? { define: fw.define } : {}),
359
377
  // `load` resolves route modules through VITE, not through Bun. That is what makes the Vite
@@ -403,6 +421,34 @@ export async function assertUseIsEdgeExported(use, configPath, frameworkFile) {
403
421
  " - Define `use` in framework.ts and re-export it from nifra.config.ts " +
404
422
  '(`export { use } from "./framework.ts"`) so `nifra dev` sees it too.');
405
423
  }
424
+ /**
425
+ * A production build must not ship a workspace-linked package whose `default`-condition artifact is
426
+ * missing or older than its source. The `"bun": "./src"` / `"default": "./dist"` split means Bun (this
427
+ * build, the tests) reads live source while the deployed app and any node consumer read `dist/` - so a
428
+ * green `nifra build` can bundle stale or absent dist and the drift never shows in a diff. The doctor
429
+ * reports the same skew as an advisory during development; at build time it is a hard failure, because
430
+ * this is the artifact that ships. No-op outside a workspace: tarball-installed deps cannot drift.
431
+ */
432
+ async function assertFreshWorkspaceDists(cwd) {
433
+ const pkgPath = resolve(cwd, "package.json");
434
+ if (!existsSync(pkgPath))
435
+ return;
436
+ let rootPackage;
437
+ try {
438
+ rootPackage = JSON.parse(readFileSync(pkgPath, "utf8"));
439
+ }
440
+ catch {
441
+ return; // an unreadable/!JSON package.json is a separate problem; don't mask it as a stale-dist failure
442
+ }
443
+ const { collectStaleWorkspaceDists } = await import("./doctor.js");
444
+ const stale = await collectStaleWorkspaceDists(cwd, rootPackage);
445
+ if (stale.length === 0)
446
+ return;
447
+ const lines = stale.map((f) => f.missing
448
+ ? ` ${f.package}: ${f.distFile} was never built (source ${f.sourceFile})`
449
+ : ` ${f.package}: ${f.distFile} is ${f.behindSeconds}s behind ${f.sourceFile}`);
450
+ throw new Error(`[nifra] build blocked: ${stale.length} workspace-linked package(s) ship a stale or missing dist artifact, so the deployed app would serve code this build did not produce:\n${lines.join("\n")}\nBuild the offending package(s) (\`bun run build\` in each) so every export target exists and is at least as new as its source, then rebuild.`);
451
+ }
406
452
  async function buildForTarget(app, target, flags) {
407
453
  const { isBuildTarget, renderSizeReport, BUILD_TARGETS } = await import("@nifrajs/web/build");
408
454
  if (!isBuildTarget(target)) {
@@ -460,6 +506,11 @@ async function buildForTarget(app, target, flags) {
460
506
  // The static target needs a built app to drive prerendering - only build it when targeting static.
461
507
  ...(target === "static" ? { prerenderApp: await buildPrerenderApp(app) } : {}),
462
508
  });
509
+ // A production build must not certify itself green while a workspace-linked dependency ships a dist
510
+ // artifact older than (or absent next to) its source: the `bun`->src condition let this compile read
511
+ // live source, but the deployed app and every node consumer read `dist/`. Gate here, after the compile
512
+ // proved the source is buildable, so the failure names the package to rebuild rather than a bundle error.
513
+ await assertFreshWorkspaceDists(cwd);
463
514
  console.log(`nifra build (${target}, ${decision.pipeline}) → ${result.run}`);
464
515
  // The pipeline is stated on every build, not only the surprising ones - an auto-selected Vite build
465
516
  // must never look like the default, and a default Bun build must not leave the question open either.
@@ -515,6 +566,7 @@ export function parseFlags(args) {
515
566
  let report = false;
516
567
  let vite = false;
517
568
  let bun = false;
569
+ let allowDuplicateIdentity = false;
518
570
  for (let i = 0; i < args.length; i++) {
519
571
  const a = args[i];
520
572
  if ((a === "--port" || a === "-p") && args[i + 1])
@@ -531,14 +583,54 @@ export function parseFlags(args) {
531
583
  vite = true;
532
584
  else if (a === "--bun")
533
585
  bun = true;
586
+ else if (a === "--allow-duplicate-identity")
587
+ allowDuplicateIdentity = true;
534
588
  }
535
589
  if (!Number.isFinite(port) || port < 0 || port > 65535) {
536
590
  throw new Error(`[nifra] invalid --port: ${port}`);
537
591
  }
538
- return { port, out, poll, target, report, vite, bun };
592
+ return { port, out, poll, target, report, vite, bun, allowDuplicateIdentity };
593
+ }
594
+ /**
595
+ * `assure` uses the structured-bundle lane only when explicitly asked. A bare `--json` is NOT a
596
+ * request for the bundle - it keeps the legacy `{ ok, routes, findings }` report machine-readable, so a
597
+ * pre-bundle consumer is never silently handed the `{ gates, verdict }` shape. `--bundle` (or a
598
+ * bundle-only flag) opts in; `--bundle --json` still yields the bundle, which is always JSON.
599
+ */
600
+ export function assureBundleRequested(argv) {
601
+ return (argv.includes("--bundle") ||
602
+ argv.includes("--strict") ||
603
+ argv.includes("--hydration") ||
604
+ argv.includes("--interact") ||
605
+ argv.includes("--out"));
606
+ }
607
+ /**
608
+ * A command that reflects the project imports it, and an app that validates its environment at module
609
+ * scope calls `process.exit` from inside that import. Nothing is throwable at that point, so the whole
610
+ * output is the app's own abort message with no hint that a nifra command triggered it. Name the cause
611
+ * on the way out and point at the flag that supplies the environment.
612
+ */
613
+ function installReflectionExitHint() {
614
+ let inFlight;
615
+ process.on("exit", (code) => {
616
+ if (inFlight === undefined || code === 0)
617
+ return;
618
+ process.stderr.write(`\n[nifra] \`nifra ${inFlight}\` reflects this project by importing it, and the import exited ` +
619
+ `with code ${code} before the command produced a result - the message above came from your app, not nifra.\n` +
620
+ ` If it is the app's environment validation, supply the environment and re-run:\n` +
621
+ ` nifra ${inFlight} --env-file .env.local\n`);
622
+ });
623
+ return (command) => {
624
+ inFlight = command;
625
+ };
539
626
  }
540
627
  async function main() {
541
- const argv = Bun.argv.slice(2);
628
+ const { argv: rawArgv, files: envFiles } = takeEnvFileFlags(Bun.argv.slice(2));
629
+ // Applied before any command runs: a reflecting command imports the app on its first await, and the
630
+ // app reads its environment at module scope, so the variables must already be in place by then.
631
+ if (envFiles.length > 0)
632
+ await applyEnvFiles(process.cwd(), envFiles);
633
+ const argv = rawArgv;
542
634
  const command = argv[0];
543
635
  if (command === undefined || command === "--help" || command === "-h" || command === "help") {
544
636
  console.log(HELP);
@@ -579,16 +671,33 @@ async function main() {
579
671
  console.log(renderLearnResult(stepArg !== undefined && /^\d+$/.test(stepArg) ? Number(stepArg) : undefined));
580
672
  return;
581
673
  }
582
- // `check` is a pure cwd-based gate (typecheck + lint) - it must run even when the project doesn't
583
- // load (API-only, not built yet), so it dispatches before the eager `loadApp` below.
584
- if (command === "check") {
585
- const { runCheck } = await import("./check.js");
586
- if (!(await runCheck(process.cwd(), {
587
- json: argv.includes("--json"),
588
- structured: argv.includes("--json"),
589
- lintsOnly: argv.includes("--lints-only"),
590
- })))
674
+ const catalogSpec = command === undefined ? undefined : findCommandSpec(command);
675
+ if (catalogSpec?.transports.includes("cli")) {
676
+ const markReflecting = installReflectionExitHint();
677
+ try {
678
+ const input = bindCommandArgv(catalogSpec, argv.slice(1));
679
+ markReflecting(command);
680
+ const output = await catalogSpec.run(input, { cwd: process.cwd(), cliVersion: CLI_VERSION });
681
+ markReflecting(undefined);
682
+ const asRecord = output;
683
+ const wantsJson = asRecord.json === true || argv.includes("--json");
684
+ if (wantsJson) {
685
+ const value = catalogSpec.json?.(output, input) ?? output;
686
+ console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
687
+ }
688
+ else {
689
+ console.log(catalogSpec.render(output, input).join("\n"));
690
+ }
691
+ if (catalogSpec.success !== undefined && !catalogSpec.success(output, input))
692
+ process.exitCode = 1;
693
+ }
694
+ catch (err) {
695
+ // A thrown error is nifra's own reporting path, not a silent abort: clear the sentinel so the
696
+ // hint stays reserved for the case where the process died inside the app import.
697
+ markReflecting(undefined);
698
+ console.error(formatCliError(err));
591
699
  process.exitCode = 1;
700
+ }
592
701
  return;
593
702
  }
594
703
  if (command === "verify") {
@@ -601,30 +710,6 @@ async function main() {
601
710
  process.exitCode = 1;
602
711
  return;
603
712
  }
604
- if (command === "fix") {
605
- const codeIndex = argv.indexOf("--code");
606
- const code = codeIndex === -1 ? undefined : argv[codeIndex + 1];
607
- try {
608
- const { collectCheckResult } = await import("./check.js");
609
- const { applyDiagnosticRecipe } = await import("./fix-recipes.js");
610
- const result = await collectCheckResult(process.cwd(), { lintsOnly: true });
611
- const changed = [];
612
- for (const diagnostic of result.structuredDiagnostics ?? []) {
613
- if (code !== undefined && diagnostic.code !== code)
614
- continue;
615
- changed.push(...(await applyDiagnosticRecipe(process.cwd(), diagnostic)));
616
- }
617
- const final = await collectCheckResult(process.cwd(), { lintsOnly: true });
618
- console.log(JSON.stringify({ changed, diagnostics: final.structuredDiagnostics ?? [] }, null, 2));
619
- if (final.ok === false)
620
- process.exitCode = 1;
621
- }
622
- catch (err) {
623
- console.error(formatCliError(err));
624
- process.exitCode = 1;
625
- }
626
- return;
627
- }
628
713
  // `init-agents` retrofits the agent-discovery files (.mcp.json, CLAUDE.md, …) into the cwd. It's a
629
714
  // pure file-writing command independent of the app loading, so dispatch it before the eager `loadApp`
630
715
  // (an existing app might be API-only or not yet built). It always succeeds unless a write throws.
@@ -636,77 +721,6 @@ async function main() {
636
721
  });
637
722
  return;
638
723
  }
639
- // `sync-manifest` rewrites a committed web `server-manifest.ts` from the current `routes/` tree, without
640
- // a full build. Pure fs/text (re-scan routes, preserve baked client assets), so - like `check` - dispatch
641
- // it before the eager `loadApp`.
642
- if (command === "sync-manifest") {
643
- const { runSyncManifest } = await import("./sync-manifest.js");
644
- if (!(await runSyncManifest(process.cwd())))
645
- process.exitCode = 1;
646
- return;
647
- }
648
- // `sync-routes` regenerates the route-search types `.d.ts` (typed `navigate({ to, search })`) from
649
- // `routes/`. Pure fs/text (needs only the route tree, not `framework.ts`), so dispatch before `loadApp`.
650
- if (command === "sync-routes") {
651
- const { runSyncRoutes } = await import("./sync-routes.js");
652
- if (!(await runSyncRoutes(process.cwd())))
653
- process.exitCode = 1;
654
- return;
655
- }
656
- // `doctor` is a pure cwd check (imports vs declared deps) - like `check`, dispatch before `loadApp`
657
- // so it runs on an API-only / not-yet-built project.
658
- if (command === "doctor") {
659
- const { runDoctor } = await import("./doctor.js");
660
- const targetIdx = argv.indexOf("--target");
661
- const target = targetIdx !== -1 ? argv[targetIdx + 1] : undefined;
662
- if (targetIdx !== -1 && (target === undefined || target.startsWith("-"))) {
663
- console.error("[nifra] --target needs a value: bun | node | deno | cf-pages | vercel");
664
- process.exitCode = 1;
665
- return;
666
- }
667
- if (!(await runDoctor(process.cwd(), {
668
- json: argv.includes("--json"),
669
- autoFix: argv.includes("--auto-fix") || argv.includes("--fix"),
670
- strict: argv.includes("--strict"),
671
- ...(target === undefined ? {} : { target }),
672
- })))
673
- process.exitCode = 1;
674
- return;
675
- }
676
- // `snapshot` / `diff` load ONLY backend.ts (the API contract) - like `check`, they must run on an
677
- // API-only project, so they dispatch before the eager `loadApp`.
678
- if (command === "snapshot") {
679
- const { runSnapshot } = await import("./diff-tool.js");
680
- const outIdx = argv.indexOf("--out");
681
- const out = outIdx !== -1 ? argv[outIdx + 1] : undefined;
682
- if (outIdx !== -1 && (out === undefined || out.startsWith("-"))) {
683
- console.error("[nifra] --out needs a file path");
684
- process.exitCode = 1;
685
- return;
686
- }
687
- try {
688
- await runSnapshot(process.cwd(), out !== undefined ? { out } : {});
689
- }
690
- catch (err) {
691
- console.error(formatCliError(err));
692
- process.exitCode = 1;
693
- }
694
- return;
695
- }
696
- if (command === "diff") {
697
- const { runDiff, DEFAULT_SNAPSHOT_FILE } = await import("./diff-tool.js");
698
- const baseline = argv.slice(1).find((arg) => !arg.startsWith("-")) ?? DEFAULT_SNAPSHOT_FILE;
699
- try {
700
- if (!(await runDiff(process.cwd(), baseline, { json: argv.includes("--json") }))) {
701
- process.exitCode = 1;
702
- }
703
- }
704
- catch (err) {
705
- console.error(formatCliError(err));
706
- process.exitCode = 1;
707
- }
708
- return;
709
- }
710
724
  if (command === "sdk") {
711
725
  const languageIndex = argv.indexOf("--lang");
712
726
  const language = languageIndex === -1 ? undefined : argv[languageIndex + 1];
@@ -732,241 +746,6 @@ async function main() {
732
746
  }
733
747
  return;
734
748
  }
735
- // Route assurance intentionally loads only its explicit config (which imports the backend it audits),
736
- // not the full SSR app. Like snapshot/diff, this works for API-only projects.
737
- if (command === "assure") {
738
- const { runAssurance } = await import("./assure.js");
739
- const configIdx = argv.indexOf("--config");
740
- const config = configIdx !== -1 ? argv[configIdx + 1] : undefined;
741
- if (configIdx !== -1 && (config === undefined || config.startsWith("-"))) {
742
- console.error("[nifra] --config needs a file path");
743
- process.exitCode = 1;
744
- return;
745
- }
746
- try {
747
- if (!(await runAssurance(process.cwd(), {
748
- json: argv.includes("--json"),
749
- bundle: argv.includes("--json") ||
750
- argv.includes("--strict") ||
751
- argv.includes("--hydration") ||
752
- argv.includes("--interact") ||
753
- argv.includes("--out"),
754
- strict: argv.includes("--strict"),
755
- hydration: argv.includes("--hydration"),
756
- interact: argv.includes("--interact"),
757
- ...(argv.includes("--out") ? { out: argv[argv.indexOf("--out") + 1] } : {}),
758
- ...(config !== undefined ? { config } : {}),
759
- })))
760
- process.exitCode = 1;
761
- }
762
- catch (err) {
763
- console.error(formatCliError(err));
764
- process.exitCode = 1;
765
- }
766
- return;
767
- }
768
- if (command === "contracts") {
769
- const action = argv[1];
770
- const outIndex = argv.indexOf("--out");
771
- const out = outIndex === -1 ? undefined : argv[outIndex + 1];
772
- try {
773
- const { checkContractsLock, snapshotContracts } = await import("./contracts.js");
774
- if (action === "snapshot") {
775
- const lock = await snapshotContracts(process.cwd(), out);
776
- if (argv.includes("--json"))
777
- console.log(JSON.stringify(lock, null, 2));
778
- else
779
- console.log(`[nifra] wrote ${Object.keys(lock.routes).length} contract digests`);
780
- }
781
- else if (action === "check") {
782
- const result = await checkContractsLock(process.cwd());
783
- const output = { ok: result.present && result.diagnostics.length === 0, ...result };
784
- console.log(JSON.stringify(output, null, 2));
785
- if (!output.ok)
786
- process.exitCode = 1;
787
- }
788
- else {
789
- console.error("[nifra] contracts needs snapshot or check");
790
- process.exitCode = 1;
791
- }
792
- }
793
- catch (err) {
794
- console.error(formatCliError(err));
795
- process.exitCode = 1;
796
- }
797
- return;
798
- }
799
- if (command === "replay") {
800
- const file = argv[1];
801
- if (file === undefined || file.startsWith("-")) {
802
- console.error("[nifra] replay needs a file path");
803
- process.exitCode = 1;
804
- return;
805
- }
806
- try {
807
- const { runReplay } = await import("./replay.js");
808
- const result = await runReplay(process.cwd(), file);
809
- console.log(JSON.stringify(result, null, 2));
810
- if (!result.ok)
811
- process.exitCode = 1;
812
- }
813
- catch (err) {
814
- console.error(formatCliError(err));
815
- process.exitCode = 1;
816
- }
817
- return;
818
- }
819
- if (command === "capabilities") {
820
- const action = argv[1];
821
- const valueAfter = (flag) => {
822
- const index = argv.indexOf(flag);
823
- const value = index === -1 ? undefined : argv[index + 1];
824
- if (index !== -1 && (value === undefined || value.startsWith("-"))) {
825
- throw new Error(`[nifra] ${flag} needs a file path`);
826
- }
827
- return value;
828
- };
829
- try {
830
- const { runCapabilityCheck, runCapabilitySnapshot } = await import("./capabilities-tool.js");
831
- const config = valueAfter("--config");
832
- if (action === "snapshot") {
833
- const out = valueAfter("--out");
834
- if (!(await runCapabilitySnapshot(process.cwd(), {
835
- ...(config !== undefined ? { config } : {}),
836
- ...(out !== undefined ? { out } : {}),
837
- })))
838
- process.exitCode = 1;
839
- }
840
- else if (action === "check") {
841
- const lockfile = valueAfter("--lockfile");
842
- if (!(await runCapabilityCheck(process.cwd(), {
843
- ...(config !== undefined ? { config } : {}),
844
- ...(lockfile !== undefined ? { lockfile } : {}),
845
- json: argv.includes("--json"),
846
- })))
847
- process.exitCode = 1;
848
- }
849
- else {
850
- throw new Error("[nifra] capabilities needs `snapshot` or `check`");
851
- }
852
- }
853
- catch (err) {
854
- console.error(formatCliError(err));
855
- process.exitCode = 1;
856
- }
857
- return;
858
- }
859
- if (command === "manifest") {
860
- const action = argv[1];
861
- const valueAfter = (flag) => {
862
- const index = argv.indexOf(flag);
863
- const value = index === -1 ? undefined : argv[index + 1];
864
- if (index !== -1 && (value === undefined || value.startsWith("-"))) {
865
- throw new Error(`[nifra] ${flag} needs a value`);
866
- }
867
- return value;
868
- };
869
- try {
870
- const { runManifestDiff, runManifestEmit } = await import("./manifest-tool.js");
871
- if (action === "emit") {
872
- const config = valueAfter("--config");
873
- const out = valueAfter("--out");
874
- const sign = valueAfter("--sign");
875
- if (!(await runManifestEmit(process.cwd(), {
876
- ...(config !== undefined ? { config } : {}),
877
- ...(out !== undefined ? { out } : {}),
878
- ...(sign !== undefined ? { sign } : {}),
879
- })))
880
- process.exitCode = 1;
881
- }
882
- else if (action === "diff") {
883
- const paths = argv.slice(2).filter((arg) => !arg.startsWith("-"));
884
- if (paths.length !== 2)
885
- throw new Error("[nifra] manifest diff needs <before> <after>");
886
- if (!(await runManifestDiff(process.cwd(), paths[0], paths[1], {
887
- json: argv.includes("--json"),
888
- })))
889
- process.exitCode = 1;
890
- }
891
- else {
892
- throw new Error("[nifra] manifest needs `emit` or `diff`");
893
- }
894
- }
895
- catch (err) {
896
- console.error(formatCliError(err));
897
- process.exitCode = 1;
898
- }
899
- return;
900
- }
901
- if (command === "levels") {
902
- const valueAfter = (flag) => {
903
- const index = argv.indexOf(flag);
904
- const value = index === -1 ? undefined : argv[index + 1];
905
- if (index !== -1 && (value === undefined || value.startsWith("-"))) {
906
- throw new Error(`[nifra] ${flag} needs a value`);
907
- }
908
- return value;
909
- };
910
- try {
911
- const { runLevels } = await import("./levels-tool.js");
912
- const config = valueAfter("--config");
913
- const minRaw = valueAfter("--min");
914
- const min = minRaw === undefined ? undefined : Number(minRaw);
915
- if (min !== undefined && (!Number.isInteger(min) || min < 0 || min > 4)) {
916
- throw new Error("[nifra] --min must be an integer level between 0 and 4");
917
- }
918
- if (!(await runLevels(process.cwd(), {
919
- json: argv.includes("--json"),
920
- ...(config !== undefined ? { config } : {}),
921
- ...(min !== undefined ? { min } : {}),
922
- })))
923
- process.exitCode = 1;
924
- }
925
- catch (err) {
926
- console.error(formatCliError(err));
927
- process.exitCode = 1;
928
- }
929
- return;
930
- }
931
- if (command === "prove") {
932
- const valueAfter = (flag) => {
933
- const index = argv.indexOf(flag);
934
- const value = index === -1 ? undefined : argv[index + 1];
935
- if (index !== -1 && (value === undefined || value.startsWith("-"))) {
936
- throw new Error(`[nifra] ${flag} needs a value`);
937
- }
938
- return value;
939
- };
940
- try {
941
- const minRaw = valueAfter("--min");
942
- const min = minRaw === undefined ? undefined : Number(minRaw);
943
- if (min !== undefined && (!Number.isInteger(min) || min < 0 || min > 4)) {
944
- throw new Error("[nifra] --min must be an integer level between 0 and 4");
945
- }
946
- const changedFiles = [];
947
- for (let index = 0; index < argv.length; index++) {
948
- if (argv[index] !== "--file")
949
- continue;
950
- const file = argv[index + 1];
951
- if (file === undefined || file.startsWith("-"))
952
- throw new Error("[nifra] --file needs a path");
953
- changedFiles.push(file);
954
- index++;
955
- }
956
- const { runWorkGraph } = await import("./work-graph.js");
957
- if (!(await runWorkGraph(process.cwd(), {
958
- json: argv.includes("--json"),
959
- ...(changedFiles.length === 0 ? {} : { changedFiles }),
960
- ...(min === undefined ? {} : { minLevel: min }),
961
- })))
962
- process.exitCode = 1;
963
- }
964
- catch (err) {
965
- console.error(formatCliError(err));
966
- process.exitCode = 1;
967
- }
968
- return;
969
- }
970
749
  // `upgrade` is a pure cwd file-transformer (package.json pins + import moves) driven by a per-release
971
750
  // recipe, then verified with `nifra check`. Dispatch before the eager `loadApp` - it must run on any
972
751
  // repo (API-only, not built, or mid-upgrade with edits that don't yet typecheck under dry-run).
@@ -993,36 +772,7 @@ async function main() {
993
772
  }
994
773
  // `port` is a pure cwd-based portability linter (scans source, doesn't run the app) - like `check`/
995
774
  // `doctor`, dispatch before the eager `loadApp` so it runs on an API-only / not-yet-built project.
996
- if (command === "port") {
997
- const { runPort } = await import("./port.js");
998
- const targetIdx = argv.indexOf("--target");
999
- const target = targetIdx !== -1 ? argv[targetIdx + 1] : undefined;
1000
- if (targetIdx !== -1 && (target === undefined || target.startsWith("-"))) {
1001
- console.error("[nifra] --target needs a value: bun | node | deno | cf-pages | vercel");
1002
- process.exitCode = 1;
1003
- return;
1004
- }
1005
- try {
1006
- const passed = await runPort(process.cwd(), {
1007
- ...(target !== undefined ? { target } : {}),
1008
- json: argv.includes("--json"),
1009
- ci: argv.includes("--ci"),
1010
- strict: argv.includes("--strict"),
1011
- });
1012
- if (!passed)
1013
- process.exitCode = 1;
1014
- }
1015
- catch (err) {
1016
- console.error(formatCliError(err));
1017
- process.exitCode = 1;
1018
- }
1019
- return;
1020
- }
1021
- if (command !== "dev" &&
1022
- command !== "build" &&
1023
- command !== "start" &&
1024
- command !== "context" &&
1025
- command !== "routes") {
775
+ if (command !== "dev" && command !== "build" && command !== "start") {
1026
776
  console.error(`[nifra] unknown command: ${command}\n`);
1027
777
  console.error(HELP);
1028
778
  process.exitCode = 1;
@@ -1034,40 +784,6 @@ async function main() {
1034
784
  await dev(app, flags);
1035
785
  else if (command === "build")
1036
786
  await buildForTarget(app, flags.target, flags);
1037
- else if (command === "context")
1038
- console.log(describeProject(app));
1039
- else if (command === "routes") {
1040
- if (argv.includes("--graph")) {
1041
- console.log(await describeRouteGraph(app, { json: argv.includes("--json") }));
1042
- }
1043
- else if (argv.includes("--modes")) {
1044
- // `--modes` answers a different question than the default table: not "what URLs exist" but "how
1045
- // does each render, and can the target I'm about to deploy to actually honour that". Resolves
1046
- // against `--target` when given, so a `static` build's server-needing route fails here, in one
1047
- // place, instead of 404ing in production.
1048
- const { buildRouteManifest, renderRouteManifest } = await import("@nifrajs/web/route-manifest");
1049
- const manifest = discoverRoutes(app.routesDir);
1050
- // `flags.target` defaults to "bun" for `build`; here an EXPLICIT `--target` is what opts into
1051
- // gating, so read it from argv rather than inheriting the build default (which would silently gate
1052
- // every `nifra routes --modes` against bun).
1053
- const targetIdx = argv.indexOf("--target");
1054
- const target = targetIdx !== -1 ? argv[targetIdx + 1] : undefined;
1055
- const routeManifest = await buildRouteManifest(manifest, {
1056
- ...(target !== undefined ? { target } : {}),
1057
- });
1058
- if (argv.includes("--json"))
1059
- console.log(JSON.stringify(routeManifest, null, 2));
1060
- else
1061
- console.log(renderRouteManifest(routeManifest));
1062
- // A target that cannot honour a route is a build-breaking fact, not a note: exit non-zero so the
1063
- // same command in CI gates the deploy it is warning about.
1064
- if (routeManifest.conflicts.length > 0)
1065
- process.exitCode = 1;
1066
- }
1067
- else {
1068
- console.log(await describeRoutes(app, { json: argv.includes("--json") }));
1069
- }
1070
- }
1071
787
  else
1072
788
  await start(app, flags);
1073
789
  }