@nifrajs/cli 3.3.0 → 4.0.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 (425) hide show
  1. package/README.md +42 -3
  2. package/dist/app-files.d.ts +11 -0
  3. package/dist/app-files.d.ts.map +1 -0
  4. package/dist/app-files.js +34 -0
  5. package/dist/app-files.js.map +1 -0
  6. package/dist/assure-hydration.d.ts +7 -1
  7. package/dist/assure-hydration.d.ts.map +1 -1
  8. package/dist/assure-hydration.js +128 -43
  9. package/dist/assure-hydration.js.map +1 -1
  10. package/dist/assure.d.ts.map +1 -1
  11. package/dist/assure.js +16 -2
  12. package/dist/assure.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts +5 -1
  14. package/dist/capabilities-tool.d.ts.map +1 -1
  15. package/dist/capabilities-tool.js +32 -10
  16. package/dist/capabilities-tool.js.map +1 -1
  17. package/dist/cdn-check.d.ts +41 -0
  18. package/dist/cdn-check.d.ts.map +1 -0
  19. package/dist/cdn-check.js +207 -0
  20. package/dist/cdn-check.js.map +1 -0
  21. package/dist/check-diagnostics.d.ts +7 -15
  22. package/dist/check-diagnostics.d.ts.map +1 -1
  23. package/dist/check-diagnostics.js +162 -713
  24. package/dist/check-diagnostics.js.map +1 -1
  25. package/dist/check-scan.d.ts +58 -7
  26. package/dist/check-scan.d.ts.map +1 -1
  27. package/dist/check-scan.js +323 -62
  28. package/dist/check-scan.js.map +1 -1
  29. package/dist/check.d.ts +1 -0
  30. package/dist/check.d.ts.map +1 -1
  31. package/dist/check.js +145 -54
  32. package/dist/check.js.map +1 -1
  33. package/dist/cli.d.ts +42 -16
  34. package/dist/cli.d.ts.map +1 -1
  35. package/dist/cli.js +306 -87
  36. package/dist/cli.js.map +1 -1
  37. package/dist/command-catalog.d.ts +60 -5
  38. package/dist/command-catalog.d.ts.map +1 -1
  39. package/dist/command-catalog.js +298 -33
  40. package/dist/command-catalog.js.map +1 -1
  41. package/dist/config-target.d.ts +8 -0
  42. package/dist/config-target.d.ts.map +1 -0
  43. package/dist/config-target.js +45 -0
  44. package/dist/config-target.js.map +1 -0
  45. package/dist/contracts.d.ts +3 -2
  46. package/dist/contracts.d.ts.map +1 -1
  47. package/dist/contracts.js +56 -16
  48. package/dist/contracts.js.map +1 -1
  49. package/dist/db-audit.d.ts +37 -0
  50. package/dist/db-audit.d.ts.map +1 -0
  51. package/dist/db-audit.js +99 -0
  52. package/dist/db-audit.js.map +1 -0
  53. package/dist/db-child.d.ts +64 -0
  54. package/dist/db-child.d.ts.map +1 -0
  55. package/dist/db-child.js +213 -0
  56. package/dist/db-child.js.map +1 -0
  57. package/dist/db-config.d.ts +60 -0
  58. package/dist/db-config.d.ts.map +1 -0
  59. package/dist/db-config.js +176 -0
  60. package/dist/db-config.js.map +1 -0
  61. package/dist/db-tool.d.ts +84 -0
  62. package/dist/db-tool.d.ts.map +1 -0
  63. package/dist/db-tool.js +537 -0
  64. package/dist/db-tool.js.map +1 -0
  65. package/dist/dev-bun-config.d.ts +20 -13
  66. package/dist/dev-bun-config.d.ts.map +1 -1
  67. package/dist/dev-bun-config.js +98 -41
  68. package/dist/dev-bun-config.js.map +1 -1
  69. package/dist/dev-feed-tool.d.ts +106 -0
  70. package/dist/dev-feed-tool.d.ts.map +1 -0
  71. package/dist/dev-feed-tool.js +713 -0
  72. package/dist/dev-feed-tool.js.map +1 -0
  73. package/dist/dev-server-client.d.ts +67 -0
  74. package/dist/dev-server-client.d.ts.map +1 -0
  75. package/dist/dev-server-client.js +195 -0
  76. package/dist/dev-server-client.js.map +1 -0
  77. package/dist/diagnostics.d.ts +86 -0
  78. package/dist/diagnostics.d.ts.map +1 -1
  79. package/dist/diagnostics.js +85 -0
  80. package/dist/diagnostics.js.map +1 -1
  81. package/dist/diff-tool.d.ts +3 -3
  82. package/dist/diff-tool.d.ts.map +1 -1
  83. package/dist/diff-tool.js +6 -5
  84. package/dist/diff-tool.js.map +1 -1
  85. package/dist/docs-search.d.ts.map +1 -1
  86. package/dist/docs-search.js +12 -4
  87. package/dist/docs-search.js.map +1 -1
  88. package/dist/doctor.d.ts +17 -0
  89. package/dist/doctor.d.ts.map +1 -1
  90. package/dist/doctor.js +76 -15
  91. package/dist/doctor.js.map +1 -1
  92. package/dist/env-file.d.ts +15 -0
  93. package/dist/env-file.d.ts.map +1 -1
  94. package/dist/env-file.js +73 -1
  95. package/dist/env-file.js.map +1 -1
  96. package/dist/fix-recipes.d.ts.map +1 -1
  97. package/dist/fix-recipes.js +41 -21
  98. package/dist/fix-recipes.js.map +1 -1
  99. package/dist/frontend-guidance.js +5 -5
  100. package/dist/frontend-guidance.js.map +1 -1
  101. package/dist/i18n-check.d.ts +16 -0
  102. package/dist/i18n-check.d.ts.map +1 -0
  103. package/dist/i18n-check.js +154 -0
  104. package/dist/i18n-check.js.map +1 -0
  105. package/dist/init-agents.d.ts +49 -13
  106. package/dist/init-agents.d.ts.map +1 -1
  107. package/dist/init-agents.js +239 -34
  108. package/dist/init-agents.js.map +1 -1
  109. package/dist/internal/code-unit-order.d.ts +4 -0
  110. package/dist/internal/code-unit-order.d.ts.map +1 -0
  111. package/dist/internal/code-unit-order.js +6 -0
  112. package/dist/internal/code-unit-order.js.map +1 -0
  113. package/dist/internal/script-kind.d.ts +7 -0
  114. package/dist/internal/script-kind.d.ts.map +1 -0
  115. package/dist/internal/script-kind.js +12 -0
  116. package/dist/internal/script-kind.js.map +1 -0
  117. package/dist/internal/source-facts.d.ts +5 -0
  118. package/dist/internal/source-facts.d.ts.map +1 -1
  119. package/dist/internal/source-facts.js +61 -5
  120. package/dist/internal/source-facts.js.map +1 -1
  121. package/dist/internal/typescript-import.d.ts +49 -24
  122. package/dist/internal/typescript-import.d.ts.map +1 -1
  123. package/dist/internal/typescript-import.js +507 -67
  124. package/dist/internal/typescript-import.js.map +1 -1
  125. package/dist/introspect.d.ts +6 -4
  126. package/dist/introspect.d.ts.map +1 -1
  127. package/dist/introspect.js +93 -74
  128. package/dist/introspect.js.map +1 -1
  129. package/dist/learn.js +3 -3
  130. package/dist/learn.js.map +1 -1
  131. package/dist/levels-tool.d.ts +10 -0
  132. package/dist/levels-tool.d.ts.map +1 -1
  133. package/dist/levels-tool.js +33 -3
  134. package/dist/levels-tool.js.map +1 -1
  135. package/dist/load.d.ts +71 -16
  136. package/dist/load.d.ts.map +1 -1
  137. package/dist/load.js +78 -18
  138. package/dist/load.js.map +1 -1
  139. package/dist/manifest-tool.d.ts.map +1 -1
  140. package/dist/manifest-tool.js +3 -2
  141. package/dist/manifest-tool.js.map +1 -1
  142. package/dist/mcp-app-tool.d.ts.map +1 -1
  143. package/dist/mcp-app-tool.js +2 -0
  144. package/dist/mcp-app-tool.js.map +1 -1
  145. package/dist/mcp-context.d.ts.map +1 -1
  146. package/dist/mcp-context.js +5 -6
  147. package/dist/mcp-context.js.map +1 -1
  148. package/dist/mcp-delegate.d.ts +57 -0
  149. package/dist/mcp-delegate.d.ts.map +1 -0
  150. package/dist/mcp-delegate.js +130 -0
  151. package/dist/mcp-delegate.js.map +1 -0
  152. package/dist/mcp-docs-tools.d.ts.map +1 -1
  153. package/dist/mcp-docs-tools.js +23 -5
  154. package/dist/mcp-docs-tools.js.map +1 -1
  155. package/dist/mcp-exec.d.ts +3 -0
  156. package/dist/mcp-exec.d.ts.map +1 -1
  157. package/dist/mcp-exec.js +263 -131
  158. package/dist/mcp-exec.js.map +1 -1
  159. package/dist/mcp-http.d.ts.map +1 -1
  160. package/dist/mcp-http.js +20 -5
  161. package/dist/mcp-http.js.map +1 -1
  162. package/dist/mcp-io.d.ts +26 -0
  163. package/dist/mcp-io.d.ts.map +1 -1
  164. package/dist/mcp-io.js +113 -0
  165. package/dist/mcp-io.js.map +1 -1
  166. package/dist/mcp-isolate.d.ts +48 -0
  167. package/dist/mcp-isolate.d.ts.map +1 -0
  168. package/dist/mcp-isolate.js +222 -0
  169. package/dist/mcp-isolate.js.map +1 -0
  170. package/dist/mcp-path.d.ts +10 -0
  171. package/dist/mcp-path.d.ts.map +1 -0
  172. package/dist/mcp-path.js +51 -0
  173. package/dist/mcp-path.js.map +1 -0
  174. package/dist/mcp-project-child.d.ts +74 -0
  175. package/dist/mcp-project-child.d.ts.map +1 -0
  176. package/dist/mcp-project-child.js +130 -0
  177. package/dist/mcp-project-child.js.map +1 -0
  178. package/dist/mcp-reflect.d.ts.map +1 -1
  179. package/dist/mcp-reflect.js +2 -2
  180. package/dist/mcp-reflect.js.map +1 -1
  181. package/dist/mcp-render.d.ts.map +1 -1
  182. package/dist/mcp-render.js +64 -45
  183. package/dist/mcp-render.js.map +1 -1
  184. package/dist/mcp-root.d.ts +37 -9
  185. package/dist/mcp-root.d.ts.map +1 -1
  186. package/dist/mcp-root.js +124 -25
  187. package/dist/mcp-root.js.map +1 -1
  188. package/dist/mcp-run.d.ts +18 -1
  189. package/dist/mcp-run.d.ts.map +1 -1
  190. package/dist/mcp-run.js +159 -55
  191. package/dist/mcp-run.js.map +1 -1
  192. package/dist/mcp-ws.js +4 -1
  193. package/dist/mcp-ws.js.map +1 -1
  194. package/dist/mcp.d.ts +18 -5
  195. package/dist/mcp.d.ts.map +1 -1
  196. package/dist/mcp.js +165 -82
  197. package/dist/mcp.js.map +1 -1
  198. package/dist/migrate-layout.d.ts +44 -0
  199. package/dist/migrate-layout.d.ts.map +1 -0
  200. package/dist/migrate-layout.js +982 -0
  201. package/dist/migrate-layout.js.map +1 -0
  202. package/dist/openapi-tool.d.ts +2 -2
  203. package/dist/openapi-tool.d.ts.map +1 -1
  204. package/dist/openapi-tool.js.map +1 -1
  205. package/dist/openapi-types.d.ts +2 -1
  206. package/dist/openapi-types.d.ts.map +1 -1
  207. package/dist/openapi-types.js +240 -12
  208. package/dist/openapi-types.js.map +1 -1
  209. package/dist/pipeline-report.d.ts +3 -2
  210. package/dist/pipeline-report.d.ts.map +1 -1
  211. package/dist/pipeline-report.js +19 -16
  212. package/dist/pipeline-report.js.map +1 -1
  213. package/dist/port.d.ts +5 -4
  214. package/dist/port.d.ts.map +1 -1
  215. package/dist/port.js +46 -29
  216. package/dist/port.js.map +1 -1
  217. package/dist/project-facts.d.ts +28 -4
  218. package/dist/project-facts.d.ts.map +1 -1
  219. package/dist/project-facts.js +12 -2
  220. package/dist/project-facts.js.map +1 -1
  221. package/dist/recipes/4.0.0.d.ts +9 -0
  222. package/dist/recipes/4.0.0.d.ts.map +1 -0
  223. package/dist/recipes/4.0.0.js +34 -0
  224. package/dist/recipes/4.0.0.js.map +1 -0
  225. package/dist/recipes/index.d.ts.map +1 -1
  226. package/dist/recipes/index.js +2 -0
  227. package/dist/recipes/index.js.map +1 -1
  228. package/dist/release-verification.d.ts +11 -1
  229. package/dist/release-verification.d.ts.map +1 -1
  230. package/dist/release-verification.js +47 -4
  231. package/dist/release-verification.js.map +1 -1
  232. package/dist/review.d.ts +21 -0
  233. package/dist/review.d.ts.map +1 -0
  234. package/dist/review.js +823 -0
  235. package/dist/review.js.map +1 -0
  236. package/dist/route-types.d.ts +28 -0
  237. package/dist/route-types.d.ts.map +1 -0
  238. package/dist/route-types.js +106 -0
  239. package/dist/route-types.js.map +1 -0
  240. package/dist/rules/codes.d.ts +9 -0
  241. package/dist/rules/codes.d.ts.map +1 -1
  242. package/dist/rules/codes.js +9 -0
  243. package/dist/rules/codes.js.map +1 -1
  244. package/dist/rules/data-guard.d.ts +12 -0
  245. package/dist/rules/data-guard.d.ts.map +1 -0
  246. package/dist/rules/data-guard.js +213 -0
  247. package/dist/rules/data-guard.js.map +1 -0
  248. package/dist/rules/index.d.ts +3 -0
  249. package/dist/rules/index.d.ts.map +1 -1
  250. package/dist/rules/index.js +17 -5
  251. package/dist/rules/index.js.map +1 -1
  252. package/dist/rules/islands.d.ts.map +1 -1
  253. package/dist/rules/islands.js +7 -4
  254. package/dist/rules/islands.js.map +1 -1
  255. package/dist/rules/legacy.d.ts +3 -1
  256. package/dist/rules/legacy.d.ts.map +1 -1
  257. package/dist/rules/legacy.js +829 -34
  258. package/dist/rules/legacy.js.map +1 -1
  259. package/dist/rules/nano.d.ts.map +1 -1
  260. package/dist/rules/nano.js +15 -6
  261. package/dist/rules/nano.js.map +1 -1
  262. package/dist/rules/pages.d.ts +15 -0
  263. package/dist/rules/pages.d.ts.map +1 -0
  264. package/dist/rules/pages.js +88 -0
  265. package/dist/rules/pages.js.map +1 -0
  266. package/dist/rules/routes.d.ts +11 -0
  267. package/dist/rules/routes.d.ts.map +1 -1
  268. package/dist/rules/routes.js +180 -2
  269. package/dist/rules/routes.js.map +1 -1
  270. package/dist/rules/secrets.d.ts +10 -0
  271. package/dist/rules/secrets.d.ts.map +1 -0
  272. package/dist/rules/secrets.js +169 -0
  273. package/dist/rules/secrets.js.map +1 -0
  274. package/dist/rules/security.d.ts +7 -0
  275. package/dist/rules/security.d.ts.map +1 -1
  276. package/dist/rules/security.js +171 -65
  277. package/dist/rules/security.js.map +1 -1
  278. package/dist/rules/typescript.d.ts +16 -0
  279. package/dist/rules/typescript.d.ts.map +1 -0
  280. package/dist/rules/typescript.js +59 -0
  281. package/dist/rules/typescript.js.map +1 -0
  282. package/dist/rules/zones.d.ts +17 -0
  283. package/dist/rules/zones.d.ts.map +1 -0
  284. package/dist/rules/zones.js +174 -0
  285. package/dist/rules/zones.js.map +1 -0
  286. package/dist/scaffold.d.ts +25 -12
  287. package/dist/scaffold.d.ts.map +1 -1
  288. package/dist/scaffold.js +154 -90
  289. package/dist/scaffold.js.map +1 -1
  290. package/dist/sdk.d.ts +17 -3
  291. package/dist/sdk.d.ts.map +1 -1
  292. package/dist/sdk.js +941 -153
  293. package/dist/sdk.js.map +1 -1
  294. package/dist/search-terms.d.ts +2 -0
  295. package/dist/search-terms.d.ts.map +1 -1
  296. package/dist/search-terms.js +23 -8
  297. package/dist/search-terms.js.map +1 -1
  298. package/dist/smoke.d.ts +79 -0
  299. package/dist/smoke.d.ts.map +1 -0
  300. package/dist/smoke.js +494 -0
  301. package/dist/smoke.js.map +1 -0
  302. package/dist/sync-manifest.d.ts.map +1 -1
  303. package/dist/sync-manifest.js +2 -1
  304. package/dist/sync-manifest.js.map +1 -1
  305. package/dist/sync-routes.d.ts +4 -3
  306. package/dist/sync-routes.d.ts.map +1 -1
  307. package/dist/sync-routes.js +4 -3
  308. package/dist/sync-routes.js.map +1 -1
  309. package/dist/test-tool.d.ts.map +1 -1
  310. package/dist/test-tool.js +30 -11
  311. package/dist/test-tool.js.map +1 -1
  312. package/dist/types-search.d.ts.map +1 -1
  313. package/dist/types-search.js +11 -5
  314. package/dist/types-search.js.map +1 -1
  315. package/dist/upgrade.d.ts +36 -6
  316. package/dist/upgrade.d.ts.map +1 -1
  317. package/dist/upgrade.js +167 -29
  318. package/dist/upgrade.js.map +1 -1
  319. package/dist/verification-plan.d.ts.map +1 -1
  320. package/dist/verification-plan.js +8 -0
  321. package/dist/verification-plan.js.map +1 -1
  322. package/dist/verification.d.ts.map +1 -1
  323. package/dist/verification.js +45 -12
  324. package/dist/verification.js.map +1 -1
  325. package/dist/web-app-options.d.ts +19 -0
  326. package/dist/web-app-options.d.ts.map +1 -0
  327. package/dist/web-app-options.js +49 -0
  328. package/dist/web-app-options.js.map +1 -0
  329. package/dist/work-graph.d.ts +3 -0
  330. package/dist/work-graph.d.ts.map +1 -1
  331. package/dist/work-graph.js +33 -16
  332. package/dist/work-graph.js.map +1 -1
  333. package/docs/examples.json +185 -29
  334. package/docs/llms-full.txt +4123 -548
  335. package/docs/types.json +14391 -10060
  336. package/package.json +19 -11
  337. package/src/app-files.ts +40 -0
  338. package/src/assure-hydration.ts +139 -46
  339. package/src/assure.ts +17 -2
  340. package/src/capabilities-tool.ts +35 -9
  341. package/src/cdn-check.ts +268 -0
  342. package/src/check-diagnostics.ts +199 -797
  343. package/src/check-scan.ts +394 -63
  344. package/src/check.ts +146 -58
  345. package/src/cli.ts +347 -92
  346. package/src/command-catalog.ts +407 -45
  347. package/src/config-target.ts +47 -0
  348. package/src/contracts.ts +66 -15
  349. package/src/db-audit.ts +128 -0
  350. package/src/db-child.ts +285 -0
  351. package/src/db-config.ts +224 -0
  352. package/src/db-tool.ts +690 -0
  353. package/src/dev-bun-config.ts +116 -40
  354. package/src/dev-feed-tool.ts +912 -0
  355. package/src/dev-server-client.ts +267 -0
  356. package/src/diagnostics.ts +179 -0
  357. package/src/diff-tool.ts +6 -5
  358. package/src/docs-search.ts +10 -3
  359. package/src/doctor.ts +98 -15
  360. package/src/env-file.ts +78 -1
  361. package/src/fix-recipes.ts +44 -31
  362. package/src/frontend-guidance.ts +5 -5
  363. package/src/i18n-check.ts +179 -0
  364. package/src/init-agents.ts +301 -38
  365. package/src/internal/code-unit-order.ts +5 -0
  366. package/src/internal/script-kind.ts +11 -0
  367. package/src/internal/source-facts.ts +96 -22
  368. package/src/internal/typescript-import.ts +663 -68
  369. package/src/introspect.ts +93 -82
  370. package/src/learn.ts +3 -3
  371. package/src/levels-tool.ts +47 -2
  372. package/src/load.ts +144 -32
  373. package/src/manifest-tool.ts +3 -2
  374. package/src/mcp-app-tool.ts +2 -0
  375. package/src/mcp-context.ts +5 -7
  376. package/src/mcp-delegate.ts +162 -0
  377. package/src/mcp-docs-tools.ts +23 -5
  378. package/src/mcp-exec.ts +301 -161
  379. package/src/mcp-http.ts +22 -5
  380. package/src/mcp-io.ts +130 -0
  381. package/src/mcp-isolate.ts +287 -0
  382. package/src/mcp-path.ts +50 -0
  383. package/src/mcp-project-child.ts +182 -0
  384. package/src/mcp-reflect.ts +3 -2
  385. package/src/mcp-render.ts +72 -42
  386. package/src/mcp-root.ts +130 -24
  387. package/src/mcp-run.ts +190 -56
  388. package/src/mcp-ws.ts +4 -1
  389. package/src/mcp.ts +193 -91
  390. package/src/migrate-layout.ts +1121 -0
  391. package/src/openapi-tool.ts +7 -3
  392. package/src/openapi-types.ts +315 -13
  393. package/src/pipeline-report.ts +22 -18
  394. package/src/port.ts +53 -30
  395. package/src/project-facts.ts +42 -8
  396. package/src/recipes/4.0.0.ts +35 -0
  397. package/src/recipes/index.ts +2 -0
  398. package/src/release-verification.ts +74 -5
  399. package/src/review.ts +988 -0
  400. package/src/route-types.ts +133 -0
  401. package/src/rules/codes.ts +9 -0
  402. package/src/rules/data-guard.ts +230 -0
  403. package/src/rules/index.ts +18 -5
  404. package/src/rules/islands.ts +12 -4
  405. package/src/rules/legacy.ts +947 -35
  406. package/src/rules/nano.ts +18 -6
  407. package/src/rules/pages.ts +99 -0
  408. package/src/rules/routes.ts +181 -2
  409. package/src/rules/secrets.ts +174 -0
  410. package/src/rules/security.ts +198 -70
  411. package/src/rules/typescript.ts +77 -0
  412. package/src/rules/zones.ts +195 -0
  413. package/src/scaffold.ts +175 -98
  414. package/src/sdk.ts +1215 -158
  415. package/src/search-terms.ts +24 -7
  416. package/src/smoke.ts +607 -0
  417. package/src/sync-manifest.ts +2 -1
  418. package/src/sync-routes.ts +4 -3
  419. package/src/test-tool.ts +32 -11
  420. package/src/types-search.ts +11 -4
  421. package/src/upgrade.ts +205 -25
  422. package/src/verification-plan.ts +27 -0
  423. package/src/verification.ts +50 -12
  424. package/src/web-app-options.ts +57 -0
  425. package/src/work-graph.ts +40 -16
