@nifrajs/cli 3.5.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 (403) hide show
  1. package/README.md +42 -3
  2. package/dist/app-files.d.ts +11 -0
  3. package/dist/app-files.d.ts.map +1 -0
  4. package/dist/app-files.js +34 -0
  5. package/dist/app-files.js.map +1 -0
  6. package/dist/assure-hydration.d.ts +7 -1
  7. package/dist/assure-hydration.d.ts.map +1 -1
  8. package/dist/assure-hydration.js +128 -43
  9. package/dist/assure-hydration.js.map +1 -1
  10. package/dist/assure.d.ts.map +1 -1
  11. package/dist/assure.js +16 -2
  12. package/dist/assure.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts +5 -1
  14. package/dist/capabilities-tool.d.ts.map +1 -1
  15. package/dist/capabilities-tool.js +32 -10
  16. package/dist/capabilities-tool.js.map +1 -1
  17. package/dist/cdn-check.d.ts +41 -0
  18. package/dist/cdn-check.d.ts.map +1 -0
  19. package/dist/cdn-check.js +207 -0
  20. package/dist/cdn-check.js.map +1 -0
  21. package/dist/check-diagnostics.d.ts +4 -0
  22. package/dist/check-diagnostics.d.ts.map +1 -1
  23. package/dist/check-diagnostics.js +13 -0
  24. package/dist/check-diagnostics.js.map +1 -1
  25. package/dist/check-scan.d.ts +49 -7
  26. package/dist/check-scan.d.ts.map +1 -1
  27. package/dist/check-scan.js +291 -62
  28. package/dist/check-scan.js.map +1 -1
  29. package/dist/check.d.ts.map +1 -1
  30. package/dist/check.js +99 -63
  31. package/dist/check.js.map +1 -1
  32. package/dist/cli.d.ts +42 -16
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +262 -82
  35. package/dist/cli.js.map +1 -1
  36. package/dist/command-catalog.d.ts +57 -5
  37. package/dist/command-catalog.d.ts.map +1 -1
  38. package/dist/command-catalog.js +270 -26
  39. package/dist/command-catalog.js.map +1 -1
  40. package/dist/config-target.d.ts +8 -0
  41. package/dist/config-target.d.ts.map +1 -0
  42. package/dist/config-target.js +45 -0
  43. package/dist/config-target.js.map +1 -0
  44. package/dist/contracts.d.ts.map +1 -1
  45. package/dist/contracts.js +52 -13
  46. package/dist/contracts.js.map +1 -1
  47. package/dist/db-audit.d.ts +37 -0
  48. package/dist/db-audit.d.ts.map +1 -0
  49. package/dist/db-audit.js +99 -0
  50. package/dist/db-audit.js.map +1 -0
  51. package/dist/db-child.d.ts +64 -0
  52. package/dist/db-child.d.ts.map +1 -0
  53. package/dist/db-child.js +213 -0
  54. package/dist/db-child.js.map +1 -0
  55. package/dist/db-config.d.ts +60 -0
  56. package/dist/db-config.d.ts.map +1 -0
  57. package/dist/db-config.js +176 -0
  58. package/dist/db-config.js.map +1 -0
  59. package/dist/db-tool.d.ts +84 -0
  60. package/dist/db-tool.d.ts.map +1 -0
  61. package/dist/db-tool.js +537 -0
  62. package/dist/db-tool.js.map +1 -0
  63. package/dist/dev-bun-config.d.ts +20 -13
  64. package/dist/dev-bun-config.d.ts.map +1 -1
  65. package/dist/dev-bun-config.js +98 -41
  66. package/dist/dev-bun-config.js.map +1 -1
  67. package/dist/dev-feed-tool.d.ts +106 -0
  68. package/dist/dev-feed-tool.d.ts.map +1 -0
  69. package/dist/dev-feed-tool.js +713 -0
  70. package/dist/dev-feed-tool.js.map +1 -0
  71. package/dist/dev-server-client.d.ts +67 -0
  72. package/dist/dev-server-client.d.ts.map +1 -0
  73. package/dist/dev-server-client.js +195 -0
  74. package/dist/dev-server-client.js.map +1 -0
  75. package/dist/diff-tool.d.ts +3 -3
  76. package/dist/diff-tool.d.ts.map +1 -1
  77. package/dist/diff-tool.js +6 -5
  78. package/dist/diff-tool.js.map +1 -1
  79. package/dist/docs-search.d.ts.map +1 -1
  80. package/dist/docs-search.js +12 -4
  81. package/dist/docs-search.js.map +1 -1
  82. package/dist/doctor.d.ts +17 -0
  83. package/dist/doctor.d.ts.map +1 -1
  84. package/dist/doctor.js +76 -15
  85. package/dist/doctor.js.map +1 -1
  86. package/dist/env-file.d.ts +15 -0
  87. package/dist/env-file.d.ts.map +1 -1
  88. package/dist/env-file.js +73 -1
  89. package/dist/env-file.js.map +1 -1
  90. package/dist/fix-recipes.d.ts.map +1 -1
  91. package/dist/fix-recipes.js +41 -21
  92. package/dist/fix-recipes.js.map +1 -1
  93. package/dist/frontend-guidance.js +5 -5
  94. package/dist/frontend-guidance.js.map +1 -1
  95. package/dist/i18n-check.d.ts +16 -0
  96. package/dist/i18n-check.d.ts.map +1 -0
  97. package/dist/i18n-check.js +154 -0
  98. package/dist/i18n-check.js.map +1 -0
  99. package/dist/init-agents.d.ts +49 -13
  100. package/dist/init-agents.d.ts.map +1 -1
  101. package/dist/init-agents.js +239 -34
  102. package/dist/init-agents.js.map +1 -1
  103. package/dist/internal/code-unit-order.d.ts +4 -0
  104. package/dist/internal/code-unit-order.d.ts.map +1 -0
  105. package/dist/internal/code-unit-order.js +6 -0
  106. package/dist/internal/code-unit-order.js.map +1 -0
  107. package/dist/internal/script-kind.d.ts +7 -0
  108. package/dist/internal/script-kind.d.ts.map +1 -0
  109. package/dist/internal/script-kind.js +12 -0
  110. package/dist/internal/script-kind.js.map +1 -0
  111. package/dist/internal/source-facts.d.ts +5 -0
  112. package/dist/internal/source-facts.d.ts.map +1 -1
  113. package/dist/internal/source-facts.js +61 -5
  114. package/dist/internal/source-facts.js.map +1 -1
  115. package/dist/internal/typescript-import.d.ts +49 -24
  116. package/dist/internal/typescript-import.d.ts.map +1 -1
  117. package/dist/internal/typescript-import.js +507 -67
  118. package/dist/internal/typescript-import.js.map +1 -1
  119. package/dist/introspect.d.ts +5 -1
  120. package/dist/introspect.d.ts.map +1 -1
  121. package/dist/introspect.js +92 -27
  122. package/dist/introspect.js.map +1 -1
  123. package/dist/learn.js +3 -3
  124. package/dist/learn.js.map +1 -1
  125. package/dist/levels-tool.d.ts +10 -0
  126. package/dist/levels-tool.d.ts.map +1 -1
  127. package/dist/levels-tool.js +33 -3
  128. package/dist/levels-tool.js.map +1 -1
  129. package/dist/load.d.ts +67 -17
  130. package/dist/load.d.ts.map +1 -1
  131. package/dist/load.js +78 -18
  132. package/dist/load.js.map +1 -1
  133. package/dist/manifest-tool.d.ts.map +1 -1
  134. package/dist/manifest-tool.js +3 -2
  135. package/dist/manifest-tool.js.map +1 -1
  136. package/dist/mcp-app-tool.d.ts.map +1 -1
  137. package/dist/mcp-app-tool.js +2 -0
  138. package/dist/mcp-app-tool.js.map +1 -1
  139. package/dist/mcp-context.js +1 -1
  140. package/dist/mcp-context.js.map +1 -1
  141. package/dist/mcp-delegate.d.ts +57 -0
  142. package/dist/mcp-delegate.d.ts.map +1 -0
  143. package/dist/mcp-delegate.js +130 -0
  144. package/dist/mcp-delegate.js.map +1 -0
  145. package/dist/mcp-docs-tools.d.ts.map +1 -1
  146. package/dist/mcp-docs-tools.js +23 -5
  147. package/dist/mcp-docs-tools.js.map +1 -1
  148. package/dist/mcp-exec.d.ts +2 -0
  149. package/dist/mcp-exec.d.ts.map +1 -1
  150. package/dist/mcp-exec.js +132 -108
  151. package/dist/mcp-exec.js.map +1 -1
  152. package/dist/mcp-http.d.ts.map +1 -1
  153. package/dist/mcp-http.js +17 -5
  154. package/dist/mcp-http.js.map +1 -1
  155. package/dist/mcp-io.d.ts +14 -0
  156. package/dist/mcp-io.d.ts.map +1 -1
  157. package/dist/mcp-io.js +45 -0
  158. package/dist/mcp-io.js.map +1 -1
  159. package/dist/mcp-isolate.d.ts +48 -0
  160. package/dist/mcp-isolate.d.ts.map +1 -0
  161. package/dist/mcp-isolate.js +222 -0
  162. package/dist/mcp-isolate.js.map +1 -0
  163. package/dist/mcp-project-child.d.ts +74 -0
  164. package/dist/mcp-project-child.d.ts.map +1 -0
  165. package/dist/mcp-project-child.js +130 -0
  166. package/dist/mcp-project-child.js.map +1 -0
  167. package/dist/mcp-render.js +2 -2
  168. package/dist/mcp-render.js.map +1 -1
  169. package/dist/mcp-root.d.ts +37 -9
  170. package/dist/mcp-root.d.ts.map +1 -1
  171. package/dist/mcp-root.js +110 -23
  172. package/dist/mcp-root.js.map +1 -1
  173. package/dist/mcp-run.d.ts +18 -1
  174. package/dist/mcp-run.d.ts.map +1 -1
  175. package/dist/mcp-run.js +82 -9
  176. package/dist/mcp-run.js.map +1 -1
  177. package/dist/mcp-ws.js +4 -1
  178. package/dist/mcp-ws.js.map +1 -1
  179. package/dist/mcp.d.ts +16 -3
  180. package/dist/mcp.d.ts.map +1 -1
  181. package/dist/mcp.js +81 -53
  182. package/dist/mcp.js.map +1 -1
  183. package/dist/migrate-layout.d.ts +44 -0
  184. package/dist/migrate-layout.d.ts.map +1 -0
  185. package/dist/migrate-layout.js +982 -0
  186. package/dist/migrate-layout.js.map +1 -0
  187. package/dist/openapi-tool.d.ts +2 -2
  188. package/dist/openapi-tool.d.ts.map +1 -1
  189. package/dist/openapi-tool.js.map +1 -1
  190. package/dist/openapi-types.d.ts +2 -1
  191. package/dist/openapi-types.d.ts.map +1 -1
  192. package/dist/openapi-types.js +240 -12
  193. package/dist/openapi-types.js.map +1 -1
  194. package/dist/pipeline-report.d.ts +3 -2
  195. package/dist/pipeline-report.d.ts.map +1 -1
  196. package/dist/pipeline-report.js +19 -16
  197. package/dist/pipeline-report.js.map +1 -1
  198. package/dist/port.d.ts +5 -4
  199. package/dist/port.d.ts.map +1 -1
  200. package/dist/port.js +46 -29
  201. package/dist/port.js.map +1 -1
  202. package/dist/project-facts.d.ts +5 -0
  203. package/dist/project-facts.d.ts.map +1 -1
  204. package/dist/project-facts.js.map +1 -1
  205. package/dist/recipes/4.0.0.d.ts +9 -0
  206. package/dist/recipes/4.0.0.d.ts.map +1 -0
  207. package/dist/recipes/4.0.0.js +34 -0
  208. package/dist/recipes/4.0.0.js.map +1 -0
  209. package/dist/recipes/index.d.ts.map +1 -1
  210. package/dist/recipes/index.js +2 -0
  211. package/dist/recipes/index.js.map +1 -1
  212. package/dist/release-verification.d.ts +11 -1
  213. package/dist/release-verification.d.ts.map +1 -1
  214. package/dist/release-verification.js +47 -4
  215. package/dist/release-verification.js.map +1 -1
  216. package/dist/review.d.ts.map +1 -1
  217. package/dist/review.js +12 -8
  218. package/dist/review.js.map +1 -1
  219. package/dist/route-types.d.ts +28 -0
  220. package/dist/route-types.d.ts.map +1 -0
  221. package/dist/route-types.js +106 -0
  222. package/dist/route-types.js.map +1 -0
  223. package/dist/rules/codes.d.ts +9 -0
  224. package/dist/rules/codes.d.ts.map +1 -1
  225. package/dist/rules/codes.js +9 -0
  226. package/dist/rules/codes.js.map +1 -1
  227. package/dist/rules/data-guard.d.ts +12 -0
  228. package/dist/rules/data-guard.d.ts.map +1 -0
  229. package/dist/rules/data-guard.js +213 -0
  230. package/dist/rules/data-guard.js.map +1 -0
  231. package/dist/rules/index.d.ts +3 -0
  232. package/dist/rules/index.d.ts.map +1 -1
  233. package/dist/rules/index.js +17 -5
  234. package/dist/rules/index.js.map +1 -1
  235. package/dist/rules/islands.d.ts.map +1 -1
  236. package/dist/rules/islands.js +7 -4
  237. package/dist/rules/islands.js.map +1 -1
  238. package/dist/rules/legacy.d.ts.map +1 -1
  239. package/dist/rules/legacy.js +71 -38
  240. package/dist/rules/legacy.js.map +1 -1
  241. package/dist/rules/nano.d.ts.map +1 -1
  242. package/dist/rules/nano.js +15 -6
  243. package/dist/rules/nano.js.map +1 -1
  244. package/dist/rules/pages.d.ts +15 -0
  245. package/dist/rules/pages.d.ts.map +1 -0
  246. package/dist/rules/pages.js +88 -0
  247. package/dist/rules/pages.js.map +1 -0
  248. package/dist/rules/routes.d.ts +11 -0
  249. package/dist/rules/routes.d.ts.map +1 -1
  250. package/dist/rules/routes.js +180 -2
  251. package/dist/rules/routes.js.map +1 -1
  252. package/dist/rules/secrets.d.ts +10 -0
  253. package/dist/rules/secrets.d.ts.map +1 -0
  254. package/dist/rules/secrets.js +169 -0
  255. package/dist/rules/secrets.js.map +1 -0
  256. package/dist/rules/security.d.ts +7 -0
  257. package/dist/rules/security.d.ts.map +1 -1
  258. package/dist/rules/security.js +171 -65
  259. package/dist/rules/security.js.map +1 -1
  260. package/dist/rules/typescript.d.ts +16 -0
  261. package/dist/rules/typescript.d.ts.map +1 -0
  262. package/dist/rules/typescript.js +59 -0
  263. package/dist/rules/typescript.js.map +1 -0
  264. package/dist/rules/zones.d.ts +17 -0
  265. package/dist/rules/zones.d.ts.map +1 -0
  266. package/dist/rules/zones.js +174 -0
  267. package/dist/rules/zones.js.map +1 -0
  268. package/dist/scaffold.d.ts +25 -12
  269. package/dist/scaffold.d.ts.map +1 -1
  270. package/dist/scaffold.js +132 -87
  271. package/dist/scaffold.js.map +1 -1
  272. package/dist/sdk.d.ts +17 -3
  273. package/dist/sdk.d.ts.map +1 -1
  274. package/dist/sdk.js +941 -153
  275. package/dist/sdk.js.map +1 -1
  276. package/dist/search-terms.d.ts +2 -0
  277. package/dist/search-terms.d.ts.map +1 -1
  278. package/dist/search-terms.js +23 -8
  279. package/dist/search-terms.js.map +1 -1
  280. package/dist/smoke.d.ts +79 -0
  281. package/dist/smoke.d.ts.map +1 -0
  282. package/dist/smoke.js +494 -0
  283. package/dist/smoke.js.map +1 -0
  284. package/dist/sync-manifest.d.ts.map +1 -1
  285. package/dist/sync-manifest.js +2 -1
  286. package/dist/sync-manifest.js.map +1 -1
  287. package/dist/sync-routes.d.ts +4 -3
  288. package/dist/sync-routes.d.ts.map +1 -1
  289. package/dist/sync-routes.js +4 -3
  290. package/dist/sync-routes.js.map +1 -1
  291. package/dist/test-tool.d.ts.map +1 -1
  292. package/dist/test-tool.js +30 -11
  293. package/dist/test-tool.js.map +1 -1
  294. package/dist/types-search.d.ts.map +1 -1
  295. package/dist/types-search.js +11 -5
  296. package/dist/types-search.js.map +1 -1
  297. package/dist/upgrade.d.ts +36 -6
  298. package/dist/upgrade.d.ts.map +1 -1
  299. package/dist/upgrade.js +167 -29
  300. package/dist/upgrade.js.map +1 -1
  301. package/dist/verification-plan.d.ts.map +1 -1
  302. package/dist/verification-plan.js +4 -0
  303. package/dist/verification-plan.js.map +1 -1
  304. package/dist/verification.d.ts.map +1 -1
  305. package/dist/verification.js +45 -12
  306. package/dist/verification.js.map +1 -1
  307. package/dist/web-app-options.d.ts +19 -0
  308. package/dist/web-app-options.d.ts.map +1 -0
  309. package/dist/web-app-options.js +49 -0
  310. package/dist/web-app-options.js.map +1 -0
  311. package/dist/work-graph.d.ts.map +1 -1
  312. package/dist/work-graph.js +29 -13
  313. package/dist/work-graph.js.map +1 -1
  314. package/docs/examples.json +185 -29
  315. package/docs/llms-full.txt +3807 -580
  316. package/docs/types.json +14229 -10939
  317. package/package.json +19 -12
  318. package/src/app-files.ts +40 -0
  319. package/src/assure-hydration.ts +139 -46
  320. package/src/assure.ts +17 -2
  321. package/src/capabilities-tool.ts +35 -9
  322. package/src/cdn-check.ts +268 -0
  323. package/src/check-diagnostics.ts +17 -0
  324. package/src/check-scan.ts +358 -63
  325. package/src/check.ts +97 -62
  326. package/src/cli.ts +296 -87
  327. package/src/command-catalog.ts +374 -38
  328. package/src/config-target.ts +47 -0
  329. package/src/contracts.ts +54 -12
  330. package/src/db-audit.ts +128 -0
  331. package/src/db-child.ts +285 -0
  332. package/src/db-config.ts +224 -0
  333. package/src/db-tool.ts +690 -0
  334. package/src/dev-bun-config.ts +116 -40
  335. package/src/dev-feed-tool.ts +912 -0
  336. package/src/dev-server-client.ts +267 -0
  337. package/src/diff-tool.ts +6 -5
  338. package/src/docs-search.ts +10 -3
  339. package/src/doctor.ts +98 -15
  340. package/src/env-file.ts +78 -1
  341. package/src/fix-recipes.ts +44 -31
  342. package/src/frontend-guidance.ts +5 -5
  343. package/src/i18n-check.ts +179 -0
  344. package/src/init-agents.ts +301 -38
  345. package/src/internal/code-unit-order.ts +5 -0
  346. package/src/internal/script-kind.ts +11 -0
  347. package/src/internal/source-facts.ts +96 -22
  348. package/src/internal/typescript-import.ts +663 -68
  349. package/src/introspect.ts +90 -27
  350. package/src/learn.ts +3 -3
  351. package/src/levels-tool.ts +47 -2
  352. package/src/load.ts +140 -33
  353. package/src/manifest-tool.ts +3 -2
  354. package/src/mcp-app-tool.ts +2 -0
  355. package/src/mcp-context.ts +1 -1
  356. package/src/mcp-delegate.ts +162 -0
  357. package/src/mcp-docs-tools.ts +23 -5
  358. package/src/mcp-exec.ts +160 -132
  359. package/src/mcp-http.ts +19 -5
  360. package/src/mcp-io.ts +57 -0
  361. package/src/mcp-isolate.ts +287 -0
  362. package/src/mcp-project-child.ts +182 -0
  363. package/src/mcp-render.ts +2 -2
  364. package/src/mcp-root.ts +117 -22
  365. package/src/mcp-run.ts +104 -10
  366. package/src/mcp-ws.ts +4 -1
  367. package/src/mcp.ts +106 -64
  368. package/src/migrate-layout.ts +1121 -0
  369. package/src/openapi-tool.ts +7 -3
  370. package/src/openapi-types.ts +315 -13
  371. package/src/pipeline-report.ts +22 -18
  372. package/src/port.ts +53 -30
  373. package/src/project-facts.ts +5 -0
  374. package/src/recipes/4.0.0.ts +35 -0
  375. package/src/recipes/index.ts +2 -0
  376. package/src/release-verification.ts +74 -5
  377. package/src/review.ts +13 -8
  378. package/src/route-types.ts +133 -0
  379. package/src/rules/codes.ts +9 -0
  380. package/src/rules/data-guard.ts +230 -0
  381. package/src/rules/index.ts +18 -5
  382. package/src/rules/islands.ts +12 -4
  383. package/src/rules/legacy.ts +71 -39
  384. package/src/rules/nano.ts +18 -6
  385. package/src/rules/pages.ts +99 -0
  386. package/src/rules/routes.ts +181 -2
  387. package/src/rules/secrets.ts +174 -0
  388. package/src/rules/security.ts +198 -70
  389. package/src/rules/typescript.ts +77 -0
  390. package/src/rules/zones.ts +195 -0
  391. package/src/scaffold.ts +147 -95
  392. package/src/sdk.ts +1215 -158
  393. package/src/search-terms.ts +24 -7
  394. package/src/smoke.ts +607 -0
  395. package/src/sync-manifest.ts +2 -1
  396. package/src/sync-routes.ts +4 -3
  397. package/src/test-tool.ts +32 -11
  398. package/src/types-search.ts +11 -4
  399. package/src/upgrade.ts +205 -25
  400. package/src/verification-plan.ts +13 -0
  401. package/src/verification.ts +50 -12
  402. package/src/web-app-options.ts +57 -0
  403. package/src/work-graph.ts +27 -13
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,27 +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,
18
17
  type CssLoadingMode,
