@nifrajs/cli 2.12.1 → 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 (74) 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 +235 -47
  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 +96 -406
  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 +18 -11
  22. package/dist/doctor.d.ts.map +1 -1
  23. package/dist/doctor.js +30 -230
  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/release-verification.d.ts +45 -0
  42. package/dist/release-verification.d.ts.map +1 -0
  43. package/dist/release-verification.js +303 -0
  44. package/dist/release-verification.js.map +1 -0
  45. package/dist/rules/comment-markers.d.ts +8 -0
  46. package/dist/rules/comment-markers.d.ts.map +1 -1
  47. package/dist/rules/comment-markers.js +36 -0
  48. package/dist/rules/comment-markers.js.map +1 -1
  49. package/dist/rules/index.d.ts +2 -3
  50. package/dist/rules/index.d.ts.map +1 -1
  51. package/dist/rules/index.js.map +1 -1
  52. package/dist/rules/routes.d.ts.map +1 -1
  53. package/dist/rules/routes.js +12 -14
  54. package/dist/rules/routes.js.map +1 -1
  55. package/dist/rules/security.js +14 -14
  56. package/dist/rules/security.js.map +1 -1
  57. package/docs/types.json +28 -15
  58. package/package.json +9 -9
  59. package/src/capabilities-tool.ts +229 -21
  60. package/src/check.ts +286 -64
  61. package/src/cli.ts +106 -399
  62. package/src/command-catalog.ts +1415 -0
  63. package/src/contracts.ts +28 -7
  64. package/src/doctor.ts +56 -264
  65. package/src/load.ts +25 -6
  66. package/src/manifest-tool.ts +43 -12
  67. package/src/mcp-http.ts +1 -1
  68. package/src/mcp.ts +82 -414
  69. package/src/project-facts.ts +86 -0
  70. package/src/release-verification.ts +377 -0
  71. package/src/rules/comment-markers.ts +36 -0
  72. package/src/rules/index.ts +3 -4
  73. package/src/rules/routes.ts +13 -14
  74. package/src/rules/security.ts +14 -14
package/dist/cli.js CHANGED
@@ -17,12 +17,12 @@ 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
26
  import { loadApp } from "./load.js";
27
27
  import { chooseBuildPipeline, describePipeline } from "./pipeline-guard.js";
