@nifrajs/cli 3.3.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (425) hide show
  1. package/README.md +42 -3
  2. package/dist/app-files.d.ts +11 -0
  3. package/dist/app-files.d.ts.map +1 -0
  4. package/dist/app-files.js +34 -0
  5. package/dist/app-files.js.map +1 -0
  6. package/dist/assure-hydration.d.ts +7 -1
  7. package/dist/assure-hydration.d.ts.map +1 -1
  8. package/dist/assure-hydration.js +128 -43
  9. package/dist/assure-hydration.js.map +1 -1
  10. package/dist/assure.d.ts.map +1 -1
  11. package/dist/assure.js +16 -2
  12. package/dist/assure.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts +5 -1
  14. package/dist/capabilities-tool.d.ts.map +1 -1
  15. package/dist/capabilities-tool.js +32 -10
  16. package/dist/capabilities-tool.js.map +1 -1
  17. package/dist/cdn-check.d.ts +41 -0
  18. package/dist/cdn-check.d.ts.map +1 -0
  19. package/dist/cdn-check.js +207 -0
  20. package/dist/cdn-check.js.map +1 -0
  21. package/dist/check-diagnostics.d.ts +7 -15
  22. package/dist/check-diagnostics.d.ts.map +1 -1
  23. package/dist/check-diagnostics.js +162 -713
  24. package/dist/check-diagnostics.js.map +1 -1
  25. package/dist/check-scan.d.ts +58 -7
  26. package/dist/check-scan.d.ts.map +1 -1
  27. package/dist/check-scan.js +323 -62
  28. package/dist/check-scan.js.map +1 -1
  29. package/dist/check.d.ts +1 -0
  30. package/dist/check.d.ts.map +1 -1
  31. package/dist/check.js +145 -54
  32. package/dist/check.js.map +1 -1
  33. package/dist/cli.d.ts +42 -16
  34. package/dist/cli.d.ts.map +1 -1
  35. package/dist/cli.js +306 -87
  36. package/dist/cli.js.map +1 -1
  37. package/dist/command-catalog.d.ts +60 -5
  38. package/dist/command-catalog.d.ts.map +1 -1
  39. package/dist/command-catalog.js +298 -33
  40. package/dist/command-catalog.js.map +1 -1
  41. package/dist/config-target.d.ts +8 -0
  42. package/dist/config-target.d.ts.map +1 -0
  43. package/dist/config-target.js +45 -0
  44. package/dist/config-target.js.map +1 -0
  45. package/dist/contracts.d.ts +3 -2
  46. package/dist/contracts.d.ts.map +1 -1
  47. package/dist/contracts.js +56 -16
  48. package/dist/contracts.js.map +1 -1
  49. package/dist/db-audit.d.ts +37 -0
  50. package/dist/db-audit.d.ts.map +1 -0
  51. package/dist/db-audit.js +99 -0
  52. package/dist/db-audit.js.map +1 -0
  53. package/dist/db-child.d.ts +64 -0
  54. package/dist/db-child.d.ts.map +1 -0
  55. package/dist/db-child.js +213 -0
  56. package/dist/db-child.js.map +1 -0
  57. package/dist/db-config.d.ts +60 -0
  58. package/dist/db-config.d.ts.map +1 -0
  59. package/dist/db-config.js +176 -0
  60. package/dist/db-config.js.map +1 -0
  61. package/dist/db-tool.d.ts +84 -0
  62. package/dist/db-tool.d.ts.map +1 -0
  63. package/dist/db-tool.js +537 -0
  64. package/dist/db-tool.js.map +1 -0
  65. package/dist/dev-bun-config.d.ts +20 -13
  66. package/dist/dev-bun-config.d.ts.map +1 -1
  67. package/dist/dev-bun-config.js +98 -41
  68. package/dist/dev-bun-config.js.map +1 -1
  69. package/dist/dev-feed-tool.d.ts +106 -0
  70. package/dist/dev-feed-tool.d.ts.map +1 -0
  71. package/dist/dev-feed-tool.js +713 -0
  72. package/dist/dev-feed-tool.js.map +1 -0
  73. package/dist/dev-server-client.d.ts +67 -0
  74. package/dist/dev-server-client.d.ts.map +1 -0
  75. package/dist/dev-server-client.js +195 -0
  76. package/dist/dev-server-client.js.map +1 -0
  77. package/dist/diagnostics.d.ts +86 -0
  78. package/dist/diagnostics.d.ts.map +1 -1
  79. package/dist/diagnostics.js +85 -0
  80. package/dist/diagnostics.js.map +1 -1
  81. package/dist/diff-tool.d.ts +3 -3
  82. package/dist/diff-tool.d.ts.map +1 -1
  83. package/dist/diff-tool.js +6 -5
  84. package/dist/diff-tool.js.map +1 -1
  85. package/dist/docs-search.d.ts.map +1 -1
  86. package/dist/docs-search.js +12 -4
  87. package/dist/docs-search.js.map +1 -1
  88. package/dist/doctor.d.ts +17 -0
  89. package/dist/doctor.d.ts.map +1 -1
  90. package/dist/doctor.js +76 -15
  91. package/dist/doctor.js.map +1 -1
  92. package/dist/env-file.d.ts +15 -0
  93. package/dist/env-file.d.ts.map +1 -1
  94. package/dist/env-file.js +73 -1
  95. package/dist/env-file.js.map +1 -1
  96. package/dist/fix-recipes.d.ts.map +1 -1
  97. package/dist/fix-recipes.js +41 -21
  98. package/dist/fix-recipes.js.map +1 -1
  99. package/dist/frontend-guidance.js +5 -5
  100. package/dist/frontend-guidance.js.map +1 -1
  101. package/dist/i18n-check.d.ts +16 -0
  102. package/dist/i18n-check.d.ts.map +1 -0
  103. package/dist/i18n-check.js +154 -0
  104. package/dist/i18n-check.js.map +1 -0
  105. package/dist/init-agents.d.ts +49 -13
  106. package/dist/init-agents.d.ts.map +1 -1
  107. package/dist/init-agents.js +239 -34
  108. package/dist/init-agents.js.map +1 -1
  109. package/dist/internal/code-unit-order.d.ts +4 -0
  110. package/dist/internal/code-unit-order.d.ts.map +1 -0
  111. package/dist/internal/code-unit-order.js +6 -0
  112. package/dist/internal/code-unit-order.js.map +1 -0
  113. package/dist/internal/script-kind.d.ts +7 -0
  114. package/dist/internal/script-kind.d.ts.map +1 -0
  115. package/dist/internal/script-kind.js +12 -0
  116. package/dist/internal/script-kind.js.map +1 -0
  117. package/dist/internal/source-facts.d.ts +5 -0
  118. package/dist/internal/source-facts.d.ts.map +1 -1
  119. package/dist/internal/source-facts.js +61 -5
  120. package/dist/internal/source-facts.js.map +1 -1
  121. package/dist/internal/typescript-import.d.ts +49 -24
  122. package/dist/internal/typescript-import.d.ts.map +1 -1
  123. package/dist/internal/typescript-import.js +507 -67
  124. package/dist/internal/typescript-import.js.map +1 -1
  125. package/dist/introspect.d.ts +6 -4
  126. package/dist/introspect.d.ts.map +1 -1
  127. package/dist/introspect.js +93 -74
  128. package/dist/introspect.js.map +1 -1
  129. package/dist/learn.js +3 -3
  130. package/dist/learn.js.map +1 -1
  131. package/dist/levels-tool.d.ts +10 -0
  132. package/dist/levels-tool.d.ts.map +1 -1
  133. package/dist/levels-tool.js +33 -3
  134. package/dist/levels-tool.js.map +1 -1
  135. package/dist/load.d.ts +71 -16
  136. package/dist/load.d.ts.map +1 -1
  137. package/dist/load.js +78 -18
  138. package/dist/load.js.map +1 -1
  139. package/dist/manifest-tool.d.ts.map +1 -1
  140. package/dist/manifest-tool.js +3 -2
  141. package/dist/manifest-tool.js.map +1 -1
  142. package/dist/mcp-app-tool.d.ts.map +1 -1
  143. package/dist/mcp-app-tool.js +2 -0
  144. package/dist/mcp-app-tool.js.map +1 -1
  145. package/dist/mcp-context.d.ts.map +1 -1
  146. package/dist/mcp-context.js +5 -6
  147. package/dist/mcp-context.js.map +1 -1
  148. package/dist/mcp-delegate.d.ts +57 -0
  149. package/dist/mcp-delegate.d.ts.map +1 -0
  150. package/dist/mcp-delegate.js +130 -0
  151. package/dist/mcp-delegate.js.map +1 -0
  152. package/dist/mcp-docs-tools.d.ts.map +1 -1
  153. package/dist/mcp-docs-tools.js +23 -5
  154. package/dist/mcp-docs-tools.js.map +1 -1
  155. package/dist/mcp-exec.d.ts +3 -0
  156. package/dist/mcp-exec.d.ts.map +1 -1
  157. package/dist/mcp-exec.js +263 -131
  158. package/dist/mcp-exec.js.map +1 -1
  159. package/dist/mcp-http.d.ts.map +1 -1
  160. package/dist/mcp-http.js +20 -5
  161. package/dist/mcp-http.js.map +1 -1
  162. package/dist/mcp-io.d.ts +26 -0
  163. package/dist/mcp-io.d.ts.map +1 -1
  164. package/dist/mcp-io.js +113 -0
  165. package/dist/mcp-io.js.map +1 -1
  166. package/dist/mcp-isolate.d.ts +48 -0
  167. package/dist/mcp-isolate.d.ts.map +1 -0
  168. package/dist/mcp-isolate.js +222 -0
  169. package/dist/mcp-isolate.js.map +1 -0
  170. package/dist/mcp-path.d.ts +10 -0
  171. package/dist/mcp-path.d.ts.map +1 -0
  172. package/dist/mcp-path.js +51 -0
  173. package/dist/mcp-path.js.map +1 -0
  174. package/dist/mcp-project-child.d.ts +74 -0
  175. package/dist/mcp-project-child.d.ts.map +1 -0
  176. package/dist/mcp-project-child.js +130 -0
  177. package/dist/mcp-project-child.js.map +1 -0
  178. package/dist/mcp-reflect.d.ts.map +1 -1
  179. package/dist/mcp-reflect.js +2 -2
  180. package/dist/mcp-reflect.js.map +1 -1
  181. package/dist/mcp-render.d.ts.map +1 -1
  182. package/dist/mcp-render.js +64 -45
  183. package/dist/mcp-render.js.map +1 -1
  184. package/dist/mcp-root.d.ts +37 -9
  185. package/dist/mcp-root.d.ts.map +1 -1
  186. package/dist/mcp-root.js +124 -25
  187. package/dist/mcp-root.js.map +1 -1
  188. package/dist/mcp-run.d.ts +18 -1
  189. package/dist/mcp-run.d.ts.map +1 -1
  190. package/dist/mcp-run.js +159 -55
  191. package/dist/mcp-run.js.map +1 -1
  192. package/dist/mcp-ws.js +4 -1
  193. package/dist/mcp-ws.js.map +1 -1
  194. package/dist/mcp.d.ts +18 -5
  195. package/dist/mcp.d.ts.map +1 -1
  196. package/dist/mcp.js +165 -82
  197. package/dist/mcp.js.map +1 -1
  198. package/dist/migrate-layout.d.ts +44 -0
  199. package/dist/migrate-layout.d.ts.map +1 -0
  200. package/dist/migrate-layout.js +982 -0
  201. package/dist/migrate-layout.js.map +1 -0
  202. package/dist/openapi-tool.d.ts +2 -2
  203. package/dist/openapi-tool.d.ts.map +1 -1
  204. package/dist/openapi-tool.js.map +1 -1
  205. package/dist/openapi-types.d.ts +2 -1
  206. package/dist/openapi-types.d.ts.map +1 -1
  207. package/dist/openapi-types.js +240 -12
  208. package/dist/openapi-types.js.map +1 -1
  209. package/dist/pipeline-report.d.ts +3 -2
  210. package/dist/pipeline-report.d.ts.map +1 -1
  211. package/dist/pipeline-report.js +19 -16
  212. package/dist/pipeline-report.js.map +1 -1
  213. package/dist/port.d.ts +5 -4
  214. package/dist/port.d.ts.map +1 -1
  215. package/dist/port.js +46 -29
  216. package/dist/port.js.map +1 -1
  217. package/dist/project-facts.d.ts +28 -4
  218. package/dist/project-facts.d.ts.map +1 -1
  219. package/dist/project-facts.js +12 -2
  220. package/dist/project-facts.js.map +1 -1
  221. package/dist/recipes/4.0.0.d.ts +9 -0
  222. package/dist/recipes/4.0.0.d.ts.map +1 -0
  223. package/dist/recipes/4.0.0.js +34 -0
  224. package/dist/recipes/4.0.0.js.map +1 -0
  225. package/dist/recipes/index.d.ts.map +1 -1
  226. package/dist/recipes/index.js +2 -0
  227. package/dist/recipes/index.js.map +1 -1
  228. package/dist/release-verification.d.ts +11 -1
  229. package/dist/release-verification.d.ts.map +1 -1
  230. package/dist/release-verification.js +47 -4
  231. package/dist/release-verification.js.map +1 -1
  232. package/dist/review.d.ts +21 -0
  233. package/dist/review.d.ts.map +1 -0
  234. package/dist/review.js +823 -0
  235. package/dist/review.js.map +1 -0
  236. package/dist/route-types.d.ts +28 -0
  237. package/dist/route-types.d.ts.map +1 -0
  238. package/dist/route-types.js +106 -0
  239. package/dist/route-types.js.map +1 -0
  240. package/dist/rules/codes.d.ts +9 -0
  241. package/dist/rules/codes.d.ts.map +1 -1
  242. package/dist/rules/codes.js +9 -0
  243. package/dist/rules/codes.js.map +1 -1
  244. package/dist/rules/data-guard.d.ts +12 -0
  245. package/dist/rules/data-guard.d.ts.map +1 -0
  246. package/dist/rules/data-guard.js +213 -0
  247. package/dist/rules/data-guard.js.map +1 -0
  248. package/dist/rules/index.d.ts +3 -0
  249. package/dist/rules/index.d.ts.map +1 -1
  250. package/dist/rules/index.js +17 -5
  251. package/dist/rules/index.js.map +1 -1
  252. package/dist/rules/islands.d.ts.map +1 -1
  253. package/dist/rules/islands.js +7 -4
  254. package/dist/rules/islands.js.map +1 -1
  255. package/dist/rules/legacy.d.ts +3 -1
  256. package/dist/rules/legacy.d.ts.map +1 -1
  257. package/dist/rules/legacy.js +829 -34
  258. package/dist/rules/legacy.js.map +1 -1
  259. package/dist/rules/nano.d.ts.map +1 -1
  260. package/dist/rules/nano.js +15 -6
  261. package/dist/rules/nano.js.map +1 -1
  262. package/dist/rules/pages.d.ts +15 -0
  263. package/dist/rules/pages.d.ts.map +1 -0
  264. package/dist/rules/pages.js +88 -0
  265. package/dist/rules/pages.js.map +1 -0
  266. package/dist/rules/routes.d.ts +11 -0
  267. package/dist/rules/routes.d.ts.map +1 -1
  268. package/dist/rules/routes.js +180 -2
  269. package/dist/rules/routes.js.map +1 -1
  270. package/dist/rules/secrets.d.ts +10 -0
  271. package/dist/rules/secrets.d.ts.map +1 -0
  272. package/dist/rules/secrets.js +169 -0
  273. package/dist/rules/secrets.js.map +1 -0
  274. package/dist/rules/security.d.ts +7 -0
  275. package/dist/rules/security.d.ts.map +1 -1
  276. package/dist/rules/security.js +171 -65
  277. package/dist/rules/security.js.map +1 -1
  278. package/dist/rules/typescript.d.ts +16 -0
  279. package/dist/rules/typescript.d.ts.map +1 -0
  280. package/dist/rules/typescript.js +59 -0
  281. package/dist/rules/typescript.js.map +1 -0
  282. package/dist/rules/zones.d.ts +17 -0
  283. package/dist/rules/zones.d.ts.map +1 -0
  284. package/dist/rules/zones.js +174 -0
  285. package/dist/rules/zones.js.map +1 -0
  286. package/dist/scaffold.d.ts +25 -12
  287. package/dist/scaffold.d.ts.map +1 -1
  288. package/dist/scaffold.js +154 -90
  289. package/dist/scaffold.js.map +1 -1
  290. package/dist/sdk.d.ts +17 -3
  291. package/dist/sdk.d.ts.map +1 -1
  292. package/dist/sdk.js +941 -153
  293. package/dist/sdk.js.map +1 -1
  294. package/dist/search-terms.d.ts +2 -0
  295. package/dist/search-terms.d.ts.map +1 -1
  296. package/dist/search-terms.js +23 -8
  297. package/dist/search-terms.js.map +1 -1
  298. package/dist/smoke.d.ts +79 -0
  299. package/dist/smoke.d.ts.map +1 -0
  300. package/dist/smoke.js +494 -0
  301. package/dist/smoke.js.map +1 -0
  302. package/dist/sync-manifest.d.ts.map +1 -1
  303. package/dist/sync-manifest.js +2 -1
  304. package/dist/sync-manifest.js.map +1 -1
  305. package/dist/sync-routes.d.ts +4 -3
  306. package/dist/sync-routes.d.ts.map +1 -1
  307. package/dist/sync-routes.js +4 -3
  308. package/dist/sync-routes.js.map +1 -1
  309. package/dist/test-tool.d.ts.map +1 -1
  310. package/dist/test-tool.js +30 -11
  311. package/dist/test-tool.js.map +1 -1
  312. package/dist/types-search.d.ts.map +1 -1
  313. package/dist/types-search.js +11 -5
  314. package/dist/types-search.js.map +1 -1
  315. package/dist/upgrade.d.ts +36 -6
  316. package/dist/upgrade.d.ts.map +1 -1
  317. package/dist/upgrade.js +167 -29
  318. package/dist/upgrade.js.map +1 -1
  319. package/dist/verification-plan.d.ts.map +1 -1
  320. package/dist/verification-plan.js +8 -0
  321. package/dist/verification-plan.js.map +1 -1
  322. package/dist/verification.d.ts.map +1 -1
  323. package/dist/verification.js +45 -12
  324. package/dist/verification.js.map +1 -1
  325. package/dist/web-app-options.d.ts +19 -0
  326. package/dist/web-app-options.d.ts.map +1 -0
  327. package/dist/web-app-options.js +49 -0
  328. package/dist/web-app-options.js.map +1 -0
  329. package/dist/work-graph.d.ts +3 -0
  330. package/dist/work-graph.d.ts.map +1 -1
  331. package/dist/work-graph.js +33 -16
  332. package/dist/work-graph.js.map +1 -1
  333. package/docs/examples.json +185 -29
  334. package/docs/llms-full.txt +4123 -548
  335. package/docs/types.json +14391 -10060
  336. package/package.json +19 -11
  337. package/src/app-files.ts +40 -0
  338. package/src/assure-hydration.ts +139 -46
  339. package/src/assure.ts +17 -2
  340. package/src/capabilities-tool.ts +35 -9
  341. package/src/cdn-check.ts +268 -0
  342. package/src/check-diagnostics.ts +199 -797
  343. package/src/check-scan.ts +394 -63
  344. package/src/check.ts +146 -58
  345. package/src/cli.ts +347 -92
  346. package/src/command-catalog.ts +407 -45
  347. package/src/config-target.ts +47 -0
  348. package/src/contracts.ts +66 -15
  349. package/src/db-audit.ts +128 -0
  350. package/src/db-child.ts +285 -0
  351. package/src/db-config.ts +224 -0
  352. package/src/db-tool.ts +690 -0
  353. package/src/dev-bun-config.ts +116 -40
  354. package/src/dev-feed-tool.ts +912 -0
  355. package/src/dev-server-client.ts +267 -0
  356. package/src/diagnostics.ts +179 -0
  357. package/src/diff-tool.ts +6 -5
  358. package/src/docs-search.ts +10 -3
  359. package/src/doctor.ts +98 -15
  360. package/src/env-file.ts +78 -1
  361. package/src/fix-recipes.ts +44 -31
  362. package/src/frontend-guidance.ts +5 -5
  363. package/src/i18n-check.ts +179 -0
  364. package/src/init-agents.ts +301 -38
  365. package/src/internal/code-unit-order.ts +5 -0
  366. package/src/internal/script-kind.ts +11 -0
  367. package/src/internal/source-facts.ts +96 -22
  368. package/src/internal/typescript-import.ts +663 -68
  369. package/src/introspect.ts +93 -82
  370. package/src/learn.ts +3 -3
  371. package/src/levels-tool.ts +47 -2
  372. package/src/load.ts +144 -32
  373. package/src/manifest-tool.ts +3 -2
  374. package/src/mcp-app-tool.ts +2 -0
  375. package/src/mcp-context.ts +5 -7
  376. package/src/mcp-delegate.ts +162 -0
  377. package/src/mcp-docs-tools.ts +23 -5
  378. package/src/mcp-exec.ts +301 -161
  379. package/src/mcp-http.ts +22 -5
  380. package/src/mcp-io.ts +130 -0
  381. package/src/mcp-isolate.ts +287 -0
  382. package/src/mcp-path.ts +50 -0
  383. package/src/mcp-project-child.ts +182 -0
  384. package/src/mcp-reflect.ts +3 -2
  385. package/src/mcp-render.ts +72 -42
  386. package/src/mcp-root.ts +130 -24
  387. package/src/mcp-run.ts +190 -56
  388. package/src/mcp-ws.ts +4 -1
  389. package/src/mcp.ts +193 -91
  390. package/src/migrate-layout.ts +1121 -0
  391. package/src/openapi-tool.ts +7 -3
  392. package/src/openapi-types.ts +315 -13
  393. package/src/pipeline-report.ts +22 -18
  394. package/src/port.ts +53 -30
  395. package/src/project-facts.ts +42 -8
  396. package/src/recipes/4.0.0.ts +35 -0
  397. package/src/recipes/index.ts +2 -0
  398. package/src/release-verification.ts +74 -5
  399. package/src/review.ts +988 -0
  400. package/src/route-types.ts +133 -0
  401. package/src/rules/codes.ts +9 -0
  402. package/src/rules/data-guard.ts +230 -0
  403. package/src/rules/index.ts +18 -5
  404. package/src/rules/islands.ts +12 -4
  405. package/src/rules/legacy.ts +947 -35
  406. package/src/rules/nano.ts +18 -6
  407. package/src/rules/pages.ts +99 -0
  408. package/src/rules/routes.ts +181 -2
  409. package/src/rules/secrets.ts +174 -0
  410. package/src/rules/security.ts +198 -70
  411. package/src/rules/typescript.ts +77 -0
  412. package/src/rules/zones.ts +195 -0
  413. package/src/scaffold.ts +175 -98
  414. package/src/sdk.ts +1215 -158
  415. package/src/search-terms.ts +24 -7
  416. package/src/smoke.ts +607 -0
  417. package/src/sync-manifest.ts +2 -1
  418. package/src/sync-routes.ts +4 -3
  419. package/src/test-tool.ts +32 -11
  420. package/src/types-search.ts +11 -4
  421. package/src/upgrade.ts +205 -25
  422. package/src/verification-plan.ts +27 -0
  423. package/src/verification.ts +50 -12
  424. package/src/web-app-options.ts +57 -0
  425. package/src/work-graph.ts +40 -16
