@nifrajs/cli 4.0.2 → 5.1.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 +10 -2
- package/dist/agent-files.d.ts +107 -0
- package/dist/agent-files.d.ts.map +1 -0
- package/dist/agent-files.js +162 -0
- package/dist/agent-files.js.map +1 -0
- package/dist/app-files.d.ts +6 -0
- package/dist/app-files.d.ts.map +1 -1
- package/dist/app-files.js +27 -2
- package/dist/app-files.js.map +1 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +9 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts +12 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +59 -8
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +36 -9
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +22 -2
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +54 -7
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +83 -24
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1516 -318
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +2 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +203 -70
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +215 -50
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +20 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +73 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +24 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +194 -45
- package/dist/command-catalog.js.map +1 -1
- package/dist/component-check.d.ts +13 -0
- package/dist/component-check.d.ts.map +1 -0
- package/dist/component-check.js +59 -0
- package/dist/component-check.js.map +1 -0
- package/dist/contract-proof.d.ts +1 -1
- package/dist/contract-proof.d.ts.map +1 -1
- package/dist/contract-proof.js +2 -24
- package/dist/contract-proof.js.map +1 -1
- package/dist/contracts.d.ts +19 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +50 -3
- package/dist/contracts.js.map +1 -1
- package/dist/db-tool.d.ts.map +1 -1
- package/dist/db-tool.js +4 -3
- package/dist/db-tool.js.map +1 -1
- package/dist/dev-bun-config.d.ts +5 -1
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +44 -11
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +18 -1
- package/dist/dev-feed-tool.d.ts.map +1 -1
- package/dist/dev-feed-tool.js +93 -0
- package/dist/dev-feed-tool.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +9 -8
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +28 -24
- package/dist/diff-tool.js.map +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +23 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +6 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +6 -0
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts +2 -0
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +102 -2
- package/dist/fix-recipes.js.map +1 -1
- package/dist/hand-built-manifest.d.ts +27 -0
- package/dist/hand-built-manifest.d.ts.map +1 -0
- package/dist/hand-built-manifest.js +124 -0
- package/dist/hand-built-manifest.js.map +1 -0
- package/dist/hooks-report.d.ts +23 -0
- package/dist/hooks-report.d.ts.map +1 -0
- package/dist/hooks-report.js +104 -0
- package/dist/hooks-report.js.map +1 -0
- package/dist/init-agents.d.ts +18 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +20 -13
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/config-paths.d.ts +16 -0
- package/dist/internal/config-paths.d.ts.map +1 -0
- package/dist/internal/config-paths.js +124 -0
- package/dist/internal/config-paths.js.map +1 -0
- package/dist/internal/installed-package.d.ts +4 -0
- package/dist/internal/installed-package.d.ts.map +1 -0
- package/dist/internal/installed-package.js +17 -0
- package/dist/internal/installed-package.js.map +1 -0
- package/dist/internal/nifra-project.d.ts +41 -0
- package/dist/internal/nifra-project.d.ts.map +1 -0
- package/dist/internal/nifra-project.js +183 -0
- package/dist/internal/nifra-project.js.map +1 -0
- package/dist/internal/project-specifiers.d.ts +29 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +435 -0
- package/dist/internal/project-specifiers.js.map +1 -0
- package/dist/internal/script-kind.d.ts.map +1 -1
- package/dist/internal/script-kind.js +2 -1
- package/dist/internal/script-kind.js.map +1 -1
- package/dist/internal/source-facts.d.ts +8 -1
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +22 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +1 -1
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +10 -6
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +15 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +87 -25
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +98 -23
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +33 -10
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +60 -10
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +5 -5
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +8 -23
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +2 -2
- package/dist/mcp-delegate.d.ts.map +1 -1
- package/dist/mcp-delegate.js +19 -7
- package/dist/mcp-delegate.js.map +1 -1
- package/dist/mcp-exec.d.ts +13 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +56 -14
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +30 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +63 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts.map +1 -1
- package/dist/mcp-isolate.js +3 -2
- package/dist/mcp-isolate.js.map +1 -1
- package/dist/mcp-path.d.ts +2 -1
- package/dist/mcp-path.d.ts.map +1 -1
- package/dist/mcp-path.js +9 -24
- package/dist/mcp-path.js.map +1 -1
- package/dist/mcp-project-child.js +1 -1
- package/dist/mcp-project-child.js.map +1 -1
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +50 -10
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-ws.d.ts.map +1 -1
- package/dist/mcp-ws.js +3 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +48 -19
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +47 -1
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +1972 -170
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +128 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +975 -0
- package/dist/output-schemas.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +3 -2
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +15 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +1 -0
- package/dist/project-facts.js.map +1 -1
- package/dist/project-path.d.ts +6 -0
- package/dist/project-path.d.ts.map +1 -1
- package/dist/project-path.js +48 -14
- package/dist/project-path.js.map +1 -1
- package/dist/recipes/5.0.0.d.ts +11 -0
- package/dist/recipes/5.0.0.d.ts.map +1 -0
- package/dist/recipes/5.0.0.js +34 -0
- package/dist/recipes/5.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts +7 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/release-verification.d.ts +5 -0
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +14 -0
- package/dist/release-verification.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +12 -3
- package/dist/replay.js.map +1 -1
- package/dist/review.d.ts +3 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +43 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +12 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +12 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/comment-markers.d.ts +16 -7
- package/dist/rules/comment-markers.d.ts.map +1 -1
- package/dist/rules/comment-markers.js +58 -19
- package/dist/rules/comment-markers.js.map +1 -1
- package/dist/rules/contract-visibility.d.ts +3 -0
- package/dist/rules/contract-visibility.d.ts.map +1 -0
- package/dist/rules/contract-visibility.js +337 -0
- package/dist/rules/contract-visibility.js.map +1 -0
- package/dist/rules/data-guard.d.ts +13 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +356 -37
- package/dist/rules/data-guard.js.map +1 -1
- package/dist/rules/layout.d.ts +4 -0
- package/dist/rules/layout.d.ts.map +1 -0
- package/dist/rules/layout.js +19 -0
- package/dist/rules/layout.js.map +1 -0
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +178 -44
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +50 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +436 -4
- package/dist/rules/pages.js.map +1 -1
- package/dist/rules/routes.d.ts +6 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +111 -16
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.js +2 -2
- package/dist/rules/secrets.js.map +1 -1
- package/dist/rules/security.d.ts +13 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +313 -69
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/zones.d.ts +13 -0
- package/dist/rules/zones.d.ts.map +1 -1
- package/dist/rules/zones.js +218 -44
- package/dist/rules/zones.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +6 -4
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +14 -5
- package/dist/sdk.js.map +1 -1
- package/dist/stylex-migrate.d.ts.map +1 -1
- package/dist/stylex-migrate.js +290 -24
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +6 -3
- package/dist/test-tool.js.map +1 -1
- package/dist/typecheck-coverage.d.ts +20 -0
- package/dist/typecheck-coverage.d.ts.map +1 -0
- package/dist/typecheck-coverage.js +129 -0
- package/dist/typecheck-coverage.js.map +1 -0
- package/dist/upgrade.d.ts +84 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +498 -78
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +16 -2
- package/dist/verification-plan.js.map +1 -1
- package/dist/version-drift.d.ts +22 -0
- package/dist/version-drift.d.ts.map +1 -0
- package/dist/version-drift.js +94 -0
- package/dist/version-drift.js.map +1 -0
- package/dist/web-app-options.d.ts +1 -1
- package/dist/web-app-options.d.ts.map +1 -1
- package/dist/web-app-options.js +1 -1
- package/dist/web-app-options.js.map +1 -1
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +20 -8
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +55 -7
- package/docs/llms-full.txt +2043 -222
- package/docs/types.json +1573 -246
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +18 -2
- package/src/assure.ts +88 -10
- package/src/capabilities-tool.ts +47 -9
- package/src/check-diagnostics.ts +75 -9
- package/src/check-scan.ts +1816 -377
- package/src/check.ts +258 -81
- package/src/cli.ts +242 -37
- package/src/client-module.ts +97 -0
- package/src/command-catalog.ts +237 -52
- package/src/component-check.ts +79 -0
- package/src/contract-proof.ts +3 -25
- package/src/contracts.ts +60 -3
- package/src/db-tool.ts +5 -2
- package/src/dev-bun-config.ts +51 -10
- package/src/dev-feed-tool.ts +112 -1
- package/src/diagnostics.ts +3 -0
- package/src/diff-tool.ts +33 -26
- package/src/doctor.ts +43 -21
- package/src/env-file.ts +7 -0
- package/src/fix-recipes.ts +116 -2
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +21 -14
- package/src/internal/config-paths.ts +154 -0
- package/src/internal/installed-package.ts +15 -0
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +477 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +48 -5
- package/src/internal/typescript-import.ts +15 -6
- package/src/introspect.ts +104 -28
- package/src/levels-tool.ts +124 -29
- package/src/load.ts +107 -17
- package/src/mcp-context.ts +12 -25
- package/src/mcp-delegate.ts +22 -10
- package/src/mcp-exec.ts +68 -18
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +74 -0
- package/src/mcp-isolate.ts +9 -2
- package/src/mcp-path.ts +8 -22
- package/src/mcp-project-child.ts +1 -1
- package/src/mcp-root.ts +58 -8
- package/src/mcp-ws.ts +4 -2
- package/src/mcp.ts +56 -22
- package/src/migrate-layout.ts +2257 -176
- package/src/output-schemas.ts +1240 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/project-path.ts +44 -13
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/release-verification.ts +20 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +44 -10
- package/src/rules/codes.ts +12 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +391 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +206 -52
- package/src/rules/pages.ts +481 -4
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +391 -72
- package/src/rules/zones.ts +232 -42
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +251 -24
- package/src/test-tool.ts +12 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +576 -72
- package/src/verification-plan.ts +33 -2
- package/src/version-drift.ts +118 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
package/dist/upgrade.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
*
|
|
6
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
|
+
* `overrides`, `resolutions` and `pnpm.overrides` are swept too, since they decide what installs.
|
|
9
|
+
* The MCP launch pin in `.mcp.json`, `.cursor/mcp.json`, CLAUDE.md and AGENTS.md moves too, the
|
|
10
|
+
* edit `nifra init-agents --sync-mcp` makes; a version pinned anywhere else (a Dockerfile, a CI
|
|
11
|
+
* workflow, another lockfile) is listed only.
|
|
8
12
|
* 2. **dependency moves** - replace removed packages with their supported successor without
|
|
9
13
|
* reserializing package.json or leaving duplicate dependency keys.
|
|
10
14
|
* 3. **import moves** - rewrite exact import specifiers to their updated module paths.
|
|
@@ -21,10 +25,14 @@
|
|
|
21
25
|
* Deliberately NOT a codemod engine: transforms are string/specifier-level only. Structural (AST)
|
|
22
26
|
* transforms are a future addition - a recipe that needs one is the signal to add the engine, not before.
|
|
23
27
|
*/
|
|
24
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
25
|
-
import { join, relative } from "node:path";
|
|
28
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from "node:fs";
|
|
29
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
26
31
|
import { Glob } from "bun";
|
|
32
|
+
import { collectHookReport, diffHookReports, HOOKS_BEFORE_UPGRADE_FILE, parseHookReport, renderHookDiff, } from "./hooks-report.js";
|
|
33
|
+
import { PIN_SITES } from "./init-agents.js";
|
|
27
34
|
import { getRecipe, listRecipeVersions } from "./recipes/index.js";
|
|
35
|
+
import { markerTarget } from "./rules/comment-markers.js";
|
|
28
36
|
const DEP_FIELDS = [
|
|
29
37
|
"dependencies",
|
|
30
38
|
"devDependencies",
|
|
@@ -131,71 +139,155 @@ export function pinSweepText(text, rules, allowDowngrade = false, exact = false)
|
|
|
131
139
|
let out = text;
|
|
132
140
|
const changes = [];
|
|
133
141
|
const downgrades = [];
|
|
142
|
+
for (const { path, key, target: name, spec: rawSpec } of pinEntries(parsed)) {
|
|
143
|
+
const field = path.join(".");
|
|
144
|
+
const rule = rules.find((r) => name === r.match || (r.match.endsWith("/") && name.startsWith(r.match)));
|
|
145
|
+
if (!rule)
|
|
146
|
+
continue;
|
|
147
|
+
const next = rewriteVersionSpec(rawSpec, rule.to, exact);
|
|
148
|
+
if (next === null)
|
|
149
|
+
continue;
|
|
150
|
+
// Refuse a rollback: `upgrade <version>` pins an exact target, so an OLD target on a newer install
|
|
151
|
+
// walks dependencies backward (e.g. ^2.3.0 → ^2.0.0) and can break a shared-package peer range.
|
|
152
|
+
const current = specVersionTuple(rawSpec);
|
|
153
|
+
const target = specVersionTuple(rule.to);
|
|
154
|
+
if (!allowDowngrade &&
|
|
155
|
+
current !== null &&
|
|
156
|
+
target !== null &&
|
|
157
|
+
compareSemverSpec(rawSpec, rule.to) > 0) {
|
|
158
|
+
downgrades.push({ field, name: key, from: rawSpec, to: next });
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
// Replace exactly `"key"<ws>:<ws>"spec"` inside this field's object, preserving the whitespace. A
|
|
162
|
+
// nested `overrides` entry repeated with the same spec maps the same way, so every one is replaced.
|
|
163
|
+
const range = objectRangeAt(out, path);
|
|
164
|
+
if (range === undefined)
|
|
165
|
+
continue;
|
|
166
|
+
const pattern = new RegExp(`("${escapeRegExp(key)}"\\s*:\\s*")${escapeRegExp(rawSpec)}(")`, "g");
|
|
167
|
+
const body = out.slice(range.start, range.end);
|
|
168
|
+
const replaced = body.replace(pattern, `$1${next}$2`);
|
|
169
|
+
if (replaced !== body) {
|
|
170
|
+
out = out.slice(0, range.start) + replaced + out.slice(range.end);
|
|
171
|
+
changes.push({ field, name: key, from: rawSpec, to: next });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { text: out, changes, downgrades };
|
|
175
|
+
}
|
|
176
|
+
/** Where a package manager forces one version over every range: npm and Bun `overrides` (nested by
|
|
177
|
+
* parent), Yarn and Bun `resolutions`, pnpm `pnpm.overrides`. */
|
|
178
|
+
const OVERRIDE_FIELDS = [
|
|
179
|
+
["overrides"],
|
|
180
|
+
["resolutions"],
|
|
181
|
+
["pnpm", "overrides"],
|
|
182
|
+
];
|
|
183
|
+
/**
|
|
184
|
+
* The package an override key pins: `@nifrajs/core`, `parent/@nifrajs/core` and the same behind a `**`
|
|
185
|
+
* glob (resolutions), `parent>@nifrajs/core` and `@nifrajs/core@<4` (pnpm).
|
|
186
|
+
*/
|
|
187
|
+
function overrideTarget(key) {
|
|
188
|
+
const last = key.slice(key.lastIndexOf(">") + 1);
|
|
189
|
+
return /(?:^|\/)((?:@[^/@]+\/)?[^/@]+)(?:@[^/]*)?$/.exec(last)?.[1] ?? last;
|
|
190
|
+
}
|
|
191
|
+
/** Every string version spec a package.json declares for a package, with the field path it sits in. */
|
|
192
|
+
function pinEntries(parsed) {
|
|
193
|
+
const entries = [];
|
|
134
194
|
for (const field of DEP_FIELDS) {
|
|
135
195
|
const deps = parsed[field];
|
|
136
196
|
if (typeof deps !== "object" || deps === null)
|
|
137
197
|
continue;
|
|
138
|
-
for (const [name,
|
|
139
|
-
if (typeof
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
compareSemverSpec(rawSpec, rule.to) > 0) {
|
|
155
|
-
downgrades.push({ field, name, from: rawSpec, to: next });
|
|
156
|
-
continue;
|
|
198
|
+
for (const [name, spec] of Object.entries(deps))
|
|
199
|
+
if (typeof spec === "string")
|
|
200
|
+
entries.push({ path: [field], key: name, target: name, spec });
|
|
201
|
+
}
|
|
202
|
+
for (const path of OVERRIDE_FIELDS) {
|
|
203
|
+
let value = parsed;
|
|
204
|
+
for (const key of path)
|
|
205
|
+
value = typeof value === "object" && value !== null ? Reflect.get(value, key) : undefined;
|
|
206
|
+
// npm nests an override under the package whose dependency it pins; `.` pins that package itself.
|
|
207
|
+
const walk = (object, parent) => {
|
|
208
|
+
for (const [key, spec] of Object.entries(object)) {
|
|
209
|
+
const target = key === "." ? parent : overrideTarget(key);
|
|
210
|
+
if (typeof spec === "string" && target !== undefined)
|
|
211
|
+
entries.push({ path, key, target, spec });
|
|
212
|
+
else if (typeof spec === "object" && spec !== null && !Array.isArray(spec))
|
|
213
|
+
walk(spec, target);
|
|
157
214
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
if (replaced !== out) {
|
|
162
|
-
out = replaced;
|
|
163
|
-
changes.push({ field, name, from: rawSpec, to: next });
|
|
164
|
-
}
|
|
165
|
-
}
|
|
215
|
+
};
|
|
216
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value))
|
|
217
|
+
walk(value, undefined);
|
|
166
218
|
}
|
|
167
|
-
return
|
|
219
|
+
return entries;
|
|
168
220
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
221
|
+
/** The body (between the braces) of the object at `path` in a JSON text: top-level key first, then
|
|
222
|
+
* each nested one, never a same-named key deeper in the document. */
|
|
223
|
+
function objectRangeAt(text, path) {
|
|
224
|
+
const rootOpen = text.indexOf("{");
|
|
225
|
+
if (rootOpen === -1)
|
|
172
226
|
return undefined;
|
|
173
|
-
|
|
227
|
+
let range = {
|
|
228
|
+
start: rootOpen + 1,
|
|
229
|
+
end: closingBrace(text, rootOpen),
|
|
230
|
+
};
|
|
231
|
+
for (const key of path) {
|
|
232
|
+
if (range === undefined)
|
|
233
|
+
return undefined;
|
|
234
|
+
range = memberObjectRange(text, range, key);
|
|
235
|
+
}
|
|
236
|
+
return range;
|
|
237
|
+
}
|
|
238
|
+
/** The index of the `"` that closes the string opening at `open`. */
|
|
239
|
+
function stringEnd(text, open) {
|
|
240
|
+
for (let index = open + 1; index < text.length; index++) {
|
|
241
|
+
if (text[index] === "\\")
|
|
242
|
+
index++;
|
|
243
|
+
else if (text[index] === '"')
|
|
244
|
+
return index;
|
|
245
|
+
}
|
|
246
|
+
return text.length;
|
|
247
|
+
}
|
|
248
|
+
/** The index of the brace that closes the one at `open`. */
|
|
249
|
+
function closingBrace(text, open) {
|
|
174
250
|
let depth = 0;
|
|
175
|
-
let quoted = false;
|
|
176
|
-
let escaped = false;
|
|
177
251
|
for (let index = open; index < text.length; index++) {
|
|
178
252
|
const char = text[index];
|
|
179
|
-
if (quoted) {
|
|
180
|
-
if (escaped)
|
|
181
|
-
escaped = false;
|
|
182
|
-
else if (char === "\\")
|
|
183
|
-
escaped = true;
|
|
184
|
-
else if (char === '"')
|
|
185
|
-
quoted = false;
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
253
|
if (char === '"')
|
|
189
|
-
|
|
190
|
-
else if (char === "{")
|
|
254
|
+
index = stringEnd(text, index);
|
|
255
|
+
else if (char === "{" || char === "[")
|
|
256
|
+
depth++;
|
|
257
|
+
else if ((char === "}" || char === "]") && --depth === 0)
|
|
258
|
+
return index;
|
|
259
|
+
}
|
|
260
|
+
return text.length;
|
|
261
|
+
}
|
|
262
|
+
/** The body of `key`'s object value among the direct members of the object body `within`. */
|
|
263
|
+
function memberObjectRange(text, within, key) {
|
|
264
|
+
let depth = 0;
|
|
265
|
+
for (let index = within.start; index < within.end; index++) {
|
|
266
|
+
const char = text[index];
|
|
267
|
+
if (char === "{" || char === "[")
|
|
191
268
|
depth++;
|
|
192
|
-
else if (char === "}"
|
|
193
|
-
|
|
269
|
+
else if (char === "}" || char === "]")
|
|
270
|
+
depth--;
|
|
271
|
+
else if (char === '"') {
|
|
272
|
+
const close = stringEnd(text, index);
|
|
273
|
+
let after = close + 1;
|
|
274
|
+
while (/\s/.test(text[after] ?? ""))
|
|
275
|
+
after++;
|
|
276
|
+
if (depth === 0 && text[after] === ":" && text.slice(index + 1, close) === key) {
|
|
277
|
+
after++;
|
|
278
|
+
while (/\s/.test(text[after] ?? ""))
|
|
279
|
+
after++;
|
|
280
|
+
return text[after] === "{"
|
|
281
|
+
? { start: after + 1, end: closingBrace(text, after) }
|
|
282
|
+
: undefined;
|
|
283
|
+
}
|
|
284
|
+
index = close;
|
|
285
|
+
}
|
|
194
286
|
}
|
|
195
287
|
return undefined;
|
|
196
288
|
}
|
|
197
289
|
function rewriteDependencyEntry(text, field, from, fromVersion, to, toVersion) {
|
|
198
|
-
const range =
|
|
290
|
+
const range = objectRangeAt(text, [field]);
|
|
199
291
|
if (range === undefined)
|
|
200
292
|
return text;
|
|
201
293
|
const body = text.slice(range.start, range.end);
|
|
@@ -308,28 +400,47 @@ const isIgnored = (path) => path
|
|
|
308
400
|
.split(/[\\/]/)
|
|
309
401
|
.slice(0, -1)
|
|
310
402
|
.some((segment) => IGNORED_DIRECTORIES.has(segment));
|
|
311
|
-
function scan(cwd, pattern) {
|
|
403
|
+
function scan(cwd, pattern, dot = false) {
|
|
312
404
|
const glob = new Glob(pattern);
|
|
313
405
|
const out = [];
|
|
314
|
-
|
|
406
|
+
// The write boundary: a symlinked file or directory (a linked repository) is never yielded, so the
|
|
407
|
+
// upgrade can only edit files that live under `cwd`.
|
|
408
|
+
for (const path of glob.scanSync({ cwd, dot, followSymlinks: false })) {
|
|
315
409
|
if (!isIgnored(path))
|
|
316
|
-
out.push(path);
|
|
410
|
+
out.push(path.replaceAll("\\", "/"));
|
|
317
411
|
}
|
|
318
412
|
return out.sort(); // deterministic order
|
|
319
413
|
}
|
|
320
414
|
// The fixed version group: every package released together at one version.
|
|
321
|
-
const isFixedGroup = (name) => name.startsWith("@nifrajs/") || name === "nifra" || name === "create-nifra";
|
|
415
|
+
export const isFixedGroup = (name) => name.startsWith("@nifrajs/") || name === "nifra" || name === "create-nifra";
|
|
322
416
|
const groupPins = (version) => [
|
|
323
417
|
{ match: "@nifrajs/", to: version },
|
|
324
418
|
{ match: "create-nifra", to: version },
|
|
325
419
|
{ match: "nifra", to: version },
|
|
326
420
|
];
|
|
421
|
+
/** Every fixed-group package a package.json under `cwd` names, whatever its spec. */
|
|
422
|
+
export function groupDeclaredNames(cwd) {
|
|
423
|
+
const names = new Set();
|
|
424
|
+
for (const rel of scan(cwd, "**/package.json")) {
|
|
425
|
+
try {
|
|
426
|
+
const parsed = JSON.parse(readFileSync(join(cwd, rel), "utf8"));
|
|
427
|
+
for (const { target } of pinEntries(parsed))
|
|
428
|
+
if (isFixedGroup(target))
|
|
429
|
+
names.add(target);
|
|
430
|
+
}
|
|
431
|
+
catch {
|
|
432
|
+
// Not JSON: it declares nothing.
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return names;
|
|
436
|
+
}
|
|
327
437
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
438
|
+
* Every fixed-group version the package.json files under `cwd` declare with a plain semver spec, each
|
|
439
|
+
* with the files that declare it. An override counts: it decides what installs. `workspace:`, `link:`
|
|
440
|
+
* and other non-semver specs are not versions.
|
|
330
441
|
*/
|
|
331
|
-
export function
|
|
332
|
-
|
|
442
|
+
export function groupPinVersions(cwd) {
|
|
443
|
+
const versions = new Map();
|
|
333
444
|
for (const rel of scan(cwd, "**/package.json")) {
|
|
334
445
|
let parsed;
|
|
335
446
|
try {
|
|
@@ -338,19 +449,171 @@ export function installedGroupVersion(cwd) {
|
|
|
338
449
|
catch {
|
|
339
450
|
continue;
|
|
340
451
|
}
|
|
341
|
-
for (const
|
|
342
|
-
|
|
343
|
-
if (typeof deps !== "object" || deps === null)
|
|
452
|
+
for (const { target, spec } of pinEntries(parsed)) {
|
|
453
|
+
if (!isFixedGroup(target))
|
|
344
454
|
continue;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
455
|
+
const version = specVersion(spec);
|
|
456
|
+
if (version === null)
|
|
457
|
+
continue;
|
|
458
|
+
const files = versions.get(version) ?? [];
|
|
459
|
+
if (!files.includes(rel))
|
|
460
|
+
files.push(rel);
|
|
461
|
+
versions.set(version, files);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return versions;
|
|
465
|
+
}
|
|
466
|
+
const STRAY_PIN_FILES = [
|
|
467
|
+
"**/{.mcp.json,mcp.json}",
|
|
468
|
+
"**/{AGENTS.md,CLAUDE.md}",
|
|
469
|
+
"**/{Dockerfile,Dockerfile.*,*.Dockerfile,*.dockerfile,Containerfile}",
|
|
470
|
+
"**/{docker-compose,compose}*.{yml,yaml}",
|
|
471
|
+
".github/workflows/*.{yml,yaml}",
|
|
472
|
+
".gitlab-ci.yml",
|
|
473
|
+
"**/*.lock",
|
|
474
|
+
"**/{package-lock.json,pnpm-lock.yaml}",
|
|
475
|
+
];
|
|
476
|
+
const LOCKFILE = /(?:\.lock|package-lock\.json|pnpm-lock\.yaml)$/;
|
|
477
|
+
const GROUP_PIN = /(?<![\w.@/-])(@nifrajs\/[a-z0-9._-]+|create-nifra|nifra)@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/g;
|
|
478
|
+
/**
|
|
479
|
+
* Fixed-group versions other than `version` pinned where `nifra upgrade` does not edit: MCP configs and
|
|
480
|
+
* agent guidance beyond the launch pin it moves, container files, CI workflows, and lockfiles besides
|
|
481
|
+
* the workspace's own (which install rewrites). `planned` holds a file's text after the upgrade's own
|
|
482
|
+
* edits, so a dry run lists what `--write` would leave.
|
|
483
|
+
*/
|
|
484
|
+
export function strayPins(cwd, version, planned = new Map()) {
|
|
485
|
+
const found = [];
|
|
486
|
+
const files = new Set(STRAY_PIN_FILES.flatMap((pattern) => scan(cwd, pattern, true)));
|
|
487
|
+
for (const rel of [...files].sort()) {
|
|
488
|
+
const lockfile = LOCKFILE.test(rel);
|
|
489
|
+
if (lockfile && !/[\\/]/.test(rel))
|
|
490
|
+
continue;
|
|
491
|
+
let text;
|
|
492
|
+
try {
|
|
493
|
+
if (statSync(join(cwd, rel)).size > 8_000_000)
|
|
494
|
+
continue;
|
|
495
|
+
text = planned.get(rel) ?? readFileSync(join(cwd, rel), "utf8");
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
const stale = [...text.matchAll(GROUP_PIN)].filter((match) => compareSemverSpec(match[2] ?? "", version) !== 0);
|
|
501
|
+
const site = (match, more) => ({
|
|
502
|
+
file: rel,
|
|
503
|
+
line: text.slice(0, match.index ?? 0).split("\n").length,
|
|
504
|
+
pin: match[0],
|
|
505
|
+
lockfile,
|
|
506
|
+
more,
|
|
507
|
+
});
|
|
508
|
+
const first = stale[0];
|
|
509
|
+
if (first === undefined)
|
|
510
|
+
continue;
|
|
511
|
+
if (lockfile)
|
|
512
|
+
found.push(site(first, stale.length - 1));
|
|
513
|
+
else
|
|
514
|
+
for (const match of stale)
|
|
515
|
+
found.push(site(match, 0));
|
|
516
|
+
}
|
|
517
|
+
return found;
|
|
518
|
+
}
|
|
519
|
+
/** Whether `rel` is a regular file under `cwd` reached through no symlink: the upgrade's write boundary. */
|
|
520
|
+
function plainFileAt(cwd, rel) {
|
|
521
|
+
let path = cwd;
|
|
522
|
+
const parts = rel.split("/");
|
|
523
|
+
for (const [index, part] of parts.entries()) {
|
|
524
|
+
path = join(path, part);
|
|
525
|
+
let stat;
|
|
526
|
+
try {
|
|
527
|
+
stat = lstatSync(path);
|
|
528
|
+
}
|
|
529
|
+
catch {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
if (stat.isSymbolicLink())
|
|
533
|
+
return false;
|
|
534
|
+
if (index === parts.length - 1 ? !stat.isFile() : !stat.isDirectory())
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* The MCP launch pin in each agent file at `cwd`, moved to `version`: the files and the edit of
|
|
541
|
+
* `nifra init-agents --sync-mcp`, so an agent launches the nifra the project installs once it does.
|
|
542
|
+
*/
|
|
543
|
+
function agentPinEdits(cwd, version) {
|
|
544
|
+
const edits = [];
|
|
545
|
+
for (const site of PIN_SITES) {
|
|
546
|
+
if (!plainFileAt(cwd, site.path))
|
|
547
|
+
continue;
|
|
548
|
+
// latin1 is one char per byte, so every byte outside a pin is written back as it was.
|
|
549
|
+
const scan = site.repin(readFileSync(join(cwd, site.path), "latin1"), version);
|
|
550
|
+
if (scan.stale.length === 0)
|
|
551
|
+
continue;
|
|
552
|
+
edits.push({ change: { file: site.path, from: scan.stale, to: version }, text: scan.text });
|
|
553
|
+
}
|
|
554
|
+
return edits;
|
|
555
|
+
}
|
|
556
|
+
const packageVersionAt = (dir) => {
|
|
557
|
+
try {
|
|
558
|
+
const meta = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
559
|
+
const version = Reflect.get(Object(meta), "version");
|
|
560
|
+
return typeof version === "string" && version.length > 0 ? version : undefined;
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
return undefined;
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
/**
|
|
567
|
+
* Every fixed-group package installed in `cwd/node_modules`, realpath-resolved so a linked repository
|
|
568
|
+
* reports its own version. `inside` is false for one that resolves outside the workspace root.
|
|
569
|
+
*/
|
|
570
|
+
export function installedGroupPackages(cwd) {
|
|
571
|
+
const modules = join(cwd, "node_modules");
|
|
572
|
+
const names = ["nifra", "create-nifra"].filter((name) => existsSync(join(modules, name)));
|
|
573
|
+
try {
|
|
574
|
+
for (const name of readdirSync(join(modules, "@nifrajs")))
|
|
575
|
+
names.push(`@nifrajs/${name}`);
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
// No scoped installs.
|
|
579
|
+
}
|
|
580
|
+
let root;
|
|
581
|
+
try {
|
|
582
|
+
root = realpathSync(cwd);
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
root = cwd;
|
|
586
|
+
}
|
|
587
|
+
const found = [];
|
|
588
|
+
for (const name of names) {
|
|
589
|
+
let path;
|
|
590
|
+
try {
|
|
591
|
+
path = realpathSync(join(modules, name));
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
continue;
|
|
353
595
|
}
|
|
596
|
+
const version = packageVersionAt(path);
|
|
597
|
+
if (version === undefined)
|
|
598
|
+
continue;
|
|
599
|
+
found.push({
|
|
600
|
+
package: name,
|
|
601
|
+
path,
|
|
602
|
+
version,
|
|
603
|
+
inside: path === root || path.startsWith(root + sep),
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
return found;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* The lowest fixed-group version any package.json under `cwd` declares with a plain semver spec, or
|
|
610
|
+
* null when none does. Recipes newer than it still have edits to make somewhere in the workspace.
|
|
611
|
+
*/
|
|
612
|
+
export function installedGroupVersion(cwd) {
|
|
613
|
+
let lowest = null;
|
|
614
|
+
for (const version of groupPinVersions(cwd).keys()) {
|
|
615
|
+
if (lowest === null || compareSemverSpec(version, lowest) < 0)
|
|
616
|
+
lowest = version;
|
|
354
617
|
}
|
|
355
618
|
return lowest;
|
|
356
619
|
}
|
|
@@ -379,15 +642,22 @@ export function chainRecipes(target, installed) {
|
|
|
379
642
|
pins: [...pins].map(([match, to]) => ({ match, to })),
|
|
380
643
|
dependencyMoves: recipes.flatMap((recipe) => recipe.dependencyMoves ?? []),
|
|
381
644
|
importMoves: recipes.flatMap((recipe) => recipe.importMoves),
|
|
645
|
+
retiredMarkers: recipes.flatMap((recipe) => recipe.retiredMarkers ?? []),
|
|
382
646
|
notes: recipes.flatMap((recipe) => (recipe.notes ?? []).map((note) => `${recipe.version}: ${note}`)),
|
|
383
647
|
};
|
|
384
648
|
}
|
|
385
649
|
/** Compute the plan (and, when `write`, apply it) for a target recipe against `cwd`. */
|
|
386
650
|
export function computeUpgrade(cwd, recipe, write, allowDowngrade = false, exact = false) {
|
|
387
651
|
const pins = [];
|
|
652
|
+
const agentPins = [];
|
|
653
|
+
const planned = new Map();
|
|
388
654
|
const dependencyMoves = [];
|
|
389
655
|
const importMoves = [];
|
|
390
656
|
const downgrades = [];
|
|
657
|
+
const retiredMarkers = [];
|
|
658
|
+
const linkedOutside = installedGroupPackages(cwd)
|
|
659
|
+
.filter((entry) => !entry.inside && entry.version !== recipe.version)
|
|
660
|
+
.map(({ package: name, path, version }) => ({ package: name, path, version }));
|
|
391
661
|
if ((recipe.dependencyMoves?.length ?? 0) > 0 || recipe.pins.length > 0) {
|
|
392
662
|
for (const rel of scan(cwd, "**/package.json")) {
|
|
393
663
|
const abs = join(cwd, rel);
|
|
@@ -404,6 +674,12 @@ export function computeUpgrade(cwd, recipe, write, allowDowngrade = false, exact
|
|
|
404
674
|
writeFileSync(abs, pinned.text);
|
|
405
675
|
}
|
|
406
676
|
}
|
|
677
|
+
for (const { change, text } of agentPinEdits(cwd, recipe.version)) {
|
|
678
|
+
agentPins.push(change);
|
|
679
|
+
planned.set(change.file, text);
|
|
680
|
+
if (write)
|
|
681
|
+
writeFileSync(join(cwd, change.file), Buffer.from(text, "latin1"));
|
|
682
|
+
}
|
|
407
683
|
if (recipe.importMoves.length > 0) {
|
|
408
684
|
for (const rel of scan(cwd, SOURCE_GLOB)) {
|
|
409
685
|
const abs = join(cwd, rel);
|
|
@@ -417,12 +693,37 @@ export function computeUpgrade(cwd, recipe, write, allowDowngrade = false, exact
|
|
|
417
693
|
writeFileSync(abs, result.text);
|
|
418
694
|
}
|
|
419
695
|
}
|
|
696
|
+
// Read after the import moves, so a listed line number is the one the file has after `--write`.
|
|
697
|
+
const retired = recipe.retiredMarkers ?? [];
|
|
698
|
+
if (retired.length > 0) {
|
|
699
|
+
for (const rel of scan(cwd, SOURCE_GLOB)) {
|
|
700
|
+
const text = readFileSync(join(cwd, rel), "utf8");
|
|
701
|
+
if (!retired.some(({ marker }) => text.includes(marker)))
|
|
702
|
+
continue;
|
|
703
|
+
const lines = text.split("\n");
|
|
704
|
+
for (let line = 1; line <= lines.length; line++) {
|
|
705
|
+
for (const { marker, instead } of retired) {
|
|
706
|
+
if (!(lines[line - 1] ?? "").includes(marker))
|
|
707
|
+
continue;
|
|
708
|
+
const target = markerTarget(lines, line);
|
|
709
|
+
const covers = target === undefined
|
|
710
|
+
? undefined
|
|
711
|
+
: { line: target, text: (lines[target - 1] ?? "").trim() };
|
|
712
|
+
retiredMarkers.push({ file: rel, line, marker, covers, instead });
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
420
717
|
return {
|
|
421
718
|
version: recipe.version,
|
|
422
719
|
pins,
|
|
720
|
+
agentPins,
|
|
423
721
|
dependencyMoves,
|
|
424
722
|
importMoves,
|
|
425
723
|
downgrades,
|
|
724
|
+
retiredMarkers,
|
|
725
|
+
linkedOutside,
|
|
726
|
+
strayPins: strayPins(cwd, recipe.version, planned),
|
|
426
727
|
notes: recipe.notes ?? [],
|
|
427
728
|
};
|
|
428
729
|
}
|
|
@@ -437,8 +738,12 @@ function renderPlan(plan, write, from, steps) {
|
|
|
437
738
|
}
|
|
438
739
|
lines.push("");
|
|
439
740
|
if (plan.pins.length === 0 &&
|
|
741
|
+
plan.agentPins.length === 0 &&
|
|
440
742
|
plan.dependencyMoves.length === 0 &&
|
|
441
|
-
plan.importMoves.length === 0
|
|
743
|
+
plan.importMoves.length === 0 &&
|
|
744
|
+
plan.retiredMarkers.length === 0 &&
|
|
745
|
+
plan.linkedOutside.length === 0 &&
|
|
746
|
+
plan.strayPins.length === 0) {
|
|
442
747
|
lines.push("Already up to date - no changes.");
|
|
443
748
|
return lines.join("\n");
|
|
444
749
|
}
|
|
@@ -458,6 +763,14 @@ function renderPlan(plan, write, from, steps) {
|
|
|
458
763
|
lines.push(` ${p.file} ${p.name}: ${p.from} → ${p.to}`);
|
|
459
764
|
lines.push("");
|
|
460
765
|
}
|
|
766
|
+
if (plan.agentPins.length > 0) {
|
|
767
|
+
lines.push(`${verb} ${plan.agentPins.length} MCP launch pin(s):`);
|
|
768
|
+
for (const p of plan.agentPins)
|
|
769
|
+
lines.push(` ${p.file} @nifrajs/cli: ${p.from.join(", ")} → ${p.to}`);
|
|
770
|
+
if (write)
|
|
771
|
+
lines.push(" Restart your agent so it launches the new nifra MCP server.");
|
|
772
|
+
lines.push("");
|
|
773
|
+
}
|
|
461
774
|
if (plan.importMoves.length > 0) {
|
|
462
775
|
const total = plan.importMoves.reduce((n, m) => n + m.count, 0);
|
|
463
776
|
lines.push(`${verb} ${total} import move(s) across ${plan.importMoves.length} file(s):`);
|
|
@@ -465,6 +778,39 @@ function renderPlan(plan, write, from, steps) {
|
|
|
465
778
|
lines.push(` ${m.file} ${m.from} → ${m.to} (${m.count})`);
|
|
466
779
|
lines.push("");
|
|
467
780
|
}
|
|
781
|
+
if (plan.retiredMarkers.length > 0) {
|
|
782
|
+
lines.push(`${plan.retiredMarkers.length} retired comment(s) that silence nothing now; nifra check reports what each covered:`);
|
|
783
|
+
for (const site of plan.retiredMarkers) {
|
|
784
|
+
const covers = site.covers === undefined
|
|
785
|
+
? "covers no line"
|
|
786
|
+
: `covers line ${site.covers.line}: ${site.covers.text}`;
|
|
787
|
+
lines.push(` ${site.file}:${site.line} ${site.marker} ${covers}`);
|
|
788
|
+
lines.push(` write instead: ${site.instead}`);
|
|
789
|
+
}
|
|
790
|
+
lines.push("");
|
|
791
|
+
}
|
|
792
|
+
if (plan.linkedOutside.length > 0) {
|
|
793
|
+
lines.push("Left alone, linked from outside the workspace root (upgrade that repository on its own):");
|
|
794
|
+
for (const entry of plan.linkedOutside)
|
|
795
|
+
lines.push(` ${entry.package} ${entry.version} at ${entry.path}`);
|
|
796
|
+
lines.push("");
|
|
797
|
+
}
|
|
798
|
+
const pinned = plan.strayPins.filter((site) => !site.lockfile);
|
|
799
|
+
if (pinned.length > 0) {
|
|
800
|
+
lines.push(`Left alone, another version pinned outside package.json (update by hand):`);
|
|
801
|
+
for (const site of pinned)
|
|
802
|
+
lines.push(` ${site.file}:${site.line} ${site.pin}`);
|
|
803
|
+
lines.push("");
|
|
804
|
+
}
|
|
805
|
+
const locks = plan.strayPins.filter((site) => site.lockfile);
|
|
806
|
+
if (locks.length > 0) {
|
|
807
|
+
lines.push(`Left alone, a lockfile besides the workspace's own (regenerate it after installing):`);
|
|
808
|
+
for (const site of locks) {
|
|
809
|
+
const more = site.more === 0 ? "" : ` and ${site.more} more`;
|
|
810
|
+
lines.push(` ${site.file}:${site.line} ${site.pin}${more}`);
|
|
811
|
+
}
|
|
812
|
+
lines.push("");
|
|
813
|
+
}
|
|
468
814
|
for (const note of plan.notes)
|
|
469
815
|
lines.push(`note: ${note}`);
|
|
470
816
|
if (!write)
|
|
@@ -483,6 +829,63 @@ function renderDowngradeRefusal(version, downgrades) {
|
|
|
483
829
|
"installed one, or pass --allow-downgrade if the rollback is intended.",
|
|
484
830
|
].join("\n");
|
|
485
831
|
}
|
|
832
|
+
async function hookReportBefore(cwd) {
|
|
833
|
+
try {
|
|
834
|
+
const { loadBackendApp } = await import("./load.js");
|
|
835
|
+
const { backend } = await loadBackendApp(cwd);
|
|
836
|
+
return await collectHookReport(backend);
|
|
837
|
+
}
|
|
838
|
+
catch (error) {
|
|
839
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/** The report after the edits, from a fresh process: the edited sources must not come from this one's
|
|
843
|
+
* module cache. Saves the pre-upgrade report for `nifra routes --hooks --since` after an install. */
|
|
844
|
+
async function hookScopeAcross(cwd, before) {
|
|
845
|
+
if ("error" in before)
|
|
846
|
+
return { error: before.error };
|
|
847
|
+
const saved = join(cwd, HOOKS_BEFORE_UPGRADE_FILE);
|
|
848
|
+
mkdirSync(dirname(saved), { recursive: true });
|
|
849
|
+
writeFileSync(saved, `${JSON.stringify(before, null, 2)}\n`);
|
|
850
|
+
const entry = fileURLToPath(new URL(import.meta.url.endsWith(".ts") ? "./cli.ts" : "./cli.js", import.meta.url));
|
|
851
|
+
const child = Bun.spawn([process.execPath, entry, "routes", "--hooks", "--json"], {
|
|
852
|
+
cwd,
|
|
853
|
+
env: process.env,
|
|
854
|
+
stdout: "pipe",
|
|
855
|
+
stderr: "pipe",
|
|
856
|
+
});
|
|
857
|
+
const [text, , code] = await Promise.all([
|
|
858
|
+
new Response(child.stdout).text(),
|
|
859
|
+
new Response(child.stderr).text(),
|
|
860
|
+
child.exited,
|
|
861
|
+
]);
|
|
862
|
+
const after = parseHookReport(text);
|
|
863
|
+
if (after === undefined) {
|
|
864
|
+
return {
|
|
865
|
+
error: `\`nifra routes --hooks\` exited ${code} after the edits`,
|
|
866
|
+
saved: HOOKS_BEFORE_UPGRADE_FILE,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
return { saved: HOOKS_BEFORE_UPGRADE_FILE, changes: diffHookReports(before, after) };
|
|
870
|
+
}
|
|
871
|
+
function renderHookScope(scope) {
|
|
872
|
+
const since = (saved) => [
|
|
873
|
+
"The new release's hooks run once its packages are installed. After installing, compare against the report saved before the edits:",
|
|
874
|
+
` nifra routes --hooks --since ${saved}`,
|
|
875
|
+
];
|
|
876
|
+
if ("error" in scope) {
|
|
877
|
+
const head = `Hook scope: not captured (${scope.error}).`;
|
|
878
|
+
return scope.saved === undefined ? head : [head, ...since(scope.saved)].join("\n");
|
|
879
|
+
}
|
|
880
|
+
return [
|
|
881
|
+
"Hook scope across the edits (nifra routes --hooks):",
|
|
882
|
+
renderHookDiff(scope.changes)
|
|
883
|
+
.split("\n")
|
|
884
|
+
.map((line) => ` ${line}`)
|
|
885
|
+
.join("\n"),
|
|
886
|
+
...since(scope.saved),
|
|
887
|
+
].join("\n");
|
|
888
|
+
}
|
|
486
889
|
/** The targets this CLI applies: each release recipe up to its own version, and that version. */
|
|
487
890
|
export function upgradeTargets(cliVersion) {
|
|
488
891
|
const versions = listRecipeVersions().filter((version) => compareSemverSpec(version, cliVersion) <= 0);
|
|
@@ -497,6 +900,7 @@ function newerCliCommand(version, options) {
|
|
|
497
900
|
options.exact === true ? "--exact" : "",
|
|
498
901
|
options.allowDowngrade === true ? "--allow-downgrade" : "",
|
|
499
902
|
options.verify === false ? "--no-verify" : "",
|
|
903
|
+
options.hooks === false ? "--no-hooks" : "",
|
|
500
904
|
options.json === true ? "--json" : "",
|
|
501
905
|
].filter((flag) => flag !== "");
|
|
502
906
|
return ["bunx", `@nifrajs/cli@${version}`, "upgrade", version, ...flags].join(" ");
|
|
@@ -565,20 +969,36 @@ export async function runUpgrade(cwd, options) {
|
|
|
565
969
|
}
|
|
566
970
|
return false;
|
|
567
971
|
}
|
|
972
|
+
const captureHooks = write && options.hooks !== false;
|
|
973
|
+
const before = captureHooks ? await hookReportBefore(cwd) : undefined;
|
|
568
974
|
const plan = write ? computeUpgrade(cwd, recipe, true, allowDowngrade, exact) : preview;
|
|
975
|
+
const hooks = before === undefined ? undefined : await hookScopeAcross(cwd, before);
|
|
976
|
+
// Verify only makes sense once edits are on disk. Default on after --write; opt out with --no-verify.
|
|
977
|
+
const verify = write && options.verify !== false;
|
|
569
978
|
if (options.json) {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
979
|
+
// The check result rides inside the one JSON document; its human report would break the stream.
|
|
980
|
+
const report = { ...plan, from, steps: recipe.steps, write };
|
|
981
|
+
if (hooks !== undefined)
|
|
982
|
+
report.hooks = hooks;
|
|
983
|
+
let ok = true;
|
|
984
|
+
if (verify) {
|
|
985
|
+
const { collectProjectVerification } = await import("./verification.js");
|
|
986
|
+
const verification = await collectProjectVerification(cwd, { lintsOnly: false });
|
|
987
|
+
const check = await verification.check();
|
|
988
|
+
report.check = check;
|
|
989
|
+
ok = check.ok;
|
|
990
|
+
}
|
|
991
|
+
console.log(JSON.stringify(report, null, 2));
|
|
992
|
+
return ok;
|
|
574
993
|
}
|
|
575
|
-
|
|
576
|
-
if (
|
|
994
|
+
console.log(renderPlan(plan, write, from, recipe.steps));
|
|
995
|
+
if (hooks !== undefined)
|
|
996
|
+
console.log(`\n${renderHookScope(hooks)}`);
|
|
997
|
+
if (verify) {
|
|
577
998
|
const { runCheck } = await import("./check.js");
|
|
578
999
|
const ok = await runCheck(cwd, { json: false });
|
|
579
1000
|
if (!ok) {
|
|
580
|
-
|
|
581
|
-
console.error("\n[nifra] upgrade applied but `nifra check` failed - review above.");
|
|
1001
|
+
console.error("\n[nifra] upgrade applied but `nifra check` failed - review above.");
|
|
582
1002
|
return false;
|
|
583
1003
|
}
|
|
584
1004
|
}
|