@nifrajs/cli 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +25 -25
  2. package/dist/assure.d.ts +2 -2
  3. package/dist/assure.d.ts.map +1 -1
  4. package/dist/assure.js +17 -10
  5. package/dist/assure.js.map +1 -1
  6. package/dist/capabilities-tool.d.ts +7 -0
  7. package/dist/capabilities-tool.d.ts.map +1 -1
  8. package/dist/capabilities-tool.js +68 -16
  9. package/dist/capabilities-tool.js.map +1 -1
  10. package/dist/check.d.ts +61 -22
  11. package/dist/check.d.ts.map +1 -1
  12. package/dist/check.js +336 -87
  13. package/dist/check.js.map +1 -1
  14. package/dist/cli.d.ts +30 -2
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +148 -39
  17. package/dist/cli.js.map +1 -1
  18. package/dist/diff-tool.d.ts +3 -3
  19. package/dist/diff-tool.js +7 -7
  20. package/dist/docs-search.d.ts +3 -3
  21. package/dist/docs-search.js +6 -6
  22. package/dist/doctor.d.ts +3 -3
  23. package/dist/doctor.d.ts.map +1 -1
  24. package/dist/doctor.js +12 -8
  25. package/dist/doctor.js.map +1 -1
  26. package/dist/examples.d.ts +5 -5
  27. package/dist/examples.js +5 -5
  28. package/dist/init-agents.d.ts +14 -14
  29. package/dist/init-agents.js +14 -14
  30. package/dist/internal/typescript-import.d.ts +24 -0
  31. package/dist/internal/typescript-import.d.ts.map +1 -0
  32. package/dist/internal/typescript-import.js +32 -0
  33. package/dist/internal/typescript-import.js.map +1 -0
  34. package/dist/introspect.d.ts +12 -12
  35. package/dist/introspect.js +24 -24
  36. package/dist/learn.d.ts +32 -0
  37. package/dist/learn.d.ts.map +1 -0
  38. package/dist/learn.js +127 -0
  39. package/dist/learn.js.map +1 -0
  40. package/dist/levels-tool.d.ts +8 -8
  41. package/dist/levels-tool.d.ts.map +1 -1
  42. package/dist/levels-tool.js +33 -30
  43. package/dist/levels-tool.js.map +1 -1
  44. package/dist/load.d.ts +6 -6
  45. package/dist/load.d.ts.map +1 -1
  46. package/dist/load.js +15 -7
  47. package/dist/load.js.map +1 -1
  48. package/dist/manifest-tool.d.ts +1 -1
  49. package/dist/manifest-tool.d.ts.map +1 -1
  50. package/dist/manifest-tool.js +4 -2
  51. package/dist/manifest-tool.js.map +1 -1
  52. package/dist/mcp-app-tool.d.ts +14 -0
  53. package/dist/mcp-app-tool.d.ts.map +1 -0
  54. package/dist/mcp-app-tool.js +80 -0
  55. package/dist/mcp-app-tool.js.map +1 -0
  56. package/dist/mcp-docs-tools.d.ts +1 -1
  57. package/dist/mcp-docs-tools.d.ts.map +1 -1
  58. package/dist/mcp-docs-tools.js +25 -9
  59. package/dist/mcp-docs-tools.js.map +1 -1
  60. package/dist/mcp-http.d.ts +14 -10
  61. package/dist/mcp-http.d.ts.map +1 -1
  62. package/dist/mcp-http.js +30 -16
  63. package/dist/mcp-http.js.map +1 -1
  64. package/dist/mcp-protocol.d.ts +2 -2
  65. package/dist/mcp-protocol.js +2 -2
  66. package/dist/mcp-render.d.ts +3 -3
  67. package/dist/mcp-render.js +6 -6
  68. package/dist/mcp-run.d.ts +2 -2
  69. package/dist/mcp-run.js +2 -2
  70. package/dist/mcp.d.ts +28 -14
  71. package/dist/mcp.d.ts.map +1 -1
  72. package/dist/mcp.js +263 -45
  73. package/dist/mcp.js.map +1 -1
  74. package/dist/openapi-tool.d.ts +1 -1
  75. package/dist/openapi-tool.js +2 -2
  76. package/dist/pipeline-guard.d.ts +1 -1
  77. package/dist/pipeline-guard.js +1 -1
  78. package/dist/port.d.ts +9 -9
  79. package/dist/port.js +27 -27
  80. package/dist/recipes/1.8.0.d.ts +2 -2
  81. package/dist/recipes/1.8.0.js +5 -5
  82. package/dist/recipes/index.d.ts +2 -2
  83. package/dist/recipes/index.js +1 -1
  84. package/dist/scaffold.d.ts +3 -3
  85. package/dist/scaffold.js +8 -8
  86. package/dist/sync-routes.d.ts +28 -0
  87. package/dist/sync-routes.d.ts.map +1 -0
  88. package/dist/sync-routes.js +55 -0
  89. package/dist/sync-routes.js.map +1 -0
  90. package/dist/types-search.d.ts +5 -5
  91. package/dist/types-search.js +7 -7
  92. package/dist/upgrade.d.ts +20 -9
  93. package/dist/upgrade.d.ts.map +1 -1
  94. package/dist/upgrade.js +117 -19
  95. package/dist/upgrade.js.map +1 -1
  96. package/dist/verification.d.ts +51 -0
  97. package/dist/verification.d.ts.map +1 -0
  98. package/dist/verification.js +80 -0
  99. package/dist/verification.js.map +1 -0
  100. package/docs/examples.json +174 -90
  101. package/docs/llms-full.txt +1771 -748
  102. package/docs/types.json +2027 -3089
  103. package/package.json +22 -11
  104. package/src/assure.ts +17 -18
  105. package/src/capabilities-tool.ts +82 -16
  106. package/src/check.ts +410 -104
  107. package/src/cli.ts +161 -41
  108. package/src/diff-tool.ts +8 -8
  109. package/src/docs-search.ts +6 -6
  110. package/src/doctor.ts +18 -10
  111. package/src/examples.ts +7 -7
  112. package/src/init-agents.ts +17 -17
  113. package/src/internal/typescript-import.ts +33 -0
  114. package/src/introspect.ts +28 -28
  115. package/src/learn.ts +152 -0
  116. package/src/levels-tool.ts +37 -31
  117. package/src/load.ts +21 -12
  118. package/src/manifest-tool.ts +4 -2
  119. package/src/mcp-app-tool.ts +86 -0
  120. package/src/mcp-docs-tools.ts +26 -9
  121. package/src/mcp-http.ts +31 -18
  122. package/src/mcp-protocol.ts +2 -2
  123. package/src/mcp-render.ts +6 -6
  124. package/src/mcp-run.ts +2 -2
  125. package/src/mcp.ts +319 -45
  126. package/src/openapi-tool.ts +2 -2
  127. package/src/pipeline-guard.ts +1 -1
  128. package/src/port.ts +31 -31
  129. package/src/recipes/1.8.0.ts +5 -5
  130. package/src/recipes/index.ts +2 -2
  131. package/src/scaffold.ts +8 -8
  132. package/src/sync-routes.ts +69 -0
  133. package/src/types-search.ts +8 -8
  134. package/src/upgrade.ts +137 -19
  135. package/src/verification.ts +123 -0