package/src/cli.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * `nifra` - the zero-config CLI for a nifra app. Reads `framework.ts` + `backend.ts` + `routes/` from
4
- * the project root (see {@link loadApp}) and wires the right `@nifrajs/web` entrypoint:
3
+ * `nifra` - the zero-config CLI for a nifra app. Reads `backend/framework.ts` + `backend/app.ts` +
4
+ * `routes/` from the project root (see {@link loadApp}) and wires the right `@nifrajs/web`
5
+ * entrypoint:
5
6
  *
6
7
  * nifra dev true-HMR dev server (Bun native HMR + nifra SSR) - @nifrajs/web/dev
7
8
  * nifra build emit a complete target-specific deploy directory - @nifrajs/web/build
@@ -12,26 +13,34 @@
12
13
  import { existsSync, readFileSync, realpathSync } from "node:fs"
13
14
  import { resolve } from "node:path"
14
15
  import { fileURLToPath } from "node:url"
15
- import { inProcessClient } from "@nifrajs/client"
16
16
  import {
17
- type CreateWebAppOptions,
17
+ type CssLoadingMode,
18
18
  createWebApp,
19
19
  DEFAULT_DEV_PORT,
20
20
  type RenderAdapter,
21
21
  } from "@nifrajs/web"
22
22
  import { discoverRoutes } from "@nifrajs/web/fs"
