@nifrajs/cli 2.10.0 → 2.12.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.
- package/README.md +7 -1
- package/dist/assure-hydration.d.ts +14 -0
- package/dist/assure-hydration.d.ts.map +1 -0
- package/dist/assure-hydration.js +483 -0
- package/dist/assure-hydration.js.map +1 -0
- package/dist/assure.d.ts +35 -0
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +222 -1
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +13 -0
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +64 -1
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check.d.ts +133 -10
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +741 -148
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +21 -14
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +288 -73
- package/dist/cli.js.map +1 -1
- package/dist/contract-proof.d.ts +49 -0
- package/dist/contract-proof.d.ts.map +1 -0
- package/dist/contract-proof.js +128 -0
- package/dist/contract-proof.js.map +1 -0
- package/dist/contracts.d.ts +22 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +134 -0
- package/dist/contracts.js.map +1 -0
- package/dist/dev-bun-config.d.ts +17 -6
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +51 -14
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/diagnostics.d.ts +23 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +9 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/diff-tool.d.ts +2 -0
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +1 -1
- package/dist/diff-tool.js.map +1 -1
- package/dist/docs-search.d.ts +8 -3
- package/dist/docs-search.d.ts.map +1 -1
- package/dist/docs-search.js +36 -15
- package/dist/docs-search.js.map +1 -1
- package/dist/doctor.d.ts +38 -2
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +189 -7
- package/dist/doctor.js.map +1 -1
- package/dist/examples.d.ts +1 -2
- package/dist/examples.d.ts.map +1 -1
- package/dist/examples.js +37 -16
- package/dist/examples.js.map +1 -1
- package/dist/fix-recipes.d.ts +12 -0
- package/dist/fix-recipes.d.ts.map +1 -0
- package/dist/fix-recipes.js +106 -0
- package/dist/fix-recipes.js.map +1 -0
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +60 -4
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/source-facts.d.ts +15 -0
- package/dist/internal/source-facts.d.ts.map +1 -0
- package/dist/internal/source-facts.js +82 -0
- package/dist/internal/source-facts.js.map +1 -0
- package/dist/internal/typescript-import.d.ts +9 -0
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +38 -0
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +6 -2
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +60 -5
- package/dist/introspect.js.map +1 -1
- package/dist/load.d.ts +10 -0
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +1 -0
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +14 -9
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +3 -0
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-root.d.ts +83 -0
- package/dist/mcp-root.d.ts.map +1 -0
- package/dist/mcp-root.js +221 -0
- package/dist/mcp-root.js.map +1 -0
- package/dist/mcp-run.d.ts.map +1 -1
- package/dist/mcp-run.js +18 -4
- package/dist/mcp-run.js.map +1 -1
- package/dist/mcp.d.ts +27 -4
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +360 -27
- package/dist/mcp.js.map +1 -1
- package/dist/pipeline-guard.d.ts +25 -10
- package/dist/pipeline-guard.d.ts.map +1 -1
- package/dist/pipeline-guard.js +35 -15
- package/dist/pipeline-guard.js.map +1 -1
- package/dist/pipeline-report.d.ts +34 -0
- package/dist/pipeline-report.d.ts.map +1 -0
- package/dist/pipeline-report.js +282 -0
- package/dist/pipeline-report.js.map +1 -0
- package/dist/project-path.d.ts +3 -0
- package/dist/project-path.d.ts.map +1 -0
- package/dist/project-path.js +36 -0
- package/dist/project-path.js.map +1 -0
- package/dist/replay.d.ts +12 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +36 -0
- package/dist/replay.js.map +1 -0
- package/dist/rules/codes.d.ts +42 -0
- package/dist/rules/codes.d.ts.map +1 -0
- package/dist/rules/codes.js +52 -0
- package/dist/rules/codes.js.map +1 -0
- package/dist/rules/comment-markers.d.ts +10 -0
- package/dist/rules/comment-markers.d.ts.map +1 -0
- package/dist/rules/comment-markers.js +24 -0
- package/dist/rules/comment-markers.js.map +1 -0
- package/dist/rules/index.d.ts +31 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +133 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/legacy.d.ts +7 -0
- package/dist/rules/legacy.d.ts.map +1 -0
- package/dist/rules/legacy.js +85 -0
- package/dist/rules/legacy.js.map +1 -0
- package/dist/rules/routes.d.ts +12 -0
- package/dist/rules/routes.d.ts.map +1 -0
- package/dist/rules/routes.js +142 -0
- package/dist/rules/routes.js.map +1 -0
- package/dist/rules/security.d.ts +10 -0
- package/dist/rules/security.d.ts.map +1 -0
- package/dist/rules/security.js +471 -0
- package/dist/rules/security.js.map +1 -0
- package/dist/search-terms.d.ts +2 -0
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +5 -1
- package/dist/search-terms.js.map +1 -1
- package/dist/types-search.d.ts +1 -2
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +33 -16
- package/dist/types-search.js.map +1 -1
- package/dist/verification.d.ts +9 -0
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +17 -1
- package/dist/verification.js.map +1 -1
- package/dist/work-graph.d.ts +123 -0
- package/dist/work-graph.d.ts.map +1 -0
- package/dist/work-graph.js +465 -0
- package/dist/work-graph.js.map +1 -0
- package/docs/examples.json +32 -2
- package/docs/llms-full.txt +1105 -27
- package/docs/types.json +1772 -137
- package/package.json +34 -9
- package/src/assure-hydration.ts +606 -0
- package/src/assure.ts +316 -2
- package/src/capabilities-tool.ts +80 -1
- package/src/check.ts +923 -169
- package/src/cli.ts +297 -76
- package/src/contract-proof.ts +194 -0
- package/src/contracts.ts +157 -0
- package/src/dev-bun-config.ts +57 -14
- package/src/diagnostics.ts +32 -0
- package/src/diff-tool.ts +1 -1
- package/src/docs-search.ts +45 -15
- package/src/doctor.ts +286 -8
- package/src/examples.ts +43 -15
- package/src/fix-recipes.ts +126 -0
- package/src/init-agents.ts +61 -4
- package/src/internal/source-facts.ts +133 -0
- package/src/internal/typescript-import.ts +29 -0
- package/src/introspect.ts +81 -6
- package/src/load.ts +11 -0
- package/src/manifest-tool.ts +16 -10
- package/src/mcp-docs-tools.ts +3 -0
- package/src/mcp-http.ts +1 -1
- package/src/mcp-root.ts +271 -0
- package/src/mcp-run.ts +19 -4
- package/src/mcp.ts +440 -34
- package/src/pipeline-guard.ts +44 -14
- package/src/pipeline-report.ts +341 -0
- package/src/project-path.ts +36 -0
- package/src/replay.ts +44 -0
- package/src/rules/codes.ts +57 -0
- package/src/rules/comment-markers.ts +26 -0
- package/src/rules/index.ts +175 -0
- package/src/rules/legacy.ts +93 -0
- package/src/rules/routes.ts +157 -0
- package/src/rules/security.ts +505 -0
- package/src/search-terms.ts +9 -4
- package/src/types-search.ts +39 -15
- package/src/verification.ts +18 -1
- package/src/work-graph.ts +613 -0
package/src/cli.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* `nifra` - the zero-config CLI for a nifra app. Reads `framework.ts` + `backend.ts` + `routes/` from
|
|
4
4
|
* the project root (see {@link loadApp}) and wires the right `@nifrajs/web` entrypoint:
|
|
5
5
|
*
|
|
6
|
-
* nifra dev true-HMR dev server (
|
|
6
|
+
* nifra dev true-HMR dev server (Bun native HMR + nifra SSR) - @nifrajs/web/dev
|
|
7
7
|
* nifra build emit a complete target-specific deploy directory - @nifrajs/web/build
|
|
8
8
|
* nifra start run the default Bun build - dist/server.js
|
|
9
9
|
*
|
|
@@ -12,12 +12,17 @@
|
|
|
12
12
|
import { existsSync } from "node:fs"
|
|
13
13
|
import { resolve } from "node:path"
|
|
14
14
|
import { inProcessClient } from "@nifrajs/client"
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
type CreateWebAppOptions,
|
|
17
|
+
createWebApp,
|
|
18
|
+
DEFAULT_DEV_PORT,
|
|
19
|
+
type RenderAdapter,
|
|
20
|
+
} from "@nifrajs/web"
|
|
16
21
|
import { discoverRoutes } from "@nifrajs/web/fs"
|
|
17
22
|
import type { BunPlugin } from "bun"
|
|
18
23
|
import { describeProject, describeRouteGraph, describeRoutes } from "./introspect.ts"
|
|
19
|
-
import { type LoadedApp, loadApp } from "./load.ts"
|
|
20
|
-
import { chooseBuildPipeline } from "./pipeline-guard.ts"
|
|
24
|
+
import { type LoadedApp, loadApp, type NifraFramework } from "./load.ts"
|
|
25
|
+
import { chooseBuildPipeline, describePipeline } from "./pipeline-guard.ts"
|
|
21
26
|
|
|
22
27
|
export interface Flags {
|
|
23
28
|
readonly port: number
|
|
@@ -30,9 +35,8 @@ export interface Flags {
|
|
|
30
35
|
/** `nifra build --vite`: force the client + server through Vite/Rollup. Without a flag the pipeline is
|
|
31
36
|
* chosen per app (`chooseBuildPipeline`) - Bun unless the app's only transforms are `vitePlugins`. */
|
|
32
37
|
readonly vite: boolean
|
|
33
|
-
/** `nifra dev --bun`:
|
|
34
|
-
*
|
|
35
|
-
* ecosystem; `--bun` trades that for one bundler across dev and prod. Refuses CSS-Modules apps.
|
|
38
|
+
/** `nifra dev --bun`: force the Bun-pipeline dev server. Rarely needed - `dev` and `build` share one
|
|
39
|
+
* rule, so Bun is already the default unless `vitePlugins` are the app's ONLY transforms.
|
|
36
40
|
*
|
|
37
41
|
* `nifra build --bun`: force the Bun build. Refuses when that would drop the app's `vitePlugins`. */
|
|
38
42
|
readonly bun: boolean
|
|
@@ -41,11 +45,14 @@ export interface Flags {
|
|
|
41
45
|
const HELP = `nifra - zero-config dev/build/start for a nifra app
|
|
42
46
|
|
|
43
47
|
Usage:
|
|
44
|
-
nifra dev [--port <n>] [--poll] Start the true-HMR dev server (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
nifra dev [--port <n>] [--poll] Start the true-HMR dev server (Bun). Default port ${DEFAULT_DEV_PORT}.
|
|
49
|
+
An app whose ONLY transforms are \`vitePlugins\` uses Vite (the
|
|
50
|
+
Bun pipeline cannot run them); everything else uses Bun.
|
|
51
|
+
[--vite] Force the Vite middleware/HMR pipeline.
|
|
52
|
+
[--bun] Force Bun.serve native HMR (React Fast Refresh included, state
|
|
53
|
+
preserved), no Vite in the process. CSS Modules, plain CSS,
|
|
54
|
+
Tailwind and the app's own \`clientPlugins\`/\`serverPlugins\` run
|
|
55
|
+
through the same Bun transforms production uses.
|
|
49
56
|
nifra build [--out <dir>] [--report] Emit a complete deploy directory.
|
|
50
57
|
[--target <t>] Target a FULL deploy dir for <t>:
|
|
51
58
|
bun | node | deno | cf-pages | vercel | static. Packages
|
|
@@ -80,8 +87,11 @@ Usage:
|
|
|
80
87
|
and a "## MCP server" section in AGENTS.md. No-clobber by
|
|
81
88
|
default (skips a file you've customized); --force overwrites the
|
|
82
89
|
owned files. AGENTS.md is only appended to, never overwritten.
|
|
83
|
-
nifra mcp
|
|
84
|
-
agent
|
|
90
|
+
nifra mcp [dir] Start an MCP server (stdio) exposing this project to a coding
|
|
91
|
+
agent. The project root is [dir] when given, else resolved from
|
|
92
|
+
cwd (marker walk-up + the client's MCP roots); tools refuse
|
|
93
|
+
with a fix when no nifra project is found - never a silent
|
|
94
|
+
wrong-project answer. Tools: nifra_context, nifra_routes,
|
|
85
95
|
nifra_run (backend), nifra_render (SSR a page), nifra_docs,
|
|
86
96
|
nifra_example (verified snippets), nifra_scaffold (route→file),
|
|
87
97
|
nifra_check (drift gate + fixes), nifra_levels (verification
|
|
@@ -94,6 +104,8 @@ Usage:
|
|
|
94
104
|
nifra check [--json] [--lints-only] Gate: typecheck + lints (hand-rolled fetch(), untyped client("…"),
|
|
95
105
|
server-only imports in routes/). Run as "done"; --json for agents;
|
|
96
106
|
--lints-only skips tsc for a near-instant inner-loop pass.
|
|
107
|
+
nifra fix [--code <NF-code>] Apply a registered diagnostic recipe, then print the remaining
|
|
108
|
+
structured diagnostics.
|
|
97
109
|
nifra sync-manifest Regenerate a committed web server-manifest.ts from routes/ WITHOUT
|
|
98
110
|
a full build - clears server-manifest drift after a route add/rename
|
|
99
111
|
(a new hydrating component still needs a full build).
|
|
@@ -109,9 +121,14 @@ Usage:
|
|
|
109
121
|
closed. Exits non-zero on any breaking change - run it in CI.
|
|
110
122
|
nifra sdk --lang <python|go> [--out <file>]
|
|
111
123
|
Generate a deterministic non-TypeScript SDK from backend.ts.
|
|
112
|
-
nifra assure [--config <file>] [--json]
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
nifra assure [--config <file>] [--json] [--strict] [--out <file>] [--hydration] [--interact]
|
|
125
|
+
Emit one structured assurance bundle. The bundle records every
|
|
126
|
+
applicable gate as pass, fail, or skip and exits non-zero unless
|
|
127
|
+
its verdict is green. Without these bundle flags, the legacy
|
|
128
|
+
route-assurance report remains available.
|
|
129
|
+
nifra contracts snapshot [--out <file>] Write the deterministic route contract lock.
|
|
130
|
+
nifra contracts check [--json] Check the current routes against contracts.lock.json.
|
|
131
|
+
nifra replay <file> Replay a token-only verification metadata file.
|
|
115
132
|
nifra capabilities snapshot [--out <file>] [--config <file>]
|
|
116
133
|
Write the deterministic, token-only capability lockfile, but
|
|
117
134
|
only after provenance + idempotency assurance passes.
|
|
@@ -128,9 +145,15 @@ Usage:
|
|
|
128
145
|
Compute the verification ladder (L0 typed contract → L1 route
|
|
129
146
|
assurance → L2 capability lockfile → L3 route manifest → L4
|
|
130
147
|
invariant-tested); --min <n> fails the exit code below level n.
|
|
148
|
+
nifra prove [--json] [--file <path>] [--min <n>]
|
|
149
|
+
Build the static verification work graph, plan the cheapest
|
|
150
|
+
proofs for changed files, and report a machine-checkable stop
|
|
151
|
+
condition. Requires a fresh build; never probes a running app.
|
|
131
152
|
nifra doctor [--json] [--auto-fix] Flag undeclared imports and multiple physical copies of
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
[--strict] [--target] @nifrajs/core/React identity-sensitive dependencies;
|
|
154
|
+
print production readiness, with --strict making absent
|
|
155
|
+
applicable guarantees fail. --auto-fix writes safe local-
|
|
156
|
+
version dependency fixes.
|
|
134
157
|
nifra upgrade <version> Run the per-release upgrade recipe for <version>: sweep every
|
|
135
158
|
[--write] [--no-verify] matching dependency pin (preserving ^/~/exact), move removed
|
|
136
159
|
[--list] [--json] packages, apply exact imports, then verify with nifra check.
|
|
@@ -152,11 +175,13 @@ Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with
|
|
|
152
175
|
\`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).`
|
|
153
176
|
|
|
154
177
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
155
|
-
const CLI_VERSION = "2.
|
|
178
|
+
const CLI_VERSION = "2.12.0"
|
|
156
179
|
|
|
157
180
|
// A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
|
|
158
181
|
const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
|
|
159
182
|
const asBunPlugins = (v: readonly unknown[]): BunPlugin[] => v as BunPlugin[]
|
|
183
|
+
const asUse = (v: (app: never) => void): NonNullable<CreateWebAppOptions["use"]> =>
|
|
184
|
+
v as NonNullable<CreateWebAppOptions["use"]>
|
|
160
185
|
const apiOf = (backend: unknown): { api?: unknown } =>
|
|
161
186
|
backend === undefined ? {} : { api: inProcessClient(backend as never) }
|
|
162
187
|
|
|
@@ -194,52 +219,26 @@ export function formatCliError(err: unknown): string {
|
|
|
194
219
|
return String(err)
|
|
195
220
|
}
|
|
196
221
|
|
|
197
|
-
/**
|
|
198
|
-
* Refuse `nifra dev --bun` for an app Bun's DEV-server bundler cannot compile.
|
|
199
|
-
*
|
|
200
|
-
* Bun's dev server and `Bun.build` are not the same bundler. `Bun.build` transforms `*.module.css` into a
|
|
201
|
-
* scoped class map (the Bun production build of a CSS-Modules app works, verified), but the dev server's
|
|
202
|
-
* bundler does not: the import compiles to a dangling reference and the browser throws
|
|
203
|
-
* `ReferenceError: import_X_module is not defined` from inside the component - a message that names
|
|
204
|
-
* neither CSS Modules nor the dev server, so the cause is invisible.
|
|
205
|
-
*
|
|
206
|
-
* A silently-broken client is the one outcome worth failing the command over, so this checks up front and
|
|
207
|
-
* names the trade plus both ways out. Narrow by design: only the transform proven missing is refused, so
|
|
208
|
-
* an app without CSS Modules still gets the Bun dev loop.
|
|
209
|
-
*/
|
|
210
|
-
export async function assertBunDevSupportsApp(app: LoadedApp): Promise<void> {
|
|
211
|
-
// Server functions and `*.server` modules are handled: the generated bunfig (dev-bun-config.ts)
|
|
212
|
-
// delivers the SAME serverFnStubPlugin/serverOnlyEmptyPlugin the production build uses to Bun's
|
|
213
|
-
// dev-server bundler via `[serve.static] plugins` - the one channel that bundler accepts plugins
|
|
214
|
-
// through (a runtime `Bun.plugin` never reaches it; programmatic ask: oven-sh/bun#36830). The old
|
|
215
|
-
// fail-closed refusal for those modules is gone because the leak it guarded is gone.
|
|
216
|
-
//
|
|
217
|
-
// CSS Modules remain gated: the `*.module.css` plugin rests on an `onResolve` virtual-module
|
|
218
|
-
// stylesheet emitter that is not yet verified under `[serve.static]`, and its failure mode is a
|
|
219
|
-
// broken client rather than a leak - refuse loudly until it is proven there.
|
|
220
|
-
const { Glob } = await import("bun")
|
|
221
|
-
const offenders: string[] = []
|
|
222
|
-
for await (const file of new Glob("**/*.module.css").scan({ cwd: app.cwd, dot: false })) {
|
|
223
|
-
if (/(^|\/)(node_modules|dist|\.nifra|\.git)\//.test(file)) continue
|
|
224
|
-
offenders.push(file)
|
|
225
|
-
if (offenders.length >= 5) break
|
|
226
|
-
}
|
|
227
|
-
if (offenders.length === 0) return
|
|
228
|
-
throw new Error(
|
|
229
|
-
"[nifra] `nifra dev --bun` can't compile CSS Modules: Bun's DEV-server bundler has no `*.module.css` " +
|
|
230
|
-
"transform (its production `Bun.build` does, so `nifra build` is unaffected). The client would load " +
|
|
231
|
-
"with a bare `ReferenceError: import_… is not defined` instead of your component.\n" +
|
|
232
|
-
` CSS Modules found: ${offenders.join(", ")}\n` +
|
|
233
|
-
" Use `nifra dev` (the Vite pipeline handles them), or drop the CSS Modules to use the Bun dev loop.",
|
|
234
|
-
)
|
|
235
|
-
}
|
|
236
|
-
|
|
237
222
|
async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
223
|
+
if (flags.vite && flags.bun) {
|
|
224
|
+
throw new Error("[nifra] `nifra dev` takes `--vite` or `--bun`, not both.")
|
|
225
|
+
}
|
|
226
|
+
// Default to Bun for the dev phase too. An app whose only transforms are Vite plugins stays on Vite so
|
|
227
|
+
// its transform is never silently dropped; `--vite`/`--bun` make that choice explicit.
|
|
228
|
+
// ONE rule for both phases - `chooseBuildPipeline` decides `dev` exactly as it decides `build`, so a
|
|
229
|
+
// project cannot end up bundled by one toolchain in dev and the other in production. An earlier
|
|
230
|
+
// version of this kept apps declaring `vitePlugins` on Vite for dev even when they also shipped Bun
|
|
231
|
+
// equivalents, on the theory that the framework plugins were the safer HMR path. Measurement killed
|
|
232
|
+
// it: Vue, Solid and Svelte all render correctly on the Bun pipeline, so the hedge bought nothing
|
|
233
|
+
// and cost an app the guarantee that dev and production bundle the same way. (Svelte used to render
|
|
234
|
+
// ONLY on Bun, which made the hedge actively harmful; it now renders on either - see the note at the
|
|
235
|
+
// Vite branch for what the adapter had to be given to make that true.)
|
|
236
|
+
const decision = chooseBuildPipeline(
|
|
237
|
+
app.resolvedPlugins,
|
|
238
|
+
flags.vite ? "vite" : flags.bun ? "bun" : undefined,
|
|
239
|
+
"dev",
|
|
240
|
+
)
|
|
241
|
+
if (decision.pipeline === "bun") {
|
|
243
242
|
// Bun's dev-server bundler takes plugins only via bunfig `[serve.static]`, read at process
|
|
244
243
|
// start - so the boundary plugins (server-fn stubs, server-only emptying) are delivered by
|
|
245
244
|
// generating a config and re-execing this same command once with `--config=`. The child proves
|
|
@@ -259,15 +258,36 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
259
258
|
"launches; refusing to serve without the client-boundary plugins.",
|
|
260
259
|
)
|
|
261
260
|
}
|
|
262
|
-
const { bunfigPath, launchToken } = await writeBunDevConfig(app.cwd)
|
|
261
|
+
const { bunfigPath, launchToken } = await writeBunDevConfig(app.cwd, app.configPath)
|
|
263
262
|
const child = Bun.spawn(
|
|
264
|
-
[
|
|
263
|
+
[
|
|
264
|
+
process.execPath,
|
|
265
|
+
`--config=${bunfigPath}`,
|
|
266
|
+
// The app's `conditions` reach SSR here or nowhere: bunfig has no key for them (a top-level,
|
|
267
|
+
// `[run]`, `[serve.static]` or `[bundle]` `conditions` is parsed and ignored), and the re-exec
|
|
268
|
+
// is the only point where this process's resolver is still configurable. Without it a package
|
|
269
|
+
// with an `exports` map resolves to a DIFFERENT file on the server than in the client bundle,
|
|
270
|
+
// which is the two-toolchains-one-module failure the pipeline guard exists to prevent.
|
|
271
|
+
//
|
|
272
|
+
// One flag per condition. `--conditions=a,b` is accepted and matches nothing: Bun takes the
|
|
273
|
+
// whole string as a single condition name, so the comma form fails silently.
|
|
274
|
+
...(app.framework.conditions ?? []).map((condition) => `--conditions=${condition}`),
|
|
275
|
+
...process.argv.slice(1),
|
|
276
|
+
],
|
|
265
277
|
{
|
|
266
278
|
stdio: ["inherit", "inherit", "inherit"],
|
|
267
279
|
env: {
|
|
268
280
|
...process.env,
|
|
269
281
|
NIFRA_BUN_DEV_TOKEN: launchToken,
|
|
270
282
|
NIFRA_BUN_DEV_DEPTH: String(depth + 1),
|
|
283
|
+
// The child is the process that renders, so it is the one that needs dev-shaped runtimes.
|
|
284
|
+
// A library ships its dev build behind the `development` export condition with a `NODE_ENV`
|
|
285
|
+
// fallback for resolvers that set no conditions - which is Bun's runtime, where SSR runs,
|
|
286
|
+
// so on the server that fallback is the only signal there is. It has to be in place before
|
|
287
|
+
// the app's config is read, because reading it imports the adapter and through it the
|
|
288
|
+
// framework runtime; setting it inside the dev server would already be a step too late.
|
|
289
|
+
// Preserved if the caller pinned one.
|
|
290
|
+
NODE_ENV: process.env.NODE_ENV ?? "development",
|
|
271
291
|
},
|
|
272
292
|
},
|
|
273
293
|
)
|
|
@@ -276,6 +296,27 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
276
296
|
process.on("SIGTERM", forward)
|
|
277
297
|
process.exit(await child.exited)
|
|
278
298
|
}
|
|
299
|
+
// SSR runs in THIS process, on Bun's runtime - a different loader from the dev-server bundler the
|
|
300
|
+
// bunfig above configures. Both halves have to agree, so the runtime gets the same transforms:
|
|
301
|
+
//
|
|
302
|
+
// - CSS Modules. The scoped name is a pure function of file path + class name, so the `"ssr"` form
|
|
303
|
+
// produces the identical map the client bundle emits (and no stylesheet - that ships from the
|
|
304
|
+
// client build). Without it `styles.box` is `undefined` on the server: the SSR markup carries no
|
|
305
|
+
// class, the page paints unstyled, and hydration then reports a className mismatch.
|
|
306
|
+
// - the app's `serverPlugins` - the SSR counterpart of the `clientPlugins` the generated bunfig
|
|
307
|
+
// delivers, exactly as `nifra build`'s prerender pass registers them.
|
|
308
|
+
//
|
|
309
|
+
// Registered BEFORE `createDevServer`, because a Bun runtime plugin only affects modules loaded
|
|
310
|
+
// after it - and `createApp` imports the route modules.
|
|
311
|
+
const { plugin } = await import("bun")
|
|
312
|
+
const { cssModulesBunPlugin } = await import("@nifrajs/web/plugins/css-modules")
|
|
313
|
+
plugin(cssModulesBunPlugin("ssr"))
|
|
314
|
+
for (const p of asBunPlugins(app.resolvedPlugins.serverPlugins)) plugin(p)
|
|
315
|
+
// "After" has one consequence an adapter has to respect. Reading the app's config is what PRODUCES
|
|
316
|
+
// these plugins, so the config's import graph - which includes the adapter - is already loaded when
|
|
317
|
+
// they register. An adapter must therefore not import an asset its own plugin compiles at module
|
|
318
|
+
// scope; it has to defer to first render, which happens after this point. `@nifrajs/web-svelte`
|
|
319
|
+
// loads its `Chain.svelte` that way for exactly this reason, and says so at the import.
|
|
279
320
|
const { createDevServer } = await import("@nifrajs/web/dev")
|
|
280
321
|
const { framework: fw, routesDir, outDir, backend } = app
|
|
281
322
|
const server = await createDevServer({
|
|
@@ -283,8 +324,10 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
283
324
|
outDir,
|
|
284
325
|
clientModule: fw.clientModule,
|
|
285
326
|
port: flags.port,
|
|
286
|
-
//
|
|
287
|
-
//
|
|
327
|
+
// For the background client-leak guard, which re-runs `buildClient` on change - the SERVED client
|
|
328
|
+
// bundle gets these same plugins through the generated bunfig, since Bun's dev-server bundler takes
|
|
329
|
+
// plugins only as module paths. Never `vitePlugins`, which belong to the other pipeline;
|
|
330
|
+
// `assertPipelineSeparation` already refuses a plugin sitting in the wrong slot.
|
|
288
331
|
plugins: asBunPlugins(app.resolvedPlugins.clientPlugins),
|
|
289
332
|
...(fw.publicDir !== undefined ? { publicDir: fw.publicDir } : {}),
|
|
290
333
|
...(fw.conditions ? { conditions: fw.conditions } : {}),
|
|
@@ -294,13 +337,15 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
294
337
|
adapter: asAdapter(fw.adapter),
|
|
295
338
|
manifest: discoverRoutes(routesDir, { importQuery }),
|
|
296
339
|
clientEntry,
|
|
340
|
+
...(fw.use ? { use: asUse(fw.use) } : {}),
|
|
297
341
|
...apiOf(backend),
|
|
298
342
|
}),
|
|
299
343
|
})
|
|
300
344
|
console.log(`nifra dev (bun) → http://localhost:${server.port}`)
|
|
345
|
+
console.log(` ${describePipeline(decision)}`)
|
|
301
346
|
return
|
|
302
347
|
}
|
|
303
|
-
// Preflight:
|
|
348
|
+
// Preflight: the Vite fallback needs `vite` resolvable from the project. Run via `bunx @nifrajs/cli dev` the CLI
|
|
304
349
|
// sits in an isolated install where the project's peer deps don't resolve, so the vite import below fails
|
|
305
350
|
// with an opaque ERR_MODULE_NOT_FOUND. Surface the real fix instead.
|
|
306
351
|
try {
|
|
@@ -321,6 +366,18 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
321
366
|
// same file in one process, with only one of them governed by Vite's `resolve.dedupe`. That is what
|
|
322
367
|
// made an app's hand-written React alias fail to reach SSR, and it is the condition being removed -
|
|
323
368
|
// not a redundancy worth keeping "just in case".
|
|
369
|
+
//
|
|
370
|
+
// Svelte is the case that proves the rule, and the discarded attempts are recorded so they are not
|
|
371
|
+
// retried. The adapter package stays `ssr.external` (its context must be the ONE instance the
|
|
372
|
+
// Bun-imported adapter provides), so BUN loads it - and `@nifrajs/web-svelte` renders through its own
|
|
373
|
+
// `Chain.svelte`, which needs a compiler that this pipeline does not put in the runtime. Registering
|
|
374
|
+
// the app's `serverPlugins` here, scoped to that package's directory, compiles it against a SECOND
|
|
375
|
+
// Svelte runtime and the first `setContext` dies (`context.function` null); externalizing the
|
|
376
|
+
// adapter's framework peer as well gets further and then dies on `context.function[FILENAME]`,
|
|
377
|
+
// because `svelteBunPlugin` and `vite-plugin-svelte` do not emit the same dev-mode component shape.
|
|
378
|
+
// Both attempts add a compiler. The fix removes one instead: `createViteDevServer` publishes its
|
|
379
|
+
// `ssrLoadModule` via `setSsrModuleLoader`, and the adapter loads `Chain.svelte` AND `svelte/server`
|
|
380
|
+
// through it - one toolchain, one runtime, context intact across the whole tree.
|
|
324
381
|
const server = await createViteDevServer({
|
|
325
382
|
root: cwd,
|
|
326
383
|
routesDir,
|
|
@@ -340,10 +397,12 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
340
397
|
adapter: asAdapter(fw.adapter),
|
|
341
398
|
manifest: discoverRoutes(routesDir, { load }),
|
|
342
399
|
clientEntry,
|
|
400
|
+
...(fw.use ? { use: asUse(fw.use) } : {}),
|
|
343
401
|
...apiOf(backend),
|
|
344
402
|
}),
|
|
345
403
|
})
|
|
346
|
-
console.log(`nifra dev → http://localhost:${server.port}`)
|
|
404
|
+
console.log(`nifra dev (vite) → http://localhost:${server.port}`)
|
|
405
|
+
console.log(` ${describePipeline(decision)}`)
|
|
347
406
|
}
|
|
348
407
|
|
|
349
408
|
/**
|
|
@@ -353,6 +412,37 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
353
412
|
* `nifra.config.ts`, which pulls in Vite plugins), and the backend from `backend.ts` when present;
|
|
354
413
|
* `buildTarget` generates the per-target server entry from those + the app's `routes/`.
|
|
355
414
|
*/
|
|
415
|
+
/**
|
|
416
|
+
* Refuse a `use` the generated server entry cannot import.
|
|
417
|
+
*
|
|
418
|
+
* `nifra build` emits `import { use } from <frameworkFile>` (framework.ts, the edge-bundlable file),
|
|
419
|
+
* but `loadApp` prefers `nifra.config.ts` - so an app with BOTH files whose `use` lives only in
|
|
420
|
+
* `nifra.config.ts` makes `fw.use` defined while `framework.ts` exports nothing of that name, and the
|
|
421
|
+
* build dies later with an opaque bundler error pointing at generated code. Refuse it here with the
|
|
422
|
+
* exact move instead, in the spirit of `assertPipelineSeparation` (load.ts).
|
|
423
|
+
*
|
|
424
|
+
* Detected by importing `frameworkFile` and checking the named export, not by comparing paths alone:
|
|
425
|
+
* a split app legitimately DEFINES `use` in framework.ts and re-exports it from nifra.config.ts
|
|
426
|
+
* (exactly how `adapter` reaches both readers), and a path compare would refuse that correct layout.
|
|
427
|
+
* The import is cheap - framework.ts is already in the loaded config's module graph in that layout.
|
|
428
|
+
*/
|
|
429
|
+
export async function assertUseIsEdgeExported(
|
|
430
|
+
use: NifraFramework["use"],
|
|
431
|
+
configPath: string,
|
|
432
|
+
frameworkFile: string,
|
|
433
|
+
): Promise<void> {
|
|
434
|
+
if (use === undefined || configPath === frameworkFile) return
|
|
435
|
+
const mod = (await import(frameworkFile).catch(() => ({}))) as { use?: unknown }
|
|
436
|
+
if (typeof mod.use === "function") return
|
|
437
|
+
throw new Error(
|
|
438
|
+
`[nifra] \`use\` is exported from ${configPath} but not from ${frameworkFile}. ` +
|
|
439
|
+
"`nifra build` generates a server entry that imports `use` from framework.ts (the edge-bundled " +
|
|
440
|
+
"file), so this build would fail later with an opaque bundler error inside generated code.\n\n" +
|
|
441
|
+
" - Define `use` in framework.ts and re-export it from nifra.config.ts " +
|
|
442
|
+
'(`export { use } from "./framework.ts"`) so `nifra dev` sees it too.',
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
|
|
356
446
|
async function buildForTarget(app: LoadedApp, target: string, flags: Flags): Promise<void> {
|
|
357
447
|
const { isBuildTarget, renderSizeReport, BUILD_TARGETS } = await import("@nifrajs/web/build")
|
|
358
448
|
if (!isBuildTarget(target)) {
|
|
@@ -384,6 +474,7 @@ async function buildForTarget(app: LoadedApp, target: string, flags: Flags): Pro
|
|
|
384
474
|
? resolve(cwd, "nifra.config.ts")
|
|
385
475
|
: resolve(cwd, "framework.ts")
|
|
386
476
|
const backendFile = resolve(cwd, "backend.ts")
|
|
477
|
+
await assertUseIsEdgeExported(fw.use, app.configPath, frameworkFile)
|
|
387
478
|
// Plugin FORMAT differs by pipeline: the Bun build takes Bun plugins (clientPlugins/serverPlugins); the
|
|
388
479
|
// Vite build takes the app's Vite plugins (fw.vitePlugins) for BOTH halves. buildTargetWith forwards
|
|
389
480
|
// whatever it's given straight to the chosen bundler, which casts to its own plugin type.
|
|
@@ -402,6 +493,7 @@ async function buildForTarget(app: LoadedApp, target: string, flags: Flags): Pro
|
|
|
402
493
|
workDir: resolve(cwd, ".nifra-build"),
|
|
403
494
|
clientModule: fw.clientModule,
|
|
404
495
|
adapterImport: frameworkFile,
|
|
496
|
+
...(fw.use ? { useImport: frameworkFile } : {}),
|
|
405
497
|
...(backend !== undefined && existsSync(backendFile) ? { backendImport: backendFile } : {}),
|
|
406
498
|
...plugins,
|
|
407
499
|
...(fw.conditions ? { conditions: fw.conditions } : {}),
|
|
@@ -411,10 +503,10 @@ async function buildForTarget(app: LoadedApp, target: string, flags: Flags): Pro
|
|
|
411
503
|
// The static target needs a built app to drive prerendering - only build it when targeting static.
|
|
412
504
|
...(target === "static" ? { prerenderApp: await buildPrerenderApp(app) } : {}),
|
|
413
505
|
})
|
|
414
|
-
console.log(`nifra build (${target}${
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
|
|
506
|
+
console.log(`nifra build (${target}, ${decision.pipeline}) → ${result.run}`)
|
|
507
|
+
// The pipeline is stated on every build, not only the surprising ones - an auto-selected Vite build
|
|
508
|
+
// must never look like the default, and a default Bun build must not leave the question open either.
|
|
509
|
+
console.log(` ${describePipeline(decision)}`)
|
|
418
510
|
if (flags.report) console.log(`\n${renderSizeReport(result.size)}`)
|
|
419
511
|
}
|
|
420
512
|
|
|
@@ -435,6 +527,7 @@ async function buildPrerenderApp(
|
|
|
435
527
|
adapter: asAdapter(fw.adapter),
|
|
436
528
|
manifest: discoverRoutes(routesDir),
|
|
437
529
|
clientEntry: client.entry,
|
|
530
|
+
...(fw.use ? { use: asUse(fw.use) } : {}),
|
|
438
531
|
...(client.routes ? { routePreload: client.routes } : {}),
|
|
439
532
|
...(client.css ? { styles: client.css } : {}),
|
|
440
533
|
...(client.routeStyles ? { routeStyles: client.routeStyles } : {}),
|
|
@@ -511,7 +604,10 @@ async function main(): Promise<void> {
|
|
|
511
604
|
// the eager `loadApp` below (which would fail fast on a project that's API-only / not yet built).
|
|
512
605
|
if (command === "mcp") {
|
|
513
606
|
const { runMcpServer } = await import("./mcp.ts")
|
|
514
|
-
|
|
607
|
+
// `nifra mcp [dir]` - an explicit project directory pins the root (for clients configured outside
|
|
608
|
+
// the project); otherwise the server resolves it from cwd + the client's MCP roots.
|
|
609
|
+
const dirArg = argv[1] !== undefined && !argv[1].startsWith("-") ? argv[1] : undefined
|
|
610
|
+
await runMcpServer(process.cwd(), CLI_VERSION, dirArg)
|
|
515
611
|
return
|
|
516
612
|
}
|
|
517
613
|
// `docs-mcp` runs the PUBLIC docs MCP over HTTP - project-independent (serves the bundled corpus), so
|
|
@@ -546,12 +642,36 @@ async function main(): Promise<void> {
|
|
|
546
642
|
if (
|
|
547
643
|
!(await runCheck(process.cwd(), {
|
|
548
644
|
json: argv.includes("--json"),
|
|
645
|
+
structured: argv.includes("--json"),
|
|
549
646
|
lintsOnly: argv.includes("--lints-only"),
|
|
550
647
|
}))
|
|
551
648
|
)
|
|
552
649
|
process.exitCode = 1
|
|
553
650
|
return
|
|
554
651
|
}
|
|
652
|
+
if (command === "fix") {
|
|
653
|
+
const codeIndex = argv.indexOf("--code")
|
|
654
|
+
const code = codeIndex === -1 ? undefined : argv[codeIndex + 1]
|
|
655
|
+
try {
|
|
656
|
+
const { collectCheckResult } = await import("./check.ts")
|
|
657
|
+
const { applyDiagnosticRecipe } = await import("./fix-recipes.ts")
|
|
658
|
+
const result = await collectCheckResult(process.cwd(), { lintsOnly: true })
|
|
659
|
+
const changed: string[] = []
|
|
660
|
+
for (const diagnostic of result.structuredDiagnostics ?? []) {
|
|
661
|
+
if (code !== undefined && diagnostic.code !== code) continue
|
|
662
|
+
changed.push(...(await applyDiagnosticRecipe(process.cwd(), diagnostic)))
|
|
663
|
+
}
|
|
664
|
+
const final = await collectCheckResult(process.cwd(), { lintsOnly: true })
|
|
665
|
+
console.log(
|
|
666
|
+
JSON.stringify({ changed, diagnostics: final.structuredDiagnostics ?? [] }, null, 2),
|
|
667
|
+
)
|
|
668
|
+
if (final.ok === false) process.exitCode = 1
|
|
669
|
+
} catch (err) {
|
|
670
|
+
console.error(formatCliError(err))
|
|
671
|
+
process.exitCode = 1
|
|
672
|
+
}
|
|
673
|
+
return
|
|
674
|
+
}
|
|
555
675
|
// `init-agents` retrofits the agent-discovery files (.mcp.json, CLAUDE.md, …) into the cwd. It's a
|
|
556
676
|
// pure file-writing command independent of the app loading, so dispatch it before the eager `loadApp`
|
|
557
677
|
// (an existing app might be API-only or not yet built). It always succeeds unless a write throws.
|
|
@@ -582,10 +702,19 @@ async function main(): Promise<void> {
|
|
|
582
702
|
// so it runs on an API-only / not-yet-built project.
|
|
583
703
|
if (command === "doctor") {
|
|
584
704
|
const { runDoctor } = await import("./doctor.ts")
|
|
705
|
+
const targetIdx = argv.indexOf("--target")
|
|
706
|
+
const target = targetIdx !== -1 ? argv[targetIdx + 1] : undefined
|
|
707
|
+
if (targetIdx !== -1 && (target === undefined || target.startsWith("-"))) {
|
|
708
|
+
console.error("[nifra] --target needs a value: bun | node | deno | cf-pages | vercel")
|
|
709
|
+
process.exitCode = 1
|
|
710
|
+
return
|
|
711
|
+
}
|
|
585
712
|
if (
|
|
586
713
|
!(await runDoctor(process.cwd(), {
|
|
587
714
|
json: argv.includes("--json"),
|
|
588
715
|
autoFix: argv.includes("--auto-fix") || argv.includes("--fix"),
|
|
716
|
+
strict: argv.includes("--strict"),
|
|
717
|
+
...(target === undefined ? {} : { target }),
|
|
589
718
|
}))
|
|
590
719
|
)
|
|
591
720
|
process.exitCode = 1
|
|
@@ -662,6 +791,16 @@ async function main(): Promise<void> {
|
|
|
662
791
|
if (
|
|
663
792
|
!(await runAssurance(process.cwd(), {
|
|
664
793
|
json: argv.includes("--json"),
|
|
794
|
+
bundle:
|
|
795
|
+
argv.includes("--json") ||
|
|
796
|
+
argv.includes("--strict") ||
|
|
797
|
+
argv.includes("--hydration") ||
|
|
798
|
+
argv.includes("--interact") ||
|
|
799
|
+
argv.includes("--out"),
|
|
800
|
+
strict: argv.includes("--strict"),
|
|
801
|
+
hydration: argv.includes("--hydration"),
|
|
802
|
+
interact: argv.includes("--interact"),
|
|
803
|
+
...(argv.includes("--out") ? { out: argv[argv.indexOf("--out") + 1] } : {}),
|
|
665
804
|
...(config !== undefined ? { config } : {}),
|
|
666
805
|
}))
|
|
667
806
|
)
|
|
@@ -672,6 +811,49 @@ async function main(): Promise<void> {
|
|
|
672
811
|
}
|
|
673
812
|
return
|
|
674
813
|
}
|
|
814
|
+
if (command === "contracts") {
|
|
815
|
+
const action = argv[1]
|
|
816
|
+
const outIndex = argv.indexOf("--out")
|
|
817
|
+
const out = outIndex === -1 ? undefined : argv[outIndex + 1]
|
|
818
|
+
try {
|
|
819
|
+
const { checkContractsLock, snapshotContracts } = await import("./contracts.ts")
|
|
820
|
+
if (action === "snapshot") {
|
|
821
|
+
const lock = await snapshotContracts(process.cwd(), out)
|
|
822
|
+
if (argv.includes("--json")) console.log(JSON.stringify(lock, null, 2))
|
|
823
|
+
else console.log(`[nifra] wrote ${Object.keys(lock.routes).length} contract digests`)
|
|
824
|
+
} else if (action === "check") {
|
|
825
|
+
const result = await checkContractsLock(process.cwd())
|
|
826
|
+
const output = { ok: result.present && result.diagnostics.length === 0, ...result }
|
|
827
|
+
console.log(JSON.stringify(output, null, 2))
|
|
828
|
+
if (!output.ok) process.exitCode = 1
|
|
829
|
+
} else {
|
|
830
|
+
console.error("[nifra] contracts needs snapshot or check")
|
|
831
|
+
process.exitCode = 1
|
|
832
|
+
}
|
|
833
|
+
} catch (err) {
|
|
834
|
+
console.error(formatCliError(err))
|
|
835
|
+
process.exitCode = 1
|
|
836
|
+
}
|
|
837
|
+
return
|
|
838
|
+
}
|
|
839
|
+
if (command === "replay") {
|
|
840
|
+
const file = argv[1]
|
|
841
|
+
if (file === undefined || file.startsWith("-")) {
|
|
842
|
+
console.error("[nifra] replay needs a file path")
|
|
843
|
+
process.exitCode = 1
|
|
844
|
+
return
|
|
845
|
+
}
|
|
846
|
+
try {
|
|
847
|
+
const { runReplay } = await import("./replay.ts")
|
|
848
|
+
const result = await runReplay(process.cwd(), file)
|
|
849
|
+
console.log(JSON.stringify(result, null, 2))
|
|
850
|
+
if (!result.ok) process.exitCode = 1
|
|
851
|
+
} catch (err) {
|
|
852
|
+
console.error(formatCliError(err))
|
|
853
|
+
process.exitCode = 1
|
|
854
|
+
}
|
|
855
|
+
return
|
|
856
|
+
}
|
|
675
857
|
if (command === "capabilities") {
|
|
676
858
|
const action = argv[1]
|
|
677
859
|
const valueAfter = (flag: string): string | undefined => {
|
|
@@ -786,6 +968,45 @@ async function main(): Promise<void> {
|
|
|
786
968
|
}
|
|
787
969
|
return
|
|
788
970
|
}
|
|
971
|
+
if (command === "prove") {
|
|
972
|
+
const valueAfter = (flag: string): string | undefined => {
|
|
973
|
+
const index = argv.indexOf(flag)
|
|
974
|
+
const value = index === -1 ? undefined : argv[index + 1]
|
|
975
|
+
if (index !== -1 && (value === undefined || value.startsWith("-"))) {
|
|
976
|
+
throw new Error(`[nifra] ${flag} needs a value`)
|
|
977
|
+
}
|
|
978
|
+
return value
|
|
979
|
+
}
|
|
980
|
+
try {
|
|
981
|
+
const minRaw = valueAfter("--min")
|
|
982
|
+
const min = minRaw === undefined ? undefined : Number(minRaw)
|
|
983
|
+
if (min !== undefined && (!Number.isInteger(min) || min < 0 || min > 4)) {
|
|
984
|
+
throw new Error("[nifra] --min must be an integer level between 0 and 4")
|
|
985
|
+
}
|
|
986
|
+
const changedFiles: string[] = []
|
|
987
|
+
for (let index = 0; index < argv.length; index++) {
|
|
988
|
+
if (argv[index] !== "--file") continue
|
|
989
|
+
const file = argv[index + 1]
|
|
990
|
+
if (file === undefined || file.startsWith("-"))
|
|
991
|
+
throw new Error("[nifra] --file needs a path")
|
|
992
|
+
changedFiles.push(file)
|
|
993
|
+
index++
|
|
994
|
+
}
|
|
995
|
+
const { runWorkGraph } = await import("./work-graph.ts")
|
|
996
|
+
if (
|
|
997
|
+
!(await runWorkGraph(process.cwd(), {
|
|
998
|
+
json: argv.includes("--json"),
|
|
999
|
+
...(changedFiles.length === 0 ? {} : { changedFiles }),
|
|
1000
|
+
...(min === undefined ? {} : { minLevel: min }),
|
|
1001
|
+
}))
|
|
1002
|
+
)
|
|
1003
|
+
process.exitCode = 1
|
|
1004
|
+
} catch (err) {
|
|
1005
|
+
console.error(formatCliError(err))
|
|
1006
|
+
process.exitCode = 1
|
|
1007
|
+
}
|
|
1008
|
+
return
|
|
1009
|
+
}
|
|
789
1010
|
// `upgrade` is a pure cwd file-transformer (package.json pins + import moves) driven by a per-release
|
|
790
1011
|
// recipe, then verified with `nifra check`. Dispatch before the eager `loadApp` - it must run on any
|
|
791
1012
|
// repo (API-only, not built, or mid-upgrade with edits that don't yet typecheck under dry-run).
|