package/src/load.ts CHANGED
@@ -1,37 +1,43 @@
1
1
  /**
2
2
  * Zero-config app discovery. The CLI reads a few conventions from the project root:
3
3
  * - `routes/` - the file-based routes (per `@nifrajs/web/fs`).
4
- * - `nifra.config.ts` - the CLI's framework wiring (see {@link NifraFramework}); `framework.ts` is
5
- * used as a fallback. One of the two is required.
6
- * - `backend.ts` - exports `backend` (a `@nifrajs/core` server, the contract). Optional.
4
+ * - `nifra.config.ts` - the CLI's framework wiring (see {@link NifraFramework});
5
+ * `backend/framework.ts` is used as a fallback. One of the two is required.
6
+ * - `backend/app.ts` - exports `backend` (a `@nifrajs/core` server, the contract). Optional.
7
7
  *
8
- * Why two filenames: a multi-target app's edge entry (`_worker.ts`) imports `framework.ts` for the
9
- * render adapter, so `framework.ts` must stay edge-bundlable - it can't reference Vite plugins or the
10
- * SFC compiler (Bun would pull them into the `target:"browser"` worker and fail on `child_process`).
11
- * `nifra.config.ts` holds that build/dev tooling instead: it's imported ONLY by this CLI (which runs on
12
- * Bun), so its Vite plugin + SFC-compiler imports (`@vitejs/plugin-vue`, `@nifrajs/web-vue/plugin`) never
13
- * reach the edge bundle. A simple single-target app with no edge build can put it all in `framework.ts`.
8
+ * Why two filenames: a multi-target app's edge entry (`_worker.ts`) imports `backend/framework.ts`
9
+ * for the render adapter, so `backend/framework.ts` must stay edge-bundlable - it can't reference
10
+ * Vite plugins or the SFC compiler (Bun would pull them into the `target:"browser"` worker and fail
11
+ * on `child_process`). `nifra.config.ts` holds that build/dev tooling instead: it's imported ONLY
12
+ * by this CLI (which runs on Bun), so its Vite plugin + SFC-compiler imports (`@vitejs/plugin-vue`,
13
+ * `@nifrajs/web-vue/plugin`) never reach the edge bundle. A simple single-target app with no edge
14
+ * build can put it all in `backend/framework.ts`.
14
15
  */
