@nifrajs/cli 3.5.0 → 4.0.2
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 +42 -3
- package/dist/app-files.d.ts +11 -0
- package/dist/app-files.d.ts.map +1 -0
- package/dist/app-files.js +34 -0
- package/dist/app-files.js.map +1 -0
- package/dist/assure-hydration.d.ts +7 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +128 -43
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +16 -2
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +5 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +32 -10
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/cdn-check.d.ts +41 -0
- package/dist/cdn-check.d.ts.map +1 -0
- package/dist/cdn-check.js +207 -0
- package/dist/cdn-check.js.map +1 -0
- package/dist/check-diagnostics.d.ts +4 -0
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +13 -0
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +49 -7
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +291 -62
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +99 -63
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +42 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +262 -82
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +62 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +275 -35
- package/dist/command-catalog.js.map +1 -1
- package/dist/config-target.d.ts +8 -0
- package/dist/config-target.d.ts.map +1 -0
- package/dist/config-target.js +45 -0
- package/dist/config-target.js.map +1 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +52 -13
- package/dist/contracts.js.map +1 -1
- package/dist/db-audit.d.ts +37 -0
- package/dist/db-audit.d.ts.map +1 -0
- package/dist/db-audit.js +99 -0
- package/dist/db-audit.js.map +1 -0
- package/dist/db-child.d.ts +64 -0
- package/dist/db-child.d.ts.map +1 -0
- package/dist/db-child.js +213 -0
- package/dist/db-child.js.map +1 -0
- package/dist/db-config.d.ts +60 -0
- package/dist/db-config.d.ts.map +1 -0
- package/dist/db-config.js +176 -0
- package/dist/db-config.js.map +1 -0
- package/dist/db-tool.d.ts +84 -0
- package/dist/db-tool.d.ts.map +1 -0
- package/dist/db-tool.js +537 -0
- package/dist/db-tool.js.map +1 -0
- package/dist/dev-bun-config.d.ts +20 -13
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +98 -41
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +106 -0
- package/dist/dev-feed-tool.d.ts.map +1 -0
- package/dist/dev-feed-tool.js +713 -0
- package/dist/dev-feed-tool.js.map +1 -0
- package/dist/dev-server-client.d.ts +67 -0
- package/dist/dev-server-client.d.ts.map +1 -0
- package/dist/dev-server-client.js +195 -0
- package/dist/dev-server-client.js.map +1 -0
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +6 -5
- package/dist/diff-tool.js.map +1 -1
- package/dist/docs-search.d.ts.map +1 -1
- package/dist/docs-search.js +12 -4
- package/dist/docs-search.js.map +1 -1
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +76 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +15 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +73 -1
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +48 -22
- package/dist/fix-recipes.js.map +1 -1
- package/dist/frontend-guidance.js +5 -5
- package/dist/frontend-guidance.js.map +1 -1
- package/dist/i18n-check.d.ts +16 -0
- package/dist/i18n-check.d.ts.map +1 -0
- package/dist/i18n-check.js +154 -0
- package/dist/i18n-check.js.map +1 -0
- package/dist/init-agents.d.ts +49 -13
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +239 -34
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/script-kind.d.ts +7 -0
- package/dist/internal/script-kind.d.ts.map +1 -0
- package/dist/internal/script-kind.js +12 -0
- package/dist/internal/script-kind.js.map +1 -0
- package/dist/internal/source-facts.d.ts +5 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +61 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +49 -24
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +507 -67
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +5 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +92 -27
- package/dist/introspect.js.map +1 -1
- package/dist/learn.js +3 -3
- package/dist/learn.js.map +1 -1
- package/dist/levels-tool.d.ts +10 -0
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -3
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +67 -17
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +78 -18
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +3 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts.map +1 -1
- package/dist/mcp-app-tool.js +2 -0
- package/dist/mcp-app-tool.js.map +1 -1
- package/dist/mcp-context.js +1 -1
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +57 -0
- package/dist/mcp-delegate.d.ts.map +1 -0
- package/dist/mcp-delegate.js +130 -0
- package/dist/mcp-delegate.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +23 -5
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +2 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +132 -108
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +17 -5
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +14 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +45 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts +48 -0
- package/dist/mcp-isolate.d.ts.map +1 -0
- package/dist/mcp-isolate.js +222 -0
- package/dist/mcp-isolate.js.map +1 -0
- package/dist/mcp-project-child.d.ts +74 -0
- package/dist/mcp-project-child.d.ts.map +1 -0
- package/dist/mcp-project-child.js +130 -0
- package/dist/mcp-project-child.js.map +1 -0
- package/dist/mcp-render.js +2 -2
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-root.d.ts +37 -9
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +110 -23
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-run.d.ts +18 -1
- package/dist/mcp-run.d.ts.map +1 -1
- package/dist/mcp-run.js +82 -9
- package/dist/mcp-run.js.map +1 -1
- package/dist/mcp-ws.js +4 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts +16 -3
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +81 -53
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +44 -0
- package/dist/migrate-layout.d.ts.map +1 -0
- package/dist/migrate-layout.js +1155 -0
- package/dist/migrate-layout.js.map +1 -0
- package/dist/openapi-tool.d.ts +2 -2
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +2 -1
- package/dist/openapi-types.d.ts.map +1 -1
- package/dist/openapi-types.js +240 -12
- package/dist/openapi-types.js.map +1 -1
- package/dist/pipeline-report.d.ts +3 -2
- package/dist/pipeline-report.d.ts.map +1 -1
- package/dist/pipeline-report.js +19 -16
- package/dist/pipeline-report.js.map +1 -1
- package/dist/port.d.ts +5 -4
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +46 -29
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +5 -0
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts +9 -0
- package/dist/recipes/4.0.0.d.ts.map +1 -0
- package/dist/recipes/4.0.0.js +36 -0
- package/dist/recipes/4.0.0.js.map +1 -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 +11 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +47 -4
- package/dist/release-verification.js.map +1 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +12 -8
- package/dist/review.js.map +1 -1
- package/dist/route-types.d.ts +28 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +106 -0
- package/dist/route-types.js.map +1 -0
- package/dist/rules/codes.d.ts +9 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +9 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/data-guard.d.ts +12 -0
- package/dist/rules/data-guard.d.ts.map +1 -0
- package/dist/rules/data-guard.js +213 -0
- package/dist/rules/data-guard.js.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +17 -5
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/islands.d.ts.map +1 -1
- package/dist/rules/islands.js +7 -4
- package/dist/rules/islands.js.map +1 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +71 -38
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/nano.d.ts.map +1 -1
- package/dist/rules/nano.js +15 -6
- package/dist/rules/nano.js.map +1 -1
- package/dist/rules/pages.d.ts +15 -0
- package/dist/rules/pages.d.ts.map +1 -0
- package/dist/rules/pages.js +88 -0
- package/dist/rules/pages.js.map +1 -0
- package/dist/rules/routes.d.ts +11 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +180 -2
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.d.ts +10 -0
- package/dist/rules/secrets.d.ts.map +1 -0
- package/dist/rules/secrets.js +169 -0
- package/dist/rules/secrets.js.map +1 -0
- package/dist/rules/security.d.ts +7 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +171 -65
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/typescript.d.ts +16 -0
- package/dist/rules/typescript.d.ts.map +1 -0
- package/dist/rules/typescript.js +59 -0
- package/dist/rules/typescript.js.map +1 -0
- package/dist/rules/zones.d.ts +17 -0
- package/dist/rules/zones.d.ts.map +1 -0
- package/dist/rules/zones.js +174 -0
- package/dist/rules/zones.js.map +1 -0
- package/dist/scaffold.d.ts +25 -12
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +132 -87
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts +17 -3
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +941 -153
- package/dist/sdk.js.map +1 -1
- package/dist/search-terms.d.ts +2 -0
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +23 -8
- package/dist/search-terms.js.map +1 -1
- package/dist/smoke.d.ts +79 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +494 -0
- package/dist/smoke.js.map +1 -0
- package/dist/sync-manifest.d.ts +10 -2
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +44 -19
- package/dist/sync-manifest.js.map +1 -1
- package/dist/sync-routes.d.ts +4 -3
- package/dist/sync-routes.d.ts.map +1 -1
- package/dist/sync-routes.js +4 -3
- package/dist/sync-routes.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +30 -11
- package/dist/test-tool.js.map +1 -1
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +11 -5
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +36 -6
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +167 -29
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +4 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +45 -12
- package/dist/verification.js.map +1 -1
- package/dist/web-app-options.d.ts +19 -0
- package/dist/web-app-options.d.ts.map +1 -0
- package/dist/web-app-options.js +49 -0
- package/dist/web-app-options.js.map +1 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +29 -13
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +185 -29
- package/docs/llms-full.txt +3807 -580
- package/docs/types.json +14229 -10939
- package/package.json +19 -12
- package/src/app-files.ts +40 -0
- package/src/assure-hydration.ts +139 -46
- package/src/assure.ts +17 -2
- package/src/capabilities-tool.ts +35 -9
- package/src/cdn-check.ts +268 -0
- package/src/check-diagnostics.ts +17 -0
- package/src/check-scan.ts +358 -63
- package/src/check.ts +97 -62
- package/src/cli.ts +296 -87
- package/src/command-catalog.ts +389 -48
- package/src/config-target.ts +47 -0
- package/src/contracts.ts +54 -12
- package/src/db-audit.ts +128 -0
- package/src/db-child.ts +285 -0
- package/src/db-config.ts +224 -0
- package/src/db-tool.ts +690 -0
- package/src/dev-bun-config.ts +116 -40
- package/src/dev-feed-tool.ts +912 -0
- package/src/dev-server-client.ts +267 -0
- package/src/diff-tool.ts +6 -5
- package/src/docs-search.ts +10 -3
- package/src/doctor.ts +98 -15
- package/src/env-file.ts +78 -1
- package/src/fix-recipes.ts +53 -32
- package/src/frontend-guidance.ts +5 -5
- package/src/i18n-check.ts +179 -0
- package/src/init-agents.ts +301 -38
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/script-kind.ts +11 -0
- package/src/internal/source-facts.ts +96 -22
- package/src/internal/typescript-import.ts +663 -68
- package/src/introspect.ts +90 -27
- package/src/learn.ts +3 -3
- package/src/levels-tool.ts +47 -2
- package/src/load.ts +140 -33
- package/src/manifest-tool.ts +3 -2
- package/src/mcp-app-tool.ts +2 -0
- package/src/mcp-context.ts +1 -1
- package/src/mcp-delegate.ts +162 -0
- package/src/mcp-docs-tools.ts +23 -5
- package/src/mcp-exec.ts +160 -132
- package/src/mcp-http.ts +19 -5
- package/src/mcp-io.ts +57 -0
- package/src/mcp-isolate.ts +287 -0
- package/src/mcp-project-child.ts +182 -0
- package/src/mcp-render.ts +2 -2
- package/src/mcp-root.ts +117 -22
- package/src/mcp-run.ts +104 -10
- package/src/mcp-ws.ts +4 -1
- package/src/mcp.ts +106 -64
- package/src/migrate-layout.ts +1338 -0
- package/src/openapi-tool.ts +7 -3
- package/src/openapi-types.ts +315 -13
- package/src/pipeline-report.ts +22 -18
- package/src/port.ts +53 -30
- package/src/project-facts.ts +5 -0
- package/src/recipes/4.0.0.ts +37 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +74 -5
- package/src/review.ts +13 -8
- package/src/route-types.ts +133 -0
- package/src/rules/codes.ts +9 -0
- package/src/rules/data-guard.ts +230 -0
- package/src/rules/index.ts +18 -5
- package/src/rules/islands.ts +12 -4
- package/src/rules/legacy.ts +71 -39
- package/src/rules/nano.ts +18 -6
- package/src/rules/pages.ts +99 -0
- package/src/rules/routes.ts +181 -2
- package/src/rules/secrets.ts +174 -0
- package/src/rules/security.ts +198 -70
- package/src/rules/typescript.ts +77 -0
- package/src/rules/zones.ts +195 -0
- package/src/scaffold.ts +147 -95
- package/src/sdk.ts +1215 -158
- package/src/search-terms.ts +24 -7
- package/src/smoke.ts +607 -0
- package/src/sync-manifest.ts +50 -19
- package/src/sync-routes.ts +4 -3
- package/src/test-tool.ts +32 -11
- package/src/types-search.ts +11 -4
- package/src/upgrade.ts +205 -25
- package/src/verification-plan.ts +13 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +27 -13
package/src/sync-manifest.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { join } from "node:path"
|
|
14
14
|
import { Glob } from "bun"
|
|
15
|
+
import { codeUnitOrder } from "./internal/code-unit-order.ts"
|
|
15
16
|
|
|
16
17
|
// The marker `generateServerManifest` writes at the top of the file - so a user file merely named
|
|
17
18
|
// `server-manifest.ts` is never mistaken for a generated one. Mirrors `nifra check`'s drift scan.
|
|
@@ -20,6 +21,14 @@ const GENERATED_MARKER = "GENERATED by @nifrajs/web generateServerManifest"
|
|
|
20
21
|
// routes dir relative to the manifest and strips imports to route-relative keys.
|
|
21
22
|
const ROUTES_PREFIX = /["'](\.{1,2}(?:\/[^"'/]+)*?\/routes\/)[^"']+["']/
|
|
22
23
|
const IGNORED = /(^|\/)(node_modules|dist(-[a-z0-9]+)?|build|\.nifra|\.git|\.wrangler|coverage)\//
|
|
24
|
+
// The fs codes that mean the routes dir is absent or unreadable. Every other throw is the route table
|
|
25
|
+
// refusing to build, which must fail the command rather than read as "no manifest found".
|
|
26
|
+
const ROUTES_DIR_UNAVAILABLE = new Set(["ENOENT", "ENOTDIR", "EACCES", "EPERM"])
|
|
27
|
+
|
|
28
|
+
function routesDirUnavailable(error: unknown): boolean {
|
|
29
|
+
if (typeof error !== "object" || error === null || !("code" in error)) return false
|
|
30
|
+
return typeof error.code === "string" && ROUTES_DIR_UNAVAILABLE.has(error.code)
|
|
31
|
+
}
|
|
23
32
|
|
|
24
33
|
export interface SyncManifestResult {
|
|
25
34
|
/** The committed server-manifest file (relative to cwd). */
|
|
@@ -33,12 +42,16 @@ export interface SyncManifestResult {
|
|
|
33
42
|
/** Set when the sync was REFUSED because discovery found zero routes for a manifest that had some
|
|
34
43
|
* (`routes/` likely emptied by mistake) - the manifest was left untouched rather than wiped. */
|
|
35
44
|
readonly refusedEmpty?: boolean
|
|
45
|
+
/** Set when the `routes/` tree could not be built into a route table (a duplicate route, two routes of
|
|
46
|
+
* one shape sharing a path, ...) - the manifest was left untouched and the sync failed. */
|
|
47
|
+
readonly error?: string
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
/**
|
|
39
51
|
* Discover every generated `server-manifest.ts` under `cwd` and re-sync its route table from the live
|
|
40
52
|
* `routes/` tree, preserving baked client assets. Returns one result per generated manifest. Never throws
|
|
41
|
-
* on a per-manifest
|
|
53
|
+
* on a per-manifest failure: an absent or unreadable routes dir is skipped, and a route table that fails
|
|
54
|
+
* to build is reported in that manifest's `error`.
|
|
42
55
|
*/
|
|
43
56
|
export async function syncServerManifests(cwd: string): Promise<SyncManifestResult[]> {
|
|
44
57
|
const { discoverRoutes } = await import("@nifrajs/web/fs")
|
|
@@ -59,8 +72,11 @@ export async function syncServerManifests(cwd: string): Promise<SyncManifestResu
|
|
|
59
72
|
let next: string
|
|
60
73
|
try {
|
|
61
74
|
next = resyncServerManifestSource(source, discoverRoutes(routesDir), routesPrefix)
|
|
62
|
-
} catch {
|
|
63
|
-
continue //
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if (routesDirUnavailable(error)) continue // not a manifest we can sync
|
|
77
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
78
|
+
results.push({ file: rel, changed: false, added: [], removed: [], error: message })
|
|
79
|
+
continue
|
|
64
80
|
}
|
|
65
81
|
const before = parseManifestRouteFiles(source, routesPrefix)
|
|
66
82
|
const after = parseManifestRouteFiles(next, routesPrefix)
|
|
@@ -77,36 +93,51 @@ export async function syncServerManifests(cwd: string): Promise<SyncManifestResu
|
|
|
77
93
|
const drift = diffManifestRoutes(before, after)
|
|
78
94
|
results.push({ file: rel, changed, added: drift.missing, removed: drift.extra })
|
|
79
95
|
}
|
|
80
|
-
return results.sort((a, b) => a.file
|
|
96
|
+
return results.sort((a, b) => codeUnitOrder(a.file, b.file))
|
|
81
97
|
}
|
|
82
98
|
|
|
83
|
-
/**
|
|
84
|
-
export
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
99
|
+
/** True when no manifest's route table failed to build. */
|
|
100
|
+
export function syncManifestsOk(results: readonly SyncManifestResult[]): boolean {
|
|
101
|
+
return results.every((r) => r.error === undefined)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Per-file summary lines + the client-rebuild caveat. */
|
|
105
|
+
export function renderSyncManifestResults(results: readonly SyncManifestResult[]): string[] {
|
|
106
|
+
if (results.length === 0)
|
|
107
|
+
return ["nifra sync-manifest: no generated server-manifest.ts found under this directory."]
|
|
108
|
+
const lines: string[] = []
|
|
90
109
|
let anyChanged = false
|
|
91
110
|
for (const r of results) {
|
|
92
|
-
if (r.
|
|
93
|
-
|
|
111
|
+
if (r.error !== undefined) {
|
|
112
|
+
lines.push(
|
|
113
|
+
`✖ ${r.file}: routes/ does not build a route table; left untouched.`,
|
|
114
|
+
` ${r.error}`,
|
|
115
|
+
)
|
|
116
|
+
} else if (r.refusedEmpty) {
|
|
117
|
+
lines.push(
|
|
94
118
|
`⚠ ${r.file}: refused - routes/ has zero routes but the manifest had some; left untouched (delete the manifest yourself if this is intentional).`,
|
|
95
119
|
)
|
|
96
120
|
} else if (r.changed) {
|
|
97
121
|
anyChanged = true
|
|
98
|
-
|
|
99
|
-
if (r.added.length > 0)
|
|
100
|
-
if (r.removed.length > 0)
|
|
122
|
+
lines.push(`✓ synced ${r.file}`)
|
|
123
|
+
if (r.added.length > 0) lines.push(` added: ${r.added.join(", ")}`)
|
|
124
|
+
if (r.removed.length > 0) lines.push(` removed: ${r.removed.join(", ")}`)
|
|
101
125
|
} else {
|
|
102
|
-
|
|
126
|
+
lines.push(`• ${r.file} already in sync`)
|
|
103
127
|
}
|
|
104
128
|
}
|
|
105
129
|
if (anyChanged) {
|
|
106
|
-
|
|
130
|
+
lines.push(
|
|
107
131
|
"\nRe-synced the server manifest's route table. If you added or changed a HYDRATING route\n" +
|
|
108
132
|
"component (not just a loader / SSR-only route), also run a full build so the client bundle matches.",
|
|
109
133
|
)
|
|
110
134
|
}
|
|
111
|
-
return
|
|
135
|
+
return lines
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** CLI entry: sync every generated manifest and print the summary. False when a route table failed to build. */
|
|
139
|
+
export async function runSyncManifest(cwd: string): Promise<boolean> {
|
|
140
|
+
const results = await syncServerManifests(cwd)
|
|
141
|
+
console.log(renderSyncManifestResults(results).join("\n"))
|
|
142
|
+
return syncManifestsOk(results)
|
|
112
143
|
}
|
package/src/sync-routes.ts
CHANGED
|
@@ -24,9 +24,10 @@ export interface SyncRoutesResult {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Generate the route-search types `.d.ts` from `<cwd>/routes` (the nifra convention). Returns
|
|
28
|
-
* there is no `routes/` directory (an API-only project - nothing to type). Never loads
|
|
29
|
-
* needs only the route tree, so it works before a build and on a broken
|
|
27
|
+
* Generate the route-search types `.d.ts` from `<cwd>/routes` (the nifra convention). Returns
|
|
28
|
+
* `null` when there is no `routes/` directory (an API-only project - nothing to type). Never loads
|
|
29
|
+
* `backend/framework.ts`: it needs only the route tree, so it works before a build and on a broken
|
|
30
|
+
* framework config.
|
|
30
31
|
*/
|
|
31
32
|
export async function syncRouteTypes(cwd: string): Promise<SyncRoutesResult | null> {
|
|
32
33
|
const routesDir = join(cwd, "routes")
|
package/src/test-tool.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { type HeadTailOutput, readHeadTail } from "./mcp-io.ts"
|
|
2
|
+
import { mcpProjectPathError, resolveMcpProjectPath } from "./mcp-path.ts"
|
|
3
|
+
|
|
1
4
|
export interface TestToolArgs {
|
|
2
5
|
readonly pattern?: unknown
|
|
3
6
|
readonly timeoutMs?: unknown
|
|
@@ -26,9 +29,11 @@ export interface TestToolResult {
|
|
|
26
29
|
|
|
27
30
|
const DEFAULT_TIMEOUT_MS = 30_000
|
|
28
31
|
const MAX_TIMEOUT_MS = 300_000
|
|
29
|
-
const
|
|
32
|
+
const HEAD_CHARS = 4_000
|
|
33
|
+
const TAIL_CHARS = 8_000
|
|
34
|
+
const MAX_OUTPUT_CHARS = HEAD_CHARS + TAIL_CHARS
|
|
30
35
|
|
|
31
|
-
function normalizePattern(value: unknown): string | undefined {
|
|
36
|
+
function normalizePattern(value: unknown, root: string): string | undefined {
|
|
32
37
|
if (value === undefined || value === null) return undefined
|
|
33
38
|
if (typeof value !== "string") throw new Error("pattern must be a string")
|
|
34
39
|
const pattern = value.trim()
|
|
@@ -39,6 +44,9 @@ function normalizePattern(value: unknown): string | undefined {
|
|
|
39
44
|
// `pattern` narrows test files; it is not a remote flag injection surface.
|
|
40
45
|
if (pattern.startsWith("-"))
|
|
41
46
|
throw new Error("pattern must be a file/path pattern, not a CLI flag")
|
|
47
|
+
// `bun test` runs any `./`, `../` or absolute path it is given, so a pattern stays inside the project.
|
|
48
|
+
if (resolveMcpProjectPath(root, pattern) === null)
|
|
49
|
+
throw new Error(mcpProjectPathError("pattern", pattern))
|
|
42
50
|
return pattern
|
|
43
51
|
}
|
|
44
52
|
|
|
@@ -52,11 +60,18 @@ function normalizeTimeout(value: unknown): number {
|
|
|
52
60
|
|
|
53
61
|
const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g")
|
|
54
62
|
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
// A UTF-8 character is at most four bytes, so these hold every character the result can show.
|
|
64
|
+
const HEAD_BYTES = HEAD_CHARS * 4
|
|
65
|
+
const TAIL_BYTES = TAIL_CHARS * 4
|
|
66
|
+
|
|
67
|
+
function clean(output: HeadTailOutput): string {
|
|
68
|
+
const strip = (text: string): string => text.replace(ANSI, "")
|
|
69
|
+
if (output.droppedBytes > 0)
|
|
70
|
+
return `${strip(output.head).slice(0, HEAD_CHARS)}\n…(trimmed more than ${output.droppedBytes} bytes)…\n${strip(output.tail).slice(-TAIL_CHARS)}`
|
|
71
|
+
const stripped = strip(output.head + output.tail)
|
|
57
72
|
return stripped.length <= MAX_OUTPUT_CHARS
|
|
58
73
|
? stripped
|
|
59
|
-
: `${stripped.slice(0,
|
|
74
|
+
: `${stripped.slice(0, HEAD_CHARS)}\n…(trimmed ${stripped.length - MAX_OUTPUT_CHARS} chars)…\n${stripped.slice(-TAIL_CHARS)}`
|
|
60
75
|
}
|
|
61
76
|
|
|
62
77
|
function firstNumber(pattern: RegExp, text: string): number | undefined {
|
|
@@ -89,7 +104,7 @@ export async function collectTestResult(
|
|
|
89
104
|
let pattern: string | undefined
|
|
90
105
|
let timeoutMs: number
|
|
91
106
|
try {
|
|
92
|
-
pattern = normalizePattern(args.pattern)
|
|
107
|
+
pattern = normalizePattern(args.pattern, cwd)
|
|
93
108
|
timeoutMs = normalizeTimeout(args.timeoutMs)
|
|
94
109
|
} catch (err) {
|
|
95
110
|
return {
|
|
@@ -124,8 +139,11 @@ export async function collectTestResult(
|
|
|
124
139
|
: "cancelled",
|
|
125
140
|
}
|
|
126
141
|
}
|
|
127
|
-
|
|
142
|
+
// The running Bun, not the first `bun` on PATH: an MCP client may start the server with no `bun` there.
|
|
143
|
+
const proc = Bun.spawn([process.execPath, ...command.slice(1)], {
|
|
128
144
|
cwd,
|
|
145
|
+
// Explicit: without `env`, Bun passes the environment it started with, missing `--env-file` values.
|
|
146
|
+
env: process.env,
|
|
129
147
|
stdout: "pipe",
|
|
130
148
|
stderr: "pipe",
|
|
131
149
|
})
|
|
@@ -140,13 +158,16 @@ export async function collectTestResult(
|
|
|
140
158
|
timedOut = true
|
|
141
159
|
proc.kill()
|
|
142
160
|
}, timeoutMs)
|
|
143
|
-
|
|
144
|
-
let
|
|
161
|
+
const empty: HeadTailOutput = { head: "", tail: "", droppedBytes: 0 }
|
|
162
|
+
let stdoutRaw = empty
|
|
163
|
+
let stderrRaw = empty
|
|
145
164
|
let exitCode: number | null = null
|
|
146
165
|
try {
|
|
166
|
+
// Drained to the end but held to what the result shows, so a test run that prints without limit
|
|
167
|
+
// cannot grow the server's memory with it.
|
|
147
168
|
const result = await Promise.all([
|
|
148
|
-
|
|
149
|
-
|
|
169
|
+
readHeadTail(proc.stdout, HEAD_BYTES, TAIL_BYTES),
|
|
170
|
+
readHeadTail(proc.stderr, HEAD_BYTES, TAIL_BYTES),
|
|
150
171
|
proc.exited,
|
|
151
172
|
])
|
|
152
173
|
stdoutRaw = result[0]
|
package/src/types-search.ts
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* signature/doc body; no args → a per-package index of the available type names.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { codeUnitOrder } from "./internal/code-unit-order.ts"
|
|
13
|
+
import { countBodyHits, queryTermGroups, tokenize, tokenSetScore } from "./search-terms.ts"
|
|
13
14
|
|
|
14
15
|
export interface TypeEntry {
|
|
15
16
|
readonly name: string
|
|
@@ -63,13 +64,19 @@ export function searchTypes(
|
|
|
63
64
|
for (const prepared of prepareTypes(types)) {
|
|
64
65
|
const t = prepared.type
|
|
65
66
|
let score = 0
|
|
67
|
+
let nameTermMatches = 0
|
|
66
68
|
for (const term of terms) {
|
|
67
|
-
|
|
69
|
+
const nameScore = tokenSetScore(prepared.nameTokens, term)
|
|
70
|
+
if (nameScore > 0) {
|
|
71
|
+
nameTermMatches += 1
|
|
72
|
+
score += 8 + nameScore * 4
|
|
73
|
+
}
|
|
68
74
|
score += countBodyHits(prepared.bodyLower, term, 4)
|
|
69
75
|
}
|
|
76
|
+
score += nameTermMatches * 4
|
|
70
77
|
if (score > 0) scored.push({ ...t, score })
|
|
71
78
|
}
|
|
72
|
-
return scored.sort((a, b) => b.score - a.score || a.name
|
|
79
|
+
return scored.sort((a, b) => b.score - a.score || codeUnitOrder(a.name, b.name)).slice(0, limit)
|
|
73
80
|
}
|
|
74
81
|
|
|
75
82
|
function renderEntry(t: TypeEntry): string {
|
|
@@ -159,7 +166,7 @@ export function renderTypesResult(
|
|
|
159
166
|
byPkg.set(t.package, list)
|
|
160
167
|
}
|
|
161
168
|
const index = [...byPkg.entries()]
|
|
162
|
-
.sort(([a], [b]) => a
|
|
169
|
+
.sort(([a], [b]) => codeUnitOrder(a, b))
|
|
163
170
|
.map(([pkg, names]) => `- **${pkg}**: ${names.sort().join(", ")}`)
|
|
164
171
|
.join("\n")
|
|
165
172
|
return `# nifra types - index (${types.length} exported symbols; pass \`name\` for the exact declaration, or \`query\` to search)\n\nSignatures are generated from the built \`.d.ts\` - authoritative + complete. Use this instead of reading any \`.d.ts\` file.\n\n${index}`
|
package/src/upgrade.ts
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
* 3. **import moves** - rewrite exact import specifiers to their updated module paths.
|
|
11
11
|
* 4. **verify** - reuse the existing `nifra check` gate; no new verification surface.
|
|
12
12
|
*
|
|
13
|
+
* Upgrading across several releases runs every recipe between the installed version (the lowest
|
|
14
|
+
* fixed-group version the workspace declares) and the target, oldest first, then pins the target. A
|
|
15
|
+
* target newer than this CLI prints the newer CLI's command instead of guessing at recipes it does not
|
|
16
|
+
* carry; nothing is downloaded or run on the user's behalf.
|
|
17
|
+
*
|
|
13
18
|
* Dry-run by default (prints the plan, writes nothing); `--write` applies. Fail-closed on an unknown
|
|
14
19
|
* target version or a missing package.json, and deterministic (same repo + target → same edits).
|
|
15
20
|
*
|
|
@@ -35,6 +40,10 @@ export interface UpgradeOptions {
|
|
|
35
40
|
readonly verify?: boolean
|
|
36
41
|
/** Permit a target BELOW the installed version (a rollback). Default false → fail-closed. */
|
|
37
42
|
readonly allowDowngrade?: boolean
|
|
43
|
+
/** Pin the exact target (`3.6.0`), dropping the `^`/`~` a spec used. Default false → keep it. */
|
|
44
|
+
readonly exact?: boolean
|
|
45
|
+
/** The running CLI's version: the newest target it applies. Default: its newest recipe. */
|
|
46
|
+
readonly cliVersion?: string
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
export interface PinChange {
|
|
@@ -85,17 +94,28 @@ const SEMVER_SPEC =
|
|
|
85
94
|
/^([\^~]|>=|<=|>|<|=)?\s*(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
|
|
86
95
|
|
|
87
96
|
/**
|
|
88
|
-
* Rewrite a dependency version spec to `toVersion`, preserving the range operator (`^`, `~`, …)
|
|
89
|
-
* Returns null when the spec is not a plain semver spec (→ skip it) or
|
|
97
|
+
* Rewrite a dependency version spec to `toVersion`, preserving the range operator (`^`, `~`, …), or
|
|
98
|
+
* dropping it when `exact`. Returns null when the spec is not a plain semver spec (→ skip it) or
|
|
99
|
+
* already equals the target.
|
|
90
100
|
*/
|
|
91
|
-
export function rewriteVersionSpec(spec: string, toVersion: string): string | null {
|
|
101
|
+
export function rewriteVersionSpec(spec: string, toVersion: string, exact = false): string | null {
|
|
92
102
|
const match = SEMVER_SPEC.exec(spec.trim())
|
|
93
103
|
if (!match) return null
|
|
94
|
-
const operator = match[1] ?? ""
|
|
104
|
+
const operator = exact ? "" : (match[1] ?? "")
|
|
95
105
|
const next = `${operator}${toVersion}`
|
|
96
106
|
return next === spec ? null : next
|
|
97
107
|
}
|
|
98
108
|
|
|
109
|
+
/** The version a plain semver spec names (`^2.4.1` → `2.4.1`), or null for any other spec. */
|
|
110
|
+
export function specVersion(spec: string): string | null {
|
|
111
|
+
const match = SEMVER_SPEC.exec(spec.trim())
|
|
112
|
+
if (!match) return null
|
|
113
|
+
return `${match[2]}.${match[3]}.${match[4]}${match[5] === undefined ? "" : `-${match[5]}`}`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// A release target: a bare version, never a range, so it can be pinned and printed in a command.
|
|
117
|
+
const RELEASE_VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/
|
|
118
|
+
|
|
99
119
|
/** Numeric core (major, minor, patch) of a semver spec, ignoring the range operator + prerelease/build. */
|
|
100
120
|
export function specVersionTuple(spec: string): readonly [number, number, number] | null {
|
|
101
121
|
const match = SEMVER_SPEC.exec(spec.trim())
|
|
@@ -168,6 +188,7 @@ export function pinSweepText(
|
|
|
168
188
|
text: string,
|
|
169
189
|
rules: readonly { match: string; to: string }[],
|
|
170
190
|
allowDowngrade = false,
|
|
191
|
+
exact = false,
|
|
171
192
|
): {
|
|
172
193
|
text: string
|
|
173
194
|
changes: Array<Omit<PinChange, "file">>
|
|
@@ -191,7 +212,7 @@ export function pinSweepText(
|
|
|
191
212
|
(r) => name === r.match || (r.match.endsWith("/") && name.startsWith(r.match)),
|
|
192
213
|
)
|
|
193
214
|
if (!rule) continue
|
|
194
|
-
const next = rewriteVersionSpec(rawSpec, rule.to)
|
|
215
|
+
const next = rewriteVersionSpec(rawSpec, rule.to, exact)
|
|
195
216
|
if (next === null) continue
|
|
196
217
|
// Refuse a rollback: `upgrade <version>` pins an exact target, so an OLD target on a newer install
|
|
197
218
|
// walks dependencies backward (e.g. ^2.3.0 → ^2.0.0) and can break a shared-package peer range.
|
|
@@ -270,12 +291,12 @@ function rewriteDependencyEntry(
|
|
|
270
291
|
let start = match.index
|
|
271
292
|
let end = match.index + match[0].length
|
|
272
293
|
let after = end
|
|
273
|
-
while (
|
|
294
|
+
while (/\s/.test(body[after] ?? "")) after++
|
|
274
295
|
if (body[after] === ",") {
|
|
275
296
|
end = after + 1
|
|
276
297
|
} else {
|
|
277
298
|
let before = start - 1
|
|
278
|
-
while (before >= 0 &&
|
|
299
|
+
while (before >= 0 && /\s/.test(body[before] ?? "")) before--
|
|
279
300
|
if (body[before] === ",") start = before
|
|
280
301
|
}
|
|
281
302
|
nextBody = body.slice(0, start) + body.slice(end)
|
|
@@ -287,6 +308,7 @@ function rewriteDependencyEntry(
|
|
|
287
308
|
export function moveDependenciesText(
|
|
288
309
|
text: string,
|
|
289
310
|
moves: readonly { from: string; to: string; toVersion: string }[],
|
|
311
|
+
exact = false,
|
|
290
312
|
): { text: string; changes: Array<Omit<DependencyMoveChange, "file">> } {
|
|
291
313
|
let out = text
|
|
292
314
|
const changes: Array<Omit<DependencyMoveChange, "file">> = []
|
|
@@ -304,7 +326,7 @@ export function moveDependenciesText(
|
|
|
304
326
|
if (!Object.hasOwn(dependencyMap, move.from)) continue
|
|
305
327
|
const fromVersion = dependencyMap[move.from]
|
|
306
328
|
if (typeof fromVersion !== "string") continue
|
|
307
|
-
const movedVersion = rewriteVersionSpec(fromVersion, move.toVersion) ?? fromVersion
|
|
329
|
+
const movedVersion = rewriteVersionSpec(fromVersion, move.toVersion, exact) ?? fromVersion
|
|
308
330
|
// Dependency fields have different install semantics. A devDependency does not satisfy a runtime
|
|
309
331
|
// dependency, so only deduplicate the successor inside the same field.
|
|
310
332
|
const targetExists = Object.hasOwn(dependencyMap, move.to)
|
|
@@ -329,6 +351,12 @@ export function moveDependenciesText(
|
|
|
329
351
|
})
|
|
330
352
|
}
|
|
331
353
|
}
|
|
354
|
+
// The edit is textual; a result that no longer parses is never written.
|
|
355
|
+
try {
|
|
356
|
+
JSON.parse(out)
|
|
357
|
+
} catch {
|
|
358
|
+
return { text, changes: [] }
|
|
359
|
+
}
|
|
332
360
|
return { text: out, changes }
|
|
333
361
|
}
|
|
334
362
|
|
|
@@ -358,10 +386,15 @@ export function applyImportMoves(
|
|
|
358
386
|
}
|
|
359
387
|
|
|
360
388
|
const SOURCE_GLOB = "**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}"
|
|
361
|
-
const
|
|
389
|
+
const IGNORED_DIRECTORIES = new Set(["node_modules", "dist", "build", ".git", "coverage", ".next"])
|
|
362
390
|
|
|
391
|
+
// Any directory on the path counts, the top-level one included (`dist/index.js`). Bun's glob yields
|
|
392
|
+
// `dist\index.js` on Windows.
|
|
363
393
|
const isIgnored = (path: string): boolean =>
|
|
364
|
-
path
|
|
394
|
+
path
|
|
395
|
+
.split(/[\\/]/)
|
|
396
|
+
.slice(0, -1)
|
|
397
|
+
.some((segment) => IGNORED_DIRECTORIES.has(segment))
|
|
365
398
|
|
|
366
399
|
function scan(cwd: string, pattern: string): string[] {
|
|
367
400
|
const glob = new Glob(pattern)
|
|
@@ -372,12 +405,89 @@ function scan(cwd: string, pattern: string): string[] {
|
|
|
372
405
|
return out.sort() // deterministic order
|
|
373
406
|
}
|
|
374
407
|
|
|
408
|
+
// The fixed version group: every package released together at one version.
|
|
409
|
+
const isFixedGroup = (name: string): boolean =>
|
|
410
|
+
name.startsWith("@nifrajs/") || name === "nifra" || name === "create-nifra"
|
|
411
|
+
|
|
412
|
+
const groupPins = (version: string): UpgradeRecipe["pins"] => [
|
|
413
|
+
{ match: "@nifrajs/", to: version },
|
|
414
|
+
{ match: "create-nifra", to: version },
|
|
415
|
+
{ match: "nifra", to: version },
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* The lowest fixed-group version any package.json under `cwd` declares with a plain semver spec, or
|
|
420
|
+
* null when none does. Recipes newer than it still have edits to make somewhere in the workspace.
|
|
421
|
+
*/
|
|
422
|
+
export function installedGroupVersion(cwd: string): string | null {
|
|
423
|
+
let lowest: string | null = null
|
|
424
|
+
for (const rel of scan(cwd, "**/package.json")) {
|
|
425
|
+
let parsed: Record<string, unknown>
|
|
426
|
+
try {
|
|
427
|
+
parsed = JSON.parse(readFileSync(join(cwd, rel), "utf8")) as Record<string, unknown>
|
|
428
|
+
} catch {
|
|
429
|
+
continue
|
|
430
|
+
}
|
|
431
|
+
for (const field of DEP_FIELDS) {
|
|
432
|
+
const deps = parsed[field]
|
|
433
|
+
if (typeof deps !== "object" || deps === null) continue
|
|
434
|
+
for (const [name, spec] of Object.entries(deps as Record<string, unknown>)) {
|
|
435
|
+
if (!isFixedGroup(name) || typeof spec !== "string") continue
|
|
436
|
+
const version = specVersion(spec)
|
|
437
|
+
if (version !== null && (lowest === null || compareSemverSpec(version, lowest) < 0)) {
|
|
438
|
+
lowest = version
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return lowest
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** One recipe that applies every release recipe after `installed` up to `target`, oldest first. */
|
|
447
|
+
export interface ChainedRecipe extends UpgradeRecipe {
|
|
448
|
+
/** The release recipes it applies, oldest first. */
|
|
449
|
+
readonly steps: readonly string[]
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Chain the recipes after `installed` (exclusive) up to `target` (inclusive): their dependency and
|
|
454
|
+
* import moves in release order, their notes labeled by release, and the fixed group pinned to
|
|
455
|
+
* `target`. Without a known installed version only the target's own recipe applies.
|
|
456
|
+
*/
|
|
457
|
+
export function chainRecipes(target: string, installed: string | null): ChainedRecipe {
|
|
458
|
+
const steps =
|
|
459
|
+
installed === null
|
|
460
|
+
? getRecipe(target) === undefined
|
|
461
|
+
? []
|
|
462
|
+
: [target]
|
|
463
|
+
: listRecipeVersions().filter(
|
|
464
|
+
(version) =>
|
|
465
|
+
compareSemverSpec(version, installed) > 0 && compareSemverSpec(version, target) <= 0,
|
|
466
|
+
)
|
|
467
|
+
const recipes = steps.flatMap((version) => getRecipe(version) ?? [])
|
|
468
|
+
// A later rule for the same match replaces an earlier one; the target's pins win last.
|
|
469
|
+
const pins = new Map<string, string>()
|
|
470
|
+
for (const recipe of recipes) for (const rule of recipe.pins) pins.set(rule.match, rule.to)
|
|
471
|
+
for (const rule of getRecipe(target)?.pins ?? groupPins(target)) pins.set(rule.match, rule.to)
|
|
472
|
+
return {
|
|
473
|
+
version: target,
|
|
474
|
+
steps,
|
|
475
|
+
pins: [...pins].map(([match, to]) => ({ match, to })),
|
|
476
|
+
dependencyMoves: recipes.flatMap((recipe) => recipe.dependencyMoves ?? []),
|
|
477
|
+
importMoves: recipes.flatMap((recipe) => recipe.importMoves),
|
|
478
|
+
notes: recipes.flatMap((recipe) =>
|
|
479
|
+
(recipe.notes ?? []).map((note) => `${recipe.version}: ${note}`),
|
|
480
|
+
),
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
375
484
|
/** Compute the plan (and, when `write`, apply it) for a target recipe against `cwd`. */
|
|
376
485
|
export function computeUpgrade(
|
|
377
486
|
cwd: string,
|
|
378
487
|
recipe: UpgradeRecipe,
|
|
379
488
|
write: boolean,
|
|
380
489
|
allowDowngrade = false,
|
|
490
|
+
exact = false,
|
|
381
491
|
): UpgradePlan {
|
|
382
492
|
const pins: PinChange[] = []
|
|
383
493
|
const dependencyMoves: DependencyMoveChange[] = []
|
|
@@ -388,9 +498,9 @@ export function computeUpgrade(
|
|
|
388
498
|
for (const rel of scan(cwd, "**/package.json")) {
|
|
389
499
|
const abs = join(cwd, rel)
|
|
390
500
|
const original = readFileSync(abs, "utf8")
|
|
391
|
-
const moved = moveDependenciesText(original, recipe.dependencyMoves ?? [])
|
|
501
|
+
const moved = moveDependenciesText(original, recipe.dependencyMoves ?? [], exact)
|
|
392
502
|
for (const change of moved.changes) dependencyMoves.push({ file: rel, ...change })
|
|
393
|
-
const pinned = pinSweepText(moved.text, recipe.pins, allowDowngrade)
|
|
503
|
+
const pinned = pinSweepText(moved.text, recipe.pins, allowDowngrade, exact)
|
|
394
504
|
for (const change of pinned.changes) pins.push({ file: rel, ...change })
|
|
395
505
|
for (const d of pinned.downgrades) downgrades.push({ file: rel, ...d })
|
|
396
506
|
if (write && pinned.text !== original) writeFileSync(abs, pinned.text)
|
|
@@ -418,10 +528,22 @@ export function computeUpgrade(
|
|
|
418
528
|
}
|
|
419
529
|
}
|
|
420
530
|
|
|
421
|
-
function renderPlan(
|
|
531
|
+
function renderPlan(
|
|
532
|
+
plan: UpgradePlan,
|
|
533
|
+
write: boolean,
|
|
534
|
+
from: string | null,
|
|
535
|
+
steps: readonly string[],
|
|
536
|
+
): string {
|
|
422
537
|
const lines: string[] = []
|
|
423
538
|
const verb = write ? "Applied" : "Planned"
|
|
424
539
|
lines.push(`nifra upgrade → ${plan.version} (${write ? "write" : "dry-run"})`)
|
|
540
|
+
if (from !== null) {
|
|
541
|
+
lines.push(
|
|
542
|
+
steps.length === 0
|
|
543
|
+
? `From ${from}; no release recipe in between.`
|
|
544
|
+
: `From ${from}, applying the ${steps.join(", ")} recipe${steps.length === 1 ? "" : "s"}.`,
|
|
545
|
+
)
|
|
546
|
+
}
|
|
425
547
|
lines.push("")
|
|
426
548
|
if (
|
|
427
549
|
plan.pins.length === 0 &&
|
|
@@ -471,24 +593,73 @@ function renderDowngradeRefusal(version: string, downgrades: readonly PinChange[
|
|
|
471
593
|
].join("\n")
|
|
472
594
|
}
|
|
473
595
|
|
|
596
|
+
/** The targets this CLI applies: each release recipe up to its own version, and that version. */
|
|
597
|
+
export function upgradeTargets(cliVersion: string): string[] {
|
|
598
|
+
const versions = listRecipeVersions().filter(
|
|
599
|
+
(version) => compareSemverSpec(version, cliVersion) <= 0,
|
|
600
|
+
)
|
|
601
|
+
if (!versions.includes(cliVersion)) versions.push(cliVersion)
|
|
602
|
+
return versions.sort(compareSemverSpec)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/** The command that runs `version`'s own CLI with the same flags. `version` is a validated release. */
|
|
606
|
+
function newerCliCommand(version: string, options: UpgradeOptions): string {
|
|
607
|
+
const flags = [
|
|
608
|
+
options.write === true ? "--write" : "",
|
|
609
|
+
options.exact === true ? "--exact" : "",
|
|
610
|
+
options.allowDowngrade === true ? "--allow-downgrade" : "",
|
|
611
|
+
options.verify === false ? "--no-verify" : "",
|
|
612
|
+
options.json === true ? "--json" : "",
|
|
613
|
+
].filter((flag) => flag !== "")
|
|
614
|
+
return ["bunx", `@nifrajs/cli@${version}`, "upgrade", version, ...flags].join(" ")
|
|
615
|
+
}
|
|
616
|
+
|
|
474
617
|
/** CLI entry. Returns false (→ non-zero exit) on an unknown version, no project, or a failed verify. */
|
|
475
618
|
export async function runUpgrade(cwd: string, options: UpgradeOptions): Promise<boolean> {
|
|
619
|
+
const cliVersion = options.cliVersion ?? listRecipeVersions().at(-1) ?? "0.0.0"
|
|
620
|
+
const targets = upgradeTargets(cliVersion)
|
|
476
621
|
if (options.list) {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
622
|
+
if (options.json) console.log(JSON.stringify({ versions: targets, cliVersion }, null, 2))
|
|
623
|
+
else {
|
|
624
|
+
console.log(
|
|
625
|
+
`available upgrade targets:\n${targets.map((v) => ` ${v}`).join("\n")}\n\n` +
|
|
626
|
+
`A newer release upgrades with its own CLI: bunx @nifrajs/cli@<version> upgrade <version>`,
|
|
627
|
+
)
|
|
628
|
+
}
|
|
480
629
|
return true
|
|
481
630
|
}
|
|
482
631
|
|
|
483
632
|
const { version } = options
|
|
484
633
|
if (version === undefined) {
|
|
485
|
-
console.error(
|
|
634
|
+
console.error(
|
|
635
|
+
`[nifra] upgrade needs a target version, e.g. \`nifra upgrade ${cliVersion}\` (or --list)`,
|
|
636
|
+
)
|
|
637
|
+
return false
|
|
638
|
+
}
|
|
639
|
+
if (!RELEASE_VERSION.test(version)) {
|
|
640
|
+
console.error(`[nifra] ${JSON.stringify(version)} is not a release version, e.g. ${cliVersion}`)
|
|
641
|
+
return false
|
|
642
|
+
}
|
|
643
|
+
if (compareSemverSpec(version, cliVersion) > 0) {
|
|
644
|
+
// Recipes ship with the CLI: only the target release's CLI knows every recipe up to it. Print its
|
|
645
|
+
// command rather than downloading and running code on the user's behalf.
|
|
646
|
+
const command = newerCliCommand(version, options)
|
|
647
|
+
if (options.json) {
|
|
648
|
+
console.log(
|
|
649
|
+
JSON.stringify({ version, cliVersion, refused: "newer-than-cli", command }, null, 2),
|
|
650
|
+
)
|
|
651
|
+
} else {
|
|
652
|
+
console.error(
|
|
653
|
+
`nifra upgrade → ${version}: this CLI is ${cliVersion} and carries the recipes up to it.\n` +
|
|
654
|
+
`Run the ${version} CLI, which carries every recipe up to ${version}:\n\n ${command}`,
|
|
655
|
+
)
|
|
656
|
+
}
|
|
486
657
|
return false
|
|
487
658
|
}
|
|
488
|
-
|
|
489
|
-
if (!recipe) {
|
|
659
|
+
if (!targets.includes(version)) {
|
|
490
660
|
console.error(
|
|
491
|
-
`[nifra] no upgrade recipe for ${version}.
|
|
661
|
+
`[nifra] no upgrade recipe for ${version}. This CLI upgrades to: ${targets.join(", ")}. ` +
|
|
662
|
+
`For ${version} itself, run: ${newerCliCommand(version, options)}`,
|
|
492
663
|
)
|
|
493
664
|
return false
|
|
494
665
|
}
|
|
@@ -505,24 +676,33 @@ export async function runUpgrade(cwd: string, options: UpgradeOptions): Promise<
|
|
|
505
676
|
|
|
506
677
|
const write = options.write === true
|
|
507
678
|
const allowDowngrade = options.allowDowngrade === true
|
|
679
|
+
const exact = options.exact === true
|
|
680
|
+
const from = installedGroupVersion(cwd)
|
|
681
|
+
const recipe = chainRecipes(version, from)
|
|
508
682
|
|
|
509
683
|
// Fail-closed on rollback: a dry pass writes nothing, so we can refuse before any package.json is touched.
|
|
510
|
-
const preview = computeUpgrade(cwd, recipe, false, allowDowngrade)
|
|
684
|
+
const preview = computeUpgrade(cwd, recipe, false, allowDowngrade, exact)
|
|
511
685
|
if (preview.downgrades.length > 0) {
|
|
512
686
|
if (options.json) {
|
|
513
|
-
console.log(
|
|
687
|
+
console.log(
|
|
688
|
+
JSON.stringify(
|
|
689
|
+
{ ...preview, from, steps: recipe.steps, write: false, refused: "downgrade" },
|
|
690
|
+
null,
|
|
691
|
+
2,
|
|
692
|
+
),
|
|
693
|
+
)
|
|
514
694
|
} else {
|
|
515
695
|
console.error(renderDowngradeRefusal(recipe.version, preview.downgrades))
|
|
516
696
|
}
|
|
517
697
|
return false
|
|
518
698
|
}
|
|
519
699
|
|
|
520
|
-
const plan = write ? computeUpgrade(cwd, recipe, true, allowDowngrade) : preview
|
|
700
|
+
const plan = write ? computeUpgrade(cwd, recipe, true, allowDowngrade, exact) : preview
|
|
521
701
|
|
|
522
702
|
if (options.json) {
|
|
523
|
-
console.log(JSON.stringify({ ...plan, write }, null, 2))
|
|
703
|
+
console.log(JSON.stringify({ ...plan, from, steps: recipe.steps, write }, null, 2))
|
|
524
704
|
} else {
|
|
525
|
-
console.log(renderPlan(plan, write))
|
|
705
|
+
console.log(renderPlan(plan, write, from, recipe.steps))
|
|
526
706
|
}
|
|
527
707
|
|
|
528
708
|
// Verify only makes sense once edits are on disk. Default on after --write; opt out with --no-verify.
|