23
+ import { formatShadowedPages, shadowedPages } from "@nifrajs/web/route-manifest"
23
24
  import type { BunPlugin } from "bun"
25
+ import { adapterFile, BACKEND_APP_FILE } from "./app-files.ts"
24
26
  import { bindCommandArgv, findCommandSpec, renderCommandCatalogHelp } from "./command-catalog.ts"
25
- import { applyEnvFiles, takeEnvFileFlags } from "./env-file.ts"
26
- import { type LoadedApp, loadApp, type NifraFramework } from "./load.ts"
27
+ import { applyEnvFiles, forgetAutoLoadedEnv, takeEnvFileFlags } from "./env-file.ts"
28
+ import { FRAMEWORK_WEB_OPTIONS, type LoadedApp, loadApp, type NifraFramework } from "./load.ts"
27
29
  import { chooseBuildPipeline, describePipeline } from "./pipeline-guard.ts"
30
+ import {
31
+ frameworkMountPaths,
32
+ frameworkOptionImports,
33
+ frameworkWebAppOptions,
34
+ } from "./web-app-options.ts"
28
35
 
29
36
  export interface Flags {
30
37
  readonly port: number
31
- readonly out: string
38
+ /** `--out <dir>`; without it `dist`, or `.vercel/output` for a Vercel build. */
39
+ readonly out: string | undefined
32
40
  readonly poll: boolean
33
- /** `nifra build --target <t>`: emit a full deploy dir for this target. Defaults to `bun`. */
34
- readonly target: string
41
+ /** `nifra build --target <t>`: emit a full deploy dir for this target. Without it, the `target` that
42
+ * `nifra.config.ts` exports, else `bun`. */
43
+ readonly target: string | undefined
35
44
  /** `nifra build --report`: print a per-chunk size + gzip table after the build. */
36
45
  readonly report: boolean
37
46
  /** `nifra build --vite`: force the client + server through Vite/Rollup. Without a flag the pipeline is
@@ -46,18 +55,22 @@ export interface Flags {
46
55
  * a hard failure to a loud warning so a duplicate coming from a linked sibling repo doesn't take dev
47
56
  * down while you fix the resolution. Dev only - `nifra build` always fails hard on a duplicate. */
48
57
  readonly allowDuplicateIdentity: boolean
58
+ /** `nifra dev --no-indicator`: no in-page issues badge; browser errors still reach the dev feed. */
59
+ readonly noIndicator: boolean
49
60
  }