15
16
  import { existsSync } from "node:fs"
16
17
  import { isAbsolute, resolve } from "node:path"
18
+ import type { CreateWebAppOptions, CssLoadingMode } from "@nifrajs/web"
19
+ import type { SecretExemption } from "@nifrajs/web/build"
20
+ import { assertCurrentLayout, BACKEND_APP_FILE, CONFIG_FILE, FRAMEWORK_FILE } from "./app-files.ts"
17
21
  import { checkPipelineSeparation } from "./pipeline-guard.ts"
18
22
 
19
23
  /**
20
- * A plugin list - either an array, or a **thunk** that returns one (optionally async). Both forms are
21
- * accepted; {@link resolvePlugins} normalizes them. The thunk is just a convenience for deferring a
22
- * plugin's construction - it does NOT make a plugin edge-safe (Bun.build follows a dynamic `import()`
23
- * even from an unused export, so a Vite/compiler import in an edge-bundled file fails regardless). Edge
24
- * safety comes from WHERE the config lives: keep these fields in `nifra.config.ts` (CLI-only), not in
25
- * the edge-imported `framework.ts`. See the module header.
24
+ * A plugin list - either an array, or a **thunk** that returns one (optionally async). Both forms
25
+ * are accepted; {@link resolvePlugins} normalizes them. The thunk is just a convenience for
26
+ * deferring a plugin's construction - it does NOT make a plugin edge-safe (Bun.build follows a
27
+ * dynamic `import()` even from an unused export, so a Vite/compiler import in an edge-bundled file
28
+ * fails regardless). Edge safety comes from WHERE the config lives: keep these fields in
29
+ * `nifra.config.ts` (CLI-only), not in the edge-imported `backend/framework.ts`. See the module
30
+ * header.
26
31
  */
