@nifrajs/cli 3.5.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (404) 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 +4 -0
  22. package/dist/check-diagnostics.d.ts.map +1 -1
  23. package/dist/check-diagnostics.js +13 -0
  24. package/dist/check-diagnostics.js.map +1 -1
  25. package/dist/check-scan.d.ts +49 -7
  26. package/dist/check-scan.d.ts.map +1 -1
  27. package/dist/check-scan.js +291 -62
  28. package/dist/check-scan.js.map +1 -1
  29. package/dist/check.d.ts.map +1 -1
  30. package/dist/check.js +99 -63
  31. package/dist/check.js.map +1 -1
  32. package/dist/cli.d.ts +42 -16
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +262 -82
  35. package/dist/cli.js.map +1 -1
  36. package/dist/command-catalog.d.ts +62 -5
  37. package/dist/command-catalog.d.ts.map +1 -1
  38. package/dist/command-catalog.js +275 -35
  39. package/dist/command-catalog.js.map +1 -1
  40. package/dist/config-target.d.ts +8 -0
  41. package/dist/config-target.d.ts.map +1 -0
  42. package/dist/config-target.js +45 -0
  43. package/dist/config-target.js.map +1 -0
  44. package/dist/contracts.d.ts.map +1 -1
  45. package/dist/contracts.js +52 -13
  46. package/dist/contracts.js.map +1 -1
  47. package/dist/db-audit.d.ts +37 -0
  48. package/dist/db-audit.d.ts.map +1 -0
  49. package/dist/db-audit.js +99 -0
  50. package/dist/db-audit.js.map +1 -0
  51. package/dist/db-child.d.ts +64 -0
  52. package/dist/db-child.d.ts.map +1 -0
  53. package/dist/db-child.js +213 -0
  54. package/dist/db-child.js.map +1 -0
  55. package/dist/db-config.d.ts +60 -0
  56. package/dist/db-config.d.ts.map +1 -0
  57. package/dist/db-config.js +176 -0
  58. package/dist/db-config.js.map +1 -0
  59. package/dist/db-tool.d.ts +84 -0
  60. package/dist/db-tool.d.ts.map +1 -0
  61. package/dist/db-tool.js +537 -0
  62. package/dist/db-tool.js.map +1 -0
  63. package/dist/dev-bun-config.d.ts +20 -13
  64. package/dist/dev-bun-config.d.ts.map +1 -1
  65. package/dist/dev-bun-config.js +98 -41
  66. package/dist/dev-bun-config.js.map +1 -1
  67. package/dist/dev-feed-tool.d.ts +106 -0
  68. package/dist/dev-feed-tool.d.ts.map +1 -0
  69. package/dist/dev-feed-tool.js +713 -0
  70. package/dist/dev-feed-tool.js.map +1 -0
  71. package/dist/dev-server-client.d.ts +67 -0
  72. package/dist/dev-server-client.d.ts.map +1 -0
  73. package/dist/dev-server-client.js +195 -0
  74. package/dist/dev-server-client.js.map +1 -0
  75. package/dist/diff-tool.d.ts +3 -3
  76. package/dist/diff-tool.d.ts.map +1 -1
  77. package/dist/diff-tool.js +6 -5
  78. package/dist/diff-tool.js.map +1 -1
  79. package/dist/docs-search.d.ts.map +1 -1
  80. package/dist/docs-search.js +12 -4
  81. package/dist/docs-search.js.map +1 -1
  82. package/dist/doctor.d.ts +17 -0
  83. package/dist/doctor.d.ts.map +1 -1
  84. package/dist/doctor.js +76 -15
  85. package/dist/doctor.js.map +1 -1
  86. package/dist/env-file.d.ts +15 -0
  87. package/dist/env-file.d.ts.map +1 -1
  88. package/dist/env-file.js +73 -1
  89. package/dist/env-file.js.map +1 -1
  90. package/dist/fix-recipes.d.ts.map +1 -1
  91. package/dist/fix-recipes.js +48 -22
  92. package/dist/fix-recipes.js.map +1 -1
  93. package/dist/frontend-guidance.js +5 -5
  94. package/dist/frontend-guidance.js.map +1 -1
  95. package/dist/i18n-check.d.ts +16 -0
  96. package/dist/i18n-check.d.ts.map +1 -0
  97. package/dist/i18n-check.js +154 -0
  98. package/dist/i18n-check.js.map +1 -0
  99. package/dist/init-agents.d.ts +49 -13
  100. package/dist/init-agents.d.ts.map +1 -1
  101. package/dist/init-agents.js +239 -34
  102. package/dist/init-agents.js.map +1 -1
  103. package/dist/internal/code-unit-order.d.ts +4 -0
  104. package/dist/internal/code-unit-order.d.ts.map +1 -0
  105. package/dist/internal/code-unit-order.js +6 -0
  106. package/dist/internal/code-unit-order.js.map +1 -0
  107. package/dist/internal/script-kind.d.ts +7 -0
  108. package/dist/internal/script-kind.d.ts.map +1 -0
  109. package/dist/internal/script-kind.js +12 -0
  110. package/dist/internal/script-kind.js.map +1 -0
  111. package/dist/internal/source-facts.d.ts +5 -0
  112. package/dist/internal/source-facts.d.ts.map +1 -1
  113. package/dist/internal/source-facts.js +61 -5
  114. package/dist/internal/source-facts.js.map +1 -1
  115. package/dist/internal/typescript-import.d.ts +49 -24
  116. package/dist/internal/typescript-import.d.ts.map +1 -1
  117. package/dist/internal/typescript-import.js +507 -67
  118. package/dist/internal/typescript-import.js.map +1 -1
  119. package/dist/introspect.d.ts +5 -1
  120. package/dist/introspect.d.ts.map +1 -1
  121. package/dist/introspect.js +92 -27
  122. package/dist/introspect.js.map +1 -1
  123. package/dist/learn.js +3 -3
  124. package/dist/learn.js.map +1 -1
  125. package/dist/levels-tool.d.ts +10 -0
  126. package/dist/levels-tool.d.ts.map +1 -1
  127. package/dist/levels-tool.js +33 -3
  128. package/dist/levels-tool.js.map +1 -1
  129. package/dist/load.d.ts +67 -17
  130. package/dist/load.d.ts.map +1 -1
  131. package/dist/load.js +78 -18
  132. package/dist/load.js.map +1 -1
  133. package/dist/manifest-tool.d.ts.map +1 -1
  134. package/dist/manifest-tool.js +3 -2
  135. package/dist/manifest-tool.js.map +1 -1
  136. package/dist/mcp-app-tool.d.ts.map +1 -1
  137. package/dist/mcp-app-tool.js +2 -0
  138. package/dist/mcp-app-tool.js.map +1 -1
  139. package/dist/mcp-context.js +1 -1
  140. package/dist/mcp-context.js.map +1 -1
  141. package/dist/mcp-delegate.d.ts +57 -0
  142. package/dist/mcp-delegate.d.ts.map +1 -0
  143. package/dist/mcp-delegate.js +130 -0
  144. package/dist/mcp-delegate.js.map +1 -0
  145. package/dist/mcp-docs-tools.d.ts.map +1 -1
  146. package/dist/mcp-docs-tools.js +23 -5
  147. package/dist/mcp-docs-tools.js.map +1 -1
  148. package/dist/mcp-exec.d.ts +2 -0
  149. package/dist/mcp-exec.d.ts.map +1 -1
  150. package/dist/mcp-exec.js +132 -108
  151. package/dist/mcp-exec.js.map +1 -1
  152. package/dist/mcp-http.d.ts.map +1 -1
  153. package/dist/mcp-http.js +17 -5
  154. package/dist/mcp-http.js.map +1 -1
  155. package/dist/mcp-io.d.ts +14 -0
  156. package/dist/mcp-io.d.ts.map +1 -1
  157. package/dist/mcp-io.js +45 -0
  158. package/dist/mcp-io.js.map +1 -1
  159. package/dist/mcp-isolate.d.ts +48 -0
  160. package/dist/mcp-isolate.d.ts.map +1 -0
  161. package/dist/mcp-isolate.js +222 -0
  162. package/dist/mcp-isolate.js.map +1 -0
  163. package/dist/mcp-project-child.d.ts +74 -0
  164. package/dist/mcp-project-child.d.ts.map +1 -0
  165. package/dist/mcp-project-child.js +130 -0
  166. package/dist/mcp-project-child.js.map +1 -0
  167. package/dist/mcp-render.js +2 -2
  168. package/dist/mcp-render.js.map +1 -1
  169. package/dist/mcp-root.d.ts +37 -9
  170. package/dist/mcp-root.d.ts.map +1 -1
  171. package/dist/mcp-root.js +110 -23
  172. package/dist/mcp-root.js.map +1 -1
  173. package/dist/mcp-run.d.ts +18 -1
  174. package/dist/mcp-run.d.ts.map +1 -1
  175. package/dist/mcp-run.js +82 -9
  176. package/dist/mcp-run.js.map +1 -1
  177. package/dist/mcp-ws.js +4 -1
  178. package/dist/mcp-ws.js.map +1 -1
  179. package/dist/mcp.d.ts +16 -3
  180. package/dist/mcp.d.ts.map +1 -1
  181. package/dist/mcp.js +81 -53
  182. package/dist/mcp.js.map +1 -1
  183. package/dist/migrate-layout.d.ts +44 -0
  184. package/dist/migrate-layout.d.ts.map +1 -0
  185. package/dist/migrate-layout.js +1155 -0
  186. package/dist/migrate-layout.js.map +1 -0
  187. package/dist/openapi-tool.d.ts +2 -2
  188. package/dist/openapi-tool.d.ts.map +1 -1
  189. package/dist/openapi-tool.js.map +1 -1
  190. package/dist/openapi-types.d.ts +2 -1
  191. package/dist/openapi-types.d.ts.map +1 -1
  192. package/dist/openapi-types.js +240 -12
  193. package/dist/openapi-types.js.map +1 -1
  194. package/dist/pipeline-report.d.ts +3 -2
  195. package/dist/pipeline-report.d.ts.map +1 -1
  196. package/dist/pipeline-report.js +19 -16
  197. package/dist/pipeline-report.js.map +1 -1
  198. package/dist/port.d.ts +5 -4
  199. package/dist/port.d.ts.map +1 -1
  200. package/dist/port.js +46 -29
  201. package/dist/port.js.map +1 -1
  202. package/dist/project-facts.d.ts +5 -0
  203. package/dist/project-facts.d.ts.map +1 -1
  204. package/dist/project-facts.js.map +1 -1
  205. package/dist/recipes/4.0.0.d.ts +9 -0
  206. package/dist/recipes/4.0.0.d.ts.map +1 -0
  207. package/dist/recipes/4.0.0.js +36 -0
  208. package/dist/recipes/4.0.0.js.map +1 -0
  209. package/dist/recipes/index.d.ts.map +1 -1
  210. package/dist/recipes/index.js +2 -0
  211. package/dist/recipes/index.js.map +1 -1
  212. package/dist/release-verification.d.ts +11 -1
  213. package/dist/release-verification.d.ts.map +1 -1
  214. package/dist/release-verification.js +47 -4
  215. package/dist/release-verification.js.map +1 -1
  216. package/dist/review.d.ts.map +1 -1
  217. package/dist/review.js +12 -8
  218. package/dist/review.js.map +1 -1
  219. package/dist/route-types.d.ts +28 -0
  220. package/dist/route-types.d.ts.map +1 -0
  221. package/dist/route-types.js +106 -0
  222. package/dist/route-types.js.map +1 -0
  223. package/dist/rules/codes.d.ts +9 -0
  224. package/dist/rules/codes.d.ts.map +1 -1
  225. package/dist/rules/codes.js +9 -0
  226. package/dist/rules/codes.js.map +1 -1
  227. package/dist/rules/data-guard.d.ts +12 -0
  228. package/dist/rules/data-guard.d.ts.map +1 -0
  229. package/dist/rules/data-guard.js +213 -0
  230. package/dist/rules/data-guard.js.map +1 -0
  231. package/dist/rules/index.d.ts +3 -0
  232. package/dist/rules/index.d.ts.map +1 -1
  233. package/dist/rules/index.js +17 -5
  234. package/dist/rules/index.js.map +1 -1
  235. package/dist/rules/islands.d.ts.map +1 -1
  236. package/dist/rules/islands.js +7 -4
  237. package/dist/rules/islands.js.map +1 -1
  238. package/dist/rules/legacy.d.ts.map +1 -1
  239. package/dist/rules/legacy.js +71 -38
  240. package/dist/rules/legacy.js.map +1 -1
  241. package/dist/rules/nano.d.ts.map +1 -1
  242. package/dist/rules/nano.js +15 -6
  243. package/dist/rules/nano.js.map +1 -1
  244. package/dist/rules/pages.d.ts +15 -0
  245. package/dist/rules/pages.d.ts.map +1 -0
  246. package/dist/rules/pages.js +88 -0
  247. package/dist/rules/pages.js.map +1 -0
  248. package/dist/rules/routes.d.ts +11 -0
  249. package/dist/rules/routes.d.ts.map +1 -1
  250. package/dist/rules/routes.js +180 -2
  251. package/dist/rules/routes.js.map +1 -1
  252. package/dist/rules/secrets.d.ts +10 -0
  253. package/dist/rules/secrets.d.ts.map +1 -0
  254. package/dist/rules/secrets.js +169 -0
  255. package/dist/rules/secrets.js.map +1 -0
  256. package/dist/rules/security.d.ts +7 -0
  257. package/dist/rules/security.d.ts.map +1 -1
  258. package/dist/rules/security.js +171 -65
  259. package/dist/rules/security.js.map +1 -1
  260. package/dist/rules/typescript.d.ts +16 -0
  261. package/dist/rules/typescript.d.ts.map +1 -0
  262. package/dist/rules/typescript.js +59 -0
  263. package/dist/rules/typescript.js.map +1 -0
  264. package/dist/rules/zones.d.ts +17 -0
  265. package/dist/rules/zones.d.ts.map +1 -0
  266. package/dist/rules/zones.js +174 -0
  267. package/dist/rules/zones.js.map +1 -0
  268. package/dist/scaffold.d.ts +25 -12
  269. package/dist/scaffold.d.ts.map +1 -1
  270. package/dist/scaffold.js +132 -87
  271. package/dist/scaffold.js.map +1 -1
  272. package/dist/sdk.d.ts +17 -3
  273. package/dist/sdk.d.ts.map +1 -1
  274. package/dist/sdk.js +941 -153
  275. package/dist/sdk.js.map +1 -1
  276. package/dist/search-terms.d.ts +2 -0
  277. package/dist/search-terms.d.ts.map +1 -1
  278. package/dist/search-terms.js +23 -8
  279. package/dist/search-terms.js.map +1 -1
  280. package/dist/smoke.d.ts +79 -0
  281. package/dist/smoke.d.ts.map +1 -0
  282. package/dist/smoke.js +494 -0
  283. package/dist/smoke.js.map +1 -0
  284. package/dist/sync-manifest.d.ts +10 -2
  285. package/dist/sync-manifest.d.ts.map +1 -1
  286. package/dist/sync-manifest.js +44 -19
  287. package/dist/sync-manifest.js.map +1 -1
  288. package/dist/sync-routes.d.ts +4 -3
  289. package/dist/sync-routes.d.ts.map +1 -1
  290. package/dist/sync-routes.js +4 -3
  291. package/dist/sync-routes.js.map +1 -1
  292. package/dist/test-tool.d.ts.map +1 -1
  293. package/dist/test-tool.js +30 -11
  294. package/dist/test-tool.js.map +1 -1
  295. package/dist/types-search.d.ts.map +1 -1
  296. package/dist/types-search.js +11 -5
  297. package/dist/types-search.js.map +1 -1
  298. package/dist/upgrade.d.ts +36 -6
  299. package/dist/upgrade.d.ts.map +1 -1
  300. package/dist/upgrade.js +167 -29
  301. package/dist/upgrade.js.map +1 -1
  302. package/dist/verification-plan.d.ts.map +1 -1
  303. package/dist/verification-plan.js +4 -0
  304. package/dist/verification-plan.js.map +1 -1
  305. package/dist/verification.d.ts.map +1 -1
  306. package/dist/verification.js +45 -12
  307. package/dist/verification.js.map +1 -1
  308. package/dist/web-app-options.d.ts +19 -0
  309. package/dist/web-app-options.d.ts.map +1 -0
  310. package/dist/web-app-options.js +49 -0
  311. package/dist/web-app-options.js.map +1 -0
  312. package/dist/work-graph.d.ts.map +1 -1
  313. package/dist/work-graph.js +29 -13
  314. package/dist/work-graph.js.map +1 -1
  315. package/docs/examples.json +185 -29
  316. package/docs/llms-full.txt +3807 -580
  317. package/docs/types.json +14229 -10939
  318. package/package.json +19 -12
  319. package/src/app-files.ts +40 -0
  320. package/src/assure-hydration.ts +139 -46
  321. package/src/assure.ts +17 -2
  322. package/src/capabilities-tool.ts +35 -9
  323. package/src/cdn-check.ts +268 -0
  324. package/src/check-diagnostics.ts +17 -0
  325. package/src/check-scan.ts +358 -63
  326. package/src/check.ts +97 -62
  327. package/src/cli.ts +296 -87
  328. package/src/command-catalog.ts +389 -48
  329. package/src/config-target.ts +47 -0
  330. package/src/contracts.ts +54 -12
  331. package/src/db-audit.ts +128 -0
  332. package/src/db-child.ts +285 -0
  333. package/src/db-config.ts +224 -0
  334. package/src/db-tool.ts +690 -0
  335. package/src/dev-bun-config.ts +116 -40
  336. package/src/dev-feed-tool.ts +912 -0
  337. package/src/dev-server-client.ts +267 -0
  338. package/src/diff-tool.ts +6 -5
  339. package/src/docs-search.ts +10 -3
  340. package/src/doctor.ts +98 -15
  341. package/src/env-file.ts +78 -1
  342. package/src/fix-recipes.ts +53 -32
  343. package/src/frontend-guidance.ts +5 -5
  344. package/src/i18n-check.ts +179 -0
  345. package/src/init-agents.ts +301 -38
  346. package/src/internal/code-unit-order.ts +5 -0
  347. package/src/internal/script-kind.ts +11 -0
  348. package/src/internal/source-facts.ts +96 -22
  349. package/src/internal/typescript-import.ts +663 -68
  350. package/src/introspect.ts +90 -27
  351. package/src/learn.ts +3 -3
  352. package/src/levels-tool.ts +47 -2
  353. package/src/load.ts +140 -33
  354. package/src/manifest-tool.ts +3 -2
  355. package/src/mcp-app-tool.ts +2 -0
  356. package/src/mcp-context.ts +1 -1
  357. package/src/mcp-delegate.ts +162 -0
  358. package/src/mcp-docs-tools.ts +23 -5
  359. package/src/mcp-exec.ts +160 -132
  360. package/src/mcp-http.ts +19 -5
  361. package/src/mcp-io.ts +57 -0
  362. package/src/mcp-isolate.ts +287 -0
  363. package/src/mcp-project-child.ts +182 -0
  364. package/src/mcp-render.ts +2 -2
  365. package/src/mcp-root.ts +117 -22
  366. package/src/mcp-run.ts +104 -10
  367. package/src/mcp-ws.ts +4 -1
  368. package/src/mcp.ts +106 -64
  369. package/src/migrate-layout.ts +1338 -0
  370. package/src/openapi-tool.ts +7 -3
  371. package/src/openapi-types.ts +315 -13
  372. package/src/pipeline-report.ts +22 -18
  373. package/src/port.ts +53 -30
  374. package/src/project-facts.ts +5 -0
  375. package/src/recipes/4.0.0.ts +37 -0
  376. package/src/recipes/index.ts +2 -0
  377. package/src/release-verification.ts +74 -5
  378. package/src/review.ts +13 -8
  379. package/src/route-types.ts +133 -0
  380. package/src/rules/codes.ts +9 -0
  381. package/src/rules/data-guard.ts +230 -0
  382. package/src/rules/index.ts +18 -5
  383. package/src/rules/islands.ts +12 -4
  384. package/src/rules/legacy.ts +71 -39
  385. package/src/rules/nano.ts +18 -6
  386. package/src/rules/pages.ts +99 -0
  387. package/src/rules/routes.ts +181 -2
  388. package/src/rules/secrets.ts +174 -0
  389. package/src/rules/security.ts +198 -70
  390. package/src/rules/typescript.ts +77 -0
  391. package/src/rules/zones.ts +195 -0
  392. package/src/scaffold.ts +147 -95
  393. package/src/sdk.ts +1215 -158
  394. package/src/search-terms.ts +24 -7
  395. package/src/smoke.ts +607 -0
  396. package/src/sync-manifest.ts +50 -19
  397. package/src/sync-routes.ts +4 -3
  398. package/src/test-tool.ts +32 -11
  399. package/src/types-search.ts +11 -4
  400. package/src/upgrade.ts +205 -25
  401. package/src/verification-plan.ts +13 -0
  402. package/src/verification.ts +50 -12
  403. package/src/web-app-options.ts +57 -0
  404. package/src/work-graph.ts +27 -13