19
18
  createWebApp,
20
19
  DEFAULT_DEV_PORT,
21
20
  type RenderAdapter,
22
21
  } from "@nifrajs/web"
23
22
  import { discoverRoutes } from "@nifrajs/web/fs"
23
+ import { formatShadowedPages, shadowedPages } from "@nifrajs/web/route-manifest"
24
24
  import type { BunPlugin } from "bun"
25
+ import { adapterFile, BACKEND_APP_FILE } from "./app-files.ts"
25
26
  import { bindCommandArgv, findCommandSpec, renderCommandCatalogHelp } from "./command-catalog.ts"
26
- import { applyEnvFiles, takeEnvFileFlags } from "./env-file.ts"
27
- 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"
28
29
  import { chooseBuildPipeline, describePipeline } from "./pipeline-guard.ts"
30
+ import {
31
+ frameworkMountPaths,
32
+ frameworkOptionImports,
33
+ frameworkWebAppOptions,
34
+ } from "./web-app-options.ts"
29
35
 
30
36
  export interface Flags {
31
37
  readonly port: number
32
- readonly out: string
38
+ /** `--out <dir>`; without it `dist`, or `.vercel/output` for a Vercel build. */
39
+ readonly out: string | undefined
33
40
  readonly poll: boolean
34
- /** `nifra build --target <t>`: emit a full deploy dir for this target. Defaults to `bun`. */
35
- 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
36
44
  /** `nifra build --report`: print a per-chunk size + gzip table after the build. */
37
45
  readonly report: boolean
38
46
  /** `nifra build --vite`: force the client + server through Vite/Rollup. Without a flag the pipeline is
@@ -47,18 +55,22 @@ export interface Flags {
47
55
  * a hard failure to a loud warning so a duplicate coming from a linked sibling repo doesn't take dev
48
56
  * down while you fix the resolution. Dev only - `nifra build` always fails hard on a duplicate. */
49
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
50
60
  }
