@nifrajs/cli 4.0.2 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (364) hide show
  1. package/README.md +10 -2
  2. package/dist/agent-files.d.ts +107 -0
  3. package/dist/agent-files.d.ts.map +1 -0
  4. package/dist/agent-files.js +162 -0
  5. package/dist/agent-files.js.map +1 -0
  6. package/dist/app-files.d.ts +6 -0
  7. package/dist/app-files.d.ts.map +1 -1
  8. package/dist/app-files.js +27 -2
  9. package/dist/app-files.js.map +1 -1
  10. package/dist/assure-hydration.d.ts.map +1 -1
  11. package/dist/assure-hydration.js +9 -2
  12. package/dist/assure-hydration.js.map +1 -1
  13. package/dist/assure.d.ts +12 -2
  14. package/dist/assure.d.ts.map +1 -1
  15. package/dist/assure.js +59 -8
  16. package/dist/assure.js.map +1 -1
  17. package/dist/capabilities-tool.d.ts.map +1 -1
  18. package/dist/capabilities-tool.js +36 -9
  19. package/dist/capabilities-tool.js.map +1 -1
  20. package/dist/check-diagnostics.d.ts +22 -2
  21. package/dist/check-diagnostics.d.ts.map +1 -1
  22. package/dist/check-diagnostics.js +54 -7
  23. package/dist/check-diagnostics.js.map +1 -1
  24. package/dist/check-scan.d.ts +83 -24
  25. package/dist/check-scan.d.ts.map +1 -1
  26. package/dist/check-scan.js +1516 -318
  27. package/dist/check-scan.js.map +1 -1
  28. package/dist/check.d.ts +2 -2
  29. package/dist/check.d.ts.map +1 -1
  30. package/dist/check.js +203 -70
  31. package/dist/check.js.map +1 -1
  32. package/dist/cli.d.ts +9 -0
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +215 -50
  35. package/dist/cli.js.map +1 -1
  36. package/dist/client-module.d.ts +20 -0
  37. package/dist/client-module.d.ts.map +1 -0
  38. package/dist/client-module.js +73 -0
  39. package/dist/client-module.js.map +1 -0
  40. package/dist/command-catalog.d.ts +24 -5
  41. package/dist/command-catalog.d.ts.map +1 -1
  42. package/dist/command-catalog.js +194 -45
  43. package/dist/command-catalog.js.map +1 -1
  44. package/dist/component-check.d.ts +13 -0
  45. package/dist/component-check.d.ts.map +1 -0
  46. package/dist/component-check.js +59 -0
  47. package/dist/component-check.js.map +1 -0
  48. package/dist/contract-proof.d.ts +1 -1
  49. package/dist/contract-proof.d.ts.map +1 -1
  50. package/dist/contract-proof.js +2 -24
  51. package/dist/contract-proof.js.map +1 -1
  52. package/dist/contracts.d.ts +19 -0
  53. package/dist/contracts.d.ts.map +1 -1
  54. package/dist/contracts.js +50 -3
  55. package/dist/contracts.js.map +1 -1
  56. package/dist/db-tool.d.ts.map +1 -1
  57. package/dist/db-tool.js +4 -3
  58. package/dist/db-tool.js.map +1 -1
  59. package/dist/dev-bun-config.d.ts +5 -1
  60. package/dist/dev-bun-config.d.ts.map +1 -1
  61. package/dist/dev-bun-config.js +44 -11
  62. package/dist/dev-bun-config.js.map +1 -1
  63. package/dist/dev-feed-tool.d.ts +18 -1
  64. package/dist/dev-feed-tool.d.ts.map +1 -1
  65. package/dist/dev-feed-tool.js +93 -0
  66. package/dist/dev-feed-tool.js.map +1 -1
  67. package/dist/diagnostics.d.ts +3 -0
  68. package/dist/diagnostics.d.ts.map +1 -1
  69. package/dist/diagnostics.js.map +1 -1
  70. package/dist/diff-tool.d.ts +9 -8
  71. package/dist/diff-tool.d.ts.map +1 -1
  72. package/dist/diff-tool.js +28 -24
  73. package/dist/diff-tool.js.map +1 -1
  74. package/dist/doctor.d.ts +13 -0
  75. package/dist/doctor.d.ts.map +1 -1
  76. package/dist/doctor.js +23 -15
  77. package/dist/doctor.js.map +1 -1
  78. package/dist/env-file.d.ts +6 -0
  79. package/dist/env-file.d.ts.map +1 -1
  80. package/dist/env-file.js +6 -0
  81. package/dist/env-file.js.map +1 -1
  82. package/dist/fix-recipes.d.ts +2 -0
  83. package/dist/fix-recipes.d.ts.map +1 -1
  84. package/dist/fix-recipes.js +102 -2
  85. package/dist/fix-recipes.js.map +1 -1
  86. package/dist/hand-built-manifest.d.ts +27 -0
  87. package/dist/hand-built-manifest.d.ts.map +1 -0
  88. package/dist/hand-built-manifest.js +124 -0
  89. package/dist/hand-built-manifest.js.map +1 -0
  90. package/dist/hooks-report.d.ts +23 -0
  91. package/dist/hooks-report.d.ts.map +1 -0
  92. package/dist/hooks-report.js +104 -0
  93. package/dist/hooks-report.js.map +1 -0
  94. package/dist/init-agents.d.ts +18 -2
  95. package/dist/init-agents.d.ts.map +1 -1
  96. package/dist/init-agents.js +20 -13
  97. package/dist/init-agents.js.map +1 -1
  98. package/dist/internal/config-paths.d.ts +16 -0
  99. package/dist/internal/config-paths.d.ts.map +1 -0
  100. package/dist/internal/config-paths.js +124 -0
  101. package/dist/internal/config-paths.js.map +1 -0
  102. package/dist/internal/installed-package.d.ts +4 -0
  103. package/dist/internal/installed-package.d.ts.map +1 -0
  104. package/dist/internal/installed-package.js +17 -0
  105. package/dist/internal/installed-package.js.map +1 -0
  106. package/dist/internal/nifra-project.d.ts +41 -0
  107. package/dist/internal/nifra-project.d.ts.map +1 -0
  108. package/dist/internal/nifra-project.js +183 -0
  109. package/dist/internal/nifra-project.js.map +1 -0
  110. package/dist/internal/project-specifiers.d.ts +29 -0
  111. package/dist/internal/project-specifiers.d.ts.map +1 -0
  112. package/dist/internal/project-specifiers.js +435 -0
  113. package/dist/internal/project-specifiers.js.map +1 -0
  114. package/dist/internal/script-kind.d.ts.map +1 -1
  115. package/dist/internal/script-kind.js +2 -1
  116. package/dist/internal/script-kind.js.map +1 -1
  117. package/dist/internal/source-facts.d.ts +8 -1
  118. package/dist/internal/source-facts.d.ts.map +1 -1
  119. package/dist/internal/source-facts.js +22 -5
  120. package/dist/internal/source-facts.js.map +1 -1
  121. package/dist/internal/typescript-import.d.ts +1 -1
  122. package/dist/internal/typescript-import.d.ts.map +1 -1
  123. package/dist/internal/typescript-import.js +10 -6
  124. package/dist/internal/typescript-import.js.map +1 -1
  125. package/dist/introspect.d.ts +15 -6
  126. package/dist/introspect.d.ts.map +1 -1
  127. package/dist/introspect.js +87 -25
  128. package/dist/introspect.js.map +1 -1
  129. package/dist/levels-tool.d.ts.map +1 -1
  130. package/dist/levels-tool.js +98 -23
  131. package/dist/levels-tool.js.map +1 -1
  132. package/dist/load.d.ts +33 -10
  133. package/dist/load.d.ts.map +1 -1
  134. package/dist/load.js +60 -10
  135. package/dist/load.js.map +1 -1
  136. package/dist/mcp-context.d.ts +5 -5
  137. package/dist/mcp-context.d.ts.map +1 -1
  138. package/dist/mcp-context.js +8 -23
  139. package/dist/mcp-context.js.map +1 -1
  140. package/dist/mcp-delegate.d.ts +2 -2
  141. package/dist/mcp-delegate.d.ts.map +1 -1
  142. package/dist/mcp-delegate.js +19 -7
  143. package/dist/mcp-delegate.js.map +1 -1
  144. package/dist/mcp-exec.d.ts +13 -4
  145. package/dist/mcp-exec.d.ts.map +1 -1
  146. package/dist/mcp-exec.js +56 -14
  147. package/dist/mcp-exec.js.map +1 -1
  148. package/dist/mcp-http.js +1 -1
  149. package/dist/mcp-io.d.ts +30 -0
  150. package/dist/mcp-io.d.ts.map +1 -1
  151. package/dist/mcp-io.js +63 -0
  152. package/dist/mcp-io.js.map +1 -1
  153. package/dist/mcp-isolate.d.ts.map +1 -1
  154. package/dist/mcp-isolate.js +3 -2
  155. package/dist/mcp-isolate.js.map +1 -1
  156. package/dist/mcp-path.d.ts +2 -1
  157. package/dist/mcp-path.d.ts.map +1 -1
  158. package/dist/mcp-path.js +9 -24
  159. package/dist/mcp-path.js.map +1 -1
  160. package/dist/mcp-project-child.js +1 -1
  161. package/dist/mcp-project-child.js.map +1 -1
  162. package/dist/mcp-root.d.ts.map +1 -1
  163. package/dist/mcp-root.js +50 -10
  164. package/dist/mcp-root.js.map +1 -1
  165. package/dist/mcp-ws.d.ts.map +1 -1
  166. package/dist/mcp-ws.js +3 -1
  167. package/dist/mcp-ws.js.map +1 -1
  168. package/dist/mcp.d.ts.map +1 -1
  169. package/dist/mcp.js +48 -19
  170. package/dist/mcp.js.map +1 -1
  171. package/dist/migrate-layout.d.ts +47 -1
  172. package/dist/migrate-layout.d.ts.map +1 -1
  173. package/dist/migrate-layout.js +1972 -170
  174. package/dist/migrate-layout.js.map +1 -1
  175. package/dist/output-schemas.d.ts +128 -0
  176. package/dist/output-schemas.d.ts.map +1 -0
  177. package/dist/output-schemas.js +975 -0
  178. package/dist/output-schemas.js.map +1 -0
  179. package/dist/port.d.ts.map +1 -1
  180. package/dist/port.js +3 -2
  181. package/dist/port.js.map +1 -1
  182. package/dist/project-facts.d.ts +15 -4
  183. package/dist/project-facts.d.ts.map +1 -1
  184. package/dist/project-facts.js +1 -0
  185. package/dist/project-facts.js.map +1 -1
  186. package/dist/project-path.d.ts +6 -0
  187. package/dist/project-path.d.ts.map +1 -1
  188. package/dist/project-path.js +48 -14
  189. package/dist/project-path.js.map +1 -1
  190. package/dist/recipes/5.0.0.d.ts +11 -0
  191. package/dist/recipes/5.0.0.d.ts.map +1 -0
  192. package/dist/recipes/5.0.0.js +34 -0
  193. package/dist/recipes/5.0.0.js.map +1 -0
  194. package/dist/recipes/index.d.ts +7 -0
  195. package/dist/recipes/index.d.ts.map +1 -1
  196. package/dist/recipes/index.js +2 -0
  197. package/dist/recipes/index.js.map +1 -1
  198. package/dist/release-verification.d.ts +5 -0
  199. package/dist/release-verification.d.ts.map +1 -1
  200. package/dist/release-verification.js +14 -0
  201. package/dist/release-verification.js.map +1 -1
  202. package/dist/replay.d.ts.map +1 -1
  203. package/dist/replay.js +12 -3
  204. package/dist/replay.js.map +1 -1
  205. package/dist/review.d.ts +3 -1
  206. package/dist/review.d.ts.map +1 -1
  207. package/dist/review.js +43 -7
  208. package/dist/review.js.map +1 -1
  209. package/dist/rules/codes.d.ts +12 -0
  210. package/dist/rules/codes.d.ts.map +1 -1
  211. package/dist/rules/codes.js +12 -0
  212. package/dist/rules/codes.js.map +1 -1
  213. package/dist/rules/comment-markers.d.ts +16 -7
  214. package/dist/rules/comment-markers.d.ts.map +1 -1
  215. package/dist/rules/comment-markers.js +58 -19
  216. package/dist/rules/comment-markers.js.map +1 -1
  217. package/dist/rules/contract-visibility.d.ts +3 -0
  218. package/dist/rules/contract-visibility.d.ts.map +1 -0
  219. package/dist/rules/contract-visibility.js +337 -0
  220. package/dist/rules/contract-visibility.js.map +1 -0
  221. package/dist/rules/data-guard.d.ts +13 -0
  222. package/dist/rules/data-guard.d.ts.map +1 -1
  223. package/dist/rules/data-guard.js +356 -37
  224. package/dist/rules/data-guard.js.map +1 -1
  225. package/dist/rules/layout.d.ts +4 -0
  226. package/dist/rules/layout.d.ts.map +1 -0
  227. package/dist/rules/layout.js +19 -0
  228. package/dist/rules/layout.js.map +1 -0
  229. package/dist/rules/legacy.d.ts.map +1 -1
  230. package/dist/rules/legacy.js +178 -44
  231. package/dist/rules/legacy.js.map +1 -1
  232. package/dist/rules/pages.d.ts +50 -0
  233. package/dist/rules/pages.d.ts.map +1 -1
  234. package/dist/rules/pages.js +436 -4
  235. package/dist/rules/pages.js.map +1 -1
  236. package/dist/rules/routes.d.ts +6 -0
  237. package/dist/rules/routes.d.ts.map +1 -1
  238. package/dist/rules/routes.js +111 -16
  239. package/dist/rules/routes.js.map +1 -1
  240. package/dist/rules/secrets.js +2 -2
  241. package/dist/rules/secrets.js.map +1 -1
  242. package/dist/rules/security.d.ts +13 -1
  243. package/dist/rules/security.d.ts.map +1 -1
  244. package/dist/rules/security.js +313 -69
  245. package/dist/rules/security.js.map +1 -1
  246. package/dist/rules/zones.d.ts +13 -0
  247. package/dist/rules/zones.d.ts.map +1 -1
  248. package/dist/rules/zones.js +218 -44
  249. package/dist/rules/zones.js.map +1 -1
  250. package/dist/scaffold.d.ts.map +1 -1
  251. package/dist/scaffold.js +6 -4
  252. package/dist/scaffold.js.map +1 -1
  253. package/dist/sdk.d.ts.map +1 -1
  254. package/dist/sdk.js +14 -5
  255. package/dist/sdk.js.map +1 -1
  256. package/dist/stylex-migrate.d.ts.map +1 -1
  257. package/dist/stylex-migrate.js +290 -24
  258. package/dist/stylex-migrate.js.map +1 -1
  259. package/dist/test-tool.d.ts.map +1 -1
  260. package/dist/test-tool.js +6 -3
  261. package/dist/test-tool.js.map +1 -1
  262. package/dist/typecheck-coverage.d.ts +20 -0
  263. package/dist/typecheck-coverage.d.ts.map +1 -0
  264. package/dist/typecheck-coverage.js +129 -0
  265. package/dist/typecheck-coverage.js.map +1 -0
  266. package/dist/upgrade.d.ts +84 -0
  267. package/dist/upgrade.d.ts.map +1 -1
  268. package/dist/upgrade.js +498 -78
  269. package/dist/upgrade.js.map +1 -1
  270. package/dist/verification-plan.d.ts.map +1 -1
  271. package/dist/verification-plan.js +16 -2
  272. package/dist/verification-plan.js.map +1 -1
  273. package/dist/version-drift.d.ts +22 -0
  274. package/dist/version-drift.d.ts.map +1 -0
  275. package/dist/version-drift.js +94 -0
  276. package/dist/version-drift.js.map +1 -0
  277. package/dist/web-app-options.d.ts +1 -1
  278. package/dist/web-app-options.d.ts.map +1 -1
  279. package/dist/web-app-options.js +1 -1
  280. package/dist/web-app-options.js.map +1 -1
  281. package/dist/work-graph.d.ts.map +1 -1
  282. package/dist/work-graph.js +20 -8
  283. package/dist/work-graph.js.map +1 -1
  284. package/docs/examples.json +55 -7
  285. package/docs/llms-full.txt +2043 -222
  286. package/docs/types.json +1573 -246
  287. package/package.json +11 -12
  288. package/src/agent-files.ts +203 -0
  289. package/src/app-files.ts +34 -4
  290. package/src/assure-hydration.ts +18 -2
  291. package/src/assure.ts +88 -10
  292. package/src/capabilities-tool.ts +47 -9
  293. package/src/check-diagnostics.ts +75 -9
  294. package/src/check-scan.ts +1816 -377
  295. package/src/check.ts +258 -81
  296. package/src/cli.ts +242 -37
  297. package/src/client-module.ts +97 -0
  298. package/src/command-catalog.ts +237 -52
  299. package/src/component-check.ts +79 -0
  300. package/src/contract-proof.ts +3 -25
  301. package/src/contracts.ts +60 -3
  302. package/src/db-tool.ts +5 -2
  303. package/src/dev-bun-config.ts +51 -10
  304. package/src/dev-feed-tool.ts +112 -1
  305. package/src/diagnostics.ts +3 -0
  306. package/src/diff-tool.ts +33 -26
  307. package/src/doctor.ts +43 -21
  308. package/src/env-file.ts +7 -0
  309. package/src/fix-recipes.ts +116 -2
  310. package/src/hand-built-manifest.ts +144 -0
  311. package/src/hooks-report.ts +135 -0
  312. package/src/init-agents.ts +21 -14
  313. package/src/internal/config-paths.ts +154 -0
  314. package/src/internal/installed-package.ts +15 -0
  315. package/src/internal/nifra-project.ts +191 -0
  316. package/src/internal/project-specifiers.ts +477 -0
  317. package/src/internal/script-kind.ts +2 -1
  318. package/src/internal/source-facts.ts +48 -5
  319. package/src/internal/typescript-import.ts +15 -6
  320. package/src/introspect.ts +104 -28
  321. package/src/levels-tool.ts +124 -29
  322. package/src/load.ts +107 -17
  323. package/src/mcp-context.ts +12 -25
  324. package/src/mcp-delegate.ts +22 -10
  325. package/src/mcp-exec.ts +68 -18
  326. package/src/mcp-http.ts +1 -1
  327. package/src/mcp-io.ts +74 -0
  328. package/src/mcp-isolate.ts +9 -2
  329. package/src/mcp-path.ts +8 -22
  330. package/src/mcp-project-child.ts +1 -1
  331. package/src/mcp-root.ts +58 -8
  332. package/src/mcp-ws.ts +4 -2
  333. package/src/mcp.ts +56 -22
  334. package/src/migrate-layout.ts +2257 -176
  335. package/src/output-schemas.ts +1240 -0
  336. package/src/port.ts +3 -2
  337. package/src/project-facts.ts +17 -3
  338. package/src/project-path.ts +44 -13
  339. package/src/recipes/5.0.0.ts +35 -0
  340. package/src/recipes/index.ts +10 -0
  341. package/src/release-verification.ts +20 -0
  342. package/src/replay.ts +14 -3
  343. package/src/review.ts +44 -10
  344. package/src/rules/codes.ts +12 -0
  345. package/src/rules/comment-markers.ts +57 -21
  346. package/src/rules/contract-visibility.ts +383 -0
  347. package/src/rules/data-guard.ts +391 -42
  348. package/src/rules/layout.ts +22 -0
  349. package/src/rules/legacy.ts +206 -52
  350. package/src/rules/pages.ts +481 -4
  351. package/src/rules/routes.ts +124 -16
  352. package/src/rules/secrets.ts +2 -2
  353. package/src/rules/security.ts +391 -72
  354. package/src/rules/zones.ts +232 -42
  355. package/src/scaffold.ts +6 -4
  356. package/src/sdk.ts +14 -5
  357. package/src/stylex-migrate.ts +251 -24
  358. package/src/test-tool.ts +12 -3
  359. package/src/typecheck-coverage.ts +134 -0
  360. package/src/upgrade.ts +576 -72
  361. package/src/verification-plan.ts +33 -2
  362. package/src/version-drift.ts +118 -0
  363. package/src/web-app-options.ts +2 -2
  364. package/src/work-graph.ts +17 -12
