@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/dist/cli.js CHANGED
@@ -8,8 +8,9 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
8
8
  return path;
9
9
  };
10
10
  /**
11
- * `nifra` - the zero-config CLI for a nifra app. Reads `framework.ts` + `backend.ts` + `routes/` from
12
- * the project root (see {@link loadApp}) and wires the right `@nifrajs/web` entrypoint:
11
+ * `nifra` - the zero-config CLI for a nifra app. Reads `backend/framework.ts` + `backend/app.ts` +
12
+ * `routes/` from the project root (see {@link loadApp}) and wires the right `@nifrajs/web`
13
+ * entrypoint:
13
14
  *
14
15
  * nifra dev true-HMR dev server (Bun native HMR + nifra SSR) - @nifrajs/web/dev
15
16
  * nifra build emit a complete target-specific deploy directory - @nifrajs/web/build
@@ -20,21 +21,24 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
20
21
  import { existsSync, readFileSync, realpathSync } from "node:fs";
21
22
  import { resolve } from "node:path";
22
23
  import { fileURLToPath } from "node:url";
23
- import { inProcessClient } from "@nifrajs/client";
24
24
  import { createWebApp, DEFAULT_DEV_PORT, } from "@nifrajs/web";
25
25
  import { discoverRoutes } from "@nifrajs/web/fs";
26
+ import { formatShadowedPages, shadowedPages } from "@nifrajs/web/route-manifest";
27
+ import { adapterFile, BACKEND_APP_FILE } from "./app-files.js";
26
28
  import { bindCommandArgv, findCommandSpec, renderCommandCatalogHelp } from "./command-catalog.js";
27
- import { applyEnvFiles, takeEnvFileFlags } from "./env-file.js";
28
- import { loadApp } from "./load.js";
29
+ import { applyEnvFiles, forgetAutoLoadedEnv, takeEnvFileFlags } from "./env-file.js";
30
+ import { FRAMEWORK_WEB_OPTIONS, loadApp } from "./load.js";
29
31
  import { chooseBuildPipeline, describePipeline } from "./pipeline-guard.js";
32
+ import { frameworkMountPaths, frameworkOptionImports, frameworkWebAppOptions, } from "./web-app-options.js";
30
33
  /** Forward a re-exec'd Bun child's output without relying on Windows inheriting a pipe-of-a-pipe. */
