@nifrajs/cli 4.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/README.md +3 -0
  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 +7 -2
  12. package/dist/assure-hydration.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts.map +1 -1
  14. package/dist/capabilities-tool.js +13 -3
  15. package/dist/capabilities-tool.js.map +1 -1
  16. package/dist/check-diagnostics.d.ts +20 -2
  17. package/dist/check-diagnostics.d.ts.map +1 -1
  18. package/dist/check-diagnostics.js +54 -7
  19. package/dist/check-diagnostics.js.map +1 -1
  20. package/dist/check-scan.d.ts +75 -22
  21. package/dist/check-scan.d.ts.map +1 -1
  22. package/dist/check-scan.js +1475 -310
  23. package/dist/check-scan.js.map +1 -1
  24. package/dist/check.d.ts +2 -2
  25. package/dist/check.d.ts.map +1 -1
  26. package/dist/check.js +192 -65
  27. package/dist/check.js.map +1 -1
  28. package/dist/cli.d.ts +7 -0
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +112 -5
  31. package/dist/cli.js.map +1 -1
  32. package/dist/client-module.d.ts +18 -0
  33. package/dist/client-module.d.ts.map +1 -0
  34. package/dist/client-module.js +49 -0
  35. package/dist/client-module.js.map +1 -0
  36. package/dist/command-catalog.d.ts +21 -5
  37. package/dist/command-catalog.d.ts.map +1 -1
  38. package/dist/command-catalog.js +138 -38
  39. package/dist/command-catalog.js.map +1 -1
  40. package/dist/component-check.d.ts +13 -0
  41. package/dist/component-check.d.ts.map +1 -0
  42. package/dist/component-check.js +59 -0
  43. package/dist/component-check.js.map +1 -0
  44. package/dist/contract-proof.d.ts +1 -1
  45. package/dist/contract-proof.d.ts.map +1 -1
  46. package/dist/contract-proof.js +2 -24
  47. package/dist/contract-proof.js.map +1 -1
  48. package/dist/contracts.d.ts +19 -0
  49. package/dist/contracts.d.ts.map +1 -1
  50. package/dist/contracts.js +50 -3
  51. package/dist/contracts.js.map +1 -1
  52. package/dist/db-tool.d.ts.map +1 -1
  53. package/dist/db-tool.js +4 -3
  54. package/dist/db-tool.js.map +1 -1
  55. package/dist/dev-bun-config.d.ts +5 -1
  56. package/dist/dev-bun-config.d.ts.map +1 -1
  57. package/dist/dev-bun-config.js +44 -11
  58. package/dist/dev-bun-config.js.map +1 -1
  59. package/dist/diagnostics.d.ts +3 -0
  60. package/dist/diagnostics.d.ts.map +1 -1
  61. package/dist/diagnostics.js.map +1 -1
  62. package/dist/diff-tool.d.ts +9 -8
  63. package/dist/diff-tool.d.ts.map +1 -1
  64. package/dist/diff-tool.js +28 -24
  65. package/dist/diff-tool.js.map +1 -1
  66. package/dist/doctor.d.ts +13 -0
  67. package/dist/doctor.d.ts.map +1 -1
  68. package/dist/doctor.js +23 -15
  69. package/dist/doctor.js.map +1 -1
  70. package/dist/env-file.d.ts +6 -0
  71. package/dist/env-file.d.ts.map +1 -1
  72. package/dist/env-file.js +6 -0
  73. package/dist/env-file.js.map +1 -1
  74. package/dist/fix-recipes.d.ts.map +1 -1
  75. package/dist/fix-recipes.js +28 -3
  76. package/dist/fix-recipes.js.map +1 -1
  77. package/dist/hand-built-manifest.d.ts +27 -0
  78. package/dist/hand-built-manifest.d.ts.map +1 -0
  79. package/dist/hand-built-manifest.js +124 -0
  80. package/dist/hand-built-manifest.js.map +1 -0
  81. package/dist/hooks-report.d.ts +23 -0
  82. package/dist/hooks-report.d.ts.map +1 -0
  83. package/dist/hooks-report.js +104 -0
  84. package/dist/hooks-report.js.map +1 -0
  85. package/dist/init-agents.d.ts +2 -2
  86. package/dist/init-agents.d.ts.map +1 -1
  87. package/dist/init-agents.js +17 -11
  88. package/dist/init-agents.js.map +1 -1
  89. package/dist/internal/nifra-project.d.ts +41 -0
  90. package/dist/internal/nifra-project.d.ts.map +1 -0
  91. package/dist/internal/nifra-project.js +183 -0
  92. package/dist/internal/nifra-project.js.map +1 -0
  93. package/dist/internal/project-specifiers.d.ts +16 -0
  94. package/dist/internal/project-specifiers.d.ts.map +1 -0
  95. package/dist/internal/project-specifiers.js +396 -0
  96. package/dist/internal/project-specifiers.js.map +1 -0
  97. package/dist/internal/script-kind.d.ts.map +1 -1
  98. package/dist/internal/script-kind.js +2 -1
  99. package/dist/internal/script-kind.js.map +1 -1
  100. package/dist/internal/source-facts.d.ts +2 -0
  101. package/dist/internal/source-facts.d.ts.map +1 -1
  102. package/dist/internal/source-facts.js +11 -0
  103. package/dist/internal/source-facts.js.map +1 -1
  104. package/dist/internal/typescript-import.d.ts +1 -1
  105. package/dist/internal/typescript-import.d.ts.map +1 -1
  106. package/dist/internal/typescript-import.js +5 -3
  107. package/dist/internal/typescript-import.js.map +1 -1
  108. package/dist/introspect.d.ts +6 -6
  109. package/dist/introspect.d.ts.map +1 -1
  110. package/dist/introspect.js +55 -24
  111. package/dist/introspect.js.map +1 -1
  112. package/dist/levels-tool.d.ts.map +1 -1
  113. package/dist/levels-tool.js +93 -15
  114. package/dist/levels-tool.js.map +1 -1
  115. package/dist/load.d.ts +31 -8
  116. package/dist/load.d.ts.map +1 -1
  117. package/dist/load.js +37 -7
  118. package/dist/load.js.map +1 -1
  119. package/dist/mcp-context.d.ts +5 -5
  120. package/dist/mcp-context.d.ts.map +1 -1
  121. package/dist/mcp-context.js +2 -2
  122. package/dist/mcp-context.js.map +1 -1
  123. package/dist/mcp-exec.d.ts +12 -4
  124. package/dist/mcp-exec.d.ts.map +1 -1
  125. package/dist/mcp-exec.js +50 -13
  126. package/dist/mcp-exec.js.map +1 -1
  127. package/dist/mcp-http.js +1 -1
  128. package/dist/mcp-io.d.ts +17 -0
  129. package/dist/mcp-io.d.ts.map +1 -1
  130. package/dist/mcp-io.js +40 -0
  131. package/dist/mcp-io.js.map +1 -1
  132. package/dist/mcp-isolate.d.ts.map +1 -1
  133. package/dist/mcp-isolate.js +3 -2
  134. package/dist/mcp-isolate.js.map +1 -1
  135. package/dist/mcp-project-child.js +1 -1
  136. package/dist/mcp-project-child.js.map +1 -1
  137. package/dist/mcp-root.d.ts.map +1 -1
  138. package/dist/mcp-root.js +50 -10
  139. package/dist/mcp-root.js.map +1 -1
  140. package/dist/mcp-ws.d.ts.map +1 -1
  141. package/dist/mcp-ws.js +3 -1
  142. package/dist/mcp-ws.js.map +1 -1
  143. package/dist/mcp.d.ts.map +1 -1
  144. package/dist/mcp.js +8 -2
  145. package/dist/mcp.js.map +1 -1
  146. package/dist/migrate-layout.d.ts +7 -0
  147. package/dist/migrate-layout.d.ts.map +1 -1
  148. package/dist/migrate-layout.js +958 -71
  149. package/dist/migrate-layout.js.map +1 -1
  150. package/dist/output-schemas.d.ts +94 -0
  151. package/dist/output-schemas.d.ts.map +1 -0
  152. package/dist/output-schemas.js +864 -0
  153. package/dist/output-schemas.js.map +1 -0
  154. package/dist/port.d.ts.map +1 -1
  155. package/dist/port.js +3 -2
  156. package/dist/port.js.map +1 -1
  157. package/dist/project-facts.d.ts +15 -4
  158. package/dist/project-facts.d.ts.map +1 -1
  159. package/dist/project-facts.js +1 -0
  160. package/dist/project-facts.js.map +1 -1
  161. package/dist/recipes/4.0.0.d.ts.map +1 -1
  162. package/dist/recipes/4.0.0.js +2 -0
  163. package/dist/recipes/4.0.0.js.map +1 -1
  164. package/dist/recipes/5.0.0.d.ts +11 -0
  165. package/dist/recipes/5.0.0.d.ts.map +1 -0
  166. package/dist/recipes/5.0.0.js +34 -0
  167. package/dist/recipes/5.0.0.js.map +1 -0
  168. package/dist/recipes/index.d.ts +7 -0
  169. package/dist/recipes/index.d.ts.map +1 -1
  170. package/dist/recipes/index.js +2 -0
  171. package/dist/recipes/index.js.map +1 -1
  172. package/dist/replay.d.ts.map +1 -1
  173. package/dist/replay.js +12 -3
  174. package/dist/replay.js.map +1 -1
  175. package/dist/review.d.ts +3 -1
  176. package/dist/review.d.ts.map +1 -1
  177. package/dist/review.js +38 -7
  178. package/dist/review.js.map +1 -1
  179. package/dist/rules/codes.d.ts +10 -0
  180. package/dist/rules/codes.d.ts.map +1 -1
  181. package/dist/rules/codes.js +10 -0
  182. package/dist/rules/codes.js.map +1 -1
  183. package/dist/rules/comment-markers.d.ts +16 -7
  184. package/dist/rules/comment-markers.d.ts.map +1 -1
  185. package/dist/rules/comment-markers.js +58 -19
  186. package/dist/rules/comment-markers.js.map +1 -1
  187. package/dist/rules/contract-visibility.d.ts +3 -0
  188. package/dist/rules/contract-visibility.d.ts.map +1 -0
  189. package/dist/rules/contract-visibility.js +337 -0
  190. package/dist/rules/contract-visibility.js.map +1 -0
  191. package/dist/rules/data-guard.d.ts +7 -0
  192. package/dist/rules/data-guard.d.ts.map +1 -1
  193. package/dist/rules/data-guard.js +319 -37
  194. package/dist/rules/data-guard.js.map +1 -1
  195. package/dist/rules/layout.d.ts +4 -0
  196. package/dist/rules/layout.d.ts.map +1 -0
  197. package/dist/rules/layout.js +19 -0
  198. package/dist/rules/layout.js.map +1 -0
  199. package/dist/rules/legacy.d.ts.map +1 -1
  200. package/dist/rules/legacy.js +158 -43
  201. package/dist/rules/legacy.js.map +1 -1
  202. package/dist/rules/pages.d.ts +24 -0
  203. package/dist/rules/pages.d.ts.map +1 -1
  204. package/dist/rules/pages.js +221 -3
  205. package/dist/rules/pages.js.map +1 -1
  206. package/dist/rules/routes.d.ts +6 -0
  207. package/dist/rules/routes.d.ts.map +1 -1
  208. package/dist/rules/routes.js +111 -16
  209. package/dist/rules/routes.js.map +1 -1
  210. package/dist/rules/secrets.js +2 -2
  211. package/dist/rules/secrets.js.map +1 -1
  212. package/dist/rules/security.d.ts +12 -1
  213. package/dist/rules/security.d.ts.map +1 -1
  214. package/dist/rules/security.js +281 -68
  215. package/dist/rules/security.js.map +1 -1
  216. package/dist/rules/zones.d.ts +13 -0
  217. package/dist/rules/zones.d.ts.map +1 -1
  218. package/dist/rules/zones.js +192 -40
  219. package/dist/rules/zones.js.map +1 -1
  220. package/dist/scaffold.d.ts.map +1 -1
  221. package/dist/scaffold.js +6 -4
  222. package/dist/scaffold.js.map +1 -1
  223. package/dist/sdk.d.ts.map +1 -1
  224. package/dist/sdk.js +14 -5
  225. package/dist/sdk.js.map +1 -1
  226. package/dist/stylex-migrate.d.ts.map +1 -1
  227. package/dist/stylex-migrate.js +163 -7
  228. package/dist/stylex-migrate.js.map +1 -1
  229. package/dist/sync-manifest.d.ts +10 -2
  230. package/dist/sync-manifest.d.ts.map +1 -1
  231. package/dist/sync-manifest.js +42 -18
  232. package/dist/sync-manifest.js.map +1 -1
  233. package/dist/test-tool.d.ts.map +1 -1
  234. package/dist/test-tool.js +12 -3
  235. package/dist/test-tool.js.map +1 -1
  236. package/dist/typecheck-coverage.d.ts +20 -0
  237. package/dist/typecheck-coverage.d.ts.map +1 -0
  238. package/dist/typecheck-coverage.js +129 -0
  239. package/dist/typecheck-coverage.js.map +1 -0
  240. package/dist/upgrade.d.ts +69 -0
  241. package/dist/upgrade.d.ts.map +1 -1
  242. package/dist/upgrade.js +420 -77
  243. package/dist/upgrade.js.map +1 -1
  244. package/dist/verification-plan.d.ts.map +1 -1
  245. package/dist/verification-plan.js +11 -0
  246. package/dist/verification-plan.js.map +1 -1
  247. package/dist/version-drift.d.ts +22 -0
  248. package/dist/version-drift.d.ts.map +1 -0
  249. package/dist/version-drift.js +42 -0
  250. package/dist/version-drift.js.map +1 -0
  251. package/dist/web-app-options.d.ts +1 -1
  252. package/dist/web-app-options.d.ts.map +1 -1
  253. package/dist/web-app-options.js +1 -1
  254. package/dist/web-app-options.js.map +1 -1
  255. package/dist/work-graph.d.ts.map +1 -1
  256. package/dist/work-graph.js +20 -8
  257. package/dist/work-graph.js.map +1 -1
  258. package/docs/examples.json +54 -6
  259. package/docs/llms-full.txt +1539 -138
  260. package/docs/types.json +1456 -206
  261. package/package.json +11 -12
  262. package/src/agent-files.ts +203 -0
  263. package/src/app-files.ts +34 -4
  264. package/src/assure-hydration.ts +15 -2
  265. package/src/capabilities-tool.ts +15 -3
  266. package/src/check-diagnostics.ts +73 -9
  267. package/src/check-scan.ts +1773 -381
  268. package/src/check.ts +237 -76
  269. package/src/cli.ts +141 -5
  270. package/src/client-module.ts +61 -0
  271. package/src/command-catalog.ts +181 -47
  272. package/src/component-check.ts +79 -0
  273. package/src/contract-proof.ts +3 -25
  274. package/src/contracts.ts +60 -3
  275. package/src/db-tool.ts +5 -2
  276. package/src/dev-bun-config.ts +51 -10
  277. package/src/diagnostics.ts +3 -0
  278. package/src/diff-tool.ts +33 -26
  279. package/src/doctor.ts +43 -21
  280. package/src/env-file.ts +7 -0
  281. package/src/fix-recipes.ts +34 -3
  282. package/src/hand-built-manifest.ts +144 -0
  283. package/src/hooks-report.ts +135 -0
  284. package/src/init-agents.ts +17 -11
  285. package/src/internal/nifra-project.ts +191 -0
  286. package/src/internal/project-specifiers.ts +424 -0
  287. package/src/internal/script-kind.ts +2 -1
  288. package/src/internal/source-facts.ts +25 -0
  289. package/src/internal/typescript-import.ts +10 -3
  290. package/src/introspect.ts +65 -27
  291. package/src/levels-tool.ts +117 -20
  292. package/src/load.ts +78 -14
  293. package/src/mcp-context.ts +7 -7
  294. package/src/mcp-exec.ts +60 -17
  295. package/src/mcp-http.ts +1 -1
  296. package/src/mcp-io.ts +41 -0
  297. package/src/mcp-isolate.ts +9 -2
  298. package/src/mcp-project-child.ts +1 -1
  299. package/src/mcp-root.ts +58 -8
  300. package/src/mcp-ws.ts +4 -2
  301. package/src/mcp.ts +8 -3
  302. package/src/migrate-layout.ts +1109 -70
  303. package/src/output-schemas.ts +1084 -0
  304. package/src/port.ts +3 -2
  305. package/src/project-facts.ts +17 -3
  306. package/src/recipes/4.0.0.ts +2 -0
  307. package/src/recipes/5.0.0.ts +35 -0
  308. package/src/recipes/index.ts +10 -0
  309. package/src/replay.ts +14 -3
  310. package/src/review.ts +39 -10
  311. package/src/rules/codes.ts +10 -0
  312. package/src/rules/comment-markers.ts +57 -21
  313. package/src/rules/contract-visibility.ts +383 -0
  314. package/src/rules/data-guard.ts +351 -42
  315. package/src/rules/layout.ts +22 -0
  316. package/src/rules/legacy.ts +184 -51
  317. package/src/rules/pages.ts +238 -3
  318. package/src/rules/routes.ts +124 -16
  319. package/src/rules/secrets.ts +2 -2
  320. package/src/rules/security.ts +352 -71
  321. package/src/rules/zones.ts +205 -38
  322. package/src/scaffold.ts +6 -4
  323. package/src/sdk.ts +14 -5
  324. package/src/stylex-migrate.ts +134 -7
  325. package/src/sync-manifest.ts +48 -18
  326. package/src/test-tool.ts +18 -3
  327. package/src/typecheck-coverage.ts +134 -0
  328. package/src/upgrade.ts +487 -71
  329. package/src/verification-plan.ts +24 -0
  330. package/src/version-drift.ts +67 -0
  331. package/src/web-app-options.ts +2 -2
  332. package/src/work-graph.ts +17 -12
