@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.
- package/dist/capabilities-tool.d.ts +54 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +146 -18
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check.d.ts +11 -11
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +234 -43
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +91 -413
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +244 -0
- package/dist/command-catalog.d.ts.map +1 -0
- package/dist/command-catalog.js +1014 -0
- package/dist/command-catalog.js.map +1 -0
- package/dist/contracts.d.ts +9 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +22 -6
- package/dist/contracts.js.map +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +18 -2
- package/dist/doctor.js.map +1 -1
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +22 -5
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts +15 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +23 -9
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp.d.ts +8 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +59 -349
- package/dist/mcp.js.map +1 -1
- package/dist/project-facts.d.ts +40 -0
- package/dist/project-facts.d.ts.map +1 -0
- package/dist/project-facts.js +36 -0
- package/dist/project-facts.js.map +1 -0
- package/dist/rules/comment-markers.d.ts +8 -0
- package/dist/rules/comment-markers.d.ts.map +1 -1
- package/dist/rules/comment-markers.js +36 -0
- package/dist/rules/comment-markers.js.map +1 -1
- package/dist/rules/index.d.ts +2 -3
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +12 -14
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/security.js +14 -14
- package/dist/rules/security.js.map +1 -1
- package/docs/types.json +27 -14
- package/package.json +9 -9
- package/src/capabilities-tool.ts +229 -21
- package/src/check.ts +285 -59
- package/src/cli.ts +100 -404
- package/src/command-catalog.ts +1415 -0
- package/src/contracts.ts +28 -7
- package/src/doctor.ts +35 -4
- package/src/load.ts +25 -6
- package/src/manifest-tool.ts +43 -12
- package/src/mcp-http.ts +1 -1
- package/src/mcp.ts +82 -414
- package/src/project-facts.ts +86 -0
- package/src/rules/comment-markers.ts +36 -0
- package/src/rules/index.ts +3 -4
- package/src/rules/routes.ts +13 -14
- package/src/rules/security.ts +14 -14
package/src/cli.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Bun-only (it runs the framework's TS + Bun plugins directly). The *output* runs anywhere.
|
|
11
11
|
*/
|
|
12
|
-
import { existsSync } from "node:fs"
|
|
12
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
13
13
|
import { resolve } from "node:path"
|
|
14
14
|
import { inProcessClient } from "@nifrajs/client"
|
|
15
15
|
import {
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from "@nifrajs/web"
|
|
21
21
|
import { discoverRoutes } from "@nifrajs/web/fs"
|
|
22
22
|
import type { BunPlugin } from "bun"
|
|
23
|
-
import {
|
|
23
|
+
import { bindCommandArgv, findCommandSpec, renderCommandCatalogHelp } from "./command-catalog.ts"
|
|
24
24
|
import { type LoadedApp, loadApp, type NifraFramework } from "./load.ts"
|
|
25
25
|
import { chooseBuildPipeline, describePipeline } from "./pipeline-guard.ts"
|
|
26
26
|
|
|
@@ -40,6 +40,10 @@ export interface Flags {
|
|
|
40
40
|
*
|
|
41
41
|
* `nifra build --bun`: force the Bun build. Refuses when that would drop the app's `vitePlugins`. */
|
|
42
42
|
readonly bun: boolean
|
|
43
|
+
/** `nifra dev --allow-duplicate-identity`: downgrade the Vite dev server's identity-parity check from
|
|
44
|
+
* a hard failure to a loud warning so a duplicate coming from a linked sibling repo doesn't take dev
|
|
45
|
+
* down while you fix the resolution. Dev only - `nifra build` always fails hard on a duplicate. */
|
|
46
|
+
readonly allowDuplicateIdentity: boolean
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
const HELP = `nifra - zero-config dev/build/start for a nifra app
|
|
@@ -53,6 +57,10 @@ Usage:
|
|
|
53
57
|
preserved), no Vite in the process. CSS Modules, plain CSS,
|
|
54
58
|
Tailwind and the app's own \`clientPlugins\`/\`serverPlugins\` run
|
|
55
59
|
through the same Bun transforms production uses.
|
|
60
|
+
[--allow-duplicate-identity] Downgrade the Vite dev server's identity-parity check to a
|
|
61
|
+
loud warning instead of a hard failure, so a duplicate React/
|
|
62
|
+
adapter copy from a linked sibling repo doesn't take dev down
|
|
63
|
+
while you fix the resolution. Dev only - \`nifra build\` still fails.
|
|
56
64
|
nifra build [--out <dir>] [--report] Emit a complete deploy directory.
|
|
57
65
|
[--target <t>] Target a FULL deploy dir for <t>:
|
|
58
66
|
bun | node | deno | cf-pages | vercel | static. Packages
|
|
@@ -123,11 +131,13 @@ Usage:
|
|
|
123
131
|
closed. Exits non-zero on any breaking change - run it in CI.
|
|
124
132
|
nifra sdk --lang <python|go> [--out <file>]
|
|
125
133
|
Generate a deterministic non-TypeScript SDK from backend.ts.
|
|
126
|
-
nifra assure [--config <file>] [--json]
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
134
|
+
nifra assure [--config <file>] [--json] Route-assurance report. Human table by default; --json emits
|
|
135
|
+
the {ok, routes, findings} report for agents.
|
|
136
|
+
nifra assure --bundle [--json] [--strict] [--out <file>] [--hydration] [--interact]
|
|
137
|
+
Emit one structured assurance {version, gates, verdict} bundle
|
|
138
|
+
(always JSON). Records every applicable gate as pass, fail, or
|
|
139
|
+
skip and exits non-zero unless its verdict is green. --strict,
|
|
140
|
+
--hydration, --interact and --out imply --bundle.
|
|
131
141
|
nifra contracts snapshot [--out <file>] Write the deterministic route contract lock.
|
|
132
142
|
nifra contracts check [--json] Check the current routes against contracts.lock.json.
|
|
133
143
|
nifra replay <file> Replay a token-only verification metadata file.
|
|
@@ -137,6 +147,8 @@ Usage:
|
|
|
137
147
|
nifra capabilities check [--lockfile <file>] [--config <file>] [--json]
|
|
138
148
|
CI gate: fail on raw effect-import bypasses, declaration/evidence
|
|
139
149
|
drift, unsafe GET/HEAD writes, idempotency gaps, or lockfile drift.
|
|
150
|
+
nifra capabilities explain <METHOD> <path> [--config <file>] [--json]
|
|
151
|
+
Explain one route's declared capability tokens and static provenance.
|
|
140
152
|
nifra manifest emit [--out <file>] [--config <file>] [--sign <key-ref>]
|
|
141
153
|
Emit a deterministic route trust artifact after assurance passes;
|
|
142
154
|
optionally write an Ed25519 signature via the configured KMS signer.
|
|
@@ -174,10 +186,12 @@ Reads nifra.config.ts (adapter + clientModule + plugins; or framework.ts), backe
|
|
|
174
186
|
routes/ from the current directory. Run from your project root.
|
|
175
187
|
|
|
176
188
|
Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with \`--port <n>\` (alias \`-p\`) or the
|
|
177
|
-
\`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default)
|
|
189
|
+
\`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).
|
|
190
|
+
|
|
191
|
+
${renderCommandCatalogHelp()}`
|
|
178
192
|
|
|
179
193
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
180
|
-
const CLI_VERSION = "2.
|
|
194
|
+
const CLI_VERSION = "2.14.1"
|
|
181
195
|
|
|
182
196
|
// A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
|
|
183
197
|
const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
|
|
@@ -389,6 +403,7 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
389
403
|
...(fw.publicEnvPrefix !== undefined ? { publicEnvPrefix: fw.publicEnvPrefix } : {}),
|
|
390
404
|
poll: flags.poll,
|
|
391
405
|
port: flags.port,
|
|
406
|
+
...(flags.allowDuplicateIdentity ? { allowDuplicateIdentity: true } : {}),
|
|
392
407
|
...(fw.conditions ? { conditions: fw.conditions } : {}),
|
|
393
408
|
...(fw.define ? { define: fw.define } : {}),
|
|
394
409
|
// `load` resolves route modules through VITE, not through Bun. That is what makes the Vite
|
|
@@ -445,6 +460,38 @@ export async function assertUseIsEdgeExported(
|
|
|
445
460
|
)
|
|
446
461
|
}
|
|
447
462
|
|
|
463
|
+
/**
|
|
464
|
+
* A production build must not ship a workspace-linked package whose `default`-condition artifact is
|
|
465
|
+
* missing or older than its source. The `"bun": "./src"` / `"default": "./dist"` split means Bun (this
|
|
466
|
+
* build, the tests) reads live source while the deployed app and any node consumer read `dist/` - so a
|
|
467
|
+
* green `nifra build` can bundle stale or absent dist and the drift never shows in a diff. The doctor
|
|
468
|
+
* reports the same skew as an advisory during development; at build time it is a hard failure, because
|
|
469
|
+
* this is the artifact that ships. No-op outside a workspace: tarball-installed deps cannot drift.
|
|
470
|
+
*/
|
|
471
|
+
async function assertFreshWorkspaceDists(cwd: string): Promise<void> {
|
|
472
|
+
const pkgPath = resolve(cwd, "package.json")
|
|
473
|
+
if (!existsSync(pkgPath)) return
|
|
474
|
+
let rootPackage: Record<string, unknown>
|
|
475
|
+
try {
|
|
476
|
+
rootPackage = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>
|
|
477
|
+
} catch {
|
|
478
|
+
return // an unreadable/!JSON package.json is a separate problem; don't mask it as a stale-dist failure
|
|
479
|
+
}
|
|
480
|
+
const { collectStaleWorkspaceDists } = await import("./doctor.ts")
|
|
481
|
+
const stale = await collectStaleWorkspaceDists(cwd, rootPackage)
|
|
482
|
+
if (stale.length === 0) return
|
|
483
|
+
const lines = stale.map((f) =>
|
|
484
|
+
f.missing
|
|
485
|
+
? ` ${f.package}: ${f.distFile} was never built (source ${f.sourceFile})`
|
|
486
|
+
: ` ${f.package}: ${f.distFile} is ${f.behindSeconds}s behind ${f.sourceFile}`,
|
|
487
|
+
)
|
|
488
|
+
throw new Error(
|
|
489
|
+
`[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(
|
|
490
|
+
"\n",
|
|
491
|
+
)}\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.`,
|
|
492
|
+
)
|
|
493
|
+
}
|
|
494
|
+
|
|
448
495
|
async function buildForTarget(app: LoadedApp, target: string, flags: Flags): Promise<void> {
|
|
449
496
|
const { isBuildTarget, renderSizeReport, BUILD_TARGETS } = await import("@nifrajs/web/build")
|
|
450
497
|
if (!isBuildTarget(target)) {
|
|
@@ -505,6 +552,11 @@ async function buildForTarget(app: LoadedApp, target: string, flags: Flags): Pro
|
|
|
505
552
|
// The static target needs a built app to drive prerendering - only build it when targeting static.
|
|
506
553
|
...(target === "static" ? { prerenderApp: await buildPrerenderApp(app) } : {}),
|
|
507
554
|
})
|
|
555
|
+
// A production build must not certify itself green while a workspace-linked dependency ships a dist
|
|
556
|
+
// artifact older than (or absent next to) its source: the `bun`->src condition let this compile read
|
|
557
|
+
// live source, but the deployed app and every node consumer read `dist/`. Gate here, after the compile
|
|
558
|
+
// proved the source is buildable, so the failure names the package to rebuild rather than a bundle error.
|
|
559
|
+
await assertFreshWorkspaceDists(cwd)
|
|
508
560
|
console.log(`nifra build (${target}, ${decision.pipeline}) → ${result.run}`)
|
|
509
561
|
// The pipeline is stated on every build, not only the surprising ones - an auto-selected Vite build
|
|
510
562
|
// must never look like the default, and a default Bun build must not leave the question open either.
|
|
@@ -575,6 +627,7 @@ export function parseFlags(args: readonly string[]): Flags {
|
|
|
575
627
|
let report = false
|
|
576
628
|
let vite = false
|
|
577
629
|
let bun = false
|
|
630
|
+
let allowDuplicateIdentity = false
|
|
578
631
|
for (let i = 0; i < args.length; i++) {
|
|
579
632
|
const a = args[i]
|
|
580
633
|
if ((a === "--port" || a === "-p") && args[i + 1]) port = Number(args[++i])
|
|
@@ -584,11 +637,28 @@ export function parseFlags(args: readonly string[]): Flags {
|
|
|
584
637
|
else if (a === "--report") report = true
|
|
585
638
|
else if (a === "--vite") vite = true
|
|
586
639
|
else if (a === "--bun") bun = true
|
|
640
|
+
else if (a === "--allow-duplicate-identity") allowDuplicateIdentity = true
|
|
587
641
|
}
|
|
588
642
|
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
|
589
643
|
throw new Error(`[nifra] invalid --port: ${port}`)
|
|
590
644
|
}
|
|
591
|
-
return { port, out, poll, target, report, vite, bun }
|
|
645
|
+
return { port, out, poll, target, report, vite, bun, allowDuplicateIdentity }
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* `assure` uses the structured-bundle lane only when explicitly asked. A bare `--json` is NOT a
|
|
650
|
+
* request for the bundle - it keeps the legacy `{ ok, routes, findings }` report machine-readable, so a
|
|
651
|
+
* pre-bundle consumer is never silently handed the `{ gates, verdict }` shape. `--bundle` (or a
|
|
652
|
+
* bundle-only flag) opts in; `--bundle --json` still yields the bundle, which is always JSON.
|
|
653
|
+
*/
|
|
654
|
+
export function assureBundleRequested(argv: readonly string[]): boolean {
|
|
655
|
+
return (
|
|
656
|
+
argv.includes("--bundle") ||
|
|
657
|
+
argv.includes("--strict") ||
|
|
658
|
+
argv.includes("--hydration") ||
|
|
659
|
+
argv.includes("--interact") ||
|
|
660
|
+
argv.includes("--out")
|
|
661
|
+
)
|
|
592
662
|
}
|
|
593
663
|
|
|
594
664
|
async function main(): Promise<void> {
|
|
@@ -637,18 +707,25 @@ async function main(): Promise<void> {
|
|
|
637
707
|
)
|
|
638
708
|
return
|
|
639
709
|
}
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
710
|
+
const catalogSpec = command === undefined ? undefined : findCommandSpec(command)
|
|
711
|
+
if (catalogSpec?.transports.includes("cli")) {
|
|
712
|
+
try {
|
|
713
|
+
const input = bindCommandArgv(catalogSpec, argv.slice(1))
|
|
714
|
+
const output = await catalogSpec.run(input, { cwd: process.cwd(), cliVersion: CLI_VERSION })
|
|
715
|
+
const asRecord = output as unknown as Record<string, unknown>
|
|
716
|
+
const wantsJson = asRecord.json === true || argv.includes("--json")
|
|
717
|
+
if (wantsJson) {
|
|
718
|
+
const value = catalogSpec.json?.(output, input) ?? output
|
|
719
|
+
console.log(typeof value === "string" ? value : JSON.stringify(value, null, 2))
|
|
720
|
+
} else {
|
|
721
|
+
console.log(catalogSpec.render(output, input).join("\n"))
|
|
722
|
+
}
|
|
723
|
+
if (catalogSpec.success !== undefined && !catalogSpec.success(output, input))
|
|
724
|
+
process.exitCode = 1
|
|
725
|
+
} catch (err) {
|
|
726
|
+
console.error(formatCliError(err))
|
|
651
727
|
process.exitCode = 1
|
|
728
|
+
}
|
|
652
729
|
return
|
|
653
730
|
}
|
|
654
731
|
if (command === "verify") {
|
|
@@ -660,29 +737,6 @@ async function main(): Promise<void> {
|
|
|
660
737
|
if (!ok) process.exitCode = 1
|
|
661
738
|
return
|
|
662
739
|
}
|
|
663
|
-
if (command === "fix") {
|
|
664
|
-
const codeIndex = argv.indexOf("--code")
|
|
665
|
-
const code = codeIndex === -1 ? undefined : argv[codeIndex + 1]
|
|
666
|
-
try {
|
|
667
|
-
const { collectCheckResult } = await import("./check.ts")
|
|
668
|
-
const { applyDiagnosticRecipe } = await import("./fix-recipes.ts")
|
|
669
|
-
const result = await collectCheckResult(process.cwd(), { lintsOnly: true })
|
|
670
|
-
const changed: string[] = []
|
|
671
|
-
for (const diagnostic of result.structuredDiagnostics ?? []) {
|
|
672
|
-
if (code !== undefined && diagnostic.code !== code) continue
|
|
673
|
-
changed.push(...(await applyDiagnosticRecipe(process.cwd(), diagnostic)))
|
|
674
|
-
}
|
|
675
|
-
const final = await collectCheckResult(process.cwd(), { lintsOnly: true })
|
|
676
|
-
console.log(
|
|
677
|
-
JSON.stringify({ changed, diagnostics: final.structuredDiagnostics ?? [] }, null, 2),
|
|
678
|
-
)
|
|
679
|
-
if (final.ok === false) process.exitCode = 1
|
|
680
|
-
} catch (err) {
|
|
681
|
-
console.error(formatCliError(err))
|
|
682
|
-
process.exitCode = 1
|
|
683
|
-
}
|
|
684
|
-
return
|
|
685
|
-
}
|
|
686
740
|
// `init-agents` retrofits the agent-discovery files (.mcp.json, CLAUDE.md, …) into the cwd. It's a
|
|
687
741
|
// pure file-writing command independent of the app loading, so dispatch it before the eager `loadApp`
|
|
688
742
|
// (an existing app might be API-only or not yet built). It always succeeds unless a write throws.
|
|
@@ -694,75 +748,6 @@ async function main(): Promise<void> {
|
|
|
694
748
|
})
|
|
695
749
|
return
|
|
696
750
|
}
|
|
697
|
-
// `sync-manifest` rewrites a committed web `server-manifest.ts` from the current `routes/` tree, without
|
|
698
|
-
// a full build. Pure fs/text (re-scan routes, preserve baked client assets), so - like `check` - dispatch
|
|
699
|
-
// it before the eager `loadApp`.
|
|
700
|
-
if (command === "sync-manifest") {
|
|
701
|
-
const { runSyncManifest } = await import("./sync-manifest.ts")
|
|
702
|
-
if (!(await runSyncManifest(process.cwd()))) process.exitCode = 1
|
|
703
|
-
return
|
|
704
|
-
}
|
|
705
|
-
// `sync-routes` regenerates the route-search types `.d.ts` (typed `navigate({ to, search })`) from
|
|
706
|
-
// `routes/`. Pure fs/text (needs only the route tree, not `framework.ts`), so dispatch before `loadApp`.
|
|
707
|
-
if (command === "sync-routes") {
|
|
708
|
-
const { runSyncRoutes } = await import("./sync-routes.ts")
|
|
709
|
-
if (!(await runSyncRoutes(process.cwd()))) process.exitCode = 1
|
|
710
|
-
return
|
|
711
|
-
}
|
|
712
|
-
// `doctor` is a pure cwd check (imports vs declared deps) - like `check`, dispatch before `loadApp`
|
|
713
|
-
// so it runs on an API-only / not-yet-built project.
|
|
714
|
-
if (command === "doctor") {
|
|
715
|
-
const { runDoctor } = await import("./doctor.ts")
|
|
716
|
-
const targetIdx = argv.indexOf("--target")
|
|
717
|
-
const target = targetIdx !== -1 ? argv[targetIdx + 1] : undefined
|
|
718
|
-
if (targetIdx !== -1 && (target === undefined || target.startsWith("-"))) {
|
|
719
|
-
console.error("[nifra] --target needs a value: bun | node | deno | cf-pages | vercel")
|
|
720
|
-
process.exitCode = 1
|
|
721
|
-
return
|
|
722
|
-
}
|
|
723
|
-
if (
|
|
724
|
-
!(await runDoctor(process.cwd(), {
|
|
725
|
-
json: argv.includes("--json"),
|
|
726
|
-
autoFix: argv.includes("--auto-fix") || argv.includes("--fix"),
|
|
727
|
-
strict: argv.includes("--strict"),
|
|
728
|
-
...(target === undefined ? {} : { target }),
|
|
729
|
-
}))
|
|
730
|
-
)
|
|
731
|
-
process.exitCode = 1
|
|
732
|
-
return
|
|
733
|
-
}
|
|
734
|
-
// `snapshot` / `diff` load ONLY backend.ts (the API contract) - like `check`, they must run on an
|
|
735
|
-
// API-only project, so they dispatch before the eager `loadApp`.
|
|
736
|
-
if (command === "snapshot") {
|
|
737
|
-
const { runSnapshot } = await import("./diff-tool.ts")
|
|
738
|
-
const outIdx = argv.indexOf("--out")
|
|
739
|
-
const out = outIdx !== -1 ? argv[outIdx + 1] : undefined
|
|
740
|
-
if (outIdx !== -1 && (out === undefined || out.startsWith("-"))) {
|
|
741
|
-
console.error("[nifra] --out needs a file path")
|
|
742
|
-
process.exitCode = 1
|
|
743
|
-
return
|
|
744
|
-
}
|
|
745
|
-
try {
|
|
746
|
-
await runSnapshot(process.cwd(), out !== undefined ? { out } : {})
|
|
747
|
-
} catch (err) {
|
|
748
|
-
console.error(formatCliError(err))
|
|
749
|
-
process.exitCode = 1
|
|
750
|
-
}
|
|
751
|
-
return
|
|
752
|
-
}
|
|
753
|
-
if (command === "diff") {
|
|
754
|
-
const { runDiff, DEFAULT_SNAPSHOT_FILE } = await import("./diff-tool.ts")
|
|
755
|
-
const baseline = argv.slice(1).find((arg) => !arg.startsWith("-")) ?? DEFAULT_SNAPSHOT_FILE
|
|
756
|
-
try {
|
|
757
|
-
if (!(await runDiff(process.cwd(), baseline, { json: argv.includes("--json") }))) {
|
|
758
|
-
process.exitCode = 1
|
|
759
|
-
}
|
|
760
|
-
} catch (err) {
|
|
761
|
-
console.error(formatCliError(err))
|
|
762
|
-
process.exitCode = 1
|
|
763
|
-
}
|
|
764
|
-
return
|
|
765
|
-
}
|
|
766
751
|
if (command === "sdk") {
|
|
767
752
|
const languageIndex = argv.indexOf("--lang")
|
|
768
753
|
const language = languageIndex === -1 ? undefined : argv[languageIndex + 1]
|
|
@@ -787,237 +772,6 @@ async function main(): Promise<void> {
|
|
|
787
772
|
}
|
|
788
773
|
return
|
|
789
774
|
}
|
|
790
|
-
// Route assurance intentionally loads only its explicit config (which imports the backend it audits),
|
|
791
|
-
// not the full SSR app. Like snapshot/diff, this works for API-only projects.
|
|
792
|
-
if (command === "assure") {
|
|
793
|
-
const { runAssurance } = await import("./assure.ts")
|
|
794
|
-
const configIdx = argv.indexOf("--config")
|
|
795
|
-
const config = configIdx !== -1 ? argv[configIdx + 1] : undefined
|
|
796
|
-
if (configIdx !== -1 && (config === undefined || config.startsWith("-"))) {
|
|
797
|
-
console.error("[nifra] --config needs a file path")
|
|
798
|
-
process.exitCode = 1
|
|
799
|
-
return
|
|
800
|
-
}
|
|
801
|
-
try {
|
|
802
|
-
if (
|
|
803
|
-
!(await runAssurance(process.cwd(), {
|
|
804
|
-
json: argv.includes("--json"),
|
|
805
|
-
bundle:
|
|
806
|
-
argv.includes("--json") ||
|
|
807
|
-
argv.includes("--strict") ||
|
|
808
|
-
argv.includes("--hydration") ||
|
|
809
|
-
argv.includes("--interact") ||
|
|
810
|
-
argv.includes("--out"),
|
|
811
|
-
strict: argv.includes("--strict"),
|
|
812
|
-
hydration: argv.includes("--hydration"),
|
|
813
|
-
interact: argv.includes("--interact"),
|
|
814
|
-
...(argv.includes("--out") ? { out: argv[argv.indexOf("--out") + 1] } : {}),
|
|
815
|
-
...(config !== undefined ? { config } : {}),
|
|
816
|
-
}))
|
|
817
|
-
)
|
|
818
|
-
process.exitCode = 1
|
|
819
|
-
} catch (err) {
|
|
820
|
-
console.error(formatCliError(err))
|
|
821
|
-
process.exitCode = 1
|
|
822
|
-
}
|
|
823
|
-
return
|
|
824
|
-
}
|
|
825
|
-
if (command === "contracts") {
|
|
826
|
-
const action = argv[1]
|
|
827
|
-
const outIndex = argv.indexOf("--out")
|
|
828
|
-
const out = outIndex === -1 ? undefined : argv[outIndex + 1]
|
|
829
|
-
try {
|
|
830
|
-
const { checkContractsLock, snapshotContracts } = await import("./contracts.ts")
|
|
831
|
-
if (action === "snapshot") {
|
|
832
|
-
const lock = await snapshotContracts(process.cwd(), out)
|
|
833
|
-
if (argv.includes("--json")) console.log(JSON.stringify(lock, null, 2))
|
|
834
|
-
else console.log(`[nifra] wrote ${Object.keys(lock.routes).length} contract digests`)
|
|
835
|
-
} else if (action === "check") {
|
|
836
|
-
const result = await checkContractsLock(process.cwd())
|
|
837
|
-
const output = { ok: result.present && result.diagnostics.length === 0, ...result }
|
|
838
|
-
console.log(JSON.stringify(output, null, 2))
|
|
839
|
-
if (!output.ok) process.exitCode = 1
|
|
840
|
-
} else {
|
|
841
|
-
console.error("[nifra] contracts needs snapshot or check")
|
|
842
|
-
process.exitCode = 1
|
|
843
|
-
}
|
|
844
|
-
} catch (err) {
|
|
845
|
-
console.error(formatCliError(err))
|
|
846
|
-
process.exitCode = 1
|
|
847
|
-
}
|
|
848
|
-
return
|
|
849
|
-
}
|
|
850
|
-
if (command === "replay") {
|
|
851
|
-
const file = argv[1]
|
|
852
|
-
if (file === undefined || file.startsWith("-")) {
|
|
853
|
-
console.error("[nifra] replay needs a file path")
|
|
854
|
-
process.exitCode = 1
|
|
855
|
-
return
|
|
856
|
-
}
|
|
857
|
-
try {
|
|
858
|
-
const { runReplay } = await import("./replay.ts")
|
|
859
|
-
const result = await runReplay(process.cwd(), file)
|
|
860
|
-
console.log(JSON.stringify(result, null, 2))
|
|
861
|
-
if (!result.ok) process.exitCode = 1
|
|
862
|
-
} catch (err) {
|
|
863
|
-
console.error(formatCliError(err))
|
|
864
|
-
process.exitCode = 1
|
|
865
|
-
}
|
|
866
|
-
return
|
|
867
|
-
}
|
|
868
|
-
if (command === "capabilities") {
|
|
869
|
-
const action = argv[1]
|
|
870
|
-
const valueAfter = (flag: string): string | undefined => {
|
|
871
|
-
const index = argv.indexOf(flag)
|
|
872
|
-
const value = index === -1 ? undefined : argv[index + 1]
|
|
873
|
-
if (index !== -1 && (value === undefined || value.startsWith("-"))) {
|
|
874
|
-
throw new Error(`[nifra] ${flag} needs a file path`)
|
|
875
|
-
}
|
|
876
|
-
return value
|
|
877
|
-
}
|
|
878
|
-
try {
|
|
879
|
-
const { runCapabilityCheck, runCapabilitySnapshot } = await import("./capabilities-tool.ts")
|
|
880
|
-
const config = valueAfter("--config")
|
|
881
|
-
if (action === "snapshot") {
|
|
882
|
-
const out = valueAfter("--out")
|
|
883
|
-
if (
|
|
884
|
-
!(await runCapabilitySnapshot(process.cwd(), {
|
|
885
|
-
...(config !== undefined ? { config } : {}),
|
|
886
|
-
...(out !== undefined ? { out } : {}),
|
|
887
|
-
}))
|
|
888
|
-
)
|
|
889
|
-
process.exitCode = 1
|
|
890
|
-
} else if (action === "check") {
|
|
891
|
-
const lockfile = valueAfter("--lockfile")
|
|
892
|
-
if (
|
|
893
|
-
!(await runCapabilityCheck(process.cwd(), {
|
|
894
|
-
...(config !== undefined ? { config } : {}),
|
|
895
|
-
...(lockfile !== undefined ? { lockfile } : {}),
|
|
896
|
-
json: argv.includes("--json"),
|
|
897
|
-
}))
|
|
898
|
-
)
|
|
899
|
-
process.exitCode = 1
|
|
900
|
-
} else {
|
|
901
|
-
throw new Error("[nifra] capabilities needs `snapshot` or `check`")
|
|
902
|
-
}
|
|
903
|
-
} catch (err) {
|
|
904
|
-
console.error(formatCliError(err))
|
|
905
|
-
process.exitCode = 1
|
|
906
|
-
}
|
|
907
|
-
return
|
|
908
|
-
}
|
|
909
|
-
if (command === "manifest") {
|
|
910
|
-
const action = argv[1]
|
|
911
|
-
const valueAfter = (flag: string): string | undefined => {
|
|
912
|
-
const index = argv.indexOf(flag)
|
|
913
|
-
const value = index === -1 ? undefined : argv[index + 1]
|
|
914
|
-
if (index !== -1 && (value === undefined || value.startsWith("-"))) {
|
|
915
|
-
throw new Error(`[nifra] ${flag} needs a value`)
|
|
916
|
-
}
|
|
917
|
-
return value
|
|
918
|
-
}
|
|
919
|
-
try {
|
|
920
|
-
const { runManifestDiff, runManifestEmit } = await import("./manifest-tool.ts")
|
|
921
|
-
if (action === "emit") {
|
|
922
|
-
const config = valueAfter("--config")
|
|
923
|
-
const out = valueAfter("--out")
|
|
924
|
-
const sign = valueAfter("--sign")
|
|
925
|
-
if (
|
|
926
|
-
!(await runManifestEmit(process.cwd(), {
|
|
927
|
-
...(config !== undefined ? { config } : {}),
|
|
928
|
-
...(out !== undefined ? { out } : {}),
|
|
929
|
-
...(sign !== undefined ? { sign } : {}),
|
|
930
|
-
}))
|
|
931
|
-
)
|
|
932
|
-
process.exitCode = 1
|
|
933
|
-
} else if (action === "diff") {
|
|
934
|
-
const paths = argv.slice(2).filter((arg) => !arg.startsWith("-"))
|
|
935
|
-
if (paths.length !== 2) throw new Error("[nifra] manifest diff needs <before> <after>")
|
|
936
|
-
if (
|
|
937
|
-
!(await runManifestDiff(process.cwd(), paths[0]!, paths[1]!, {
|
|
938
|
-
json: argv.includes("--json"),
|
|
939
|
-
}))
|
|
940
|
-
)
|
|
941
|
-
process.exitCode = 1
|
|
942
|
-
} else {
|
|
943
|
-
throw new Error("[nifra] manifest needs `emit` or `diff`")
|
|
944
|
-
}
|
|
945
|
-
} catch (err) {
|
|
946
|
-
console.error(formatCliError(err))
|
|
947
|
-
process.exitCode = 1
|
|
948
|
-
}
|
|
949
|
-
return
|
|
950
|
-
}
|
|
951
|
-
if (command === "levels") {
|
|
952
|
-
const valueAfter = (flag: string): string | undefined => {
|
|
953
|
-
const index = argv.indexOf(flag)
|
|
954
|
-
const value = index === -1 ? undefined : argv[index + 1]
|
|
955
|
-
if (index !== -1 && (value === undefined || value.startsWith("-"))) {
|
|
956
|
-
throw new Error(`[nifra] ${flag} needs a value`)
|
|
957
|
-
}
|
|
958
|
-
return value
|
|
959
|
-
}
|
|
960
|
-
try {
|
|
961
|
-
const { runLevels } = await import("./levels-tool.ts")
|
|
962
|
-
const config = valueAfter("--config")
|
|
963
|
-
const minRaw = valueAfter("--min")
|
|
964
|
-
const min = minRaw === undefined ? undefined : Number(minRaw)
|
|
965
|
-
if (min !== undefined && (!Number.isInteger(min) || min < 0 || min > 4)) {
|
|
966
|
-
throw new Error("[nifra] --min must be an integer level between 0 and 4")
|
|
967
|
-
}
|
|
968
|
-
if (
|
|
969
|
-
!(await runLevels(process.cwd(), {
|
|
970
|
-
json: argv.includes("--json"),
|
|
971
|
-
...(config !== undefined ? { config } : {}),
|
|
972
|
-
...(min !== undefined ? { min } : {}),
|
|
973
|
-
}))
|
|
974
|
-
)
|
|
975
|
-
process.exitCode = 1
|
|
976
|
-
} catch (err) {
|
|
977
|
-
console.error(formatCliError(err))
|
|
978
|
-
process.exitCode = 1
|
|
979
|
-
}
|
|
980
|
-
return
|
|
981
|
-
}
|
|
982
|
-
if (command === "prove") {
|
|
983
|
-
const valueAfter = (flag: string): string | undefined => {
|
|
984
|
-
const index = argv.indexOf(flag)
|
|
985
|
-
const value = index === -1 ? undefined : argv[index + 1]
|
|
986
|
-
if (index !== -1 && (value === undefined || value.startsWith("-"))) {
|
|
987
|
-
throw new Error(`[nifra] ${flag} needs a value`)
|
|
988
|
-
}
|
|
989
|
-
return value
|
|
990
|
-
}
|
|
991
|
-
try {
|
|
992
|
-
const minRaw = valueAfter("--min")
|
|
993
|
-
const min = minRaw === undefined ? undefined : Number(minRaw)
|
|
994
|
-
if (min !== undefined && (!Number.isInteger(min) || min < 0 || min > 4)) {
|
|
995
|
-
throw new Error("[nifra] --min must be an integer level between 0 and 4")
|
|
996
|
-
}
|
|
997
|
-
const changedFiles: string[] = []
|
|
998
|
-
for (let index = 0; index < argv.length; index++) {
|
|
999
|
-
if (argv[index] !== "--file") continue
|
|
1000
|
-
const file = argv[index + 1]
|
|
1001
|
-
if (file === undefined || file.startsWith("-"))
|
|
1002
|
-
throw new Error("[nifra] --file needs a path")
|
|
1003
|
-
changedFiles.push(file)
|
|
1004
|
-
index++
|
|
1005
|
-
}
|
|
1006
|
-
const { runWorkGraph } = await import("./work-graph.ts")
|
|
1007
|
-
if (
|
|
1008
|
-
!(await runWorkGraph(process.cwd(), {
|
|
1009
|
-
json: argv.includes("--json"),
|
|
1010
|
-
...(changedFiles.length === 0 ? {} : { changedFiles }),
|
|
1011
|
-
...(min === undefined ? {} : { minLevel: min }),
|
|
1012
|
-
}))
|
|
1013
|
-
)
|
|
1014
|
-
process.exitCode = 1
|
|
1015
|
-
} catch (err) {
|
|
1016
|
-
console.error(formatCliError(err))
|
|
1017
|
-
process.exitCode = 1
|
|
1018
|
-
}
|
|
1019
|
-
return
|
|
1020
|
-
}
|
|
1021
775
|
// `upgrade` is a pure cwd file-transformer (package.json pins + import moves) driven by a per-release
|
|
1022
776
|
// recipe, then verified with `nifra check`. Dispatch before the eager `loadApp` - it must run on any
|
|
1023
777
|
// repo (API-only, not built, or mid-upgrade with edits that don't yet typecheck under dry-run).
|
|
@@ -1042,36 +796,7 @@ async function main(): Promise<void> {
|
|
|
1042
796
|
}
|
|
1043
797
|
// `port` is a pure cwd-based portability linter (scans source, doesn't run the app) - like `check`/
|
|
1044
798
|
// `doctor`, dispatch before the eager `loadApp` so it runs on an API-only / not-yet-built project.
|
|
1045
|
-
if (command
|
|
1046
|
-
const { runPort } = await import("./port.ts")
|
|
1047
|
-
const targetIdx = argv.indexOf("--target")
|
|
1048
|
-
const target = targetIdx !== -1 ? argv[targetIdx + 1] : undefined
|
|
1049
|
-
if (targetIdx !== -1 && (target === undefined || target.startsWith("-"))) {
|
|
1050
|
-
console.error("[nifra] --target needs a value: bun | node | deno | cf-pages | vercel")
|
|
1051
|
-
process.exitCode = 1
|
|
1052
|
-
return
|
|
1053
|
-
}
|
|
1054
|
-
try {
|
|
1055
|
-
const passed = await runPort(process.cwd(), {
|
|
1056
|
-
...(target !== undefined ? { target } : {}),
|
|
1057
|
-
json: argv.includes("--json"),
|
|
1058
|
-
ci: argv.includes("--ci"),
|
|
1059
|
-
strict: argv.includes("--strict"),
|
|
1060
|
-
})
|
|
1061
|
-
if (!passed) process.exitCode = 1
|
|
1062
|
-
} catch (err) {
|
|
1063
|
-
console.error(formatCliError(err))
|
|
1064
|
-
process.exitCode = 1
|
|
1065
|
-
}
|
|
1066
|
-
return
|
|
1067
|
-
}
|
|
1068
|
-
if (
|
|
1069
|
-
command !== "dev" &&
|
|
1070
|
-
command !== "build" &&
|
|
1071
|
-
command !== "start" &&
|
|
1072
|
-
command !== "context" &&
|
|
1073
|
-
command !== "routes"
|
|
1074
|
-
) {
|
|
799
|
+
if (command !== "dev" && command !== "build" && command !== "start") {
|
|
1075
800
|
console.error(`[nifra] unknown command: ${command}\n`)
|
|
1076
801
|
console.error(HELP)
|
|
1077
802
|
process.exitCode = 1
|
|
@@ -1081,36 +806,7 @@ async function main(): Promise<void> {
|
|
|
1081
806
|
const app = await loadApp(process.cwd(), flags.out)
|
|
1082
807
|
if (command === "dev") await dev(app, flags)
|
|
1083
808
|
else if (command === "build") await buildForTarget(app, flags.target, flags)
|
|
1084
|
-
else
|
|
1085
|
-
else if (command === "routes") {
|
|
1086
|
-
if (argv.includes("--graph")) {
|
|
1087
|
-
console.log(await describeRouteGraph(app, { json: argv.includes("--json") }))
|
|
1088
|
-
} else if (argv.includes("--modes")) {
|
|
1089
|
-
// `--modes` answers a different question than the default table: not "what URLs exist" but "how
|
|
1090
|
-
// does each render, and can the target I'm about to deploy to actually honour that". Resolves
|
|
1091
|
-
// against `--target` when given, so a `static` build's server-needing route fails here, in one
|
|
1092
|
-
// place, instead of 404ing in production.
|
|
1093
|
-
const { buildRouteManifest, renderRouteManifest } = await import(
|
|
1094
|
-
"@nifrajs/web/route-manifest"
|
|
1095
|
-
)
|
|
1096
|
-
const manifest = discoverRoutes(app.routesDir)
|
|
1097
|
-
// `flags.target` defaults to "bun" for `build`; here an EXPLICIT `--target` is what opts into
|
|
1098
|
-
// gating, so read it from argv rather than inheriting the build default (which would silently gate
|
|
1099
|
-
// every `nifra routes --modes` against bun).
|
|
1100
|
-
const targetIdx = argv.indexOf("--target")
|
|
1101
|
-
const target = targetIdx !== -1 ? argv[targetIdx + 1] : undefined
|
|
1102
|
-
const routeManifest = await buildRouteManifest(manifest, {
|
|
1103
|
-
...(target !== undefined ? { target } : {}),
|
|
1104
|
-
})
|
|
1105
|
-
if (argv.includes("--json")) console.log(JSON.stringify(routeManifest, null, 2))
|
|
1106
|
-
else console.log(renderRouteManifest(routeManifest))
|
|
1107
|
-
// A target that cannot honour a route is a build-breaking fact, not a note: exit non-zero so the
|
|
1108
|
-
// same command in CI gates the deploy it is warning about.
|
|
1109
|
-
if (routeManifest.conflicts.length > 0) process.exitCode = 1
|
|
1110
|
-
} else {
|
|
1111
|
-
console.log(await describeRoutes(app, { json: argv.includes("--json") }))
|
|
1112
|
-
}
|
|
1113
|
-
} else await start(app, flags)
|
|
809
|
+
else await start(app, flags)
|
|
1114
810
|
}
|
|
1115
811
|
|
|
1116
812
|
// Only run the CLI when invoked as the entry (`bun cli.ts …`), not when a test imports it for the
|