@nifrajs/cli 2.2.0 → 2.4.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 +25 -25
- package/dist/assure.d.ts +2 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +17 -10
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +7 -0
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +68 -16
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check.d.ts +61 -22
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +336 -87
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +30 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +148 -39
- package/dist/cli.js.map +1 -1
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.js +7 -7
- package/dist/docs-search.d.ts +3 -3
- package/dist/docs-search.js +6 -6
- package/dist/doctor.d.ts +3 -3
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +12 -8
- package/dist/doctor.js.map +1 -1
- package/dist/examples.d.ts +5 -5
- package/dist/examples.js +5 -5
- package/dist/init-agents.d.ts +14 -14
- package/dist/init-agents.js +14 -14
- package/dist/internal/typescript-import.d.ts +24 -0
- package/dist/internal/typescript-import.d.ts.map +1 -0
- package/dist/internal/typescript-import.js +32 -0
- package/dist/internal/typescript-import.js.map +1 -0
- package/dist/introspect.d.ts +12 -12
- package/dist/introspect.js +24 -24
- package/dist/learn.d.ts +32 -0
- package/dist/learn.d.ts.map +1 -0
- package/dist/learn.js +127 -0
- package/dist/learn.js.map +1 -0
- package/dist/levels-tool.d.ts +8 -8
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -30
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +6 -6
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +15 -7
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +4 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts +14 -0
- package/dist/mcp-app-tool.d.ts.map +1 -0
- package/dist/mcp-app-tool.js +80 -0
- package/dist/mcp-app-tool.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts +1 -1
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +25 -9
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-http.d.ts +14 -10
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +30 -16
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-protocol.d.ts +2 -2
- package/dist/mcp-protocol.js +2 -2
- package/dist/mcp-render.d.ts +3 -3
- package/dist/mcp-render.js +6 -6
- package/dist/mcp-run.d.ts +2 -2
- package/dist/mcp-run.js +2 -2
- package/dist/mcp.d.ts +28 -14
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +263 -45
- package/dist/mcp.js.map +1 -1
- package/dist/openapi-tool.d.ts +1 -1
- package/dist/openapi-tool.js +2 -2
- package/dist/pipeline-guard.d.ts +1 -1
- package/dist/pipeline-guard.js +1 -1
- package/dist/port.d.ts +9 -9
- package/dist/port.js +27 -27
- package/dist/recipes/1.8.0.d.ts +2 -2
- package/dist/recipes/1.8.0.js +5 -5
- package/dist/recipes/index.d.ts +2 -2
- package/dist/recipes/index.js +1 -1
- package/dist/scaffold.d.ts +3 -3
- package/dist/scaffold.js +8 -8
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.d.ts.map +1 -0
- package/dist/sync-routes.js +55 -0
- package/dist/sync-routes.js.map +1 -0
- package/dist/types-search.d.ts +5 -5
- package/dist/types-search.js +7 -7
- package/dist/upgrade.d.ts +20 -9
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +117 -19
- package/dist/upgrade.js.map +1 -1
- package/dist/verification.d.ts +51 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +80 -0
- package/dist/verification.js.map +1 -0
- package/docs/examples.json +174 -90
- package/docs/llms-full.txt +1771 -748
- package/docs/types.json +2027 -3089
- package/package.json +22 -11
- package/src/assure.ts +17 -18
- package/src/capabilities-tool.ts +82 -16
- package/src/check.ts +410 -104
- package/src/cli.ts +161 -41
- package/src/diff-tool.ts +8 -8
- package/src/docs-search.ts +6 -6
- package/src/doctor.ts +18 -10
- package/src/examples.ts +7 -7
- package/src/init-agents.ts +17 -17
- package/src/internal/typescript-import.ts +33 -0
- package/src/introspect.ts +28 -28
- package/src/learn.ts +152 -0
- package/src/levels-tool.ts +37 -31
- package/src/load.ts +21 -12
- package/src/manifest-tool.ts +4 -2
- package/src/mcp-app-tool.ts +86 -0
- package/src/mcp-docs-tools.ts +26 -9
- package/src/mcp-http.ts +31 -18
- package/src/mcp-protocol.ts +2 -2
- package/src/mcp-render.ts +6 -6
- package/src/mcp-run.ts +2 -2
- package/src/mcp.ts +319 -45
- package/src/openapi-tool.ts +2 -2
- package/src/pipeline-guard.ts +1 -1
- package/src/port.ts +31 -31
- package/src/recipes/1.8.0.ts +5 -5
- package/src/recipes/index.ts +2 -2
- package/src/scaffold.ts +8 -8
- package/src/sync-routes.ts +69 -0
- package/src/types-search.ts +8 -8
- package/src/upgrade.ts +137 -19
- package/src/verification.ts +123 -0
package/src/upgrade.ts
CHANGED
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `nifra upgrade <version>`
|
|
2
|
+
* `nifra upgrade <version>` - an executable, per-release upgrade runner. Framework and shared-package
|
|
3
3
|
* bumps otherwise spread the same mechanical edits across every consuming app by hand; a recipe turns
|
|
4
4
|
* them into `detect → transform → verify`:
|
|
5
5
|
*
|
|
6
|
-
* 1. **pin sweep**
|
|
6
|
+
* 1. **pin sweep** - set every matching dependency to the target version across the workspace's
|
|
7
7
|
* package.json files, preserving the caret/tilde/exact style and skipping `workspace:`/`link:` specs.
|
|
8
|
-
* 2. **dependency moves**
|
|
8
|
+
* 2. **dependency moves** - replace removed packages with their supported successor without
|
|
9
9
|
* reserializing package.json or leaving duplicate dependency keys.
|
|
10
|
-
* 3. **import moves**
|
|
11
|
-
* 4. **verify**
|
|
10
|
+
* 3. **import moves** - rewrite exact import specifiers to their updated module paths.
|
|
11
|
+
* 4. **verify** - reuse the existing `nifra check` gate; no new verification surface.
|
|
12
12
|
*
|
|
13
13
|
* Dry-run by default (prints the plan, writes nothing); `--write` applies. Fail-closed on an unknown
|
|
14
14
|
* target version or a missing package.json, and deterministic (same repo + target → same edits).
|
|
15
15
|
*
|
|
16
16
|
* Deliberately NOT a codemod engine: transforms are string/specifier-level only. Structural (AST)
|
|
17
|
-
* transforms are a future addition
|
|
17
|
+
* transforms are a future addition - a recipe that needs one is the signal to add the engine, not before.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { readFileSync, writeFileSync } from "node:fs"
|
|
@@ -33,6 +33,8 @@ export interface UpgradeOptions {
|
|
|
33
33
|
readonly list?: boolean
|
|
34
34
|
/** After `--write`, run `nifra check` and fail the command if it fails. Default true. */
|
|
35
35
|
readonly verify?: boolean
|
|
36
|
+
/** Permit a target BELOW the installed version (a rollback). Default false → fail-closed. */
|
|
37
|
+
readonly allowDowngrade?: boolean
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export interface PinChange {
|
|
@@ -65,6 +67,8 @@ export interface UpgradePlan {
|
|
|
65
67
|
readonly pins: readonly PinChange[]
|
|
66
68
|
readonly dependencyMoves: readonly DependencyMoveChange[]
|
|
67
69
|
readonly importMoves: readonly ImportChange[]
|
|
70
|
+
/** Pins refused because the target is BELOW the installed version (a rollback). Empty when allowed. */
|
|
71
|
+
readonly downgrades: readonly PinChange[]
|
|
68
72
|
readonly notes: readonly string[]
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -75,10 +79,10 @@ const DEP_FIELDS = [
|
|
|
75
79
|
"optionalDependencies",
|
|
76
80
|
] as const
|
|
77
81
|
|
|
78
|
-
// Only a bare semver spec is rewritten. Anything else
|
|
79
|
-
// urls, "*", "latest", or a multi-part range
|
|
82
|
+
// Only a bare semver spec is rewritten. Anything else - workspace:*, link:/file:, npm: aliases, git
|
|
83
|
+
// urls, "*", "latest", or a multi-part range - is intentionally left untouched (skipped, not guessed).
|
|
80
84
|
const SEMVER_SPEC =
|
|
81
|
-
/^([\^~]|>=|<=|>|<|=)?\s*(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/
|
|
85
|
+
/^([\^~]|>=|<=|>|<|=)?\s*(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
|
|
82
86
|
|
|
83
87
|
/**
|
|
84
88
|
* Rewrite a dependency version spec to `toVersion`, preserving the range operator (`^`, `~`, …).
|
|
@@ -92,25 +96,92 @@ export function rewriteVersionSpec(spec: string, toVersion: string): string | nu
|
|
|
92
96
|
return next === spec ? null : next
|
|
93
97
|
}
|
|
94
98
|
|
|
99
|
+
/** Numeric core (major, minor, patch) of a semver spec, ignoring the range operator + prerelease/build. */
|
|
100
|
+
export function specVersionTuple(spec: string): readonly [number, number, number] | null {
|
|
101
|
+
const match = SEMVER_SPEC.exec(spec.trim())
|
|
102
|
+
if (!match) return null
|
|
103
|
+
return [Number(match[2]), Number(match[3]), Number(match[4])]
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface ParsedSpecVersion {
|
|
107
|
+
readonly core: readonly [number, number, number]
|
|
108
|
+
readonly prerelease: readonly (number | string)[]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseSpecVersion(spec: string): ParsedSpecVersion | null {
|
|
112
|
+
const match = SEMVER_SPEC.exec(spec.trim())
|
|
113
|
+
if (!match) return null
|
|
114
|
+
const prerelease = (match[5] ?? "").split(".")
|
|
115
|
+
return {
|
|
116
|
+
core: [Number(match[2]), Number(match[3]), Number(match[4])],
|
|
117
|
+
prerelease:
|
|
118
|
+
match[5] === undefined
|
|
119
|
+
? []
|
|
120
|
+
: prerelease.map((part) => (/^\d+$/.test(part) ? Number(part) : part)),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Compare two semver cores: <0 when a<b, 0 equal, >0 when a>b. */
|
|
125
|
+
export function compareSemverCore(
|
|
126
|
+
a: readonly [number, number, number],
|
|
127
|
+
b: readonly [number, number, number],
|
|
128
|
+
): number {
|
|
129
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Compare full semver precedence, including prereleases (`2.3.0-beta` < `2.3.0`). */
|
|
133
|
+
export function compareSemverSpec(a: string, b: string): number {
|
|
134
|
+
const left = parseSpecVersion(a)
|
|
135
|
+
const right = parseSpecVersion(b)
|
|
136
|
+
if (left === null || right === null) return 0
|
|
137
|
+
const core = compareSemverCore(left.core, right.core)
|
|
138
|
+
if (core !== 0) return core
|
|
139
|
+
if (left.prerelease.length === 0 || right.prerelease.length === 0) {
|
|
140
|
+
return left.prerelease.length === right.prerelease.length
|
|
141
|
+
? 0
|
|
142
|
+
: left.prerelease.length === 0
|
|
143
|
+
? 1
|
|
144
|
+
: -1
|
|
145
|
+
}
|
|
146
|
+
const length = Math.max(left.prerelease.length, right.prerelease.length)
|
|
147
|
+
for (let i = 0; i < length; i++) {
|
|
148
|
+
const x = left.prerelease[i]
|
|
149
|
+
const y = right.prerelease[i]
|
|
150
|
+
if (x === undefined || y === undefined) return x === undefined ? -1 : 1
|
|
151
|
+
if (typeof x === "number" && typeof y === "number") {
|
|
152
|
+
if (x !== y) return x - y
|
|
153
|
+
} else if (typeof x === "number") return -1
|
|
154
|
+
else if (typeof y === "number") return 1
|
|
155
|
+
else if (x !== y) return x < y ? -1 : 1
|
|
156
|
+
}
|
|
157
|
+
return 0
|
|
158
|
+
}
|
|
159
|
+
|
|
95
160
|
const escapeRegExp = (value: string): string => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
96
161
|
|
|
97
162
|
/**
|
|
98
|
-
* Apply pin rules to one package.json's TEXT (format-preserving
|
|
163
|
+
* Apply pin rules to one package.json's TEXT (format-preserving - targeted string replaces, never a
|
|
99
164
|
* JSON re-serialize that would reorder keys or drop comments-as-formatting). Returns the new text and
|
|
100
165
|
* the changes made.
|
|
101
166
|
*/
|
|
102
167
|
export function pinSweepText(
|
|
103
168
|
text: string,
|
|
104
169
|
rules: readonly { match: string; to: string }[],
|
|
105
|
-
|
|
170
|
+
allowDowngrade = false,
|
|
171
|
+
): {
|
|
172
|
+
text: string
|
|
173
|
+
changes: Array<Omit<PinChange, "file">>
|
|
174
|
+
downgrades: Array<Omit<PinChange, "file">>
|
|
175
|
+
} {
|
|
106
176
|
let parsed: Record<string, unknown>
|
|
107
177
|
try {
|
|
108
178
|
parsed = JSON.parse(text) as Record<string, unknown>
|
|
109
179
|
} catch {
|
|
110
|
-
return { text, changes: [] }
|
|
180
|
+
return { text, changes: [], downgrades: [] }
|
|
111
181
|
}
|
|
112
182
|
let out = text
|
|
113
183
|
const changes: Array<Omit<PinChange, "file">> = []
|
|
184
|
+
const downgrades: Array<Omit<PinChange, "file">> = []
|
|
114
185
|
for (const field of DEP_FIELDS) {
|
|
115
186
|
const deps = parsed[field]
|
|
116
187
|
if (typeof deps !== "object" || deps === null) continue
|
|
@@ -122,6 +193,19 @@ export function pinSweepText(
|
|
|
122
193
|
if (!rule) continue
|
|
123
194
|
const next = rewriteVersionSpec(rawSpec, rule.to)
|
|
124
195
|
if (next === null) continue
|
|
196
|
+
// Refuse a rollback: `upgrade <version>` pins an exact target, so an OLD target on a newer install
|
|
197
|
+
// walks dependencies backward (e.g. ^2.3.0 → ^2.0.0) and can break a shared-package peer range.
|
|
198
|
+
const current = specVersionTuple(rawSpec)
|
|
199
|
+
const target = specVersionTuple(rule.to)
|
|
200
|
+
if (
|
|
201
|
+
!allowDowngrade &&
|
|
202
|
+
current !== null &&
|
|
203
|
+
target !== null &&
|
|
204
|
+
compareSemverSpec(rawSpec, rule.to) > 0
|
|
205
|
+
) {
|
|
206
|
+
downgrades.push({ field, name, from: rawSpec, to: next })
|
|
207
|
+
continue
|
|
208
|
+
}
|
|
125
209
|
// Replace exactly `"name"<ws>:<ws>"spec"`, preserving the surrounding whitespace.
|
|
126
210
|
const pattern = new RegExp(`("${escapeRegExp(name)}"\\s*:\\s*")${escapeRegExp(rawSpec)}(")`)
|
|
127
211
|
const replaced = out.replace(pattern, `$1${next}$2`)
|
|
@@ -131,7 +215,7 @@ export function pinSweepText(
|
|
|
131
215
|
}
|
|
132
216
|
}
|
|
133
217
|
}
|
|
134
|
-
return { text: out, changes }
|
|
218
|
+
return { text: out, changes, downgrades }
|
|
135
219
|
}
|
|
136
220
|
|
|
137
221
|
function dependencyObjectRange(
|
|
@@ -289,10 +373,16 @@ function scan(cwd: string, pattern: string): string[] {
|
|
|
289
373
|
}
|
|
290
374
|
|
|
291
375
|
/** Compute the plan (and, when `write`, apply it) for a target recipe against `cwd`. */
|
|
292
|
-
export function computeUpgrade(
|
|
376
|
+
export function computeUpgrade(
|
|
377
|
+
cwd: string,
|
|
378
|
+
recipe: UpgradeRecipe,
|
|
379
|
+
write: boolean,
|
|
380
|
+
allowDowngrade = false,
|
|
381
|
+
): UpgradePlan {
|
|
293
382
|
const pins: PinChange[] = []
|
|
294
383
|
const dependencyMoves: DependencyMoveChange[] = []
|
|
295
384
|
const importMoves: ImportChange[] = []
|
|
385
|
+
const downgrades: PinChange[] = []
|
|
296
386
|
|
|
297
387
|
if ((recipe.dependencyMoves?.length ?? 0) > 0 || recipe.pins.length > 0) {
|
|
298
388
|
for (const rel of scan(cwd, "**/package.json")) {
|
|
@@ -300,8 +390,9 @@ export function computeUpgrade(cwd: string, recipe: UpgradeRecipe, write: boolea
|
|
|
300
390
|
const original = readFileSync(abs, "utf8")
|
|
301
391
|
const moved = moveDependenciesText(original, recipe.dependencyMoves ?? [])
|
|
302
392
|
for (const change of moved.changes) dependencyMoves.push({ file: rel, ...change })
|
|
303
|
-
const pinned = pinSweepText(moved.text, recipe.pins)
|
|
393
|
+
const pinned = pinSweepText(moved.text, recipe.pins, allowDowngrade)
|
|
304
394
|
for (const change of pinned.changes) pins.push({ file: rel, ...change })
|
|
395
|
+
for (const d of pinned.downgrades) downgrades.push({ file: rel, ...d })
|
|
305
396
|
if (write && pinned.text !== original) writeFileSync(abs, pinned.text)
|
|
306
397
|
}
|
|
307
398
|
}
|
|
@@ -322,6 +413,7 @@ export function computeUpgrade(cwd: string, recipe: UpgradeRecipe, write: boolea
|
|
|
322
413
|
pins,
|
|
323
414
|
dependencyMoves,
|
|
324
415
|
importMoves,
|
|
416
|
+
downgrades,
|
|
325
417
|
notes: recipe.notes ?? [],
|
|
326
418
|
}
|
|
327
419
|
}
|
|
@@ -336,7 +428,7 @@ function renderPlan(plan: UpgradePlan, write: boolean): string {
|
|
|
336
428
|
plan.dependencyMoves.length === 0 &&
|
|
337
429
|
plan.importMoves.length === 0
|
|
338
430
|
) {
|
|
339
|
-
lines.push("Already up to date
|
|
431
|
+
lines.push("Already up to date - no changes.")
|
|
340
432
|
return lines.join("\n")
|
|
341
433
|
}
|
|
342
434
|
if (plan.dependencyMoves.length > 0) {
|
|
@@ -366,6 +458,19 @@ function renderPlan(plan: UpgradePlan, write: boolean): string {
|
|
|
366
458
|
return lines.join("\n").trimEnd()
|
|
367
459
|
}
|
|
368
460
|
|
|
461
|
+
/** Explain a refused rollback: which pins would roll back, why it's blocked, and the escape hatch. */
|
|
462
|
+
function renderDowngradeRefusal(version: string, downgrades: readonly PinChange[]): string {
|
|
463
|
+
return [
|
|
464
|
+
`nifra upgrade → ${version}: refusing to DOWNGRADE ${downgrades.length} pin(s) below what's installed.`,
|
|
465
|
+
"",
|
|
466
|
+
...downgrades.map((d) => ` ${d.file} ${d.name}: ${d.from} → ${d.to} (would roll back)`),
|
|
467
|
+
"",
|
|
468
|
+
"`nifra upgrade <version>` sets each pin to exactly <version>; an older target on a newer install",
|
|
469
|
+
"rolls dependencies backward and can violate a shared-package peer range. Target a version >= the",
|
|
470
|
+
"installed one, or pass --allow-downgrade if the rollback is intended.",
|
|
471
|
+
].join("\n")
|
|
472
|
+
}
|
|
473
|
+
|
|
369
474
|
/** CLI entry. Returns false (→ non-zero exit) on an unknown version, no project, or a failed verify. */
|
|
370
475
|
export async function runUpgrade(cwd: string, options: UpgradeOptions): Promise<boolean> {
|
|
371
476
|
if (options.list) {
|
|
@@ -393,13 +498,26 @@ export async function runUpgrade(cwd: string, options: UpgradeOptions): Promise<
|
|
|
393
498
|
readFileSync(join(cwd, "package.json"), "utf8")
|
|
394
499
|
} catch {
|
|
395
500
|
console.error(
|
|
396
|
-
`[nifra] no package.json in ${relative(process.cwd(), cwd) || "."}
|
|
501
|
+
`[nifra] no package.json in ${relative(process.cwd(), cwd) || "."} - run from a project root`,
|
|
397
502
|
)
|
|
398
503
|
return false
|
|
399
504
|
}
|
|
400
505
|
|
|
401
506
|
const write = options.write === true
|
|
402
|
-
const
|
|
507
|
+
const allowDowngrade = options.allowDowngrade === true
|
|
508
|
+
|
|
509
|
+
// Fail-closed on rollback: a dry pass writes nothing, so we can refuse before any package.json is touched.
|
|
510
|
+
const preview = computeUpgrade(cwd, recipe, false, allowDowngrade)
|
|
511
|
+
if (preview.downgrades.length > 0) {
|
|
512
|
+
if (options.json) {
|
|
513
|
+
console.log(JSON.stringify({ ...preview, write: false, refused: "downgrade" }, null, 2))
|
|
514
|
+
} else {
|
|
515
|
+
console.error(renderDowngradeRefusal(recipe.version, preview.downgrades))
|
|
516
|
+
}
|
|
517
|
+
return false
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const plan = write ? computeUpgrade(cwd, recipe, true, allowDowngrade) : preview
|
|
403
521
|
|
|
404
522
|
if (options.json) {
|
|
405
523
|
console.log(JSON.stringify({ ...plan, write }, null, 2))
|
|
@@ -413,7 +531,7 @@ export async function runUpgrade(cwd: string, options: UpgradeOptions): Promise<
|
|
|
413
531
|
const ok = await runCheck(cwd, { json: false })
|
|
414
532
|
if (!ok) {
|
|
415
533
|
if (!options.json)
|
|
416
|
-
console.error("\n[nifra] upgrade applied but `nifra check` failed
|
|
534
|
+
console.error("\n[nifra] upgrade applied but `nifra check` failed - review above.")
|
|
417
535
|
return false
|
|
418
536
|
}
|
|
419
537
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One project verification, four commands. `nifra check`, `nifra assure`, `nifra levels`, and
|
|
3
|
+
* `nifra capabilities` are not separate audits - they are renderings of the same reflected project: the
|
|
4
|
+
* typed-contract scan, the route-assurance evaluation, and the static capability provenance. Historically
|
|
5
|
+
* each command re-derived the slice it needed, so a caller had to know which command ran which policy (and
|
|
6
|
+
* `levels` paid for the capability walk twice). `collectProjectVerification` runs each underlying policy
|
|
7
|
+
* exactly once and hands back a single value; the commands become thin formatters over subsets of it -
|
|
8
|
+
* `check`/`assure`/`levels` read their view directly, and `capabilities snapshot`/`check` read the same
|
|
9
|
+
* capability report before writing or diffing the lockfile.
|
|
10
|
+
*
|
|
11
|
+
* The typed-contract check is the one heavy pass (source walk + optional `tsc`), and `nifra assure`
|
|
12
|
+
* never needs it, so it is exposed as a lazily-invoked `check()` rather than computed up front.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync } from "node:fs"
|
|
16
|
+
import { resolve } from "node:path"
|
|
17
|
+
import {
|
|
18
|
+
type AssuranceConfig,
|
|
19
|
+
type AssuranceReport,
|
|
20
|
+
evaluateRouteAssurance,
|
|
21
|
+
} from "@nifrajs/core/assurance"
|
|
22
|
+
import {
|
|
23
|
+
type CapabilityProjectReport,
|
|
24
|
+
collectCapabilityProjectReport,
|
|
25
|
+
} from "./capabilities-tool.ts"
|
|
26
|
+
import { type CheckAssuranceContext, type CheckResult, collectCheckResult } from "./check.ts"
|
|
27
|
+
|
|
28
|
+
export interface CollectProjectVerificationOptions {
|
|
29
|
+
/** Assurance config path, default `nifra.assurance.ts`. Honored by the assurance + levels views. */
|
|
30
|
+
readonly config?: string
|
|
31
|
+
/** Skip the `tsc` pass in the check view (the agent inner-loop mode). */
|
|
32
|
+
readonly lintsOnly?: boolean
|
|
33
|
+
/** Cancels the check view's typecheck. */
|
|
34
|
+
readonly signal?: AbortSignal
|
|
35
|
+
/** Cap the check view's diagnostics (the `levels` L0 and MCP paths set this). */
|
|
36
|
+
readonly maxDiagnostics?: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ProjectVerification {
|
|
40
|
+
readonly cwd: string
|
|
41
|
+
/** Whether `nifra.assurance.ts` (or the requested `config` path) exists. */
|
|
42
|
+
readonly assuranceConfigPresent: boolean
|
|
43
|
+
/** The loaded assurance config, when it loaded cleanly. */
|
|
44
|
+
readonly config?: AssuranceConfig
|
|
45
|
+
/** The error from loading/evaluating the config, if any; the assurance views surface or throw it. */
|
|
46
|
+
readonly configError?: unknown
|
|
47
|
+
/** `evaluateRouteAssurance` over the config's source + policy. Present whenever {@link config} is. */
|
|
48
|
+
readonly routeAssurance?: AssuranceReport
|
|
49
|
+
/** Static capability provenance, when the config declares a capabilities policy. */
|
|
50
|
+
readonly capability?: CapabilityProjectReport
|
|
51
|
+
/**
|
|
52
|
+
* The typed-contract check (scanners + typecheck + the assurance-fed capability/manifest diagnostics).
|
|
53
|
+
* Lazy and memoized: `nifra assure` never calls it, so that command pays nothing for the source walk.
|
|
54
|
+
*/
|
|
55
|
+
check(): Promise<CheckResult>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Run each verification policy once and return the unified value. The config is loaded (and reflected)
|
|
60
|
+
* a single time; the resulting route-assurance + capability evidence is both returned for the assurance
|
|
61
|
+
* views and fed into the check view, so nothing is computed twice.
|
|
62
|
+
*/
|
|
63
|
+
export async function collectProjectVerification(
|
|
64
|
+
cwd: string,
|
|
65
|
+
options: CollectProjectVerificationOptions = {},
|
|
66
|
+
): Promise<ProjectVerification> {
|
|
67
|
+
const { loadAssuranceConfig, DEFAULT_ASSURANCE_CONFIG } = await import("./assure.ts")
|
|
68
|
+
const configPath = resolve(cwd, options.config ?? DEFAULT_ASSURANCE_CONFIG)
|
|
69
|
+
const assuranceConfigPresent = existsSync(configPath)
|
|
70
|
+
|
|
71
|
+
let config: AssuranceConfig | undefined
|
|
72
|
+
let configError: unknown
|
|
73
|
+
let routeAssurance: AssuranceReport | undefined
|
|
74
|
+
let capability: CapabilityProjectReport | undefined
|
|
75
|
+
// One try mirrors `check`'s single catch: any failure to load or reflect the config becomes the
|
|
76
|
+
// `configError` each view then renders in its own idiom (a diagnostic, a throw, a failed rung).
|
|
77
|
+
try {
|
|
78
|
+
config = await loadAssuranceConfig(cwd, options.config)
|
|
79
|
+
routeAssurance = evaluateRouteAssurance(config.source, config.policy, {
|
|
80
|
+
...(config.capabilities !== undefined
|
|
81
|
+
? { definitions: config.capabilities.definitions }
|
|
82
|
+
: {}),
|
|
83
|
+
})
|
|
84
|
+
if (config.capabilities !== undefined) {
|
|
85
|
+
capability = await collectCapabilityProjectReport(cwd, config.source, config.capabilities)
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
configError = error
|
|
89
|
+
config = undefined
|
|
90
|
+
routeAssurance = undefined
|
|
91
|
+
capability = undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The check view's assurance inputs. Fed to `check()` only when no custom config path was requested,
|
|
95
|
+
// so `nifra check` (which always reads the default `nifra.assurance.ts`) and `levels` L0 keep reading
|
|
96
|
+
// exactly the config they read before; a `--config` override stays a route-assurance/levels concern.
|
|
97
|
+
const checkContext: CheckAssuranceContext = {
|
|
98
|
+
present: assuranceConfigPresent,
|
|
99
|
+
...(config !== undefined ? { config } : {}),
|
|
100
|
+
...(configError !== undefined ? { error: configError } : {}),
|
|
101
|
+
...(routeAssurance !== undefined ? { routeAssurance } : {}),
|
|
102
|
+
...(capability !== undefined ? { capability } : {}),
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let checkResult: Promise<CheckResult> | undefined
|
|
106
|
+
return Object.freeze({
|
|
107
|
+
cwd,
|
|
108
|
+
assuranceConfigPresent,
|
|
109
|
+
...(config !== undefined ? { config } : {}),
|
|
110
|
+
...(configError !== undefined ? { configError } : {}),
|
|
111
|
+
...(routeAssurance !== undefined ? { routeAssurance } : {}),
|
|
112
|
+
...(capability !== undefined ? { capability } : {}),
|
|
113
|
+
check(): Promise<CheckResult> {
|
|
114
|
+
checkResult ??= collectCheckResult(cwd, {
|
|
115
|
+
lintsOnly: options.lintsOnly ?? false,
|
|
116
|
+
...(options.signal !== undefined ? { signal: options.signal } : {}),
|
|
117
|
+
...(options.maxDiagnostics !== undefined ? { maxDiagnostics: options.maxDiagnostics } : {}),
|
|
118
|
+
...(options.config === undefined ? { assurance: checkContext } : {}),
|
|
119
|
+
})
|
|
120
|
+
return checkResult
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
}
|