@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
@@ -13,17 +13,30 @@ import type {
13
13
  CapabilityExplainCommandResult,
14
14
  CapabilitySnapshotCommandResult,
15
15
  } from "./capabilities-tool.ts"
16
+ import { cdnCheckSpec } from "./cdn-check.ts"
16
17
  import { type CheckResult, renderCheckReport } from "./check.ts"
17
18
  import type { ContractsLock } from "./contracts.ts"
19
+ import { dbAuditSpec, dbQuerySpec, dbRoleSpec, dbSchemaSpec } from "./db-tool.ts"
20
+ import { errorsSpec, logsSpec } from "./dev-feed-tool.ts"
18
21
  import { type Diagnostic, diagnostic, normalizeSeverity, toSarifLog } from "./diagnostics.ts"
19
22
  import type { DoctorResult } from "./doctor.ts"
23
+ import { type I18nCheckOutput, i18nCheckPassed, renderI18nCheck } from "./i18n-check.ts"
20
24
  import type { VerificationLevelsResult } from "./levels-tool.ts"
21
25
  import type { LoadedApp } from "./load.ts"
22
26
  import type { ManifestEmitCommandResult } from "./manifest-tool.ts"
27
+ import type { LayoutMigrationResult } from "./migrate-layout.ts"
23
28
  import { collectPortResult, type PortResult, renderReport } from "./port.ts"
24
29
  import type { ReplayResult } from "./replay.ts"
25
30
  import { reviewSpec } from "./review.ts"
31
+ import type { RouteTypesReport } from "./route-types.ts"
32
+ import type { SmokeReport } from "./smoke.ts"
26
33
  import type { StylexMigrationResult } from "./stylex-migrate.ts"