package/src/mcp-root.ts CHANGED
@@ -5,9 +5,12 @@
5
5
  * 1. An explicit `nifra mcp <dir>` argument is taken literally (human configuration wins).
6
6
  * 2. A cwd guess walks UP to the nearest nifra marker (a `package.json` depending on `@nifrajs/*`,
7
7
  * or a `nifra.config.ts` monorepo root), so running from a subdirectory still finds the project.
8
+ * When nothing above is a project but the spawn directory is a package-manager workspace root
9
+ * (`package.json` `workspaces`) with exactly one nifra member, the server adopts that member.
8
10
  * 3. The client's MCP `roots` (its workspace folders) are requested after the handshake. When the
9
11
  * guess found no project - or found one DISJOINT from every workspace root - and exactly one
10
- * workspace root is a nifra project, the server adopts it.
12
+ * workspace root (or one member of a workspace-root folder) is a nifra project, the server
13
+ * adopts it.
11
14
  * 4. What cannot be resolved fails CLOSED: project-scoped tools refuse with a remediation message
12
15
  * instead of describing whatever directory the server happened to start in.
13
16
  *
@@ -15,12 +18,13 @@
15
18
  */
16
19
 
17
20
  import { readFile } from "node:fs/promises"
18
- import { dirname, isAbsolute, relative, resolve, sep } from "node:path"
21
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path"
19
22
  import { fileURLToPath } from "node:url"