27
32
  export type PluginsField =
28
33
  | readonly unknown[]
29
34
  | (() => readonly unknown[] | Promise<readonly unknown[]>)
30
35
 
31
36
  /**
32
- * The CLI's framework wiring - exported from `nifra.config.ts` (or `framework.ts` as a fallback).
33
- * `create-nifra` generates it. Only `adapter` + `clientModule` are required; the plugin/condition
34
- * fields are framework-specific extras (Vue/Svelte/Solid need them; React/Preact don't).
37
+ * The CLI's framework wiring - exported from `nifra.config.ts` (or `backend/framework.ts` as a
38
+ * fallback). `create-nifra` generates it. Only `adapter` + `clientModule` are required; the
39
+ * plugin/condition fields are framework-specific extras (Vue/Svelte/Solid need them; React/Preact
40
+ * don't).
35
41
  */
36
42
  export interface NifraFramework {
37
43
  /** The render adapter, e.g. `reactAdapter` from `@nifrajs/web-react`. */
@@ -41,7 +47,7 @@ export interface NifraFramework {
41
47
  /** App-level middleware applied to the web app before its page routes are declared -
42
48
  * e.g. `(app) => app.use(securityHeaders())`. Runs ahead of the `api`/`mounts` hooks.
43
49
  *
44
- * MUST be exported from `framework.ts`, not `nifra.config.ts`: `nifra build` emits a
50
+ * MUST be exported from `backend/framework.ts`, not `nifra.config.ts`: `nifra build` emits a
45
51
  * server entry that imports it, and that entry is edge-bundled. See the module header. */
46
52
  readonly use?: (app: never) => void
47
53
  /** Vite plugins for `nifra dev`'s HMR, e.g. `[react()]` / `() => import("@vitejs/plugin-vue")…`. */
@@ -56,10 +62,88 @@ export interface NifraFramework {
56
62
  readonly conditions?: readonly string[]
57
63
  /** Compile-time `define` replacements, e.g. Vue's `__VUE_*` flags. */
58
64
  readonly define?: Readonly<Record<string, string>>
65
+ /** The deploy target `nifra build` emits without `--target` (default `"bun"`); `nifra target <t>`
66
+ * rewrites it. */
67
+ readonly target?: string
68
+ /** `"platform"` makes a `cloudflare` / `vercel` build read `c.clientIp` from the header that
69
+ * platform's edge overwrites (`cf-connecting-ip` / `x-real-ip`). Left out, an edge build has no
70
+ * caller address, so a per-caller `rateLimit` refuses every request. Declare it only while that edge
71
+ * is the sole way in: served any other way the header is whatever the client sent. Self-hosting
72
+ * targets use the socket peer either way. */
73
+ readonly clientIp?: "platform"
74
+ /** `nifra dev` settings. `indicator: false` turns the in-page issues badge off for every run
75
+ * (`nifra dev --no-indicator` does it for one); browser errors still reach `nifra errors`. */
76
+ readonly dev?: { readonly indicator?: boolean }
59
77
  /** Static files directory. Defaults to `<app>/public`; `false` disables it. */
60
78
  readonly publicDir?: string | false
61
79
  /** Client-visible environment prefix (default `"PUBLIC_"`; empty disables exposure). */
62
80
  readonly publicEnvPrefix?: string
81
+ /** Reviewed false positives of `nifra build`'s secret scan, each `{ rule, file, reason }` (or
82
+ * `{ rule: "private-env-value", env, reason }`). There is no exemption by value. */
83
+ readonly secretExemptions?: readonly SecretExemption[]
84
+ /** Vite production CSS output policy. `false` emits one aggregate stylesheet for the whole client. */
85
+ readonly cssCodeSplit?: boolean
86
+ /** Framework-owned SSR stylesheet activation. Deferred mode is intended for aggregate CSS. */
87
+ readonly cssLoading?: CssLoadingMode
88
+ /** Path `backend/app.ts`'s backend is served at (default `"/api"`; `""` turns the mount off). A
89
+ * page file under it can never render, so the app refuses to start and `nifra build` refuses to
90
+ * build.
91
+ *
92
+ * The fields from here down reach the generated server entry, so like `use` they MUST be exported
93
+ * from `backend/framework.ts` (re-export them from `nifra.config.ts` when both exist). */
94
+ readonly apiPrefix?: string
95
+ /** Strip {@link apiPrefix} before the backend sees the path, for a backend that declares its routes
96
+ * without it. Default `false`. */
97
+ readonly apiStrip?: boolean
98
+ /** Apps mounted ahead of page routing - an auth handler, a webhook receiver, a backend served at an
99
+ * extra path. See `createWebApp`'s `mounts`. */
100
+ readonly mounts?: CreateWebAppOptions["mounts"]
101
+ /** A hash-based Content-Security-Policy from `createCspPolicy`. Exclusive with {@link nonce}. */
102
+ readonly csp?: CreateWebAppOptions["csp"]
103
+ /** A per-request CSP nonce resolver. Exclusive with {@link csp}. */
104
+ readonly nonce?: CreateWebAppOptions["nonce"]
105
+ }
106
+
107
+ /** The {@link NifraFramework} fields forwarded to `createWebApp` and imported by the generated server
108
+ * entry, in one list so every command that builds the app forwards the same set. */
109
+ export const FRAMEWORK_WEB_OPTIONS = ["apiPrefix", "apiStrip", "mounts", "csp", "nonce"] as const
110
+ export type FrameworkWebOption = (typeof FRAMEWORK_WEB_OPTIONS)[number]
111
+
112
+ /** Refuse a forwarded field of the wrong shape at load, naming the field, rather than letting it fail
113
+ * later inside `createWebApp` or the generated server entry. */
114
+ function assertFrameworkWebOptions(fw: Partial<NifraFramework>, configFile: string): void {
115
+ const wrong = (field: string, expected: string): never => {
116
+ throw new Error(`[nifra] ${configFile}: \`${field}\` must be ${expected}.`)
117
+ }
118
+ if (fw.apiPrefix !== undefined && typeof fw.apiPrefix !== "string") {
119
+ wrong("apiPrefix", 'a path string such as "/api", or "" to turn the backend mount off')
120
+ }
121
+ if (fw.apiStrip !== undefined && typeof fw.apiStrip !== "boolean") wrong("apiStrip", "a boolean")
122
+ if (fw.mounts !== undefined && !Array.isArray(fw.mounts)) {
123
+ wrong("mounts", "an array of { path, app } entries")
124
+ }
125
+ if (fw.csp !== undefined && (typeof fw.csp !== "object" || fw.csp === null)) {
126
+ wrong("csp", "the policy createCspPolicy() returns")
127
+ }
128
+ if (fw.nonce !== undefined && typeof fw.nonce !== "function") {
129
+ wrong("nonce", "a nonce resolver function, such as createNonceResolver() returns")
130
+ }
131
+ if (fw.secretExemptions !== undefined && !Array.isArray(fw.secretExemptions)) {
132
+ wrong("secretExemptions", "an array of { rule, file, reason } entries")
133
+ }
134
+ if (fw.target !== undefined && typeof fw.target !== "string") {
135
+ wrong("target", 'a deploy target name such as "bun"')
136
+ }
137
+ if (fw.clientIp !== undefined && fw.clientIp !== "platform") {
138
+ wrong("clientIp", '"platform" (trust the edge platform\'s client-address header), or left out')
139
+ }
140
+ if (fw.dev !== undefined) {
141
+ if (typeof fw.dev !== "object" || fw.dev === null)
142
+ wrong("dev", "an object such as { indicator: false }")
143
+ const indicator: unknown = Reflect.get(fw.dev, "indicator")
144
+ if (indicator !== undefined && typeof indicator !== "boolean")
145
+ wrong("dev.indicator", "a boolean")
146
+ }
63
147
  }
64
148
 
65
149
  /**
@@ -99,8 +183,9 @@ export interface ResolvedPlugins {
99
183
 
100
184
  export interface LoadedApp {
101
185
  readonly cwd: string
102
- /** Absolute path of the config this app was loaded from - `nifra.config.ts`, or `framework.ts` as the
103
- * fallback. `nifra dev`'s Bun pipeline re-imports it from a GENERATED module so the app's own
186
+ /** Absolute path of the config this app was loaded from - `nifra.config.ts`, or
187
+ * `backend/framework.ts` as the fallback. `nifra dev`'s Bun pipeline re-imports it from a
188
+ * GENERATED module so the app's own
104
189
  * `clientPlugins` reach Bun's dev-server bundler, which takes plugins only as module paths. */
105
190
  readonly configPath: string
106
191
  readonly routesDir: string
@@ -109,7 +194,7 @@ export interface LoadedApp {
109
194
  readonly framework: NifraFramework
110
195
  /** Plugin thunks resolved exactly once during app loading and reused by every command phase. */
111
196
  readonly resolvedPlugins: ResolvedPlugins
112
- /** The `backend` export from `backend.ts`, or `undefined` if there's no `backend.ts`. */
197
+ /** The `backend` export from `backend/app.ts`, or `undefined` if there's no `backend/app.ts`. */
113
198
  readonly backend: unknown
114
199
  }
115
200
 
@@ -121,7 +206,8 @@ export interface LoadAppOptions {
121
206
  /**
122
207
  * Root-level monorepo config - exported from `nifra.config.ts` at the workspace root.
123
208
  * Each key in `apps` is the short name used to namespace MCP tools (`nifra_<name>_context` etc.);
124
- * each value is a path relative to the root that contains its own `nifra.config.ts` / `framework.ts`.
209
+ * each value is a path relative to the root that contains its own `nifra.config.ts` /
210
+ * `backend/framework.ts`.
125
211
  *
126
212
  * Example:
127
213
  * ```ts
@@ -172,23 +258,48 @@ const isAdapter = (v: unknown): boolean => typeof v === "object" && v !== null
172
258
  const importWithQuery = (path: string, query: string | undefined): Promise<unknown> =>
173
259
  import(query === undefined || query === "" ? path : `${path}?${query}`)
174
260
 
175
- /** Discover + validate the app conventions rooted at `cwd`. Prefers `nifra.config.ts`, falls back to
176
- * `framework.ts`. Throws a clear, actionable error if neither exists or the config is malformed. */
261
+ /**
262
+ * What reads only the backend (`nifra openapi`): the whole app when it has a web config, else
263
+ * `backend/app.ts` alone, so an API-only app needs no adapter to describe its routes.
264
+ */
265
+ export async function loadBackendApp(
266
+ cwd: string,
267
+ options: LoadAppOptions = {},
268
+ ): Promise<Pick<LoadedApp, "cwd" | "backend">> {
269
+ if (existsSync(resolve(cwd, CONFIG_FILE)) || existsSync(resolve(cwd, FRAMEWORK_FILE))) {
270
+ return loadApp(cwd, "dist", options)
271
+ }
272
+ assertCurrentLayout(cwd)
273
+ const backendPath = resolve(cwd, BACKEND_APP_FILE)
274
+ if (!existsSync(backendPath)) {
275
+ throw new Error(
276
+ `[nifra] no ${BACKEND_APP_FILE} in ${cwd}: the OpenAPI document describes its routes`,
277
+ )
278
+ }
279
+ const backend = (
280
+ (await importWithQuery(backendPath, options.importQuery)) as { backend?: unknown }
281
+ ).backend
282
+ return { cwd, backend }
283
+ }
284
+
285
+ /** Discover + validate the app conventions rooted at `cwd`. Prefers `nifra.config.ts`, falls back
286
+ * to
287
+ * `backend/framework.ts`. Throws a clear, actionable error if neither exists or the config is
288
+ * malformed. */
177
289
  export async function loadApp(
178
290
  cwd: string,
179
291
  outDirName = "dist",
180
292
  options: LoadAppOptions = {},
181
293
  ): Promise<LoadedApp> {
182
294
  // nifra.config.ts is the CLI's config (it may import Vite plugins / the SFC compiler, which a
183
- // multi-target app keeps OUT of the edge-imported framework.ts). framework.ts is the fallback for a
184
- // simple single-target app that has no edge build.
185
- const configFile = existsSync(resolve(cwd, "nifra.config.ts"))
186
- ? "nifra.config.ts"
187
- : "framework.ts"
295
+ // multi-target app keeps OUT of the edge-imported backend/framework.ts). backend/framework.ts is
296
+ // the fallback for a simple single-target app that has no edge build.
297
+ assertCurrentLayout(cwd)
298
+ const configFile = existsSync(resolve(cwd, CONFIG_FILE)) ? CONFIG_FILE : FRAMEWORK_FILE
188
299
  const configPath = resolve(cwd, configFile)
189
300
  if (!existsSync(configPath)) {
190
301
  throw new Error(
191
- `[nifra] no nifra.config.ts or framework.ts found in ${cwd}.\n` +
302
+ `[nifra] no ${CONFIG_FILE} or ${FRAMEWORK_FILE} found in ${cwd}.\n` +
192
303
  " nifra is zero-config but needs one of them exporting at least:\n" +
193
304
  " export const adapter = reactAdapter // from @nifrajs/web-react\n" +
194
305
  ' export const clientModule = "@nifrajs/web-react/client"\n' +
@@ -206,6 +317,7 @@ export async function loadApp(
206
317
  `[nifra] ${configFile} must export \`adapter\` (a render adapter object) and \`clientModule\` (a string).`,
207
318
  )
208
319
  }
320
+ assertFrameworkWebOptions(fw, configFile)
209
321
  // A `clientModule` given as a RELATIVE path (a local client entry, e.g. `./src/client.tsx`) is
210
322
  // resolved to absolute here. It is embedded verbatim as an import specifier in the generated client
211
323
  // entry, and `nifra dev` and `nifra build` write that entry into DIFFERENT directories - so a relative
@@ -246,7 +358,7 @@ export async function loadApp(
246
358
  assertPipelineSeparation(resolvedPlugins, configFile)
247
359
 
248
360
  let backend: unknown
249
- const backendPath = resolve(cwd, "backend.ts")
361
+ const backendPath = resolve(cwd, BACKEND_APP_FILE)
250
362
  if (existsSync(backendPath)) {
251
363
  backend = ((await importWithQuery(backendPath, options.importQuery)) as { backend?: unknown })
252
364
  .backend
@@ -12,6 +12,7 @@ import {
12
12
  serializeNifraManifestSignature,
13
13
  signNifraManifest,
14
14
  } from "@nifrajs/core/manifest"
15
+ import { codeUnitOrder } from "./internal/code-unit-order.ts"
15
16
  import { collectProjectVerification } from "./verification.ts"
16
17
 
17
18
  export const DEFAULT_MANIFEST_FILE = "nifra.manifest.json"
@@ -23,8 +24,8 @@ export function formatManifestDiff(diff: NifraManifestDiff): string {
23
24
  .sort(
24
25
  (a, b) =>
25
26
  weight[a.severity] - weight[b.severity] ||
26
- a.path.localeCompare(b.path) ||
27
- a.method.localeCompare(b.method),
27
+ codeUnitOrder(a.path, b.path) ||
28
+ codeUnitOrder(a.method, b.method),
28
29
  )
29
30
  .map(
30
31
  (change) =>
@@ -54,6 +54,8 @@ export function examplesAppTool(loadExamples: () => Promise<Example[] | undefine
54
54
  annotations: {
55
55
  title: "Browse the nifra example gallery",
56
56
  readOnlyHint: true,
57
+ destructiveHint: false,
58
+ idempotentHint: true,
57
59
  openWorldHint: false,
58
60
  },
59
61
  description:
@@ -7,9 +7,10 @@
7
7
 
8
8
  import { realpathSync } from "node:fs"
9
9
  import { readFile } from "node:fs/promises"
10
- import { basename, isAbsolute, relative, resolve, sep } from "node:path"
10
+ import { basename, isAbsolute, relative, resolve } from "node:path"
11
11
  import type { LoadedApp } from "./load.ts"
12
12
  import { createCachedAppLoader } from "./mcp-exec.ts"
13
+ import { resolveMcpProjectPath } from "./mcp-path.ts"
13
14
  import type { McpPrompt, McpResource, McpServerFeatures, McpTool } from "./mcp-protocol.ts"
14
15
 
15
16
  function openApiFormat(args: Record<string, unknown>): "json" | "yaml" {
@@ -31,11 +32,8 @@ async function readProjectFile(
31
32
  relativeFile: string,
32
33
  maxChars: number,
33
34
  ): Promise<string> {
34
- const target = resolve(cwd, relativeFile)
35
- const root = resolve(cwd)
36
- if (target !== root && !target.startsWith(`${root}${sep}`)) {
37
- throw new Error(`refusing to read outside project root: ${relativeFile}`)
38
- }
35
+ const target = resolveMcpProjectPath(cwd, relativeFile)
36
+ if (target === null) throw new Error(`refusing to read outside project root: ${relativeFile}`)
39
37
  try {
40
38
  const text = await readFile(target, "utf8")
41
39
  return text.length <= maxChars
@@ -76,7 +74,7 @@ export function projectResources(
76
74
  {
77
75
  uri: "nifra://openapi",
78
76
  name: "OpenAPI 3.1",
79
- description: "OpenAPI document generated from backend.ts using @nifrajs/schema.",
77
+ description: "OpenAPI document generated from backend/app.ts using @nifrajs/schema.",
80
78
  mimeType: "application/json",
81
79
  read: async () => ({ text: await openApiHandler({ format: "json" }, loadAppCached) }),
82
80
  },
@@ -0,0 +1,162 @@
1
+ /**
2
+ * One nifra per MCP session: the version the project installs.
3
+ *
4
+ * A client that spawns a globally installed `nifra mcp` against a project pinned to a different
5
+ * release gets types, checks, docs and contracts for a surface the project does not have - and every
6
+ * answer still reads as authoritative. So the server settles the version before it serves:
7
+ *
8
+ * 1. The project installs its own `@nifrajs/cli` at a different version: hand the whole session to
9
+ * that CLI (`node_modules/.bin/nifra mcp`, stdio inherited) and step aside.
10
+ * 2. Handing off is impossible (no project CLI, a missing bin, or this process IS the hand-off and
11
+ * the versions still disagree): serve, but the version-sensitive tools refuse with the fix
12
+ * instead of answering for the wrong release.
13
+ */
14
+
15
+ import { existsSync } from "node:fs"
16
+ import { readFile } from "node:fs/promises"
17
+ import { dirname, join } from "node:path"
18
+ import type { McpTool, McpToolResult } from "./mcp-protocol.ts"
19
+ import type { ToolingDrift } from "./mcp-root.ts"
20
+
21
+ /** Set on the hand-off child so it never hands off again, whatever it finds. */
22
+ export const MCP_DELEGATED_ENV = "NIFRA_MCP_DELEGATED"
23
+
24
+ /** The tools whose answer is a property of the nifra release: they refuse under a version split. */
25
+ export const VERSION_SENSITIVE_TOOLS: ReadonlySet<string> = new Set([
26
+ "nifra_check",
27
+ "nifra_types",
28
+ "nifra_docs",
29
+ "nifra_example",
30
+ "nifra_assure",
31
+ "nifra_contracts",
32
+ ])
33
+
34
+ /** The project's own CLI: its version and, when the install linked one, its bin. */
35
+ export interface ProjectCli {
36
+ readonly version: string
37
+ readonly bin?: string
38
+ }
39
+
40
+ const BIN_NAME = process.platform === "win32" ? "nifra.exe" : "nifra"
41
+ const MAX_ANCESTOR_DIRS = 8
42
+
43
+ /** The nearest `@nifrajs/cli` install at or above `root` (a monorepo app hoists to the repo root). */
44
+ export async function findProjectCli(root: string): Promise<ProjectCli | undefined> {
45
+ let dir = root
46
+ for (let depth = 0; depth < MAX_ANCESTOR_DIRS; depth++) {
47
+ const modules = join(dir, "node_modules")
48
+ try {
49
+ const meta = JSON.parse(
50
+ await readFile(join(modules, "@nifrajs", "cli", "package.json"), "utf8"),
51
+ ) as { version?: unknown }
52
+ if (typeof meta.version === "string" && meta.version.length > 0) {
53
+ const bin = join(modules, ".bin", BIN_NAME)
54
+ return existsSync(bin) ? { version: meta.version, bin } : { version: meta.version }
55
+ }
56
+ } catch {
57
+ // Not installed at this level - keep walking up.
58
+ }
59
+ const parent = dirname(dir)
60
+ if (parent === dir) return undefined
61
+ dir = parent
62
+ }
63
+ return undefined
64
+ }
65
+
66
+ /**
67
+ * Hand this stdio session to the project's own CLI when it installs a different version. Resolves to
68
+ * the child's exit code once the session ends, or `undefined` when this process should serve itself
69
+ * (versions agree, no project CLI or bin, already a hand-off, or the spawn failed). Must run before
70
+ * anything reads stdin: the child inherits the descriptor and owns the whole conversation.
71
+ */
72
+ export async function delegateToProjectCli(options: {
73
+ readonly cwd: string
74
+ readonly root: string
75
+ readonly version: string
76
+ readonly args: readonly string[]
77
+ }): Promise<number | undefined> {
78
+ if (process.env[MCP_DELEGATED_ENV] === "1") return undefined
79
+ const cli = await findProjectCli(options.root)
80
+ if (cli === undefined || cli.version === options.version || cli.bin === undefined)
81
+ return undefined
82
+ return handOffSession([cli.bin, "mcp", ...options.args], {
83
+ cwd: options.cwd,
84
+ env: { ...process.env, [MCP_DELEGATED_ENV]: "1" },
85
+ announce: `[nifra] mcp: this CLI is ${options.version}; the project installs @nifrajs/cli ${cli.version} - serving this session from ${cli.bin}`,
86
+ })
87
+ }
88
+
89
+ /**
90
+ * Hand this stdio session to `command`: the child inherits stdin, stdout and stderr and owns the
91
+ * whole conversation, and this process forwards signals and waits. Resolves to the child's exit code,
92
+ * or `undefined` when it could not start. Must run before anything reads stdin.
93
+ */
94
+ export async function handOffSession(
95
+ command: readonly string[],
96
+ options: {
97
+ readonly cwd: string
98
+ readonly env: Record<string, string | undefined>
99
+ /** Written to stderr once the child runs. */
100
+ readonly announce?: string
101
+ },
102
+ ): Promise<number | undefined> {
103
+ let child: ReturnType<typeof Bun.spawn>
104
+ try {
105
+ child = Bun.spawn([...command], {
106
+ cwd: options.cwd,
107
+ env: options.env,
108
+ stdin: "inherit",
109
+ stdout: "inherit",
110
+ stderr: "inherit",
111
+ })
112
+ } catch {
113
+ return undefined
114
+ }
115
+ // stderr only: stdout is the JSON-RPC channel, and it now belongs to the child.
116
+ if (options.announce !== undefined) process.stderr.write(`${options.announce}\n`)
117
+ const forward = (signal: NodeJS.Signals) => () => child.kill(signal)
118
+ const onTerm = forward("SIGTERM")
119
+ const onInt = forward("SIGINT")
120
+ process.on("SIGTERM", onTerm)
121
+ process.on("SIGINT", onInt)
122
+ try {
123
+ return await child.exited
124
+ } finally {
125
+ process.off("SIGTERM", onTerm)
126
+ process.off("SIGINT", onInt)
127
+ }
128
+ }
129
+
130
+ const refusalFix = (drift: ToolingDrift): string =>
131
+ drift.package === "@nifrajs/cli"
132
+ ? "Fix: point the MCP client at the project's own CLI (`./node_modules/.bin/nifra mcp`, reinstalling if that file is missing), or restart this server from the project directory so it hands the session over."
133
+ : `Fix: add the matching CLI to the project (\`bun add -d @nifrajs/cli@${drift.project}\`) and restart the MCP server - it then hands the session to the project's CLI - or point the client at a nifra ${drift.project} install.`
134
+
135
+ /** The in-band refusal a version-sensitive tool returns under a version split. */
136
+ export const versionRefusal = (tool: string, drift: ToolingDrift): McpToolResult => ({
137
+ content: [
138
+ {
139
+ type: "text",
140
+ text:
141
+ `${tool} refused: this MCP server runs nifra CLI ${drift.cli}, but the project installs ${drift.package} ${drift.project}, ` +
142
+ `so its answer would describe a different nifra than your code builds with. ${refusalFix(drift)}`,
143
+ },
144
+ ],
145
+ isError: true,
146
+ })
147
+
148
+ /**
149
+ * Swap each version-sensitive tool's handler for the refusal while `drift` holds. Apply it before
150
+ * namespacing, so a monorepo's `nifra_<app>_check` refuses too. No drift: the tools pass through.
151
+ */
152
+ export function refuseVersionSensitive(
153
+ tools: McpTool[],
154
+ drift: ToolingDrift | undefined,
155
+ ): McpTool[] {
156
+ if (drift === undefined) return tools
157
+ return tools.map((tool) =>
158
+ VERSION_SENSITIVE_TOOLS.has(tool.name)
159
+ ? { ...tool, handler: async () => versionRefusal(tool.name, drift) }
160
+ : tool,
161
+ )
162
+ }
@@ -28,7 +28,13 @@ export function docsTools(
28
28
  // The annotations state that for MCP hosts (and directory reviews check for them).
29
29
  {
30
30
  name: "nifra_docs",
31
- annotations: { title: "Search nifra docs", readOnlyHint: true, openWorldHint: false },
31
+ annotations: {
32
+ title: "Search nifra docs",
33
+ readOnlyHint: true,
34
+ destructiveHint: false,
35
+ idempotentHint: true,
36
+ openWorldHint: false,
37
+ },
32
38
  description:
33
39
  "Search nifra's framework documentation and get back ONLY the matching sections - auth, uploads, ISR, WebSockets, loaders, deployment, anything. Call with no query for the cheap section index; pass query (e.g. \"isr revalidate\") for the top matching sections. Use this instead of reading llms-full.txt (~150 KB) whole. For the EXACT TypeScript of a type/interface/function, call nifra_types instead (don't read .d.ts files).",
34
40
  inputSchema: {
@@ -57,10 +63,12 @@ export function docsTools(
57
63
  annotations: {
58
64
  title: "Get a verified nifra example",
59
65
  readOnlyHint: true,
66
+ destructiveHint: false,
67
+ idempotentHint: true,
60
68
  openWorldHint: false,
61
69
  },
62
70
  description:
63
- 'Get a VERIFIED, copy-pasteable nifra code example for a task - auth route, file upload, ISR page, loader/action, typed client, SSE, deployment, etc. Every snippet is typechecked against the installed nifra version, so it compiles as-is. PREFER THIS over writing nifra code from memory (training data drifts). Returns code as TEXT for direct use; to visually browse the whole example set instead, use nifra_gallery. Call with no query for the grouped index; pass query (e.g. "protected route", "upload", "isr revalidate") for matching snippets.',
71
+ 'Get a verified, copy-pasteable Nifra code example for a task - auth route, file upload, ISR page, loader/action, typed client, SSE, deployment, and more. Every snippet is typechecked against the installed Nifra version, so it compiles as-is. Returns code as text for direct use. Call with no query for the grouped index; pass query (e.g. "protected route", "upload", "isr revalidate") for matching snippets.',
64
72
  inputSchema: {
65
73
  type: "object",
66
74
  properties: {
@@ -84,9 +92,15 @@ export function docsTools(
84
92
  },
85
93
  {
86
94
  name: "nifra_types",
87
- annotations: { title: "Look up nifra API types", readOnlyHint: true, openWorldHint: false },
95
+ annotations: {
96
+ title: "Look up nifra API types",
97
+ readOnlyHint: true,
98
+ destructiveHint: false,
99
+ idempotentHint: true,
100
+ openWorldHint: false,
101
+ },
88
102
  description:
89
- 'Get the EXACT TypeScript declaration of any exported @nifrajs/* symbol - interface, type, class, function, const. Each signature is generated from the package\'s built .d.ts, so it is the LITERAL declaration: complete, authoritative, never prose and never truncated. THIS IS THE SOURCE OF TRUTH for nifra\'s types - do NOT read node_modules/@nifrajs/**/*.d.ts; call this instead. Pass `name` for an exact symbol (e.g. "RateLimitStore", "RouteSchema", "Context", "rateLimit"); pass `query` to search by keyword; omit both for the per-package index of names. A `name` lookup is always the complete declaration; a `query` returns a one-line summary plus the signature, collapsing an oversized body - pass `full: true` to override.',
103
+ 'Get the exact TypeScript declaration of any exported @nifrajs/* symbol - interface, type, class, function, or const. Each signature is generated from the package\'s built .d.ts and returns the complete declaration rather than prose. Pass `name` for an exact symbol (e.g. "RateLimitStore", "RouteSchema", "Context", "rateLimit"); pass `query` to search by keyword; omit both for the per-package index of names. A `name` lookup is always the complete declaration; a `query` returns a one-line summary plus the signature, collapsing an oversized body - pass `full: true` to override.',
90
104
  inputSchema: {
91
105
  type: "object",
92
106
  properties: {
@@ -132,6 +146,8 @@ export function docsTools(
132
146
  annotations: {
133
147
  title: "Guided nifra learning path",
134
148
  readOnlyHint: true,
149
+ destructiveHint: false,
150
+ idempotentHint: true,
135
151
  openWorldHint: false,
136
152
  },
137
153
  description:
@@ -153,10 +169,12 @@ export function docsTools(
153
169
  annotations: {
154
170
  title: "Frontend footgun guidance",
155
171
  readOnlyHint: true,
172
+ destructiveHint: false,
173
+ idempotentHint: true,
156
174
  openWorldHint: false,
157
175
  },
158
176
  description:
159
- 'Diagnose a CLIENT-SIDE nifra problem by symptom, across every adapter (React/Preact/Solid/Vue/Svelte/vanilla). Returns the cause, the concrete fix, and how to verify it. Covers the nifra SEAM (a server-only import leaking into a client component, a hydration mismatch, a duplicated framework runtime, loader-data typing) - where it points at the nifra_* tool that fixes and checks it - and the per-framework reactivity-loss idioms (Vue ref, Solid props, Svelte runes, React effect deps), where it points at that framework\'s own ESLint plugin. Call with no args for the index; pass `symptom` (e.g. "hydration mismatch", "value stopped updating") and/or `adapter` to filter. Use this when a rendered page misbehaves and nifra_check is green (check owns the static seam; this owns the rest).',
177
+ 'Diagnose a client-side Nifra problem by symptom across every adapter (React, Preact, Solid, Vue, Svelte, and vanilla). Returns the likely cause, a concrete fix, and how to verify it. Covers the Nifra server/client seam, hydration mismatches, duplicated framework runtimes, loader-data typing, and adapter-specific reactivity guidance. Call with no args for the index; pass `symptom` (e.g. "hydration mismatch", "value stopped updating") and/or `adapter` to filter.',
160
178
  inputSchema: {
161
179
  type: "object",
162
180
  properties: {