@nifrajs/cli 1.6.0 → 1.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 +33 -1
- package/dist/assure.d.ts +11 -0
- package/dist/assure.d.ts.map +1 -0
- package/dist/assure.js +67 -0
- package/dist/assure.js.map +1 -0
- package/dist/capabilities-tool.d.ts +33 -0
- package/dist/capabilities-tool.d.ts.map +1 -0
- package/dist/capabilities-tool.js +320 -0
- package/dist/capabilities-tool.js.map +1 -0
- package/dist/check.d.ts +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +136 -11
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +199 -12
- package/dist/cli.js.map +1 -1
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +146 -10
- package/dist/doctor.js.map +1 -1
- package/dist/levels-tool.d.ts +37 -0
- package/dist/levels-tool.d.ts.map +1 -0
- package/dist/levels-tool.js +183 -0
- package/dist/levels-tool.js.map +1 -0
- package/dist/manifest-tool.d.ts +15 -0
- package/dist/manifest-tool.d.ts.map +1 -0
- package/dist/manifest-tool.js +67 -0
- package/dist/manifest-tool.js.map +1 -0
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +31 -0
- package/dist/mcp.js.map +1 -1
- package/dist/recipes/1.8.0.d.ts +12 -0
- package/dist/recipes/1.8.0.d.ts.map +1 -0
- package/dist/recipes/1.8.0.js +20 -0
- package/dist/recipes/1.8.0.js.map +1 -0
- package/dist/recipes/index.d.ts +28 -0
- package/dist/recipes/index.d.ts.map +1 -0
- package/dist/recipes/index.js +27 -0
- package/dist/recipes/index.js.map +1 -0
- package/dist/upgrade.d.ts +82 -0
- package/dist/upgrade.d.ts.map +1 -0
- package/dist/upgrade.js +220 -0
- package/dist/upgrade.js.map +1 -0
- package/docs/examples.json +12 -0
- package/docs/llms-full.txt +524 -15
- package/docs/types.json +2409 -50
- package/package.json +9 -8
- package/src/assure.ts +88 -0
- package/src/capabilities-tool.ts +406 -0
- package/src/check.ts +155 -12
- package/src/cli.ts +206 -17
- package/src/doctor.ts +179 -11
- package/src/levels-tool.ts +232 -0
- package/src/manifest-tool.ts +103 -0
- package/src/mcp-http.ts +1 -1
- package/src/mcp.ts +37 -0
- package/src/recipes/1.8.0.ts +21 -0
- package/src/recipes/index.ts +52 -0
- package/src/upgrade.ts +269 -0
package/src/check.ts
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
* anything fails. Pure scanners (`scanFetchText`, `scanServerOnlyImports`) are unit-tested.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { readFileSync } from "node:fs"
|
|
20
|
-
import { dirname, isAbsolute, join } from "node:path"
|
|
19
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
20
|
+
import { dirname, isAbsolute, join, resolve } from "node:path"
|
|
21
21
|
import { Glob } from "bun"
|
|
22
22
|
|
|
23
23
|
export interface SourceFinding {
|
|
@@ -195,8 +195,13 @@ function parseSimpleFetchCall(snippet: string): SimpleFetchCall | undefined {
|
|
|
195
195
|
return { path, method, start: match.index, end: close + 1 }
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
function
|
|
199
|
-
return
|
|
198
|
+
function isPotentialBackendSource(code: string): boolean {
|
|
199
|
+
return (
|
|
200
|
+
code.includes("@nifrajs/core") ||
|
|
201
|
+
/(?<![.\w])server\s*\(/.test(code) ||
|
|
202
|
+
/(?:from\s*|import\s*)["']hono["']/.test(code) ||
|
|
203
|
+
/new\s+Hono(?:<[^>]+>)?\s*\(/.test(code)
|
|
204
|
+
)
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
function scanRoutePattern(
|
|
@@ -289,7 +294,7 @@ export function scanFetchText(file: string, content: string): SourceFinding[] {
|
|
|
289
294
|
/** Statically collect simple Nifra route registrations from source, without importing app code. */
|
|
290
295
|
export function scanStaticRouteText(file: string, content: string): StaticRouteFinding[] {
|
|
291
296
|
const code = stripComments(content)
|
|
292
|
-
if (!
|
|
297
|
+
if (!isPotentialBackendSource(code)) return []
|
|
293
298
|
return [
|
|
294
299
|
...scanRoutePattern(file, content, code, ROUTE_REGISTRATION_DQ),
|
|
295
300
|
...scanRoutePattern(file, content, code, ROUTE_REGISTRATION_SQ),
|
|
@@ -765,7 +770,11 @@ export interface CheckDiagnostic {
|
|
|
765
770
|
| "server-only-import"
|
|
766
771
|
| "response-route"
|
|
767
772
|
| "undeclared-dependency"
|
|
773
|
+
| "duplicate-install"
|
|
768
774
|
| "server-manifest-drift"
|
|
775
|
+
| "manifest-drift"
|
|
776
|
+
| "capability-assurance"
|
|
777
|
+
| "capability-config"
|
|
769
778
|
/** `error` fails the gate (a real contract break); `warning` is advisory — surfaced to the agent but
|
|
770
779
|
* does NOT fail `nifra check`, for patterns that are sometimes intentional (a route returning a raw
|
|
771
780
|
* `Response`, which silently drops the typed client to `data: never` but is valid for files/redirects). */
|
|
@@ -826,6 +835,10 @@ const UNDECLARED_DEP_HINT =
|
|
|
826
835
|
"imported package is not declared in package.json dependencies — run bun add to declare it"
|
|
827
836
|
const MANIFEST_DRIFT_HINT =
|
|
828
837
|
"server-manifest.ts is out of sync with routes/ — re-run the build to regenerate it (a disk-less worker bakes this route table, so the drift is a silent edge break), then commit it"
|
|
838
|
+
const TRUST_MANIFEST_DRIFT_HINT =
|
|
839
|
+
"nifra.manifest.json is missing, invalid, or out of sync — run `nifra manifest emit`, review it, and commit the regenerated trust artifact"
|
|
840
|
+
const CAPABILITY_HINT =
|
|
841
|
+
"effect/capability assurance failed — align the route declaration with approved adapter provenance; never bypass an owned effect seam"
|
|
829
842
|
|
|
830
843
|
function oneLineDiff(file: string, line: number, before: string, after: string): string {
|
|
831
844
|
return `--- ${file}:${line}\n+++ ${file}:${line}\n@@\n-${before}\n+${after}`
|
|
@@ -1127,6 +1140,24 @@ export async function collectCheckResult(
|
|
|
1127
1140
|
},
|
|
1128
1141
|
})
|
|
1129
1142
|
}
|
|
1143
|
+
for (const finding of dr.duplicateInstalls) {
|
|
1144
|
+
const copies = finding.copies.map((copy) => `${copy.version} at ${copy.path}`).join("; ")
|
|
1145
|
+
diagnostics.push({
|
|
1146
|
+
rule: "duplicate-install",
|
|
1147
|
+
severity: "error",
|
|
1148
|
+
message: `${finding.package} resolves to multiple physical copies (${copies}) - module identity is unsafe`,
|
|
1149
|
+
fix: "align dependency ranges and reinstall from the workspace root",
|
|
1150
|
+
suggestion: {
|
|
1151
|
+
kind: "manual",
|
|
1152
|
+
title: `Deduplicate ${finding.package}`,
|
|
1153
|
+
steps: [
|
|
1154
|
+
"Align workspace dependency and peer ranges on one compatible version.",
|
|
1155
|
+
"Remove stale nested installs and run the package manager from the workspace root.",
|
|
1156
|
+
"Run `nifra doctor` again; do not suppress same-version duplicate paths.",
|
|
1157
|
+
],
|
|
1158
|
+
},
|
|
1159
|
+
})
|
|
1160
|
+
}
|
|
1130
1161
|
}
|
|
1131
1162
|
// #7: a committed server-manifest.ts that drifted from routes/ — name the exact missing/extra routes.
|
|
1132
1163
|
for (const f of manifestDrift) {
|
|
@@ -1150,19 +1181,127 @@ export async function collectCheckResult(
|
|
|
1150
1181
|
})
|
|
1151
1182
|
}
|
|
1152
1183
|
|
|
1184
|
+
// G+B+D+F: when the project opts into capability assurance, `nifra check` becomes the static
|
|
1185
|
+
// provenance firewall as well as the typed-contract gate. Loading is explicit/config-owned; projects
|
|
1186
|
+
// without nifra.assurance.ts retain the historical scan and hot path unchanged.
|
|
1187
|
+
const assuranceConfigPath = join(cwd, "nifra.assurance.ts")
|
|
1188
|
+
if (existsSync(assuranceConfigPath)) {
|
|
1189
|
+
try {
|
|
1190
|
+
const { loadAssuranceConfig } = await import("./assure.ts")
|
|
1191
|
+
const config = await loadAssuranceConfig(cwd)
|
|
1192
|
+
let capabilityReport:
|
|
1193
|
+
| Awaited<
|
|
1194
|
+
ReturnType<typeof import("./capabilities-tool.ts")["collectCapabilityProjectReport"]>
|
|
1195
|
+
>["report"]
|
|
1196
|
+
| undefined
|
|
1197
|
+
if (config.capabilities !== undefined) {
|
|
1198
|
+
const { collectCapabilityProjectReport } = await import("./capabilities-tool.ts")
|
|
1199
|
+
const project = await collectCapabilityProjectReport(
|
|
1200
|
+
cwd,
|
|
1201
|
+
config.source,
|
|
1202
|
+
config.capabilities,
|
|
1203
|
+
)
|
|
1204
|
+
capabilityReport = project.report
|
|
1205
|
+
for (const finding of project.report.findings) {
|
|
1206
|
+
const violation =
|
|
1207
|
+
finding.code === "forbidden-effect-import"
|
|
1208
|
+
? project.violations.find(
|
|
1209
|
+
(candidate) =>
|
|
1210
|
+
candidate.method === finding.method && candidate.path === finding.path,
|
|
1211
|
+
)
|
|
1212
|
+
: undefined
|
|
1213
|
+
diagnostics.push({
|
|
1214
|
+
rule: "capability-assurance",
|
|
1215
|
+
severity: "error",
|
|
1216
|
+
...(violation !== undefined ? { file: violation.module, chain: violation.chain } : {}),
|
|
1217
|
+
message: `${finding.message} — ${CAPABILITY_HINT}`,
|
|
1218
|
+
fix: CAPABILITY_HINT,
|
|
1219
|
+
suggestion: {
|
|
1220
|
+
kind: "manual",
|
|
1221
|
+
title: "Restore declared effect provenance",
|
|
1222
|
+
steps: [
|
|
1223
|
+
"Route effectful work through an import listed in capabilities.provenance.imports.",
|
|
1224
|
+
"Declare the exact capability token on the route; do not widen unrelated routes in the same file.",
|
|
1225
|
+
"For domain writes, add the request-idempotency or durable-command adapter required by the capability definition.",
|
|
1226
|
+
"Run `nifra capabilities snapshot` only after assurance passes, then review the lockfile diff.",
|
|
1227
|
+
],
|
|
1228
|
+
},
|
|
1229
|
+
})
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
if (config.manifest !== undefined) {
|
|
1233
|
+
const { evaluateRouteAssurance } = await import("@nifrajs/core/assurance")
|
|
1234
|
+
const { buildNifraManifest, parseNifraManifest, serializeNifraManifest } = await import(
|
|
1235
|
+
"@nifrajs/core/manifest"
|
|
1236
|
+
)
|
|
1237
|
+
const assurance = evaluateRouteAssurance(config.source, config.policy)
|
|
1238
|
+
const path = resolve(cwd, config.manifest.path ?? "nifra.manifest.json")
|
|
1239
|
+
let message: string | undefined
|
|
1240
|
+
if (!assurance.ok || (capabilityReport !== undefined && !capabilityReport.ok)) {
|
|
1241
|
+
message =
|
|
1242
|
+
"the configured assurance policy is failing, so a trusted manifest cannot be built"
|
|
1243
|
+
} else if (!existsSync(path)) {
|
|
1244
|
+
message = "the configured trust manifest is missing"
|
|
1245
|
+
} else {
|
|
1246
|
+
try {
|
|
1247
|
+
const current = await buildNifraManifest({
|
|
1248
|
+
source: config.source,
|
|
1249
|
+
assurance,
|
|
1250
|
+
...(capabilityReport !== undefined ? { capabilities: capabilityReport } : {}),
|
|
1251
|
+
})
|
|
1252
|
+
const storedText = await Bun.file(path).text()
|
|
1253
|
+
const stored = await parseNifraManifest(storedText, path)
|
|
1254
|
+
const expectedText = `${serializeNifraManifest(current)}\n`
|
|
1255
|
+
if (storedText !== expectedText || stored.contentHash !== current.contentHash) {
|
|
1256
|
+
message = "the configured trust manifest does not match live route reflection"
|
|
1257
|
+
}
|
|
1258
|
+
} catch (error) {
|
|
1259
|
+
message = `the configured trust manifest is invalid: ${error instanceof Error ? error.message : String(error)}`
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (message !== undefined) {
|
|
1263
|
+
diagnostics.push({
|
|
1264
|
+
rule: "manifest-drift",
|
|
1265
|
+
severity: "error",
|
|
1266
|
+
file: path,
|
|
1267
|
+
message: `${message} — ${TRUST_MANIFEST_DRIFT_HINT}`,
|
|
1268
|
+
fix: TRUST_MANIFEST_DRIFT_HINT,
|
|
1269
|
+
suggestion: {
|
|
1270
|
+
kind: "command",
|
|
1271
|
+
title: "Regenerate the signed-manifest input artifact",
|
|
1272
|
+
command: ["nifra", "manifest", "emit"],
|
|
1273
|
+
steps: [
|
|
1274
|
+
"Review the route, assurance, capability, and classification delta before committing it.",
|
|
1275
|
+
],
|
|
1276
|
+
},
|
|
1277
|
+
})
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
} catch (err) {
|
|
1281
|
+
diagnostics.push({
|
|
1282
|
+
rule: "capability-config",
|
|
1283
|
+
severity: "error",
|
|
1284
|
+
file: "nifra.assurance.ts",
|
|
1285
|
+
message: `capability assurance config could not be evaluated: ${err instanceof Error ? err.message : String(err)}`,
|
|
1286
|
+
suggestion: {
|
|
1287
|
+
kind: "manual",
|
|
1288
|
+
title: "Repair the assurance config",
|
|
1289
|
+
steps: [
|
|
1290
|
+
"Ensure nifra.assurance.ts default-exports defineAssuranceConfig({ source, policy, capabilities }).",
|
|
1291
|
+
"Fix configuration/import errors; the provenance firewall fails closed when its policy cannot load.",
|
|
1292
|
+
],
|
|
1293
|
+
},
|
|
1294
|
+
})
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1153
1298
|
// Cap the diagnostics when asked (the MCP path), so a project with thousands of findings can't return a
|
|
1154
1299
|
// message that breaks the stdio transport. `ok` reflects the FULL set — truncation never flips it.
|
|
1155
1300
|
const total = diagnostics.length
|
|
1156
1301
|
const max = opts.maxDiagnostics
|
|
1157
1302
|
const shown = max !== undefined && total > max ? diagnostics.slice(0, max) : diagnostics
|
|
1158
1303
|
return {
|
|
1159
|
-
ok:
|
|
1160
|
-
tc.ok &&
|
|
1161
|
-
fetches.length === 0 &&
|
|
1162
|
-
untypedClients.length === 0 &&
|
|
1163
|
-
serverImports.length === 0 &&
|
|
1164
|
-
manifestDrift.length === 0 &&
|
|
1165
|
-
(!dr.ran || dr.findings.length === 0),
|
|
1304
|
+
ok: !diagnostics.some((diagnostic) => diagnostic.severity === "error"),
|
|
1166
1305
|
typecheck: tc.ran ? (tc.ok ? "pass" : "fail") : "skipped",
|
|
1167
1306
|
diagnostics: shown,
|
|
1168
1307
|
...(shown.length < total ? { truncated: { shown: shown.length, total } } : {}),
|
|
@@ -1197,7 +1336,11 @@ export async function runCheck(
|
|
|
1197
1336
|
["server-only-import", "server-only import in a route module"],
|
|
1198
1337
|
["response-route", "route returns a raw Response (typed client → data: never)"],
|
|
1199
1338
|
["undeclared-dependency", "undeclared dependency in package.json"],
|
|
1339
|
+
["duplicate-install", "duplicate identity-sensitive dependency install"],
|
|
1200
1340
|
["server-manifest-drift", "server-manifest.ts drifted from routes/"],
|
|
1341
|
+
["manifest-drift", "versioned trust manifest drift"],
|
|
1342
|
+
["capability-assurance", "effect/capability assurance"],
|
|
1343
|
+
["capability-config", "capability assurance config"],
|
|
1201
1344
|
] as const) {
|
|
1202
1345
|
const ds = counts(rule)
|
|
1203
1346
|
if (rule === "response-route") {
|
package/src/cli.ts
CHANGED
|
@@ -59,7 +59,7 @@ Usage:
|
|
|
59
59
|
agent: nifra_context, nifra_routes (routes+schemas as JSON),
|
|
60
60
|
nifra_run (backend), nifra_render (SSR a page), nifra_docs,
|
|
61
61
|
nifra_example (verified snippets), nifra_scaffold (route→file),
|
|
62
|
-
nifra_check (drift gate + fixes), nifra_doctor (
|
|
62
|
+
nifra_check (drift gate + fixes), nifra_doctor (deps + duplicate installs).
|
|
63
63
|
nifra docs-mcp [--port <n>] Serve the PUBLIC docs MCP over HTTP (nifra_docs + nifra_example) —
|
|
64
64
|
self-host on a VPS so any remote agent can learn nifra. Default :8787.
|
|
65
65
|
nifra check [--json] [--lints-only] Gate: typecheck + lints (hand-rolled fetch(), untyped client("…"),
|
|
@@ -72,9 +72,34 @@ Usage:
|
|
|
72
72
|
request field or a removed response field breaks; widening a
|
|
73
73
|
request enum or adding a response field doesn't) and fails
|
|
74
74
|
closed. Exits non-zero on any breaking change — run it in CI.
|
|
75
|
-
nifra
|
|
76
|
-
|
|
75
|
+
nifra assure [--config <file>] [--json] Route-assurance gate: load nifra.assurance.ts, classify every
|
|
76
|
+
reflected backend route, and fail when required enforcement
|
|
77
|
+
evidence is missing/forbidden or a route is unclassified.
|
|
78
|
+
nifra capabilities snapshot [--out <file>] [--config <file>]
|
|
79
|
+
Write the deterministic, token-only capability lockfile, but
|
|
80
|
+
only after provenance + idempotency assurance passes.
|
|
81
|
+
nifra capabilities check [--lockfile <file>] [--config <file>] [--json]
|
|
82
|
+
CI gate: fail on raw effect-import bypasses, declaration/evidence
|
|
83
|
+
drift, unsafe GET/HEAD writes, idempotency gaps, or lockfile drift.
|
|
84
|
+
nifra manifest emit [--out <file>] [--config <file>] [--sign <key-ref>]
|
|
85
|
+
Emit a deterministic route trust artifact after assurance passes;
|
|
86
|
+
optionally write an Ed25519 signature via the configured KMS signer.
|
|
87
|
+
nifra manifest diff <before> <after> [--json]
|
|
88
|
+
Hash-verify and compare two manifests; fail on contract, assurance,
|
|
89
|
+
capability, or response-classification regressions.
|
|
90
|
+
nifra levels [--json] [--min <n>] [--config <file>]
|
|
91
|
+
Compute the verification ladder (L0 typed contract → L1 route
|
|
92
|
+
assurance → L2 capability lockfile → L3 route manifest → L4
|
|
93
|
+
invariant-tested); --min <n> fails the exit code below level n.
|
|
94
|
+
nifra doctor [--json] [--auto-fix] Flag undeclared imports and multiple physical copies of
|
|
95
|
+
@nifrajs/core/React identity-sensitive dependencies;
|
|
77
96
|
--auto-fix writes safe local-version dependency fixes.
|
|
97
|
+
nifra upgrade <version> Run the per-release upgrade recipe for <version>: sweep every
|
|
98
|
+
[--write] [--no-verify] matching dependency pin (preserving ^/~/exact) + apply exact
|
|
99
|
+
[--list] [--json] import moves across the workspace, then verify with nifra check.
|
|
100
|
+
Dry-run by default; --write applies then verifies (--no-verify
|
|
101
|
+
to skip). --list shows available targets. Fail-closed on an
|
|
102
|
+
unknown version. Deterministic + idempotent.
|
|
78
103
|
nifra port [--target <t>] [--json] Portability linter: print a feature × deploy-target capability
|
|
79
104
|
[--ci] [--strict] matrix (in-memory stores, in-process cron/WebSocket, Bun/Deno
|
|
80
105
|
globals, node: builtins) with file:line evidence. --target auto-
|
|
@@ -89,7 +114,7 @@ Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with
|
|
|
89
114
|
\`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).`
|
|
90
115
|
|
|
91
116
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
92
|
-
const CLI_VERSION = "1.
|
|
117
|
+
const CLI_VERSION = "1.12.0"
|
|
93
118
|
|
|
94
119
|
// A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
|
|
95
120
|
const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
|
|
@@ -181,24 +206,28 @@ async function buildForTarget(app: LoadedApp, target: string, report: boolean):
|
|
|
181
206
|
if (report) console.log(`\n${renderSizeReport(result.size)}`)
|
|
182
207
|
}
|
|
183
208
|
|
|
184
|
-
/** Build
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
209
|
+
/** Build the app FACTORY for the `static` target's prerender pass. Mirrors `nifra start`: register the
|
|
210
|
+
* framework's SSR Bun plugins (so `.vue`/`.svelte`/Solid routes import) once, then return a factory that
|
|
211
|
+
* `createWebApp`s the app for the client build's manifest. The client entry MUST be the real content-hashed
|
|
212
|
+
* bundle (`client.entry`) — it's the hydration `<script src>` the prerendered HTML emits, so a placeholder
|
|
213
|
+
* would 404 and the pages would render but never hydrate (inert controls). Styles/route-preload are wired
|
|
214
|
+
* from the same manifest so the static HTML matches what `nifra start` serves. */
|
|
188
215
|
async function buildPrerenderApp(
|
|
189
216
|
app: LoadedApp,
|
|
190
|
-
): Promise<{ fetch(req: Request): Response | Promise<Response> }> {
|
|
217
|
+
): Promise<(client: BuiltManifest) => { fetch(req: Request): Response | Promise<Response> }> {
|
|
191
218
|
const { plugin } = await import("bun")
|
|
192
219
|
const { framework: fw, routesDir, backend } = app
|
|
193
220
|
for (const p of asBunPlugins(await resolvePlugins(fw.serverPlugins))) plugin(p)
|
|
194
|
-
return
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
221
|
+
return (client) =>
|
|
222
|
+
createWebApp({
|
|
223
|
+
adapter: asAdapter(fw.adapter),
|
|
224
|
+
manifest: discoverRoutes(routesDir),
|
|
225
|
+
clientEntry: client.entry,
|
|
226
|
+
...(client.routes ? { routePreload: client.routes } : {}),
|
|
227
|
+
...(client.css ? { styles: client.css } : {}),
|
|
228
|
+
...(client.routeStyles ? { routeStyles: client.routeStyles } : {}),
|
|
229
|
+
...apiOf(backend),
|
|
230
|
+
})
|
|
202
231
|
}
|
|
203
232
|
|
|
204
233
|
interface BuiltManifest {
|
|
@@ -371,6 +400,166 @@ async function main(): Promise<void> {
|
|
|
371
400
|
}
|
|
372
401
|
return
|
|
373
402
|
}
|
|
403
|
+
// Route assurance intentionally loads only its explicit config (which imports the backend it audits),
|
|
404
|
+
// not the full SSR app. Like snapshot/diff, this works for API-only projects.
|
|
405
|
+
if (command === "assure") {
|
|
406
|
+
const { runAssurance } = await import("./assure.ts")
|
|
407
|
+
const configIdx = argv.indexOf("--config")
|
|
408
|
+
const config = configIdx !== -1 ? argv[configIdx + 1] : undefined
|
|
409
|
+
if (configIdx !== -1 && (config === undefined || config.startsWith("-"))) {
|
|
410
|
+
console.error("[nifra] --config needs a file path")
|
|
411
|
+
process.exitCode = 1
|
|
412
|
+
return
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
if (
|
|
416
|
+
!(await runAssurance(process.cwd(), {
|
|
417
|
+
json: argv.includes("--json"),
|
|
418
|
+
...(config !== undefined ? { config } : {}),
|
|
419
|
+
}))
|
|
420
|
+
)
|
|
421
|
+
process.exitCode = 1
|
|
422
|
+
} catch (err) {
|
|
423
|
+
console.error(err instanceof Error ? err.message : String(err))
|
|
424
|
+
process.exitCode = 1
|
|
425
|
+
}
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
if (command === "capabilities") {
|
|
429
|
+
const action = argv[1]
|
|
430
|
+
const valueAfter = (flag: string): string | undefined => {
|
|
431
|
+
const index = argv.indexOf(flag)
|
|
432
|
+
const value = index === -1 ? undefined : argv[index + 1]
|
|
433
|
+
if (index !== -1 && (value === undefined || value.startsWith("-"))) {
|
|
434
|
+
throw new Error(`[nifra] ${flag} needs a file path`)
|
|
435
|
+
}
|
|
436
|
+
return value
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
const { runCapabilityCheck, runCapabilitySnapshot } = await import("./capabilities-tool.ts")
|
|
440
|
+
const config = valueAfter("--config")
|
|
441
|
+
if (action === "snapshot") {
|
|
442
|
+
const out = valueAfter("--out")
|
|
443
|
+
if (
|
|
444
|
+
!(await runCapabilitySnapshot(process.cwd(), {
|
|
445
|
+
...(config !== undefined ? { config } : {}),
|
|
446
|
+
...(out !== undefined ? { out } : {}),
|
|
447
|
+
}))
|
|
448
|
+
)
|
|
449
|
+
process.exitCode = 1
|
|
450
|
+
} else if (action === "check") {
|
|
451
|
+
const lockfile = valueAfter("--lockfile")
|
|
452
|
+
if (
|
|
453
|
+
!(await runCapabilityCheck(process.cwd(), {
|
|
454
|
+
...(config !== undefined ? { config } : {}),
|
|
455
|
+
...(lockfile !== undefined ? { lockfile } : {}),
|
|
456
|
+
json: argv.includes("--json"),
|
|
457
|
+
}))
|
|
458
|
+
)
|
|
459
|
+
process.exitCode = 1
|
|
460
|
+
} else {
|
|
461
|
+
throw new Error("[nifra] capabilities needs `snapshot` or `check`")
|
|
462
|
+
}
|
|
463
|
+
} catch (err) {
|
|
464
|
+
console.error(err instanceof Error ? err.message : String(err))
|
|
465
|
+
process.exitCode = 1
|
|
466
|
+
}
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
if (command === "manifest") {
|
|
470
|
+
const action = argv[1]
|
|
471
|
+
const valueAfter = (flag: string): string | undefined => {
|
|
472
|
+
const index = argv.indexOf(flag)
|
|
473
|
+
const value = index === -1 ? undefined : argv[index + 1]
|
|
474
|
+
if (index !== -1 && (value === undefined || value.startsWith("-"))) {
|
|
475
|
+
throw new Error(`[nifra] ${flag} needs a value`)
|
|
476
|
+
}
|
|
477
|
+
return value
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
const { runManifestDiff, runManifestEmit } = await import("./manifest-tool.ts")
|
|
481
|
+
if (action === "emit") {
|
|
482
|
+
const config = valueAfter("--config")
|
|
483
|
+
const out = valueAfter("--out")
|
|
484
|
+
const sign = valueAfter("--sign")
|
|
485
|
+
if (
|
|
486
|
+
!(await runManifestEmit(process.cwd(), {
|
|
487
|
+
...(config !== undefined ? { config } : {}),
|
|
488
|
+
...(out !== undefined ? { out } : {}),
|
|
489
|
+
...(sign !== undefined ? { sign } : {}),
|
|
490
|
+
}))
|
|
491
|
+
)
|
|
492
|
+
process.exitCode = 1
|
|
493
|
+
} else if (action === "diff") {
|
|
494
|
+
const paths = argv.slice(2).filter((arg) => !arg.startsWith("-"))
|
|
495
|
+
if (paths.length !== 2) throw new Error("[nifra] manifest diff needs <before> <after>")
|
|
496
|
+
if (
|
|
497
|
+
!(await runManifestDiff(process.cwd(), paths[0]!, paths[1]!, {
|
|
498
|
+
json: argv.includes("--json"),
|
|
499
|
+
}))
|
|
500
|
+
)
|
|
501
|
+
process.exitCode = 1
|
|
502
|
+
} else {
|
|
503
|
+
throw new Error("[nifra] manifest needs `emit` or `diff`")
|
|
504
|
+
}
|
|
505
|
+
} catch (err) {
|
|
506
|
+
console.error(err instanceof Error ? err.message : String(err))
|
|
507
|
+
process.exitCode = 1
|
|
508
|
+
}
|
|
509
|
+
return
|
|
510
|
+
}
|
|
511
|
+
if (command === "levels") {
|
|
512
|
+
const valueAfter = (flag: string): string | undefined => {
|
|
513
|
+
const index = argv.indexOf(flag)
|
|
514
|
+
const value = index === -1 ? undefined : argv[index + 1]
|
|
515
|
+
if (index !== -1 && (value === undefined || value.startsWith("-"))) {
|
|
516
|
+
throw new Error(`[nifra] ${flag} needs a value`)
|
|
517
|
+
}
|
|
518
|
+
return value
|
|
519
|
+
}
|
|
520
|
+
try {
|
|
521
|
+
const { runLevels } = await import("./levels-tool.ts")
|
|
522
|
+
const config = valueAfter("--config")
|
|
523
|
+
const minRaw = valueAfter("--min")
|
|
524
|
+
const min = minRaw === undefined ? undefined : Number(minRaw)
|
|
525
|
+
if (min !== undefined && (!Number.isInteger(min) || min < 0 || min > 4)) {
|
|
526
|
+
throw new Error("[nifra] --min must be an integer level between 0 and 4")
|
|
527
|
+
}
|
|
528
|
+
if (
|
|
529
|
+
!(await runLevels(process.cwd(), {
|
|
530
|
+
json: argv.includes("--json"),
|
|
531
|
+
...(config !== undefined ? { config } : {}),
|
|
532
|
+
...(min !== undefined ? { min } : {}),
|
|
533
|
+
}))
|
|
534
|
+
)
|
|
535
|
+
process.exitCode = 1
|
|
536
|
+
} catch (err) {
|
|
537
|
+
console.error(err instanceof Error ? err.message : String(err))
|
|
538
|
+
process.exitCode = 1
|
|
539
|
+
}
|
|
540
|
+
return
|
|
541
|
+
}
|
|
542
|
+
// `upgrade` is a pure cwd file-transformer (package.json pins + import moves) driven by a per-release
|
|
543
|
+
// recipe, then verified with `nifra check`. Dispatch before the eager `loadApp` — it must run on any
|
|
544
|
+
// repo (API-only, not built, or mid-upgrade with edits that don't yet typecheck under dry-run).
|
|
545
|
+
if (command === "upgrade") {
|
|
546
|
+
const { runUpgrade } = await import("./upgrade.ts")
|
|
547
|
+
const version = argv.slice(1).find((arg) => !arg.startsWith("-"))
|
|
548
|
+
try {
|
|
549
|
+
const ok = await runUpgrade(process.cwd(), {
|
|
550
|
+
...(version !== undefined ? { version } : {}),
|
|
551
|
+
write: argv.includes("--write"),
|
|
552
|
+
json: argv.includes("--json"),
|
|
553
|
+
list: argv.includes("--list"),
|
|
554
|
+
verify: !argv.includes("--no-verify"),
|
|
555
|
+
})
|
|
556
|
+
if (!ok) process.exitCode = 1
|
|
557
|
+
} catch (err) {
|
|
558
|
+
console.error(err instanceof Error ? err.message : String(err))
|
|
559
|
+
process.exitCode = 1
|
|
560
|
+
}
|
|
561
|
+
return
|
|
562
|
+
}
|
|
374
563
|
// `port` is a pure cwd-based portability linter (scans source, doesn't run the app) — like `check`/
|
|
375
564
|
// `doctor`, dispatch before the eager `loadApp` so it runs on an API-only / not-yet-built project.
|
|
376
565
|
if (command === "port") {
|