20
23
 
21
24
  /** How the current root was chosen. `arg` = explicit `nifra mcp <dir>`; `cwd` = the spawn directory;
22
- * `parent` = a marker found walking up from cwd; `client-root` = adopted from the client's MCP roots. */
23
- export type McpRootSource = "arg" | "cwd" | "parent" | "client-root"
25
+ * `parent` = a marker found walking up from cwd; `workspace` = the one nifra member of the spawn
26
+ * directory's package-manager workspaces; `client-root` = adopted from the client's MCP roots. */
27
+ export type McpRootSource = "arg" | "cwd" | "parent" | "workspace" | "client-root"
24
28
 
25
29
  export interface McpRootState {
26
30
  /** Absolute directory every project-scoped tool operates on. */
@@ -31,11 +35,17 @@ export interface McpRootState {
31
35
  /** The client's workspace roots (absolute paths), or `null` until (unless) the client answers
32
36
  * `roots/list`. `null` disables mismatch detection - no data is not a mismatch. */
33
37
  readonly clientRoots: readonly string[] | null
38
+ /** Nifra members of the spawn directory's workspaces when there were several to choose from - the
39
+ * remediation list, since adopting one of them would be a guess. */
40
+ readonly workspaceCandidates?: readonly string[]
34
41
  }
35
42
 
36
43
  const MAX_CLIENT_ROOTS = 64
37
44
  const MAX_ROOT_URI_LENGTH = 16_384
38
45
  const MAX_ROOT_PATH_LENGTH = 4_096
46
+ /** Workspace members probed per workspace root. A monorepo larger than this is pointed at explicitly
47
+ * (`nifra mcp <dir>`) rather than scanned on every server start. */
48
+ const MAX_WORKSPACE_MEMBERS = 256
39
49
 
40
50
  /** A directory is a nifra project when its `package.json` depends on any `@nifrajs/*` package, or it
41
51
  * is a `nifra.config.ts` monorepo root. Backend-only projects (no web config, no `routes/`) count -
@@ -69,8 +79,56 @@ export async function findNifraRoot(start: string): Promise<string | null> {
69
79
  }
70
80
  }
71
81
 
82
+ /** The `workspaces` globs a `package.json` declares (the array form, or Yarn's `{ packages }`), or
83
+ * `null` when the directory is not a workspace root. Negated patterns are dropped: they only exclude. */
84
+ async function workspacePatterns(dir: string): Promise<readonly string[] | null> {
85
+ let field: unknown
86
+ try {
87
+ field = (
88
+ JSON.parse(await readFile(join(dir, "package.json"), "utf8")) as Record<string, unknown>
89
+ ).workspaces
90
+ } catch {
91
+ return null
92
+ }
93
+ const list = Array.isArray(field)
94
+ ? field
95
+ : typeof field === "object" && field !== null
96
+ ? (field as { packages?: unknown }).packages
97
+ : undefined
98
+ if (!Array.isArray(list)) return null
99
+ return list.filter(
100
+ (pattern): pattern is string =>
101
+ typeof pattern === "string" && pattern !== "" && !pattern.startsWith("!"),
102
+ )
103
+ }
104
+
105
+ /**
106
+ * The nifra projects among `dir`'s package-manager workspace members, in declaration order. Empty when
107
+ * `dir` declares no `workspaces`. A member is a directory a pattern matches that holds a `package.json`;
108
+ * `node_modules` is never a member.
109
+ */
110
+ export async function workspaceProjects(dir: string): Promise<string[]> {
111
+ const patterns = await workspacePatterns(dir)
112
+ if (patterns === null) return []
113
+ const members = new Set<string>()
114
+ let probed = 0
115
+ for (const pattern of patterns) {
116
+ const glob = new Bun.Glob(`${pattern.replace(/\/+$/, "")}/package.json`)
117
+ for await (const match of glob.scan({ cwd: dir, onlyFiles: true })) {
118
+ if (match.split(/[\\/]/).includes("node_modules")) continue
119
+ if (++probed > MAX_WORKSPACE_MEMBERS) break
120
+ members.add(dirname(join(dir, match)))
121
+ }
122
+ if (probed > MAX_WORKSPACE_MEMBERS) break
123
+ }
124
+ const projects: string[] = []
125
+ for (const member of members) if (await isNifraProjectDir(member)) projects.push(member)
126
+ return projects
127
+ }
128
+
72
129
  /** Initial root state. An explicit dir is taken literally (no walk-up - the human named it); a cwd
73
- * guess walks up so a subdirectory spawn still lands on the project. */
130
+ * guess walks up so a subdirectory spawn still lands on the project, then tries the spawn directory's
131
+ * workspace members so a monorepo root finds its one nifra app. */
74
132
  export async function resolveRootState(
75
133
  requested: string,
76
134
  explicit: boolean,
@@ -92,7 +150,17 @@ export async function resolveRootState(
92
150
  clientRoots: null,
93
151
  }
94
152
  }
95
- return { root: requested, source: "cwd", isProject: false, clientRoots: null }
153
+ const members = await workspaceProjects(requested)
154
+ if (members.length === 1) {
155
+ return { root: members[0] as string, source: "workspace", isProject: true, clientRoots: null }
156
+ }
157
+ return {
158
+ root: requested,
159
+ source: "cwd",
160
+ isProject: false,
161
+ clientRoots: null,
162
+ ...(members.length > 1 ? { workspaceCandidates: members } : {}),
163
+ }
96
164
  }