50
61
 
51
62
  /** Forward a re-exec'd Bun child's output without relying on Windows inheriting a pipe-of-a-pipe. */
52
63
  async function forwardChildOutput(
53
64
  stream: ReadableStream<Uint8Array>,
54
65
  sink: { write(chunk: Uint8Array): unknown },
66
+ observe?: (chunk: Uint8Array) => void,
55
67
  ): Promise<void> {
56
68
  const reader = stream.getReader()
57
69
  try {
58
70
  for (;;) {
59
71
  const { done, value } = await reader.read()
60
72
  if (done) return
73
+ observe?.(value)
61
74
  await sink.write(value)
62
75
  }
63
76
  } finally {
@@ -65,6 +78,31 @@ async function forwardChildOutput(
65
78
  }
66
79
  }
67
80
 
81
+ /** The last `max` bytes of a stream, decoded: what a crashed child printed last. */
82
+ function streamTail(max: number): { push(chunk: Uint8Array): void; text(): string } {
83
+ let chunks: Uint8Array[] = []
84
+ let size = 0
85
+ return {
86
+ push(chunk) {
87
+ chunks.push(chunk)
88
+ size += chunk.length
89
+ while (size - (chunks[0]?.length ?? 0) >= max && chunks.length > 1) {
90
+ size -= chunks.shift()?.length ?? 0
91
+ }
92
+ },
93
+ text() {
94
+ const all = new Uint8Array(size)
95
+ let offset = 0
96
+ for (const chunk of chunks) {
97
+ all.set(chunk, offset)
98
+ offset += chunk.length
99
+ }
100
+ chunks = [all]
101
+ return new TextDecoder().decode(all.subarray(Math.max(0, size - max)))
102
+ },
103
+ }
104
+ }
105
+
68
106
  const HELP = `nifra - zero-config dev/build/start for a nifra app
69
107
 
70
108
  Usage:
@@ -80,14 +118,33 @@ Usage:
80
118
  loud warning instead of a hard failure, so a duplicate React/
81
119
  adapter copy from a linked sibling repo doesn't take dev down
82
120
  while you fix the resolution. Dev only - \`nifra build\` still fails.
121
+ [--no-indicator] No in-page issues badge. Browser errors still reach
122
+ \`nifra errors\` and the agent tools.
123
+ nifra errors [--since <n>] [--category <c>] [--request <id>] What the running dev server caught:
124
+ SSR/loader/API/build/browser/hydration errors, each a structured
125
+ diagnostic. Finds the server itself; reads the log a crashed
126
+ one left. Exits 1 while current-code errors are open.
127
+ nifra logs [--since <n>] [--level <l>] [--grep <s>] The dev server's console, server and
128
+ browser, each line tagged with its request.
129
+ nifra cdn-check <url> Check a deployed page behind a CDN: served from cache, no
130
+ internal or CDN-only headers reaching the visitor, and soft
131
+ navigations get page data, not the cached document. Exits 1
132
+ on a failure.
133
+ nifra db schema [<table>] The development database declared as \`devDatabase\` in
134
+ nifra.config.ts: tables, columns, keys, indexes.
135
+ nifra db query "<sql>" [--explain [--analyze]] One read-only SELECT (or its plan), in a fresh
136
+ process killed at devDatabase.timeoutMs. Rows capped, secrets
137
+ masked. Postgres: a superuser connection is refused.
138
+ nifra db role Print the SQL for a read-only Postgres role. Runs nothing.
139
+ nifra db audit [--limit <n>] What nifra db ran (.nifra/db-audit.jsonl), never the rows.
83
140
  nifra build [--out <dir>] [--report] Emit a complete deploy directory.
84
141
  [--target <t>] Target a FULL deploy dir for <t>:
85
- bun | node | deno | cf-pages | vercel | static. Packages
142
+ bun | node | deno | cloudflare | vercel | static. Packages
86
143
  buildClient + buildServer (+ prerender for static) so an app
87
144
  no longer hand-writes build-<target>.ts + _worker.ts +
88
145
  _routes.json. The server entry is generated from your
89
- framework.ts (adapter) + backend.ts + routes/. --report prints
90
- a per-chunk size + gzip table (biggest first).
146
+ backend/framework.ts (adapter) + backend/app.ts + routes/.
147
+ --report prints a per-chunk size + gzip table (biggest first).
91
148
  [--vite | --bun] Force the bundler. Without a flag it follows your config: Bun
92
149
  (faster, Bun-native) unless your ONLY transforms are
93
150
  \`vitePlugins\`, which the Bun build cannot run - then Vite,
@@ -108,12 +165,20 @@ Usage:
108
165
  against it: a route the target can't honour (an ssr route on a
109
166
  static build, ISR where there's no revalidation) exits nonzero
110
167
  with the consequence - run in CI so it fails the build, not prod.
111
- nifra init-agents [--force] [--json] Retrofit an EXISTING app with the agent-discovery files a new
112
- app ships: .mcp.json + .cursor/mcp.json (register this project's
113
- nifra MCP), CLAUDE.md (MCP-first preamble + @AGENTS.md import),
114
- and a "## MCP server" section in AGENTS.md. No-clobber by
115
- default (skips a file you've customized); --force overwrites the
116
- owned files. AGENTS.md is only appended to, never overwritten.
168
+ nifra init-agents [--force] [--json] Retrofit an EXISTING app with the agent files a new app ships:
169
+ .mcp.json + .cursor/mcp.json (register this project's nifra MCP),
170
+ each agent's pointer to AGENTS.md (CLAUDE.md, GEMINI.md,
171
+ .cursor/rules/nifra.mdc, .github/copilot-instructions.md), and
172
+ AGENTS.md's "## MCP server" and (with routes/) "## Project
173
+ structure" sections. No-clobber by default (skips a file you've
174
+ customized); --force overwrites the owned files. AGENTS.md is
175
+ only appended to, never overwritten.
176
+ nifra init-agents --sync-mcp [--json] Re-pin an existing app's MCP launch to the nifra the project
177
+ installs: rewrites only the @nifrajs/cli@x.y.z version in
178
+ .mcp.json, .cursor/mcp.json, CLAUDE.md and AGENTS.md's MCP
179
+ section - every other byte stays as it is. Creates nothing.
180
+ At a workspace root with one nifra member, the two registries'
181
+ launch also names that member (\`mcp app\`).
117
182
  nifra mcp [dir] Start an MCP server (stdio) exposing this project to a coding
118
183
  agent. The project root is [dir] when given, else resolved from
119
184
  cwd (marker walk-up + the client's MCP roots); tools refuse
@@ -122,15 +187,20 @@ Usage:
122
187
  nifra_run (backend), nifra_render (SSR a page), nifra_docs,
123
188
  nifra_example (verified snippets), nifra_scaffold (route→file),
124
189
  nifra_check (drift gate + fixes), nifra_levels (verification
125
- ladder), nifra_doctor (deps), nifra_explain (structured errors),
126
- nifra_inspect (request traces), nifra_learn (guided build path).
190
+ ladder), nifra_doctor (deps), nifra_errors + nifra_logs (what
191
+ the running dev server saw), nifra_db_schema + nifra_db_query +
192
+ nifra_db_role (the declared dev database, read-only),
193
+ nifra_explain (structured errors), nifra_inspect (request
194
+ traces), nifra_learn (guided build path).
127
195
  nifra docs-mcp [--port <n>] Serve the PUBLIC docs MCP over HTTP (nifra_docs + nifra_example) -
128
196
  self-host on a VPS so any remote agent can learn nifra. Default :8787.
129
197
  nifra learn [<step>] Print the guided build-an-app path (the human view of nifra_learn):
130
198
  no arg for the step index, a number for one step's goal/do/verify.
131
- nifra check [--json] [--lints-only] Gate: typecheck + lints (hand-rolled fetch(), untyped client("…"),
132
- server-only imports in routes/). Run as "done"; --json for agents;
133
- --lints-only skips tsc for a near-instant inner-loop pass.
199
+ nifra check [--json] [--sarif] [--lints-only]
200
+ Gate: typecheck + lints (hand-rolled fetch(), untyped client("…"),
201
+ backend code in browser code). Run as "done"; --json for agents;
202
+ --sarif emits the same stable diagnostics as SARIF 2.1.0 for external
203
+ review tools; --lints-only skips tsc for a near-instant inner-loop pass.
134
204
  nifra verify [--release] [--json] Run the shared repository verification gate. --release runs the
135
205
  full build, test, coverage, corpus, consumer, and cross-runtime gate.
136
206
  nifra fix [--code <NF-code>] Apply a registered diagnostic recipe, then print the remaining
@@ -148,8 +218,8 @@ Usage:
148
218
  request field or a removed response field breaks; widening a
149
219
  request enum or adding a response field doesn't) and fails
150
220
  closed. Exits non-zero on any breaking change - run it in CI.
151
- nifra sdk --lang <python|go> [--out <file>]
152
- Generate a deterministic non-TypeScript SDK from backend.ts.
221
+ nifra sdk --lang <python|go> [--out <file>] [--strict]
222
+ Generate a deterministic non-TypeScript SDK from backend/app.ts.
153
223
  nifra assure [--config <file>] [--json] Route-assurance report. Human table by default; --json emits
154
224
  the {ok, routes, findings} report for agents.
155
225
  nifra assure --bundle [--json] [--strict] [--out <file>] [--hydration] [--interact]
@@ -187,22 +257,40 @@ Usage:
187
257
  print production readiness, with --strict making absent
188
258
  applicable guarantees fail. --auto-fix writes safe local-
189
259
  version dependency fixes.
190
- nifra upgrade <version> Run the per-release upgrade recipe for <version>: sweep every
191
- [--write] [--no-verify] matching dependency pin (preserving ^/~/exact), move removed
192
- [--list] [--json] packages, apply exact imports, then verify with nifra check.
260
+ nifra upgrade <version> Upgrade to <version>, running every release recipe between the
261
+ [--write] [--no-verify] installed version and it, oldest first: move removed packages,
262
+ [--list] [--json] apply exact imports, pin every matching dependency (keeping
263
+ [--exact] ^/~, or exact with --exact), then verify with nifra check.
193
264
  [--allow-downgrade] Dry-run by default; --write applies then verifies (--no-verify
194
- to skip). --list shows available targets. Fail-closed on an
195
- unknown version or a rollback (--allow-downgrade overrides).
196
- Deterministic + idempotent.
265
+ to skip). --list shows available targets. A version newer than
266
+ this CLI prints the command for that release's CLI. Fail-closed
267
+ on an unknown version or a rollback (--allow-downgrade
268
+ overrides). Deterministic + idempotent.
269
+ nifra migrate layout [--write] [--json]
270
+ Move an app onto the frontend/backend split: split each route
271
+ into x.tsx + x.backend.ts, fold _middleware.ts into
272
+ _layout.backend.ts, move backend.ts/framework.ts under backend/,
273
+ and zone the other modules into frontend/, backend/ or shared/.
274
+ Dry-run by default; --write applies.
275
+ nifra types [--check] [--json] Generate each route's ./+types module under .nifra/types: its
276
+ params, the data its output schemas let through, and api typed
277
+ by backend/app.ts. dev, build and check refresh them too;
278
+ --check fails when one is stale.
197
279
  nifra port [--target <t>] [--json] Portability linter: print a feature × deploy-target capability
198
280
  [--ci] [--strict] matrix (in-memory stores, in-process cron/WebSocket, Bun/Deno
199
281
  globals, node: builtins) with file:line evidence. --target auto-
200
282
  detected from build/deploy scripts, wrangler.toml, or vercel config;
201
283
  --ci (or any --target) exits nonzero when a used feature is
202
284
  unsupported on the target; --strict also fails on caveats.
285
+ nifra i18n check [entry] Check i18n catalogs: coverage, missing + unused keys, ICU syntax,
286
+ [--json] [--strict] placeholder + tag parity, plural cases, script purity,
287
+ untranslated messages. IMPORTS the entry (default: the first
288
+ i18n.ts in ., lib/, src/, src/lib/, app/), which exports
289
+ \`locales\` + \`catalogs\` (+ optional \`ignore\`). Exits 1 on
290
+ errors; --strict also on warnings.
203
291
 
204
- Reads nifra.config.ts (adapter + clientModule + plugins; or framework.ts), backend.ts (optional), and
205
- routes/ from the current directory. Run from your project root.
292
+ Reads nifra.config.ts (adapter + clientModule + plugins; or backend/framework.ts), backend/app.ts
293
+ (optional), and routes/ from the current directory. Run from your project root.
206
294
 
207
295
  Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with \`--port <n>\` (alias \`-p\`) or the
208
296
  \`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).
@@ -216,15 +304,11 @@ set in the process environment is never overwritten by a file.
216
304
  ${renderCommandCatalogHelp()}`
217
305
 
218
306
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
219
- const CLI_VERSION = "3.3.0"
307
+ const CLI_VERSION = "4.0.0"
220
308
 
221
309
  // A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
222
310
  const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
223
311
  const asBunPlugins = (v: readonly unknown[]): BunPlugin[] => v as BunPlugin[]
224
- const asUse = (v: (app: never) => void): NonNullable<CreateWebAppOptions["use"]> =>
225
- v as NonNullable<CreateWebAppOptions["use"]>
226
- const apiOf = (backend: unknown): { api?: unknown } =>
227
- backend === undefined ? {} : { api: inProcessClient(backend as never) }
228
312
 
229
313
  /**
230
314
  * Render an error for the CLI, unwrapping the detail a bare `.message` drops.
@@ -281,7 +365,7 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
281
365
  )
282
366
  if (decision.pipeline === "bun") {
283
367
  // Bun's dev-server bundler takes plugins only via bunfig `[serve.static]`, read at process
284
- // start - so the boundary plugins (server-fn stubs, server-only emptying) are delivered by
368
+ // start - so the boundary plugins (zone guard, server-fn stubs) are delivered by
285
369
  // generating a config and re-execing this same command once with `--config=`. The child proves
286
370
  // it IS the configured child with a per-launch random token (matched against the file the
287
371
  // parent just wrote, consumed on first read). A fixed sentinel here would be a secret-leak
@@ -299,7 +383,11 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
299
383
  "launches; refusing to serve without the client-boundary plugins.",
300
384
  )
301
385
  }
302
- const { bunfigPath, launchToken } = await writeBunDevConfig(app.cwd, app.configPath)
386
+ const { bunfigPath, launchToken } = await writeBunDevConfig(
387
+ app.cwd,
388
+ app.configPath,
389
+ app.framework.define,
390
+ )
303
391
  // Bun 1.4 on Windows may discard the script-relative portion of argv when `--config` is
304
392
  // present. Keep a second, authenticated copy of the user vector in the child's environment so
305
393
  // the configured process cannot silently fall back to `nifra`'s help and exit 0. The launch token
@@ -350,16 +438,30 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
350
438
  },
351
439
  },
352
440
  )
441
+ // The child's last words, kept so a crash leaves a record `nifra errors` can read after the fact.
442
+ const stderrTail = streamTail(16 * 1024)
353
443
  const forwarded = Promise.all([
354
444
  forwardChildOutput(child.stdout as ReadableStream<Uint8Array>, Bun.stdout),
355
- forwardChildOutput(child.stderr as ReadableStream<Uint8Array>, Bun.stderr),
445
+ forwardChildOutput(child.stderr as ReadableStream<Uint8Array>, Bun.stderr, stderrTail.push),
356
446
  ])
357
- const forward = (): void => child.kill("SIGINT")
447
+ let interrupted = false
448
+ const forward = (): void => {
449
+ interrupted = true
450
+ child.kill("SIGINT")
451
+ }
358
452
  process.on("SIGINT", forward)
359
453
  process.on("SIGTERM", forward)
360
454
  try {
361
455
  const [code] = await Promise.all([child.exited, forwarded])
362
456
  process.exitCode = code
457
+ if (code !== 0 && !interrupted) {
458
+ try {
459
+ const { recordDevCrash } = await import("@nifrajs/web/dev-feed")
460
+ recordDevCrash(app.cwd, code, stderrTail.text())
461
+ } catch {
462
+ // The crash is already on the terminal; failing to persist it must not mask the exit code.
463
+ }
464
+ }
363
465
  } finally {
364
466
  process.off("SIGINT", forward)
365
467
  process.off("SIGTERM", forward)
@@ -408,13 +510,14 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
408
510
  ...(fw.publicDir !== undefined ? { publicDir: fw.publicDir } : {}),
409
511
  ...(fw.conditions ? { conditions: fw.conditions } : {}),
410
512
  ...(fw.define ? { define: fw.define } : {}),
411
- createApp: (clientEntry, importQuery) =>
513
+ indicator: showsIndicator(flags, fw),
514
+ createApp: (clientEntry, importQuery, dev) =>
412
515
  createWebApp({
413
516
  adapter: asAdapter(fw.adapter),
414
517
  manifest: discoverRoutes(routesDir, { importQuery }),
415
518
  clientEntry,
416
- ...(fw.use ? { use: asUse(fw.use) } : {}),
417
- ...apiOf(backend),
519
+ ...frameworkWebAppOptions(fw, backend),
520
+ onLoaderError: dev.onLoaderError,
418
521
  }),
419
522
  })
420
523
  console.log(`nifra dev (bun) → http://localhost:${server.port}`)
@@ -464,18 +567,19 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
464
567
  poll: flags.poll,
465
568
  port: flags.port,
466
569
  ...(flags.allowDuplicateIdentity ? { allowDuplicateIdentity: true } : {}),
570
+ indicator: showsIndicator(flags, fw),
467
571
  ...(fw.conditions ? { conditions: fw.conditions } : {}),
468
572
  ...(fw.define ? { define: fw.define } : {}),
469
573
  // `load` resolves route modules through VITE, not through Bun. That is what makes the Vite
470
574
  // pipeline own the whole phase: the client and the server now agree on every specifier, so
471
575
  // `resolve.dedupe` finally governs SSR and the dual-React crash cannot occur.
472
- createApp: (clientEntry, load) =>
576
+ createApp: (clientEntry, load, dev) =>
473
577
  createWebApp({
474
578
  adapter: asAdapter(fw.adapter),
475
579
  manifest: discoverRoutes(routesDir, { load }),
476
580
  clientEntry,
477
- ...(fw.use ? { use: asUse(fw.use) } : {}),
478
- ...apiOf(backend),
581
+ ...frameworkWebAppOptions(fw, backend),
582
+ onLoaderError: dev.onLoaderError,
479
583
  }),
480
584
  })
481
585
  console.log(`nifra dev (vite) → http://localhost:${server.port}`)
@@ -485,23 +589,26 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
485
589
  /**
486
590
  * `nifra build --target <t>` - package the engine (buildClient + buildServer + prerender) into one
487
591
  * command that emits a full deploy dir, so an app no longer hand-writes build-bun.ts + _worker.ts +
488
- * _routes.json per target. The adapter is imported from `framework.ts` (the edge-bundlable file - never
489
- * `nifra.config.ts`, which pulls in Vite plugins), and the backend from `backend.ts` when present;
490
- * `buildTarget` generates the per-target server entry from those + the app's `routes/`.
592
+ * _routes.json per target. The adapter is imported from `backend/framework.ts` (the edge-bundlable
593
+ * file - never `nifra.config.ts`, which pulls in Vite plugins), and the backend from
594
+ * `backend/app.ts` when present; `buildTarget` generates the per-target server entry from those +
595
+ * the app's `routes/`.
491
596
  */
492
597
  /**
493
598
  * Refuse a `use` the generated server entry cannot import.
494
599
  *
495
- * `nifra build` emits `import { use } from <frameworkFile>` (framework.ts, the edge-bundlable file),
496
- * but `loadApp` prefers `nifra.config.ts` - so an app with BOTH files whose `use` lives only in
497
- * `nifra.config.ts` makes `fw.use` defined while `framework.ts` exports nothing of that name, and the
498
- * build dies later with an opaque bundler error pointing at generated code. Refuse it here with the
499
- * exact move instead, in the spirit of `assertPipelineSeparation` (load.ts).
600
+ * `nifra build` emits `import { use } from <frameworkFile>` (backend/framework.ts, the
601
+ * edge-bundlable file), but `loadApp` prefers `nifra.config.ts` - so an app with BOTH files whose
602
+ * `use` lives only in `nifra.config.ts` makes `fw.use` defined while `backend/framework.ts` exports
603
+ * nothing of that name, and the build dies later with an opaque bundler error pointing at generated
604
+ * code. Refuse it here with the exact move instead, in the spirit of `assertPipelineSeparation`
605
+ * (load.ts).
500
606
  *
501
- * Detected by importing `frameworkFile` and checking the named export, not by comparing paths alone:
502
- * a split app legitimately DEFINES `use` in framework.ts and re-exports it from nifra.config.ts
503
- * (exactly how `adapter` reaches both readers), and a path compare would refuse that correct layout.
504
- * The import is cheap - framework.ts is already in the loaded config's module graph in that layout.
607
+ * Detected by importing `frameworkFile` and checking the named export, not by comparing paths
608
+ * alone: a split app legitimately DEFINES `use` in backend/framework.ts and re-exports it from
609
+ * nifra.config.ts (exactly how `adapter` reaches both readers), and a path compare would refuse
610
+ * that correct layout. The import is cheap - backend/framework.ts is already in the loaded config's
611
+ * module graph in that layout.
505
612
  */
506
613
  export async function assertUseIsEdgeExported(
507
614
  use: NifraFramework["use"],
@@ -513,13 +620,65 @@ export async function assertUseIsEdgeExported(
513
620
  if (typeof mod.use === "function") return
514
621
  throw new Error(
515
622
  `[nifra] \`use\` is exported from ${configPath} but not from ${frameworkFile}. ` +
516
- "`nifra build` generates a server entry that imports `use` from framework.ts (the edge-bundled " +
517
- "file), so this build would fail later with an opaque bundler error inside generated code.\n\n" +
518
- " - Define `use` in framework.ts and re-export it from nifra.config.ts " +
519
- '(`export { use } from "./framework.ts"`) so `nifra dev` sees it too.',
623
+ "`nifra build` generates a server entry that imports `use` from backend/framework.ts (the " +
624
+ "edge-bundled file), so this build would fail later with an opaque bundler error inside generated code.\n\n" +
625
+ " - Define `use` in backend/framework.ts and re-export it from nifra.config.ts " +
626
+ '(`export { use } from "./backend/framework.ts"`) so `nifra dev` sees it too.',
520
627
  )
521
628
  }
522
629
 
630
+ /**
631
+ * Refuse a forwarded framework field (`apiPrefix`, `mounts`, `csp`, ...) the generated server entry
632
+ * would not see, or would see with a different value.
633
+ *
634
+ * `nifra dev` reads these from the loaded config (`nifra.config.ts` when it exists) while the
635
+ * server entry imports them from backend/framework.ts. A field set only in nifra.config.ts would be
636
+ * missing from production, and one defined separately in each file could differ - dev mounting the
637
+ * backend at one path while production mounts it at another. Identity is required, which a
638
+ * re-export satisfies.
639
+ */
640
+ export async function assertFrameworkOptionsEdgeExported(
641
+ fw: NifraFramework,
642
+ configPath: string,
643
+ frameworkFile: string,
644
+ ): Promise<void> {
645
+ if (configPath === frameworkFile) return
646
+ const set = FRAMEWORK_WEB_OPTIONS.filter((name) => fw[name] !== undefined)
647
+ if (set.length === 0) return
648
+ const mod = (await import(frameworkFile).catch(() => ({}))) as Record<string, unknown>
649
+ const drifted = set.filter((name) => mod[name] !== fw[name])
650
+ if (drifted.length === 0) return
651
+ const names = drifted.map((name) => `\`${name}\``).join(", ")
652
+ throw new Error(
653
+ `[nifra] ${names} ${drifted.length === 1 ? "is" : "are"} exported from ${configPath} but not, or not as the same value, from ${frameworkFile}. ` +
654
+ "`nifra build` generates a server entry that imports these from backend/framework.ts, so production " +
655
+ "would serve a different app than `nifra dev`.\n\n" +
656
+ ` - Define ${drifted.length === 1 ? "it" : "them"} in backend/framework.ts and re-export from nifra.config.ts ` +
657
+ `(\`export { ${drifted.join(", ")} } from "./backend/framework.ts"\`).`,
658
+ )
659
+ }
660
+
661
+ /**
662
+ * Refuse a build whose page routes sit under a mount: each would ship as a route that can never render.
663
+ * Read from the config and `routes/` alone; the server also checks its own mount table at startup,
664
+ * which covers a mount added inside `use`.
665
+ */
666
+ /** `nifra dev` shows the in-page issues badge unless `--no-indicator` or `dev.indicator: false` turns it off. */
667
+ export function showsIndicator(
668
+ flags: Pick<Flags, "noIndicator">,
669
+ fw: Pick<NifraFramework, "dev">,
670
+ ): boolean {
671
+ return !flags.noIndicator && fw.dev?.indicator !== false
672
+ }
673
+
674
+ export function assertNoShadowedPages(app: LoadedApp): void {
675
+ const paths = frameworkMountPaths(app.framework, app.backend !== undefined)
676
+ if (paths.length === 0) return
677
+ const shadowed = shadowedPages(discoverRoutes(app.routesDir), paths)
678
+ if (shadowed.length === 0) return
679
+ throw new Error(`[nifra] build blocked: ${formatShadowedPages(shadowed)}`)
680
+ }
681
+
523
682
  /**
524
683
  * A production build must not ship a workspace-linked package whose `default`-condition artifact is
525
684
  * missing or older than its source. The `"bun": "./src"` / `"default": "./dist"` split means Bun (this
@@ -554,38 +713,71 @@ async function assertFreshWorkspaceDists(cwd: string): Promise<void> {
554
713
  )
555
714
  }
556
715
 
557
- async function buildForTarget(app: LoadedApp, target: string, flags: Flags): Promise<void> {
558
- const { isBuildTarget, renderSizeReport, BUILD_TARGETS } = await import("@nifrajs/web/build")
559
- if (!isBuildTarget(target)) {
560
- throw new Error(`[nifra] unknown --target "${target}". Valid: ${BUILD_TARGETS.join(", ")}.`)
561
- }
716
+ async function buildForTarget(
717
+ app: LoadedApp,
718
+ flag: string | undefined,
719
+ flags: Flags,
720
+ ): Promise<void> {
721
+ const { parseBuildTarget, renderSizeReport } = await import("@nifrajs/web/build")
722
+ const target = parseBuildTarget(
723
+ flag ?? app.framework.target ?? "bun",
724
+ flag === undefined ? "target in nifra.config.ts" : "--target",
725
+ )
562
726
  if (flags.vite && flags.bun) {
563
727
  throw new Error("[nifra] `nifra build` takes `--vite` or `--bun`, not both.")
564
728
  }
729
+ const { framework: fw } = app
730
+ const cssCodeSplitRequested = fw.cssCodeSplit === false
731
+ const hasBunTransforms =
732
+ app.resolvedPlugins.clientPlugins.length > 0 || app.resolvedPlugins.serverPlugins.length > 0
733
+ if (cssCodeSplitRequested && flags.bun) {
734
+ throw new Error(
735
+ "[nifra] `cssCodeSplit: false` is a Vite-only production option, but `nifra build --bun` was requested. " +
736
+ "Drop `--bun` (Nifra will select Vite automatically) or remove the aggregate CSS setting.",
737
+ )
738
+ }
739
+ if (cssCodeSplitRequested && hasBunTransforms) {
740
+ throw new Error(
741
+ "[nifra] `cssCodeSplit: false` requires the Vite production pipeline, but this app also declares " +
742
+ "Bun client/server plugins. Use `--vite` with Vite equivalents, or remove the aggregate CSS setting; " +
743
+ "switching pipelines would silently drop the Bun transforms.",
744
+ )
745
+ }
565
746
  // Which bundler builds this app. Default is Bun; an app whose ONLY transforms are `vitePlugins` gets
566
747
  // Vite instead, because the Bun build cannot run them and would silently omit the work. `--vite`/`--bun`
567
748
  // force it, and the one forced combination that would drop transforms throws.
568
- const decision = chooseBuildPipeline(
749
+ let decision = chooseBuildPipeline(
569
750
  app.resolvedPlugins,
570
751
  flags.vite ? "vite" : flags.bun ? "bun" : undefined,
571
752
  )
753
+ if (cssCodeSplitRequested && decision.pipeline === "bun") {
754
+ // With no Bun transforms, aggregate CSS is a Vite-only request that can be honored safely by
755
+ // selecting the same pipeline the user would otherwise have to force explicitly.
756
+ decision = {
757
+ pipeline: "vite",
758
+ why: "auto",
759
+ reason: "auto: this app requested Vite's aggregate CSS output (`cssCodeSplit: false`)",
760
+ }
761
+ }
572
762
  const useVite = decision.pipeline === "vite"
573
763
  // Same deploy-dir output - buildTargetVite delegates to the same orchestrator as buildTarget - so only
574
764
  // the bundler and the plugin FORMAT (Vite plugins, not Bun) differ.
575
765
  const buildTarget = useVite
576
766
  ? (await import("@nifrajs/web/build-vite")).buildTargetVite
577
767
  : (await import("@nifrajs/web/build")).buildTarget
578
- const { framework: fw, routesDir, outDir, cwd, backend } = app
579
- // The server entry must import the adapter from `framework.ts` (edge-safe), not the loaded config.
580
- // `loadApp` guarantees one of them exists; prefer framework.ts so a multi-target app's Vite-plugin
581
- // config never reaches the edge bundle (see load.ts module header).
582
- const frameworkFile = existsSync(resolve(cwd, "framework.ts"))
583
- ? resolve(cwd, "framework.ts")
584
- : existsSync(resolve(cwd, "nifra.config.ts"))
585
- ? resolve(cwd, "nifra.config.ts")
586
- : resolve(cwd, "framework.ts")
587
- const backendFile = resolve(cwd, "backend.ts")
768
+ const { routesDir, cwd, backend } = app
769
+ // `vercel deploy --prebuilt` uploads `.vercel/output` from the project root, so that is where a
770
+ // Vercel build goes unless `--out` says otherwise.
771
+ const outDir =
772
+ target === "vercel" && flags.out === undefined ? resolve(cwd, ".vercel/output") : app.outDir
773
+ // The server entry must import the adapter from `backend/framework.ts` (edge-safe), not the
774
+ // loaded config, so a multi-target app's Vite-plugin config never reaches the edge bundle
775
+ // (load.ts header).
776
+ const frameworkFile = adapterFile(cwd)
777
+ const backendFile = resolve(cwd, BACKEND_APP_FILE)
588
778
  await assertUseIsEdgeExported(fw.use, app.configPath, frameworkFile)
779
+ await assertFrameworkOptionsEdgeExported(fw, app.configPath, frameworkFile)
780
+ assertNoShadowedPages(app)
589
781
  // Plugin FORMAT differs by pipeline: the Bun build takes Bun plugins (clientPlugins/serverPlugins); the
590
782
  // Vite build takes the app's Vite plugins (fw.vitePlugins) for BOTH halves. buildTargetWith forwards
591
783
  // whatever it's given straight to the chosen bundler, which casts to its own plugin type.
@@ -605,12 +797,17 @@ async function buildForTarget(app: LoadedApp, target: string, flags: Flags): Pro
605
797
  clientModule: fw.clientModule,
606
798
  adapterImport: frameworkFile,
607
799
  ...(fw.use ? { useImport: frameworkFile } : {}),
800
+ ...frameworkOptionImports(fw, frameworkFile),
608
801
  ...(backend !== undefined && existsSync(backendFile) ? { backendImport: backendFile } : {}),
609
802
  ...plugins,
610
803
  ...(fw.conditions ? { conditions: fw.conditions } : {}),
611
804
  ...(fw.define ? { define: fw.define } : {}),
612
805
  ...(fw.publicDir !== undefined ? { publicDir: fw.publicDir } : {}),
613
806
  ...(fw.publicEnvPrefix !== undefined ? { publicEnvPrefix: fw.publicEnvPrefix } : {}),
807
+ ...(fw.secretExemptions !== undefined ? { secretExemptions: fw.secretExemptions } : {}),
808
+ ...(fw.cssCodeSplit !== undefined ? { cssCodeSplit: fw.cssCodeSplit } : {}),
809
+ ...(fw.cssLoading !== undefined ? { cssLoading: fw.cssLoading } : {}),
810
+ ...(fw.clientIp !== undefined ? { clientIp: fw.clientIp } : {}),
614
811
  // The static target needs a built app to drive prerendering - only build it when targeting static.
615
812
  ...(target === "static" ? { prerenderApp: await buildPrerenderApp(app) } : {}),
616
813
  })
@@ -643,11 +840,11 @@ async function buildPrerenderApp(
643
840
  adapter: asAdapter(fw.adapter),
644
841
  manifest: discoverRoutes(routesDir),
645
842
  clientEntry: client.entry,
646
- ...(fw.use ? { use: asUse(fw.use) } : {}),
843
+ ...frameworkWebAppOptions(fw, backend),
647
844
  ...(client.routes ? { routePreload: client.routes } : {}),
648
845
  ...(client.css ? { styles: client.css } : {}),
649
846
  ...(client.routeStyles ? { routeStyles: client.routeStyles } : {}),
650
- ...apiOf(backend),
847
+ ...(client.cssLoading !== undefined ? { cssLoading: client.cssLoading } : {}),
651
848
  })
652
849
  }
653
850
 
@@ -656,13 +853,14 @@ interface BuiltManifest {
656
853
  readonly routes?: Readonly<Record<string, readonly string[]>>
657
854
  readonly css?: readonly string[]
658
855
  readonly routeStyles?: Readonly<Record<string, readonly string[]>>
856
+ readonly cssLoading?: CssLoadingMode
659
857
  }
660
858
 
661
859
  async function start(app: LoadedApp, flags: Flags): Promise<void> {
662
860
  const serverFile = resolve(app.outDir, "server.js")
663
861
  if (!existsSync(serverFile)) {
664
862
  // A Cloudflare Pages build emits `_worker.js` (a Workers bundle), not a self-hosting `server.js` - so
665
- // `nifra start` on a dir built for cf-pages would otherwise fail with a bare "no server.js". Name the
863
+ // `nifra start` on a dir built for cloudflare would otherwise fail with a bare "no server.js". Name the
666
864
  // actual mismatch and the fix.
667
865
  if (existsSync(resolve(app.outDir, "_worker.js"))) {
668
866
  throw new Error(
@@ -683,13 +881,14 @@ export function parseFlags(args: readonly string[]): Flags {
683
881
  // the SAME uncommon port for `nifra dev` and `nifra start` (DEFAULT_DEV_PORT) so a project's URL is
684
882
  // stable across commands and doesn't collide with the usual 3000/5173/8080 crowd.
685
883
  let port = Number(Bun.env.PORT ?? DEFAULT_DEV_PORT)
686
- let out = "dist"
884
+ let out: string | undefined
687
885
  let poll = Bun.env.CHOKIDAR_USEPOLLING === "1"
688
- let target = "bun"
886
+ let target: string | undefined
689
887
  let report = false
690
888
  let vite = false
691
889
  let bun = false
692
890
  let allowDuplicateIdentity = false
891
+ let noIndicator = false
693
892
  for (let i = 0; i < args.length; i++) {
694
893
  const a = args[i]
695
894
  if ((a === "--port" || a === "-p") && args[i + 1]) port = Number(args[++i])
@@ -700,11 +899,12 @@ export function parseFlags(args: readonly string[]): Flags {
700
899
  else if (a === "--vite") vite = true
701
900
  else if (a === "--bun") bun = true
702
901
  else if (a === "--allow-duplicate-identity") allowDuplicateIdentity = true
902
+ else if (a === "--no-indicator") noIndicator = true
703
903
  }
704
904
  if (!Number.isFinite(port) || port < 0 || port > 65535) {
705
905
  throw new Error(`[nifra] invalid --port: ${port}`)
706
906
  }
707
- return { port, out, poll, target, report, vite, bun, allowDuplicateIdentity }
907
+ return { port, out, poll, target, report, vite, bun, allowDuplicateIdentity, noIndicator }
708
908
  }
709
909
 
710
910
  /**
@@ -745,8 +945,17 @@ function installReflectionExitHint(): (command: string | undefined) => void {
745
945
  }
746
946
  }
747
947
 
948
+ /** Set on the copy of `nifra mcp` that serves without `.env` files, so it does not restart again. */
949
+ const MCP_WITHOUT_ENV_FILES = "NIFRA_MCP_WITHOUT_ENV_FILES"
950
+
748
951
  async function main(): Promise<void> {
749
952
  const { argv: rawArgv, files: envFiles } = takeEnvFileFlags(cliArgv())
953
+ // `nifra mcp` keeps none of the `.env` values Bun loaded at startup. Decided before the
954
+ // `--env-file`s below, which the caller names explicitly and which stay.
955
+ const forgotten =
956
+ rawArgv[0] === "mcp" && process.env[MCP_WITHOUT_ENV_FILES] !== "1"
957
+ ? await forgetAutoLoadedEnv()
958
+ : []
750
959
  // Applied before any command runs: a reflecting command imports the app on its first await, and the
751
960
  // app reads its environment at module scope, so the variables must already be in place by then.
752
961
  if (envFiles.length > 0) await applyEnvFiles(process.cwd(), envFiles)
@@ -769,6 +978,19 @@ async function main(): Promise<void> {
769
978
  // `mcp` runs a long-lived stdio server and loads the project lazily per-tool - it must not go through
770
979
  // the eager `loadApp` below (which would fail fast on a project that's API-only / not yet built).
771
980
  if (command === "mcp") {
981
+ // Bun passes what it loaded to every subprocess whatever `process.env` says now, so the session
982
+ // moves to a copy of this process that never loads `.env` files.
983
+ if (forgotten.length > 0) {
984
+ const { handOffSession } = await import("./mcp-delegate.ts")
985
+ const exitCode = await handOffSession(
986
+ [process.execPath, "--no-env-file", ...process.argv.slice(1)],
987
+ { cwd: process.cwd(), env: { ...process.env, [MCP_WITHOUT_ENV_FILES]: "1" } },
988
+ )
989
+ if (exitCode !== undefined) {
990
+ process.exitCode = exitCode
991
+ return
992
+ }
993
+ }
772
994
  const { runMcpServer } = await import("./mcp.ts")
773
995
  // `nifra mcp [dir]` - an explicit project directory pins the root (for clients configured outside
774
996
  // the project); otherwise the server resolves it from cwd + the client's MCP roots.
@@ -820,11 +1042,17 @@ async function main(): Promise<void> {
820
1042
  )
821
1043
  return
822
1044
  }
823
- const catalogSpec = command === undefined ? undefined : findCommandSpec(command)
1045
+ // `nifra db <sub>` is the catalog's `db-<sub>` command, projected to MCP as `nifra_db_<sub>`.
1046
+ const dbSub =
1047
+ command === "db" && argv[1] !== undefined && !argv[1].startsWith("-") ? argv[1] : undefined
1048
+ if (command === "db" && (dbSub === undefined || findCommandSpec(`db-${dbSub}`) === undefined)) {
1049
+ throw new Error("[nifra] usage: nifra db schema|query|role|audit - see `nifra help`")
1050
+ }
1051
+ const catalogSpec = findCommandSpec(dbSub === undefined ? command : `db-${dbSub}`)
824
1052
  if (catalogSpec?.transports.includes("cli")) {
825
1053
  const markReflecting = installReflectionExitHint()
826
1054
  try {
827
- const input = bindCommandArgv(catalogSpec, argv.slice(1))
1055
+ const input = bindCommandArgv(catalogSpec, argv.slice(dbSub === undefined ? 1 : 2))
828
1056
  markReflecting(command)
829
1057
  const output = await catalogSpec.run(input, { cwd: process.cwd(), cliVersion: CLI_VERSION })
830
1058
  markReflecting(undefined)
@@ -836,14 +1064,27 @@ async function main(): Promise<void> {
836
1064
  } else {
837
1065
  console.log(catalogSpec.render(output, input).join("\n"))
838
1066
  }
839
- if (catalogSpec.success !== undefined && !catalogSpec.success(output, input))
1067
+ if (catalogSpec.exitCode !== undefined) {
1068
+ process.exitCode = catalogSpec.exitCode(output, input)
1069
+ } else if (catalogSpec.success !== undefined && !catalogSpec.success(output, input)) {
840
1070
  process.exitCode = 1
1071
+ }
841
1072
  } catch (err) {
842
1073
  // A thrown error is nifra's own reporting path, not a silent abort: clear the sentinel so the
843
1074
  // hint stays reserved for the case where the process died inside the app import.
844
1075
  markReflecting(undefined)
845
1076
  console.error(formatCliError(err))
846
- process.exitCode = 1
1077
+ const candidate =
1078
+ typeof err === "object" && err !== null && "exitCode" in err
1079
+ ? (err as { readonly exitCode?: unknown }).exitCode
1080
+ : undefined
1081
+ process.exitCode =
1082
+ typeof candidate === "number" &&
1083
+ Number.isInteger(candidate) &&
1084
+ candidate >= 0 &&
1085
+ candidate <= 255
1086
+ ? candidate
1087
+ : 1
847
1088
  }
848
1089
  return
849
1090
  }
@@ -856,7 +1097,7 @@ async function main(): Promise<void> {
856
1097
  if (!ok) process.exitCode = 1
857
1098
  return
858
1099
  }
859
- // `init-agents` retrofits the agent-discovery files (.mcp.json, CLAUDE.md, …) into the cwd. It's a
1100
+ // `init-agents` retrofits the agent files (.mcp.json, CLAUDE.md, AGENTS.md, …) into the cwd. It's a
860
1101
  // pure file-writing command independent of the app loading, so dispatch it before the eager `loadApp`
861
1102
  // (an existing app might be API-only or not yet built). It always succeeds unless a write throws.
862
1103
  if (command === "init-agents") {
@@ -864,6 +1105,7 @@ async function main(): Promise<void> {
864
1105
  await runInitAgents(process.cwd(), {
865
1106
  json: argv.includes("--json"),
866
1107
  force: argv.includes("--force"),
1108
+ syncMcp: argv.includes("--sync-mcp"),
867
1109
  })
868
1110
  return
869
1111
  }
@@ -884,7 +1126,11 @@ async function main(): Promise<void> {
884
1126
  }
885
1127
  try {
886
1128
  const { runSdk } = await import("./sdk.ts")
887
- await runSdk(process.cwd(), { language, ...(out !== undefined ? { out } : {}) })
1129
+ await runSdk(process.cwd(), {
1130
+ language,
1131
+ ...(out !== undefined ? { out } : {}),
1132
+ ...(argv.includes("--strict") ? { strict: true } : {}),
1133
+ })
888
1134
  } catch (err) {
889
1135
  console.error(formatCliError(err))
890
1136
  process.exitCode = 1
@@ -905,6 +1151,8 @@ async function main(): Promise<void> {
905
1151
  list: argv.includes("--list"),
906
1152
  verify: !argv.includes("--no-verify"),
907
1153
  allowDowngrade: argv.includes("--allow-downgrade"),
1154
+ exact: argv.includes("--exact"),
1155
+ cliVersion: CLI_VERSION,
908
1156
  })
909
1157
  if (!ok) process.exitCode = 1
910
1158
  } catch (err) {
@@ -923,6 +1171,12 @@ async function main(): Promise<void> {
923
1171
  }
924
1172
  const flags = parseFlags(argv.slice(1))
925
1173
  const app = await loadApp(process.cwd(), flags.out)
1174
+ if (command !== "start") {
1175
+ const { refreshRouteTypes, watchRouteTypes } = await import("./route-types.ts")
1176
+ refreshRouteTypes(app.cwd)
1177
+ // A relaunched `nifra dev --bun` child runs beside its parent, which already watches.
1178
+ if (command === "dev" && process.env.NIFRA_BUN_DEV_TOKEN === undefined) watchRouteTypes(app.cwd)
1179
+ }
926
1180
  if (command === "dev") await dev(app, flags)
927
1181
  else if (command === "build") await buildForTarget(app, flags.target, flags)
928
1182
  else await start(app, flags)
@@ -1007,6 +1261,7 @@ function isCliCommandToken(value: string): boolean {
1007
1261
  value === "mcp" ||
1008
1262
  value === "init-agents" ||
1009
1263
  value === "upgrade" ||
1264
+ value === "db" ||
1010
1265
  value === "help" ||
1011
1266
  value === "--help" ||
1012
1267
  value === "-h" ||