@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/src/upgrade.ts
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.
|
|
@@ -22,10 +26,31 @@
|
|
|
22
26
|
* transforms are a future addition - a recipe that needs one is the signal to add the engine, not before.
|
|
23
27
|
*/
|
|
24
28
|
|
|
25
|
-
import {
|
|
26
|
-
|
|
29
|
+
import {
|
|
30
|
+
existsSync,
|
|
31
|
+
lstatSync,
|
|
32
|
+
mkdirSync,
|
|
33
|
+
readdirSync,
|
|
34
|
+
readFileSync,
|
|
35
|
+
realpathSync,
|
|
36
|
+
statSync,
|
|
37
|
+
writeFileSync,
|
|
38
|
+
} from "node:fs"
|
|
39
|
+
import { dirname, join, relative, sep } from "node:path"
|
|
40
|
+
import { fileURLToPath } from "node:url"
|
|
41
|
+
import type { ReflectedHookReport } from "@nifrajs/core/reflection"
|
|
27
42
|
import { Glob } from "bun"
|
|
43
|
+
import {
|
|
44
|
+
collectHookReport,
|
|
45
|
+
diffHookReports,
|
|
46
|
+
HOOKS_BEFORE_UPGRADE_FILE,
|
|
47
|
+
type HookReportChange,
|
|
48
|
+
parseHookReport,
|
|
49
|
+
renderHookDiff,
|
|
50
|
+
} from "./hooks-report.ts"
|
|
51
|
+
import { PIN_SITES } from "./init-agents.ts"
|
|
28
52
|
import { getRecipe, listRecipeVersions, type UpgradeRecipe } from "./recipes/index.ts"
|
|
53
|
+
import { markerTarget } from "./rules/comment-markers.ts"
|
|
29
54
|
|
|
30
55
|
export interface UpgradeOptions {
|
|
31
56
|
/** Target version, e.g. "1.8.0". Required unless `list` is set. */
|
|
@@ -44,6 +69,11 @@ export interface UpgradeOptions {
|
|
|
44
69
|
readonly exact?: boolean
|
|
45
70
|
/** The running CLI's version: the newest target it applies. Default: its newest recipe. */
|
|
46
71
|
readonly cliVersion?: string
|
|
72
|
+
/**
|
|
73
|
+
* With `--write`, capture `nifra routes --hooks` before and after the edits and print the diff, so a
|
|
74
|
+
* change in which hooks run where is visible. Loads the backend. Default true; `--no-hooks` skips it.
|
|
75
|
+
*/
|
|
76
|
+
readonly hooks?: boolean
|
|
47
77
|
}
|
|
48
78
|
|
|
49
79
|
export interface PinChange {
|
|
@@ -71,13 +101,38 @@ export interface DependencyMoveChange {
|
|
|
71
101
|
readonly action: "renamed" | "removed"
|
|
72
102
|
}
|
|
73
103
|
|
|
104
|
+
/** An agent file whose MCP launch pins another `@nifrajs/cli`, re-pinned to the target. */
|
|
105
|
+
export interface AgentPinChange {
|
|
106
|
+
readonly file: string
|
|
107
|
+
/** The versions it pinned, in order of appearance. */
|
|
108
|
+
readonly from: readonly string[]
|
|
109
|
+
readonly to: string
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** A retired comment in the project, and the line it applied to before the release retired it. */
|
|
113
|
+
export interface RetiredMarkerSite {
|
|
114
|
+
readonly file: string
|
|
115
|
+
readonly line: number
|
|
116
|
+
readonly marker: string
|
|
117
|
+
/** The line the comment covered; `undefined` when a blank line follows its comment block. */
|
|
118
|
+
readonly covers: { readonly line: number; readonly text: string } | undefined
|
|
119
|
+
readonly instead: string
|
|
120
|
+
}
|
|
121
|
+
|
|
74
122
|
export interface UpgradePlan {
|
|
75
123
|
readonly version: string
|
|
76
124
|
readonly pins: readonly PinChange[]
|
|
125
|
+
readonly agentPins: readonly AgentPinChange[]
|
|
77
126
|
readonly dependencyMoves: readonly DependencyMoveChange[]
|
|
78
127
|
readonly importMoves: readonly ImportChange[]
|
|
79
128
|
/** Pins refused because the target is BELOW the installed version (a rollback). Empty when allowed. */
|
|
80
129
|
readonly downgrades: readonly PinChange[]
|
|
130
|
+
/** Comments a release in the chain stopped honoring. Listed only, never rewritten. */
|
|
131
|
+
readonly retiredMarkers: readonly RetiredMarkerSite[]
|
|
132
|
+
/** Installed group packages linked from outside the workspace root, at another version. Never edited. */
|
|
133
|
+
readonly linkedOutside: readonly LinkedGroupInstall[]
|
|
134
|
+
/** Other versions pinned outside package.json. Never edited. */
|
|
135
|
+
readonly strayPins: readonly StrayPin[]
|
|
81
136
|
readonly notes: readonly string[]
|
|
82
137
|
}
|
|
83
138
|
|
|
@@ -203,63 +258,153 @@ export function pinSweepText(
|
|
|
203
258
|
let out = text
|
|
204
259
|
const changes: Array<Omit<PinChange, "file">> = []
|
|
205
260
|
const downgrades: Array<Omit<PinChange, "file">> = []
|
|
261
|
+
for (const { path, key, target: name, spec: rawSpec } of pinEntries(parsed)) {
|
|
262
|
+
const field = path.join(".")
|
|
263
|
+
const rule = rules.find(
|
|
264
|
+
(r) => name === r.match || (r.match.endsWith("/") && name.startsWith(r.match)),
|
|
265
|
+
)
|
|
266
|
+
if (!rule) continue
|
|
267
|
+
const next = rewriteVersionSpec(rawSpec, rule.to, exact)
|
|
268
|
+
if (next === null) continue
|
|
269
|
+
// Refuse a rollback: `upgrade <version>` pins an exact target, so an OLD target on a newer install
|
|
270
|
+
// walks dependencies backward (e.g. ^2.3.0 → ^2.0.0) and can break a shared-package peer range.
|
|
271
|
+
const current = specVersionTuple(rawSpec)
|
|
272
|
+
const target = specVersionTuple(rule.to)
|
|
273
|
+
if (
|
|
274
|
+
!allowDowngrade &&
|
|
275
|
+
current !== null &&
|
|
276
|
+
target !== null &&
|
|
277
|
+
compareSemverSpec(rawSpec, rule.to) > 0
|
|
278
|
+
) {
|
|
279
|
+
downgrades.push({ field, name: key, from: rawSpec, to: next })
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
// Replace exactly `"key"<ws>:<ws>"spec"` inside this field's object, preserving the whitespace. A
|
|
283
|
+
// nested `overrides` entry repeated with the same spec maps the same way, so every one is replaced.
|
|
284
|
+
const range = objectRangeAt(out, path)
|
|
285
|
+
if (range === undefined) continue
|
|
286
|
+
const pattern = new RegExp(`("${escapeRegExp(key)}"\\s*:\\s*")${escapeRegExp(rawSpec)}(")`, "g")
|
|
287
|
+
const body = out.slice(range.start, range.end)
|
|
288
|
+
const replaced = body.replace(pattern, `$1${next}$2`)
|
|
289
|
+
if (replaced !== body) {
|
|
290
|
+
out = out.slice(0, range.start) + replaced + out.slice(range.end)
|
|
291
|
+
changes.push({ field, name: key, from: rawSpec, to: next })
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return { text: out, changes, downgrades }
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Where a package manager forces one version over every range: npm and Bun `overrides` (nested by
|
|
298
|
+
* parent), Yarn and Bun `resolutions`, pnpm `pnpm.overrides`. */
|
|
299
|
+
const OVERRIDE_FIELDS: readonly (readonly string[])[] = [
|
|
300
|
+
["overrides"],
|
|
301
|
+
["resolutions"],
|
|
302
|
+
["pnpm", "overrides"],
|
|
303
|
+
]
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The package an override key pins: `@nifrajs/core`, `parent/@nifrajs/core` and the same behind a `**`
|
|
307
|
+
* glob (resolutions), `parent>@nifrajs/core` and `@nifrajs/core@<4` (pnpm).
|
|
308
|
+
*/
|
|
309
|
+
function overrideTarget(key: string): string {
|
|
310
|
+
const last = key.slice(key.lastIndexOf(">") + 1)
|
|
311
|
+
return /(?:^|\/)((?:@[^/@]+\/)?[^/@]+)(?:@[^/]*)?$/.exec(last)?.[1] ?? last
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Every string version spec a package.json declares for a package, with the field path it sits in. */
|
|
315
|
+
function pinEntries(
|
|
316
|
+
parsed: Record<string, unknown>,
|
|
317
|
+
): Array<{ path: readonly string[]; key: string; target: string; spec: string }> {
|
|
318
|
+
const entries: Array<{ path: readonly string[]; key: string; target: string; spec: string }> = []
|
|
206
319
|
for (const field of DEP_FIELDS) {
|
|
207
320
|
const deps = parsed[field]
|
|
208
321
|
if (typeof deps !== "object" || deps === null) continue
|
|
209
|
-
for (const [name,
|
|
210
|
-
if (typeof
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
target !== null &&
|
|
225
|
-
compareSemverSpec(rawSpec, rule.to) > 0
|
|
226
|
-
) {
|
|
227
|
-
downgrades.push({ field, name, from: rawSpec, to: next })
|
|
228
|
-
continue
|
|
229
|
-
}
|
|
230
|
-
// Replace exactly `"name"<ws>:<ws>"spec"`, preserving the surrounding whitespace.
|
|
231
|
-
const pattern = new RegExp(`("${escapeRegExp(name)}"\\s*:\\s*")${escapeRegExp(rawSpec)}(")`)
|
|
232
|
-
const replaced = out.replace(pattern, `$1${next}$2`)
|
|
233
|
-
if (replaced !== out) {
|
|
234
|
-
out = replaced
|
|
235
|
-
changes.push({ field, name, from: rawSpec, to: next })
|
|
322
|
+
for (const [name, spec] of Object.entries(deps))
|
|
323
|
+
if (typeof spec === "string") entries.push({ path: [field], key: name, target: name, spec })
|
|
324
|
+
}
|
|
325
|
+
for (const path of OVERRIDE_FIELDS) {
|
|
326
|
+
let value: unknown = parsed
|
|
327
|
+
for (const key of path)
|
|
328
|
+
value = typeof value === "object" && value !== null ? Reflect.get(value, key) : undefined
|
|
329
|
+
// npm nests an override under the package whose dependency it pins; `.` pins that package itself.
|
|
330
|
+
const walk = (object: object, parent: string | undefined): void => {
|
|
331
|
+
for (const [key, spec] of Object.entries(object)) {
|
|
332
|
+
const target = key === "." ? parent : overrideTarget(key)
|
|
333
|
+
if (typeof spec === "string" && target !== undefined)
|
|
334
|
+
entries.push({ path, key, target, spec })
|
|
335
|
+
else if (typeof spec === "object" && spec !== null && !Array.isArray(spec))
|
|
336
|
+
walk(spec, target)
|
|
236
337
|
}
|
|
237
338
|
}
|
|
339
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) walk(value, undefined)
|
|
238
340
|
}
|
|
239
|
-
return
|
|
341
|
+
return entries
|
|
240
342
|
}
|
|
241
343
|
|
|
242
|
-
|
|
344
|
+
/** The body (between the braces) of the object at `path` in a JSON text: top-level key first, then
|
|
345
|
+
* each nested one, never a same-named key deeper in the document. */
|
|
346
|
+
function objectRangeAt(
|
|
243
347
|
text: string,
|
|
244
|
-
|
|
348
|
+
path: readonly string[],
|
|
245
349
|
): { start: number; end: number } | undefined {
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
248
|
-
|
|
350
|
+
const rootOpen = text.indexOf("{")
|
|
351
|
+
if (rootOpen === -1) return undefined
|
|
352
|
+
let range: { start: number; end: number } | undefined = {
|
|
353
|
+
start: rootOpen + 1,
|
|
354
|
+
end: closingBrace(text, rootOpen),
|
|
355
|
+
}
|
|
356
|
+
for (const key of path) {
|
|
357
|
+
if (range === undefined) return undefined
|
|
358
|
+
range = memberObjectRange(text, range, key)
|
|
359
|
+
}
|
|
360
|
+
return range
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** The index of the `"` that closes the string opening at `open`. */
|
|
364
|
+
function stringEnd(text: string, open: number): number {
|
|
365
|
+
for (let index = open + 1; index < text.length; index++) {
|
|
366
|
+
if (text[index] === "\\") index++
|
|
367
|
+
else if (text[index] === '"') return index
|
|
368
|
+
}
|
|
369
|
+
return text.length
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** The index of the brace that closes the one at `open`. */
|
|
373
|
+
function closingBrace(text: string, open: number): number {
|
|
249
374
|
let depth = 0
|
|
250
|
-
let quoted = false
|
|
251
|
-
let escaped = false
|
|
252
375
|
for (let index = open; index < text.length; index++) {
|
|
253
376
|
const char = text[index]
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
377
|
+
if (char === '"') index = stringEnd(text, index)
|
|
378
|
+
else if (char === "{" || char === "[") depth++
|
|
379
|
+
else if ((char === "}" || char === "]") && --depth === 0) return index
|
|
380
|
+
}
|
|
381
|
+
return text.length
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** The body of `key`'s object value among the direct members of the object body `within`. */
|
|
385
|
+
function memberObjectRange(
|
|
386
|
+
text: string,
|
|
387
|
+
within: { start: number; end: number },
|
|
388
|
+
key: string,
|
|
389
|
+
): { start: number; end: number } | undefined {
|
|
390
|
+
let depth = 0
|
|
391
|
+
for (let index = within.start; index < within.end; index++) {
|
|
392
|
+
const char = text[index]
|
|
393
|
+
if (char === "{" || char === "[") depth++
|
|
394
|
+
else if (char === "}" || char === "]") depth--
|
|
395
|
+
else if (char === '"') {
|
|
396
|
+
const close = stringEnd(text, index)
|
|
397
|
+
let after = close + 1
|
|
398
|
+
while (/\s/.test(text[after] ?? "")) after++
|
|
399
|
+
if (depth === 0 && text[after] === ":" && text.slice(index + 1, close) === key) {
|
|
400
|
+
after++
|
|
401
|
+
while (/\s/.test(text[after] ?? "")) after++
|
|
402
|
+
return text[after] === "{"
|
|
403
|
+
? { start: after + 1, end: closingBrace(text, after) }
|
|
404
|
+
: undefined
|
|
405
|
+
}
|
|
406
|
+
index = close
|
|
259
407
|
}
|
|
260
|
-
if (char === '"') quoted = true
|
|
261
|
-
else if (char === "{") depth++
|
|
262
|
-
else if (char === "}" && --depth === 0) return { start: open + 1, end: index }
|
|
263
408
|
}
|
|
264
409
|
return undefined
|
|
265
410
|
}
|
|
@@ -272,7 +417,7 @@ function rewriteDependencyEntry(
|
|
|
272
417
|
to: string | undefined,
|
|
273
418
|
toVersion: string | undefined,
|
|
274
419
|
): string {
|
|
275
|
-
const range =
|
|
420
|
+
const range = objectRangeAt(text, [field])
|
|
276
421
|
if (range === undefined) return text
|
|
277
422
|
const body = text.slice(range.start, range.end)
|
|
278
423
|
const key = escapeRegExp(from)
|
|
@@ -396,17 +541,19 @@ const isIgnored = (path: string): boolean =>
|
|
|
396
541
|
.slice(0, -1)
|
|
397
542
|
.some((segment) => IGNORED_DIRECTORIES.has(segment))
|
|
398
543
|
|
|
399
|
-
function scan(cwd: string, pattern: string): string[] {
|
|
544
|
+
function scan(cwd: string, pattern: string, dot = false): string[] {
|
|
400
545
|
const glob = new Glob(pattern)
|
|
401
546
|
const out: string[] = []
|
|
402
|
-
|
|
403
|
-
|
|
547
|
+
// The write boundary: a symlinked file or directory (a linked repository) is never yielded, so the
|
|
548
|
+
// upgrade can only edit files that live under `cwd`.
|
|
549
|
+
for (const path of glob.scanSync({ cwd, dot, followSymlinks: false })) {
|
|
550
|
+
if (!isIgnored(path)) out.push(path.replaceAll("\\", "/"))
|
|
404
551
|
}
|
|
405
552
|
return out.sort() // deterministic order
|
|
406
553
|
}
|
|
407
554
|
|
|
408
555
|
// The fixed version group: every package released together at one version.
|
|
409
|
-
const isFixedGroup = (name: string): boolean =>
|
|
556
|
+
export const isFixedGroup = (name: string): boolean =>
|
|
410
557
|
name.startsWith("@nifrajs/") || name === "nifra" || name === "create-nifra"
|
|
411
558
|
|
|
412
559
|
const groupPins = (version: string): UpgradeRecipe["pins"] => [
|
|
@@ -415,12 +562,27 @@ const groupPins = (version: string): UpgradeRecipe["pins"] => [
|
|
|
415
562
|
{ match: "nifra", to: version },
|
|
416
563
|
]
|
|
417
564
|
|
|
565
|
+
/** Every fixed-group package a package.json under `cwd` names, whatever its spec. */
|
|
566
|
+
export function groupDeclaredNames(cwd: string): ReadonlySet<string> {
|
|
567
|
+
const names = new Set<string>()
|
|
568
|
+
for (const rel of scan(cwd, "**/package.json")) {
|
|
569
|
+
try {
|
|
570
|
+
const parsed = JSON.parse(readFileSync(join(cwd, rel), "utf8")) as Record<string, unknown>
|
|
571
|
+
for (const { target } of pinEntries(parsed)) if (isFixedGroup(target)) names.add(target)
|
|
572
|
+
} catch {
|
|
573
|
+
// Not JSON: it declares nothing.
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return names
|
|
577
|
+
}
|
|
578
|
+
|
|
418
579
|
/**
|
|
419
|
-
*
|
|
420
|
-
*
|
|
580
|
+
* Every fixed-group version the package.json files under `cwd` declare with a plain semver spec, each
|
|
581
|
+
* with the files that declare it. An override counts: it decides what installs. `workspace:`, `link:`
|
|
582
|
+
* and other non-semver specs are not versions.
|
|
421
583
|
*/
|
|
422
|
-
export function
|
|
423
|
-
|
|
584
|
+
export function groupPinVersions(cwd: string): ReadonlyMap<string, readonly string[]> {
|
|
585
|
+
const versions = new Map<string, string[]>()
|
|
424
586
|
for (const rel of scan(cwd, "**/package.json")) {
|
|
425
587
|
let parsed: Record<string, unknown>
|
|
426
588
|
try {
|
|
@@ -428,17 +590,187 @@ export function installedGroupVersion(cwd: string): string | null {
|
|
|
428
590
|
} catch {
|
|
429
591
|
continue
|
|
430
592
|
}
|
|
431
|
-
for (const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
593
|
+
for (const { target, spec } of pinEntries(parsed)) {
|
|
594
|
+
if (!isFixedGroup(target)) continue
|
|
595
|
+
const version = specVersion(spec)
|
|
596
|
+
if (version === null) continue
|
|
597
|
+
const files = versions.get(version) ?? []
|
|
598
|
+
if (!files.includes(rel)) files.push(rel)
|
|
599
|
+
versions.set(version, files)
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return versions
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** A fixed-group version spelled outside package.json, which the upgrade lists and never edits. */
|
|
606
|
+
export interface StrayPin {
|
|
607
|
+
readonly file: string
|
|
608
|
+
readonly line: number
|
|
609
|
+
/** As written: `@nifrajs/cli@3.1.0`. */
|
|
610
|
+
readonly pin: string
|
|
611
|
+
/** A lockfile other than the workspace's own: regenerate it. Listed once, with its other stale pins
|
|
612
|
+
* counted in `more`. */
|
|
613
|
+
readonly lockfile: boolean
|
|
614
|
+
readonly more: number
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const STRAY_PIN_FILES = [
|
|
618
|
+
"**/{.mcp.json,mcp.json}",
|
|
619
|
+
"**/{AGENTS.md,CLAUDE.md}",
|
|
620
|
+
"**/{Dockerfile,Dockerfile.*,*.Dockerfile,*.dockerfile,Containerfile}",
|
|
621
|
+
"**/{docker-compose,compose}*.{yml,yaml}",
|
|
622
|
+
".github/workflows/*.{yml,yaml}",
|
|
623
|
+
".gitlab-ci.yml",
|
|
624
|
+
"**/*.lock",
|
|
625
|
+
"**/{package-lock.json,pnpm-lock.yaml}",
|
|
626
|
+
]
|
|
627
|
+
const LOCKFILE = /(?:\.lock|package-lock\.json|pnpm-lock\.yaml)$/
|
|
628
|
+
const GROUP_PIN =
|
|
629
|
+
/(?<![\w.@/-])(@nifrajs\/[a-z0-9._-]+|create-nifra|nifra)@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/g
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Fixed-group versions other than `version` pinned where `nifra upgrade` does not edit: MCP configs and
|
|
633
|
+
* agent guidance beyond the launch pin it moves, container files, CI workflows, and lockfiles besides
|
|
634
|
+
* the workspace's own (which install rewrites). `planned` holds a file's text after the upgrade's own
|
|
635
|
+
* edits, so a dry run lists what `--write` would leave.
|
|
636
|
+
*/
|
|
637
|
+
export function strayPins(
|
|
638
|
+
cwd: string,
|
|
639
|
+
version: string,
|
|
640
|
+
planned: ReadonlyMap<string, string> = new Map(),
|
|
641
|
+
): StrayPin[] {
|
|
642
|
+
const found: StrayPin[] = []
|
|
643
|
+
const files = new Set(STRAY_PIN_FILES.flatMap((pattern) => scan(cwd, pattern, true)))
|
|
644
|
+
for (const rel of [...files].sort()) {
|
|
645
|
+
const lockfile = LOCKFILE.test(rel)
|
|
646
|
+
if (lockfile && !/[\\/]/.test(rel)) continue
|
|
647
|
+
let text: string
|
|
648
|
+
try {
|
|
649
|
+
if (statSync(join(cwd, rel)).size > 8_000_000) continue
|
|
650
|
+
text = planned.get(rel) ?? readFileSync(join(cwd, rel), "utf8")
|
|
651
|
+
} catch {
|
|
652
|
+
continue
|
|
441
653
|
}
|
|
654
|
+
const stale = [...text.matchAll(GROUP_PIN)].filter(
|
|
655
|
+
(match) => compareSemverSpec(match[2] ?? "", version) !== 0,
|
|
656
|
+
)
|
|
657
|
+
const site = (match: RegExpMatchArray, more: number): StrayPin => ({
|
|
658
|
+
file: rel,
|
|
659
|
+
line: text.slice(0, match.index ?? 0).split("\n").length,
|
|
660
|
+
pin: match[0],
|
|
661
|
+
lockfile,
|
|
662
|
+
more,
|
|
663
|
+
})
|
|
664
|
+
const first = stale[0]
|
|
665
|
+
if (first === undefined) continue
|
|
666
|
+
if (lockfile) found.push(site(first, stale.length - 1))
|
|
667
|
+
else for (const match of stale) found.push(site(match, 0))
|
|
668
|
+
}
|
|
669
|
+
return found
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** Whether `rel` is a regular file under `cwd` reached through no symlink: the upgrade's write boundary. */
|
|
673
|
+
function plainFileAt(cwd: string, rel: string): boolean {
|
|
674
|
+
let path = cwd
|
|
675
|
+
const parts = rel.split("/")
|
|
676
|
+
for (const [index, part] of parts.entries()) {
|
|
677
|
+
path = join(path, part)
|
|
678
|
+
let stat: ReturnType<typeof lstatSync>
|
|
679
|
+
try {
|
|
680
|
+
stat = lstatSync(path)
|
|
681
|
+
} catch {
|
|
682
|
+
return false
|
|
683
|
+
}
|
|
684
|
+
if (stat.isSymbolicLink()) return false
|
|
685
|
+
if (index === parts.length - 1 ? !stat.isFile() : !stat.isDirectory()) return false
|
|
686
|
+
}
|
|
687
|
+
return true
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* The MCP launch pin in each agent file at `cwd`, moved to `version`: the files and the edit of
|
|
692
|
+
* `nifra init-agents --sync-mcp`, so an agent launches the nifra the project installs once it does.
|
|
693
|
+
*/
|
|
694
|
+
function agentPinEdits(
|
|
695
|
+
cwd: string,
|
|
696
|
+
version: string,
|
|
697
|
+
): Array<{ readonly change: AgentPinChange; readonly text: string }> {
|
|
698
|
+
const edits: Array<{ change: AgentPinChange; text: string }> = []
|
|
699
|
+
for (const site of PIN_SITES) {
|
|
700
|
+
if (!plainFileAt(cwd, site.path)) continue
|
|
701
|
+
// latin1 is one char per byte, so every byte outside a pin is written back as it was.
|
|
702
|
+
const scan = site.repin(readFileSync(join(cwd, site.path), "latin1"), version)
|
|
703
|
+
if (scan.stale.length === 0) continue
|
|
704
|
+
edits.push({ change: { file: site.path, from: scan.stale, to: version }, text: scan.text })
|
|
705
|
+
}
|
|
706
|
+
return edits
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/** An installed fixed-group package, followed to its real location. */
|
|
710
|
+
export interface LinkedGroupInstall {
|
|
711
|
+
readonly package: string
|
|
712
|
+
readonly path: string
|
|
713
|
+
readonly version: string
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const packageVersionAt = (dir: string): string | undefined => {
|
|
717
|
+
try {
|
|
718
|
+
const meta: unknown = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"))
|
|
719
|
+
const version = Reflect.get(Object(meta), "version")
|
|
720
|
+
return typeof version === "string" && version.length > 0 ? version : undefined
|
|
721
|
+
} catch {
|
|
722
|
+
return undefined
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Every fixed-group package installed in `cwd/node_modules`, realpath-resolved so a linked repository
|
|
728
|
+
* reports its own version. `inside` is false for one that resolves outside the workspace root.
|
|
729
|
+
*/
|
|
730
|
+
export function installedGroupPackages(
|
|
731
|
+
cwd: string,
|
|
732
|
+
): readonly (LinkedGroupInstall & { readonly inside: boolean })[] {
|
|
733
|
+
const modules = join(cwd, "node_modules")
|
|
734
|
+
const names = ["nifra", "create-nifra"].filter((name) => existsSync(join(modules, name)))
|
|
735
|
+
try {
|
|
736
|
+
for (const name of readdirSync(join(modules, "@nifrajs"))) names.push(`@nifrajs/${name}`)
|
|
737
|
+
} catch {
|
|
738
|
+
// No scoped installs.
|
|
739
|
+
}
|
|
740
|
+
let root: string
|
|
741
|
+
try {
|
|
742
|
+
root = realpathSync(cwd)
|
|
743
|
+
} catch {
|
|
744
|
+
root = cwd
|
|
745
|
+
}
|
|
746
|
+
const found: (LinkedGroupInstall & { inside: boolean })[] = []
|
|
747
|
+
for (const name of names) {
|
|
748
|
+
let path: string
|
|
749
|
+
try {
|
|
750
|
+
path = realpathSync(join(modules, name))
|
|
751
|
+
} catch {
|
|
752
|
+
continue
|
|
753
|
+
}
|
|
754
|
+
const version = packageVersionAt(path)
|
|
755
|
+
if (version === undefined) continue
|
|
756
|
+
found.push({
|
|
757
|
+
package: name,
|
|
758
|
+
path,
|
|
759
|
+
version,
|
|
760
|
+
inside: path === root || path.startsWith(root + sep),
|
|
761
|
+
})
|
|
762
|
+
}
|
|
763
|
+
return found
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* The lowest fixed-group version any package.json under `cwd` declares with a plain semver spec, or
|
|
768
|
+
* null when none does. Recipes newer than it still have edits to make somewhere in the workspace.
|
|
769
|
+
*/
|
|
770
|
+
export function installedGroupVersion(cwd: string): string | null {
|
|
771
|
+
let lowest: string | null = null
|
|
772
|
+
for (const version of groupPinVersions(cwd).keys()) {
|
|
773
|
+
if (lowest === null || compareSemverSpec(version, lowest) < 0) lowest = version
|
|
442
774
|
}
|
|
443
775
|
return lowest
|
|
444
776
|
}
|
|
@@ -475,6 +807,7 @@ export function chainRecipes(target: string, installed: string | null): ChainedR
|
|
|
475
807
|
pins: [...pins].map(([match, to]) => ({ match, to })),
|
|
476
808
|
dependencyMoves: recipes.flatMap((recipe) => recipe.dependencyMoves ?? []),
|
|
477
809
|
importMoves: recipes.flatMap((recipe) => recipe.importMoves),
|
|
810
|
+
retiredMarkers: recipes.flatMap((recipe) => recipe.retiredMarkers ?? []),
|
|
478
811
|
notes: recipes.flatMap((recipe) =>
|
|
479
812
|
(recipe.notes ?? []).map((note) => `${recipe.version}: ${note}`),
|
|
480
813
|
),
|
|
@@ -490,9 +823,15 @@ export function computeUpgrade(
|
|
|
490
823
|
exact = false,
|
|
491
824
|
): UpgradePlan {
|
|
492
825
|
const pins: PinChange[] = []
|
|
826
|
+
const agentPins: AgentPinChange[] = []
|
|
827
|
+
const planned = new Map<string, string>()
|
|
493
828
|
const dependencyMoves: DependencyMoveChange[] = []
|
|
494
829
|
const importMoves: ImportChange[] = []
|
|
495
830
|
const downgrades: PinChange[] = []
|
|
831
|
+
const retiredMarkers: RetiredMarkerSite[] = []
|
|
832
|
+
const linkedOutside = installedGroupPackages(cwd)
|
|
833
|
+
.filter((entry) => !entry.inside && entry.version !== recipe.version)
|
|
834
|
+
.map(({ package: name, path, version }) => ({ package: name, path, version }))
|
|
496
835
|
|
|
497
836
|
if ((recipe.dependencyMoves?.length ?? 0) > 0 || recipe.pins.length > 0) {
|
|
498
837
|
for (const rel of scan(cwd, "**/package.json")) {
|
|
@@ -507,6 +846,12 @@ export function computeUpgrade(
|
|
|
507
846
|
}
|
|
508
847
|
}
|
|
509
848
|
|
|
849
|
+
for (const { change, text } of agentPinEdits(cwd, recipe.version)) {
|
|
850
|
+
agentPins.push(change)
|
|
851
|
+
planned.set(change.file, text)
|
|
852
|
+
if (write) writeFileSync(join(cwd, change.file), Buffer.from(text, "latin1"))
|
|
853
|
+
}
|
|
854
|
+
|
|
510
855
|
if (recipe.importMoves.length > 0) {
|
|
511
856
|
for (const rel of scan(cwd, SOURCE_GLOB)) {
|
|
512
857
|
const abs = join(cwd, rel)
|
|
@@ -518,12 +863,37 @@ export function computeUpgrade(
|
|
|
518
863
|
}
|
|
519
864
|
}
|
|
520
865
|
|
|
866
|
+
// Read after the import moves, so a listed line number is the one the file has after `--write`.
|
|
867
|
+
const retired = recipe.retiredMarkers ?? []
|
|
868
|
+
if (retired.length > 0) {
|
|
869
|
+
for (const rel of scan(cwd, SOURCE_GLOB)) {
|
|
870
|
+
const text = readFileSync(join(cwd, rel), "utf8")
|
|
871
|
+
if (!retired.some(({ marker }) => text.includes(marker))) continue
|
|
872
|
+
const lines = text.split("\n")
|
|
873
|
+
for (let line = 1; line <= lines.length; line++) {
|
|
874
|
+
for (const { marker, instead } of retired) {
|
|
875
|
+
if (!(lines[line - 1] ?? "").includes(marker)) continue
|
|
876
|
+
const target = markerTarget(lines, line)
|
|
877
|
+
const covers =
|
|
878
|
+
target === undefined
|
|
879
|
+
? undefined
|
|
880
|
+
: { line: target, text: (lines[target - 1] ?? "").trim() }
|
|
881
|
+
retiredMarkers.push({ file: rel, line, marker, covers, instead })
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
521
887
|
return {
|
|
522
888
|
version: recipe.version,
|
|
523
889
|
pins,
|
|
890
|
+
agentPins,
|
|
524
891
|
dependencyMoves,
|
|
525
892
|
importMoves,
|
|
526
893
|
downgrades,
|
|
894
|
+
retiredMarkers,
|
|
895
|
+
linkedOutside,
|
|
896
|
+
strayPins: strayPins(cwd, recipe.version, planned),
|
|
527
897
|
notes: recipe.notes ?? [],
|
|
528
898
|
}
|
|
529
899
|
}
|
|
@@ -547,8 +917,12 @@ function renderPlan(
|
|
|
547
917
|
lines.push("")
|
|
548
918
|
if (
|
|
549
919
|
plan.pins.length === 0 &&
|
|
920
|
+
plan.agentPins.length === 0 &&
|
|
550
921
|
plan.dependencyMoves.length === 0 &&
|
|
551
|
-
plan.importMoves.length === 0
|
|
922
|
+
plan.importMoves.length === 0 &&
|
|
923
|
+
plan.retiredMarkers.length === 0 &&
|
|
924
|
+
plan.linkedOutside.length === 0 &&
|
|
925
|
+
plan.strayPins.length === 0
|
|
552
926
|
) {
|
|
553
927
|
lines.push("Already up to date - no changes.")
|
|
554
928
|
return lines.join("\n")
|
|
@@ -569,12 +943,58 @@ function renderPlan(
|
|
|
569
943
|
for (const p of plan.pins) lines.push(` ${p.file} ${p.name}: ${p.from} → ${p.to}`)
|
|
570
944
|
lines.push("")
|
|
571
945
|
}
|
|
946
|
+
if (plan.agentPins.length > 0) {
|
|
947
|
+
lines.push(`${verb} ${plan.agentPins.length} MCP launch pin(s):`)
|
|
948
|
+
for (const p of plan.agentPins)
|
|
949
|
+
lines.push(` ${p.file} @nifrajs/cli: ${p.from.join(", ")} → ${p.to}`)
|
|
950
|
+
if (write) lines.push(" Restart your agent so it launches the new nifra MCP server.")
|
|
951
|
+
lines.push("")
|
|
952
|
+
}
|
|
572
953
|
if (plan.importMoves.length > 0) {
|
|
573
954
|
const total = plan.importMoves.reduce((n, m) => n + m.count, 0)
|
|
574
955
|
lines.push(`${verb} ${total} import move(s) across ${plan.importMoves.length} file(s):`)
|
|
575
956
|
for (const m of plan.importMoves) lines.push(` ${m.file} ${m.from} → ${m.to} (${m.count})`)
|
|
576
957
|
lines.push("")
|
|
577
958
|
}
|
|
959
|
+
if (plan.retiredMarkers.length > 0) {
|
|
960
|
+
lines.push(
|
|
961
|
+
`${plan.retiredMarkers.length} retired comment(s) that silence nothing now; nifra check reports what each covered:`,
|
|
962
|
+
)
|
|
963
|
+
for (const site of plan.retiredMarkers) {
|
|
964
|
+
const covers =
|
|
965
|
+
site.covers === undefined
|
|
966
|
+
? "covers no line"
|
|
967
|
+
: `covers line ${site.covers.line}: ${site.covers.text}`
|
|
968
|
+
lines.push(` ${site.file}:${site.line} ${site.marker} ${covers}`)
|
|
969
|
+
lines.push(` write instead: ${site.instead}`)
|
|
970
|
+
}
|
|
971
|
+
lines.push("")
|
|
972
|
+
}
|
|
973
|
+
if (plan.linkedOutside.length > 0) {
|
|
974
|
+
lines.push(
|
|
975
|
+
"Left alone, linked from outside the workspace root (upgrade that repository on its own):",
|
|
976
|
+
)
|
|
977
|
+
for (const entry of plan.linkedOutside)
|
|
978
|
+
lines.push(` ${entry.package} ${entry.version} at ${entry.path}`)
|
|
979
|
+
lines.push("")
|
|
980
|
+
}
|
|
981
|
+
const pinned = plan.strayPins.filter((site) => !site.lockfile)
|
|
982
|
+
if (pinned.length > 0) {
|
|
983
|
+
lines.push(`Left alone, another version pinned outside package.json (update by hand):`)
|
|
984
|
+
for (const site of pinned) lines.push(` ${site.file}:${site.line} ${site.pin}`)
|
|
985
|
+
lines.push("")
|
|
986
|
+
}
|
|
987
|
+
const locks = plan.strayPins.filter((site) => site.lockfile)
|
|
988
|
+
if (locks.length > 0) {
|
|
989
|
+
lines.push(
|
|
990
|
+
`Left alone, a lockfile besides the workspace's own (regenerate it after installing):`,
|
|
991
|
+
)
|
|
992
|
+
for (const site of locks) {
|
|
993
|
+
const more = site.more === 0 ? "" : ` and ${site.more} more`
|
|
994
|
+
lines.push(` ${site.file}:${site.line} ${site.pin}${more}`)
|
|
995
|
+
}
|
|
996
|
+
lines.push("")
|
|
997
|
+
}
|
|
578
998
|
for (const note of plan.notes) lines.push(`note: ${note}`)
|
|
579
999
|
if (!write) lines.push("\nRe-run with --write to apply, then nifra check verifies.")
|
|
580
1000
|
return lines.join("\n").trimEnd()
|
|
@@ -593,6 +1013,74 @@ function renderDowngradeRefusal(version: string, downgrades: readonly PinChange[
|
|
|
593
1013
|
].join("\n")
|
|
594
1014
|
}
|
|
595
1015
|
|
|
1016
|
+
/** The hook report around `--write`: the diff and where the pre-upgrade report was saved, or why not. */
|
|
1017
|
+
export type UpgradeHookScope =
|
|
1018
|
+
| { readonly saved: string; readonly changes: readonly HookReportChange[] }
|
|
1019
|
+
| { readonly error: string; readonly saved?: string }
|
|
1020
|
+
|
|
1021
|
+
async function hookReportBefore(cwd: string): Promise<ReflectedHookReport | { error: string }> {
|
|
1022
|
+
try {
|
|
1023
|
+
const { loadBackendApp } = await import("./load.ts")
|
|
1024
|
+
const { backend } = await loadBackendApp(cwd)
|
|
1025
|
+
return await collectHookReport(backend)
|
|
1026
|
+
} catch (error) {
|
|
1027
|
+
return { error: error instanceof Error ? error.message : String(error) }
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
/** The report after the edits, from a fresh process: the edited sources must not come from this one's
|
|
1032
|
+
* module cache. Saves the pre-upgrade report for `nifra routes --hooks --since` after an install. */
|
|
1033
|
+
async function hookScopeAcross(
|
|
1034
|
+
cwd: string,
|
|
1035
|
+
before: ReflectedHookReport | { error: string },
|
|
1036
|
+
): Promise<UpgradeHookScope> {
|
|
1037
|
+
if ("error" in before) return { error: before.error }
|
|
1038
|
+
const saved = join(cwd, HOOKS_BEFORE_UPGRADE_FILE)
|
|
1039
|
+
mkdirSync(dirname(saved), { recursive: true })
|
|
1040
|
+
writeFileSync(saved, `${JSON.stringify(before, null, 2)}\n`)
|
|
1041
|
+
const entry = fileURLToPath(
|
|
1042
|
+
new URL(import.meta.url.endsWith(".ts") ? "./cli.ts" : "./cli.js", import.meta.url),
|
|
1043
|
+
)
|
|
1044
|
+
const child = Bun.spawn([process.execPath, entry, "routes", "--hooks", "--json"], {
|
|
1045
|
+
cwd,
|
|
1046
|
+
env: process.env,
|
|
1047
|
+
stdout: "pipe",
|
|
1048
|
+
stderr: "pipe",
|
|
1049
|
+
})
|
|
1050
|
+
const [text, , code] = await Promise.all([
|
|
1051
|
+
new Response(child.stdout).text(),
|
|
1052
|
+
new Response(child.stderr).text(),
|
|
1053
|
+
child.exited,
|
|
1054
|
+
])
|
|
1055
|
+
const after = parseHookReport(text)
|
|
1056
|
+
if (after === undefined) {
|
|
1057
|
+
return {
|
|
1058
|
+
error: `\`nifra routes --hooks\` exited ${code} after the edits`,
|
|
1059
|
+
saved: HOOKS_BEFORE_UPGRADE_FILE,
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return { saved: HOOKS_BEFORE_UPGRADE_FILE, changes: diffHookReports(before, after) }
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function renderHookScope(scope: UpgradeHookScope): string {
|
|
1066
|
+
const since = (saved: string): string[] => [
|
|
1067
|
+
"The new release's hooks run once its packages are installed. After installing, compare against the report saved before the edits:",
|
|
1068
|
+
` nifra routes --hooks --since ${saved}`,
|
|
1069
|
+
]
|
|
1070
|
+
if ("error" in scope) {
|
|
1071
|
+
const head = `Hook scope: not captured (${scope.error}).`
|
|
1072
|
+
return scope.saved === undefined ? head : [head, ...since(scope.saved)].join("\n")
|
|
1073
|
+
}
|
|
1074
|
+
return [
|
|
1075
|
+
"Hook scope across the edits (nifra routes --hooks):",
|
|
1076
|
+
renderHookDiff(scope.changes)
|
|
1077
|
+
.split("\n")
|
|
1078
|
+
.map((line) => ` ${line}`)
|
|
1079
|
+
.join("\n"),
|
|
1080
|
+
...since(scope.saved),
|
|
1081
|
+
].join("\n")
|
|
1082
|
+
}
|
|
1083
|
+
|
|
596
1084
|
/** The targets this CLI applies: each release recipe up to its own version, and that version. */
|
|
597
1085
|
export function upgradeTargets(cliVersion: string): string[] {
|
|
598
1086
|
const versions = listRecipeVersions().filter(
|
|
@@ -609,6 +1097,7 @@ function newerCliCommand(version: string, options: UpgradeOptions): string {
|
|
|
609
1097
|
options.exact === true ? "--exact" : "",
|
|
610
1098
|
options.allowDowngrade === true ? "--allow-downgrade" : "",
|
|
611
1099
|
options.verify === false ? "--no-verify" : "",
|
|
1100
|
+
options.hooks === false ? "--no-hooks" : "",
|
|
612
1101
|
options.json === true ? "--json" : "",
|
|
613
1102
|
].filter((flag) => flag !== "")
|
|
614
1103
|
return ["bunx", `@nifrajs/cli@${version}`, "upgrade", version, ...flags].join(" ")
|
|
@@ -697,21 +1186,36 @@ export async function runUpgrade(cwd: string, options: UpgradeOptions): Promise<
|
|
|
697
1186
|
return false
|
|
698
1187
|
}
|
|
699
1188
|
|
|
1189
|
+
const captureHooks = write && options.hooks !== false
|
|
1190
|
+
const before = captureHooks ? await hookReportBefore(cwd) : undefined
|
|
700
1191
|
const plan = write ? computeUpgrade(cwd, recipe, true, allowDowngrade, exact) : preview
|
|
1192
|
+
const hooks = before === undefined ? undefined : await hookScopeAcross(cwd, before)
|
|
701
1193
|
|
|
1194
|
+
// Verify only makes sense once edits are on disk. Default on after --write; opt out with --no-verify.
|
|
1195
|
+
const verify = write && options.verify !== false
|
|
702
1196
|
if (options.json) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1197
|
+
// The check result rides inside the one JSON document; its human report would break the stream.
|
|
1198
|
+
const report: Record<string, unknown> = { ...plan, from, steps: recipe.steps, write }
|
|
1199
|
+
if (hooks !== undefined) report.hooks = hooks
|
|
1200
|
+
let ok = true
|
|
1201
|
+
if (verify) {
|
|
1202
|
+
const { collectProjectVerification } = await import("./verification.ts")
|
|
1203
|
+
const verification = await collectProjectVerification(cwd, { lintsOnly: false })
|
|
1204
|
+
const check = await verification.check()
|
|
1205
|
+
report.check = check
|
|
1206
|
+
ok = check.ok
|
|
1207
|
+
}
|
|
1208
|
+
console.log(JSON.stringify(report, null, 2))
|
|
1209
|
+
return ok
|
|
706
1210
|
}
|
|
1211
|
+
console.log(renderPlan(plan, write, from, recipe.steps))
|
|
1212
|
+
if (hooks !== undefined) console.log(`\n${renderHookScope(hooks)}`)
|
|
707
1213
|
|
|
708
|
-
|
|
709
|
-
if (write && options.verify !== false) {
|
|
1214
|
+
if (verify) {
|
|
710
1215
|
const { runCheck } = await import("./check.ts")
|
|
711
1216
|
const ok = await runCheck(cwd, { json: false })
|
|
712
1217
|
if (!ok) {
|
|
713
|
-
|
|
714
|
-
console.error("\n[nifra] upgrade applied but `nifra check` failed - review above.")
|
|
1218
|
+
console.error("\n[nifra] upgrade applied but `nifra check` failed - review above.")
|
|
715
1219
|
return false
|
|
716
1220
|
}
|
|
717
1221
|
}
|