@nifrajs/cli 3.5.0 → 4.0.2

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