@@ -21,6 +21,14 @@ const GENERATED_MARKER = "GENERATED by @nifrajs/web generateServerManifest"
21
21
  // routes dir relative to the manifest and strips imports to route-relative keys.
22
22
  const ROUTES_PREFIX = /["'](\.{1,2}(?:\/[^"'/]+)*?\/routes\/)[^"']+["']/
23
23
  const IGNORED = /(^|\/)(node_modules|dist(-[a-z0-9]+)?|build|\.nifra|\.git|\.wrangler|coverage)\//
24
+ // The fs codes that mean the routes dir is absent or unreadable. Every other throw is the route table
25
+ // refusing to build, which must fail the command rather than read as "no manifest found".
26
+ const ROUTES_DIR_UNAVAILABLE = new Set(["ENOENT", "ENOTDIR", "EACCES", "EPERM"])
27
+
28
+ function routesDirUnavailable(error: unknown): boolean {
29
+ if (typeof error !== "object" || error === null || !("code" in error)) return false
30
+ return typeof error.code === "string" && ROUTES_DIR_UNAVAILABLE.has(error.code)
31
+ }
24
32
 
25
33
  export interface SyncManifestResult {
26
34
  /** The committed server-manifest file (relative to cwd). */
@@ -34,12 +42,16 @@ export interface SyncManifestResult {
34
42
  /** Set when the sync was REFUSED because discovery found zero routes for a manifest that had some
35
43
  * (`routes/` likely emptied by mistake) - the manifest was left untouched rather than wiped. */
36
44
  readonly refusedEmpty?: boolean
45
+ /** Set when the `routes/` tree could not be built into a route table (a duplicate route, two routes of
46
+ * one shape sharing a path, ...) - the manifest was left untouched and the sync failed. */
47
+ readonly error?: string
37
48
  }
38
49
 
39
50
  /**
40
51
  * Discover every generated `server-manifest.ts` under `cwd` and re-sync its route table from the live
41
52
  * `routes/` tree, preserving baked client assets. Returns one result per generated manifest. Never throws
42
- * on a per-manifest miss (an unreadable/absent routes dir is skipped).
53
+ * on a per-manifest failure: an absent or unreadable routes dir is skipped, and a route table that fails
54
+ * to build is reported in that manifest's `error`.
43
55
  */
44
56
  export async function syncServerManifests(cwd: string): Promise<SyncManifestResult[]> {
45
57
  const { discoverRoutes } = await import("@nifrajs/web/fs")
@@ -60,8 +72,11 @@ export async function syncServerManifests(cwd: string): Promise<SyncManifestResu
60
72
  let next: string
61
73
  try {
62
74
  next = resyncServerManifestSource(source, discoverRoutes(routesDir), routesPrefix)
63
- } catch {
64
- continue // routes dir gone/unreadable - not a manifest we can sync
75
+ } catch (error) {
76
+ if (routesDirUnavailable(error)) continue // not a manifest we can sync
77
+ const message = error instanceof Error ? error.message : String(error)
78
+ results.push({ file: rel, changed: false, added: [], removed: [], error: message })
79
+ continue
65
80
  }
66
81
  const before = parseManifestRouteFiles(source, routesPrefix)
67
82
  const after = parseManifestRouteFiles(next, routesPrefix)
@@ -81,33 +96,48 @@ export async function syncServerManifests(cwd: string): Promise<SyncManifestResu
81
96
  return results.sort((a, b) => codeUnitOrder(a.file, b.file))
82
97
  }
83
98
 
84
- /** CLI entry: sync every generated manifest, print a per-file summary + the client-rebuild caveat. */
85
- export async function runSyncManifest(cwd: string): Promise<boolean> {
86
- const results = await syncServerManifests(cwd)
87
- if (results.length === 0) {
88
- console.log("nifra sync-manifest: no generated server-manifest.ts found under this directory.")
89
- return true
90
- }
99
+ /** True when no manifest's route table failed to build. */
100
+ export function syncManifestsOk(results: readonly SyncManifestResult[]): boolean {
101
+ return results.every((r) => r.error === undefined)
102
+ }
103
+
104
+ /** Per-file summary lines + the client-rebuild caveat. */
105
+ export function renderSyncManifestResults(results: readonly SyncManifestResult[]): string[] {
106
+ if (results.length === 0)
107
+ return ["nifra sync-manifest: no generated server-manifest.ts found under this directory."]
108
+ const lines: string[] = []
91
109
  let anyChanged = false
92
110
  for (const r of results) {
93
- if (r.refusedEmpty) {
94
- console.log(
111
+ if (r.error !== undefined) {
112
+ lines.push(
113
+ `✖ ${r.file}: routes/ does not build a route table; left untouched.`,
114
+ ` ${r.error}`,
115
+ )
116
+ } else if (r.refusedEmpty) {
117
+ lines.push(
95
118
  `⚠ ${r.file}: refused - routes/ has zero routes but the manifest had some; left untouched (delete the manifest yourself if this is intentional).`,
96
119
  )
97
120
  } else if (r.changed) {
98
121
  anyChanged = true
99
- console.log(`✓ synced ${r.file}`)
100
- if (r.added.length > 0) console.log(` added: ${r.added.join(", ")}`)
101
- if (r.removed.length > 0) console.log(` removed: ${r.removed.join(", ")}`)
122
+ lines.push(`✓ synced ${r.file}`)
123
+ if (r.added.length > 0) lines.push(` added: ${r.added.join(", ")}`)
124
+ if (r.removed.length > 0) lines.push(` removed: ${r.removed.join(", ")}`)
102
125
  } else {
103
- console.log(`• ${r.file} already in sync`)
126
+ lines.push(`• ${r.file} already in sync`)
104
127
  }
105
128
  }
106
129
  if (anyChanged) {
107
- console.log(
130
+ lines.push(
108
131
  "\nRe-synced the server manifest's route table. If you added or changed a HYDRATING route\n" +
109
132
  "component (not just a loader / SSR-only route), also run a full build so the client bundle matches.",
110
133
  )
111
134
  }
112
- return true
135
+ return lines
136
+ }
137
+
138
+ /** CLI entry: sync every generated manifest and print the summary. False when a route table failed to build. */
139
+ export async function runSyncManifest(cwd: string): Promise<boolean> {
140
+ const results = await syncServerManifests(cwd)
141
+ console.log(renderSyncManifestResults(results).join("\n"))
142
+ return syncManifestsOk(results)
113
143
  }
package/src/test-tool.ts CHANGED
@@ -1,4 +1,9 @@
1
- import { type HeadTailOutput, readHeadTail } from "./mcp-io.ts"
1
+ import {
2
+ type HeadTailOutput,
3
+ OWN_PROCESS_GROUP,
4
+ readHeadTail,
5
+ signalProcessGroup,
6
+ } from "./mcp-io.ts"
2
7
  import { mcpProjectPathError, resolveMcpProjectPath } from "./mcp-path.ts"
3
8
 
4
9
  export interface TestToolArgs {
@@ -58,6 +63,8 @@ function normalizeTimeout(value: unknown): number {
58
63
  return Math.min(Math.max(Math.trunc(value), 1_000), MAX_TIMEOUT_MS)
59
64
  }
60
65
 
66
+ const KILL_GRACE_MS = 2_000
67
+
61
68
  const ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, "g")
62
69
 
63
70
  // A UTF-8 character is at most four bytes, so these hold every character the result can show.
@@ -146,17 +153,24 @@ export async function collectTestResult(
146
153
  env: process.env,
147
154
  stdout: "pipe",
148
155
  stderr: "pipe",
156
+ detached: OWN_PROCESS_GROUP,
149
157
  })
150
158
  let timedOut = false
151
159
  let cancelled = false
160
+ let escalation: ReturnType<typeof setTimeout> | undefined
161
+ // A test can ignore SIGTERM, so the group gets SIGKILL once the grace period runs out.
162
+ const stop = (): void => {
163
+ signalProcessGroup(proc, "SIGTERM")
164
+ escalation ??= setTimeout(() => signalProcessGroup(proc, "SIGKILL"), KILL_GRACE_MS)
165
+ }
152
166
  const abort = (): void => {
153
167
  cancelled = true
154
- proc.kill()
168
+ stop()
155
169
  }
156
170
  opts.signal?.addEventListener("abort", abort, { once: true })
157
171
  const timer = setTimeout(() => {
158
172
  timedOut = true
159
- proc.kill()
173
+ stop()
160
174
  }, timeoutMs)
161
175
  const empty: HeadTailOutput = { head: "", tail: "", droppedBytes: 0 }
162
176
  let stdoutRaw = empty
@@ -175,6 +189,7 @@ export async function collectTestResult(
175
189
  exitCode = result[2]
176
190
  } finally {
177
191
  clearTimeout(timer)
192
+ clearTimeout(escalation)
178
193
  opts.signal?.removeEventListener("abort", abort)
179
194
  }
180
195
  const stdout = clean(stdoutRaw)
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Which zoned files the typecheck never read. `tsc --noEmit` passes for code outside its program, so
3
+ * a stale tsconfig `include` (a folder moved under `backend/`, say) turns the contract gate into a
4
+ * pass that checked nothing there. The check runs tsc with `--listFiles` and compares the program
5
+ * against the app's zone folders.
6
+ */
7
+ import { existsSync, readFileSync, realpathSync } from "node:fs"
8
+ import { dirname, isAbsolute, join, relative, resolve } from "node:path"
9
+ import { stripComments } from "./check-scan.ts"
10
+
11
+ const ZONE_FOLDER = new Set(["routes", "frontend", "backend", "shared"])
12
+ const TS_SOURCE = /\.(?:[cm]?ts|tsx)$/
13
+ const NOT_PROGRAM_CODE = /\.d\.[cm]?ts$|\.(?:test|spec)\.[cm]?tsx?$/
14
+ const TSC_DIAGNOSTIC = /\(\d+,\d+\):\s*(?:error|warning)\s+TS\d+:/
15
+
16
+ /** Split `tsc --listFiles` output into the program's files and the compiler's own output. */
17
+ export function splitListedFiles(output: string): {
18
+ readonly programFiles: readonly string[]
19
+ readonly output: string
20
+ } {
21
+ const programFiles: string[] = []
22
+ const rest: string[] = []
23
+ for (const line of output.split(/\r?\n/)) {
24
+ if (line !== "" && line === line.trim() && isAbsolute(line) && !TSC_DIAGNOSTIC.test(line))
25
+ programFiles.push(line)
26
+ else rest.push(line)
27
+ }
28
+ return { programFiles, output: rest.join("\n").trim() }
29
+ }
30
+
31
+ const real = (path: string): string => {
32
+ try {
33
+ return realpathSync.native(path)
34
+ } catch {
35
+ return path
36
+ }
37
+ }
38
+
39
+ // tsc prints forward slashes, and on Windows the drive letter's case is not stable.
40
+ const canonical = (path: string): string => {
41
+ const posix = path.replaceAll("\\", "/")
42
+ return process.platform === "win32" ? posix.toLowerCase() : posix
43
+ }
44
+
45
+ /** The zone folder a project file sits in (`backend`, `apps/web/routes`), or `undefined`. */
46
+ export function zoneFolderOf(file: string): string | undefined {
47
+ const parts = file.split("/")
48
+ const at = parts.findIndex((part, i) => i < parts.length - 1 && ZONE_FOLDER.has(part))
49
+ return at === -1 ? undefined : parts.slice(0, at + 1).join("/")
50
+ }
51
+
52
+ /** True when a tsconfig between the app root and `cwd` makes the app its own program. */
53
+ function hasOwnProgram(cwd: string, app: string): boolean {
54
+ let dir = app
55
+ while (dir !== "" && dir !== ".") {
56
+ if (existsSync(join(cwd, dir, "tsconfig.json"))) return true
57
+ const parent = dirname(dir)
58
+ if (parent === dir) return false
59
+ dir = parent
60
+ }
61
+ return false
62
+ }
63
+
64
+ /**
65
+ * The `exclude` patterns of `<cwd>/tsconfig.json`, inherited through relative `extends` as tsc does,
66
+ * rewritten relative to `cwd`. A file a tsconfig excludes is out of the program on purpose.
67
+ */
68
+ export function tsconfigExcludes(cwd: string): string[] {
69
+ let path = join(cwd, "tsconfig.json")
70
+ for (let depth = 0; depth < 8 && existsSync(path); depth++) {
71
+ let config: unknown
72
+ try {
73
+ config = JSON.parse(stripComments(readFileSync(path, "utf8")).replace(/,(\s*[}\]])/g, "$1"))
74
+ } catch {
75
+ return []
76
+ }
77
+ if (typeof config !== "object" || config === null) return []
78
+ const exclude: unknown = Reflect.get(config, "exclude")
79
+ if (Array.isArray(exclude)) {
80
+ const from = dirname(path)
81
+ return exclude
82
+ .filter((pattern): pattern is string => typeof pattern === "string")
83
+ .map((pattern) => relative(cwd, resolve(from, pattern)).replaceAll("\\", "/"))
84
+ }
85
+ const base: unknown = Reflect.get(config, "extends")
86
+ if (typeof base !== "string" || !base.startsWith(".")) return []
87
+ path = resolve(dirname(path), base.endsWith(".json") ? base : `${base}.json`)
88
+ }
89
+ return []
90
+ }
91
+
92
+ // tsc reads a pattern that names a folder as everything below it.
93
+ const excludedBy = (patterns: readonly string[], file: string): boolean =>
94
+ patterns.some(
95
+ (pattern) => new Bun.Glob(pattern).match(file) || new Bun.Glob(`${pattern}/**`).match(file),
96
+ )
97
+
98
+ /**
99
+ * The TypeScript files under a zone folder that `programFiles` does not contain, project-relative and
100
+ * in input order. Declaration files and tests are left out: a project may typecheck tests with a
101
+ * separate config. So is an app nested under its own tsconfig, which this program does not own, and a
102
+ * file the tsconfig's `exclude` names.
103
+ */
104
+ export function filesOutsideProgram(
105
+ cwd: string,
106
+ files: readonly string[],
107
+ programFiles: readonly string[],
108
+ ): string[] {
109
+ const program = new Set<string>()
110
+ for (const file of programFiles) {
111
+ program.add(canonical(file))
112
+ if (!file.includes("/node_modules/")) program.add(canonical(real(file)))
113
+ }
114
+ const ownProgram = new Map<string, boolean>()
115
+ const excluded = tsconfigExcludes(cwd)
116
+ const outside: string[] = []
117
+ for (const file of files) {
118
+ if (!TS_SOURCE.test(file) || NOT_PROGRAM_CODE.test(file)) continue
119
+ if (excludedBy(excluded, file)) continue
120
+ const folder = zoneFolderOf(file)
121
+ if (folder === undefined) continue
122
+ const app = dirname(folder)
123
+ let own = ownProgram.get(app)
124
+ if (own === undefined) {
125
+ own = hasOwnProgram(cwd, app)
126
+ ownProgram.set(app, own)
127
+ }
128
+ if (own) continue
129
+ const abs = join(cwd, file)
130
+ if (program.has(canonical(abs)) || program.has(canonical(real(abs)))) continue
131
+ outside.push(file)
132
+ }
133
+ return outside
134
+ }