51
61
 
52
62
  /** Forward a re-exec'd Bun child's output without relying on Windows inheriting a pipe-of-a-pipe. */
53
63
  async function forwardChildOutput(
54
64
  stream: ReadableStream<Uint8Array>,
55
65
  sink: { write(chunk: Uint8Array): unknown },
66
+ observe?: (chunk: Uint8Array) => void,
56
67
  ): Promise<void> {
57
68
  const reader = stream.getReader()
58
69
  try {
59
70
  for (;;) {
60
71
  const { done, value } = await reader.read()
61
72
  if (done) return
73
+ observe?.(value)
62
74
  await sink.write(value)
63
75
  }
64
76
  } finally {
@@ -66,6 +78,31 @@ async function forwardChildOutput(
66
78
  }
67
79
  }
68
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
+
69
106
  const HELP = `nifra - zero-config dev/build/start for a nifra app
70
107
 
71
108
  Usage:
@@ -81,14 +118,33 @@ Usage:
81
118
  loud warning instead of a hard failure, so a duplicate React/
82
119
  adapter copy from a linked sibling repo doesn't take dev down
83
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.
84
140
  nifra build [--out <dir>] [--report] Emit a complete deploy directory.
85
141
  [--target <t>] Target a FULL deploy dir for <t>:
86
- bun | node | deno | cf-pages | vercel | static. Packages
142
+ bun | node | deno | cloudflare | vercel | static. Packages
87
143
  buildClient + buildServer (+ prerender for static) so an app
88
144
  no longer hand-writes build-<target>.ts + _worker.ts +
89
145
  _routes.json. The server entry is generated from your
90
- framework.ts (adapter) + backend.ts + routes/. --report prints
91
- 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).
92
148
  [--vite | --bun] Force the bundler. Without a flag it follows your config: Bun
93
149
  (faster, Bun-native) unless your ONLY transforms are
94
150
  \`vitePlugins\`, which the Bun build cannot run - then Vite,
@@ -109,12 +165,20 @@ Usage:
109
165
  against it: a route the target can't honour (an ssr route on a
110
166
  static build, ISR where there's no revalidation) exits nonzero
111
167
  with the consequence - run in CI so it fails the build, not prod.
112
- nifra init-agents [--force] [--json] Retrofit an EXISTING app with the agent-discovery files a new
113
- app ships: .mcp.json + .cursor/mcp.json (register this project's
114
- nifra MCP), CLAUDE.md (MCP-first preamble + @AGENTS.md import),
115
- and a "## MCP server" section in AGENTS.md. No-clobber by
116
- default (skips a file you've customized); --force overwrites the
117
- 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\`).
118
182
  nifra mcp [dir] Start an MCP server (stdio) exposing this project to a coding
119
183
  agent. The project root is [dir] when given, else resolved from
120
184
  cwd (marker walk-up + the client's MCP roots); tools refuse
@@ -123,15 +187,18 @@ Usage:
123
187
  nifra_run (backend), nifra_render (SSR a page), nifra_docs,
124
188
  nifra_example (verified snippets), nifra_scaffold (route→file),
125
189
  nifra_check (drift gate + fixes), nifra_levels (verification
126
- ladder), nifra_doctor (deps), nifra_explain (structured errors),
127
- 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).
128
195
  nifra docs-mcp [--port <n>] Serve the PUBLIC docs MCP over HTTP (nifra_docs + nifra_example) -
129
196
  self-host on a VPS so any remote agent can learn nifra. Default :8787.
130
197
  nifra learn [<step>] Print the guided build-an-app path (the human view of nifra_learn):
131
198
  no arg for the step index, a number for one step's goal/do/verify.
132
199
  nifra check [--json] [--sarif] [--lints-only]
133
200
  Gate: typecheck + lints (hand-rolled fetch(), untyped client("…"),
134
- server-only imports in routes/). Run as "done"; --json for agents;
201
+ backend code in browser code). Run as "done"; --json for agents;
135
202
  --sarif emits the same stable diagnostics as SARIF 2.1.0 for external
136
203
  review tools; --lints-only skips tsc for a near-instant inner-loop pass.
137
204
  nifra verify [--release] [--json] Run the shared repository verification gate. --release runs the
@@ -151,8 +218,8 @@ Usage:
151
218
  request field or a removed response field breaks; widening a
152
219
  request enum or adding a response field doesn't) and fails
153
220
  closed. Exits non-zero on any breaking change - run it in CI.
154
- nifra sdk --lang <python|go> [--out <file>]
155
- 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.
156
223
  nifra assure [--config <file>] [--json] Route-assurance report. Human table by default; --json emits
157
224
  the {ok, routes, findings} report for agents.
158
225
  nifra assure --bundle [--json] [--strict] [--out <file>] [--hydration] [--interact]
@@ -190,22 +257,40 @@ Usage:
190
257
  print production readiness, with --strict making absent
191
258
  applicable guarantees fail. --auto-fix writes safe local-
192
259
  version dependency fixes.
193
- nifra upgrade <version> Run the per-release upgrade recipe for <version>: sweep every
194
- [--write] [--no-verify] matching dependency pin (preserving ^/~/exact), move removed
195
- [--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.
196
264
  [--allow-downgrade] Dry-run by default; --write applies then verifies (--no-verify
197
- to skip). --list shows available targets. Fail-closed on an
198
- unknown version or a rollback (--allow-downgrade overrides).
199
- 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.
200
279
  nifra port [--target <t>] [--json] Portability linter: print a feature × deploy-target capability
201
280
  [--ci] [--strict] matrix (in-memory stores, in-process cron/WebSocket, Bun/Deno
202
281
  globals, node: builtins) with file:line evidence. --target auto-
203
282
  detected from build/deploy scripts, wrangler.toml, or vercel config;
204
283
  --ci (or any --target) exits nonzero when a used feature is
205
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.
206
291
 
207
- Reads nifra.config.ts (adapter + clientModule + plugins; or framework.ts), backend.ts (optional), and
208
- 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.
209
294
 
210
295
  Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with \`--port <n>\` (alias \`-p\`) or the
211
296
  \`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).
@@ -219,15 +304,11 @@ set in the process environment is never overwritten by a file.
219
304
  ${renderCommandCatalogHelp()}`
220
305
 
221
306
  // Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
222
- const CLI_VERSION = "3.5.0"
307
+ const CLI_VERSION = "4.0.0"
223
308
 
224
309
  // A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
225
310
  const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
226
311
  const asBunPlugins = (v: readonly unknown[]): BunPlugin[] => v as BunPlugin[]
227
- const asUse = (v: (app: never) => void): NonNullable<CreateWebAppOptions["use"]> =>
228
- v as NonNullable<CreateWebAppOptions["use"]>
229
- const apiOf = (backend: unknown): { api?: unknown } =>
230
- backend === undefined ? {} : { api: inProcessClient(backend as never) }
231
312
 
232
313
  /**
233
314
  * Render an error for the CLI, unwrapping the detail a bare `.message` drops.
@@ -284,7 +365,7 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
284
365
  )
285
366
  if (decision.pipeline === "bun") {
286
367
  // Bun's dev-server bundler takes plugins only via bunfig `[serve.static]`, read at process
287
- // 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
288
369
  // generating a config and re-execing this same command once with `--config=`. The child proves
289
370
  // it IS the configured child with a per-launch random token (matched against the file the
290
371
  // parent just wrote, consumed on first read). A fixed sentinel here would be a secret-leak
@@ -302,7 +383,11 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
302
383
  "launches; refusing to serve without the client-boundary plugins.",
303
384
  )
304
385
  }
305
- 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
+ )
306
391
  // Bun 1.4 on Windows may discard the script-relative portion of argv when `--config` is
307
392
  // present. Keep a second, authenticated copy of the user vector in the child's environment so
308
393
  // the configured process cannot silently fall back to `nifra`'s help and exit 0. The launch token
@@ -353,16 +438,30 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
353
438
  },
354
439
  },
355
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)
356
443
  const forwarded = Promise.all([
357
444
  forwardChildOutput(child.stdout as ReadableStream<Uint8Array>, Bun.stdout),
358
- forwardChildOutput(child.stderr as ReadableStream<Uint8Array>, Bun.stderr),
445
+ forwardChildOutput(child.stderr as ReadableStream<Uint8Array>, Bun.stderr, stderrTail.push),
359
446
  ])
360
- const forward = (): void => child.kill("SIGINT")
447
+ let interrupted = false
448
+ const forward = (): void => {
449
+ interrupted = true
450
+ child.kill("SIGINT")
451
+ }
361
452
  process.on("SIGINT", forward)
362
453
  process.on("SIGTERM", forward)
363
454
  try {
364
455
  const [code] = await Promise.all([child.exited, forwarded])
365
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
+ }
366
465
  } finally {
367
466
  process.off("SIGINT", forward)
368
467
  process.off("SIGTERM", forward)
@@ -411,13 +510,14 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
411
510
  ...(fw.publicDir !== undefined ? { publicDir: fw.publicDir } : {}),
412
511
  ...(fw.conditions ? { conditions: fw.conditions } : {}),
413
512
  ...(fw.define ? { define: fw.define } : {}),
414
- createApp: (clientEntry, importQuery) =>
513
+ indicator: showsIndicator(flags, fw),
514
+ createApp: (clientEntry, importQuery, dev) =>
415
515
  createWebApp({
416
516
  adapter: asAdapter(fw.adapter),
417
517
  manifest: discoverRoutes(routesDir, { importQuery }),
418
518
  clientEntry,
419
- ...(fw.use ? { use: asUse(fw.use) } : {}),
420
- ...apiOf(backend),
519
+ ...frameworkWebAppOptions(fw, backend),
520
+ onLoaderError: dev.onLoaderError,
421
521
  }),
422
522
  })
423
523
  console.log(`nifra dev (bun) → http://localhost:${server.port}`)
@@ -467,18 +567,19 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
467
567
  poll: flags.poll,
468
568
  port: flags.port,
469
569
  ...(flags.allowDuplicateIdentity ? { allowDuplicateIdentity: true } : {}),
570
+ indicator: showsIndicator(flags, fw),
470
571
  ...(fw.conditions ? { conditions: fw.conditions } : {}),
471
572
  ...(fw.define ? { define: fw.define } : {}),
472
573
  // `load` resolves route modules through VITE, not through Bun. That is what makes the Vite
473
574
  // pipeline own the whole phase: the client and the server now agree on every specifier, so
474
575
  // `resolve.dedupe` finally governs SSR and the dual-React crash cannot occur.
475
- createApp: (clientEntry, load) =>
576
+ createApp: (clientEntry, load, dev) =>
476
577
  createWebApp({
477
578
  adapter: asAdapter(fw.adapter),
478
579
  manifest: discoverRoutes(routesDir, { load }),
479
580
  clientEntry,
480
- ...(fw.use ? { use: asUse(fw.use) } : {}),
481
- ...apiOf(backend),
581
+ ...frameworkWebAppOptions(fw, backend),
582
+ onLoaderError: dev.onLoaderError,
482
583
  }),
483
584
  })
484
585
  console.log(`nifra dev (vite) → http://localhost:${server.port}`)
@@ -488,23 +589,26 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
488
589
  /**
489
590
  * `nifra build --target <t>` - package the engine (buildClient + buildServer + prerender) into one
490
591
  * command that emits a full deploy dir, so an app no longer hand-writes build-bun.ts + _worker.ts +
491
- * _routes.json per target. The adapter is imported from `framework.ts` (the edge-bundlable file - never
492
- * `nifra.config.ts`, which pulls in Vite plugins), and the backend from `backend.ts` when present;
493
- * `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/`.
494
596
  */
495
597
  /**
496
598
  * Refuse a `use` the generated server entry cannot import.
497
599
  *
498
- * `nifra build` emits `import { use } from <frameworkFile>` (framework.ts, the edge-bundlable file),
499
- * but `loadApp` prefers `nifra.config.ts` - so an app with BOTH files whose `use` lives only in
500
- * `nifra.config.ts` makes `fw.use` defined while `framework.ts` exports nothing of that name, and the
501
- * build dies later with an opaque bundler error pointing at generated code. Refuse it here with the
502
- * 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).
503
606
  *
504
- * Detected by importing `frameworkFile` and checking the named export, not by comparing paths alone:
505
- * a split app legitimately DEFINES `use` in framework.ts and re-exports it from nifra.config.ts
506
- * (exactly how `adapter` reaches both readers), and a path compare would refuse that correct layout.
507
- * 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.
508
612
  */
509
613
  export async function assertUseIsEdgeExported(
510
614
  use: NifraFramework["use"],
@@ -516,13 +620,65 @@ export async function assertUseIsEdgeExported(
516
620
  if (typeof mod.use === "function") return
517
621
  throw new Error(
518
622
  `[nifra] \`use\` is exported from ${configPath} but not from ${frameworkFile}. ` +
519
- "`nifra build` generates a server entry that imports `use` from framework.ts (the edge-bundled " +
520
- "file), so this build would fail later with an opaque bundler error inside generated code.\n\n" +
521
- " - Define `use` in framework.ts and re-export it from nifra.config.ts " +
522
- '(`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.',
627
+ )
628
+ }
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"\`).`,
523
658
  )
524
659
  }