34
+ import {
35
+ renderSyncManifestResults,
36
+ type SyncManifestResult,
37
+ syncManifestsOk,
38
+ syncServerManifests,
39
+ } from "./sync-manifest.ts"
27
40
  import {
28
41
  collectProjectWorkGraph,
29
42
  type ProjectWorkGraphResult,
@@ -79,6 +92,11 @@ export interface CommandSpec<Input, Output> {
79
92
  readonly transports: readonly CommandTransport[]
80
93
  readonly stability: CommandStability
81
94
  readonly argv?: CommandArgvBinding<Input>
95
+ /**
96
+ * Input fields only the CLI may set: MCP neither advertises nor accepts them. For a field that acts
97
+ * with the operator's own credentials, such as a signing key, which an agent must not invoke.
98
+ */
99
+ readonly cliOnlyFields?: readonly string[]
82
100
  readonly run: (input: Input, ctx: CommandCtx) => Promise<Output>
83
101
  readonly render: (out: Output, input?: Input) => readonly string[]
84
102
  readonly success?: (out: Output, input: Input) => boolean
@@ -95,6 +113,8 @@ export interface CommandCatalogEntry {
95
113
  readonly outputVersion: number
96
114
  readonly transports: readonly CommandTransport[]
97
115
  readonly stability: CommandStability
116
+ /** {@link CommandSpec.cliOnlyFields}: present in `inputSchema`, absent from the MCP tool's. */
117
+ readonly cliOnlyFields: readonly string[]
98
118
  }
99
119
 
100
120
  function freezeJson<T>(value: T): T {
@@ -116,6 +136,20 @@ export function toCommandCatalogEntry<Input, Output>(
116
136
  outputVersion: spec.output.version,
117
137
  transports: Object.freeze([...spec.transports]),
118
138
  stability: spec.stability,
139
+ cliOnlyFields: Object.freeze([...(spec.cliOnlyFields ?? [])]),
140
+ })
141
+ }
142
+
143
+ /** The input schema a command's MCP tool advertises: its catalog schema without CLI-only fields. */
144
+ export function commandMcpInputSchema(entry: CommandCatalogEntry): CommandJsonSchema {
145
+ const properties = entry.inputSchema.properties
146
+ if (entry.cliOnlyFields.length === 0 || typeof properties !== "object" || properties === null)
147
+ return entry.inputSchema
148
+ return freezeJson({
149
+ ...entry.inputSchema,
150
+ properties: Object.fromEntries(
151
+ Object.entries(properties).filter(([key]) => !entry.cliOnlyFields.includes(key)),
152
+ ),
119
153
  })
120
154
  }
121
155
 
@@ -324,14 +358,38 @@ interface FixInput {
324
358
  readonly dir?: string | undefined
325
359
  }
326
360
 
361
+ /** `migrate layout`, or `migrate --from tailwind --to stylex`; the parser admits only those two. */
327
362
  interface MigrateInput {
328
- readonly from: "tailwind"
329
- readonly to: "stylex"
363
+ readonly kind?: "layout" | undefined
364
+ readonly from?: "tailwind" | undefined
365
+ readonly to?: "stylex" | undefined
330
366
  readonly write?: boolean | undefined
331
367
  readonly json?: boolean | undefined
332
368
  readonly dir?: string | undefined
333
369
  }
334
370
 
371
+ interface TargetInput {
372
+ readonly target?: string | undefined
373
+ readonly json?: boolean | undefined
374
+ readonly dir?: string | undefined
375
+ }
376
+
377
+ interface TargetOutput {
378
+ readonly ok: boolean
379
+ readonly target: string
380
+ /** The target before this run, when it switched one; `undefined` when none was declared. */
381
+ readonly previous?: string | undefined
382
+ readonly changed: boolean
383
+ /** What `nifra build` now emits, and how it deploys. */
384
+ readonly run: string
385
+ }
386
+
387
+ interface TypesInput {
388
+ readonly check?: boolean | undefined
389
+ readonly json?: boolean | undefined
390
+ readonly dir?: string | undefined
391
+ }
392
+
335
393
  interface SnapshotInput {
336
394
  readonly out?: string | undefined
337
395
  readonly json?: boolean | undefined
@@ -376,6 +434,21 @@ interface PortInput {
376
434
  readonly dir?: string | undefined
377
435
  }
378
436
 
437
+ interface SmokeInput {
438
+ readonly fixture?: string | undefined
439
+ readonly inProcess?: boolean | undefined
440
+ readonly json?: boolean | undefined
441
+ readonly dir?: string | undefined
442
+ }
443
+
444
+ interface I18nInput {
445
+ readonly action: "check"
446
+ readonly entry?: string | undefined
447
+ readonly json?: boolean | undefined
448
+ readonly strict?: boolean | undefined
449
+ readonly dir?: string | undefined
450
+ }
451
+
379
452
  interface CheckCommandOutput extends CheckResult {}
380
453
  interface AssureCommandOutput {
381
454
  readonly report?: unknown
@@ -406,7 +479,9 @@ interface FixCommandOutput {
406
479
  readonly failed: readonly { readonly code: string; readonly reason: string }[]
407
480
  readonly diagnostics: readonly unknown[]
408
481
  }
409
- interface MigrateCommandOutput extends StylexMigrationResult {}
482
+ type MigrateCommandOutput =
483
+ | (StylexMigrationResult & { readonly kind?: undefined })
484
+ | (LayoutMigrationResult & { readonly kind: "layout" })
410
485
  interface SnapshotCommandOutput {
411
486
  readonly ok: true
412
487
  readonly file: string
@@ -429,6 +504,12 @@ interface SyncCommandOutput {
429
504
  readonly ok: true
430
505
  readonly results: readonly unknown[]
431
506
  }
507
+ interface SyncManifestCommandOutput {
508
+ readonly ok: boolean
509
+ readonly results: readonly SyncManifestResult[]
510
+ }
511
+
512
+ interface SmokeCommandOutput extends SmokeReport {}
432
513
 
433
514
  const CHECK_SCHEMA = input<CheckInput>(
434
515
  objectSchema({
@@ -648,26 +729,53 @@ const FIX_SCHEMA = input<FixInput>(
648
729
  )
649
730
 
650
731
  const MIGRATE_SCHEMA = input<MigrateInput>(
651
- objectSchema(
652
- {
653
- from: { type: "string", enum: ["tailwind"] },
654
- to: { type: "string", enum: ["stylex"] },
655
- write: { type: "boolean" },
656
- json: { type: "boolean" },
657
- dir: { type: "string" },
658
- },
659
- ["from", "to"],
660
- ),
732
+ objectSchema({
733
+ kind: { type: "string", enum: ["layout"] },
734
+ from: { type: "string", enum: ["tailwind"] },
735
+ to: { type: "string", enum: ["stylex"] },
736
+ write: { type: "boolean" },
737
+ json: { type: "boolean" },
738
+ dir: { type: "string" },
739
+ }),
661
740
  (value) => {
662
741
  const raw = withDir(record(value))
742
+ const flags = {
743
+ ...parseBooleanFlags(raw, ["write", "json"]),
744
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
745
+ }
746
+ const kind = optionalString(raw.kind, "kind")
747
+ if (kind !== undefined) {
748
+ if (kind !== "layout")
749
+ throw new TypeError("the migration must be `layout` (or --from tailwind --to stylex)")
750
+ return { kind, ...flags }
751
+ }
663
752
  const from = optionalString(raw.from, "from")
664
753
  const to = optionalString(raw.to, "to")
665
754
  if (from !== "tailwind") throw new TypeError("from must be tailwind")
666
755
  if (to !== "stylex") throw new TypeError("to must be stylex")
756
+ return { from, to, ...flags }
757
+ },
758
+ )
759
+
760
+ const TARGET_SCHEMA = input<TargetInput>(
761
+ objectSchema({ target: { type: "string" }, json: { type: "boolean" }, dir: { type: "string" } }),
762
+ (value) => {
763
+ const raw = withDir(record(value))
764
+ const target = optionalString(raw.target, "target")
667
765
  return {
668
- from,
669
- to,
670
- ...parseBooleanFlags(raw, ["write", "json"]),
766
+ ...parseBooleanFlags(raw, ["json"]),
767
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
768
+ ...(target === undefined ? {} : { target }),
769
+ }
770
+ },
771
+ )
772
+
773
+ const TYPES_SCHEMA = input<TypesInput>(
774
+ objectSchema({ check: { type: "boolean" }, json: { type: "boolean" }, dir: { type: "string" } }),
775
+ (value) => {
776
+ const raw = withDir(record(value))
777
+ return {
778
+ ...parseBooleanFlags(raw, ["check", "json"]),
671
779
  ...(raw.dir === undefined ? {} : { dir: raw.dir }),
672
780
  }
673
781
  },
@@ -790,6 +898,52 @@ const PORT_SCHEMA = input<PortInput>(
790
898
  },
791
899
  )
792
900
 
901
+ const SMOKE_SCHEMA = input<SmokeInput>(
902
+ objectSchema({
903
+ fixture: { type: "string" },
904
+ inProcess: { type: "boolean" },
905
+ json: { type: "boolean" },
906
+ dir: { type: "string" },
907
+ }),
908
+ (value) => {
909
+ const raw = withDir(record(value))
910
+ return {
911
+ fixture: optionalString(raw.fixture, "fixture"),
912
+ ...parseBooleanFlags(raw, ["inProcess", "json"]),
913
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
914
+ }
915
+ },
916
+ )
917
+
918
+ const I18N_SCHEMA = input<I18nInput>(
919
+ objectSchema(
920
+ {
921
+ action: { type: "string", enum: ["check"] },
922
+ entry: { type: "string" },
923
+ json: { type: "boolean" },
924
+ strict: { type: "boolean" },
925
+ dir: { type: "string" },
926
+ },
927
+ ["action"],
928
+ ),
929
+ (value) => {
930
+ const raw = withDir(record(value))
931
+ if (raw.action !== "check") {
932
+ throw new TypeError(
933
+ raw.action === undefined
934
+ ? "i18n needs an action: nifra i18n check [entry]"
935
+ : `unknown i18n action: ${String(raw.action)} (the one action is check)`,
936
+ )
937
+ }
938
+ return {
939
+ action: "check",
940
+ entry: optionalString(raw.entry, "entry"),
941
+ ...parseBooleanFlags(raw, ["json", "strict"]),
942
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
943
+ }
944
+ },
945
+ )
946
+
793
947
  function loadAppFor(ctx: CommandCtx): Promise<LoadedApp> {
794
948
  if (ctx.loadApp !== undefined) return ctx.loadApp()
795
949
  return import("./load.ts").then(({ loadApp }) => loadApp(ctx.cwd))
@@ -995,6 +1149,8 @@ const manifestSpec: CommandSpec<ManifestInput, ManifestEmitCommandResult | DiffC
995
1149
  output: output({ type: "object" }),
996
1150
  transports: ["cli", "mcp"],
997
1151
  stability: "stable",
1152
+ // The signer is the operator's KMS/HSM callback; an agent signing its own manifest defeats it.
1153
+ cliOnlyFields: ["sign"],
998
1154
  argv: {
999
1155
  positionals: ["action", "before", "after"],
1000
1156
  flags: [
@@ -1136,7 +1292,10 @@ const openApiSpec: CommandSpec<OpenApiInput, OpenApiCommandOutput> = {
1136
1292
  ],
1137
1293
  },
1138
1294
  async run(value, ctx) {
1139
- const app = await loadAppFor(ctx)
1295
+ const app =
1296
+ ctx.loadApp !== undefined
1297
+ ? await ctx.loadApp()
1298
+ : await import("./load.ts").then(({ loadBackendApp }) => loadBackendApp(ctx.cwd))
1140
1299
  const { renderOpenApiWithTypes } = await import("./openapi-tool.ts")
1141
1300
  const format = value.format ?? "json"
1142
1301
  return {
@@ -1252,12 +1411,14 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
1252
1411
 
1253
1412
  const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
1254
1413
  name: "migrate",
1255
- summary: "Migrate safe static Tailwind className utilities to native StyleX props.",
1414
+ summary:
1415
+ "Move an app onto the frontend/backend split (`migrate layout`), or migrate static Tailwind utilities to StyleX.",
1256
1416
  input: MIGRATE_SCHEMA,
1257
1417
  output: output({
1258
1418
  type: "object",
1259
1419
  properties: {
1260
1420
  ok: { type: "boolean" },
1421
+ kind: { type: "string" },
1261
1422
  from: { type: "string" },
1262
1423
  to: { type: "string" },
1263
1424
  write: { type: "boolean" },
@@ -1266,12 +1427,16 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
1266
1427
  written: { type: "array" },
1267
1428
  issues: { type: "array" },
1268
1429
  files: { type: "array" },
1430
+ splits: { type: "array" },
1431
+ moves: { type: "array" },
1432
+ rewritten: { type: "array" },
1269
1433
  },
1270
- required: ["ok", "from", "to", "write", "scanned", "changed", "written", "issues", "files"],
1434
+ required: ["ok", "write", "issues"],
1271
1435
  }),
1272
1436
  transports: ["cli"],
1273
1437
  stability: "stable",
1274
1438
  argv: {
1439
+ positionals: ["kind"],
1275
1440
  flags: [
1276
1441
  { name: "from", field: "from", type: "string" },
1277
1442
  { name: "to", field: "to", type: "string" },
@@ -1281,27 +1446,142 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
1281
1446
  ],
1282
1447
  },
1283
1448
  async run(value, ctx) {
1449
+ const dir = resolve(ctx.cwd, value.dir ?? ".")
1450
+ const write = value.write === undefined ? {} : { write: value.write }
1451
+ if (value.kind === "layout") {
1452
+ const { migrateLayout } = await import("./migrate-layout.ts")
1453
+ return { kind: "layout" as const, ...(await migrateLayout(dir, write)) }
1454
+ }
1284
1455
  const { migrateTailwindToStylex } = await import("./stylex-migrate.ts")
1285
- return migrateTailwindToStylex(resolve(ctx.cwd, value.dir ?? "."), {
1286
- ...(value.write === undefined ? {} : { write: value.write }),
1287
- })
1456
+ return migrateTailwindToStylex(dir, write)
1288
1457
  },
1289
- render: (out) => [
1290
- out.ok
1291
- ? `✓ migrated ${out.changed.length} file${out.changed.length === 1 ? "" : "s"}`
1292
- : `⚠ migrated ${out.changed.length} safe file${out.changed.length === 1 ? "" : "s"}; ${out.issues.length} manual issue${out.issues.length === 1 ? "" : "s"} remain`,
1293
- ` scanned ${out.scanned} source file${out.scanned === 1 ? "" : "s"}`,
1294
- ...(out.write
1295
- ? [` wrote ${out.written.length} file${out.written.length === 1 ? "" : "s"}`]
1296
- : out.changed.length > 0
1297
- ? [" dry run: pass --write to apply the safe changes"]
1458
+ render: (out) => {
1459
+ if (out.kind === "layout") {
1460
+ return [
1461
+ out.ok
1462
+ ? `✓ ${out.splits.length} route file${out.splits.length === 1 ? "" : "s"} split, ${out.moves.length} file${out.moves.length === 1 ? "" : "s"} moved`
1463
+ : `⚠ ${out.issues.length} issue${out.issues.length === 1 ? "" : "s"} need a decision; everything else is planned`,
1464
+ ...out.splits.map(
1465
+ (split) => ` split ${split.file} → ${split.backend} (${split.moved.join(", ")})`,
1466
+ ),
1467
+ ...out.moves.map((move) => ` move ${move.from} → ${move.to}`),
1468
+ ...out.rewritten.map((file) => ` edit ${file} (imports)`),
1469
+ ...out.issues.map((issue) => ` ⚠ ${issue.file}: ${issue.reason}`),
1470
+ ...(out.write ? [] : [" dry run: pass --write to apply"]),
1471
+ ]
1472
+ }
1473
+ return [
1474
+ out.ok
1475
+ ? `✓ migrated ${out.changed.length} file${out.changed.length === 1 ? "" : "s"}`
1476
+ : `⚠ migrated ${out.changed.length} safe file${out.changed.length === 1 ? "" : "s"}; ${out.issues.length} manual issue${out.issues.length === 1 ? "" : "s"} remain`,
1477
+ ` scanned ${out.scanned} source file${out.scanned === 1 ? "" : "s"}`,
1478
+ ...(out.write
1479
+ ? [` wrote ${out.written.length} file${out.written.length === 1 ? "" : "s"}`]
1480
+ : out.changed.length > 0
1481
+ ? [" dry run: pass --write to apply the safe changes"]
1482
+ : []),
1483
+ ...out.issues
1484
+ .slice(0, 30)
1485
+ .map((issue) => ` ${issue.file}:${issue.line} ${issue.token} — ${issue.reason}`),
1486
+ ...(out.issues.length > 30
1487
+ ? [` … ${out.issues.length - 30} more issue${out.issues.length - 30 === 1 ? "" : "s"}`]
1298
1488
  : []),
1299
- ...out.issues
1300
- .slice(0, 30)
1301
- .map((issue) => ` ${issue.file}:${issue.line} ${issue.token} — ${issue.reason}`),
1302
- ...(out.issues.length > 30
1303
- ? [` … ${out.issues.length - 30} more issue${out.issues.length - 30 === 1 ? "" : "s"}`]
1304
- : []),
1489
+ ]
1490
+ },
1491
+ success: (out) => out.ok,
1492
+ }
1493
+
1494
+ const targetSpec: CommandSpec<TargetInput, TargetOutput> = {
1495
+ name: "target",
1496
+ summary:
1497
+ "Show the app's deploy target, or switch it (`nifra target cloudflare`): the `target` `nifra build` emits, kept in nifra.config.ts.",
1498
+ input: TARGET_SCHEMA,
1499
+ output: output({
1500
+ type: "object",
1501
+ properties: {
1502
+ ok: { type: "boolean" },
1503
+ target: { type: "string" },
1504
+ previous: { type: "string" },
1505
+ changed: { type: "boolean" },
1506
+ run: { type: "string" },
1507
+ },
1508
+ required: ["ok", "target", "changed", "run"],
1509
+ }),
1510
+ transports: ["cli"],
1511
+ stability: "stable",
1512
+ argv: {
1513
+ positionals: ["target"],
1514
+ flags: [
1515
+ { name: "json", field: "json", type: "boolean" },
1516
+ { name: "dir", field: "dir", type: "string" },
1517
+ ],
1518
+ },
1519
+ async run(value, ctx) {
1520
+ const dir = resolve(ctx.cwd, value.dir ?? ".")
1521
+ const { parseBuildTarget, planBuildTarget } = await import("@nifrajs/web/build")
1522
+ const { readConfigTarget, writeConfigTarget } = await import("./config-target.ts")
1523
+ const current = readConfigTarget(dir)
1524
+ const target = parseBuildTarget(
1525
+ value.target ?? current ?? "bun",
1526
+ value.target === undefined ? "target in nifra.config.ts" : "target",
1527
+ )
1528
+ const changed = value.target !== undefined && value.target !== current
1529
+ if (changed) writeConfigTarget(dir, target)
1530
+ return {
1531
+ ok: true,
1532
+ target,
1533
+ ...(changed && current !== undefined ? { previous: current } : {}),
1534
+ changed,
1535
+ run: planBuildTarget(target, "dist").run,
1536
+ }
1537
+ },
1538
+ render: (out) => [
1539
+ out.changed
1540
+ ? `✓ target: ${out.target}${out.previous === undefined ? "" : ` (was ${out.previous})`}`
1541
+ : `target: ${out.target}`,
1542
+ ` nifra build → ${out.run}`,
1543
+ ],
1544
+ success: (out) => out.ok,
1545
+ }
1546
+
1547
+ const typesSpec: CommandSpec<TypesInput, RouteTypesReport> = {
1548
+ name: "types",
1549
+ summary:
1550
+ "Generate each route's `./+types` module (params, schema-typed data, typed `api`); `--check` fails when one is stale.",
1551
+ input: TYPES_SCHEMA,
1552
+ output: output({
1553
+ type: "object",
1554
+ properties: {
1555
+ ok: { type: "boolean" },
1556
+ check: { type: "boolean" },
1557
+ written: { type: "array" },
1558
+ removed: { type: "array" },
1559
+ stale: { type: "array" },
1560
+ tsconfig: { type: "string" },
1561
+ },
1562
+ required: ["ok", "check", "written", "removed", "stale"],
1563
+ }),
1564
+ transports: ["cli"],
1565
+ stability: "stable",
1566
+ argv: {
1567
+ flags: [
1568
+ { name: "check", field: "check", type: "boolean" },
1569
+ { name: "json", field: "json", type: "boolean" },
1570
+ { name: "dir", field: "dir", type: "string" },
1571
+ ],
1572
+ },
1573
+ async run(value, ctx) {
1574
+ const { routeTypes } = await import("./route-types.ts")
1575
+ return routeTypes(resolve(ctx.cwd, value.dir ?? "."), { check: value.check === true })
1576
+ },
1577
+ render: (out) => [
1578
+ out.check
1579
+ ? out.ok
1580
+ ? "✓ route types are up to date"
1581
+ : `✖ ${out.stale.length} route type file${out.stale.length === 1 ? " is" : "s are"} stale - run \`nifra types\``
1582
+ : `✓ route types: ${out.written.length} written, ${out.removed.length} removed`,
1583
+ ...out.stale.map((file) => ` stale ${file}`),
1584
+ ...(out.tsconfig === undefined ? [] : [` ⚠ ${out.tsconfig}`]),
1305
1585
  ],
1306
1586
  success: (out) => out.ok,
1307
1587
  }
@@ -1404,7 +1684,7 @@ const contractsSpec: CommandSpec<ContractsInput, ContractsCommandOutput> = {
1404
1684
  success: (out) => out.ok,
1405
1685
  }
1406
1686
 
1407
- const syncManifestSpec: CommandSpec<SyncInput, SyncCommandOutput> = {
1687
+ const syncManifestSpec: CommandSpec<SyncInput, SyncManifestCommandOutput> = {
1408
1688
  name: "sync-manifest",
1409
1689
  summary: "Regenerate generated server-manifest route tables without a full build.",
1410
1690
  input: SYNC_SCHEMA,
@@ -1417,16 +1697,11 @@ const syncManifestSpec: CommandSpec<SyncInput, SyncCommandOutput> = {
1417
1697
  stability: "stable",
1418
1698
  argv: { flags: [{ name: "json", field: "json", type: "boolean" }] },
1419
1699
  async run(_value, ctx) {
1420
- return {
1421
- ok: true,
1422
- results: await (await import("./sync-manifest.ts")).syncServerManifests(ctx.cwd),
1423
- }
1700
+ const results = await syncServerManifests(ctx.cwd)
1701
+ return { ok: syncManifestsOk(results), results }
1424
1702
  },
1425
- render: (out) => [
1426
- out.results.length === 0
1427
- ? "nifra sync-manifest: no generated server-manifest.ts found under this directory."
1428
- : `✓ checked ${out.results.length} generated server manifest${out.results.length === 1 ? "" : "s"}`,
1429
- ],
1703
+ render: (out) => renderSyncManifestResults(out.results),
1704
+ success: (out) => out.ok,
1430
1705
  }
1431
1706
 
1432
1707
  const syncRoutesSpec: CommandSpec<SyncInput, SyncCommandOutput> = {
@@ -1493,6 +1768,34 @@ const replaySpec: CommandSpec<ReplayInput, ReplayResult> = {
1493
1768
  success: (out) => out.ok,
1494
1769
  }
1495
1770
 
1771
+ const smokeSpec: CommandSpec<SmokeInput, SmokeCommandOutput> = {
1772
+ name: "smoke",
1773
+ summary:
1774
+ "Run the declared production SSR, mounted-API, 404, header, auth, contract, and hydration smoke checks.",
1775
+ input: SMOKE_SCHEMA,
1776
+ output: output({ type: "object" }),
1777
+ transports: ["cli", "mcp"],
1778
+ stability: "stable",
1779
+ argv: {
1780
+ flags: [
1781
+ { name: "fixture", field: "fixture", type: "string" },
1782
+ { name: "in-process", field: "inProcess", type: "boolean" },
1783
+ { name: "json", field: "json", type: "boolean" },
1784
+ ],
1785
+ },
1786
+ async run(value, ctx) {
1787
+ const { loadSmokeFixture, runSmoke } = await import("./smoke.ts")
1788
+ const fixture = await loadSmokeFixture(ctx.cwd, value.fixture)
1789
+ return runSmoke(fixture, { inProcess: value.inProcess === true })
1790
+ },
1791
+ render: (out) =>
1792
+ out.checks.map(
1793
+ (check) =>
1794
+ `${check.status === "pass" ? "✓" : check.status === "skip" ? "•" : "✖"} ${check.id}${check.message === undefined ? "" : ` - ${check.message}`}`,
1795
+ ),
1796
+ success: (out) => out.ok,
1797
+ }
1798
+
1496
1799
  const portSpec: CommandSpec<PortInput, PortResult> = {
1497
1800
  name: "port",
1498
1801
  summary:
@@ -1522,7 +1825,7 @@ const portSpec: CommandSpec<PortInput, PortResult> = {
1522
1825
  return [
1523
1826
  report,
1524
1827
  "",
1525
- "[nifra] --ci needs a deploy target to gate against, and none was detected. Pass --target <bun|node|deno|cf-pages|vercel>.",
1828
+ "[nifra] --ci needs a deploy target to gate against, and none was detected. Pass --target <bun|node|deno|cloudflare|vercel>.",
1526
1829
  ]
1527
1830
  return [report]
1528
1831
  },
@@ -1535,6 +1838,33 @@ const portSpec: CommandSpec<PortInput, PortResult> = {
1535
1838
  json: (out) => out.json,
1536
1839
  }
1537
1840
 
1841
+ // CLI only: it imports app code (the catalogs), which a long-lived MCP server process would cache.
1842
+ const i18nSpec: CommandSpec<I18nInput, I18nCheckOutput> = {
1843
+ name: "i18n",
1844
+ summary:
1845
+ "Check i18n catalogs (imports the module exporting `locales` and `catalogs`): coverage, missing and unused keys, ICU syntax, placeholder and tag parity, plural cases, script purity, untranslated messages.",
1846
+ input: I18N_SCHEMA,
1847
+ output: output({ type: "object" }),
1848
+ transports: ["cli"],
1849
+ stability: "stable",
1850
+ argv: {
1851
+ positionals: ["action", "entry"],
1852
+ flags: [
1853
+ { name: "json", field: "json", type: "boolean" },
1854
+ { name: "strict", field: "strict", type: "boolean" },
1855
+ ],
1856
+ },
1857
+ async run(value, ctx) {
1858
+ const { runI18nCheck } = await import("./i18n-check.ts")
1859
+ return runI18nCheck(value.dir === undefined ? ctx.cwd : resolve(ctx.cwd, value.dir), {
1860
+ entry: value.entry,
1861
+ })
1862
+ },
1863
+ render: (out) => renderI18nCheck(out),
1864
+ success: (out, input) => i18nCheckPassed(out, input.strict === true),
1865
+ json: (out) => out,
1866
+ }
1867
+
1538
1868
  export const commandSpecs = Object.freeze([
1539
1869
  checkSpec,
1540
1870
  reviewSpec,
@@ -1548,6 +1878,8 @@ export const commandSpecs = Object.freeze([
1548
1878
  doctorSpec,
1549
1879
  fixSpec,
1550
1880
  migrateSpec,
1881
+ targetSpec,
1882
+ typesSpec,
1551
1883
  snapshotSpec,
1552
1884
  diffSpec,
1553
1885
  contractsSpec,
@@ -1555,7 +1887,16 @@ export const commandSpecs = Object.freeze([
1555
1887
  syncRoutesSpec,
1556
1888
  proveSpec,
1557
1889
  replaySpec,
1890
+ smokeSpec,
1558
1891
  portSpec,
1892
+ i18nSpec,
1893
+ errorsSpec,
1894
+ logsSpec,
1895
+ cdnCheckSpec,
1896
+ dbSchemaSpec,
1897
+ dbQuerySpec,
1898
+ dbRoleSpec,
1899
+ dbAuditSpec,
1559
1900
  ] as const)
1560
1901
 
1561
1902
  const commandByName = new Map(commandSpecs.map((spec) => [spec.name, spec]))
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The app's deploy target as `nifra.config.ts` declares it (`export const target = "bun"`), read and
3
+ * rewritten as text: `nifra port` and `nifra doctor` never run app code, and `nifra target <t>` edits
4
+ * one line of a file the developer owns.
5
+ */
6
+ import { existsSync, readFileSync, writeFileSync } from "node:fs"
7
+ import { join } from "node:path"
8
+ import { CONFIG_FILE } from "./app-files.ts"
9
+ import { stripComments } from "./check-scan.ts"
10
+
11
+ const TARGET_EXPORT = /\bexport\s+const\s+target\b[^=\n]*=\s*(["'])([^"'\n]*)\1/
12
+
13
+ /** The `target` string `nifra.config.ts` exports, or `undefined` when it exports none. */
14
+ export function readConfigTarget(appRoot: string): string | undefined {
15
+ const path = join(appRoot, CONFIG_FILE)
16
+ if (!existsSync(path)) return undefined
17
+ return TARGET_EXPORT.exec(stripComments(readFileSync(path, "utf8")))?.[2]
18
+ }
19
+
20
+ /**
21
+ * Point `nifra.config.ts` at `target`: rewrite its `target` export, or append one. Returns the previous
22
+ * value. Throws when the app has no `nifra.config.ts`.
23
+ */
24
+ export function writeConfigTarget(appRoot: string, target: string): string | undefined {
25
+ const path = join(appRoot, CONFIG_FILE)
26
+ if (!existsSync(path)) {
27
+ throw new Error(
28
+ `[nifra] no ${CONFIG_FILE} in ${appRoot} - \`nifra target\` sets its \`target\``,
29
+ )
30
+ }
31
+ const text = readFileSync(path, "utf8")
32
+ // Matched on the comment-blanked text, whose offsets are the original's, so a commented-out
33
+ // `target` line is never the one rewritten.
34
+ const found = TARGET_EXPORT.exec(stripComments(text))
35
+ const previous = found?.[2]
36
+ let next: string
37
+ if (found === null) {
38
+ next = `${text.replace(/\n*$/, "\n")}\n// The deploy target \`nifra build\` emits; \`nifra target <t>\` switches it.\nexport const target = "${target}"\n`
39
+ } else {
40
+ // Only the quoted value changes, so a type annotation on the export survives.
41
+ const end = found.index + found[0].length
42
+ const start = end - (previous ?? "").length - 2
43
+ next = `${text.slice(0, start)}"${target}"${text.slice(end)}`
44
+ }
45
+ writeFileSync(path, next)
46
+ return previous
47
+ }