28
28
  const HELP = `nifra - zero-config dev/build/start for a nifra app
@@ -36,6 +36,10 @@ Usage:
36
36
  preserved), no Vite in the process. CSS Modules, plain CSS,
37
37
  Tailwind and the app's own \`clientPlugins\`/\`serverPlugins\` run
38
38
  through the same Bun transforms production uses.
39
+ [--allow-duplicate-identity] Downgrade the Vite dev server's identity-parity check to a
40
+ loud warning instead of a hard failure, so a duplicate React/
41
+ adapter copy from a linked sibling repo doesn't take dev down
42
+ while you fix the resolution. Dev only - \`nifra build\` still fails.
39
43
  nifra build [--out <dir>] [--report] Emit a complete deploy directory.
40
44
  [--target <t>] Target a FULL deploy dir for <t>:
41
45
  bun | node | deno | cf-pages | vercel | static. Packages
@@ -87,6 +91,8 @@ Usage:
87
91
  nifra check [--json] [--lints-only] Gate: typecheck + lints (hand-rolled fetch(), untyped client("…"),
88
92
  server-only imports in routes/). Run as "done"; --json for agents;
89
93
  --lints-only skips tsc for a near-instant inner-loop pass.
94
+ nifra verify [--release] [--json] Run the shared repository verification gate. --release runs the
95
+ full build, test, coverage, corpus, consumer, and cross-runtime gate.
90
96
  nifra fix [--code <NF-code>] Apply a registered diagnostic recipe, then print the remaining
91
97
  structured diagnostics.
92
98
  nifra sync-manifest Regenerate a committed web server-manifest.ts from routes/ WITHOUT
@@ -104,11 +110,13 @@ Usage:
104
110
  closed. Exits non-zero on any breaking change - run it in CI.
105
111
  nifra sdk --lang <python|go> [--out <file>]
106
112
  Generate a deterministic non-TypeScript SDK from backend.ts.
107
- nifra assure [--config <file>] [--json] [--strict] [--out <file>] [--hydration] [--interact]
108
- Emit one structured assurance bundle. The bundle records every
109
- applicable gate as pass, fail, or skip and exits non-zero unless
110
- its verdict is green. Without these bundle flags, the legacy
111
- route-assurance report remains available.
113
+ nifra assure [--config <file>] [--json] Route-assurance report. Human table by default; --json emits
114
+ the {ok, routes, findings} report for agents.
115
+ nifra assure --bundle [--json] [--strict] [--out <file>] [--hydration] [--interact]
116
+ Emit one structured assurance {version, gates, verdict} bundle
117
+ (always JSON). Records every applicable gate as pass, fail, or
118
+ skip and exits non-zero unless its verdict is green. --strict,
119
+ --hydration, --interact and --out imply --bundle.
112
120
  nifra contracts snapshot [--out <file>] Write the deterministic route contract lock.
113
121
  nifra contracts check [--json] Check the current routes against contracts.lock.json.
114
122
  nifra replay <file> Replay a token-only verification metadata file.
@@ -118,6 +126,8 @@ Usage:
118
126
  nifra capabilities check [--lockfile <file>] [--config <file>] [--json]
119
127
  CI gate: fail on raw effect-import bypasses, declaration/evidence
120
128
  drift, unsafe GET/HEAD writes, idempotency gaps, or lockfile drift.
129
+ nifra capabilities explain <METHOD> <path> [--config <file>] [--json]
130
+ Explain one route's declared capability tokens and static provenance.
121
131
  nifra manifest emit [--out <file>] [--config <file>] [--sign <key-ref>]
122
132
  Emit a deterministic route trust artifact after assurance passes;
123
133
  optionally write an Ed25519 signature via the configured KMS signer.
@@ -155,9 +165,11 @@ Reads nifra.config.ts (adapter + clientModule + plugins; or framework.ts), backe
155
165
  routes/ from the current directory. Run from your project root.
156
166
 
157
167
  Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with \`--port <n>\` (alias \`-p\`) or the
158
- \`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).`;
168
+ \`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).
169
+
170
+ ${renderCommandCatalogHelp()}`;
159
171
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
160
- const CLI_VERSION = "2.12.1";
172
+ const CLI_VERSION = "2.14.1";
161
173
  // A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
162
174
  const asAdapter = (v) => v;
163
175
  const asBunPlugins = (v) => v;
@@ -352,6 +364,7 @@ async function dev(app, flags) {
352
364
  ...(fw.publicEnvPrefix !== undefined ? { publicEnvPrefix: fw.publicEnvPrefix } : {}),
353
365
  poll: flags.poll,
354
366
  port: flags.port,
367
+ ...(flags.allowDuplicateIdentity ? { allowDuplicateIdentity: true } : {}),
355
368
  ...(fw.conditions ? { conditions: fw.conditions } : {}),
356
369
  ...(fw.define ? { define: fw.define } : {}),
357
370
  // `load` resolves route modules through VITE, not through Bun. That is what makes the Vite
@@ -401,6 +414,34 @@ export async function assertUseIsEdgeExported(use, configPath, frameworkFile) {
401
414
  " - Define `use` in framework.ts and re-export it from nifra.config.ts " +
402
415
  '(`export { use } from "./framework.ts"`) so `nifra dev` sees it too.');
403
416
  }