@@ -17,14 +17,15 @@ import { cdnCheckSpec } from "./cdn-check.ts"
17
17
  import { type CheckResult, renderCheckReport } from "./check.ts"
18
18
  import type { ContractsLock } from "./contracts.ts"
19
19
  import { dbAuditSpec, dbQuerySpec, dbRoleSpec, dbSchemaSpec } from "./db-tool.ts"
20
- import { errorsSpec, logsSpec } from "./dev-feed-tool.ts"
20
+ import { errorsSpec, explainSpec, logsSpec } from "./dev-feed-tool.ts"
21
21
  import { type Diagnostic, diagnostic, normalizeSeverity, toSarifLog } from "./diagnostics.ts"
22
22
  import type { DoctorResult } from "./doctor.ts"
23
23
  import { type I18nCheckOutput, i18nCheckPassed, renderI18nCheck } from "./i18n-check.ts"
24
24
  import type { VerificationLevelsResult } from "./levels-tool.ts"
25
- import type { LoadedApp } from "./load.ts"
25
+ import type { AppLoader, IntrospectedApp } from "./load.ts"
26
26
  import type { ManifestEmitCommandResult } from "./manifest-tool.ts"
27
27
  import type { LayoutMigrationResult } from "./migrate-layout.ts"
28
+ import type { OutputSchemaStatus, OutputSchemasReport } from "./output-schemas.ts"
28
29
  import { collectPortResult, type PortResult, renderReport } from "./port.ts"