97
165
 
98
166
  /** `file://` roots from a `roots/list` result, as absolute paths. Non-file and malformed URIs are
@@ -151,9 +219,9 @@ export function rootMismatch(state: McpRootState): boolean {
151
219
  /**
152
220
  * Fold the client's workspace roots into the state, adopting one of them when the current root is
153
221
  * wrong and the correction is unambiguous: the root was GUESSED (never an explicit `nifra mcp <dir>`),
154
- * it is not a project - or is one disjoint from the whole workspace - and exactly ONE workspace root
155
- * is a nifra project. Ambiguity (zero or several candidates) adopts nothing; the guard's remediation
156
- * message lists the candidates instead.
222
+ * it is not a project - or is one disjoint from the whole workspace - and exactly ONE candidate exists
223
+ * (see {@link nifraCandidates}). Ambiguity (zero or several candidates) adopts nothing; the guard's
224
+ * remediation message lists the candidates instead.
157
225
  */
158
226
  export async function applyClientRoots(
159
227
  state: McpRootState,
@@ -162,8 +230,7 @@ export async function applyClientRoots(
162
230
  const next: McpRootState = { ...state, clientRoots: paths }
163
231
  if (state.source === "arg") return next
164
232
  if (next.isProject && !rootMismatch(next)) return next
165
- const candidates: string[] = []
166
- for (const path of paths) if (await isNifraProjectDir(path)) candidates.push(path)
233
+ const candidates = await nifraCandidates(paths)
167
234
  if (candidates.length !== 1) return next
168
235
  return {
169
236
  root: candidates[0] as string,
@@ -173,11 +240,15 @@ export async function applyClientRoots(
173
240
  }
174
241
  }
175
242
 
176
- /** The nifra project roots among the client's workspace roots - the remediation list when adoption
177
- * was ambiguous. */
243
+ /** The nifra projects the client's workspace roots name: each root that is a project itself, and
244
+ * otherwise the nifra members of a root that is a package-manager workspace. The remediation list
245
+ * when adoption was ambiguous. */
178
246
  export async function nifraCandidates(paths: readonly string[]): Promise<string[]> {
179
247
  const candidates: string[] = []
180
- for (const path of paths) if (await isNifraProjectDir(path)) candidates.push(path)
248
+ for (const path of paths) {
249
+ if (await isNifraProjectDir(path)) candidates.push(path)
250
+ else for (const member of await workspaceProjects(path)) candidates.push(member)
251
+ }
181
252
  return candidates
182
253
  }
183
254
 
@@ -198,10 +269,15 @@ export async function rootVerdict(state: McpRootState): Promise<McpRootVerdict>
198
269
  ? ` Client workspace roots: ${state.clientRoots.join(", ")}.`
199
270
  : ""
200
271
  if (!state.isProject) {
201
- const candidates = state.clientRoots !== null ? await nifraCandidates(state.clientRoots) : []
272
+ const candidates = [
273
+ ...new Set([
274
+ ...(state.workspaceCandidates ?? []),
275
+ ...(state.clientRoots !== null ? await nifraCandidates(state.clientRoots) : []),
276
+ ]),
277
+ ]
202
278
  const hint =
203
279
  candidates.length > 1
204
- ? ` Several workspace roots are nifra projects (${candidates.join(", ")}) - pass one: \`nifra mcp <dir>\`.`
280
+ ? ` Several workspace folders are nifra projects (${candidates.join(", ")}) - pass one: \`nifra mcp <dir>\`.`
205
281
  : ""
206
282
  return {
207
283
  blocked:
@@ -250,7 +326,8 @@ export interface ToolingDrift {
250
326
  export const driftNote = (drift: ToolingDrift): string =>
251
327
  `WARNING: this server runs nifra CLI ${drift.cli}, but the project installs ${drift.package} ${drift.project}. ` +
252
328
  "Types, checks, and docs may describe a different version than your code builds with. " +
253
- "Fix: run the project's own CLI (`bunx --bun nifra mcp` from the project directory, or point the client at ./node_modules/.bin/nifra)."
329
+ `Fix: re-pin the project's MCP launch to the nifra it installs with \`${syncMcpCommand(drift.cli)}\` (run in the project directory), then restart the agent. ` +
330
+ "If this server is not launched from the project's .mcp.json, run the project's own CLI instead (`bunx --bun nifra mcp` from the project directory, or point the client at ./node_modules/.bin/nifra)."
254
331
 
255
332
  /** Feature-level agreement. Patch drift is not reported: it never changes the surface being described. */
256
333
  const featureVersion = (version: string): string => version.split(".").slice(0, 2).join(".")
@@ -285,16 +362,34 @@ export async function detectToolingDrift(
285
362
  root: string,
286
363
  cliVersion: string,
287
364
  ): Promise<ToolingDrift | undefined> {
365
+ const installed = await installedNifraVersion(root)
366
+ if (installed === undefined) return undefined
367
+ return featureVersion(installed.version) === featureVersion(cliVersion)
368
+ ? undefined
369
+ : { cli: cliVersion, project: installed.version, package: installed.package }
370
+ }
371
+
372
+ /**
373
+ * The nifra the project builds with: its `@nifrajs/cli` when installed, else its `@nifrajs/core`
374
+ * (`fixed` versioning keeps the two in lockstep, so either names the CLI that matches the project).
375
+ * The version drift detection compares against and the version `init-agents --sync-mcp` pins to.
376
+ */
377
+ export async function installedNifraVersion(
378
+ root: string,
379
+ ): Promise<{ readonly package: string; readonly version: string } | undefined> {
288
380
  for (const name of ["@nifrajs/cli", "@nifrajs/core"]) {
289
- const project = await installedVersion(root, name)
290
- if (project === undefined) continue
291
- return featureVersion(project) === featureVersion(cliVersion)
292
- ? undefined
293
- : { cli: cliVersion, project, package: name }
381
+ const version = await installedVersion(root, name)
382
+ if (version !== undefined) return { package: name, version }
294
383
  }
295
384
  return undefined
296
385
  }
297
386
 
387
+ /** The command that re-pins a project's MCP launch to the nifra it installs. Named with the running
388
+ * CLI's own exact version: the binary printing this is the one known to have `--sync-mcp`, and it pins
389
+ * to the project's version, not its own. */
390
+ export const syncMcpCommand = (cliVersion: string): string =>
391
+ `bunx @nifrajs/cli@${cliVersion} init-agents --sync-mcp`
392
+
298
393
  /** Tool names served from the bundled corpus; they do not depend on the project root. */
299
394
  const PROJECT_FREE_TOOLS = new Set(["nifra_docs", "nifra_example", "nifra_types", "nifra_learn"])
300
395
 
package/src/mcp-run.ts CHANGED
@@ -8,7 +8,16 @@
8
8
  import { existsSync, realpathSync } from "node:fs"
9
9
  import { isAbsolute, relative, resolve, sep } from "node:path"
10
10
  import { pathToFileURL } from "node:url"
11
- import { type AppLike, runApp } from "@nifrajs/runner"
11
+ import { type AppLike, type RequestSpec, type RunResult, runApp } from "@nifrajs/runner"
12
+ import {
13
+ captureInto,
14
+ createDevFeed,
15
+ type DevFeed,
16
+ type DevLogLevel,
17
+ runWithDevRequest,
18
+ } from "@nifrajs/web/dev-feed"
19
+ import type { Diagnostic } from "@nifrajs/web/diagnostic"
20
+ import { BACKEND_APP_FILE } from "./app-files.ts"
12
21
  import {
13
22
  CHILD_INPUT_MAX_BYTES,
14
23
  CHILD_OUTPUT_MAX_BYTES,
@@ -17,7 +26,7 @@ import {
17
26
  serializeBoundedJson,
18
27
  } from "./mcp-io.ts"
19
28
 
20
- const ENTRY_CANDIDATES = ["backend.ts", "app.ts", "src/backend.ts", "src/app.ts"]
29
+ const ENTRY_CANDIDATES = [BACKEND_APP_FILE, "app.ts", "src/app.ts"]
21
30
 
22
31
  const errString = (err: unknown): string =>
23
32
  err instanceof Error ? `${err.name}: ${err.message}` : String(err)
@@ -85,12 +94,75 @@ export async function runBackend(
85
94
  if ("error" in loaded) return loaded
86
95
 
87
96
  try {
88
- return { results: await runApp(loaded.app, requests as Parameters<typeof runApp>[1]) }
97
+ return { results: await runCaptured(cwd, loaded.app, requests) }
89
98
  } catch (err) {
90
99
  return { error: errString(err) }
91
100
  }
92
101
  }
93
102
 
103
+ /** What `nifra_run` adds to the runner's result: what the app printed, and what failed, structured. */
104
+ export interface RunCapture {
105
+ readonly logs?: ReadonlyArray<{ readonly level: DevLogLevel; readonly message: string }>
106
+ readonly errors?: readonly Diagnostic[]
107
+ }
108
+
109
+ const MAX_LOGS_PER_REQUEST = 100
110
+
111
+ /**
112
+ * Run `requests` one at a time, each inside its own request context, so what a handler logs - and
113
+ * core's `unhandled request error` line behind a bare 500 - lands on the result of the request that
114
+ * produced it, redacted, with the error as a Diagnostic (codeframe, cause, fix). Without this a 500 came
115
+ * back as `internal_error` and the reason only ever reached a stderr nobody reads.
116
+ */
117
+ export async function runCaptured(
118
+ cwd: string,
119
+ app: AppLike,
120
+ requests: readonly unknown[],
121
+ ): Promise<Array<RunResult & RunCapture>> {
122
+ const feed = createDevFeed({ root: cwd, persist: false })
123
+ const detach = captureInto(feed)
124
+ const runs: Array<{ readonly requestId: string; readonly result: RunResult }> = []
125
+ try {
126
+ for (const [index, spec] of requests.entries()) {
127
+ const requestId = `run${index + 1}`
128
+ const [method, path] = describeSpec(spec)
129
+ const [result] = await runWithDevRequest(
130
+ { feedId: feed.id, requestId, method, path },
131
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: runApp validates each spec itself and reports a malformed one as that request's error.
132
+ () => runApp(app, [spec as RequestSpec]),
133
+ )
134
+ if (result !== undefined) runs.push({ requestId, result })
135
+ }
136
+ // A line a handler scheduled for later (a promise continuation, a 0 ms timer) still lands.
137
+ await new Promise((resolve) => setTimeout(resolve, 0))
138
+ } finally {
139
+ detach()
140
+ }
141
+ return runs.map(({ requestId, result }) => withCapture(feed, requestId, result))
142
+ }
143
+
144
+ function describeSpec(spec: unknown): [string, string] {
145
+ if (typeof spec !== "object" || spec === null) return ["GET", "/"]
146
+ const method = "method" in spec && typeof spec.method === "string" ? spec.method : "GET"
147
+ const path = "path" in spec && typeof spec.path === "string" ? spec.path : "/"
148
+ return [method, path]
149
+ }
150
+
151
+ function withCapture(feed: DevFeed, requestId: string, result: RunResult): RunResult & RunCapture {
152
+ if (result.error !== undefined) {
153
+ const thrown = new Error(result.error.message)
154
+ thrown.name = result.error.name
155
+ if (result.error.stack !== undefined) thrown.stack = result.error.stack
156
+ feed.recordError(thrown, { category: "api", requestId })
157
+ }
158
+ const capture: { -readonly [K in keyof RunCapture]: RunCapture[K] } = {}
159
+ const logs = feed.logs({ requestId, limit: MAX_LOGS_PER_REQUEST }).logs
160
+ if (logs.length > 0) capture.logs = logs.map(({ level, message }) => ({ level, message }))
161
+ const errors = feed.errors({ requestId }).errors
162
+ if (errors.length > 0) capture.errors = errors.map((entry) => entry.diagnostic)
163
+ return { ...result, ...capture }
164
+ }
165
+
94
166
  interface WorkerMessage {
95
167
  readonly id?: unknown
96
168
  readonly input?: { readonly requests?: unknown; readonly entry?: string }
@@ -113,13 +185,31 @@ function safeResponseId(value: unknown): number | string | null {
113
185
  return null
114
186
  }
115
187
 
116
- async function runWorker(cwd: string): Promise<void> {
188
+ /**
189
+ * Keep stdout for the protocol. The app's console and any direct `process.stdout.write` go to stderr,
190
+ * or a stray `console.log` in a handler would corrupt the JSON the parent reads. Returns the writer
191
+ * that still reaches the real stdout.
192
+ */
193
+ function reserveStdout(): (text: string) => void {
117
194
  redirectConsoleToStderr()
195
+ const original: unknown = Reflect.get(process.stdout, "write")
196
+ const write = typeof original === "function" ? original.bind(process.stdout) : undefined
197
+ // Bound now, before any capture patches stderr: a redirected stdout line is captured once, as stdout.
198
+ const toStderr = process.stderr.write.bind(process.stderr)
199
+ Reflect.set(process.stdout, "write", (chunk: unknown, ...rest: unknown[]) =>
200
+ Reflect.apply(toStderr, process.stderr, [chunk, ...rest]),
201
+ )
202
+ return (text) => {
203
+ write?.(text)
204
+ }
205
+ }
206
+
207
+ async function runWorker(cwd: string, protocol: (text: string) => void): Promise<void> {
118
208
  const apps = new Map<string, Promise<{ app: AppLike } | { error: string }>>()
119
209
  const send = (id: unknown, output: unknown): void => {
120
210
  const responseId = safeResponseId(id)
121
211
  const encoded = serializeBoundedJson({ id: responseId, output }, CHILD_OUTPUT_MAX_BYTES - 1)
122
- process.stdout.write(
212
+ protocol(
123
213
  `${encoded ?? JSON.stringify({ id: responseId, output: { error: "worker output exceeded the size limit" } })}\n`,
124
214
  )
125
215
  }
@@ -175,9 +265,7 @@ async function runWorker(cwd: string): Promise<void> {
175
265
  continue
176
266
  }
177
267
  try {
178
- send(id, {
179
- results: await runApp(app.app, requests as Parameters<typeof runApp>[1]),
180
- })
268
+ send(id, { results: await runCaptured(cwd, app.app, requests) })
181
269
  } catch (err) {
182
270
  send(id, { error: errString(err) })
183
271
  }
@@ -188,8 +276,9 @@ async function runWorker(cwd: string): Promise<void> {
188
276
  // this only executes when run directly (not when imported by a test).
189
277
  if (import.meta.main) {
190
278
  const cwd = process.argv[2] ?? process.cwd()
279
+ const protocol = reserveStdout()
191
280
  if (process.argv.includes("--worker")) {
192
- await runWorker(cwd)
281
+ await runWorker(cwd, protocol)
193
282
  process.exit(0)
194
283
  }
195
284
  let output: unknown
@@ -207,8 +296,13 @@ if (import.meta.main) {
207
296
  output = { error: "invalid input: expected JSON { requests: [...] }" }
208
297
  }
209
298
  }
210
- process.stdout.write(
299
+ // `Bun.write` to the fd, not the patched `process.stdout.write`, so the answer is flushed when it
300
+ // resolves. Then exit: the app's own handles (a pool, an interval) would keep this single-use
301
+ // process alive until the parent's timeout, and the parent waits for it to exit.
302
+ await Bun.write(
303
+ Bun.stdout,
211
304
  serializeBoundedJson(output, CHILD_OUTPUT_MAX_BYTES, 2) ??
212
305
  JSON.stringify({ error: `output exceeded ${CHILD_OUTPUT_MAX_BYTES} bytes` }),
213
306
  )
307
+ process.exit(0)
214
308
  }
package/src/mcp-ws.ts CHANGED
@@ -324,5 +324,8 @@ if (import.meta.main) {
324
324
  error: 'invalid input: expected JSON { "path": "/..." }',
325
325
  } satisfies WebSocketVerificationResult
326
326
  }
327
- process.stdout.write(JSON.stringify(output, null, 2))
327
+ // Flushed, then exit: the app's own handles would keep this single-use process alive until the
328
+ // parent's timeout, as in mcp-run and mcp-render.
329
+ await Bun.write(Bun.stdout, JSON.stringify(output, null, 2))
330
+ process.exit(0)
328
331
  }