@nifrajs/cli 3.1.0 → 3.3.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 (158) hide show
  1. package/README.md +10 -0
  2. package/dist/assure-hydration.d.ts.map +1 -1
  3. package/dist/assure-hydration.js +6 -1
  4. package/dist/assure-hydration.js.map +1 -1
  5. package/dist/assure.d.ts.map +1 -1
  6. package/dist/assure.js +3 -2
  7. package/dist/assure.js.map +1 -1
  8. package/dist/check-diagnostics.d.ts +165 -0
  9. package/dist/check-diagnostics.d.ts.map +1 -0
  10. package/dist/check-diagnostics.js +1009 -0
  11. package/dist/check-diagnostics.js.map +1 -0
  12. package/dist/check-scan.d.ts +245 -0
  13. package/dist/check-scan.d.ts.map +1 -0
  14. package/dist/check-scan.js +1599 -0
  15. package/dist/check-scan.js.map +1 -0
  16. package/dist/check.d.ts +4 -324
  17. package/dist/check.d.ts.map +1 -1
  18. package/dist/check.js +8 -2366
  19. package/dist/check.js.map +1 -1
  20. package/dist/cli.d.ts.map +1 -1
  21. package/dist/cli.js +247 -10
  22. package/dist/cli.js.map +1 -1
  23. package/dist/command-catalog.d.ts +21 -1
  24. package/dist/command-catalog.d.ts.map +1 -1
  25. package/dist/command-catalog.js +122 -0
  26. package/dist/command-catalog.js.map +1 -1
  27. package/dist/env-file.js +5 -5
  28. package/dist/env-file.js.map +1 -1
  29. package/dist/frontend-guidance.d.ts +57 -0
  30. package/dist/frontend-guidance.d.ts.map +1 -0
  31. package/dist/frontend-guidance.js +195 -0
  32. package/dist/frontend-guidance.js.map +1 -0
  33. package/dist/levels-tool.d.ts.map +1 -1
  34. package/dist/levels-tool.js +6 -0
  35. package/dist/levels-tool.js.map +1 -1
  36. package/dist/load.d.ts.map +1 -1
  37. package/dist/load.js +4 -1
  38. package/dist/load.js.map +1 -1
  39. package/dist/mcp-context.d.ts +48 -0
  40. package/dist/mcp-context.d.ts.map +1 -0
  41. package/dist/mcp-context.js +215 -0
  42. package/dist/mcp-context.js.map +1 -0
  43. package/dist/mcp-docs-tools.d.ts.map +1 -1
  44. package/dist/mcp-docs-tools.js +31 -0
  45. package/dist/mcp-docs-tools.js.map +1 -1
  46. package/dist/mcp-exec.d.ts +74 -0
  47. package/dist/mcp-exec.d.ts.map +1 -0
  48. package/dist/mcp-exec.js +844 -0
  49. package/dist/mcp-exec.js.map +1 -0
  50. package/dist/mcp-http.js +1 -1
  51. package/dist/mcp-io.d.ts +26 -0
  52. package/dist/mcp-io.d.ts.map +1 -0
  53. package/dist/mcp-io.js +100 -0
  54. package/dist/mcp-io.js.map +1 -0
  55. package/dist/mcp-reflect.d.ts +16 -0
  56. package/dist/mcp-reflect.d.ts.map +1 -0
  57. package/dist/mcp-reflect.js +101 -0
  58. package/dist/mcp-reflect.js.map +1 -0
  59. package/dist/mcp-root.d.ts +7 -0
  60. package/dist/mcp-root.d.ts.map +1 -1
  61. package/dist/mcp-root.js +51 -3
  62. package/dist/mcp-root.js.map +1 -1
  63. package/dist/mcp.d.ts +10 -92
  64. package/dist/mcp.d.ts.map +1 -1
  65. package/dist/mcp.js +13 -1232
  66. package/dist/mcp.js.map +1 -1
  67. package/dist/openapi-tool.d.ts +5 -10
  68. package/dist/openapi-tool.d.ts.map +1 -1
  69. package/dist/openapi-tool.js +21 -2
  70. package/dist/openapi-tool.js.map +1 -1
  71. package/dist/openapi-types.d.ts +21 -0
  72. package/dist/openapi-types.d.ts.map +1 -0
  73. package/dist/openapi-types.js +214 -0
  74. package/dist/openapi-types.js.map +1 -0
  75. package/dist/port.d.ts.map +1 -1
  76. package/dist/port.js +2 -1
  77. package/dist/port.js.map +1 -1
  78. package/dist/project-facts.d.ts +2 -1
  79. package/dist/project-facts.d.ts.map +1 -1
  80. package/dist/project-facts.js.map +1 -1
  81. package/dist/release-verification.d.ts +4 -1
  82. package/dist/release-verification.d.ts.map +1 -1
  83. package/dist/release-verification.js +6 -138
  84. package/dist/release-verification.js.map +1 -1
  85. package/dist/rules/codes.d.ts +4 -0
  86. package/dist/rules/codes.d.ts.map +1 -1
  87. package/dist/rules/codes.js +4 -0
  88. package/dist/rules/codes.js.map +1 -1
  89. package/dist/rules/islands.d.ts +4 -0
  90. package/dist/rules/islands.d.ts.map +1 -0
  91. package/dist/rules/islands.js +155 -0
  92. package/dist/rules/islands.js.map +1 -0
  93. package/dist/rules/nano.d.ts +6 -0
  94. package/dist/rules/nano.d.ts.map +1 -0
  95. package/dist/rules/nano.js +264 -0
  96. package/dist/rules/nano.js.map +1 -0
  97. package/dist/rules/security.d.ts.map +1 -1
  98. package/dist/rules/security.js +38 -4
  99. package/dist/rules/security.js.map +1 -1
  100. package/dist/scaffold.d.ts +12 -6
  101. package/dist/scaffold.d.ts.map +1 -1
  102. package/dist/scaffold.js +149 -10
  103. package/dist/scaffold.js.map +1 -1
  104. package/dist/search-terms.d.ts.map +1 -1
  105. package/dist/search-terms.js +5 -3
  106. package/dist/search-terms.js.map +1 -1
  107. package/dist/stylex-migrate.d.ts +38 -0
  108. package/dist/stylex-migrate.d.ts.map +1 -0
  109. package/dist/stylex-migrate.js +1052 -0
  110. package/dist/stylex-migrate.js.map +1 -0
  111. package/dist/sync-manifest.d.ts.map +1 -1
  112. package/dist/sync-manifest.js +2 -1
  113. package/dist/sync-manifest.js.map +1 -1
  114. package/dist/verification-plan.d.ts +24 -0
  115. package/dist/verification-plan.d.ts.map +1 -0
  116. package/dist/verification-plan.js +116 -0
  117. package/dist/verification-plan.js.map +1 -0
  118. package/dist/work-graph.d.ts.map +1 -1
  119. package/dist/work-graph.js +6 -3
  120. package/dist/work-graph.js.map +1 -1
  121. package/docs/examples.json +46 -4
  122. package/docs/llms-full.txt +1346 -28
  123. package/docs/types.json +4185 -87
  124. package/package.json +9 -9
  125. package/src/assure-hydration.ts +6 -1
  126. package/src/assure.ts +4 -2
  127. package/src/check-diagnostics.ts +1219 -0
  128. package/src/check-scan.ts +1825 -0
  129. package/src/check.ts +47 -2752
  130. package/src/cli.ts +252 -10
  131. package/src/command-catalog.ts +155 -0
  132. package/src/env-file.ts +5 -5
  133. package/src/frontend-guidance.ts +259 -0
  134. package/src/levels-tool.ts +6 -0
  135. package/src/load.ts +4 -1
  136. package/src/mcp-context.ts +266 -0
  137. package/src/mcp-docs-tools.ts +37 -0
  138. package/src/mcp-exec.ts +1040 -0
  139. package/src/mcp-http.ts +1 -1
  140. package/src/mcp-io.ts +118 -0
  141. package/src/mcp-reflect.ts +140 -0
  142. package/src/mcp-root.ts +62 -4
  143. package/src/mcp.ts +61 -1511
  144. package/src/openapi-tool.ts +32 -2
  145. package/src/openapi-types.ts +261 -0
  146. package/src/port.ts +2 -1
  147. package/src/project-facts.ts +2 -4
  148. package/src/release-verification.ts +22 -164
  149. package/src/rules/codes.ts +4 -0
  150. package/src/rules/islands.ts +171 -0
  151. package/src/rules/nano.ts +295 -0
  152. package/src/rules/security.ts +40 -4
  153. package/src/scaffold.ts +169 -9
  154. package/src/search-terms.ts +7 -3
  155. package/src/stylex-migrate.ts +1139 -0
  156. package/src/sync-manifest.ts +2 -1
  157. package/src/verification-plan.ts +242 -0
  158. package/src/work-graph.ts +6 -3
