@hominis/fireforge 0.35.0 → 0.37.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 (369) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +34 -9
  3. package/dist/bin/fireforge.js +21 -6
  4. package/dist/src/cli.js +22 -5
  5. package/dist/src/commands/bootstrap-checks.js +0 -1
  6. package/dist/src/commands/bootstrap.d.ts +2 -1
  7. package/dist/src/commands/bootstrap.js +16 -7
  8. package/dist/src/commands/build.js +5 -4
  9. package/dist/src/commands/config.js +30 -10
  10. package/dist/src/commands/discard.js +18 -3
  11. package/dist/src/commands/doctor/post-rebase-audit.js +0 -1
  12. package/dist/src/commands/doctor-check-core.js +0 -1
  13. package/dist/src/commands/doctor-external-toolchains.d.ts +3 -0
  14. package/dist/src/commands/doctor-external-toolchains.js +54 -0
  15. package/dist/src/commands/doctor-furnace-jar.js +0 -1
  16. package/dist/src/commands/doctor-furnace-manifest-sync.js +0 -1
  17. package/dist/src/commands/doctor-furnace.js +2 -2
  18. package/dist/src/commands/doctor-orphaned-harness.d.ts +52 -0
  19. package/dist/src/commands/doctor-orphaned-harness.js +132 -0
  20. package/dist/src/commands/doctor-working-tree.js +0 -1
  21. package/dist/src/commands/doctor.js +6 -1
  22. package/dist/src/commands/download.js +28 -13
  23. package/dist/src/commands/export-all.js +5 -3
  24. package/dist/src/commands/export-flow.js +12 -7
  25. package/dist/src/commands/export-placement-gate.js +0 -1
  26. package/dist/src/commands/export-placement-policy.js +0 -1
  27. package/dist/src/commands/export-shared.d.ts +5 -1
  28. package/dist/src/commands/export-shared.js +18 -6
  29. package/dist/src/commands/export.js +22 -10
  30. package/dist/src/commands/furnace/apply.js +22 -6
  31. package/dist/src/commands/furnace/chrome-doc-remove.js +0 -1
  32. package/dist/src/commands/furnace/chrome-doc-templates.js +0 -1
  33. package/dist/src/commands/furnace/chrome-doc-tests.js +0 -1
  34. package/dist/src/commands/furnace/chrome-doc.js +0 -1
  35. package/dist/src/commands/furnace/create-browser-test.js +0 -1
  36. package/dist/src/commands/furnace/create-dry-run.js +0 -1
  37. package/dist/src/commands/furnace/create-features.js +0 -1
  38. package/dist/src/commands/furnace/create-mochikit.js +0 -1
  39. package/dist/src/commands/furnace/create-readback.js +0 -1
  40. package/dist/src/commands/furnace/create-templates.js +0 -1
  41. package/dist/src/commands/furnace/create-validation.js +0 -1
  42. package/dist/src/commands/furnace/create-xpcshell.js +0 -1
  43. package/dist/src/commands/furnace/create.js +0 -1
  44. package/dist/src/commands/furnace/deploy.js +7 -83
  45. package/dist/src/commands/furnace/diff.js +0 -1
  46. package/dist/src/commands/furnace/index.js +59 -25
  47. package/dist/src/commands/furnace/init.js +0 -1
  48. package/dist/src/commands/furnace/list.js +0 -1
  49. package/dist/src/commands/furnace/override.js +0 -1
  50. package/dist/src/commands/furnace/preview.js +3 -3
  51. package/dist/src/commands/furnace/refresh.js +0 -1
  52. package/dist/src/commands/furnace/remove-state.d.ts +15 -0
  53. package/dist/src/commands/furnace/remove-state.js +47 -1
  54. package/dist/src/commands/furnace/remove.js +3 -9
  55. package/dist/src/commands/furnace/rename-browser-test.js +1 -5
  56. package/dist/src/commands/furnace/rename-helpers.d.ts +6 -0
  57. package/dist/src/commands/furnace/rename-helpers.js +30 -1
  58. package/dist/src/commands/furnace/rename-xpcshell.js +7 -6
  59. package/dist/src/commands/furnace/rename.js +15 -36
  60. package/dist/src/commands/furnace/scan.js +0 -1
  61. package/dist/src/commands/furnace/status.js +0 -1
  62. package/dist/src/commands/furnace/sync.js +76 -4
  63. package/dist/src/commands/furnace/validate.js +0 -1
  64. package/dist/src/commands/furnace/validation-output.js +0 -1
  65. package/dist/src/commands/import.js +35 -9
  66. package/dist/src/commands/lint-per-patch.js +19 -7
  67. package/dist/src/commands/lint.js +13 -3
  68. package/dist/src/commands/manifest.js +1 -2
  69. package/dist/src/commands/package.js +2 -1
  70. package/dist/src/commands/patch/compact.js +1 -1
  71. package/dist/src/commands/patch/delete.js +1 -1
  72. package/dist/src/commands/patch/index.js +1 -1
  73. package/dist/src/commands/patch/lint-ignore.js +21 -6
  74. package/dist/src/commands/patch/move-files.js +1 -1
  75. package/dist/src/commands/patch/patch-context.js +0 -1
  76. package/dist/src/commands/patch/rename.js +4 -4
  77. package/dist/src/commands/patch/reorder.js +3 -4
  78. package/dist/src/commands/patch/split-plan.js +2 -1
  79. package/dist/src/commands/patch/split.js +4 -4
  80. package/dist/src/commands/patch/staged-dependency.d.ts +1 -1
  81. package/dist/src/commands/patch/staged-dependency.js +114 -52
  82. package/dist/src/commands/patch/tier.js +18 -5
  83. package/dist/src/commands/re-export-bulk-scan.js +0 -1
  84. package/dist/src/commands/re-export-files.js +5 -1
  85. package/dist/src/commands/re-export-options.js +0 -1
  86. package/dist/src/commands/re-export-scan.js +0 -1
  87. package/dist/src/commands/re-export.js +14 -4
  88. package/dist/src/commands/rebase/abort.js +2 -9
  89. package/dist/src/commands/rebase/confirm.js +0 -1
  90. package/dist/src/commands/rebase/conflict-summary.js +0 -1
  91. package/dist/src/commands/rebase/continue.js +0 -1
  92. package/dist/src/commands/rebase/index.d.ts +1 -1
  93. package/dist/src/commands/rebase/index.js +10 -15
  94. package/dist/src/commands/rebase/patch-loop.js +38 -7
  95. package/dist/src/commands/rebase/summary.js +2 -3
  96. package/dist/src/commands/rebase.js +0 -1
  97. package/dist/src/commands/register.js +1 -1
  98. package/dist/src/commands/reset.js +16 -9
  99. package/dist/src/commands/resolve.js +2 -2
  100. package/dist/src/commands/run.js +4 -4
  101. package/dist/src/commands/setup-support.js +0 -1
  102. package/dist/src/commands/setup.js +1 -2
  103. package/dist/src/commands/source.js +1 -2
  104. package/dist/src/commands/status-output.js +1 -12
  105. package/dist/src/commands/status.js +147 -133
  106. package/dist/src/commands/test-appdir.js +0 -1
  107. package/dist/src/commands/test-diagnose.js +38 -3
  108. package/dist/src/commands/test-modes.d.ts +15 -0
  109. package/dist/src/commands/test-modes.js +61 -0
  110. package/dist/src/commands/test-register.d.ts +4 -0
  111. package/dist/src/commands/test-register.js +64 -0
  112. package/dist/src/commands/test-run.d.ts +23 -7
  113. package/dist/src/commands/test-run.js +15 -14
  114. package/dist/src/commands/test.d.ts +0 -4
  115. package/dist/src/commands/test.js +147 -96
  116. package/dist/src/commands/token-coverage.js +0 -1
  117. package/dist/src/commands/token.js +1 -2
  118. package/dist/src/commands/typecheck.js +1 -1
  119. package/dist/src/commands/verify.js +3 -9
  120. package/dist/src/commands/watch.js +5 -1
  121. package/dist/src/commands/wire.js +1 -1
  122. package/dist/src/core/ast-utils.js +0 -1
  123. package/dist/src/core/brand-validation.js +0 -1
  124. package/dist/src/core/branding.js +0 -1
  125. package/dist/src/core/browser-wire.js +0 -1
  126. package/dist/src/core/build-audit-platform.js +0 -1
  127. package/dist/src/core/build-audit-registration.js +0 -1
  128. package/dist/src/core/build-audit-resolve.js +0 -1
  129. package/dist/src/core/build-audit-transforms.js +0 -1
  130. package/dist/src/core/build-audit.js +0 -1
  131. package/dist/src/core/build-baseline-types.d.ts +24 -0
  132. package/dist/src/core/build-baseline-types.js +0 -1
  133. package/dist/src/core/build-baseline.d.ts +5 -2
  134. package/dist/src/core/build-baseline.js +5 -2
  135. package/dist/src/core/build-prepare.js +2 -2
  136. package/dist/src/core/config-mutate.js +0 -1
  137. package/dist/src/core/config-paths.d.ts +2 -2
  138. package/dist/src/core/config-paths.js +6 -1
  139. package/dist/src/core/config-state.js +0 -1
  140. package/dist/src/core/config-validate-patch-policy.js +0 -1
  141. package/dist/src/core/config-validate-test-toolchains.d.ts +8 -0
  142. package/dist/src/core/config-validate-test-toolchains.js +105 -0
  143. package/dist/src/core/config-validate.js +3 -1
  144. package/dist/src/core/config.js +0 -1
  145. package/dist/src/core/destructive.js +0 -1
  146. package/dist/src/core/diff-hunks.js +13 -7
  147. package/dist/src/core/engine-changes.js +0 -1
  148. package/dist/src/core/engine-session-lock.d.ts +12 -0
  149. package/dist/src/core/engine-session-lock.js +45 -0
  150. package/dist/src/core/file-lock.d.ts +8 -0
  151. package/dist/src/core/file-lock.js +157 -48
  152. package/dist/src/core/firefox-archive.d.ts +4 -0
  153. package/dist/src/core/firefox-archive.js +6 -1
  154. package/dist/src/core/firefox-cache.js +60 -4
  155. package/dist/src/core/firefox-download.js +0 -1
  156. package/dist/src/core/firefox-extract.js +75 -16
  157. package/dist/src/core/firefox-ignorefile.js +0 -1
  158. package/dist/src/core/firefox.d.ts +12 -0
  159. package/dist/src/core/firefox.js +64 -2
  160. package/dist/src/core/furnace-apply-dry-run.js +0 -1
  161. package/dist/src/core/furnace-apply-ftl.d.ts +7 -3
  162. package/dist/src/core/furnace-apply-ftl.js +10 -5
  163. package/dist/src/core/furnace-apply-helpers.js +0 -1
  164. package/dist/src/core/furnace-apply-output.js +2 -2
  165. package/dist/src/core/furnace-apply.d.ts +7 -1
  166. package/dist/src/core/furnace-apply.js +47 -13
  167. package/dist/src/core/furnace-checksum-utils.js +0 -1
  168. package/dist/src/core/furnace-config-array-utils.js +0 -1
  169. package/dist/src/core/furnace-config-custom.js +10 -1
  170. package/dist/src/core/furnace-config-order.js +0 -1
  171. package/dist/src/core/furnace-config-tokens.js +0 -1
  172. package/dist/src/core/furnace-config-validate.js +0 -1
  173. package/dist/src/core/furnace-config.d.ts +11 -0
  174. package/dist/src/core/furnace-config.js +37 -1
  175. package/dist/src/core/furnace-constants.js +0 -1
  176. package/dist/src/core/furnace-css-fragments.js +0 -1
  177. package/dist/src/core/furnace-graph-utils.js +0 -1
  178. package/dist/src/core/furnace-jsconfig.d.ts +2 -3
  179. package/dist/src/core/furnace-jsconfig.js +25 -18
  180. package/dist/src/core/furnace-marker.js +0 -1
  181. package/dist/src/core/furnace-operation.js +0 -1
  182. package/dist/src/core/furnace-refresh.js +0 -1
  183. package/dist/src/core/furnace-registration-ast.d.ts +14 -0
  184. package/dist/src/core/furnace-registration-ast.js +1 -2
  185. package/dist/src/core/furnace-registration-remove.js +0 -1
  186. package/dist/src/core/furnace-registration-validate.js +1 -5
  187. package/dist/src/core/furnace-registration.js +0 -1
  188. package/dist/src/core/furnace-rollback.js +0 -1
  189. package/dist/src/core/furnace-scanner.js +0 -1
  190. package/dist/src/core/furnace-stale-export.d.ts +59 -0
  191. package/dist/src/core/furnace-stale-export.js +123 -0
  192. package/dist/src/core/furnace-staleness.js +0 -1
  193. package/dist/src/core/furnace-state-persist.d.ts +72 -0
  194. package/dist/src/core/furnace-state-persist.js +118 -0
  195. package/dist/src/core/furnace-step-errors.d.ts +23 -0
  196. package/dist/src/core/furnace-step-errors.js +25 -0
  197. package/dist/src/core/furnace-stories.js +0 -1
  198. package/dist/src/core/furnace-validate-accessibility.js +0 -1
  199. package/dist/src/core/furnace-validate-checks.js +0 -1
  200. package/dist/src/core/furnace-validate-compatibility.js +9 -3
  201. package/dist/src/core/furnace-validate-helpers.js +0 -1
  202. package/dist/src/core/furnace-validate-registration.js +7 -7
  203. package/dist/src/core/furnace-validate-structure.d.ts +8 -0
  204. package/dist/src/core/furnace-validate-structure.js +14 -4
  205. package/dist/src/core/furnace-validate.js +0 -1
  206. package/dist/src/core/furnace-version-drift.d.ts +6 -3
  207. package/dist/src/core/furnace-version-drift.js +6 -4
  208. package/dist/src/core/git-base.js +0 -1
  209. package/dist/src/core/git-diff.js +0 -1
  210. package/dist/src/core/git-file-ops.js +0 -1
  211. package/dist/src/core/git-status.d.ts +13 -0
  212. package/dist/src/core/git-status.js +32 -2
  213. package/dist/src/core/git.d.ts +10 -1
  214. package/dist/src/core/git.js +63 -6
  215. package/dist/src/core/license-headers.js +0 -1
  216. package/dist/src/core/lint-cache.js +0 -1
  217. package/dist/src/core/lint-projection.js +0 -1
  218. package/dist/src/core/mach-build-artifacts.js +0 -1
  219. package/dist/src/core/mach-error-hints.js +0 -1
  220. package/dist/src/core/mach-known-noise-filter.d.ts +60 -0
  221. package/dist/src/core/mach-known-noise-filter.js +193 -0
  222. package/dist/src/core/mach-mozconfig.js +0 -1
  223. package/dist/src/core/mach-python.js +0 -1
  224. package/dist/src/core/mach-resource-shim.js +0 -1
  225. package/dist/src/core/mach.d.ts +8 -0
  226. package/dist/src/core/mach.js +36 -4
  227. package/dist/src/core/manifest-helpers.js +0 -1
  228. package/dist/src/core/manifest-register.js +0 -1
  229. package/dist/src/core/manifest-rules.js +0 -1
  230. package/dist/src/core/manifest-tokenizers.js +0 -1
  231. package/dist/src/core/marionette-port.d.ts +8 -0
  232. package/dist/src/core/marionette-port.js +34 -3
  233. package/dist/src/core/marionette-preflight.js +0 -1
  234. package/dist/src/core/markdown-table.js +0 -1
  235. package/dist/src/core/ownership-table.js +2 -9
  236. package/dist/src/core/parser-fallback.js +0 -1
  237. package/dist/src/core/patch-apply-fuzz.d.ts +27 -9
  238. package/dist/src/core/patch-apply-fuzz.js +71 -34
  239. package/dist/src/core/patch-apply.d.ts +30 -0
  240. package/dist/src/core/patch-apply.js +101 -18
  241. package/dist/src/core/patch-artifact-normalize.d.ts +18 -5
  242. package/dist/src/core/patch-artifact-normalize.js +19 -15
  243. package/dist/src/core/patch-export-coverage.js +0 -1
  244. package/dist/src/core/patch-export-metadata.js +0 -1
  245. package/dist/src/core/patch-export-update.js +2 -3
  246. package/dist/src/core/patch-export.d.ts +2 -6
  247. package/dist/src/core/patch-export.js +24 -48
  248. package/dist/src/core/patch-files.js +0 -1
  249. package/dist/src/core/patch-identifier-suggest.js +0 -1
  250. package/dist/src/core/patch-lint-checkjs.js +0 -1
  251. package/dist/src/core/patch-lint-chrome-jsdoc.js +0 -1
  252. package/dist/src/core/patch-lint-cross.js +80 -3
  253. package/dist/src/core/patch-lint-css.js +0 -1
  254. package/dist/src/core/patch-lint-diff-tag.js +0 -1
  255. package/dist/src/core/patch-lint-diff.d.ts +2 -1
  256. package/dist/src/core/patch-lint-diff.js +17 -32
  257. package/dist/src/core/patch-lint-imports.js +0 -1
  258. package/dist/src/core/patch-lint-jsdoc.js +0 -1
  259. package/dist/src/core/patch-lint-observer.js +0 -1
  260. package/dist/src/core/patch-lint-ownership.js +0 -1
  261. package/dist/src/core/patch-lint-reexports.js +0 -1
  262. package/dist/src/core/patch-lint.d.ts +37 -2
  263. package/dist/src/core/patch-lint.js +47 -34
  264. package/dist/src/core/patch-lock.js +0 -1
  265. package/dist/src/core/patch-manifest-consistency.js +9 -1
  266. package/dist/src/core/patch-manifest-io.d.ts +19 -2
  267. package/dist/src/core/patch-manifest-io.js +57 -21
  268. package/dist/src/core/patch-manifest-query.js +18 -10
  269. package/dist/src/core/patch-manifest-resolve.js +0 -1
  270. package/dist/src/core/patch-manifest-validate.js +24 -1
  271. package/dist/src/core/patch-manifest.d.ts +1 -1
  272. package/dist/src/core/patch-manifest.js +1 -2
  273. package/dist/src/core/patch-parse.d.ts +59 -0
  274. package/dist/src/core/patch-parse.js +209 -92
  275. package/dist/src/core/patch-policy.js +0 -1
  276. package/dist/src/core/patch-registration-refs.js +16 -26
  277. package/dist/src/core/patch-source-metadata.js +0 -1
  278. package/dist/src/core/patch-transform.d.ts +4 -0
  279. package/dist/src/core/patch-transform.js +30 -38
  280. package/dist/src/core/rebase-session.js +0 -1
  281. package/dist/src/core/register-browser-content.js +0 -1
  282. package/dist/src/core/register-module.js +0 -1
  283. package/dist/src/core/register-result.js +0 -1
  284. package/dist/src/core/register-scaffold.js +0 -1
  285. package/dist/src/core/register-shared-css.js +0 -1
  286. package/dist/src/core/register-test-manifest.js +0 -1
  287. package/dist/src/core/register-xpcshell-test.js +0 -1
  288. package/dist/src/core/shared-ftl.js +0 -1
  289. package/dist/src/core/signal-critical.js +0 -1
  290. package/dist/src/core/smoke-patterns.js +0 -1
  291. package/dist/src/core/state-file.js +0 -1
  292. package/dist/src/core/status-classify.d.ts +16 -0
  293. package/dist/src/core/status-classify.js +24 -16
  294. package/dist/src/core/test-harness-crash.d.ts +80 -7
  295. package/dist/src/core/test-harness-crash.js +251 -15
  296. package/dist/src/core/test-harness-output.js +0 -1
  297. package/dist/src/core/test-path-scope.d.ts +21 -13
  298. package/dist/src/core/test-path-scope.js +43 -28
  299. package/dist/src/core/test-stale-check.d.ts +29 -1
  300. package/dist/src/core/test-stale-check.js +59 -1
  301. package/dist/src/core/test-stale-symlink.js +0 -1
  302. package/dist/src/core/test-xpcshell-retry.js +0 -1
  303. package/dist/src/core/token-coverage.js +0 -1
  304. package/dist/src/core/token-dark-mode.js +0 -1
  305. package/dist/src/core/token-docs.js +0 -1
  306. package/dist/src/core/token-manager.js +0 -1
  307. package/dist/src/core/token-scaffold.js +0 -1
  308. package/dist/src/core/token-variant.js +0 -1
  309. package/dist/src/core/toolchain-preflight.d.ts +31 -10
  310. package/dist/src/core/toolchain-preflight.js +97 -36
  311. package/dist/src/core/typecheck-shim.js +13 -1
  312. package/dist/src/core/typecheck.js +0 -1
  313. package/dist/src/core/wire-destroy.js +0 -1
  314. package/dist/src/core/wire-dom-fragment.js +0 -1
  315. package/dist/src/core/wire-init.js +0 -1
  316. package/dist/src/core/wire-subscript.js +0 -1
  317. package/dist/src/core/wire-targets.js +0 -1
  318. package/dist/src/core/wire-utils.js +0 -1
  319. package/dist/src/core/xpcshell-appdir.js +1 -4
  320. package/dist/src/errors/base.d.ts +29 -0
  321. package/dist/src/errors/base.js +35 -1
  322. package/dist/src/errors/build.js +0 -1
  323. package/dist/src/errors/codes.d.ts +6 -0
  324. package/dist/src/errors/codes.js +6 -1
  325. package/dist/src/errors/config.js +0 -1
  326. package/dist/src/errors/download.js +0 -1
  327. package/dist/src/errors/furnace.d.ts +11 -1
  328. package/dist/src/errors/furnace.js +6 -5
  329. package/dist/src/errors/git.js +0 -1
  330. package/dist/src/errors/patch.d.ts +17 -0
  331. package/dist/src/errors/patch.js +29 -1
  332. package/dist/src/errors/rebase.js +0 -1
  333. package/dist/src/errors/run.js +1 -1
  334. package/dist/src/index.js +0 -1
  335. package/dist/src/types/cli.js +0 -1
  336. package/dist/src/types/commands/index.d.ts +1 -1
  337. package/dist/src/types/commands/index.js +0 -1
  338. package/dist/src/types/commands/options.d.ts +40 -9
  339. package/dist/src/types/commands/options.js +0 -1
  340. package/dist/src/types/commands/patches.d.ts +37 -0
  341. package/dist/src/types/commands/patches.js +0 -1
  342. package/dist/src/types/commands/project.d.ts +11 -2
  343. package/dist/src/types/commands/project.js +0 -1
  344. package/dist/src/types/config.d.ts +32 -0
  345. package/dist/src/types/config.js +0 -1
  346. package/dist/src/types/furnace.d.ts +23 -0
  347. package/dist/src/types/furnace.js +0 -1
  348. package/dist/src/types/index.js +0 -1
  349. package/dist/src/types/typecheck.js +0 -1
  350. package/dist/src/utils/elapsed.js +0 -1
  351. package/dist/src/utils/errors.js +0 -1
  352. package/dist/src/utils/fs.js +37 -5
  353. package/dist/src/utils/logger.d.ts +7 -0
  354. package/dist/src/utils/logger.js +85 -3
  355. package/dist/src/utils/options.d.ts +15 -0
  356. package/dist/src/utils/options.js +25 -1
  357. package/dist/src/utils/package-root.js +11 -4
  358. package/dist/src/utils/parse.js +0 -1
  359. package/dist/src/utils/paths.js +0 -1
  360. package/dist/src/utils/platform.js +0 -1
  361. package/dist/src/utils/process-group.d.ts +33 -0
  362. package/dist/src/utils/process-group.js +161 -0
  363. package/dist/src/utils/process.d.ts +48 -4
  364. package/dist/src/utils/process.js +258 -59
  365. package/dist/src/utils/regex.d.ts +13 -3
  366. package/dist/src/utils/regex.js +53 -4
  367. package/dist/src/utils/validation.d.ts +0 -12
  368. package/dist/src/utils/validation.js +13 -26
  369. package/package.json +9 -8