417
+ /**
418
+ * A production build must not ship a workspace-linked package whose `default`-condition artifact is
419
+ * missing or older than its source. The `"bun": "./src"` / `"default": "./dist"` split means Bun (this
420
+ * build, the tests) reads live source while the deployed app and any node consumer read `dist/` - so a
421
+ * green `nifra build` can bundle stale or absent dist and the drift never shows in a diff. The doctor
422
+ * reports the same skew as an advisory during development; at build time it is a hard failure, because
423
+ * this is the artifact that ships. No-op outside a workspace: tarball-installed deps cannot drift.
424
+ */
425
+ async function assertFreshWorkspaceDists(cwd) {
426
+ const pkgPath = resolve(cwd, "package.json");
427
+ if (!existsSync(pkgPath))
428
+ return;
429
+ let rootPackage;
430
+ try {
431
+ rootPackage = JSON.parse(readFileSync(pkgPath, "utf8"));
432
+ }
433
+ catch {
434
+ return; // an unreadable/!JSON package.json is a separate problem; don't mask it as a stale-dist failure
435
+ }
436
+ const { collectStaleWorkspaceDists } = await import("./doctor.js");
437
+ const stale = await collectStaleWorkspaceDists(cwd, rootPackage);
438
+ if (stale.length === 0)
439
+ return;
440
+ const lines = stale.map((f) => f.missing
441
+ ? ` ${f.package}: ${f.distFile} was never built (source ${f.sourceFile})`
442
+ : ` ${f.package}: ${f.distFile} is ${f.behindSeconds}s behind ${f.sourceFile}`);
443
+ 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.`);
444
+ }
404
445
  async function buildForTarget(app, target, flags) {
405
446
  const { isBuildTarget, renderSizeReport, BUILD_TARGETS } = await import("@nifrajs/web/build");
406
447
  if (!isBuildTarget(target)) {
@@ -458,6 +499,11 @@ async function buildForTarget(app, target, flags) {
458
499
  // The static target needs a built app to drive prerendering - only build it when targeting static.
459
500
  ...(target === "static" ? { prerenderApp: await buildPrerenderApp(app) } : {}),
460
501
  });
502
+ // A production build must not certify itself green while a workspace-linked dependency ships a dist
503
+ // artifact older than (or absent next to) its source: the `bun`->src condition let this compile read
504
+ // live source, but the deployed app and every node consumer read `dist/`. Gate here, after the compile
505
+ // proved the source is buildable, so the failure names the package to rebuild rather than a bundle error.
506
+ await assertFreshWorkspaceDists(cwd);
461
507
  console.log(`nifra build (${target}, ${decision.pipeline}) → ${result.run}`);
462
508
  // The pipeline is stated on every build, not only the surprising ones - an auto-selected Vite build
463
509
  // must never look like the default, and a default Bun build must not leave the question open either.
@@ -513,6 +559,7 @@ export function parseFlags(args) {
513
559
  let report = false;
514
560
  let vite = false;
515
561
  let bun = false;
562
+ let allowDuplicateIdentity = false;
516
563
  for (let i = 0; i < args.length; i++) {
517
564
  const a = args[i];
518
565
  if ((a === "--port" || a === "-p") && args[i + 1])
@@ -529,11 +576,26 @@ export function parseFlags(args) {
529
576
  vite = true;
530
577
  else if (a === "--bun")
531
578
  bun = true;
579
+ else if (a === "--allow-duplicate-identity")
580
+ allowDuplicateIdentity = true;
532
581
  }
533
582
  if (!Number.isFinite(port) || port < 0 || port > 65535) {
534
583
  throw new Error(`[nifra] invalid --port: ${port}`);
535
584
  }
536
- return { port, out, poll, target, report, vite, bun };
585
+ return { port, out, poll, target, report, vite, bun, allowDuplicateIdentity };
586
+ }
587
+ /**
588
+ * `assure` uses the structured-bundle lane only when explicitly asked. A bare `--json` is NOT a
589
+ * request for the bundle - it keeps the legacy `{ ok, routes, findings }` report machine-readable, so a
590
+ * pre-bundle consumer is never silently handed the `{ gates, verdict }` shape. `--bundle` (or a
591
+ * bundle-only flag) opts in; `--bundle --json` still yields the bundle, which is always JSON.
592
+ */
593
+ export function assureBundleRequested(argv) {
594
+ return (argv.includes("--bundle") ||
595
+ argv.includes("--strict") ||
596
+ argv.includes("--hydration") ||
597
+ argv.includes("--interact") ||
598
+ argv.includes("--out"));
537
599
  }
538
600
  async function main() {
539
601
  const argv = Bun.argv.slice(2);
@@ -577,34 +639,21 @@ async function main() {
577
639
  console.log(renderLearnResult(stepArg !== undefined && /^\d+$/.test(stepArg) ? Number(stepArg) : undefined));
578
640
  return;
579
641
  }
580
- // `check` is a pure cwd-based gate (typecheck + lint) - it must run even when the project doesn't
581
- // load (API-only, not built yet), so it dispatches before the eager `loadApp` below.
582
- if (command === "check") {
583
- const { runCheck } = await import("./check.js");
584
- if (!(await runCheck(process.cwd(), {
585
- json: argv.includes("--json"),
586
- structured: argv.includes("--json"),
587
- lintsOnly: argv.includes("--lints-only"),
588
- })))
589
- process.exitCode = 1;
590
- return;
591
- }
592
- if (command === "fix") {
593
- const codeIndex = argv.indexOf("--code");
594
- const code = codeIndex === -1 ? undefined : argv[codeIndex + 1];
642
+ const catalogSpec = command === undefined ? undefined : findCommandSpec(command);
643
+ if (catalogSpec?.transports.includes("cli")) {
595
644
  try {
596
- const { collectCheckResult } = await import("./check.js");
597
- const { applyDiagnosticRecipe } = await import("./fix-recipes.js");
598
- const result = await collectCheckResult(process.cwd(), { lintsOnly: true });
599
- const changed = [];
600
- for (const diagnostic of result.structuredDiagnostics ?? []) {
601
- if (code !== undefined && diagnostic.code !== code)
602
- continue;
603
- changed.push(...(await applyDiagnosticRecipe(process.cwd(), diagnostic)));
645
+ const input = bindCommandArgv(catalogSpec, argv.slice(1));
646
+ const output = await catalogSpec.run(input, { cwd: process.cwd(), cliVersion: CLI_VERSION });
647
+ const asRecord = output;
648
+ const wantsJson = asRecord.json === true || argv.includes("--json");
649
+ if (wantsJson) {
650
+ const value = catalogSpec.json?.(output, input) ?? output;
651
+ console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2));
604
652
  }
605
- const final = await collectCheckResult(process.cwd(), { lintsOnly: true });
606
- console.log(JSON.stringify({ changed, diagnostics: final.structuredDiagnostics ?? [] }, null, 2));
607
- if (final.ok === false)
653
+ else {
654
+ console.log(catalogSpec.render(output, input).join("\n"));
655
+ }
656
+ if (catalogSpec.success !== undefined && !catalogSpec.success(output, input))
608
657
  process.exitCode = 1;
609
658
  }
610
659
  catch (err) {
@@ -613,6 +662,16 @@ async function main() {
613
662
  }
614
663
  return;
615
664
  }
665
+ if (command === "verify") {
666
+ const { runReleaseVerification } = await import("./release-verification.js");
667
+ const ok = await runReleaseVerification(process.cwd(), {
668
+ mode: argv.includes("--release") ? "release" : "default",
669
+ json: argv.includes("--json"),
670
+ });
671
+ if (!ok)
672
+ process.exitCode = 1;
673
+ return;
674
+ }
616
675
  // `init-agents` retrofits the agent-discovery files (.mcp.json, CLAUDE.md, …) into the cwd. It's a
617
676
  // pure file-writing command independent of the app loading, so dispatch it before the eager `loadApp`
618
677
  // (an existing app might be API-only or not yet built). It always succeeds unless a write throws.
@@ -624,77 +683,6 @@ async function main() {
624
683
  });
625
684
  return;
626
685
  }
627
- // `sync-manifest` rewrites a committed web `server-manifest.ts` from the current `routes/` tree, without
628
- // a full build. Pure fs/text (re-scan routes, preserve baked client assets), so - like `check` - dispatch
629
- // it before the eager `loadApp`.
630
- if (command === "sync-manifest") {
631
- const { runSyncManifest } = await import("./sync-manifest.js");
632
- if (!(await runSyncManifest(process.cwd())))
633
- process.exitCode = 1;
634
- return;
635
- }
636
- // `sync-routes` regenerates the route-search types `.d.ts` (typed `navigate({ to, search })`) from
637
- // `routes/`. Pure fs/text (needs only the route tree, not `framework.ts`), so dispatch before `loadApp`.
638
- if (command === "sync-routes") {
639
- const { runSyncRoutes } = await import("./sync-routes.js");
640
- if (!(await runSyncRoutes(process.cwd())))
641
- process.exitCode = 1;
642
- return;
643
- }
644
- // `doctor` is a pure cwd check (imports vs declared deps) - like `check`, dispatch before `loadApp`
645
- // so it runs on an API-only / not-yet-built project.
646
- if (command === "doctor") {
647
- const { runDoctor } = await import("./doctor.js");
648
- const targetIdx = argv.indexOf("--target");
649
- const target = targetIdx !== -1 ? argv[targetIdx + 1] : undefined;
650
- if (targetIdx !== -1 && (target === undefined || target.startsWith("-"))) {
651
- console.error("[nifra] --target needs a value: bun | node | deno | cf-pages | vercel");
652
- process.exitCode = 1;
653
- return;
654
- }
655
- if (!(await runDoctor(process.cwd(), {
656
- json: argv.includes("--json"),
657
- autoFix: argv.includes("--auto-fix") || argv.includes("--fix"),
658
- strict: argv.includes("--strict"),
659
- ...(target === undefined ? {} : { target }),
660
- })))
661
- process.exitCode = 1;
662
- return;
663
- }
664
- // `snapshot` / `diff` load ONLY backend.ts (the API contract) - like `check`, they must run on an
665
- // API-only project, so they dispatch before the eager `loadApp`.
666
- if (command === "snapshot") {
667
- const { runSnapshot } = await import("./diff-tool.js");
668
- const outIdx = argv.indexOf("--out");
669
- const out = outIdx !== -1 ? argv[outIdx + 1] : undefined;
670
- if (outIdx !== -1 && (out === undefined || out.startsWith("-"))) {
671
- console.error("[nifra] --out needs a file path");
672
- process.exitCode = 1;
673
- return;
674
- }
675
- try {
676
- await runSnapshot(process.cwd(), out !== undefined ? { out } : {});
677
- }
678
- catch (err) {
679
- console.error(formatCliError(err));
680
- process.exitCode = 1;
681
- }
682
- return;
683
- }
684
- if (command === "diff") {
685
- const { runDiff, DEFAULT_SNAPSHOT_FILE } = await import("./diff-tool.js");
686
- const baseline = argv.slice(1).find((arg) => !arg.startsWith("-")) ?? DEFAULT_SNAPSHOT_FILE;
687
- try {
688
- if (!(await runDiff(process.cwd(), baseline, { json: argv.includes("--json") }))) {
689
- process.exitCode = 1;
690
- }
691
- }
692
- catch (err) {
693
- console.error(formatCliError(err));
694
- process.exitCode = 1;
695
- }
696
- return;
697
- }
698
686
  if (command === "sdk") {
699
687
  const languageIndex = argv.indexOf("--lang");
700
688
  const language = languageIndex === -1 ? undefined : argv[languageIndex + 1];
@@ -720,241 +708,6 @@ async function main() {
720
708
  }
721
709
  return;
722
710
  }
723
- // Route assurance intentionally loads only its explicit config (which imports the backend it audits),
724
- // not the full SSR app. Like snapshot/diff, this works for API-only projects.
725
- if (command === "assure") {
726
- const { runAssurance } = await import("./assure.js");
727
- const configIdx = argv.indexOf("--config");
728
- const config = configIdx !== -1 ? argv[configIdx + 1] : undefined;
729
- if (configIdx !== -1 && (config === undefined || config.startsWith("-"))) {
730
- console.error("[nifra] --config needs a file path");
731
- process.exitCode = 1;
732
- return;
733
- }
734
- try {
735
- if (!(await runAssurance(process.cwd(), {
736
- json: argv.includes("--json"),
737
- bundle: argv.includes("--json") ||
738
- argv.includes("--strict") ||
739
- argv.includes("--hydration") ||
740
- argv.includes("--interact") ||
741
- argv.includes("--out"),
742
- strict: argv.includes("--strict"),
743
- hydration: argv.includes("--hydration"),
744
- interact: argv.includes("--interact"),
745
- ...(argv.includes("--out") ? { out: argv[argv.indexOf("--out") + 1] } : {}),
746
- ...(config !== undefined ? { config } : {}),
747
- })))
748
- process.exitCode = 1;
749
- }
750
- catch (err) {
751
- console.error(formatCliError(err));
752
- process.exitCode = 1;
753
- }
754
- return;
755
- }
756
- if (command === "contracts") {
757
- const action = argv[1];
758
- const outIndex = argv.indexOf("--out");
759
- const out = outIndex === -1 ? undefined : argv[outIndex + 1];
760
- try {
761
- const { checkContractsLock, snapshotContracts } = await import("./contracts.js");
762
- if (action === "snapshot") {
763
- const lock = await snapshotContracts(process.cwd(), out);
764
- if (argv.includes("--json"))
765
- console.log(JSON.stringify(lock, null, 2));
766
- else
767
- console.log(`[nifra] wrote ${Object.keys(lock.routes).length} contract digests`);
768
- }
769
- else if (action === "check") {
770
- const result = await checkContractsLock(process.cwd());
771
- const output = { ok: result.present && result.diagnostics.length === 0, ...result };
772
- console.log(JSON.stringify(output, null, 2));
773
- if (!output.ok)
774
- process.exitCode = 1;
775
- }
776
- else {
777
- console.error("[nifra] contracts needs snapshot or check");
778
- process.exitCode = 1;
779
- }
780
- }
781
- catch (err) {
782
- console.error(formatCliError(err));
783
- process.exitCode = 1;
784
- }
785
- return;
786
- }
787
- if (command === "replay") {
788
- const file = argv[1];
789
- if (file === undefined || file.startsWith("-")) {
790
- console.error("[nifra] replay needs a file path");
791
- process.exitCode = 1;
792
- return;
793
- }
794
- try {
795
- const { runReplay } = await import("./replay.js");
796
- const result = await runReplay(process.cwd(), file);
797
- console.log(JSON.stringify(result, null, 2));
798
- if (!result.ok)
799
- process.exitCode = 1;
800
- }
801
- catch (err) {
802
- console.error(formatCliError(err));
803
- process.exitCode = 1;
804
- }
805
- return;
806
- }
807
- if (command === "capabilities") {
808
- const action = argv[1];
809
- const valueAfter = (flag) => {
810
- const index = argv.indexOf(flag);
811
- const value = index === -1 ? undefined : argv[index + 1];
812
- if (index !== -1 && (value === undefined || value.startsWith("-"))) {
813
- throw new Error(`[nifra] ${flag} needs a file path`);
814
- }
815
- return value;
816
- };
817
- try {
818
- const { runCapabilityCheck, runCapabilitySnapshot } = await import("./capabilities-tool.js");
819
- const config = valueAfter("--config");
820
- if (action === "snapshot") {
821
- const out = valueAfter("--out");
822
- if (!(await runCapabilitySnapshot(process.cwd(), {
823
- ...(config !== undefined ? { config } : {}),
824
- ...(out !== undefined ? { out } : {}),
825
- })))
826
- process.exitCode = 1;
827
- }
828
- else if (action === "check") {
829
- const lockfile = valueAfter("--lockfile");
830
- if (!(await runCapabilityCheck(process.cwd(), {
831
- ...(config !== undefined ? { config } : {}),
832
- ...(lockfile !== undefined ? { lockfile } : {}),
833
- json: argv.includes("--json"),
834
- })))
835
- process.exitCode = 1;
836
- }
837
- else {
838
- throw new Error("[nifra] capabilities needs `snapshot` or `check`");
839
- }
840
- }
841
- catch (err) {
842
- console.error(formatCliError(err));
843
- process.exitCode = 1;
844
- }
845
- return;
846
- }
847
- if (command === "manifest") {
848
- const action = argv[1];
849
- const valueAfter = (flag) => {
850
- const index = argv.indexOf(flag);
851
- const value = index === -1 ? undefined : argv[index + 1];
852
- if (index !== -1 && (value === undefined || value.startsWith("-"))) {
853
- throw new Error(`[nifra] ${flag} needs a value`);
854
- }
855
- return value;
856
- };
857
- try {
858
- const { runManifestDiff, runManifestEmit } = await import("./manifest-tool.js");
859
- if (action === "emit") {
860
- const config = valueAfter("--config");
861
- const out = valueAfter("--out");
862
- const sign = valueAfter("--sign");
863
- if (!(await runManifestEmit(process.cwd(), {
864
- ...(config !== undefined ? { config } : {}),
865
- ...(out !== undefined ? { out } : {}),
866
- ...(sign !== undefined ? { sign } : {}),
867
- })))
868
- process.exitCode = 1;
869
- }
870
- else if (action === "diff") {
871
- const paths = argv.slice(2).filter((arg) => !arg.startsWith("-"));
872
- if (paths.length !== 2)
873
- throw new Error("[nifra] manifest diff needs <before> <after>");
874
- if (!(await runManifestDiff(process.cwd(), paths[0], paths[1], {
875
- json: argv.includes("--json"),
876
- })))
877
- process.exitCode = 1;
878
- }
879
- else {
880
- throw new Error("[nifra] manifest needs `emit` or `diff`");
881
- }
882
- }
883
- catch (err) {
884
- console.error(formatCliError(err));
885
- process.exitCode = 1;
886
- }
887
- return;
888
- }
889
- if (command === "levels") {
890
- const valueAfter = (flag) => {
891
- const index = argv.indexOf(flag);
892
- const value = index === -1 ? undefined : argv[index + 1];
893
- if (index !== -1 && (value === undefined || value.startsWith("-"))) {
894
- throw new Error(`[nifra] ${flag} needs a value`);
895
- }
896
- return value;
897
- };
898
- try {
899
- const { runLevels } = await import("./levels-tool.js");
900
- const config = valueAfter("--config");
901
- const minRaw = valueAfter("--min");
902
- const min = minRaw === undefined ? undefined : Number(minRaw);
903
- if (min !== undefined && (!Number.isInteger(min) || min < 0 || min > 4)) {
904
- throw new Error("[nifra] --min must be an integer level between 0 and 4");
905
- }
906
- if (!(await runLevels(process.cwd(), {
907
- json: argv.includes("--json"),
908
- ...(config !== undefined ? { config } : {}),
909
- ...(min !== undefined ? { min } : {}),
910
- })))
911
- process.exitCode = 1;
912
- }
913
- catch (err) {
914
- console.error(formatCliError(err));
915
- process.exitCode = 1;
916
- }
917
- return;
918
- }
919
- if (command === "prove") {
920
- const valueAfter = (flag) => {
921
- const index = argv.indexOf(flag);
922
- const value = index === -1 ? undefined : argv[index + 1];
923
- if (index !== -1 && (value === undefined || value.startsWith("-"))) {
924
- throw new Error(`[nifra] ${flag} needs a value`);
925
- }
926
- return value;
927
- };
928
- try {
929
- const minRaw = valueAfter("--min");
930
- const min = minRaw === undefined ? undefined : Number(minRaw);
931
- if (min !== undefined && (!Number.isInteger(min) || min < 0 || min > 4)) {
932
- throw new Error("[nifra] --min must be an integer level between 0 and 4");
933
- }
934
- const changedFiles = [];
935
- for (let index = 0; index < argv.length; index++) {
936
- if (argv[index] !== "--file")
937
- continue;
938
- const file = argv[index + 1];
939
- if (file === undefined || file.startsWith("-"))
940
- throw new Error("[nifra] --file needs a path");
941
- changedFiles.push(file);
942
- index++;
943
- }
944
- const { runWorkGraph } = await import("./work-graph.js");
945
- if (!(await runWorkGraph(process.cwd(), {
946
- json: argv.includes("--json"),
947
- ...(changedFiles.length === 0 ? {} : { changedFiles }),
948
- ...(min === undefined ? {} : { minLevel: min }),
949
- })))
950
- process.exitCode = 1;
951
- }
952
- catch (err) {
953
- console.error(formatCliError(err));
954
- process.exitCode = 1;
955
- }
956
- return;
957
- }
958
711
  // `upgrade` is a pure cwd file-transformer (package.json pins + import moves) driven by a per-release
959
712
  // recipe, then verified with `nifra check`. Dispatch before the eager `loadApp` - it must run on any
960
713
  // repo (API-only, not built, or mid-upgrade with edits that don't yet typecheck under dry-run).
@@ -981,36 +734,7 @@ async function main() {
981
734
  }
982
735
  // `port` is a pure cwd-based portability linter (scans source, doesn't run the app) - like `check`/
983
736
  // `doctor`, dispatch before the eager `loadApp` so it runs on an API-only / not-yet-built project.
984
- if (command === "port") {
985
- const { runPort } = await import("./port.js");
986
- const targetIdx = argv.indexOf("--target");
987
- const target = targetIdx !== -1 ? argv[targetIdx + 1] : undefined;
988
- if (targetIdx !== -1 && (target === undefined || target.startsWith("-"))) {
989
- console.error("[nifra] --target needs a value: bun | node | deno | cf-pages | vercel");
990
- process.exitCode = 1;
991
- return;
992
- }
993
- try {
994
- const passed = await runPort(process.cwd(), {
995
- ...(target !== undefined ? { target } : {}),
996
- json: argv.includes("--json"),
997
- ci: argv.includes("--ci"),
998
- strict: argv.includes("--strict"),
999
- });
1000
- if (!passed)
1001
- process.exitCode = 1;
1002
- }
1003
- catch (err) {
1004
- console.error(formatCliError(err));
1005
- process.exitCode = 1;
1006
- }
1007
- return;
1008
- }
1009
- if (command !== "dev" &&
1010
- command !== "build" &&
1011
- command !== "start" &&
1012
- command !== "context" &&
1013
- command !== "routes") {
737
+ if (command !== "dev" && command !== "build" && command !== "start") {
1014
738
  console.error(`[nifra] unknown command: ${command}\n`);
1015
739
  console.error(HELP);
1016
740
  process.exitCode = 1;
@@ -1022,40 +746,6 @@ async function main() {
1022
746
  await dev(app, flags);
1023
747
  else if (command === "build")
1024
748
  await buildForTarget(app, flags.target, flags);
1025
- else if (command === "context")
1026
- console.log(describeProject(app));
1027
- else if (command === "routes") {
1028
- if (argv.includes("--graph")) {
1029
- console.log(await describeRouteGraph(app, { json: argv.includes("--json") }));
1030
- }
1031
- else if (argv.includes("--modes")) {
1032
- // `--modes` answers a different question than the default table: not "what URLs exist" but "how
1033
- // does each render, and can the target I'm about to deploy to actually honour that". Resolves
1034
- // against `--target` when given, so a `static` build's server-needing route fails here, in one
1035
- // place, instead of 404ing in production.
1036
- const { buildRouteManifest, renderRouteManifest } = await import("@nifrajs/web/route-manifest");
1037
- const manifest = discoverRoutes(app.routesDir);
1038
- // `flags.target` defaults to "bun" for `build`; here an EXPLICIT `--target` is what opts into
1039
- // gating, so read it from argv rather than inheriting the build default (which would silently gate
1040
- // every `nifra routes --modes` against bun).
1041
- const targetIdx = argv.indexOf("--target");
1042
- const target = targetIdx !== -1 ? argv[targetIdx + 1] : undefined;
1043
- const routeManifest = await buildRouteManifest(manifest, {
1044
- ...(target !== undefined ? { target } : {}),
1045
- });
1046
- if (argv.includes("--json"))
1047
- console.log(JSON.stringify(routeManifest, null, 2));
1048
- else
1049
- console.log(renderRouteManifest(routeManifest));
1050
- // A target that cannot honour a route is a build-breaking fact, not a note: exit non-zero so the
1051
- // same command in CI gates the deploy it is warning about.
1052
- if (routeManifest.conflicts.length > 0)
1053
- process.exitCode = 1;
1054
- }
1055
- else {
1056
- console.log(await describeRoutes(app, { json: argv.includes("--json") }));
1057
- }
1058
- }
1059
749
  else
1060
750
  await start(app, flags);
1061
751
  }