package/src/scaffold.ts CHANGED
@@ -4,9 +4,10 @@
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);
8
- * for vue/svelte/vanilla we return the correct PATH + the route-module contract and point at
9
- * `nifra_example` for the body, rather than hand-writing an SFC we can't typecheck here.
7
+ * Page stubs are emitted for the JSX family (react/preact/solid - one shared, verified shape) and
8
+ * for vanilla (a zero-runtime `html` page carrying the golden island pattern); for vue/svelte we
9
+ * return the correct PATH + the route-module contract and point at `nifra_example` for the body,
10
+ * rather than hand-writing an SFC we can't typecheck here.
10
11
  */
11
12
 
12
13
  import { lstat, mkdir, realpath, writeFile } from "node:fs/promises"
@@ -14,6 +15,12 @@ import { dirname, resolve, sep } from "node:path"
14
15
 
15
16
  export type Framework = "react" | "preact" | "solid" | "vue" | "svelte" | "vanilla"
16
17
 
18
+ /** A scaffold flavour on top of the framework. `default` is the plain page stub; `stateful` only
19
+ * applies to vanilla and emits the golden **nano** pattern (explicit reactivity: `signal` +
20
+ * `computed(fn, [deps])` + keyed `bindList` + collected cleanups) instead of the static island stub -
21
+ * the AI-safe small-app lane whose three mistakes NF-C021/C022/C023 catch statically. */
22
+ export type ScaffoldVariant = "default" | "stateful"
23
+
17
24
  const EXT: Record<Framework, string> = {
18
25
  react: "tsx",
19
26
  preact: "tsx",
@@ -75,6 +82,134 @@ export default function Page() {
75
82
  `
76
83
  }
77
84
 
85
+ /**
86
+ * The @nifrajs/web-vanilla golden stub: a zero-runtime `html` page (`hydrate = false`), plus the
87
+ * copy-paste island path as guidance. The uncommented body is a valid, typecheckable static page;
88
+ * the commented block is the AI-safe interactivity pattern - an imperative enhancer that ALWAYS
89
+ * returns its cleanup (the one thing NF-C020 checks), wired through `mountIslands`. Interactivity is
90
+ * added by uncommenting and writing the companion `<name>.client.ts`, never by turning on hydration.
91
+ */
92
+ function vanillaStub(file: string, params: string[]): string {
93
+ const paramsNote =
94
+ params.length > 0 ? `params.${params.join(", params.")}` : "no path params on this route"
95
+ const loaderLine =
96
+ params.length > 0
97
+ ? `// export async function loader({ params, api }: LoaderArgs<typeof backend>) { return { /* fetch by ${params[0]} */ } }`
98
+ : `// export async function loader({ api }: LoaderArgs<typeof backend>) { return {} }`
99
+ return `// ${file} - @nifrajs/web-vanilla route. Server-rendered HTML, ZERO framework runtime.
100
+ // Available here: ${paramsNote}. Fetch data in a loader via the typed \`api\`; see nifra_example("loader").
101
+ import { html } from "@nifrajs/web-vanilla"
102
+
103
+ // No client framework to hydrate with - vanilla routes are documents, not hydrated apps.
104
+ export const hydrate = false
105
+
106
+ ${loaderLine}
107
+
108
+ export default function Page() {
109
+ return html\`<main><h1>TODO: ${file}</h1></main>\`
110
+ }
111
+
112
+ // --- Add interactivity the AI-safe way (islands), NOT hydration ---------------------------------
113
+ // 1. Render a marker in the page above: html\`<nifra-island data-id="counter" data-props=\${JSON.stringify({ start: 0 })}></nifra-island>\`
114
+ // 2. Wire the route to its enhancer bundle: export const islandScripts = [/* built URL of ./counter.client.ts */]
115
+ // 3. Write ./counter.client.ts as an imperative enhancer that ALWAYS returns its cleanup:
116
+ //
117
+ // import { defineIsland, mountIslands } from "@nifrajs/web/islands"
118
+ // const counter = defineIsland<{ start: number }>((el, props) => {
119
+ // let n = props.start
120
+ // const out = el.querySelector("output")!
121
+ // const onClick = () => { out.textContent = String(++n) }
122
+ // el.querySelector("button")?.addEventListener("click", onClick)
123
+ // return () => el.querySelector("button")?.removeEventListener("click", onClick) // cleanup - NF-C020
124
+ // })
125
+ // mountIslands({ counter })
126
+ //
127
+ // Cross-island coordination: create ONE createIslandBus() and close over it in each enhancer.
128
+ `
129
+ }
130
+
131
+ /**
132
+ * The nano golden stub (`nifra scaffold --variant stateful`, vanilla only): a zero-runtime `html`
133
+ * page that renders a `<nifra-island>` marker, plus the companion `<name>.client.ts` shown as the
134
+ * copy-paste block. The client is the AI-safe small-app lane - explicit reactivity, no VDOM, no
135
+ * auto-tracking - and every reactive edge is a visible call, which is what makes the three nano
136
+ * mistakes STATICALLY catchable:
137
+ * - a `bind`/`bindList` whose disposer is discarded -> NF-C021 (the block collects every disposer)
138
+ * - a `bindList` keyed by array index -> NF-C022 (the block keys by `item.id`)
139
+ * - a `computed` reading a signal its `[deps]` omits -> NF-C023 (the block declares `[todos]`)
140
+ * The page body is a valid, typecheckable static document; interactivity is added by writing the
141
+ * companion client shown, never by turning on hydration.
142
+ */
143
+ function nanoStub(file: string, params: string[]): string {
144
+ const paramsNote =
145
+ params.length > 0 ? `params.${params.join(", params.")}` : "no path params on this route"
146
+ const loaderLine =
147
+ params.length > 0
148
+ ? `// export async function loader({ params, api }: LoaderArgs<typeof backend>) { return { /* seed by ${params[0]} */ } }`
149
+ : `// export async function loader({ api }: LoaderArgs<typeof backend>) { return {} }`
150
+ return `// ${file} - @nifrajs/web-vanilla route + a nano island (explicit reactivity, zero VDOM).
151
+ // Available here: ${paramsNote}. Seed data in a loader via the typed \`api\`; see nifra_example("loader").
152
+ import { html } from "@nifrajs/web-vanilla"
153
+
154
+ // The document is static; the nano client below owns all interactivity. No hydration.
155
+ export const hydrate = false
156
+
157
+ ${loaderLine}
158
+
159
+ export default function Page() {
160
+ // The island marker: data-props is the initial state the client reads on mount.
161
+ return html\`<main>
162
+ <h1>Todos</h1>
163
+ <nifra-island data-id="todos" data-props=\${JSON.stringify({ items: [] })}></nifra-island>
164
+ </main>\`
165
+ }
166
+
167
+ // Wire the route to the built URL of ./todos.client.ts (the nano enhancer below).
168
+ // export const islandScripts = [/* built URL of ./todos.client.ts */]
169
+
170
+ // --- ./todos.client.ts - the golden nano pattern (write this as a companion file) -----------------
171
+ //
172
+ // import { defineIsland, mountIslands } from "@nifrajs/web/islands"
173
+ // import { signal, computed, bind, bindList } from "@nifrajs/web/nano"
174
+ //
175
+ // interface Todo { id: string; text: string; done: boolean }
176
+ //
177
+ // const todos = defineIsland<{ items: Todo[] }>((el, props) => {
178
+ // // 1. State is a signal. Reads are \`.get()\`, writes are \`.set(...)\` - every edge is visible.
179
+ // const items = signal<Todo[]>(props.items)
180
+ //
181
+ // // 2. Derived state declares its deps EXPLICITLY. Omitting \`todos\`/\`items\` here is NF-C023.
182
+ // const remaining = computed(() => items.get().filter((t) => !t.done).length, [items])
183
+ //
184
+ // // 3. Collect EVERY disposer. A bare \`bind(...)\`/\`bindList(...)\` that drops it is NF-C021.
185
+ // const cleanups: Array<() => void> = []
186
+ // const count = el.querySelector("[data-count]")!
187
+ // cleanups.push(bind(count, remaining, (node, n) => { node.textContent = String(n) + " left" }))
188
+ //
189
+ // // 4. Keyed list. Key by a STABLE id on the item, never the array index (NF-C022) - or
190
+ // // add/remove/reorder reuses the wrong DOM node.
191
+ // const list = el.querySelector("[data-list]")!
192
+ // cleanups.push(bindList(items, list, {
193
+ // key: (t) => t.id,
194
+ // create: (t) => { const li = document.createElement("li"); li.dataset.id = t.id; return li },
195
+ // update: (li, t) => { li.textContent = t.text; li.classList.toggle("done", t.done) },
196
+ // }))
197
+ //
198
+ // // Mutations replace the value (Object.is-deduped); subscribers run synchronously.
199
+ // const add = (text: string) => items.set([...items.get(), { id: crypto.randomUUID(), text, done: false }])
200
+ // void add
201
+ //
202
+ // // 5. Return the teardown - islands call it on soft-nav. This is what NF-C021 protects.
203
+ // return () => { for (const off of cleanups) off() }
204
+ // })
205
+ //
206
+ // mountIslands({ todos })
207
+ //
208
+ // Cross-island coordination: one createIslandBus() closed over in each enhancer. nifra_docs("nano")
209
+ // has the full cookbook; \`nifra check\` runs NF-C021/C022/C023 over the client you write.
210
+ `
211
+ }
212
+
78
213
  /** Param names a route file declares, for the stub's notes. */
79
214
  function paramsOf(file: string): string[] {
80
215
  const out: string[] = []
@@ -96,20 +231,40 @@ export interface ScaffoldWriteResult extends ScaffoldResult {
96
231
 
97
232
  /** Scaffold a page route for `urlPath` under the project's `framework`. Returns the correct file path
98
233
  * always; a ready-to-write stub for the JSX family, contract guidance otherwise. */
99
- export function scaffoldRoute(urlPath: string, framework: Framework): ScaffoldResult {
234
+ export function scaffoldRoute(
235
+ urlPath: string,
236
+ framework: Framework,
237
+ variant: ScaffoldVariant = "default",
238
+ ): ScaffoldResult {
100
239
  const ext = EXT[framework]
101
240
  const file = routePathToFile(urlPath, ext)
102
241
  const params = paramsOf(file)
242
+ // `stateful` is vanilla-only: it swaps the static island stub for the nano golden pattern. Asking
243
+ // for it on a JSX/SFC framework is a no-op on the flavour (those lanes have their own reactivity).
244
+ if (framework === "vanilla" && variant === "stateful") {
245
+ return {
246
+ file,
247
+ content: nanoStub(file, params),
248
+ note: `Create ${file} as a zero-runtime @nifrajs/web-vanilla route with a nano island. ${ROUTE_CONTRACT}\nThe stub embeds the golden nano client (signal + computed(fn,[deps]) + keyed bindList + collected cleanups); write it as the companion .client.ts. \`nifra check\` runs NF-C021/C022/C023 over it; nifra_docs("nano") is the full cookbook.`,
249
+ }
250
+ }
103
251
  if (framework === "react" || framework === "preact" || framework === "solid") {
104
252
  return {
105
253
  file,
106
254
  content: jsxStub(file, params),
107
- note: `Create ${file}. ${ROUTE_CONTRACT}`,
255
+ note: `Create ${file}. ${ROUTE_CONTRACT}\nIf the rendered page misbehaves (hydration warning, a value that stops updating), call nifra_frontend { adapter: "${framework}" } for the cause + fix.`,
256
+ }
257
+ }
258
+ if (framework === "vanilla") {
259
+ return {
260
+ file,
261
+ content: vanillaStub(file, params),
262
+ note: `Create ${file} as a zero-runtime @nifrajs/web-vanilla route. ${ROUTE_CONTRACT}\nInteractivity comes from islands (imperative enhancers), never hydration - the stub embeds the golden pattern; nifra_example("islands") has the full cookbook. For explicit local state (a list a human edits), scaffold with variant "stateful" to get the nano pattern instead.`,
108
263
  }
109
264
  }
110
265
  return {
111
266
  file,
112
- note: `Create ${file} as a ${framework} route module. ${ROUTE_CONTRACT}\nFor the ${framework} page body, call nifra_example (it ships verified ${framework} snippets) rather than guessing the SFC shape.`,
267
+ note: `Create ${file} as a ${framework} route module. ${ROUTE_CONTRACT}\nFor the ${framework} page body, call nifra_example (it ships verified ${framework} snippets) rather than guessing the SFC shape. If a value stops updating the template, call nifra_frontend { adapter: "${framework}" } for the ${framework} reactivity-loss fix.`,
113
268
  }
114
269
  }
115
270
 
@@ -160,8 +315,9 @@ export async function writeScaffoldRoute(
160
315
  cwd: string,
161
316
  urlPath: string,
162
317
  framework: Framework,
318
+ variant: ScaffoldVariant = "default",
163
319
  ): Promise<ScaffoldWriteResult> {
164
- const result = scaffoldRoute(urlPath, framework)
320
+ const result = scaffoldRoute(urlPath, framework, variant)
165
321
  if (result.content === undefined) {
166
322
  return {
167
323
  ...result,
@@ -185,10 +341,14 @@ export async function writeScaffoldRoute(
185
341
  }
186
342
 
187
343
  /** Render the tool result as markdown - the file path, the stub (if any), and the contract note. */
188
- export function renderScaffold(urlPath: string, framework: Framework): string {
344
+ export function renderScaffold(
345
+ urlPath: string,
346
+ framework: Framework,
347
+ variant: ScaffoldVariant = "default",
348
+ ): string {
189
349
  let r: ScaffoldResult
190
350
  try {
191
- r = scaffoldRoute(urlPath, framework)
351
+ r = scaffoldRoute(urlPath, framework, variant)
192
352
  } catch (err) {
193
353
  return `Cannot scaffold ${JSON.stringify(urlPath)}: ${err instanceof Error ? err.message : String(err)}`
194
354
  }
@@ -62,10 +62,14 @@ export function queryTermGroups(query: string): SearchTermGroup[] {
62
62
  }))
63
63
  }
64
64
 
65
- function tokenMatches(term: string, token: string): boolean {
66
- if (term === token) return true
65
+ function tokenMatches(term: string, candidate: string): boolean {
66
+ if (term === candidate) return true
67
67
  // Prefix matching catches plural/stem variants without letting very short terms get noisy.
68
- return term.length >= 4 && token.length >= 4 && (term.startsWith(token) || token.startsWith(term))
68
+ return (
69
+ term.length >= 4 &&
70
+ candidate.length >= 4 &&
71
+ (term.startsWith(candidate) || candidate.startsWith(term))
72
+ )
69
73
  }
70
74
 
71
75
  export function tokenSetHas(tokens: ReadonlySet<string>, group: SearchTermGroup): boolean {