31
- async function forwardChildOutput(stream, sink) {
34
+ async function forwardChildOutput(stream, sink, observe) {
32
35
  const reader = stream.getReader();
33
36
  try {
34
37
  for (;;) {
35
38
  const { done, value } = await reader.read();
36
39
  if (done)
37
40
  return;
41
+ observe?.(value);
38
42
  await sink.write(value);
39
43
  }
40
44
  }
@@ -42,6 +46,30 @@ async function forwardChildOutput(stream, sink) {
42
46
  reader.releaseLock();
43
47
  }
44
48
  }
49
+ /** The last `max` bytes of a stream, decoded: what a crashed child printed last. */
50
+ function streamTail(max) {
51
+ let chunks = [];
52
+ let size = 0;
53
+ return {
54
+ push(chunk) {
55
+ chunks.push(chunk);
56
+ size += chunk.length;
57
+ while (size - (chunks[0]?.length ?? 0) >= max && chunks.length > 1) {
58
+ size -= chunks.shift()?.length ?? 0;
59
+ }
60
+ },
61
+ text() {
62
+ const all = new Uint8Array(size);
63
+ let offset = 0;
64
+ for (const chunk of chunks) {
65
+ all.set(chunk, offset);
66
+ offset += chunk.length;
67
+ }
68
+ chunks = [all];
69
+ return new TextDecoder().decode(all.subarray(Math.max(0, size - max)));
70
+ },
71
+ };
72
+ }
45
73
  const HELP = `nifra - zero-config dev/build/start for a nifra app
46
74
 
47
75
  Usage:
@@ -57,14 +85,33 @@ Usage:
57
85
  loud warning instead of a hard failure, so a duplicate React/
58
86
  adapter copy from a linked sibling repo doesn't take dev down
59
87
  while you fix the resolution. Dev only - \`nifra build\` still fails.
88
+ [--no-indicator] No in-page issues badge. Browser errors still reach
89
+ \`nifra errors\` and the agent tools.
90
+ nifra errors [--since <n>] [--category <c>] [--request <id>] What the running dev server caught:
91
+ SSR/loader/API/build/browser/hydration errors, each a structured
92
+ diagnostic. Finds the server itself; reads the log a crashed
93
+ one left. Exits 1 while current-code errors are open.
94
+ nifra logs [--since <n>] [--level <l>] [--grep <s>] The dev server's console, server and
95
+ browser, each line tagged with its request.
96
+ nifra cdn-check <url> Check a deployed page behind a CDN: served from cache, no
97
+ internal or CDN-only headers reaching the visitor, and soft
98
+ navigations get page data, not the cached document. Exits 1
99
+ on a failure.
100
+ nifra db schema [<table>] The development database declared as \`devDatabase\` in
101
+ nifra.config.ts: tables, columns, keys, indexes.
102
+ nifra db query "<sql>" [--explain [--analyze]] One read-only SELECT (or its plan), in a fresh
103
+ process killed at devDatabase.timeoutMs. Rows capped, secrets
104
+ masked. Postgres: a superuser connection is refused.
105
+ nifra db role Print the SQL for a read-only Postgres role. Runs nothing.
106
+ nifra db audit [--limit <n>] What nifra db ran (.nifra/db-audit.jsonl), never the rows.
60
107
  nifra build [--out <dir>] [--report] Emit a complete deploy directory.
61
108
  [--target <t>] Target a FULL deploy dir for <t>:
62
- bun | node | deno | cf-pages | vercel | static. Packages
109
+ bun | node | deno | cloudflare | vercel | static. Packages
63
110
  buildClient + buildServer (+ prerender for static) so an app
64
111
  no longer hand-writes build-<target>.ts + _worker.ts +
65
112
  _routes.json. The server entry is generated from your
66
- framework.ts (adapter) + backend.ts + routes/. --report prints
67
- a per-chunk size + gzip table (biggest first).
113
+ backend/framework.ts (adapter) + backend/app.ts + routes/.
114
+ --report prints a per-chunk size + gzip table (biggest first).
68
115
  [--vite | --bun] Force the bundler. Without a flag it follows your config: Bun
69
116
  (faster, Bun-native) unless your ONLY transforms are
70
117
  \`vitePlugins\`, which the Bun build cannot run - then Vite,
@@ -85,12 +132,20 @@ Usage:
85
132
  against it: a route the target can't honour (an ssr route on a
86
133
  static build, ISR where there's no revalidation) exits nonzero
87
134
  with the consequence - run in CI so it fails the build, not prod.
88
- nifra init-agents [--force] [--json] Retrofit an EXISTING app with the agent-discovery files a new
89
- app ships: .mcp.json + .cursor/mcp.json (register this project's
90
- nifra MCP), CLAUDE.md (MCP-first preamble + @AGENTS.md import),
91
- and a "## MCP server" section in AGENTS.md. No-clobber by
92
- default (skips a file you've customized); --force overwrites the
93
- owned files. AGENTS.md is only appended to, never overwritten.
135
+ nifra init-agents [--force] [--json] Retrofit an EXISTING app with the agent files a new app ships:
136
+ .mcp.json + .cursor/mcp.json (register this project's nifra MCP),
137
+ each agent's pointer to AGENTS.md (CLAUDE.md, GEMINI.md,
138
+ .cursor/rules/nifra.mdc, .github/copilot-instructions.md), and
139
+ AGENTS.md's "## MCP server" and (with routes/) "## Project
140
+ structure" sections. No-clobber by default (skips a file you've
141
+ customized); --force overwrites the owned files. AGENTS.md is
142
+ only appended to, never overwritten.
143
+ nifra init-agents --sync-mcp [--json] Re-pin an existing app's MCP launch to the nifra the project
144
+ installs: rewrites only the @nifrajs/cli@x.y.z version in
145
+ .mcp.json, .cursor/mcp.json, CLAUDE.md and AGENTS.md's MCP
146
+ section - every other byte stays as it is. Creates nothing.
147
+ At a workspace root with one nifra member, the two registries'
148
+ launch also names that member (\`mcp app\`).
94
149
  nifra mcp [dir] Start an MCP server (stdio) exposing this project to a coding
95
150
  agent. The project root is [dir] when given, else resolved from
96
151
  cwd (marker walk-up + the client's MCP roots); tools refuse
@@ -99,15 +154,20 @@ Usage:
99
154
  nifra_run (backend), nifra_render (SSR a page), nifra_docs,
100
155
  nifra_example (verified snippets), nifra_scaffold (route→file),
101
156
  nifra_check (drift gate + fixes), nifra_levels (verification
102
- ladder), nifra_doctor (deps), nifra_explain (structured errors),
103
- nifra_inspect (request traces), nifra_learn (guided build path).
157
+ ladder), nifra_doctor (deps), nifra_errors + nifra_logs (what
158
+ the running dev server saw), nifra_db_schema + nifra_db_query +
159
+ nifra_db_role (the declared dev database, read-only),
160
+ nifra_explain (structured errors), nifra_inspect (request
161
+ traces), nifra_learn (guided build path).
104
162
  nifra docs-mcp [--port <n>] Serve the PUBLIC docs MCP over HTTP (nifra_docs + nifra_example) -
105
163
  self-host on a VPS so any remote agent can learn nifra. Default :8787.
106
164
  nifra learn [<step>] Print the guided build-an-app path (the human view of nifra_learn):
107
165
  no arg for the step index, a number for one step's goal/do/verify.
108
- nifra check [--json] [--lints-only] Gate: typecheck + lints (hand-rolled fetch(), untyped client("…"),
109
- server-only imports in routes/). Run as "done"; --json for agents;
110
- --lints-only skips tsc for a near-instant inner-loop pass.
166
+ nifra check [--json] [--sarif] [--lints-only]
167
+ Gate: typecheck + lints (hand-rolled fetch(), untyped client("…"),
168
+ backend code in browser code). Run as "done"; --json for agents;
169
+ --sarif emits the same stable diagnostics as SARIF 2.1.0 for external
170
+ review tools; --lints-only skips tsc for a near-instant inner-loop pass.
111
171
  nifra verify [--release] [--json] Run the shared repository verification gate. --release runs the
112
172
  full build, test, coverage, corpus, consumer, and cross-runtime gate.
113
173
  nifra fix [--code <NF-code>] Apply a registered diagnostic recipe, then print the remaining
@@ -125,8 +185,8 @@ Usage:
125
185
  request field or a removed response field breaks; widening a
126
186
  request enum or adding a response field doesn't) and fails
127
187
  closed. Exits non-zero on any breaking change - run it in CI.
128
- nifra sdk --lang <python|go> [--out <file>]
129
- Generate a deterministic non-TypeScript SDK from backend.ts.
188
+ nifra sdk --lang <python|go> [--out <file>] [--strict]
189
+ Generate a deterministic non-TypeScript SDK from backend/app.ts.
130
190
  nifra assure [--config <file>] [--json] Route-assurance report. Human table by default; --json emits
131
191
  the {ok, routes, findings} report for agents.
132
192
  nifra assure --bundle [--json] [--strict] [--out <file>] [--hydration] [--interact]
@@ -164,22 +224,40 @@ Usage:
164
224
  print production readiness, with --strict making absent
165
225
  applicable guarantees fail. --auto-fix writes safe local-
166
226
  version dependency fixes.
167
- nifra upgrade <version> Run the per-release upgrade recipe for <version>: sweep every
168
- [--write] [--no-verify] matching dependency pin (preserving ^/~/exact), move removed
169
- [--list] [--json] packages, apply exact imports, then verify with nifra check.
227
+ nifra upgrade <version> Upgrade to <version>, running every release recipe between the
228
+ [--write] [--no-verify] installed version and it, oldest first: move removed packages,
229
+ [--list] [--json] apply exact imports, pin every matching dependency (keeping
230
+ [--exact] ^/~, or exact with --exact), then verify with nifra check.
170
231
  [--allow-downgrade] Dry-run by default; --write applies then verifies (--no-verify
171
- to skip). --list shows available targets. Fail-closed on an
172
- unknown version or a rollback (--allow-downgrade overrides).
173
- Deterministic + idempotent.
232
+ to skip). --list shows available targets. A version newer than
233
+ this CLI prints the command for that release's CLI. Fail-closed
234
+ on an unknown version or a rollback (--allow-downgrade
235
+ overrides). Deterministic + idempotent.
236
+ nifra migrate layout [--write] [--json]
237
+ Move an app onto the frontend/backend split: split each route
238
+ into x.tsx + x.backend.ts, fold _middleware.ts into
239
+ _layout.backend.ts, move backend.ts/framework.ts under backend/,
240
+ and zone the other modules into frontend/, backend/ or shared/.
241
+ Dry-run by default; --write applies.
242
+ nifra types [--check] [--json] Generate each route's ./+types module under .nifra/types: its
243
+ params, the data its output schemas let through, and api typed
244
+ by backend/app.ts. dev, build and check refresh them too;
245
+ --check fails when one is stale.
174
246
  nifra port [--target <t>] [--json] Portability linter: print a feature × deploy-target capability
175
247
  [--ci] [--strict] matrix (in-memory stores, in-process cron/WebSocket, Bun/Deno
176
248
  globals, node: builtins) with file:line evidence. --target auto-
177
249
  detected from build/deploy scripts, wrangler.toml, or vercel config;
178
250
  --ci (or any --target) exits nonzero when a used feature is
179
251
  unsupported on the target; --strict also fails on caveats.
252
+ nifra i18n check [entry] Check i18n catalogs: coverage, missing + unused keys, ICU syntax,
253
+ [--json] [--strict] placeholder + tag parity, plural cases, script purity,
254
+ untranslated messages. IMPORTS the entry (default: the first
255
+ i18n.ts in ., lib/, src/, src/lib/, app/), which exports
256
+ \`locales\` + \`catalogs\` (+ optional \`ignore\`). Exits 1 on
257
+ errors; --strict also on warnings.
180
258
 
181
- Reads nifra.config.ts (adapter + clientModule + plugins; or framework.ts), backend.ts (optional), and
182
- routes/ from the current directory. Run from your project root.
259
+ Reads nifra.config.ts (adapter + clientModule + plugins; or backend/framework.ts), backend/app.ts
260
+ (optional), and routes/ from the current directory. Run from your project root.
183
261
 
184
262
  Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with \`--port <n>\` (alias \`-p\`) or the
185
263
  \`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).
@@ -192,12 +270,10 @@ set in the process environment is never overwritten by a file.
192
270
 
193
271
  ${renderCommandCatalogHelp()}`;
194
272
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
195
- const CLI_VERSION = "3.3.0";
273
+ const CLI_VERSION = "4.0.0";
196
274
  // A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
197
275
  const asAdapter = (v) => v;
198
276
  const asBunPlugins = (v) => v;
199
- const asUse = (v) => v;
200
- const apiOf = (backend) => backend === undefined ? {} : { api: inProcessClient(backend) };
201
277
  /**
202
278
  * Render an error for the CLI, unwrapping the detail a bare `.message` drops.
203
279
  *
@@ -246,7 +322,7 @@ async function dev(app, flags) {
246
322
  const decision = chooseBuildPipeline(app.resolvedPlugins, flags.vite ? "vite" : flags.bun ? "bun" : undefined, "dev");
247
323
  if (decision.pipeline === "bun") {
248
324
  // Bun's dev-server bundler takes plugins only via bunfig `[serve.static]`, read at process
249
- // start - so the boundary plugins (server-fn stubs, server-only emptying) are delivered by
325
+ // start - so the boundary plugins (zone guard, server-fn stubs) are delivered by
250
326
  // generating a config and re-execing this same command once with `--config=`. The child proves
251
327
  // it IS the configured child with a per-launch random token (matched against the file the
252
328
  // parent just wrote, consumed on first read). A fixed sentinel here would be a secret-leak
@@ -262,7 +338,7 @@ async function dev(app, flags) {
262
338
  "relaunches. Something is rewriting `.nifra/dev-bun/` or the environment between " +
263
339
  "launches; refusing to serve without the client-boundary plugins.");
264
340
  }
265
- const { bunfigPath, launchToken } = await writeBunDevConfig(app.cwd, app.configPath);
341
+ const { bunfigPath, launchToken } = await writeBunDevConfig(app.cwd, app.configPath, app.framework.define);
266
342
  // Bun 1.4 on Windows may discard the script-relative portion of argv when `--config` is
267
343
  // present. Keep a second, authenticated copy of the user vector in the child's environment so
268
344
  // the configured process cannot silently fall back to `nifra`'s help and exit 0. The launch token
@@ -310,16 +386,31 @@ async function dev(app, flags) {
310
386
  NODE_ENV: process.env.NODE_ENV ?? "development",
311
387
  },
312
388
  });
389
+ // The child's last words, kept so a crash leaves a record `nifra errors` can read after the fact.
390
+ const stderrTail = streamTail(16 * 1024);
313
391
  const forwarded = Promise.all([
314
392
  forwardChildOutput(child.stdout, Bun.stdout),
315
- forwardChildOutput(child.stderr, Bun.stderr),
393
+ forwardChildOutput(child.stderr, Bun.stderr, stderrTail.push),
316
394
  ]);
317
- const forward = () => child.kill("SIGINT");
395
+ let interrupted = false;
396
+ const forward = () => {
397
+ interrupted = true;
398
+ child.kill("SIGINT");
399
+ };
318
400
  process.on("SIGINT", forward);
319
401
  process.on("SIGTERM", forward);
320
402
  try {
321
403
  const [code] = await Promise.all([child.exited, forwarded]);
322
404
  process.exitCode = code;
405
+ if (code !== 0 && !interrupted) {
406
+ try {
407
+ const { recordDevCrash } = await import("@nifrajs/web/dev-feed");
408
+ recordDevCrash(app.cwd, code, stderrTail.text());
409
+ }
410
+ catch {
411
+ // The crash is already on the terminal; failing to persist it must not mask the exit code.
412
+ }
413
+ }
323
414
  }
324
415
  finally {
325
416
  process.off("SIGINT", forward);
@@ -370,12 +461,13 @@ async function dev(app, flags) {
370
461
  ...(fw.publicDir !== undefined ? { publicDir: fw.publicDir } : {}),
371
462
  ...(fw.conditions ? { conditions: fw.conditions } : {}),
372
463
  ...(fw.define ? { define: fw.define } : {}),
373
- createApp: (clientEntry, importQuery) => createWebApp({
464
+ indicator: showsIndicator(flags, fw),
465
+ createApp: (clientEntry, importQuery, dev) => createWebApp({
374
466
  adapter: asAdapter(fw.adapter),
375
467
  manifest: discoverRoutes(routesDir, { importQuery }),
376
468
  clientEntry,
377
- ...(fw.use ? { use: asUse(fw.use) } : {}),
378
- ...apiOf(backend),
469
+ ...frameworkWebAppOptions(fw, backend),
470
+ onLoaderError: dev.onLoaderError,
379
471
  }),
380
472
  });
381
473
  console.log(`nifra dev (bun) → http://localhost:${server.port}`);
@@ -424,17 +516,18 @@ async function dev(app, flags) {
424
516
  poll: flags.poll,
425
517
  port: flags.port,
426
518
  ...(flags.allowDuplicateIdentity ? { allowDuplicateIdentity: true } : {}),
519
+ indicator: showsIndicator(flags, fw),
427
520
  ...(fw.conditions ? { conditions: fw.conditions } : {}),
428
521
  ...(fw.define ? { define: fw.define } : {}),
429
522
  // `load` resolves route modules through VITE, not through Bun. That is what makes the Vite
430
523
  // pipeline own the whole phase: the client and the server now agree on every specifier, so
431
524
  // `resolve.dedupe` finally governs SSR and the dual-React crash cannot occur.
432
- createApp: (clientEntry, load) => createWebApp({
525
+ createApp: (clientEntry, load, dev) => createWebApp({
433
526
  adapter: asAdapter(fw.adapter),
434
527
  manifest: discoverRoutes(routesDir, { load }),
435
528
  clientEntry,
436
- ...(fw.use ? { use: asUse(fw.use) } : {}),
437
- ...apiOf(backend),
529
+ ...frameworkWebAppOptions(fw, backend),
530
+ onLoaderError: dev.onLoaderError,
438
531
  }),
439
532
  });
440
533
  console.log(`nifra dev (vite) → http://localhost:${server.port}`);
@@ -443,23 +536,26 @@ async function dev(app, flags) {
443
536
  /**
444
537
  * `nifra build --target <t>` - package the engine (buildClient + buildServer + prerender) into one
445
538
  * command that emits a full deploy dir, so an app no longer hand-writes build-bun.ts + _worker.ts +
446
- * _routes.json per target. The adapter is imported from `framework.ts` (the edge-bundlable file - never
447
- * `nifra.config.ts`, which pulls in Vite plugins), and the backend from `backend.ts` when present;
448
- * `buildTarget` generates the per-target server entry from those + the app's `routes/`.
539
+ * _routes.json per target. The adapter is imported from `backend/framework.ts` (the edge-bundlable
540
+ * file - never `nifra.config.ts`, which pulls in Vite plugins), and the backend from
541
+ * `backend/app.ts` when present; `buildTarget` generates the per-target server entry from those +
542
+ * the app's `routes/`.
449
543
  */
450
544
  /**
451
545
  * Refuse a `use` the generated server entry cannot import.
452
546
  *
453
- * `nifra build` emits `import { use } from <frameworkFile>` (framework.ts, the edge-bundlable file),
454
- * but `loadApp` prefers `nifra.config.ts` - so an app with BOTH files whose `use` lives only in
455
- * `nifra.config.ts` makes `fw.use` defined while `framework.ts` exports nothing of that name, and the
456
- * build dies later with an opaque bundler error pointing at generated code. Refuse it here with the
457
- * exact move instead, in the spirit of `assertPipelineSeparation` (load.ts).
547
+ * `nifra build` emits `import { use } from <frameworkFile>` (backend/framework.ts, the
548
+ * edge-bundlable file), but `loadApp` prefers `nifra.config.ts` - so an app with BOTH files whose
549
+ * `use` lives only in `nifra.config.ts` makes `fw.use` defined while `backend/framework.ts` exports
550
+ * nothing of that name, and the build dies later with an opaque bundler error pointing at generated
551
+ * code. Refuse it here with the exact move instead, in the spirit of `assertPipelineSeparation`
552
+ * (load.ts).
458
553
  *
459
- * Detected by importing `frameworkFile` and checking the named export, not by comparing paths alone:
460
- * a split app legitimately DEFINES `use` in framework.ts and re-exports it from nifra.config.ts
461
- * (exactly how `adapter` reaches both readers), and a path compare would refuse that correct layout.
462
- * The import is cheap - framework.ts is already in the loaded config's module graph in that layout.
554
+ * Detected by importing `frameworkFile` and checking the named export, not by comparing paths
555
+ * alone: a split app legitimately DEFINES `use` in backend/framework.ts and re-exports it from
556
+ * nifra.config.ts (exactly how `adapter` reaches both readers), and a path compare would refuse
557
+ * that correct layout. The import is cheap - backend/framework.ts is already in the loaded config's
558
+ * module graph in that layout.
463
559
  */
464
560
  export async function assertUseIsEdgeExported(use, configPath, frameworkFile) {
465
561
  if (use === undefined || configPath === frameworkFile)
@@ -468,10 +564,55 @@ export async function assertUseIsEdgeExported(use, configPath, frameworkFile) {
468
564
  if (typeof mod.use === "function")
469
565
  return;
470
566
  throw new Error(`[nifra] \`use\` is exported from ${configPath} but not from ${frameworkFile}. ` +
471
- "`nifra build` generates a server entry that imports `use` from framework.ts (the edge-bundled " +
472
- "file), so this build would fail later with an opaque bundler error inside generated code.\n\n" +
473
- " - Define `use` in framework.ts and re-export it from nifra.config.ts " +
474
- '(`export { use } from "./framework.ts"`) so `nifra dev` sees it too.');
567
+ "`nifra build` generates a server entry that imports `use` from backend/framework.ts (the " +
568
+ "edge-bundled file), so this build would fail later with an opaque bundler error inside generated code.\n\n" +
569
+ " - Define `use` in backend/framework.ts and re-export it from nifra.config.ts " +
570
+ '(`export { use } from "./backend/framework.ts"`) so `nifra dev` sees it too.');
571
+ }
572
+ /**
573
+ * Refuse a forwarded framework field (`apiPrefix`, `mounts`, `csp`, ...) the generated server entry
574
+ * would not see, or would see with a different value.
575
+ *
576
+ * `nifra dev` reads these from the loaded config (`nifra.config.ts` when it exists) while the
577
+ * server entry imports them from backend/framework.ts. A field set only in nifra.config.ts would be
578
+ * missing from production, and one defined separately in each file could differ - dev mounting the
579
+ * backend at one path while production mounts it at another. Identity is required, which a
580
+ * re-export satisfies.
581
+ */
582
+ export async function assertFrameworkOptionsEdgeExported(fw, configPath, frameworkFile) {
583
+ if (configPath === frameworkFile)
584
+ return;
585
+ const set = FRAMEWORK_WEB_OPTIONS.filter((name) => fw[name] !== undefined);
586
+ if (set.length === 0)
587
+ return;
588
+ const mod = (await import(__rewriteRelativeImportExtension(frameworkFile)).catch(() => ({})));
589
+ const drifted = set.filter((name) => mod[name] !== fw[name]);
590
+ if (drifted.length === 0)
591
+ return;
592
+ const names = drifted.map((name) => `\`${name}\``).join(", ");
593
+ throw new Error(`[nifra] ${names} ${drifted.length === 1 ? "is" : "are"} exported from ${configPath} but not, or not as the same value, from ${frameworkFile}. ` +
594
+ "`nifra build` generates a server entry that imports these from backend/framework.ts, so production " +
595
+ "would serve a different app than `nifra dev`.\n\n" +
596
+ ` - Define ${drifted.length === 1 ? "it" : "them"} in backend/framework.ts and re-export from nifra.config.ts ` +
597
+ `(\`export { ${drifted.join(", ")} } from "./backend/framework.ts"\`).`);
598
+ }
599
+ /**
600
+ * Refuse a build whose page routes sit under a mount: each would ship as a route that can never render.
601
+ * Read from the config and `routes/` alone; the server also checks its own mount table at startup,
602
+ * which covers a mount added inside `use`.
603
+ */
604
+ /** `nifra dev` shows the in-page issues badge unless `--no-indicator` or `dev.indicator: false` turns it off. */
605
+ export function showsIndicator(flags, fw) {
606
+ return !flags.noIndicator && fw.dev?.indicator !== false;
607
+ }
608
+ export function assertNoShadowedPages(app) {
609
+ const paths = frameworkMountPaths(app.framework, app.backend !== undefined);
610
+ if (paths.length === 0)
611
+ return;
612
+ const shadowed = shadowedPages(discoverRoutes(app.routesDir), paths);
613
+ if (shadowed.length === 0)
614
+ return;
615
+ throw new Error(`[nifra] build blocked: ${formatShadowedPages(shadowed)}`);
475
616
  }
476
617
  /**
477
618
  * A production build must not ship a workspace-linked package whose `default`-condition artifact is
@@ -503,35 +644,55 @@ async function assertFreshWorkspaceDists(cwd) {
503
644
  : ` ${f.package}: ${f.distFile} is ${f.behindSeconds}s behind ${f.sourceFile}`);
504
645
  throw new Error(`[nifra] build blocked: ${stale.length} workspace-linked package(s) ship a stale or missing dist artifact, so the deployed app would serve code this build did not produce:\n${lines.join("\n")}\nBuild the offending package(s) (\`bun run build\` in each) so every export target exists and is at least as new as its source, then rebuild.`);
505
646
  }
506
- async function buildForTarget(app, target, flags) {
507
- const { isBuildTarget, renderSizeReport, BUILD_TARGETS } = await import("@nifrajs/web/build");
508
- if (!isBuildTarget(target)) {
509
- throw new Error(`[nifra] unknown --target "${target}". Valid: ${BUILD_TARGETS.join(", ")}.`);
510
- }
647
+ async function buildForTarget(app, flag, flags) {
648
+ const { parseBuildTarget, renderSizeReport } = await import("@nifrajs/web/build");
649
+ const target = parseBuildTarget(flag ?? app.framework.target ?? "bun", flag === undefined ? "target in nifra.config.ts" : "--target");
511
650
  if (flags.vite && flags.bun) {
512
651
  throw new Error("[nifra] `nifra build` takes `--vite` or `--bun`, not both.");
513
652
  }
653
+ const { framework: fw } = app;
654
+ const cssCodeSplitRequested = fw.cssCodeSplit === false;
655
+ const hasBunTransforms = app.resolvedPlugins.clientPlugins.length > 0 || app.resolvedPlugins.serverPlugins.length > 0;
656
+ if (cssCodeSplitRequested && flags.bun) {
657
+ throw new Error("[nifra] `cssCodeSplit: false` is a Vite-only production option, but `nifra build --bun` was requested. " +
658
+ "Drop `--bun` (Nifra will select Vite automatically) or remove the aggregate CSS setting.");
659
+ }
660
+ if (cssCodeSplitRequested && hasBunTransforms) {
661
+ throw new Error("[nifra] `cssCodeSplit: false` requires the Vite production pipeline, but this app also declares " +
662
+ "Bun client/server plugins. Use `--vite` with Vite equivalents, or remove the aggregate CSS setting; " +
663
+ "switching pipelines would silently drop the Bun transforms.");
664
+ }
514
665
  // Which bundler builds this app. Default is Bun; an app whose ONLY transforms are `vitePlugins` gets
515
666
  // Vite instead, because the Bun build cannot run them and would silently omit the work. `--vite`/`--bun`
516
667
  // force it, and the one forced combination that would drop transforms throws.
517
- const decision = chooseBuildPipeline(app.resolvedPlugins, flags.vite ? "vite" : flags.bun ? "bun" : undefined);
668
+ let decision = chooseBuildPipeline(app.resolvedPlugins, flags.vite ? "vite" : flags.bun ? "bun" : undefined);
669
+ if (cssCodeSplitRequested && decision.pipeline === "bun") {
670
+ // With no Bun transforms, aggregate CSS is a Vite-only request that can be honored safely by
671
+ // selecting the same pipeline the user would otherwise have to force explicitly.
672
+ decision = {
673
+ pipeline: "vite",
674
+ why: "auto",
675
+ reason: "auto: this app requested Vite's aggregate CSS output (`cssCodeSplit: false`)",
676
+ };
677
+ }
518
678
  const useVite = decision.pipeline === "vite";
519
679
  // Same deploy-dir output - buildTargetVite delegates to the same orchestrator as buildTarget - so only
520
680
  // the bundler and the plugin FORMAT (Vite plugins, not Bun) differ.
521
681
  const buildTarget = useVite
522
682
  ? (await import("@nifrajs/web/build-vite")).buildTargetVite
523
683
  : (await import("@nifrajs/web/build")).buildTarget;
524
- const { framework: fw, routesDir, outDir, cwd, backend } = app;
525
- // The server entry must import the adapter from `framework.ts` (edge-safe), not the loaded config.
526
- // `loadApp` guarantees one of them exists; prefer framework.ts so a multi-target app's Vite-plugin
527
- // config never reaches the edge bundle (see load.ts module header).
528
- const frameworkFile = existsSync(resolve(cwd, "framework.ts"))
529
- ? resolve(cwd, "framework.ts")
530
- : existsSync(resolve(cwd, "nifra.config.ts"))
531
- ? resolve(cwd, "nifra.config.ts")
532
- : resolve(cwd, "framework.ts");
533
- const backendFile = resolve(cwd, "backend.ts");
684
+ const { routesDir, cwd, backend } = app;
685
+ // `vercel deploy --prebuilt` uploads `.vercel/output` from the project root, so that is where a
686
+ // Vercel build goes unless `--out` says otherwise.
687
+ const outDir = target === "vercel" && flags.out === undefined ? resolve(cwd, ".vercel/output") : app.outDir;
688
+ // The server entry must import the adapter from `backend/framework.ts` (edge-safe), not the
689
+ // loaded config, so a multi-target app's Vite-plugin config never reaches the edge bundle
690
+ // (load.ts header).
691
+ const frameworkFile = adapterFile(cwd);
692
+ const backendFile = resolve(cwd, BACKEND_APP_FILE);
534
693
  await assertUseIsEdgeExported(fw.use, app.configPath, frameworkFile);
694
+ await assertFrameworkOptionsEdgeExported(fw, app.configPath, frameworkFile);
695
+ assertNoShadowedPages(app);
535
696
  // Plugin FORMAT differs by pipeline: the Bun build takes Bun plugins (clientPlugins/serverPlugins); the
536
697
  // Vite build takes the app's Vite plugins (fw.vitePlugins) for BOTH halves. buildTargetWith forwards
537
698
  // whatever it's given straight to the chosen bundler, which casts to its own plugin type.
@@ -551,12 +712,17 @@ async function buildForTarget(app, target, flags) {
551
712
  clientModule: fw.clientModule,
552
713
  adapterImport: frameworkFile,
553
714
  ...(fw.use ? { useImport: frameworkFile } : {}),
715
+ ...frameworkOptionImports(fw, frameworkFile),
554
716
  ...(backend !== undefined && existsSync(backendFile) ? { backendImport: backendFile } : {}),
555
717
  ...plugins,
556
718
  ...(fw.conditions ? { conditions: fw.conditions } : {}),
557
719
  ...(fw.define ? { define: fw.define } : {}),
558
720
  ...(fw.publicDir !== undefined ? { publicDir: fw.publicDir } : {}),
559
721
  ...(fw.publicEnvPrefix !== undefined ? { publicEnvPrefix: fw.publicEnvPrefix } : {}),
722
+ ...(fw.secretExemptions !== undefined ? { secretExemptions: fw.secretExemptions } : {}),
723
+ ...(fw.cssCodeSplit !== undefined ? { cssCodeSplit: fw.cssCodeSplit } : {}),
724
+ ...(fw.cssLoading !== undefined ? { cssLoading: fw.cssLoading } : {}),
725
+ ...(fw.clientIp !== undefined ? { clientIp: fw.clientIp } : {}),
560
726
  // The static target needs a built app to drive prerendering - only build it when targeting static.
561
727
  ...(target === "static" ? { prerenderApp: await buildPrerenderApp(app) } : {}),
562
728
  });
@@ -587,18 +753,18 @@ async function buildPrerenderApp(app) {
587
753
  adapter: asAdapter(fw.adapter),
588
754
  manifest: discoverRoutes(routesDir),
589
755
  clientEntry: client.entry,
590
- ...(fw.use ? { use: asUse(fw.use) } : {}),
756
+ ...frameworkWebAppOptions(fw, backend),
591
757
  ...(client.routes ? { routePreload: client.routes } : {}),
592
758
  ...(client.css ? { styles: client.css } : {}),
593
759
  ...(client.routeStyles ? { routeStyles: client.routeStyles } : {}),
594
- ...apiOf(backend),
760
+ ...(client.cssLoading !== undefined ? { cssLoading: client.cssLoading } : {}),
595
761
  });
596
762
  }
597
763
  async function start(app, flags) {
598
764
  const serverFile = resolve(app.outDir, "server.js");
599
765
  if (!existsSync(serverFile)) {
600
766
  // A Cloudflare Pages build emits `_worker.js` (a Workers bundle), not a self-hosting `server.js` - so
601
- // `nifra start` on a dir built for cf-pages would otherwise fail with a bare "no server.js". Name the
767
+ // `nifra start` on a dir built for cloudflare would otherwise fail with a bare "no server.js". Name the
602
768
  // actual mismatch and the fix.
603
769
  if (existsSync(resolve(app.outDir, "_worker.js"))) {
604
770
  throw new Error(`[nifra] ${app.outDir} holds a Cloudflare Pages worker bundle (_worker.js), not a self-hosting ` +
@@ -614,13 +780,14 @@ export function parseFlags(args) {
614
780
  // the SAME uncommon port for `nifra dev` and `nifra start` (DEFAULT_DEV_PORT) so a project's URL is
615
781
  // stable across commands and doesn't collide with the usual 3000/5173/8080 crowd.
616
782
  let port = Number(Bun.env.PORT ?? DEFAULT_DEV_PORT);
617
- let out = "dist";
783
+ let out;
618
784
  let poll = Bun.env.CHOKIDAR_USEPOLLING === "1";
619
- let target = "bun";
785
+ let target;
620
786
  let report = false;
621
787
  let vite = false;
622
788
  let bun = false;
623
789
  let allowDuplicateIdentity = false;
790
+ let noIndicator = false;
624
791
  for (let i = 0; i < args.length; i++) {
625
792
  const a = args[i];
626
793
  if ((a === "--port" || a === "-p") && args[i + 1])
@@ -639,11 +806,13 @@ export function parseFlags(args) {
639
806
  bun = true;
640
807
  else if (a === "--allow-duplicate-identity")
641
808
  allowDuplicateIdentity = true;
809
+ else if (a === "--no-indicator")
810
+ noIndicator = true;
642
811
  }
643
812
  if (!Number.isFinite(port) || port < 0 || port > 65535) {
644
813
  throw new Error(`[nifra] invalid --port: ${port}`);
645
814
  }
646
- return { port, out, poll, target, report, vite, bun, allowDuplicateIdentity };
815
+ return { port, out, poll, target, report, vite, bun, allowDuplicateIdentity, noIndicator };
647
816
  }
648
817
  /**
649
818
  * `assure` uses the structured-bundle lane only when explicitly asked. A bare `--json` is NOT a
@@ -678,8 +847,15 @@ function installReflectionExitHint() {
678
847
  inFlight = command;
679
848
  };
680
849
  }
850
+ /** Set on the copy of `nifra mcp` that serves without `.env` files, so it does not restart again. */
851
+ const MCP_WITHOUT_ENV_FILES = "NIFRA_MCP_WITHOUT_ENV_FILES";
681
852
  async function main() {
682
853
  const { argv: rawArgv, files: envFiles } = takeEnvFileFlags(cliArgv());
854
+ // `nifra mcp` keeps none of the `.env` values Bun loaded at startup. Decided before the
855
+ // `--env-file`s below, which the caller names explicitly and which stay.
856
+ const forgotten = rawArgv[0] === "mcp" && process.env[MCP_WITHOUT_ENV_FILES] !== "1"
857
+ ? await forgetAutoLoadedEnv()
858
+ : [];
683
859
  // Applied before any command runs: a reflecting command imports the app on its first await, and the
684
860
  // app reads its environment at module scope, so the variables must already be in place by then.
685
861
  if (envFiles.length > 0)
@@ -701,6 +877,16 @@ async function main() {
701
877
  // `mcp` runs a long-lived stdio server and loads the project lazily per-tool - it must not go through
702
878
  // the eager `loadApp` below (which would fail fast on a project that's API-only / not yet built).
703
879
  if (command === "mcp") {
880
+ // Bun passes what it loaded to every subprocess whatever `process.env` says now, so the session
881
+ // moves to a copy of this process that never loads `.env` files.
882
+ if (forgotten.length > 0) {
883
+ const { handOffSession } = await import("./mcp-delegate.js");
884
+ const exitCode = await handOffSession([process.execPath, "--no-env-file", ...process.argv.slice(1)], { cwd: process.cwd(), env: { ...process.env, [MCP_WITHOUT_ENV_FILES]: "1" } });
885
+ if (exitCode !== undefined) {
886
+ process.exitCode = exitCode;
887
+ return;
888
+ }
889
+ }
704
890
  const { runMcpServer } = await import("./mcp.js");
705
891
  // `nifra mcp [dir]` - an explicit project directory pins the root (for clients configured outside
706
892
  // the project); otherwise the server resolves it from cwd + the client's MCP roots.
@@ -746,11 +932,16 @@ async function main() {
746
932
  }));
747
933
  return;
748
934
  }
749
- const catalogSpec = command === undefined ? undefined : findCommandSpec(command);
935
+ // `nifra db <sub>` is the catalog's `db-<sub>` command, projected to MCP as `nifra_db_<sub>`.
936
+ const dbSub = command === "db" && argv[1] !== undefined && !argv[1].startsWith("-") ? argv[1] : undefined;
937
+ if (command === "db" && (dbSub === undefined || findCommandSpec(`db-${dbSub}`) === undefined)) {
938
+ throw new Error("[nifra] usage: nifra db schema|query|role|audit - see `nifra help`");
939
+ }
940
+ const catalogSpec = findCommandSpec(dbSub === undefined ? command : `db-${dbSub}`);
750
941
  if (catalogSpec?.transports.includes("cli")) {
751
942
  const markReflecting = installReflectionExitHint();
752
943
  try {
753
- const input = bindCommandArgv(catalogSpec, argv.slice(1));
944
+ const input = bindCommandArgv(catalogSpec, argv.slice(dbSub === undefined ? 1 : 2));
754
945
  markReflecting(command);
755
946
  const output = await catalogSpec.run(input, { cwd: process.cwd(), cliVersion: CLI_VERSION });
756
947
  markReflecting(undefined);
@@ -763,15 +954,28 @@ async function main() {
763
954
  else {
764
955
  console.log(catalogSpec.render(output, input).join("\n"));
765
956
  }
766
- if (catalogSpec.success !== undefined && !catalogSpec.success(output, input))
957
+ if (catalogSpec.exitCode !== undefined) {
958
+ process.exitCode = catalogSpec.exitCode(output, input);
959
+ }
960
+ else if (catalogSpec.success !== undefined && !catalogSpec.success(output, input)) {
767
961
  process.exitCode = 1;
962
+ }
768
963
  }
769
964
  catch (err) {
770
965
  // A thrown error is nifra's own reporting path, not a silent abort: clear the sentinel so the
771
966
  // hint stays reserved for the case where the process died inside the app import.
772
967
  markReflecting(undefined);
773
968
  console.error(formatCliError(err));
774
- process.exitCode = 1;
969
+ const candidate = typeof err === "object" && err !== null && "exitCode" in err
970
+ ? err.exitCode
971
+ : undefined;
972
+ process.exitCode =
973
+ typeof candidate === "number" &&
974
+ Number.isInteger(candidate) &&
975
+ candidate >= 0 &&
976
+ candidate <= 255
977
+ ? candidate
978
+ : 1;
775
979
  }
776
980
  return;
777
981
  }
@@ -785,7 +989,7 @@ async function main() {
785
989
  process.exitCode = 1;
786
990
  return;
787
991
  }
788
- // `init-agents` retrofits the agent-discovery files (.mcp.json, CLAUDE.md, …) into the cwd. It's a
992
+ // `init-agents` retrofits the agent files (.mcp.json, CLAUDE.md, AGENTS.md, …) into the cwd. It's a
789
993
  // pure file-writing command independent of the app loading, so dispatch it before the eager `loadApp`
790
994
  // (an existing app might be API-only or not yet built). It always succeeds unless a write throws.
791
995
  if (command === "init-agents") {
@@ -793,6 +997,7 @@ async function main() {
793
997
  await runInitAgents(process.cwd(), {
794
998
  json: argv.includes("--json"),
795
999
  force: argv.includes("--force"),
1000
+ syncMcp: argv.includes("--sync-mcp"),
796
1001
  });
797
1002
  return;
798
1003
  }
@@ -813,7 +1018,11 @@ async function main() {
813
1018
  }
814
1019
  try {
815
1020
  const { runSdk } = await import("./sdk.js");
816
- await runSdk(process.cwd(), { language, ...(out !== undefined ? { out } : {}) });
1021
+ await runSdk(process.cwd(), {
1022
+ language,
1023
+ ...(out !== undefined ? { out } : {}),
1024
+ ...(argv.includes("--strict") ? { strict: true } : {}),
1025
+ });
817
1026
  }
818
1027
  catch (err) {
819
1028
  console.error(formatCliError(err));
@@ -835,6 +1044,8 @@ async function main() {
835
1044
  list: argv.includes("--list"),
836
1045
  verify: !argv.includes("--no-verify"),
837
1046
  allowDowngrade: argv.includes("--allow-downgrade"),
1047
+ exact: argv.includes("--exact"),
1048
+ cliVersion: CLI_VERSION,
838
1049
  });
839
1050
  if (!ok)
840
1051
  process.exitCode = 1;
@@ -855,6 +1066,13 @@ async function main() {
855
1066
  }
856
1067
  const flags = parseFlags(argv.slice(1));
857
1068
  const app = await loadApp(process.cwd(), flags.out);
1069
+ if (command !== "start") {
1070
+ const { refreshRouteTypes, watchRouteTypes } = await import("./route-types.js");
1071
+ refreshRouteTypes(app.cwd);
1072
+ // A relaunched `nifra dev --bun` child runs beside its parent, which already watches.
1073
+ if (command === "dev" && process.env.NIFRA_BUN_DEV_TOKEN === undefined)
1074
+ watchRouteTypes(app.cwd);
1075
+ }
858
1076
  if (command === "dev")
859
1077
  await dev(app, flags);
860
1078
  else if (command === "build")
@@ -946,6 +1164,7 @@ function isCliCommandToken(value) {
946
1164
  value === "mcp" ||
947
1165
  value === "init-agents" ||
948
1166
  value === "upgrade" ||
1167
+ value === "db" ||
949
1168
  value === "help" ||
950
1169
  value === "--help" ||
951
1170
  value === "-h" ||