29
30
  import type { ReplayResult } from "./replay.ts"
30
31
  import { reviewSpec } from "./review.ts"
@@ -80,8 +81,8 @@ export interface CommandCtx {
80
81
  readonly signal?: AbortSignal
81
82
  readonly progress?: (message: string) => void
82
83
  readonly cliVersion?: string
83
- /** Adapter-provided cache for commands that need the loaded web app. */
84
- readonly loadApp?: () => Promise<LoadedApp>
84
+ /** Adapter-provided cache for commands that need the loaded app. */
85
+ readonly loadApp?: AppLoader
85
86
  }
86
87
 
87
88
  export interface CommandSpec<Input, Output> {
@@ -328,6 +329,8 @@ interface RoutesInput {
328
329
  readonly json?: boolean | undefined
329
330
  readonly graph?: boolean | undefined
330
331
  readonly modes?: boolean | undefined
332
+ readonly hooks?: boolean | undefined
333
+ readonly since?: string | undefined
331
334
  readonly target?: string | undefined
332
335
  readonly dir?: string | undefined
333
336
  }
@@ -364,6 +367,8 @@ interface MigrateInput {
364
367
  readonly from?: "tailwind" | undefined
365
368
  readonly to?: "stylex" | undefined
366
369
  readonly write?: boolean | undefined
370
+ /** `migrate layout`: list the folders modules would leave in package.json zones instead. */
371
+ readonly zones?: boolean | undefined
367
372
  readonly json?: boolean | undefined
368
373
  readonly dir?: string | undefined
369
374
  }
@@ -390,6 +395,13 @@ interface TypesInput {
390
395
  readonly dir?: string | undefined
391
396
  }
392
397
 
398
+ interface SchemasInput {
399
+ readonly write?: boolean | undefined
400
+ readonly check?: boolean | undefined
401
+ readonly json?: boolean | undefined
402
+ readonly dir?: string | undefined
403
+ }
404
+
393
405
  interface SnapshotInput {
394
406
  readonly out?: string | undefined
395
407
  readonly json?: boolean | undefined
@@ -458,7 +470,7 @@ interface AssureCommandOutput {
458
470
  interface LevelsCommandOutput extends VerificationLevelsResult {}
459
471
  interface RoutesCommandOutput {
460
472
  readonly ok: boolean
461
- readonly mode: "default" | "graph" | "modes"
473
+ readonly mode: "default" | "graph" | "modes" | "hooks"
462
474
  readonly data: unknown
463
475
  readonly text: string
464
476
  }
@@ -477,6 +489,8 @@ interface FixCommandOutput {
477
489
  /** Recipes that refused, with the reason. A recipe that cannot act says so here instead of
478
490
  * aborting the run or - worse - reporting nothing at all. */
479
491
  readonly failed: readonly { readonly code: string; readonly reason: string }[]
492
+ /** Findings whose recipe runs only when its code is named, because its change needs review. */
493
+ readonly skipped: readonly { readonly code: string; readonly reason: string }[]
480
494
  readonly diagnostics: readonly unknown[]
481
495
  }
482
496
  type MigrateCommandOutput =
@@ -493,6 +507,9 @@ interface DiffCommandOutput {
493
507
  readonly hasBreaking: boolean
494
508
  readonly changes: readonly unknown[]
495
509
  }
510
+ interface RouteDiffCommandOutput extends DiffCommandOutput {
511
+ readonly text: string
512
+ }
496
513
  interface ContractsCommandOutput {
497
514
  readonly ok: boolean
498
515
  readonly lock?: ContractsLock
@@ -647,15 +664,20 @@ const ROUTES_SCHEMA = input<RoutesInput>(
647
664
  json: { type: "boolean" },
648
665
  graph: { type: "boolean" },
649
666
  modes: { type: "boolean" },
667
+ hooks: { type: "boolean" },
668
+ since: { type: "string" },
650
669
  target: { type: "string" },
651
670
  dir: { type: "string" },
652
671
  }),
653
672
  (value) => {
654
673
  const raw = withDir(record(value))
655
- if (raw.graph === true && raw.modes === true)
656
- throw new TypeError("graph and modes cannot be combined")
674
+ if ([raw.graph, raw.modes, raw.hooks].filter((flag) => flag === true).length > 1)
675
+ throw new TypeError("graph, modes and hooks cannot be combined")
676
+ if (raw.since !== undefined && raw.hooks !== true)
677
+ throw new TypeError("since compares hook reports and needs hooks")
657
678
  return {
658
- ...parseBooleanFlags(raw, ["json", "graph", "modes"]),
679
+ ...parseBooleanFlags(raw, ["json", "graph", "modes", "hooks"]),
680
+ since: optionalString(raw.since, "since"),
659
681
  target: optionalString(raw.target, "target"),
660
682
  ...(raw.dir === undefined ? {} : { dir: raw.dir }),
661
683
  }
@@ -734,13 +756,14 @@ const MIGRATE_SCHEMA = input<MigrateInput>(
734
756
  from: { type: "string", enum: ["tailwind"] },
735
757
  to: { type: "string", enum: ["stylex"] },
736
758
  write: { type: "boolean" },
759
+ zones: { type: "boolean" },
737
760
  json: { type: "boolean" },
738
761
  dir: { type: "string" },
739
762
  }),
740
763
  (value) => {
741
764
  const raw = withDir(record(value))
742
765
  const flags = {
743
- ...parseBooleanFlags(raw, ["write", "json"]),
766
+ ...parseBooleanFlags(raw, ["write", "zones", "json"]),
744
767
  ...(raw.dir === undefined ? {} : { dir: raw.dir }),
745
768
  }
746
769
  const kind = optionalString(raw.kind, "kind")
@@ -749,6 +772,7 @@ const MIGRATE_SCHEMA = input<MigrateInput>(
749
772
  throw new TypeError("the migration must be `layout` (or --from tailwind --to stylex)")
750
773
  return { kind, ...flags }
751
774
  }
775
+ if (raw.zones === true) throw new TypeError("--zones applies to `migrate layout` only")
752
776
  const from = optionalString(raw.from, "from")
753
777
  const to = optionalString(raw.to, "to")
754
778
  if (from !== "tailwind") throw new TypeError("from must be tailwind")
@@ -781,6 +805,22 @@ const TYPES_SCHEMA = input<TypesInput>(
781
805
  },
782
806
  )
783
807
 
808
+ const SCHEMAS_SCHEMA = input<SchemasInput>(
809
+ objectSchema({
810
+ write: { type: "boolean" },
811
+ check: { type: "boolean" },
812
+ json: { type: "boolean" },
813
+ dir: { type: "string" },
814
+ }),
815
+ (value) => {
816
+ const raw = withDir(record(value))
817
+ return {
818
+ ...parseBooleanFlags(raw, ["write", "check", "json"]),
819
+ ...(raw.dir === undefined ? {} : { dir: raw.dir }),
820
+ }
821
+ },
822
+ )
823
+
784
824
  const SNAPSHOT_SCHEMA = input<SnapshotInput>(
785
825
  objectSchema({ out: { type: "string" }, json: { type: "boolean" }, dir: { type: "string" } }),
786
826
  (value) => {
@@ -944,9 +984,10 @@ const I18N_SCHEMA = input<I18nInput>(
944
984
  },
945
985
  )
946
986
 
947
- function loadAppFor(ctx: CommandCtx): Promise<LoadedApp> {
948
- if (ctx.loadApp !== undefined) return ctx.loadApp()
949
- return import("./load.ts").then(({ loadApp }) => loadApp(ctx.cwd))
987
+ /** The app as the backend readers see it: an API-only app is its `backend/app.ts` alone. */
988
+ function backendAppFor(ctx: CommandCtx): Promise<IntrospectedApp> {
989
+ if (ctx.loadApp !== undefined) return ctx.loadApp.backend()
990
+ return import("./load.ts").then(({ loadBackendApp }) => loadBackendApp(ctx.cwd))
950
991
  }
951
992
 
952
993
  function structuredDiagnostics(out: CheckCommandOutput): readonly Diagnostic[] {
@@ -1195,7 +1236,8 @@ const manifestSpec: CommandSpec<ManifestInput, ManifestEmitCommandResult | DiffC
1195
1236
 
1196
1237
  const routesSpec: CommandSpec<RoutesInput, RoutesCommandOutput> = {
1197
1238
  name: "routes",
1198
- summary: "List, graph, or target-check every page and API route the app serves.",
1239
+ summary:
1240
+ "List, graph, or target-check every page and API route the app serves, or the hooks each runs.",
1199
1241
  input: ROUTES_SCHEMA,
1200
1242
  output: output({
1201
1243
  type: "object",
@@ -1209,12 +1251,47 @@ const routesSpec: CommandSpec<RoutesInput, RoutesCommandOutput> = {
1209
1251
  { name: "json", field: "json", type: "boolean" },
1210
1252
  { name: "graph", field: "graph", type: "boolean" },
1211
1253
  { name: "modes", field: "modes", type: "boolean" },
1254
+ { name: "hooks", field: "hooks", type: "boolean" },
1255
+ { name: "since", field: "since", type: "string" },
1212
1256
  { name: "target", field: "target", type: "string" },
1213
1257
  ],
1214
1258
  },
1215
1259
  async run(value, ctx) {
1216
- const app = await loadAppFor(ctx)
1260
+ if (value.hooks === true) {
1261
+ const app = await backendAppFor(ctx)
1262
+ const hooks = await import("./hooks-report.ts")
1263
+ const report = await hooks.collectHookReport(app.backend)
1264
+ if (value.since === undefined) {
1265
+ return { ok: true, mode: "hooks", data: report, text: hooks.renderHookReport(report) }
1266
+ }
1267
+ const saved = hooks.parseHookReport(
1268
+ await Bun.file(resolve(ctx.cwd, value.since))
1269
+ .text()
1270
+ .catch(() => ""),
1271
+ )
1272
+ if (saved === undefined) {
1273
+ return {
1274
+ ok: false,
1275
+ mode: "hooks",
1276
+ data: { report, error: `${value.since} is not a saved hook report` },
1277
+ text: `[nifra] ${value.since} is not a saved hook report (write one with \`nifra routes --hooks --json\`)`,
1278
+ }
1279
+ }
1280
+ const changes = hooks.diffHookReports(saved, report)
1281
+ return {
1282
+ ok: true,
1283
+ mode: "hooks",
1284
+ data: { report, changes },
1285
+ text: hooks.renderHookDiff(changes),
1286
+ }
1287
+ }
1288
+ const app = await backendAppFor(ctx)
1217
1289
  if (value.modes === true) {
1290
+ if (app.framework === undefined) {
1291
+ throw new Error(
1292
+ `[nifra] routes --modes reads page routes, and ${ctx.cwd} is an API-only app (backend/app.ts, no web config)`,
1293
+ )
1294
+ }
1218
1295
  const { buildRouteManifest, renderRouteManifest } = await import(
1219
1296
  "@nifrajs/web/route-manifest"
1220
1297
  )
@@ -1264,7 +1341,7 @@ const contextSpec: CommandSpec<ContextInput, ContextCommandOutput> = {
1264
1341
  async run(value, ctx) {
1265
1342
  return {
1266
1343
  ok: true,
1267
- text: (await import("./introspect.ts")).describeProject(await loadAppFor(ctx), {
1344
+ text: (await import("./introspect.ts")).describeProject(await backendAppFor(ctx), {
1268
1345
  ...(value.path === undefined ? {} : { path: value.path }),
1269
1346
  ...(value.kind === undefined ? {} : { kind: value.kind }),
1270
1347
  }),
@@ -1292,10 +1369,7 @@ const openApiSpec: CommandSpec<OpenApiInput, OpenApiCommandOutput> = {
1292
1369
  ],
1293
1370
  },
1294
1371
  async run(value, ctx) {
1295
- const app =
1296
- ctx.loadApp !== undefined
1297
- ? await ctx.loadApp()
1298
- : await import("./load.ts").then(({ loadBackendApp }) => loadBackendApp(ctx.cwd))
1372
+ const app = await backendAppFor(ctx)
1299
1373
  const { renderOpenApiWithTypes } = await import("./openapi-tool.ts")
1300
1374
  const format = value.format ?? "json"
1301
1375
  return {
@@ -1356,6 +1430,7 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
1356
1430
  ok: { type: "boolean" },
1357
1431
  changed: { type: "array" },
1358
1432
  failed: { type: "array" },
1433
+ skipped: { type: "array" },
1359
1434
  diagnostics: { type: "array" },
1360
1435
  },
1361
1436
  required: ["ok", "diagnostics"],
@@ -1370,25 +1445,37 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
1370
1445
  },
1371
1446
  async run(value, ctx) {
1372
1447
  const { collectCheckResult } = await import("./check.ts")
1373
- const { applyDiagnosticRecipe } = await import("./fix-recipes.ts")
1448
+ const { applyDiagnosticRecipe, getFixRecipe } = await import("./fix-recipes.ts")
1374
1449
  const result = await collectCheckResult(ctx.cwd, {
1375
1450
  lintsOnly: true,
1376
1451
  ...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
1377
1452
  })
1378
1453
  const changed: string[] = []
1379
1454
  const failed: { code: string; reason: string }[] = []
1380
- for (const diagnostic of result.structuredDiagnostics ?? [])
1381
- if (value.code === undefined || diagnostic.code === value.code)
1382
- // One recipe that cannot act must not cancel the others, and must not pass silently either:
1383
- // record why and keep going, so a run over many diagnostics still reports every outcome.
1384
- try {
1385
- changed.push(...(await applyDiagnosticRecipe(ctx.cwd, diagnostic)))
1386
- } catch (error) {
1387
- failed.push({
1455
+ const skipped: { code: string; reason: string }[] = []
1456
+ for (const diagnostic of result.structuredDiagnostics ?? []) {
1457
+ if (value.code !== undefined && diagnostic.code !== value.code) continue
1458
+ const recipe =
1459
+ diagnostic.fix?.recipe === undefined ? undefined : getFixRecipe(diagnostic.fix.recipe)
1460
+ if (value.code === undefined && recipe?.onlyWhenNamed === true) {
1461
+ if (!skipped.some((entry) => entry.code === diagnostic.code))
1462
+ skipped.push({
1388
1463
  code: diagnostic.code,
1389
- reason: error instanceof Error ? error.message : String(error),
1464
+ reason: `${recipe.description} Review the finding, then run \`nifra fix --code ${diagnostic.code}\`.`,
1390
1465
  })
1391
- }
1466
+ continue
1467
+ }
1468
+ // One recipe that cannot act must not cancel the others, and must not pass silently either:
1469
+ // record why and keep going, so a run over many diagnostics still reports every outcome.
1470
+ try {
1471
+ changed.push(...(await applyDiagnosticRecipe(ctx.cwd, diagnostic)))
1472
+ } catch (error) {
1473
+ failed.push({
1474
+ code: diagnostic.code,
1475
+ reason: error instanceof Error ? error.message : String(error),
1476
+ })
1477
+ }
1478
+ }
1392
1479
  const final = await collectCheckResult(ctx.cwd, {
1393
1480
  lintsOnly: true,
1394
1481
  ...(ctx.signal === undefined ? {} : { signal: ctx.signal }),
@@ -1397,6 +1484,7 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
1397
1484
  ok: final.ok,
1398
1485
  changed,
1399
1486
  failed,
1487
+ skipped,
1400
1488
  diagnostics: final.structuredDiagnostics ?? final.diagnostics,
1401
1489
  }
1402
1490
  },
@@ -1404,6 +1492,7 @@ const fixSpec: CommandSpec<FixInput, FixCommandOutput> = {
1404
1492
  out.ok ? "✓ fixes applied" : "✖ unresolved diagnostics remain",
1405
1493
  ...out.changed.map((file) => ` changed ${file}`),
1406
1494
  ...out.failed.map((f) => ` could not fix ${f.code}: ${f.reason}`),
1495
+ ...out.skipped.map((s) => ` needs review ${s.code}: ${s.reason}`),
1407
1496
  ...out.diagnostics.map((d) => ` ${String((d as { code?: unknown }).code ?? "diagnostic")}`),
1408
1497
  ],
1409
1498
  success: (out) => out.ok,
@@ -1430,6 +1519,9 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
1430
1519
  splits: { type: "array" },
1431
1520
  moves: { type: "array" },
1432
1521
  rewritten: { type: "array" },
1522
+ tooling: { type: "array" },
1523
+ followUps: { type: "array" },
1524
+ mode: { type: "string" },
1433
1525
  },
1434
1526
  required: ["ok", "write", "issues"],
1435
1527
  }),
@@ -1441,6 +1533,7 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
1441
1533
  { name: "from", field: "from", type: "string" },
1442
1534
  { name: "to", field: "to", type: "string" },
1443
1535
  { name: "write", field: "write", type: "boolean" },
1536
+ { name: "zones", field: "zones", type: "boolean" },
1444
1537
  { name: "json", field: "json", type: "boolean" },
1445
1538
  { name: "dir", field: "dir", type: "string" },
1446
1539
  ],
@@ -1450,7 +1543,8 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
1450
1543
  const write = value.write === undefined ? {} : { write: value.write }
1451
1544
  if (value.kind === "layout") {
1452
1545
  const { migrateLayout } = await import("./migrate-layout.ts")
1453
- return { kind: "layout" as const, ...(await migrateLayout(dir, write)) }
1546
+ const options = { ...write, zones: value.zones === true }
1547
+ return { kind: "layout" as const, ...(await migrateLayout(dir, options)) }
1454
1548
  }
1455
1549
  const { migrateTailwindToStylex } = await import("./stylex-migrate.ts")
1456
1550
  return migrateTailwindToStylex(dir, write)
@@ -1459,14 +1553,25 @@ const migrateSpec: CommandSpec<MigrateInput, MigrateCommandOutput> = {
1459
1553
  if (out.kind === "layout") {
1460
1554
  return [
1461
1555
  out.ok
1462
- ? `✓ ${out.splits.length} route file${out.splits.length === 1 ? "" : "s"} split, ${out.moves.length} file${out.moves.length === 1 ? "" : "s"} moved`
1556
+ ? `✓ ${out.mode === "api" ? "API-only app" : `${out.splits.length} route file${out.splits.length === 1 ? "" : "s"} split`}, ${out.moves.length} file${out.moves.length === 1 ? "" : "s"} moved`
1463
1557
  : `⚠ ${out.issues.length} issue${out.issues.length === 1 ? "" : "s"} need a decision; everything else is planned`,
1464
1558
  ...out.splits.map(
1465
- (split) => ` split ${split.file} → ${split.backend} (${split.moved.join(", ")})`,
1559
+ (split) =>
1560
+ ` split ${split.file} → ${split.backend} (${split.moved.join(", ")})${split.shared === undefined ? "" : `, shared data → ${split.shared}`}`,
1466
1561
  ),
1467
1562
  ...out.moves.map((move) => ` move ${move.from} → ${move.to}`),
1468
1563
  ...out.rewritten.map((file) => ` edit ${file} (imports)`),
1564
+ ...out.tooling.map((edit) => ` edit ${edit.file} (${edit.change})`),
1565
+ ...out.schemas
1566
+ .filter((entry) => entry.reason === undefined)
1567
+ .map(
1568
+ (entry) =>
1569
+ ` draft ${entry.file} ${entry.schema} - review what it lets the browser see${entry.omitted === undefined ? "" : `; leaves out ${entry.omitted.join(", ")}`}${entry.open === undefined ? "" : `; admits any value at ${entry.open.join(", ")}, typed any or unknown`}`,
1570
+ ),
1469
1571
  ...out.issues.map((issue) => ` ⚠ ${issue.file}: ${issue.reason}`),
1572
+ ...out.followUps.map(
1573
+ (step) => ` next ${step.file}: ${step.reason} - \`${step.command}\``,
1574
+ ),
1470
1575
  ...(out.write ? [] : [" dry run: pass --write to apply"]),
1471
1576
  ]
1472
1577
  }
@@ -1586,6 +1691,90 @@ const typesSpec: CommandSpec<TypesInput, RouteTypesReport> = {
1586
1691
  success: (out) => out.ok,
1587
1692
  }
1588
1693
 
1694
+ const SCHEMA_STATUS_LINE: Readonly<Record<OutputSchemaStatus, string>> = {
1695
+ missing: "+ would write",
1696
+ generated: "+ wrote",
1697
+ stale: "↻ stale",
1698
+ regenerated: "↻ rewrote",
1699
+ current: "✓ current",
1700
+ declared: "· hand-written",
1701
+ "no-data": "· no data",
1702
+ unreadable: "⚠ unreadable",
1703
+ }
1704
+
1705
+ const schemasSpec: CommandSpec<SchemasInput, OutputSchemasReport> = {
1706
+ name: "schemas",
1707
+ summary:
1708
+ "Write a reviewable `loaderOutput`/`actionOutput` from the loader's or action's return type where a route has none (`--write`), keep generated ones current, and list returned fields a hand-written schema drops.",
1709
+ input: SCHEMAS_SCHEMA,
1710
+ output: output({
1711
+ type: "object",
1712
+ properties: {
1713
+ ok: { type: "boolean" },
1714
+ write: { type: "boolean" },
1715
+ entries: { type: "array" },
1716
+ written: { type: "array" },
1717
+ warnings: { type: "array" },
1718
+ },
1719
+ required: ["ok", "write", "entries", "written", "warnings"],
1720
+ }),
1721
+ transports: ["cli"],
1722
+ stability: "stable",
1723
+ argv: {
1724
+ flags: [
1725
+ { name: "write", field: "write", type: "boolean" },
1726
+ { name: "check", field: "check", type: "boolean" },
1727
+ { name: "json", field: "json", type: "boolean" },
1728
+ { name: "dir", field: "dir", type: "string" },
1729
+ ],
1730
+ },
1731
+ async run(value, ctx) {
1732
+ const { outputSchemas } = await import("./output-schemas.ts")
1733
+ return outputSchemas(resolve(ctx.cwd, value.dir ?? "."), {
1734
+ write: value.write === true,
1735
+ check: value.check === true,
1736
+ })
1737
+ },
1738
+ render: (out) => {
1739
+ const shown = out.entries.filter(
1740
+ (entry) =>
1741
+ (entry.status !== "current" || entry.open !== undefined) &&
1742
+ entry.status !== "no-data" &&
1743
+ (entry.status !== "declared" || entry.dropped.length > 0),
1744
+ )
1745
+ const current = out.entries.filter((entry) => entry.status === "current").length
1746
+ return [
1747
+ ...out.warnings.map((warning) => `⚠ ${warning}`),
1748
+ `${out.ok ? "✓" : "✖"} ${out.entries.length} output schema${out.entries.length === 1 ? "" : "s"} read, ${current} generated and current`,
1749
+ ...shown.map((entry) => {
1750
+ const detail =
1751
+ entry.reason ??
1752
+ (entry.edited === true
1753
+ ? "edited by hand under its nifra:generated line"
1754
+ : entry.dropped.length > 0
1755
+ ? `drops ${entry.dropped.join(", ")}`
1756
+ : entry.omitted !== undefined
1757
+ ? `leaves out ${entry.omitted.join(", ")}`
1758
+ : undefined)
1759
+ const open =
1760
+ entry.open === undefined
1761
+ ? []
1762
+ : [
1763
+ ` admits any value at ${entry.open.join(", ")}: the type there is any or unknown, so the output guard drops nothing below it`,
1764
+ ]
1765
+ return [
1766
+ ` ${SCHEMA_STATUS_LINE[entry.status]} ${entry.file}:${entry.line} ${entry.schema}${detail === undefined ? "" : ` - ${detail}`}`,
1767
+ ...open,
1768
+ ].join("\n")
1769
+ }),
1770
+ ...(out.write || !out.entries.some((e) => e.status === "missing" || e.status === "stale")
1771
+ ? []
1772
+ : [" dry run: pass --write to apply"]),
1773
+ ]
1774
+ },
1775
+ success: (out) => out.ok,
1776
+ }
1777
+
1589
1778
  const snapshotSpec: CommandSpec<SnapshotInput, SnapshotCommandOutput> = {
1590
1779
  name: "snapshot",
1591
1780
  summary: "Write the backend API contract as a versioned JSON baseline.",
@@ -1610,18 +1799,16 @@ const snapshotSpec: CommandSpec<SnapshotInput, SnapshotCommandOutput> = {
1610
1799
  },
1611
1800
  async run(value, ctx) {
1612
1801
  const mod = await import("./diff-tool.ts")
1613
- const routes = await mod.snapshotBackend(ctx.cwd)
1614
1802
  const file = value.out ?? mod.DEFAULT_SNAPSHOT_FILE
1615
- const snapshot = { nifraSnapshot: 1 as const, routes }
1616
- await Bun.write(resolve(ctx.cwd, file), `${JSON.stringify(snapshot, null, 2)}\n`)
1617
- return { ok: true, file, routes: routes.length, snapshot }
1803
+ const snapshot = await mod.writeSnapshot(ctx.cwd, file)
1804
+ return { ok: true, file, routes: snapshot.routes.length, snapshot }
1618
1805
  },
1619
1806
  render: (out) => [
1620
1807
  `[nifra] wrote ${out.routes} route${out.routes === 1 ? "" : "s"} to ${out.file}`,
1621
1808
  ],
1622
1809
  }
1623
1810
 
1624
- const diffSpec: CommandSpec<DiffInput, DiffCommandOutput> = {
1811
+ const diffSpec: CommandSpec<DiffInput, RouteDiffCommandOutput> = {
1625
1812
  name: "diff",
1626
1813
  summary: "Compare the current backend contract with a baseline and fail on breaking changes.",
1627
1814
  input: DIFF_SCHEMA,
@@ -1638,21 +1825,17 @@ const diffSpec: CommandSpec<DiffInput, DiffCommandOutput> = {
1638
1825
  stability: "stable",
1639
1826
  argv: { positionals: ["baseline"], flags: [{ name: "json", field: "json", type: "boolean" }] },
1640
1827
  async run(value, ctx) {
1641
- const mod = await import("./diff-tool.ts")
1642
- const baseline = value.baseline ?? mod.DEFAULT_SNAPSHOT_FILE
1643
- const file = await Bun.file(resolve(ctx.cwd, baseline)).text()
1644
- const before = mod.parseSnapshotFile(file, baseline)
1645
- const current = await mod.snapshotBackend(ctx.cwd)
1646
- const { diffRouteSnapshots } = await import("@nifrajs/core/diff")
1647
- const diff = diffRouteSnapshots(before.routes, current)
1648
- return { ok: !diff.hasBreaking, hasBreaking: diff.hasBreaking, changes: diff.changes }
1828
+ const { diffAgainstBaseline, formatDiff } = await import("./diff-tool.ts")
1829
+ const diff = await diffAgainstBaseline(ctx.cwd, value.baseline)
1830
+ return {
1831
+ ok: !diff.hasBreaking,
1832
+ hasBreaking: diff.hasBreaking,
1833
+ changes: diff.changes,
1834
+ text: formatDiff(diff),
1835
+ }
1649
1836
  },
1650
- render: (out) => [
1651
- out.ok ? "✓ no breaking changes" : "✖ breaking changes",
1652
- ...(out.changes as readonly { method: string; path: string; message: string }[]).map(
1653
- (c) => `${c.method} ${c.path}: ${c.message}`,
1654
- ),
1655
- ],
1837
+ render: (out) => [out.text],
1838
+ json: ({ ok, hasBreaking, changes }) => ({ ok, hasBreaking, changes }),
1656
1839
  success: (out) => out.ok,
1657
1840
  }
1658
1841
 
@@ -1880,6 +2063,7 @@ export const commandSpecs = Object.freeze([
1880
2063
  migrateSpec,
1881
2064
  targetSpec,
1882
2065
  typesSpec,
2066
+ schemasSpec,
1883
2067
  snapshotSpec,
1884
2068
  diffSpec,
1885
2069
  contractsSpec,
@@ -1891,6 +2075,7 @@ export const commandSpecs = Object.freeze([
1891
2075
  portSpec,
1892
2076
  i18nSpec,
1893
2077
  errorsSpec,
2078
+ explainSpec,
1894
2079
  logsSpec,
1895
2080
  cdnCheckSpec,
1896
2081
  dbSchemaSpec,
@@ -0,0 +1,79 @@
1
+ import { existsSync, readFileSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+ import {
4
+ isUnsupportedTypeScriptError,
5
+ type UnsupportedTypeScriptError,
6
+ } from "./internal/typescript-import.ts"
7
+ import type { SourceIndex } from "./rules/index.ts"
8
+
9
+ type ComponentSource = typeof import("@nifrajs/web/server-components/source")
10
+
11
+ export interface ComponentCheck {
12
+ readonly receipts: ReadonlySet<string>
13
+ /** Set when the project's TypeScript has no compiler API to read component modules with
14
+ * (TypeScript 7): no module gets a receipt, so the ordinary import rule judges each one. */
15
+ readonly unsupported: UnsupportedTypeScriptError | undefined
16
+ }
17
+
18
+ const COMPONENT_SERVER = /\.component\.server\.[cm]?[jt]sx?$/
19
+
20
+ /** A source-only proof. nifra check never executes nifra.config.ts. */
21
+ export async function componentCheckReceipts(
22
+ root: string,
23
+ source: SourceIndex,
24
+ load: () => Promise<ComponentSource> = () => import("@nifrajs/web/server-components/source"),
25
+ ): Promise<ComponentCheck> {
26
+ const receipts = new Set<string>()
27
+ const result: ComponentCheck = { receipts, unsupported: undefined }
28
+ if (!source.files.some((file) => COMPONENT_SERVER.test(file))) return result
29
+ const configFile = ["nifra.config.ts", "backend/framework.ts"].find((file) =>
30
+ existsSync(resolve(root, file)),
31
+ )
32
+ if (configFile === undefined) return result
33
+ let components: ComponentSource
34
+ try {
35
+ components = await load()
36
+ } catch (error) {
37
+ if (isUnsupportedTypeScriptError(error)) return { receipts, unsupported: error }
38
+ throw error
39
+ }
40
+ const pipeline = components.staticComponentPipeline(
41
+ readFileSync(resolve(root, configFile), "utf8"),
42
+ configFile,
43
+ )
44
+ if (typeof pipeline !== "object" || pipeline === null || Array.isArray(pipeline)) return result
45
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Static receipt inspection checks object shape before reading fields; the source scanner never executes configuration.
46
+ const options = pipeline as Record<string, unknown>
47
+ if (
48
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Static receipt inspection checks object shape before reading fields; the source scanner never executes configuration.
49
+ !["react", "vue", "svelte", "solid", "preact"].includes(options.adapter as string) ||
50
+ typeof options.entries !== "object" ||
51
+ options.entries === null ||
52
+ Array.isArray(options.entries)
53
+ )
54
+ return result
55
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Static receipt inspection checks object shape before reading fields; the source scanner never executes configuration.
56
+ const entries = options.entries as Record<string, unknown>
57
+ if (
58
+ typeof entries.client !== "string" ||
59
+ typeof entries.ssr !== "string" ||
60
+ (options.adapter === "react" && typeof entries.rsc !== "string") ||
61
+ Object.values(entries).some(
62
+ (file) => typeof file !== "string" || !existsSync(resolve(root, file)),
63
+ )
64
+ )
65
+ return result
66
+ for (const file of source.files) {
67
+ if (!COMPONENT_SERVER.test(file)) continue
68
+ const content = source.read(file)
69
+ if (content === undefined) continue
70
+ try {
71
+ components.compileComponentModule(content, resolve(root, file), root, "client")
72
+ receipts.add(file)
73
+ receipts.add(resolve(root, file))
74
+ } catch {
75
+ /* A module without a compilation receipt remains forbidden by the ordinary import rule. */
76
+ }
77
+ }
78
+ return result
79
+ }
@@ -1,14 +1,12 @@
1
- import { existsSync, realpathSync } from "node:fs"
2
- import { resolve, sep } from "node:path"
3
1
  import type { AssuranceFinding, AssuranceReport, AssuredRoute } from "@nifrajs/core/assurance"
4
2
  import type {
5
3
  AssuredCapabilityRoute,
6
4
  CapabilityAssuranceReport,
7
5
  CapabilityFinding,
8
6
  } from "@nifrajs/core/capabilities"
9
- import { diffRouteSnapshots, type RouteChange, type RoutesDiff } from "@nifrajs/core/diff"
7
+ import type { RouteChange, RoutesDiff } from "@nifrajs/core/diff"
10
8
  import type { CheckResult } from "./check.ts"
11
- import { DEFAULT_SNAPSHOT_FILE, parseSnapshotFile, snapshotBackend } from "./diff-tool.ts"
9
+ import { DEFAULT_SNAPSHOT_FILE, diffAgainstBaseline } from "./diff-tool.ts"
12
10
  import { collectProjectVerification } from "./verification.ts"
13
11
 
14
12
  const routeKey = (method: string, path: string): string => `${method.toUpperCase()}\n${path}`
@@ -59,15 +57,6 @@ export interface ContractProofOptions {
59
57
  readonly signal?: AbortSignal
60
58
  }
61
59
 
62
- function safeBaselinePath(cwd: string, baselinePath: string): string {
63
- const root = resolve(cwd)
64
- const target = resolve(root, baselinePath)
65
- if (target !== root && !target.startsWith(`${root}${sep}`)) {
66
- throw new Error("contract proof baseline must stay inside the project directory")
67
- }
68
- return target
69
- }
70
-
71
60
  function errorMessage(error: unknown): string {
72
61
  return error instanceof Error ? error.message : String(error)
73
62
  }
@@ -144,18 +133,7 @@ export async function collectContractProof(
144
133
  options: ContractProofOptions = {},
145
134
  ): Promise<ContractProofReport> {
146
135
  const baselinePath = options.baselinePath ?? DEFAULT_SNAPSHOT_FILE
147
- const resolvedBaseline = safeBaselinePath(cwd, baselinePath)
148
- if (!existsSync(resolvedBaseline)) {
149
- throw new Error(`contract proof baseline not found: ${resolvedBaseline}`)
150
- }
151
- const realRoot = realpathSync(resolve(cwd))
152
- const realBaseline = realpathSync(resolvedBaseline)
153
- if (realBaseline !== realRoot && !realBaseline.startsWith(`${realRoot}${sep}`)) {
154
- throw new Error("contract proof baseline must stay inside the project directory")
155
- }
156
- const baseline = parseSnapshotFile(await Bun.file(resolvedBaseline).text(), baselinePath)
157
- const current = await snapshotBackend(cwd)
158
- const diff = diffRouteSnapshots(baseline.routes, current)
136
+ const diff = await diffAgainstBaseline(cwd, baselinePath, { contain: true })
159
137
  const verification = await collectProjectVerification(cwd, {
160
138
  ...(options.signal === undefined ? {} : { signal: options.signal }),
161
139
  })