package/dist/scaffold.js CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
- * `nifra_scaffold` — turn a URL path into the correct `routes/` file (the convention an agent most often
2
+ * `nifra_scaffold` - turn a URL path into the correct `routes/` file (the convention an agent most often
3
3
  * gets wrong) + a minimal, contract-correct stub. The mapping is the inverse of @nifrajs/web's
4
4
  * `filePathToPatterns`: `:id`/`[id]` → `[id]`, `*rest`/`[...rest]` → `[...rest]`, `[[lang]]` optional,
5
5
  * `/` → `index`. The framework (→ file extension) comes from the project's `clientModule`.
6
6
  *
7
- * Page stubs are emitted only for the JSX family (react/preact/solid — one shared, verified shape);
7
+ * Page stubs are emitted only for the JSX family (react/preact/solid - one shared, verified shape);
8
8
  * for vue/svelte/vanilla we return the correct PATH + the route-module contract and point at
9
9
  * `nifra_example` for the body, rather than hand-writing an SFC we can't typecheck here.
10
10
  */
@@ -49,17 +49,17 @@ export function routePathToFile(urlPath, ext) {
49
49
  return `routes/${segments.map(segmentToFile).join("/")}.${ext}`;
50
50
  }
51
51
  const ROUTE_CONTRACT = `A route module may export:
52
- - \`export default function Page(props: { data: LoaderData<typeof loader> }) { … }\` — the page component.
53
- - \`export async function loader({ params, request, api }: LoaderArgs<typeof backend>) { … }\` — server-only; data for SSR. Reach the backend via \`api\` (typed) / DB via the backend, NEVER a top-level server-only import.
54
- - \`export async function action({ request, api }: ActionArgs<typeof backend>) { … }\` — server-only; handles the form POST.
55
- - \`export const meta = { title, meta:[…] }\` — head tags.
52
+ - \`export default function Page(props: { data: LoaderData<typeof loader> }) { … }\` - the page component.
53
+ - \`export async function loader({ params, request, api }: LoaderArgs<typeof backend>) { … }\` - server-only; data for SSR. Reach the backend via \`api\` (typed) / DB via the backend, NEVER a top-level server-only import.
54
+ - \`export async function action({ request, api }: ActionArgs<typeof backend>) { … }\` - server-only; handles the form POST.
55
+ - \`export const meta = { title, meta:[…] }\` - head tags.
56
56
  Path params are typed on \`params\`.`;
57
57
  function jsxStub(file, params) {
58
58
  const paramsNote = params.length > 0 ? `params.${params.join(", params.")}` : "no path params on this route";
59
59
  const loaderLine = params.length > 0
60
60
  ? `// export async function loader({ params, api }: LoaderArgs<typeof backend>) { return { /* fetch by ${params[0]} */ } }`
61
61
  : `// export async function loader({ api }: LoaderArgs<typeof backend>) { return {} }`;
62
- return `// ${file} — server-only loader/action allowed; never top-level-import server-only code (DB/secrets).
62
+ return `// ${file} - server-only loader/action allowed; never top-level-import server-only code (DB/secrets).
63
63
  // Available here: ${paramsNote}. Fetch data in a loader via the typed \`api\`; see nifra_example("loader").
64
64
  ${loaderLine}
65
65
  export default function Page() {
@@ -125,7 +125,7 @@ export async function writeScaffoldRoute(cwd, urlPath, framework) {
125
125
  }
126
126
  return { ...result, written: true };
127
127
  }
128
- /** Render the tool result as markdown — the file path, the stub (if any), and the contract note. */
128
+ /** Render the tool result as markdown - the file path, the stub (if any), and the contract note. */
129
129
  export function renderScaffold(urlPath, framework) {
130
130
  let r;
131
131
  try {
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `nifra sync-routes` - regenerate the route-search types (`nifra-routes.d.ts`) from the current `routes/`
3
+ * tree, so `navigate({ to, search })` is typed against each static route's `searchSchema`. It runs
4
+ * `@nifrajs/web`'s `generateRouteSearchTypes` over the discovered routes and writes the `.d.ts` at the
5
+ * project root; include it in your tsconfig's `include`. Because it is regenerated from the route files, a
6
+ * stale navigate search shape becomes a `tsc` error - run it after adding/renaming a route or changing a
7
+ * `searchSchema` (a pure file write, no build). A dynamic route (`:param`/catch-all) is omitted, since its
8
+ * pattern is not a concrete `to`; those keep the loose object form.
9
+ */
10
+ /** The committed route-types file `sync-routes` writes, so `nifra check` can recognise + drift-check it. */
11
+ export declare const ROUTE_TYPES_FILE = "nifra-routes.d.ts";
12
+ export interface SyncRoutesResult {
13
+ /** The written `.d.ts` (relative to the project root). */
14
+ readonly file: string;
15
+ /** True when the file's content changed and it was rewritten. */
16
+ readonly changed: boolean;
17
+ /** How many static routes got a typed entry. */
18
+ readonly typedRoutes: number;
19
+ }
20
+ /**
21
+ * Generate the route-search types `.d.ts` from `<cwd>/routes` (the nifra convention). Returns `null` when
22
+ * there is no `routes/` directory (an API-only project - nothing to type). Never loads `framework.ts`: it
23
+ * needs only the route tree, so it works before a build and on a broken framework config.
24
+ */
25
+ export declare function syncRouteTypes(cwd: string): Promise<SyncRoutesResult | null>;
26
+ /** CLI entry: write/refresh the route-types file and print a one-line summary + the tsconfig reminder. */
27
+ export declare function runSyncRoutes(cwd: string): Promise<boolean>;
28
+ //# sourceMappingURL=sync-routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-routes.d.ts","sourceRoot":"","sources":["../src/sync-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,4GAA4G;AAC5G,eAAO,MAAM,gBAAgB,sBAAsB,CAAA;AAEnD,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,iEAAiE;IACjE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,gDAAgD;IAChD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAC7B;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAkBlF;AAED,0GAA0G;AAC1G,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAiBjE"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * `nifra sync-routes` - regenerate the route-search types (`nifra-routes.d.ts`) from the current `routes/`
3
+ * tree, so `navigate({ to, search })` is typed against each static route's `searchSchema`. It runs
4
+ * `@nifrajs/web`'s `generateRouteSearchTypes` over the discovered routes and writes the `.d.ts` at the
5
+ * project root; include it in your tsconfig's `include`. Because it is regenerated from the route files, a
6
+ * stale navigate search shape becomes a `tsc` error - run it after adding/renaming a route or changing a
7
+ * `searchSchema` (a pure file write, no build). A dynamic route (`:param`/catch-all) is omitted, since its
8
+ * pattern is not a concrete `to`; those keep the loose object form.
9
+ */
10
+ import { existsSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ /** The committed route-types file `sync-routes` writes, so `nifra check` can recognise + drift-check it. */
13
+ export const ROUTE_TYPES_FILE = "nifra-routes.d.ts";
14
+ /**
15
+ * Generate the route-search types `.d.ts` from `<cwd>/routes` (the nifra convention). Returns `null` when
16
+ * there is no `routes/` directory (an API-only project - nothing to type). Never loads `framework.ts`: it
17
+ * needs only the route tree, so it works before a build and on a broken framework config.
18
+ */
19
+ export async function syncRouteTypes(cwd) {
20
+ const routesDir = join(cwd, "routes");
21
+ if (!existsSync(routesDir))
22
+ return null;
23
+ const { discoverRoutes } = await import("@nifrajs/web/fs");
24
+ const { generateRouteSearchTypes } = await import("@nifrajs/web");
25
+ const manifest = discoverRoutes(routesDir);
26
+ const code = generateRouteSearchTypes(manifest, {
27
+ // A route file is relative to routesDir (`reports.tsx`, `users/index.tsx`); the `.d.ts` sits at the
28
+ // project root, so the `typeof import(...)` specifier is `./routes/<file без extension>`.
29
+ resolve: (file) => `./routes/${file.replace(/\.[^./]+$/, "")}`,
30
+ });
31
+ const outFile = join(cwd, ROUTE_TYPES_FILE);
32
+ const existing = await Bun.file(outFile)
33
+ .text()
34
+ .catch(() => "");
35
+ const changed = existing !== code;
36
+ if (changed)
37
+ await Bun.write(outFile, code);
38
+ return { file: ROUTE_TYPES_FILE, changed, typedRoutes: (code.match(/: SearchOf</g) ?? []).length };
39
+ }
40
+ /** CLI entry: write/refresh the route-types file and print a one-line summary + the tsconfig reminder. */
41
+ export async function runSyncRoutes(cwd) {
42
+ const result = await syncRouteTypes(cwd);
43
+ if (result === null) {
44
+ console.log("nifra sync-routes: no routes/ directory found. Run from your project root.");
45
+ return true;
46
+ }
47
+ const n = result.typedRoutes;
48
+ const routes = `${n} typed route${n === 1 ? "" : "s"}`;
49
+ console.log(result.changed
50
+ ? `✓ wrote ${result.file} (${routes})`
51
+ : `• ${result.file} already in sync (${routes})`);
52
+ console.log(" Include it in your tsconfig's `include` so `navigate({ to, search })` is typed per route.");
53
+ return true;
54
+ }
55
+ //# sourceMappingURL=sync-routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-routes.js","sourceRoot":"","sources":["../src/sync-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACpC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,4GAA4G;AAC5G,MAAM,CAAC,MAAM,gBAAgB,GAAG,mBAAmB,CAAA;AAWnD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,GAAW;IAC9C,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAA;IACrC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC;QAAE,OAAO,IAAI,CAAA;IACvC,MAAM,EAAE,cAAc,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAA;IAC1D,MAAM,EAAE,wBAAwB,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAA;IACjE,MAAM,QAAQ,GAAG,cAAc,CAAC,SAAS,CAAC,CAAA;IAC1C,MAAM,IAAI,GAAG,wBAAwB,CAAC,QAAQ,EAAE;QAC9C,oGAAoG;QACpG,0FAA0F;QAC1F,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,YAAY,IAAI,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,EAAE;KAC/D,CAAC,CAAA;IACF,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAA;IAC3C,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,OAAO,CAAC;SACrC,IAAI,EAAE;SACN,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAA;IAClB,MAAM,OAAO,GAAG,QAAQ,KAAK,IAAI,CAAA;IACjC,IAAI,OAAO;QAAE,MAAM,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,CAAA;IAC3C,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAA;AACpG,CAAC;AAED,0GAA0G;AAC1G,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAW;IAC7C,MAAM,MAAM,GAAG,MAAM,cAAc,CAAC,GAAG,CAAC,CAAA;IACxC,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACpB,OAAO,CAAC,GAAG,CAAC,4EAA4E,CAAC,CAAA;QACzF,OAAO,IAAI,CAAA;IACb,CAAC;IACD,MAAM,CAAC,GAAG,MAAM,CAAC,WAAW,CAAA;IAC5B,MAAM,MAAM,GAAG,GAAG,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAA;IACtD,OAAO,CAAC,GAAG,CACT,MAAM,CAAC,OAAO;QACZ,CAAC,CAAC,WAAW,MAAM,CAAC,IAAI,KAAK,MAAM,GAAG;QACtC,CAAC,CAAC,KAAK,MAAM,CAAC,IAAI,qBAAqB,MAAM,GAAG,CACnD,CAAA;IACD,OAAO,CAAC,GAAG,CACT,6FAA6F,CAC9F,CAAA;IACD,OAAO,IAAI,CAAA;AACb,CAAC"}
@@ -1,7 +1,7 @@
1
1
  /**
2
- * `nifra_types` — the EXACT TypeScript of every exported `@nifrajs/*` symbol. The corpus is `types.json`
2
+ * `nifra_types` - the EXACT TypeScript of every exported `@nifrajs/*` symbol. The corpus is `types.json`
3
3
  * (written into this package by `bun run gen:llms`): each entry's `signature` is the literal declaration
4
- * parsed from the package's BUILT `.d.ts` with the TS compiler — a clean signature, never prose, never
4
+ * parsed from the package's BUILT `.d.ts` with the TS compiler - a clean signature, never prose, never
5
5
  * truncated. So an agent that needs the precise shape of a type (`RateLimitStore`, `RouteSchema`, a
6
6
  * function signature) gets the authoritative source here and never has to read a `.d.ts` file.
7
7
  *
@@ -12,7 +12,7 @@ export interface TypeEntry {
12
12
  readonly name: string;
13
13
  readonly kind: "interface" | "type" | "class" | "function" | "enum" | "const";
14
14
  readonly package: string;
15
- /** The literal declaration text from the `.d.ts` — a clean signature, no implementation. */
15
+ /** The literal declaration text from the `.d.ts` - a clean signature, no implementation. */
16
16
  readonly signature: string;
17
17
  /** The declaration's JSDoc block, if any. */
18
18
  readonly doc?: string;
@@ -20,7 +20,7 @@ export interface TypeEntry {
20
20
  export interface ScoredType extends TypeEntry {
21
21
  readonly score: number;
22
22
  }
23
- /** Exact (case-insensitive) name lookup — returns every declaration with that name (a name can exist in
23
+ /** Exact (case-insensitive) name lookup - returns every declaration with that name (a name can exist in
24
24
  * more than one package). */
25
25
  export declare function lookupType(types: readonly TypeEntry[], name: string): TypeEntry[];
26
26
  /** Keyword-score types: a term in the name is worth far more than one in the signature/doc body. */
@@ -28,7 +28,7 @@ export declare function searchTypes(types: readonly TypeEntry[], query: string,
28
28
  /**
29
29
  * Render the tool result. `name` → the exact declaration(s); else `query` → the top matches; else a
30
30
  * per-package index of type names. The signatures are authoritative (generated from the built `.d.ts`),
31
- * so this is the complete answer — there is no need to read a `.d.ts` file.
31
+ * so this is the complete answer - there is no need to read a `.d.ts` file.
32
32
  */
33
33
  export declare function renderTypesResult(types: readonly TypeEntry[], name: string | undefined, query: string | undefined, limit: number, full?: boolean): string;
34
34
  /** Load the bundled type corpus (`docs/types.json`), resolved relative to this module so it works from
@@ -1,7 +1,7 @@
1
1
  /**
2
- * `nifra_types` — the EXACT TypeScript of every exported `@nifrajs/*` symbol. The corpus is `types.json`
2
+ * `nifra_types` - the EXACT TypeScript of every exported `@nifrajs/*` symbol. The corpus is `types.json`
3
3
  * (written into this package by `bun run gen:llms`): each entry's `signature` is the literal declaration
4
- * parsed from the package's BUILT `.d.ts` with the TS compiler — a clean signature, never prose, never
4
+ * parsed from the package's BUILT `.d.ts` with the TS compiler - a clean signature, never prose, never
5
5
  * truncated. So an agent that needs the precise shape of a type (`RateLimitStore`, `RouteSchema`, a
6
6
  * function signature) gets the authoritative source here and never has to read a `.d.ts` file.
7
7
  *
@@ -9,7 +9,7 @@
9
9
  * signature/doc body; no args → a per-package index of the available type names.
10
10
  */
11
11
  import { countBodyHits, queryTermGroups, tokenize, tokenSetHas } from "./search-terms.js";
12
- /** Exact (case-insensitive) name lookup — returns every declaration with that name (a name can exist in
12
+ /** Exact (case-insensitive) name lookup - returns every declaration with that name (a name can exist in
13
13
  * more than one package). */
14
14
  export function lookupType(types, name) {
15
15
  const wanted = name.trim().toLowerCase();
@@ -36,7 +36,7 @@ export function searchTypes(types, query, limit) {
36
36
  return scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name)).slice(0, limit);
37
37
  }
38
38
  function renderEntry(t) {
39
- const head = `## ${t.name} — ${t.kind}, \`${t.package}\``;
39
+ const head = `## ${t.name} - ${t.kind}, \`${t.package}\``;
40
40
  const doc = t.doc ? `${t.doc}\n` : "";
41
41
  return `${head}\n\n\`\`\`ts\n${doc}${t.signature}\n\`\`\``;
42
42
  }
@@ -76,7 +76,7 @@ function collapseSignature(signature) {
76
76
  }
77
77
  /** A search hit: enough to choose, not the whole declaration. */
78
78
  function renderSearchHit(t) {
79
- const head = `## ${t.name} — ${t.kind}, \`${t.package}\``;
79
+ const head = `## ${t.name} - ${t.kind}, \`${t.package}\``;
80
80
  const summary = summarize(t.doc);
81
81
  const signature = t.signature.length <= SEARCH_SIGNATURE_MAX ? t.signature : collapseSignature(t.signature);
82
82
  const note = t.signature.length <= SEARCH_SIGNATURE_MAX
@@ -87,7 +87,7 @@ function renderSearchHit(t) {
87
87
  /**
88
88
  * Render the tool result. `name` → the exact declaration(s); else `query` → the top matches; else a
89
89
  * per-package index of type names. The signatures are authoritative (generated from the built `.d.ts`),
90
- * so this is the complete answer — there is no need to read a `.d.ts` file.
90
+ * so this is the complete answer - there is no need to read a `.d.ts` file.
91
91
  */
92
92
  export function renderTypesResult(types, name, query, limit, full = false) {
93
93
  if (name !== undefined && name.trim() !== "") {
@@ -115,7 +115,7 @@ export function renderTypesResult(types, name, query, limit, full = false) {
115
115
  .sort(([a], [b]) => a.localeCompare(b))
116
116
  .map(([pkg, names]) => `- **${pkg}**: ${names.sort().join(", ")}`)
117
117
  .join("\n");
118
- 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}`;
118
+ 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}`;
119
119
  }
120
120
  /** Load the bundled type corpus (`docs/types.json`), resolved relative to this module so it works from
121
121
  * `src/` (dev) and `dist/` (published) alike. */
package/dist/upgrade.d.ts CHANGED
@@ -1,20 +1,20 @@
1
1
  /**
2
- * `nifra upgrade <version>` — an executable, per-release upgrade runner. Framework and shared-package
2
+ * `nifra upgrade <version>` - an executable, per-release upgrade runner. Framework and shared-package
3
3
  * bumps otherwise spread the same mechanical edits across every consuming app by hand; a recipe turns
4
4
  * them into `detect → transform → verify`:
5
5
  *
6
- * 1. **pin sweep** — set every matching dependency to the target version across the workspace's
6
+ * 1. **pin sweep** - set every matching dependency to the target version across the workspace's
7
7
  * package.json files, preserving the caret/tilde/exact style and skipping `workspace:`/`link:` specs.
8
- * 2. **dependency moves** — replace removed packages with their supported successor without
8
+ * 2. **dependency moves** - replace removed packages with their supported successor without
9
9
  * reserializing package.json or leaving duplicate dependency keys.
10
- * 3. **import moves** — rewrite exact import specifiers to their updated module paths.
11
- * 4. **verify** — reuse the existing `nifra check` gate; no new verification surface.
10
+ * 3. **import moves** - rewrite exact import specifiers to their updated module paths.
11
+ * 4. **verify** - reuse the existing `nifra check` gate; no new verification surface.
12
12
  *
13
13
  * Dry-run by default (prints the plan, writes nothing); `--write` applies. Fail-closed on an unknown
14
14
  * target version or a missing package.json, and deterministic (same repo + target → same edits).
15
15
  *
16
16
  * Deliberately NOT a codemod engine: transforms are string/specifier-level only. Structural (AST)
17
- * transforms are a future addition — a recipe that needs one is the signal to add the engine, not before.
17
+ * transforms are a future addition - a recipe that needs one is the signal to add the engine, not before.
18
18
  */
19
19
  import { type UpgradeRecipe } from "./recipes/index.js";
20
20
  export interface UpgradeOptions {
@@ -28,6 +28,8 @@ export interface UpgradeOptions {
28
28
  readonly list?: boolean;
29
29
  /** After `--write`, run `nifra check` and fail the command if it fails. Default true. */
30
30
  readonly verify?: boolean;
31
+ /** Permit a target BELOW the installed version (a rollback). Default false → fail-closed. */
32
+ readonly allowDowngrade?: boolean;
31
33
  }
32
34
  export interface PinChange {
33
35
  readonly file: string;
@@ -56,6 +58,8 @@ export interface UpgradePlan {
56
58
  readonly pins: readonly PinChange[];
57
59
  readonly dependencyMoves: readonly DependencyMoveChange[];
58
60
  readonly importMoves: readonly ImportChange[];
61
+ /** Pins refused because the target is BELOW the installed version (a rollback). Empty when allowed. */
62
+ readonly downgrades: readonly PinChange[];
59
63
  readonly notes: readonly string[];
60
64
  }
61
65
  /**
@@ -63,17 +67,24 @@ export interface UpgradePlan {
63
67
  * Returns null when the spec is not a plain semver spec (→ skip it) or already equals the target.
64
68
  */
65
69
  export declare function rewriteVersionSpec(spec: string, toVersion: string): string | null;
70
+ /** Numeric core (major, minor, patch) of a semver spec, ignoring the range operator + prerelease/build. */
71
+ export declare function specVersionTuple(spec: string): readonly [number, number, number] | null;
72
+ /** Compare two semver cores: <0 when a<b, 0 equal, >0 when a>b. */
73
+ export declare function compareSemverCore(a: readonly [number, number, number], b: readonly [number, number, number]): number;
74
+ /** Compare full semver precedence, including prereleases (`2.3.0-beta` < `2.3.0`). */
75
+ export declare function compareSemverSpec(a: string, b: string): number;
66
76
  /**
67
- * Apply pin rules to one package.json's TEXT (format-preserving — targeted string replaces, never a
77
+ * Apply pin rules to one package.json's TEXT (format-preserving - targeted string replaces, never a
68
78
  * JSON re-serialize that would reorder keys or drop comments-as-formatting). Returns the new text and
69
79
  * the changes made.
70
80
  */
71
81
  export declare function pinSweepText(text: string, rules: readonly {
72
82
  match: string;
73
83
  to: string;
74
- }[]): {
84
+ }[], allowDowngrade?: boolean): {
75
85
  text: string;
76
86
  changes: Array<Omit<PinChange, "file">>;
87
+ downgrades: Array<Omit<PinChange, "file">>;
77
88
  };
78
89
  /** Move removed package dependencies without reserializing or reordering package.json. */
79
90
  export declare function moveDependenciesText(text: string, moves: readonly {
@@ -97,7 +108,7 @@ export declare function applyImportMoves(text: string, moves: readonly {
97
108
  }>;
98
109
  };
99
110
  /** Compute the plan (and, when `write`, apply it) for a target recipe against `cwd`. */
100
- export declare function computeUpgrade(cwd: string, recipe: UpgradeRecipe, write: boolean): UpgradePlan;
111
+ export declare function computeUpgrade(cwd: string, recipe: UpgradeRecipe, write: boolean, allowDowngrade?: boolean): UpgradePlan;
101
112
  /** CLI entry. Returns false (→ non-zero exit) on an unknown version, no project, or a failed verify. */
102
113
  export declare function runUpgrade(cwd: string, options: UpgradeOptions): Promise<boolean>;
103
114
  //# sourceMappingURL=upgrade.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../src/upgrade.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"upgrade.d.ts","sourceRoot":"","sources":["../src/upgrade.ts"],"names":[],"mappings":"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"}
package/dist/upgrade.js CHANGED
@@ -1,20 +1,20 @@
1
1
  /**
2
- * `nifra upgrade <version>` — an executable, per-release upgrade runner. Framework and shared-package
2
+ * `nifra upgrade <version>` - an executable, per-release upgrade runner. Framework and shared-package
3
3
  * bumps otherwise spread the same mechanical edits across every consuming app by hand; a recipe turns
4
4
  * them into `detect → transform → verify`:
5
5
  *
6
- * 1. **pin sweep** — set every matching dependency to the target version across the workspace's
6
+ * 1. **pin sweep** - set every matching dependency to the target version across the workspace's
7
7
  * package.json files, preserving the caret/tilde/exact style and skipping `workspace:`/`link:` specs.
8
- * 2. **dependency moves** — replace removed packages with their supported successor without
8
+ * 2. **dependency moves** - replace removed packages with their supported successor without
9
9
  * reserializing package.json or leaving duplicate dependency keys.
10
- * 3. **import moves** — rewrite exact import specifiers to their updated module paths.
11
- * 4. **verify** — reuse the existing `nifra check` gate; no new verification surface.
10
+ * 3. **import moves** - rewrite exact import specifiers to their updated module paths.
11
+ * 4. **verify** - reuse the existing `nifra check` gate; no new verification surface.
12
12
  *
13
13
  * Dry-run by default (prints the plan, writes nothing); `--write` applies. Fail-closed on an unknown
14
14
  * target version or a missing package.json, and deterministic (same repo + target → same edits).
15
15
  *
16
16
  * Deliberately NOT a codemod engine: transforms are string/specifier-level only. Structural (AST)
17
- * transforms are a future addition — a recipe that needs one is the signal to add the engine, not before.
17
+ * transforms are a future addition - a recipe that needs one is the signal to add the engine, not before.
18
18
  */
19
19
  import { readFileSync, writeFileSync } from "node:fs";
20
20
  import { join, relative } from "node:path";
@@ -26,9 +26,9 @@ const DEP_FIELDS = [
26
26
  "peerDependencies",
27
27
  "optionalDependencies",
28
28
  ];
29
- // Only a bare semver spec is rewritten. Anything else — workspace:*, link:/file:, npm: aliases, git
30
- // urls, "*", "latest", or a multi-part range — is intentionally left untouched (skipped, not guessed).
31
- const SEMVER_SPEC = /^([\^~]|>=|<=|>|<|=)?\s*(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
29
+ // Only a bare semver spec is rewritten. Anything else - workspace:*, link:/file:, npm: aliases, git
30
+ // urls, "*", "latest", or a multi-part range - is intentionally left untouched (skipped, not guessed).
31
+ const SEMVER_SPEC = /^([\^~]|>=|<=|>|<|=)?\s*(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
32
32
  /**
33
33
  * Rewrite a dependency version spec to `toVersion`, preserving the range operator (`^`, `~`, …).
34
34
  * Returns null when the spec is not a plain semver spec (→ skip it) or already equals the target.
@@ -41,22 +41,81 @@ export function rewriteVersionSpec(spec, toVersion) {
41
41
  const next = `${operator}${toVersion}`;
42
42
  return next === spec ? null : next;
43
43
  }
44
+ /** Numeric core (major, minor, patch) of a semver spec, ignoring the range operator + prerelease/build. */
45
+ export function specVersionTuple(spec) {
46
+ const match = SEMVER_SPEC.exec(spec.trim());
47
+ if (!match)
48
+ return null;
49
+ return [Number(match[2]), Number(match[3]), Number(match[4])];
50
+ }
51
+ function parseSpecVersion(spec) {
52
+ const match = SEMVER_SPEC.exec(spec.trim());
53
+ if (!match)
54
+ return null;
55
+ const prerelease = (match[5] ?? "").split(".");
56
+ return {
57
+ core: [Number(match[2]), Number(match[3]), Number(match[4])],
58
+ prerelease: match[5] === undefined
59
+ ? []
60
+ : prerelease.map((part) => (/^\d+$/.test(part) ? Number(part) : part)),
61
+ };
62
+ }
63
+ /** Compare two semver cores: <0 when a<b, 0 equal, >0 when a>b. */
64
+ export function compareSemverCore(a, b) {
65
+ return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
66
+ }
67
+ /** Compare full semver precedence, including prereleases (`2.3.0-beta` < `2.3.0`). */
68
+ export function compareSemverSpec(a, b) {
69
+ const left = parseSpecVersion(a);
70
+ const right = parseSpecVersion(b);
71
+ if (left === null || right === null)
72
+ return 0;
73
+ const core = compareSemverCore(left.core, right.core);
74
+ if (core !== 0)
75
+ return core;
76
+ if (left.prerelease.length === 0 || right.prerelease.length === 0) {
77
+ return left.prerelease.length === right.prerelease.length
78
+ ? 0
79
+ : left.prerelease.length === 0
80
+ ? 1
81
+ : -1;
82
+ }
83
+ const length = Math.max(left.prerelease.length, right.prerelease.length);
84
+ for (let i = 0; i < length; i++) {
85
+ const x = left.prerelease[i];
86
+ const y = right.prerelease[i];
87
+ if (x === undefined || y === undefined)
88
+ return x === undefined ? -1 : 1;
89
+ if (typeof x === "number" && typeof y === "number") {
90
+ if (x !== y)
91
+ return x - y;
92
+ }
93
+ else if (typeof x === "number")
94
+ return -1;
95
+ else if (typeof y === "number")
96
+ return 1;
97
+ else if (x !== y)
98
+ return x < y ? -1 : 1;
99
+ }
100
+ return 0;
101
+ }
44
102
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
45
103
  /**
46
- * Apply pin rules to one package.json's TEXT (format-preserving — targeted string replaces, never a
104
+ * Apply pin rules to one package.json's TEXT (format-preserving - targeted string replaces, never a
47
105
  * JSON re-serialize that would reorder keys or drop comments-as-formatting). Returns the new text and
48
106
  * the changes made.
49
107
  */
50
- export function pinSweepText(text, rules) {
108
+ export function pinSweepText(text, rules, allowDowngrade = false) {
51
109
  let parsed;
52
110
  try {
53
111
  parsed = JSON.parse(text);
54
112
  }
55
113
  catch {
56
- return { text, changes: [] };
114
+ return { text, changes: [], downgrades: [] };
57
115
  }
58
116
  let out = text;
59
117
  const changes = [];
118
+ const downgrades = [];
60
119
  for (const field of DEP_FIELDS) {
61
120
  const deps = parsed[field];
62
121
  if (typeof deps !== "object" || deps === null)
@@ -70,6 +129,17 @@ export function pinSweepText(text, rules) {
70
129
  const next = rewriteVersionSpec(rawSpec, rule.to);
71
130
  if (next === null)
72
131
  continue;
132
+ // Refuse a rollback: `upgrade <version>` pins an exact target, so an OLD target on a newer install
133
+ // walks dependencies backward (e.g. ^2.3.0 → ^2.0.0) and can break a shared-package peer range.
134
+ const current = specVersionTuple(rawSpec);
135
+ const target = specVersionTuple(rule.to);
136
+ if (!allowDowngrade &&
137
+ current !== null &&
138
+ target !== null &&
139
+ compareSemverSpec(rawSpec, rule.to) > 0) {
140
+ downgrades.push({ field, name, from: rawSpec, to: next });
141
+ continue;
142
+ }
73
143
  // Replace exactly `"name"<ws>:<ws>"spec"`, preserving the surrounding whitespace.
74
144
  const pattern = new RegExp(`("${escapeRegExp(name)}"\\s*:\\s*")${escapeRegExp(rawSpec)}(")`);
75
145
  const replaced = out.replace(pattern, `$1${next}$2`);
@@ -79,7 +149,7 @@ export function pinSweepText(text, rules) {
79
149
  }
80
150
  }
81
151
  }
82
- return { text: out, changes };
152
+ return { text: out, changes, downgrades };
83
153
  }
84
154
  function dependencyObjectRange(text, field) {
85
155
  const match = new RegExp(`"${field}"\\s*:\\s*\\{`).exec(text);
@@ -221,10 +291,11 @@ function scan(cwd, pattern) {
221
291
  return out.sort(); // deterministic order
222
292
  }
223
293
  /** Compute the plan (and, when `write`, apply it) for a target recipe against `cwd`. */
224
- export function computeUpgrade(cwd, recipe, write) {
294
+ export function computeUpgrade(cwd, recipe, write, allowDowngrade = false) {
225
295
  const pins = [];
226
296
  const dependencyMoves = [];
227
297
  const importMoves = [];
298
+ const downgrades = [];
228
299
  if ((recipe.dependencyMoves?.length ?? 0) > 0 || recipe.pins.length > 0) {
229
300
  for (const rel of scan(cwd, "**/package.json")) {
230
301
  const abs = join(cwd, rel);
@@ -232,9 +303,11 @@ export function computeUpgrade(cwd, recipe, write) {
232
303
  const moved = moveDependenciesText(original, recipe.dependencyMoves ?? []);
233
304
  for (const change of moved.changes)
234
305
  dependencyMoves.push({ file: rel, ...change });
235
- const pinned = pinSweepText(moved.text, recipe.pins);
306
+ const pinned = pinSweepText(moved.text, recipe.pins, allowDowngrade);
236
307
  for (const change of pinned.changes)
237
308
  pins.push({ file: rel, ...change });
309
+ for (const d of pinned.downgrades)
310
+ downgrades.push({ file: rel, ...d });
238
311
  if (write && pinned.text !== original)
239
312
  writeFileSync(abs, pinned.text);
240
313
  }
@@ -257,6 +330,7 @@ export function computeUpgrade(cwd, recipe, write) {
257
330
  pins,
258
331
  dependencyMoves,
259
332
  importMoves,
333
+ downgrades,
260
334
  notes: recipe.notes ?? [],
261
335
  };
262
336
  }
@@ -268,7 +342,7 @@ function renderPlan(plan, write) {
268
342
  if (plan.pins.length === 0 &&
269
343
  plan.dependencyMoves.length === 0 &&
270
344
  plan.importMoves.length === 0) {
271
- lines.push("Already up to date — no changes.");
345
+ lines.push("Already up to date - no changes.");
272
346
  return lines.join("\n");
273
347
  }
274
348
  if (plan.dependencyMoves.length > 0) {
@@ -300,6 +374,18 @@ function renderPlan(plan, write) {
300
374
  lines.push("\nRe-run with --write to apply, then nifra check verifies.");
301
375
  return lines.join("\n").trimEnd();
302
376
  }
377
+ /** Explain a refused rollback: which pins would roll back, why it's blocked, and the escape hatch. */
378
+ function renderDowngradeRefusal(version, downgrades) {
379
+ return [
380
+ `nifra upgrade → ${version}: refusing to DOWNGRADE ${downgrades.length} pin(s) below what's installed.`,
381
+ "",
382
+ ...downgrades.map((d) => ` ${d.file} ${d.name}: ${d.from} → ${d.to} (would roll back)`),
383
+ "",
384
+ "`nifra upgrade <version>` sets each pin to exactly <version>; an older target on a newer install",
385
+ "rolls dependencies backward and can violate a shared-package peer range. Target a version >= the",
386
+ "installed one, or pass --allow-downgrade if the rollback is intended.",
387
+ ].join("\n");
388
+ }
303
389
  /** CLI entry. Returns false (→ non-zero exit) on an unknown version, no project, or a failed verify. */
304
390
  export async function runUpgrade(cwd, options) {
305
391
  if (options.list) {
@@ -325,11 +411,23 @@ export async function runUpgrade(cwd, options) {
325
411
  readFileSync(join(cwd, "package.json"), "utf8");
326
412
  }
327
413
  catch {
328
- console.error(`[nifra] no package.json in ${relative(process.cwd(), cwd) || "."} — run from a project root`);
414
+ console.error(`[nifra] no package.json in ${relative(process.cwd(), cwd) || "."} - run from a project root`);
329
415
  return false;
330
416
  }
331
417
  const write = options.write === true;
332
- const plan = computeUpgrade(cwd, recipe, write);
418
+ const allowDowngrade = options.allowDowngrade === true;
419
+ // Fail-closed on rollback: a dry pass writes nothing, so we can refuse before any package.json is touched.
420
+ const preview = computeUpgrade(cwd, recipe, false, allowDowngrade);
421
+ if (preview.downgrades.length > 0) {
422
+ if (options.json) {
423
+ console.log(JSON.stringify({ ...preview, write: false, refused: "downgrade" }, null, 2));
424
+ }
425
+ else {
426
+ console.error(renderDowngradeRefusal(recipe.version, preview.downgrades));
427
+ }
428
+ return false;
429
+ }
430
+ const plan = write ? computeUpgrade(cwd, recipe, true, allowDowngrade) : preview;
333
431
  if (options.json) {
334
432
  console.log(JSON.stringify({ ...plan, write }, null, 2));
335
433
  }
@@ -342,7 +440,7 @@ export async function runUpgrade(cwd, options) {
342
440
  const ok = await runCheck(cwd, { json: false });
343
441
  if (!ok) {
344
442
  if (!options.json)
345
- console.error("\n[nifra] upgrade applied but `nifra check` failed — review above.");
443
+ console.error("\n[nifra] upgrade applied but `nifra check` failed - review above.");
346
444
  return false;
347
445
  }
348
446
  }