525
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
+
526
682
  /**
527
683
  * A production build must not ship a workspace-linked package whose `default`-condition artifact is
528
684
  * missing or older than its source. The `"bun": "./src"` / `"default": "./dist"` split means Bun (this
@@ -557,11 +713,16 @@ async function assertFreshWorkspaceDists(cwd: string): Promise<void> {
557
713
  )
558
714
  }
559
715
 
560
- async function buildForTarget(app: LoadedApp, target: string, flags: Flags): Promise<void> {
561
- const { isBuildTarget, renderSizeReport, BUILD_TARGETS } = await import("@nifrajs/web/build")
562
- if (!isBuildTarget(target)) {
563
- throw new Error(`[nifra] unknown --target "${target}". Valid: ${BUILD_TARGETS.join(", ")}.`)
564
- }
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
+ )
565
726
  if (flags.vite && flags.bun) {
566
727
  throw new Error("[nifra] `nifra build` takes `--vite` or `--bun`, not both.")
567
728
  }
@@ -604,17 +765,19 @@ async function buildForTarget(app: LoadedApp, target: string, flags: Flags): Pro
604
765
  const buildTarget = useVite
605
766
  ? (await import("@nifrajs/web/build-vite")).buildTargetVite
606
767
  : (await import("@nifrajs/web/build")).buildTarget
607
- const { routesDir, outDir, cwd, backend } = app
608
- // The server entry must import the adapter from `framework.ts` (edge-safe), not the loaded config.
609
- // `loadApp` guarantees one of them exists; prefer framework.ts so a multi-target app's Vite-plugin
610
- // config never reaches the edge bundle (see load.ts module header).
611
- const frameworkFile = existsSync(resolve(cwd, "framework.ts"))
612
- ? resolve(cwd, "framework.ts")
613
- : existsSync(resolve(cwd, "nifra.config.ts"))
614
- ? resolve(cwd, "nifra.config.ts")
615
- : resolve(cwd, "framework.ts")
616
- 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)
617
778
  await assertUseIsEdgeExported(fw.use, app.configPath, frameworkFile)
779
+ await assertFrameworkOptionsEdgeExported(fw, app.configPath, frameworkFile)
780
+ assertNoShadowedPages(app)
618
781
  // Plugin FORMAT differs by pipeline: the Bun build takes Bun plugins (clientPlugins/serverPlugins); the
619
782
  // Vite build takes the app's Vite plugins (fw.vitePlugins) for BOTH halves. buildTargetWith forwards
620
783
  // whatever it's given straight to the chosen bundler, which casts to its own plugin type.
@@ -634,14 +797,17 @@ async function buildForTarget(app: LoadedApp, target: string, flags: Flags): Pro
634
797
  clientModule: fw.clientModule,
635
798
  adapterImport: frameworkFile,
636
799
  ...(fw.use ? { useImport: frameworkFile } : {}),
800
+ ...frameworkOptionImports(fw, frameworkFile),
637
801
  ...(backend !== undefined && existsSync(backendFile) ? { backendImport: backendFile } : {}),
638
802
  ...plugins,
639
803
  ...(fw.conditions ? { conditions: fw.conditions } : {}),
640
804
  ...(fw.define ? { define: fw.define } : {}),
641
805
  ...(fw.publicDir !== undefined ? { publicDir: fw.publicDir } : {}),
642
806
  ...(fw.publicEnvPrefix !== undefined ? { publicEnvPrefix: fw.publicEnvPrefix } : {}),
807
+ ...(fw.secretExemptions !== undefined ? { secretExemptions: fw.secretExemptions } : {}),
643
808
  ...(fw.cssCodeSplit !== undefined ? { cssCodeSplit: fw.cssCodeSplit } : {}),
644
809
  ...(fw.cssLoading !== undefined ? { cssLoading: fw.cssLoading } : {}),
810
+ ...(fw.clientIp !== undefined ? { clientIp: fw.clientIp } : {}),
645
811
  // The static target needs a built app to drive prerendering - only build it when targeting static.
646
812
  ...(target === "static" ? { prerenderApp: await buildPrerenderApp(app) } : {}),
647
813
  })
@@ -674,12 +840,11 @@ async function buildPrerenderApp(
674
840
  adapter: asAdapter(fw.adapter),
675
841
  manifest: discoverRoutes(routesDir),
676
842
  clientEntry: client.entry,
677
- ...(fw.use ? { use: asUse(fw.use) } : {}),
843
+ ...frameworkWebAppOptions(fw, backend),
678
844
  ...(client.routes ? { routePreload: client.routes } : {}),
679
845
  ...(client.css ? { styles: client.css } : {}),
680
846
  ...(client.routeStyles ? { routeStyles: client.routeStyles } : {}),
681
847
  ...(client.cssLoading !== undefined ? { cssLoading: client.cssLoading } : {}),
682
- ...apiOf(backend),
683
848
  })
684
849
  }
685
850
 
@@ -695,7 +860,7 @@ async function start(app: LoadedApp, flags: Flags): Promise<void> {
695
860
  const serverFile = resolve(app.outDir, "server.js")
696
861
  if (!existsSync(serverFile)) {
697
862
  // A Cloudflare Pages build emits `_worker.js` (a Workers bundle), not a self-hosting `server.js` - so
698
- // `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
699
864
  // actual mismatch and the fix.
700
865
  if (existsSync(resolve(app.outDir, "_worker.js"))) {
701
866
  throw new Error(
@@ -716,13 +881,14 @@ export function parseFlags(args: readonly string[]): Flags {
716
881
  // the SAME uncommon port for `nifra dev` and `nifra start` (DEFAULT_DEV_PORT) so a project's URL is
717
882
  // stable across commands and doesn't collide with the usual 3000/5173/8080 crowd.
718
883
  let port = Number(Bun.env.PORT ?? DEFAULT_DEV_PORT)
719
- let out = "dist"
884
+ let out: string | undefined
720
885
  let poll = Bun.env.CHOKIDAR_USEPOLLING === "1"
721
- let target = "bun"
886
+ let target: string | undefined
722
887
  let report = false
723
888
  let vite = false
724
889
  let bun = false
725
890
  let allowDuplicateIdentity = false
891
+ let noIndicator = false
726
892
  for (let i = 0; i < args.length; i++) {
727
893
  const a = args[i]
728
894
  if ((a === "--port" || a === "-p") && args[i + 1]) port = Number(args[++i])
@@ -733,11 +899,12 @@ export function parseFlags(args: readonly string[]): Flags {
733
899
  else if (a === "--vite") vite = true
734
900
  else if (a === "--bun") bun = true
735
901
  else if (a === "--allow-duplicate-identity") allowDuplicateIdentity = true
902
+ else if (a === "--no-indicator") noIndicator = true
736
903
  }
737
904
  if (!Number.isFinite(port) || port < 0 || port > 65535) {
738
905
  throw new Error(`[nifra] invalid --port: ${port}`)
739
906
  }
740
- return { port, out, poll, target, report, vite, bun, allowDuplicateIdentity }
907
+ return { port, out, poll, target, report, vite, bun, allowDuplicateIdentity, noIndicator }
741
908
  }
742
909
 
743
910
  /**
@@ -778,8 +945,17 @@ function installReflectionExitHint(): (command: string | undefined) => void {
778
945
  }
779
946
  }
780
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
+
781
951
  async function main(): Promise<void> {
782
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
+ : []
783
959
  // Applied before any command runs: a reflecting command imports the app on its first await, and the
784
960
  // app reads its environment at module scope, so the variables must already be in place by then.
785
961
  if (envFiles.length > 0) await applyEnvFiles(process.cwd(), envFiles)
@@ -802,6 +978,19 @@ async function main(): Promise<void> {
802
978
  // `mcp` runs a long-lived stdio server and loads the project lazily per-tool - it must not go through
803
979
  // the eager `loadApp` below (which would fail fast on a project that's API-only / not yet built).
804
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
+ }
805
994
  const { runMcpServer } = await import("./mcp.ts")
806
995
  // `nifra mcp [dir]` - an explicit project directory pins the root (for clients configured outside
807
996
  // the project); otherwise the server resolves it from cwd + the client's MCP roots.
@@ -853,11 +1042,17 @@ async function main(): Promise<void> {
853
1042
  )
854
1043
  return
855
1044
  }
856
- 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}`)
857
1052
  if (catalogSpec?.transports.includes("cli")) {
858
1053
  const markReflecting = installReflectionExitHint()
859
1054
  try {
860
- const input = bindCommandArgv(catalogSpec, argv.slice(1))
1055
+ const input = bindCommandArgv(catalogSpec, argv.slice(dbSub === undefined ? 1 : 2))
861
1056
  markReflecting(command)
862
1057
  const output = await catalogSpec.run(input, { cwd: process.cwd(), cliVersion: CLI_VERSION })
863
1058
  markReflecting(undefined)
@@ -902,7 +1097,7 @@ async function main(): Promise<void> {
902
1097
  if (!ok) process.exitCode = 1
903
1098
  return
904
1099
  }
905
- // `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
906
1101
  // pure file-writing command independent of the app loading, so dispatch it before the eager `loadApp`
907
1102
  // (an existing app might be API-only or not yet built). It always succeeds unless a write throws.
908
1103
  if (command === "init-agents") {
@@ -910,6 +1105,7 @@ async function main(): Promise<void> {
910
1105
  await runInitAgents(process.cwd(), {
911
1106
  json: argv.includes("--json"),
912
1107
  force: argv.includes("--force"),
1108
+ syncMcp: argv.includes("--sync-mcp"),
913
1109
  })
914
1110
  return
915
1111
  }
@@ -930,7 +1126,11 @@ async function main(): Promise<void> {
930
1126
  }
931
1127
  try {
932
1128
  const { runSdk } = await import("./sdk.ts")
933
- 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
+ })
934
1134
  } catch (err) {
935
1135
  console.error(formatCliError(err))
936
1136
  process.exitCode = 1
@@ -951,6 +1151,8 @@ async function main(): Promise<void> {
951
1151
  list: argv.includes("--list"),
952
1152
  verify: !argv.includes("--no-verify"),
953
1153
  allowDowngrade: argv.includes("--allow-downgrade"),
1154
+ exact: argv.includes("--exact"),
1155
+ cliVersion: CLI_VERSION,
954
1156
  })
955
1157
  if (!ok) process.exitCode = 1
956
1158
  } catch (err) {
@@ -969,6 +1171,12 @@ async function main(): Promise<void> {
969
1171
  }
970
1172
  const flags = parseFlags(argv.slice(1))
971
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
+ }
972
1180
  if (command === "dev") await dev(app, flags)
973
1181
  else if (command === "build") await buildForTarget(app, flags.target, flags)
974
1182
  else await start(app, flags)
@@ -1053,6 +1261,7 @@ function isCliCommandToken(value: string): boolean {
1053
1261
  value === "mcp" ||
1054
1262
  value === "init-agents" ||
1055
1263
  value === "upgrade" ||
1264
+ value === "db" ||
1056
1265
  value === "help" ||
1057
1266
  value === "--help" ||
1058
1267
  value === "-h" ||