@@ -1,8 +1,9 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { access, chmod, copyFile as fsCopyFile, lstat, mkdir, open, readdir, readFile, rename, rm, stat, statfs, } from 'node:fs/promises';
4
- import { dirname, join } from 'node:path';
4
+ import { basename, dirname, join } from 'node:path';
5
5
  const RETRIABLE_REMOVE_ERRORS = new Set(['ENOTEMPTY', 'EBUSY', 'EPERM']);
6
+ const RETRIABLE_RENAME_ERRORS = new Set(['EPERM', 'EACCES', 'EBUSY']);
6
7
  function sleep(ms) {
7
8
  return new Promise((resolve) => {
8
9
  setTimeout(resolve, ms);
@@ -199,7 +200,7 @@ export async function writeFileAtomic(path, content) {
199
200
  if (existingMode !== undefined) {
200
201
  await chmod(tempPath, existingMode);
201
202
  }
202
- await rename(tempPath, path);
203
+ await renameWithRetries(tempPath, path);
203
204
  await syncParentDir(path);
204
205
  }
205
206
  catch (error) {
@@ -207,6 +208,31 @@ export async function writeFileAtomic(path, content) {
207
208
  throw error;
208
209
  }
209
210
  }
211
+ /**
212
+ * Renames the temp file over its destination, retrying transient sharing
213
+ * violations. On Windows, a rename onto a target that another writer is
214
+ * concurrently replacing — or that an antivirus/indexer briefly holds open —
215
+ * fails with EPERM/EACCES/EBUSY even though the same rename succeeds a moment
216
+ * later; POSIX renames never hit this. Non-retriable codes and exhausted
217
+ * budgets rethrow, so a genuine permission problem still surfaces.
218
+ */
219
+ async function renameWithRetries(from, to) {
220
+ for (let attempt = 0;; attempt++) {
221
+ try {
222
+ await rename(from, to);
223
+ return;
224
+ }
225
+ catch (error) {
226
+ const code = error instanceof Error && 'code' in error && typeof error.code === 'string'
227
+ ? error.code
228
+ : undefined;
229
+ if (!code || !RETRIABLE_RENAME_ERRORS.has(code) || attempt === 4) {
230
+ throw error;
231
+ }
232
+ await sleep(25 * (attempt + 1));
233
+ }
234
+ }
235
+ }
210
236
  async function syncParentDir(path) {
211
237
  let directoryHandle;
212
238
  try {
@@ -270,7 +296,10 @@ export const FIREFORGE_TMP_PATH_PATTERN = /(^|\/)\.[^/]+\.fireforge-tmp-\d+-[0-9
270
296
  */
271
297
  function createAtomicTempPath(path) {
272
298
  const directory = dirname(path);
273
- const filename = path.slice(directory.length + 1);
299
+ // basename, not slice(dirname.length + 1): when dirname ends with the
300
+ // separator (filesystem/drive roots, e.g. dirname('/foo') === '/'), the
301
+ // slice arithmetic chopped the first filename character ('/foo' → 'oo').
302
+ const filename = basename(path);
274
303
  return join(directory, `.${filename}.fireforge-tmp-${process.pid}-${randomUUID()}`);
275
304
  }
276
305
  /**
@@ -287,7 +316,11 @@ function createAtomicTempPath(path) {
287
316
  export async function checkDiskSpace(path, minBytes, onLowSpace) {
288
317
  try {
289
318
  const stats = await statfs(path);
290
- const availableBytes = stats.bfree * stats.bsize;
319
+ // bavail = blocks available to UNPRIVILEGED users; bfree includes the
320
+ // root-reserved blocks (typically 5% on ext4), so it over-reported free
321
+ // space and silently suppressed the low-space warning exactly when it
322
+ // mattered.
323
+ const availableBytes = stats.bavail * stats.bsize;
291
324
  if (availableBytes < minBytes) {
292
325
  const availableGB = (availableBytes / (1024 * 1024 * 1024)).toFixed(1);
293
326
  const requiredGB = (minBytes / (1024 * 1024 * 1024)).toFixed(1);
@@ -302,4 +335,3 @@ export async function checkDiskSpace(path, minBytes, onLowSpace) {
302
335
  return undefined;
303
336
  }
304
337
  }
305
- //# sourceMappingURL=fs.js.map
@@ -3,6 +3,13 @@
3
3
  * @param enabled - Whether to enable verbose output
4
4
  */
5
5
  export declare function setVerbose(enabled: boolean): void;
6
+ /**
7
+ * Switches the logger into (or out of) machine-output mode. Commands with
8
+ * `--json`/`--raw` flags call this before producing any output.
9
+ */
10
+ export declare function setMachineOutputMode(enabled: boolean): void;
11
+ /** True when machine-output mode is active. */
12
+ export declare function isMachineOutputMode(): boolean;
6
13
  /**
7
14
  * Displays a verbose/debug message (only shown if verbose mode is enabled).
8
15
  * @param message - Message to display
@@ -3,6 +3,19 @@ import * as p from '@clack/prompts';
3
3
  import pc from 'picocolors';
4
4
  /** Whether verbose mode is enabled */
5
5
  let verboseMode = false;
6
+ /**
7
+ * Whether machine-output mode is active (`--json` / `--raw`).
8
+ *
9
+ * In machine mode, stdout belongs EXCLUSIVELY to the machine-readable
10
+ * payload: all human-facing diagnostics (intro/outro banners, info, warn,
11
+ * error, steps, notes) are routed to stderr as plain unstyled lines.
12
+ * Before this mode existed, clack's log helpers wrote styled warnings to
13
+ * stdout *before* the JSON body — a truncated-directory warning during
14
+ * `status --json` broke every `JSON.parse(stdout)` consumer, and error
15
+ * objects on the machine contract were followed by a styled duplicate on
16
+ * the same stream.
17
+ */
18
+ let machineOutputMode = false;
6
19
  /**
7
20
  * Enables or disables verbose mode.
8
21
  * @param enabled - Whether to enable verbose output
@@ -10,6 +23,21 @@ let verboseMode = false;
10
23
  export function setVerbose(enabled) {
11
24
  verboseMode = enabled;
12
25
  }
26
+ /**
27
+ * Switches the logger into (or out of) machine-output mode. Commands with
28
+ * `--json`/`--raw` flags call this before producing any output.
29
+ */
30
+ export function setMachineOutputMode(enabled) {
31
+ machineOutputMode = enabled;
32
+ }
33
+ /** True when machine-output mode is active. */
34
+ export function isMachineOutputMode() {
35
+ return machineOutputMode;
36
+ }
37
+ /** Writes a plain diagnostic line to stderr (machine-mode side channel). */
38
+ function writeDiagnostic(prefix, message) {
39
+ process.stderr.write(`${prefix}${message}\n`);
40
+ }
13
41
  /**
14
42
  * Checks if verbose mode is enabled.
15
43
  * @returns True if verbose mode is enabled
@@ -22,43 +50,75 @@ function isVerbose() {
22
50
  * @param message - Message to display
23
51
  */
24
52
  export function verbose(message) {
25
- if (isVerbose()) {
26
- p.log.info(`[debug] ${message}`);
53
+ if (!isVerbose())
54
+ return;
55
+ if (machineOutputMode) {
56
+ writeDiagnostic('[debug] ', message);
57
+ return;
27
58
  }
59
+ p.log.info(`[debug] ${message}`);
28
60
  }
29
61
  function supportsInteractiveSpinner() {
30
62
  return process.stdout.isTTY && process.stderr.isTTY;
31
63
  }
32
64
  /** Displays the top-level intro banner for a command. */
33
65
  export function intro(message) {
66
+ if (machineOutputMode)
67
+ return;
34
68
  p.intro(message);
35
69
  }
36
70
  /** Displays the closing outro banner for a command. */
37
71
  export function outro(message) {
72
+ if (machineOutputMode)
73
+ return;
38
74
  p.outro(message);
39
75
  }
40
76
  /** Logs an informational message. */
41
77
  export function info(message) {
78
+ if (machineOutputMode) {
79
+ writeDiagnostic('', message);
80
+ return;
81
+ }
42
82
  p.log.info(message);
43
83
  }
44
84
  /** Logs a success message. */
45
85
  export function success(message) {
86
+ if (machineOutputMode) {
87
+ writeDiagnostic('', message);
88
+ return;
89
+ }
46
90
  p.log.success(message);
47
91
  }
48
92
  /** Logs a warning message. */
49
93
  export function warn(message) {
94
+ if (machineOutputMode) {
95
+ writeDiagnostic('warning: ', message);
96
+ return;
97
+ }
50
98
  p.log.warn(message);
51
99
  }
52
100
  /** Logs an error message. */
53
101
  export function error(message) {
102
+ if (machineOutputMode) {
103
+ writeDiagnostic('error: ', message);
104
+ return;
105
+ }
54
106
  p.log.error(message);
55
107
  }
56
108
  /** Logs an in-progress step message. */
57
109
  export function step(message) {
110
+ if (machineOutputMode) {
111
+ writeDiagnostic('', message);
112
+ return;
113
+ }
58
114
  p.log.step(message);
59
115
  }
60
116
  /** Logs a plain message without a status prefix. */
61
117
  export function message(message) {
118
+ if (machineOutputMode) {
119
+ writeDiagnostic('', message);
120
+ return;
121
+ }
62
122
  p.log.message(message);
63
123
  }
64
124
  /** Formats text using the success color without logging it. */
@@ -75,6 +135,21 @@ export function formatErrorText(message) {
75
135
  * @returns Spinner handle with message(), stop(), and error() methods
76
136
  */
77
137
  export function spinner(initialMessage) {
138
+ if (machineOutputMode) {
139
+ // Spinner progress is diagnostics; keep stdout clean for the payload.
140
+ return {
141
+ message: (msg) => {
142
+ writeDiagnostic('', msg);
143
+ },
144
+ stop: (msg) => {
145
+ if (msg)
146
+ writeDiagnostic('', msg);
147
+ },
148
+ error: (msg) => {
149
+ writeDiagnostic('error: ', msg ?? 'Failed');
150
+ },
151
+ };
152
+ }
78
153
  if (!supportsInteractiveSpinner()) {
79
154
  let latestMessage = initialMessage;
80
155
  return {
@@ -107,6 +182,10 @@ export function spinner(initialMessage) {
107
182
  }
108
183
  /** Emits a cancellation message. */
109
184
  export function cancel(message) {
185
+ if (machineOutputMode) {
186
+ writeDiagnostic('cancelled: ', message);
187
+ return;
188
+ }
110
189
  p.cancel(message);
111
190
  }
112
191
  /** Checks whether a prompt result represents a user cancellation. */
@@ -115,6 +194,9 @@ export function isCancel(value) {
115
194
  }
116
195
  /** Displays a titled note block for follow-up details. */
117
196
  export function note(message, title) {
197
+ if (machineOutputMode) {
198
+ writeDiagnostic('', title ? `${title}: ${message}` : message);
199
+ return;
200
+ }
118
201
  p.note(message, title);
119
202
  }
120
- //# sourceMappingURL=logger.js.map
@@ -6,3 +6,18 @@
6
6
  export declare function pickDefined<T extends Record<string, unknown>>(obj: T): {
7
7
  [K in keyof T]+?: Exclude<T[K], undefined>;
8
8
  };
9
+ /**
10
+ * Wraps an option-argument parser so its failures surface through
11
+ * commander's own invalid-argument channel.
12
+ *
13
+ * Commander only treats errors whose `code` is `'commander.invalidArgument'`
14
+ * as argument-validation failures; anything else re-throws out of
15
+ * `parseAsync`, BYPASSING `withErrorHandling` entirely and landing in the
16
+ * bin's `main().catch` as an unformatted `Fatal error: …` dump with exit 1.
17
+ * That was the observed behavior for `run --smoke-exit abc` (plain Error),
18
+ * `test --harness-retries 99` (GeneralError), and `export --order 0`
19
+ * (FireForge InvalidArgumentError, whose `code` is a numeric ExitCode).
20
+ * Every `.argParser()`/option-parser callback must be wrapped with this
21
+ * helper (or throw commander's InvalidArgumentError directly).
22
+ */
23
+ export declare function commanderArgParser<T>(parse: (raw: string) => T): (raw: string) => T;
@@ -13,4 +13,28 @@ export function pickDefined(obj) {
13
13
  }
14
14
  return result;
15
15
  }
16
- //# sourceMappingURL=options.js.map
16
+ import { InvalidArgumentError as CommanderInvalidArgumentError } from 'commander';
17
+ /**
18
+ * Wraps an option-argument parser so its failures surface through
19
+ * commander's own invalid-argument channel.
20
+ *
21
+ * Commander only treats errors whose `code` is `'commander.invalidArgument'`
22
+ * as argument-validation failures; anything else re-throws out of
23
+ * `parseAsync`, BYPASSING `withErrorHandling` entirely and landing in the
24
+ * bin's `main().catch` as an unformatted `Fatal error: …` dump with exit 1.
25
+ * That was the observed behavior for `run --smoke-exit abc` (plain Error),
26
+ * `test --harness-retries 99` (GeneralError), and `export --order 0`
27
+ * (FireForge InvalidArgumentError, whose `code` is a numeric ExitCode).
28
+ * Every `.argParser()`/option-parser callback must be wrapped with this
29
+ * helper (or throw commander's InvalidArgumentError directly).
30
+ */
31
+ export function commanderArgParser(parse) {
32
+ return (raw) => {
33
+ try {
34
+ return parse(raw);
35
+ }
36
+ catch (error) {
37
+ throw new CommanderInvalidArgumentError(error instanceof Error ? error.message : String(error));
38
+ }
39
+ };
40
+ }
@@ -34,8 +34,8 @@ export function getPackageRoot() {
34
34
  }
35
35
  let current = dirname(fileURLToPath(import.meta.url));
36
36
  for (;;) {
37
+ const packagePath = join(current, 'package.json');
37
38
  try {
38
- const packagePath = join(current, 'package.json');
39
39
  const pkg = readPackageMetadata(packagePath);
40
40
  if (isFireForgePackageMetadata(pkg)) {
41
41
  cachedPackageRoot = current;
@@ -43,8 +43,16 @@ export function getPackageRoot() {
43
43
  }
44
44
  }
45
45
  catch (error) {
46
- void error;
47
- // no package.json herekeep walking
46
+ // Absent package.json: keep walking. A package.json that EXISTS but
47
+ // fails to parse is a different animal walking past it could bind
48
+ // to a wrong ancestor package (any parent exposing bin.fireforge) or
49
+ // end in the unhelpful generic "could not locate" error, hiding the
50
+ // actual syntax problem.
51
+ const code = error.code;
52
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') {
53
+ throw new Error(`Found ${packagePath} but could not parse it: ` +
54
+ (error instanceof Error ? error.message : String(error)), { cause: error });
55
+ }
48
56
  }
49
57
  const parent = dirname(current);
50
58
  if (parent === current) {
@@ -66,4 +74,3 @@ export function getPackageVersion() {
66
74
  const packageRoot = getPackageRoot();
67
75
  return readPackageMetadata(join(packageRoot, 'package.json')).version;
68
76
  }
69
- //# sourceMappingURL=package-root.js.map
@@ -212,4 +212,3 @@ export function parseObject(data, label) {
212
212
  }
213
213
  return new ParsedRecord(data, label);
214
214
  }
215
- //# sourceMappingURL=parse.js.map
@@ -75,4 +75,3 @@ export function toRootRelativePath(root, candidate) {
75
75
  }
76
76
  return normalizePathSlashes(relative(resolvedRoot, resolvedCandidate));
77
77
  }
78
- //# sourceMappingURL=paths.js.map
@@ -53,4 +53,3 @@ export function isWindows() {
53
53
  export function getExecutableExtension() {
54
54
  return isWindows() ? '.exe' : '';
55
55
  }
56
- //# sourceMappingURL=platform.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Process-group kill and post-run sweep helpers for the exec layer
3
+ * (0.37.0 item 9a). Split out of `process.ts` to keep that file within the
4
+ * per-file line budget; deliberately spawn-based (not `exec`-based) so the
5
+ * two modules do not import each other cyclically.
6
+ */
7
+ import { type ChildProcess } from 'node:child_process';
8
+ /**
9
+ * Sends `signal` to the child's whole tree: the process GROUP on POSIX
10
+ * (negative-PID kill — reaches mach → python → firefox → content-process
11
+ * chains), or a `taskkill /T /F` tree kill plus a direct `child.kill`
12
+ * fallback on Windows. No-ops once the direct child has exited (group
13
+ * survivors after exit are the post-run sweep's job, not this function's).
14
+ */
15
+ export declare function killProcessTree(child: ChildProcess, signal: NodeJS.Signals, usesProcessGroup: boolean): void;
16
+ /** One process still alive in a swept group. */
17
+ export interface ProcessGroupSurvivor {
18
+ /** PID, or -1 when pgrep was unavailable and only a liveness probe ran. */
19
+ pid: number;
20
+ /** Command line (pgrep -lf output), best-effort. */
21
+ command: string;
22
+ }
23
+ /**
24
+ * Post-run reaper for a process group FireForge itself spawned (`pgid` is
25
+ * the PID of a child started with `detached: true`): lists survivors,
26
+ * SIGTERMs the group, waits a short grace, escalates to SIGKILL, and warns
27
+ * about anything that still refuses to die. POSIX only (no-op on win32).
28
+ * The only kill target is `-pgid` — never anything outside the group.
29
+ * A healthy run costs exactly one `pgrep`.
30
+ */
31
+ export declare function sweepProcessGroup(pgid: number, graceMs?: number): Promise<{
32
+ survivors: ProcessGroupSurvivor[];
33
+ }>;
@@ -0,0 +1,161 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ /**
3
+ * Process-group kill and post-run sweep helpers for the exec layer
4
+ * (0.37.0 item 9a). Split out of `process.ts` to keep that file within the
5
+ * per-file line budget; deliberately spawn-based (not `exec`-based) so the
6
+ * two modules do not import each other cyclically.
7
+ */
8
+ import { spawn } from 'node:child_process';
9
+ import { verbose, warn } from './logger.js';
10
+ /**
11
+ * Sends `signal` to the child's whole tree: the process GROUP on POSIX
12
+ * (negative-PID kill — reaches mach → python → firefox → content-process
13
+ * chains), or a `taskkill /T /F` tree kill plus a direct `child.kill`
14
+ * fallback on Windows. No-ops once the direct child has exited (group
15
+ * survivors after exit are the post-run sweep's job, not this function's).
16
+ */
17
+ export function killProcessTree(child, signal, usesProcessGroup) {
18
+ if (child.exitCode !== null || child.signalCode !== null)
19
+ return;
20
+ const targetPid = child.pid;
21
+ if (targetPid === undefined)
22
+ return;
23
+ try {
24
+ if (usesProcessGroup) {
25
+ // Negative PID routes to the process group, killing the Python
26
+ // wrapper, the firefox it forked, and every content process
27
+ // that inherited the group.
28
+ process.kill(-targetPid, signal);
29
+ }
30
+ else {
31
+ // No process group on Windows — taskkill /T walks the descendant
32
+ // tree instead. Always forced (/F): there is no SIGTERM analogue,
33
+ // so the grace window only exists on POSIX.
34
+ spawn('taskkill', ['/pid', String(targetPid), '/T', '/F'], {
35
+ stdio: 'ignore',
36
+ }).on('error', () => {
37
+ // taskkill unavailable — nothing more we can do beyond the
38
+ // direct-child kill below.
39
+ });
40
+ child.kill(signal);
41
+ }
42
+ }
43
+ catch {
44
+ // Already gone. Nothing to do.
45
+ }
46
+ }
47
+ const SWEEP_GRACE_MS = 2000;
48
+ const MULTIPROCESSING_WORKER_PATTERN = /multiprocessing\.(?:spawn|forkserver)|resource_tracker/;
49
+ function sweepDelay(ms) {
50
+ // Deliberately ref'd (no unref()): this promise is AWAITED between the
51
+ // group SIGTERM and the post-grace re-list/SIGKILL escalation, from a
52
+ // child 'close' handler after the signal forwarder is disposed — with an
53
+ // unref'd timer nothing kept the event loop alive, so Node could exit
54
+ // mid-grace and skip the escalation entirely. Healthy runs never reach
55
+ // this function (sweepProcessGroup early-returns on zero survivors), so
56
+ // the ref never holds a clean exit open.
57
+ return new Promise((resolve) => {
58
+ setTimeout(resolve, ms);
59
+ });
60
+ }
61
+ /**
62
+ * Minimal spawn-based pgrep runner. exitCode -1 means pgrep itself was
63
+ * unavailable or errored (distinct from exit 1 = "no matches").
64
+ */
65
+ async function runPgrep(args) {
66
+ return new Promise((resolve) => {
67
+ const child = spawn('pgrep', args, { stdio: ['ignore', 'pipe', 'ignore'] });
68
+ let stdout = '';
69
+ child.stdout.on('data', (data) => {
70
+ stdout += data.toString('utf8');
71
+ });
72
+ child.on('error', () => {
73
+ resolve({ exitCode: -1, stdout: '' });
74
+ });
75
+ child.on('close', (code) => {
76
+ resolve({ exitCode: code ?? -1, stdout });
77
+ });
78
+ });
79
+ }
80
+ /** Lists processes still in `pgid` via `pgrep -g -lf`, with a kill(0) fallback. */
81
+ async function listGroupSurvivors(pgid) {
82
+ const result = await runPgrep(['-g', String(pgid), '-lf']);
83
+ if (result.exitCode === 0) {
84
+ return result.stdout
85
+ .split('\n')
86
+ .map((line) => line.trim())
87
+ .filter((line) => line.length > 0)
88
+ .map((line) => {
89
+ const match = /^(\d+)\s+(.*)$/.exec(line);
90
+ return match
91
+ ? { pid: Number(match[1]), command: match[2] ?? '' }
92
+ : { pid: -1, command: line };
93
+ });
94
+ }
95
+ if (result.exitCode === 1)
96
+ return []; // no matches
97
+ // pgrep unavailable/broken: fall back to a group liveness probe.
98
+ try {
99
+ process.kill(-pgid, 0);
100
+ return [{ pid: -1, command: 'unknown (pgrep unavailable; group still has live members)' }];
101
+ }
102
+ catch {
103
+ return [];
104
+ }
105
+ }
106
+ function describeSurvivors(list) {
107
+ return list
108
+ .map((s) => {
109
+ const tag = MULTIPROCESSING_WORKER_PATTERN.test(s.command)
110
+ ? ' [multiprocessing worker — the known busy-spin orphan shape]'
111
+ : '';
112
+ return `PID ${String(s.pid)}: ${s.command}${tag}`;
113
+ })
114
+ .join('; ');
115
+ }
116
+ /**
117
+ * Post-run reaper for a process group FireForge itself spawned (`pgid` is
118
+ * the PID of a child started with `detached: true`): lists survivors,
119
+ * SIGTERMs the group, waits a short grace, escalates to SIGKILL, and warns
120
+ * about anything that still refuses to die. POSIX only (no-op on win32).
121
+ * The only kill target is `-pgid` — never anything outside the group.
122
+ * A healthy run costs exactly one `pgrep`.
123
+ */
124
+ export async function sweepProcessGroup(pgid, graceMs = SWEEP_GRACE_MS) {
125
+ if (process.platform === 'win32')
126
+ return { survivors: [] };
127
+ const survivors = await listGroupSurvivors(pgid);
128
+ if (survivors.length === 0)
129
+ return { survivors };
130
+ warn(`Harness process group ${String(pgid)} left ${String(survivors.length)} surviving ` +
131
+ `process(es) after exit — reaping the group. ${describeSurvivors(survivors)}`);
132
+ try {
133
+ process.kill(-pgid, 'SIGTERM');
134
+ }
135
+ catch {
136
+ return { survivors };
137
+ }
138
+ await sweepDelay(graceMs);
139
+ let remaining = await listGroupSurvivors(pgid);
140
+ if (remaining.length > 0) {
141
+ try {
142
+ process.kill(-pgid, 'SIGKILL');
143
+ }
144
+ catch {
145
+ return { survivors };
146
+ }
147
+ await sweepDelay(Math.min(200, graceMs));
148
+ remaining = await listGroupSurvivors(pgid);
149
+ if (remaining.length > 0) {
150
+ warn(`Process group ${String(pgid)} still has survivors after SIGKILL: ${describeSurvivors(remaining)}. ` +
151
+ 'Inspect manually (ps -axo pid,ppid,time,command) and kill by PID.');
152
+ }
153
+ else {
154
+ verbose(`Process group ${String(pgid)} reaped after SIGKILL escalation.`);
155
+ }
156
+ }
157
+ else {
158
+ verbose(`Process group ${String(pgid)} reaped cleanly with SIGTERM.`);
159
+ }
160
+ return { survivors };
161
+ }
@@ -8,6 +8,16 @@ export interface ExecResult {
8
8
  stderr: string;
9
9
  /** Process exit code */
10
10
  exitCode: number;
11
+ /**
12
+ * True when `stdout` was cut off at the 50 MB collector cap. Optional so
13
+ * existing mocks stay valid; absent means "not truncated". Callers that
14
+ * feed the output into safety decisions (e.g. archive-listing scans) must
15
+ * treat `true` as a hard failure — a truncated listing looks exactly like
16
+ * a complete one otherwise.
17
+ */
18
+ stdoutTruncated?: boolean;
19
+ /** True when `stderr` was cut off at the 50 MB collector cap. See {@link ExecResult.stdoutTruncated}. */
20
+ stderrTruncated?: boolean;
11
21
  }
12
22
  /**
13
23
  * Options for command execution.
@@ -19,6 +29,21 @@ export interface ExecOptions {
19
29
  env?: Record<string, string>;
20
30
  /** Timeout in milliseconds */
21
31
  timeout?: number;
32
+ /**
33
+ * POSIX: spawn the child as a process-group leader and route every kill
34
+ * (parent-signal forwarding, abort, escalation) to the whole GROUP, then
35
+ * sweep the group for survivors after close — so a harness that dies at
36
+ * startup cannot strand spinning `multiprocessing` workers (field
37
+ * incident: an orphaned Python spawn worker reparented to launchd and
38
+ * busy-spun at 100% CPU for ~26 days). Win32: tree-kill via
39
+ * `taskkill /T /F` on abort/signals only, no post-run sweep
40
+ * (best-effort). Default false — non-mach consumers are unaffected.
41
+ *
42
+ * NOTE for callers: a detached group leader does NOT receive terminal
43
+ * Ctrl+C; the exec layer installs its own group-aware signal forwarder
44
+ * whenever this option is set.
45
+ */
46
+ processGroup?: boolean;
22
47
  }
23
48
  /**
24
49
  * Executes a command and returns its output.
@@ -49,6 +74,13 @@ export interface StreamOptions extends ExecOptions {
49
74
  * @returns Exit code of the process
50
75
  */
51
76
  export declare function execStream(command: string, args: string[], options?: StreamOptions): Promise<number>;
77
+ /**
78
+ * Waits (bounded) for every tracked child process to close. Called by the
79
+ * bin signal handler after forwarding SIGINT/SIGTERM, so the parent stays
80
+ * alive long enough for the grace-then-SIGKILL escalation to actually run.
81
+ * Resolves immediately when no children are active.
82
+ */
83
+ export declare function waitForActiveChildShutdown(timeoutMs: number): Promise<void>;
52
84
  /**
53
85
  * Executes a command and inherits stdio (shows output directly).
54
86
  *
@@ -81,8 +113,16 @@ export declare function execInheritCapture(command: string, args: string[], opti
81
113
  }): Promise<ExecResult>;
82
114
  /** Per-line callback for smoke-run stream dispatch. */
83
115
  export type SmokeLineCallback = (line: string) => void;
84
- /** Options for {@link execSmokeRun}. */
85
- export interface SmokeRunOptions extends ExecOptions {
116
+ /**
117
+ * Options for {@link execSmokeRun}.
118
+ *
119
+ * Deliberately omits `ExecOptions.timeout`: the smoke run's only deadline is
120
+ * {@link SmokeRunOptions.smokeTimeoutMs}, which signals the whole process
121
+ * group. Inheriting `timeout` used to be a leaky trap — it was accepted by
122
+ * the type but silently ignored, so a caller setting it got no deadline at
123
+ * all.
124
+ */
125
+ export interface SmokeRunOptions extends Omit<ExecOptions, 'timeout'> {
86
126
  /**
87
127
  * Hard deadline in milliseconds. When it elapses the child process
88
128
  * group is sent SIGTERM and, after `killGraceMs`, SIGKILL. The returned
@@ -138,8 +178,12 @@ export interface SmokeRunResult extends ExecResult {
138
178
  * that inherited the group.
139
179
  *
140
180
  * Windows fallback: `detached: true` does not create an equivalent group
141
- * there, so we degrade to `child.kill()` and log a best-effort warning
142
- * via the `onStderrLine` callback if the caller wired one.
181
+ * there, so we kill the descendant tree with `taskkill /pid <pid> /T /F`
182
+ * instead. taskkill has no graceful-signal equivalent both the deadline
183
+ * and the grace re-invocation are forced kills, so Windows children get
184
+ * no shutdown window (best-effort; Windows is untested, see README).
185
+ * `child.kill()` is still sent as a direct-child fallback in case
186
+ * taskkill itself is unavailable.
143
187
  */
144
188
  export declare function execSmokeRun(command: string, args: string[], options: SmokeRunOptions): Promise<SmokeRunResult>;
145
189
  /**