@hominis/fireforge 0.35.0 → 0.36.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 (355) hide show
  1. package/CHANGELOG.md +29 -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 +4 -3
  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-working-tree.js +0 -1
  19. package/dist/src/commands/doctor.js +4 -1
  20. package/dist/src/commands/download.js +28 -13
  21. package/dist/src/commands/export-all.js +5 -3
  22. package/dist/src/commands/export-flow.js +12 -7
  23. package/dist/src/commands/export-placement-gate.js +0 -1
  24. package/dist/src/commands/export-placement-policy.js +0 -1
  25. package/dist/src/commands/export-shared.d.ts +5 -1
  26. package/dist/src/commands/export-shared.js +18 -6
  27. package/dist/src/commands/export.js +15 -10
  28. package/dist/src/commands/furnace/apply.js +22 -6
  29. package/dist/src/commands/furnace/chrome-doc-remove.js +0 -1
  30. package/dist/src/commands/furnace/chrome-doc-templates.js +0 -1
  31. package/dist/src/commands/furnace/chrome-doc-tests.js +0 -1
  32. package/dist/src/commands/furnace/chrome-doc.js +0 -1
  33. package/dist/src/commands/furnace/create-browser-test.js +0 -1
  34. package/dist/src/commands/furnace/create-dry-run.js +0 -1
  35. package/dist/src/commands/furnace/create-features.js +0 -1
  36. package/dist/src/commands/furnace/create-mochikit.js +0 -1
  37. package/dist/src/commands/furnace/create-readback.js +0 -1
  38. package/dist/src/commands/furnace/create-templates.js +0 -1
  39. package/dist/src/commands/furnace/create-validation.js +0 -1
  40. package/dist/src/commands/furnace/create-xpcshell.js +0 -1
  41. package/dist/src/commands/furnace/create.js +0 -1
  42. package/dist/src/commands/furnace/deploy.js +7 -83
  43. package/dist/src/commands/furnace/diff.js +0 -1
  44. package/dist/src/commands/furnace/index.js +59 -25
  45. package/dist/src/commands/furnace/init.js +0 -1
  46. package/dist/src/commands/furnace/list.js +0 -1
  47. package/dist/src/commands/furnace/override.js +0 -1
  48. package/dist/src/commands/furnace/preview.js +3 -3
  49. package/dist/src/commands/furnace/refresh.js +0 -1
  50. package/dist/src/commands/furnace/remove-state.d.ts +15 -0
  51. package/dist/src/commands/furnace/remove-state.js +47 -1
  52. package/dist/src/commands/furnace/remove.js +3 -9
  53. package/dist/src/commands/furnace/rename-browser-test.js +1 -5
  54. package/dist/src/commands/furnace/rename-helpers.d.ts +6 -0
  55. package/dist/src/commands/furnace/rename-helpers.js +30 -1
  56. package/dist/src/commands/furnace/rename-xpcshell.js +7 -6
  57. package/dist/src/commands/furnace/rename.js +15 -36
  58. package/dist/src/commands/furnace/scan.js +0 -1
  59. package/dist/src/commands/furnace/status.js +0 -1
  60. package/dist/src/commands/furnace/sync.js +76 -4
  61. package/dist/src/commands/furnace/validate.js +0 -1
  62. package/dist/src/commands/furnace/validation-output.js +0 -1
  63. package/dist/src/commands/import.js +35 -9
  64. package/dist/src/commands/lint-per-patch.js +19 -7
  65. package/dist/src/commands/lint.js +13 -3
  66. package/dist/src/commands/manifest.js +1 -2
  67. package/dist/src/commands/package.js +2 -1
  68. package/dist/src/commands/patch/compact.js +1 -1
  69. package/dist/src/commands/patch/delete.js +1 -1
  70. package/dist/src/commands/patch/index.js +1 -1
  71. package/dist/src/commands/patch/lint-ignore.js +21 -6
  72. package/dist/src/commands/patch/move-files.js +1 -1
  73. package/dist/src/commands/patch/patch-context.js +0 -1
  74. package/dist/src/commands/patch/rename.js +4 -4
  75. package/dist/src/commands/patch/reorder.js +3 -4
  76. package/dist/src/commands/patch/split-plan.js +2 -1
  77. package/dist/src/commands/patch/split.js +4 -4
  78. package/dist/src/commands/patch/staged-dependency.js +1 -1
  79. package/dist/src/commands/patch/tier.js +18 -5
  80. package/dist/src/commands/re-export-bulk-scan.js +0 -1
  81. package/dist/src/commands/re-export-files.js +1 -1
  82. package/dist/src/commands/re-export-options.js +0 -1
  83. package/dist/src/commands/re-export-scan.js +0 -1
  84. package/dist/src/commands/re-export.js +6 -4
  85. package/dist/src/commands/rebase/abort.js +2 -9
  86. package/dist/src/commands/rebase/confirm.js +0 -1
  87. package/dist/src/commands/rebase/conflict-summary.js +0 -1
  88. package/dist/src/commands/rebase/continue.js +0 -1
  89. package/dist/src/commands/rebase/index.d.ts +1 -1
  90. package/dist/src/commands/rebase/index.js +10 -15
  91. package/dist/src/commands/rebase/patch-loop.js +38 -7
  92. package/dist/src/commands/rebase/summary.js +2 -3
  93. package/dist/src/commands/rebase.js +0 -1
  94. package/dist/src/commands/register.js +1 -1
  95. package/dist/src/commands/reset.js +16 -9
  96. package/dist/src/commands/resolve.js +2 -2
  97. package/dist/src/commands/run.js +4 -4
  98. package/dist/src/commands/setup-support.js +0 -1
  99. package/dist/src/commands/setup.js +1 -2
  100. package/dist/src/commands/source.js +1 -2
  101. package/dist/src/commands/status-output.js +1 -12
  102. package/dist/src/commands/status.js +147 -133
  103. package/dist/src/commands/test-appdir.js +0 -1
  104. package/dist/src/commands/test-diagnose.js +18 -2
  105. package/dist/src/commands/test-modes.d.ts +15 -0
  106. package/dist/src/commands/test-modes.js +61 -0
  107. package/dist/src/commands/test-register.d.ts +4 -0
  108. package/dist/src/commands/test-register.js +64 -0
  109. package/dist/src/commands/test-run.d.ts +23 -7
  110. package/dist/src/commands/test-run.js +15 -14
  111. package/dist/src/commands/test.d.ts +0 -4
  112. package/dist/src/commands/test.js +85 -86
  113. package/dist/src/commands/token-coverage.js +0 -1
  114. package/dist/src/commands/token.js +1 -2
  115. package/dist/src/commands/typecheck.js +1 -1
  116. package/dist/src/commands/verify.js +3 -9
  117. package/dist/src/commands/watch.js +5 -1
  118. package/dist/src/commands/wire.js +1 -1
  119. package/dist/src/core/ast-utils.js +0 -1
  120. package/dist/src/core/brand-validation.js +0 -1
  121. package/dist/src/core/branding.js +0 -1
  122. package/dist/src/core/browser-wire.js +0 -1
  123. package/dist/src/core/build-audit-platform.js +0 -1
  124. package/dist/src/core/build-audit-registration.js +0 -1
  125. package/dist/src/core/build-audit-resolve.js +0 -1
  126. package/dist/src/core/build-audit-transforms.js +0 -1
  127. package/dist/src/core/build-audit.js +0 -1
  128. package/dist/src/core/build-baseline-types.js +0 -1
  129. package/dist/src/core/build-baseline.js +0 -1
  130. package/dist/src/core/build-prepare.js +2 -2
  131. package/dist/src/core/config-mutate.js +0 -1
  132. package/dist/src/core/config-paths.d.ts +2 -2
  133. package/dist/src/core/config-paths.js +6 -1
  134. package/dist/src/core/config-state.js +0 -1
  135. package/dist/src/core/config-validate-patch-policy.js +0 -1
  136. package/dist/src/core/config-validate-test-toolchains.d.ts +8 -0
  137. package/dist/src/core/config-validate-test-toolchains.js +105 -0
  138. package/dist/src/core/config-validate.js +3 -1
  139. package/dist/src/core/config.js +0 -1
  140. package/dist/src/core/destructive.js +0 -1
  141. package/dist/src/core/diff-hunks.js +13 -7
  142. package/dist/src/core/engine-changes.js +0 -1
  143. package/dist/src/core/engine-session-lock.d.ts +12 -0
  144. package/dist/src/core/engine-session-lock.js +45 -0
  145. package/dist/src/core/file-lock.d.ts +8 -0
  146. package/dist/src/core/file-lock.js +157 -48
  147. package/dist/src/core/firefox-archive.d.ts +4 -0
  148. package/dist/src/core/firefox-archive.js +6 -1
  149. package/dist/src/core/firefox-cache.js +60 -4
  150. package/dist/src/core/firefox-download.js +0 -1
  151. package/dist/src/core/firefox-extract.js +75 -16
  152. package/dist/src/core/firefox-ignorefile.js +0 -1
  153. package/dist/src/core/firefox.d.ts +12 -0
  154. package/dist/src/core/firefox.js +64 -2
  155. package/dist/src/core/furnace-apply-dry-run.js +0 -1
  156. package/dist/src/core/furnace-apply-ftl.d.ts +7 -3
  157. package/dist/src/core/furnace-apply-ftl.js +10 -5
  158. package/dist/src/core/furnace-apply-helpers.js +0 -1
  159. package/dist/src/core/furnace-apply-output.js +2 -2
  160. package/dist/src/core/furnace-apply.d.ts +7 -1
  161. package/dist/src/core/furnace-apply.js +47 -13
  162. package/dist/src/core/furnace-checksum-utils.js +0 -1
  163. package/dist/src/core/furnace-config-array-utils.js +0 -1
  164. package/dist/src/core/furnace-config-custom.js +0 -1
  165. package/dist/src/core/furnace-config-order.js +0 -1
  166. package/dist/src/core/furnace-config-tokens.js +0 -1
  167. package/dist/src/core/furnace-config-validate.js +0 -1
  168. package/dist/src/core/furnace-config.d.ts +11 -0
  169. package/dist/src/core/furnace-config.js +37 -1
  170. package/dist/src/core/furnace-constants.js +0 -1
  171. package/dist/src/core/furnace-css-fragments.js +0 -1
  172. package/dist/src/core/furnace-graph-utils.js +0 -1
  173. package/dist/src/core/furnace-jsconfig.d.ts +2 -3
  174. package/dist/src/core/furnace-jsconfig.js +25 -18
  175. package/dist/src/core/furnace-marker.js +0 -1
  176. package/dist/src/core/furnace-operation.js +0 -1
  177. package/dist/src/core/furnace-refresh.js +0 -1
  178. package/dist/src/core/furnace-registration-ast.d.ts +14 -0
  179. package/dist/src/core/furnace-registration-ast.js +1 -2
  180. package/dist/src/core/furnace-registration-remove.js +0 -1
  181. package/dist/src/core/furnace-registration-validate.js +1 -5
  182. package/dist/src/core/furnace-registration.js +0 -1
  183. package/dist/src/core/furnace-rollback.js +0 -1
  184. package/dist/src/core/furnace-scanner.js +0 -1
  185. package/dist/src/core/furnace-staleness.js +0 -1
  186. package/dist/src/core/furnace-state-persist.d.ts +72 -0
  187. package/dist/src/core/furnace-state-persist.js +118 -0
  188. package/dist/src/core/furnace-step-errors.d.ts +23 -0
  189. package/dist/src/core/furnace-step-errors.js +25 -0
  190. package/dist/src/core/furnace-stories.js +0 -1
  191. package/dist/src/core/furnace-validate-accessibility.js +0 -1
  192. package/dist/src/core/furnace-validate-checks.js +0 -1
  193. package/dist/src/core/furnace-validate-compatibility.js +0 -1
  194. package/dist/src/core/furnace-validate-helpers.js +0 -1
  195. package/dist/src/core/furnace-validate-registration.js +7 -7
  196. package/dist/src/core/furnace-validate-structure.d.ts +8 -0
  197. package/dist/src/core/furnace-validate-structure.js +11 -2
  198. package/dist/src/core/furnace-validate.js +0 -1
  199. package/dist/src/core/furnace-version-drift.d.ts +6 -3
  200. package/dist/src/core/furnace-version-drift.js +6 -4
  201. package/dist/src/core/git-base.js +0 -1
  202. package/dist/src/core/git-diff.js +0 -1
  203. package/dist/src/core/git-file-ops.js +0 -1
  204. package/dist/src/core/git-status.d.ts +13 -0
  205. package/dist/src/core/git-status.js +32 -2
  206. package/dist/src/core/git.d.ts +10 -1
  207. package/dist/src/core/git.js +63 -6
  208. package/dist/src/core/license-headers.js +0 -1
  209. package/dist/src/core/lint-cache.js +0 -1
  210. package/dist/src/core/lint-projection.js +0 -1
  211. package/dist/src/core/mach-build-artifacts.js +0 -1
  212. package/dist/src/core/mach-error-hints.js +0 -1
  213. package/dist/src/core/mach-mozconfig.js +0 -1
  214. package/dist/src/core/mach-python.js +0 -1
  215. package/dist/src/core/mach-resource-shim.js +0 -1
  216. package/dist/src/core/mach.js +0 -1
  217. package/dist/src/core/manifest-helpers.js +0 -1
  218. package/dist/src/core/manifest-register.js +0 -1
  219. package/dist/src/core/manifest-rules.js +0 -1
  220. package/dist/src/core/manifest-tokenizers.js +0 -1
  221. package/dist/src/core/marionette-port.d.ts +8 -0
  222. package/dist/src/core/marionette-port.js +34 -3
  223. package/dist/src/core/marionette-preflight.js +0 -1
  224. package/dist/src/core/markdown-table.js +0 -1
  225. package/dist/src/core/ownership-table.js +2 -9
  226. package/dist/src/core/parser-fallback.js +0 -1
  227. package/dist/src/core/patch-apply-fuzz.d.ts +27 -9
  228. package/dist/src/core/patch-apply-fuzz.js +71 -34
  229. package/dist/src/core/patch-apply.d.ts +30 -0
  230. package/dist/src/core/patch-apply.js +101 -18
  231. package/dist/src/core/patch-artifact-normalize.d.ts +18 -5
  232. package/dist/src/core/patch-artifact-normalize.js +19 -15
  233. package/dist/src/core/patch-export-coverage.js +0 -1
  234. package/dist/src/core/patch-export-metadata.js +0 -1
  235. package/dist/src/core/patch-export-update.js +2 -3
  236. package/dist/src/core/patch-export.d.ts +2 -6
  237. package/dist/src/core/patch-export.js +24 -48
  238. package/dist/src/core/patch-files.js +0 -1
  239. package/dist/src/core/patch-identifier-suggest.js +0 -1
  240. package/dist/src/core/patch-lint-checkjs.js +0 -1
  241. package/dist/src/core/patch-lint-chrome-jsdoc.js +0 -1
  242. package/dist/src/core/patch-lint-cross.js +0 -1
  243. package/dist/src/core/patch-lint-css.js +0 -1
  244. package/dist/src/core/patch-lint-diff-tag.js +0 -1
  245. package/dist/src/core/patch-lint-diff.d.ts +2 -1
  246. package/dist/src/core/patch-lint-diff.js +17 -32
  247. package/dist/src/core/patch-lint-imports.js +0 -1
  248. package/dist/src/core/patch-lint-jsdoc.js +0 -1
  249. package/dist/src/core/patch-lint-observer.js +0 -1
  250. package/dist/src/core/patch-lint-ownership.js +0 -1
  251. package/dist/src/core/patch-lint-reexports.js +0 -1
  252. package/dist/src/core/patch-lint.d.ts +37 -2
  253. package/dist/src/core/patch-lint.js +47 -34
  254. package/dist/src/core/patch-lock.js +0 -1
  255. package/dist/src/core/patch-manifest-consistency.js +9 -1
  256. package/dist/src/core/patch-manifest-io.d.ts +16 -0
  257. package/dist/src/core/patch-manifest-io.js +30 -5
  258. package/dist/src/core/patch-manifest-query.js +18 -10
  259. package/dist/src/core/patch-manifest-resolve.js +0 -1
  260. package/dist/src/core/patch-manifest-validate.js +0 -1
  261. package/dist/src/core/patch-manifest.d.ts +1 -1
  262. package/dist/src/core/patch-manifest.js +1 -2
  263. package/dist/src/core/patch-parse.d.ts +59 -0
  264. package/dist/src/core/patch-parse.js +209 -92
  265. package/dist/src/core/patch-policy.js +0 -1
  266. package/dist/src/core/patch-registration-refs.js +16 -26
  267. package/dist/src/core/patch-source-metadata.js +0 -1
  268. package/dist/src/core/patch-transform.d.ts +4 -0
  269. package/dist/src/core/patch-transform.js +30 -38
  270. package/dist/src/core/rebase-session.js +0 -1
  271. package/dist/src/core/register-browser-content.js +0 -1
  272. package/dist/src/core/register-module.js +0 -1
  273. package/dist/src/core/register-result.js +0 -1
  274. package/dist/src/core/register-scaffold.js +0 -1
  275. package/dist/src/core/register-shared-css.js +0 -1
  276. package/dist/src/core/register-test-manifest.js +0 -1
  277. package/dist/src/core/register-xpcshell-test.js +0 -1
  278. package/dist/src/core/shared-ftl.js +0 -1
  279. package/dist/src/core/signal-critical.js +0 -1
  280. package/dist/src/core/smoke-patterns.js +0 -1
  281. package/dist/src/core/state-file.js +0 -1
  282. package/dist/src/core/status-classify.d.ts +16 -0
  283. package/dist/src/core/status-classify.js +24 -16
  284. package/dist/src/core/test-harness-crash.d.ts +64 -7
  285. package/dist/src/core/test-harness-crash.js +196 -15
  286. package/dist/src/core/test-harness-output.js +0 -1
  287. package/dist/src/core/test-path-scope.d.ts +21 -13
  288. package/dist/src/core/test-path-scope.js +43 -28
  289. package/dist/src/core/test-stale-check.js +0 -1
  290. package/dist/src/core/test-stale-symlink.js +0 -1
  291. package/dist/src/core/test-xpcshell-retry.js +0 -1
  292. package/dist/src/core/token-coverage.js +0 -1
  293. package/dist/src/core/token-dark-mode.js +0 -1
  294. package/dist/src/core/token-docs.js +0 -1
  295. package/dist/src/core/token-manager.js +0 -1
  296. package/dist/src/core/token-scaffold.js +0 -1
  297. package/dist/src/core/token-variant.js +0 -1
  298. package/dist/src/core/toolchain-preflight.d.ts +31 -10
  299. package/dist/src/core/toolchain-preflight.js +97 -36
  300. package/dist/src/core/typecheck-shim.js +13 -1
  301. package/dist/src/core/typecheck.js +0 -1
  302. package/dist/src/core/wire-destroy.js +0 -1
  303. package/dist/src/core/wire-dom-fragment.js +0 -1
  304. package/dist/src/core/wire-init.js +0 -1
  305. package/dist/src/core/wire-subscript.js +0 -1
  306. package/dist/src/core/wire-targets.js +0 -1
  307. package/dist/src/core/wire-utils.js +0 -1
  308. package/dist/src/core/xpcshell-appdir.js +1 -4
  309. package/dist/src/errors/base.d.ts +29 -0
  310. package/dist/src/errors/base.js +35 -1
  311. package/dist/src/errors/build.js +0 -1
  312. package/dist/src/errors/codes.d.ts +6 -0
  313. package/dist/src/errors/codes.js +6 -1
  314. package/dist/src/errors/config.js +0 -1
  315. package/dist/src/errors/download.js +0 -1
  316. package/dist/src/errors/furnace.d.ts +11 -1
  317. package/dist/src/errors/furnace.js +6 -5
  318. package/dist/src/errors/git.js +0 -1
  319. package/dist/src/errors/patch.d.ts +17 -0
  320. package/dist/src/errors/patch.js +29 -1
  321. package/dist/src/errors/rebase.js +0 -1
  322. package/dist/src/errors/run.js +1 -1
  323. package/dist/src/index.js +0 -1
  324. package/dist/src/types/cli.js +0 -1
  325. package/dist/src/types/commands/index.js +0 -1
  326. package/dist/src/types/commands/options.d.ts +13 -5
  327. package/dist/src/types/commands/options.js +0 -1
  328. package/dist/src/types/commands/patches.d.ts +8 -0
  329. package/dist/src/types/commands/patches.js +0 -1
  330. package/dist/src/types/commands/project.d.ts +11 -2
  331. package/dist/src/types/commands/project.js +0 -1
  332. package/dist/src/types/config.d.ts +32 -0
  333. package/dist/src/types/config.js +0 -1
  334. package/dist/src/types/furnace.d.ts +10 -0
  335. package/dist/src/types/furnace.js +0 -1
  336. package/dist/src/types/index.js +0 -1
  337. package/dist/src/types/typecheck.js +0 -1
  338. package/dist/src/utils/elapsed.js +0 -1
  339. package/dist/src/utils/errors.js +0 -1
  340. package/dist/src/utils/fs.js +37 -5
  341. package/dist/src/utils/logger.d.ts +7 -0
  342. package/dist/src/utils/logger.js +85 -3
  343. package/dist/src/utils/options.d.ts +15 -0
  344. package/dist/src/utils/options.js +25 -1
  345. package/dist/src/utils/package-root.js +11 -4
  346. package/dist/src/utils/parse.js +0 -1
  347. package/dist/src/utils/paths.js +0 -1
  348. package/dist/src/utils/platform.js +0 -1
  349. package/dist/src/utils/process.d.ts +33 -4
  350. package/dist/src/utils/process.js +183 -28
  351. package/dist/src/utils/regex.d.ts +13 -3
  352. package/dist/src/utils/regex.js +53 -4
  353. package/dist/src/utils/validation.d.ts +0 -12
  354. package/dist/src/utils/validation.js +13 -26
  355. package/package.json +8 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.36.0
4
+
5
+ - **Review-findings remediation for the 0.36.0 release gate**: `fireforge test` no longer has a pathless dead-end — no-path runs now require `--auto`, `--doctor`, or `--canary`, `--auto` forwards mach's own selection mode, stale packaged-engine drift fails fast unless `--build` or `--allow-stale-build` is used, `--kill-stale-marionette` can terminate a recognized stale browser listener, and `--canary [path]` / `test.canaryPath` provides a short green/hang/crash harness probe. Harness verdicts now let real failure lines (`FAIL`, `TEST-UNEXPECTED-*`, nonzero unexpected summaries, assertions) beat resource-monitor traceback classification, while still noting the traceback as secondary noise. Patch naming is normalized consistently for `export --name` and `patch rename --to` (bare, category-prefixed, and full stems converge), and `lint --per-patch --patches` accepts repeated flags, comma lists, filenames, stems, manifest names, category-prefixed slugs, and bare slugs. Furnace-managed jsconfig edits are JSONC-preserving via `jsonc-parser`, the Firefox globals shim now includes JSWindowActor bases and `ChromeUtils.registerWindowActor`, `doctor` can validate opt-in `externalToolchains`, and CLI engine-mutating commands take a `.fireforge/engine-session.lock` plus test-run tree-generation checks to catch concurrent engine mutation.
6
+ - **Fixed a SIGSEGV-truncated test run being reported as PASSED** (corrects the green-summary override shipped in 0.34.0 and still present in 0.35.0): an 8-file browser-chrome run whose parent process crashed at the second file's `TEST_START` (`Main app process: killed by SIGSEGV`) left an embedded summary that read `Passed: 2 / Failed: 0 / Unexpected results: 0` — green only because the crash prevented the other six files from producing any results — and FireForge overrode the mach exit 1 as "non-fatal harness noise", masking a real regression through an entire source-refresh drill while the operator recorded "full matrix green". The override now runs two vetoes first: a crash-marker scan over the RAW output (`Main app process: killed by SIG*` for any signal, `PROCESS-CRASH`, and the mozcrash minidump-processing headers — these also disqualify the summary from vetoing crash-signature classification), and a completeness cross-check that every requested test file produced a `TEST_START`/`TEST_END` (or `TEST-START`/`TEST-OK`) pair, paired positionally because the xpcshell dialect's `TEST_END` lines do not name the file. Either signal fails the run with a dedicated message naming the crash evidence line and the never-started / started-but-never-finished files, in both single and sharded runs. The lenient path survives only when neither signal fires — the resource-monitor/telemetry noise it was built for (green summary, all files paired, monitor traceback owning the exit code) still passes with the override note, and mach exit 0 classification is untouched. Fixture tests pin all four shapes, with the crash lines verbatim from the field log.
7
+ - **Fixed the build toolchain preflight probing a different cbindgen than mach resolves** (corrects 0.35.0): the preflight compared the tree's declared minimum against env-override-or-PATH only, but mach's configure tries the mozbuild state directory FIRST (`~/.mozbuild/cbindgen/cbindgen`, where `fireforge bootstrap` installs; its configure log shows that candidate probed ahead of PATH) — so a host with a current bootstrap-installed 0.29.4 behind a stale `~/.cargo/bin` 0.29.1 was blocked from a build that succeeds (field workaround was `CBINDGEN=… fireforge build`). The probe now enumerates configure's own candidate order — env override (which, when set, is the ONLY candidate: the override wins even over a newer state-dir copy, matching configure), then `$MOZBUILD_STATE_PATH`/`~/.mozbuild` locations, then PATH — and fails only when at least one candidate resolved and NONE meets the minimum, listing every probed candidate in the error; no resolvable candidate still skips silently (fail-soft unchanged). Tests pin all four resolution shapes.
8
+ - **Fixed exact-directory test selection being cosmetic — and its exclusion echo being wrong** (corrects 0.35.0): the trailing-`/` normalization does not defeat mochitest's prefix matching on Firefox 153's mach — in the verifying run all 33 `hominis-tiles` sibling files executed anyway, while the new echo gave the operator positive reason to believe they were excluded. A directory argument now dispatches as its enumerated explicit test-file list (a file list cannot prefix-match a sibling), still in ONE mach invocation so the one-browser-instance semantics of a directory run are preserved (cross-file state carries within the directory; sharding of multi-argument runs is per argument, keeping a directory's files together). The echo now describes the real mechanism, and the rare zero-enumerable-files fallback (raw directory form passed through) explicitly warns that prefix matching may still widen the scope instead of claiming exclusion. `test --help` and the README describe the new mechanism; tests pin the explicit-file-list dispatch, the single-invocation directory run, and the per-argument sharding of a directory + file mix.
9
+ - **Extended the verbatim upstream MPL-2.0 block header acceptance to non-JS block-comment files** (0.35.0 residual — its fix covered new JS files only, per its own "non-JS styles are unchanged"): a derived CSS file carrying the exact same three-line `/* … */` MPL block header still errored `missing-license-header` on an EUPL project without a patch-level lintIgnore, while the derived JS on the same patch passed natively. The carve-out now keys on the comment syntax, accepting the upstream block form (optionally behind a leading editor-directive comment) on JS and CSS; hash-style files (FTL) stay excluded because `/* … */` is not a comment there, and the `// `-style FireForge-generated MPL header still requires the project's own header on non-MPL projects. The acceptance predicate is now a single shared function (`isAcceptableNewFileHeader`) used by both the lint rule and export's interactive header auto-fix, which previously would have offered to stack a second header onto a file the lint already accepts. Lint tests cover accept/reject for CSS and the FTL exclusion.
10
+ - **Fixed the rebase drift-tolerance feature being dead code**: `git apply` has no `--fuzz` flag (that is GNU `patch(1)` syntax), so every escalation level failed with a swallowed usage error and any patch needing context drift was reported as a hard conflict with `.rej` residue — `--max-fuzz` had no effect at all. Reimplemented with git's real knob, `-C<n>` reduced-context matching (exact, then `-C2`/`-C1`/`-C0`, capped at git's default context of 3); `.rej` extraction now parses git's actual `Applying patch <file> with N rejects...` phrasing (the old regex targeted GNU patch output and never matched, so the conflict summary's ".rej files created" hint never fired). The fuzz unit tests were exec-mocked and validated behavior real git cannot produce — they now run against a **real git binary**, and a rebase integration test pins end-to-end drift absorption. User-facing wording now says "context reduction (fuzz-like)"; the `--max-fuzz` flag name is unchanged.
11
+ - Fixed `rebase --max-fuzz` **accepting invalid numbers**: the flag was wired to raw `parseInt`, so `--max-fuzz foo` became `NaN`, `--max-fuzz -1` stayed negative, and `--max-fuzz 1.5` silently truncated to 1. With `NaN` or a negative value the escalation loop in `applyPatchWithFuzz` (`for (fuzz = 0; fuzz <= maxFuzz; …)`) never ran a single iteration — the patch skipped even the exact-match attempt and fell straight through to `git apply --reject`, spraying `.rej` files for patches that would have applied cleanly. The flag now uses the same strict `parsePositiveIntegerFlag` parser as `export --order` and `patch reorder --to`, failing fast at parse time, and `applyPatchWithFuzz` itself rejects a non-integer or negative `maxFuzz` as defense in depth (0 = "exact only" stays legal at the core layer). CLI-parse tests pin `foo`/`-1`/`0`/`1.5`/`01` rejection plus a valid value flowing through; core tests pin the guard.
12
+ - **Fixed a corrupt `patches.json` being silently destroyed on the next export**: `loadPatchesManifest` returned null for "missing" AND "unparseable", so `addPatchToManifest` rebuilt an empty manifest over a hand-edit typo (losing every patch's tiers/descriptions/lintIgnore/staged dependencies) and a failing export's rollback then deleted `patches.json` because the "before" state looked absent. Every mutating path now loads through `loadPatchesManifestForWrite`, which throws a typed `PatchManifestCorruptError` naming the parse error and the repair steps.
13
+ - **Fixed `furnace apply <name>` wiping every other component's checksum state**: named apply ran the batch persist path, whose wholesale `appliedChecksums` replace kept only the named component (deploy documented and avoided this exact trap). Orphan detection and deleted-file undeploy key on that state, so the wiped components' stale engine files became invisible to apply and `furnace validate`. Named apply now mirrors deploy: `persistState: false` plus a per-component merge, both hoisted into `core/furnace-state-persist.ts` as the single owner of state-persistence policy.
14
+ - **Fixed ordinary `config <key> <value>` deleting `--force`-written keys**: the non-force branch round-tripped through `validateConfig`, whose typed clone contains only known schema fields. Both branches now mutate the raw document (validation strength unchanged — the result is still strictly validated for known keys), and the catch only labels genuine validation failures as "Invalid value" — lock timeouts and I/O errors keep their own types instead of misdirecting diagnosis.
15
+ - **Downloads verify against Mozilla's published SHA256SUMS by default**, failing closed on mismatch (artifact deleted) and warning loudly when the checksum file is unavailable; an explicit `firefox.sha256` still takes precedence and always fails closed. TLS-only trust was thin for the artifact that becomes the git baseline every patch builds on.
16
+ - **Closed the silent-degradation class**: the archive-safety preflight streams tar listings line-by-line instead of scanning a silently-truncated 50 MB capture (a padded archive could hide traversal entries past the cap); `export`/`export-all --dry-run` are genuinely read-only (the license-header auto-fix no longer prompts-and-writes into `engine/` mid-"preview"); `furnace sync` refuses to apply while workspace files carry unresolved merge conflict markers or overrides still have baseVersion drift (it used to copy conflict markers into the engine right after warning about them); `bootstrap` propagates error-severity post-check results instead of exiting 0; furnace post-apply consistency failures now trigger rollback (the gate read a pre-check snapshot of step errors); and `.ftl` degradation is a real advisory tier (`StepError.advisory`) instead of a documented-but-false promise — a fork without a locale jar.mn can ship a working component again.
17
+ - **Hardened `applyPatchIdempotent` recovery**: git exit codes checked (a failed restore no longer surfaces as a confusing "already exists" later), pathspecs chunked against `E2BIG`, patch-created leftovers deleted explicitly, and a `protectedFiles` set threaded from both apply loops so an `--allow-overlap` queue's later patch no longer resets shared files a previous patch just modified. Relatedly, rebase re-export skips patches whose files another patch also claims (the cumulative per-file diff used to duplicate shared hunks into every owner, corrupting the queue for the next import) and excludes them from version stamping with a manual-re-export warning.
18
+ - **File locks are race-hardened**: ownership is PID + per-acquisition token, stale reaping is rename-aside (exactly one reaper can win; the live path is never `rm`'d, and a lock re-acquired mid-probe is restored), the stale probe re-runs every ~5 s while waiting (a build-lock holder dying mid-wait no longer stranded waiters for up to 24 h), and release verifies the lock still belongs to this acquisition. The download lock waits up to 30 min with honest messaging instead of failing after 30 s with "remove the lock" advice while a legitimate download was mid-extraction.
19
+ - **Signals**: the bin handler now waits (bounded, 12 s) for spawned children after forwarding SIGINT/SIGTERM, making `execInherit`'s SIGTERM → grace → SIGKILL escalation actually reachable — a hung Firefox is no longer orphaned by Ctrl+C, and a healthy one keeps its AsyncShutdown flush window. `execSmokeRun` forwards parent signals to its detached process group (second signal = group SIGKILL).
20
+ - Fixed Windows smoke-run cleanup **killing only the direct child**: `execSmokeRun` signals the whole POSIX process group on timeout, but the Windows fallback was a bare `child.kill()` — for a `mach → python → firefox → content processes` chain that leaves the Firefox tree running after the smoke deadline. The fallback now runs `taskkill /pid <pid> /T /F` to kill the descendant tree (always forced — Windows has no graceful-signal analogue, so the POSIX SIGTERM→SIGKILL grace window does not exist there), keeping `child.kill()` as a last resort if taskkill is unavailable. The docstring's claim that a best-effort warning was logged on Windows (it never was) is gone, and the README's Windows caveat now names the taskkill strategy as best-effort/untested. A regression test pins that the win32 timeout path spawns `taskkill /T /F` instead of signalling `-pid`.
21
+ - **Windows atomic-write and test-suite correctness** (from the first Windows-runner failure of the cross-platform CI subset): `writeFileAtomic` now retries its final rename on transient `EPERM`/`EACCES`/`EBUSY` — Windows fails a rename onto a target another writer is concurrently replacing (or that an antivirus/indexer briefly holds open), so concurrent atomic writes to the same path spuriously errored where POSIX succeeded; bounded retries with backoff make them reliable there too. Four test-suite fixes make the Windows subset honest: the POSIX process-group kill test pins a non-Windows `process.platform` (the win32 `taskkill` branch has its own test), the git-diff mocks key their `stat`/`hashObjectBatch` lookups on `path.join`-built paths instead of hard-coded forward-slash strings (they missed the backslash paths the source computes on Windows, silently exercising the wrong branch), and the file-mode preservation test is skipped on win32, where no execute bit exists for chmod to preserve.
22
+ - **Machine output**: `status --json`/`--raw` own stdout exclusively — every diagnostic (clack warnings, styled errors, spinner steps, banners) routes to stderr as plain text, so `status --json | jq .` parses even when warnings fire. Truncated untracked directories are reported once, not twice.
23
+ - **Encoding and platform correctness**: all captured/streamed child output decodes through `StringDecoder` (multibyte characters split across pipe chunks no longer corrupt smoke-run allowlist matching or mach error hints); patch parsing is CRLF-tolerant via the new single shared diff walker (`parseDiffSections` — quoted/octal-escaped `diff --git` paths, ` b/`-in-path headers, binary-section detection, one implementation instead of six drifting walkers); `exec` timeouts reject with a typed `ExecTimeoutError` naming the command and budget instead of a bare `AbortError`.
24
+ - **CLI polish**: `--version`/`-V` works alongside other root flags (handled pre-parse; a commander root `.version()` would hijack `source set --version`); option argParsers surface through commander's invalid-argument channel via the `commanderArgParser` wrapper (`run --smoke-exit abc` prints one clean line instead of a fatal dump); `import --until <ordinal>` resolves ordinals with the same matcher as the apply loop (in-range integrity issues can no longer be silently dropped while the loop applies anyway); prompt cancellation exits 130 (`ExitCode.USER_CANCELLED`); a bare `lint cache` explains the cache-vs-path ambiguity.
25
+ - **Furnace authoring safety**: `furnace remove` deletes only the files the component deployed (never `removeDir` of a possibly-shared targetPath; empty dirs pruned; furnace.json warns when two components share a targetPath), and `furnace rename` journals the new-name side (workspace dir + files, test scaffolds, FTL destination) plus furnace-state.json before re-keying, so failed or interrupted renames roll back completely.
26
+ - **Patch-core precision**: `normalizePatchArtifact` is the identity chokepoint (whitespace-only payload lines are preserved byte-for-byte — the old trimming made freshly exported patches fail `git apply --check` and silently changed `+` line content; the worktree whitespace check already excludes `patches/*.patch`); zero-old-count insertion hunks splice after line N per unified-diff convention; the LCS-fallback coalesced hunk emits correct `@@` lengths; auto-resolved new-file originals survive the whole run so a later failure's rollback can restore them; empty new files extract as zero bytes; binary new-file sections refuse auto-resolve instead of writing base85 as text.
27
+ - **Performance**: `validatePatchIntegrity` batches HEAD-existence checks through one chunked `ls-tree` (one spawn instead of one per patch × file), and import's dirty-file classification loads the manifest/patch set once with bounded concurrency instead of O(files × patches) IO.
28
+ - **Structure**: `escapeRegex` (6 copies), `getPrimaryStatusCode` (2), the untracked-directory expander (2 — the shared one now carries the 5000-per-dir cap, and `reset`/`discard` gained status's unborn-HEAD guidance), the pendingRepair-preserving state reset (4 copies → `clearAppliedFurnaceState`), and the cross-patch claim multimap (3 copies → `buildPatchClaims`) are each single implementations; the second patch-delete implementation (`deletePatch`) is gone in favor of `removePatchFileAndManifest`; the furnace batch-apply loops take an `ApplyBatchContext` instead of 12–13 positional parameters; `stripJsComments` tokenizes with acorn so regex literals survive (legacy regex fallback for preprocessor-laden chrome sources).
29
+ - **Follow-up findings** (same review, second pass): `doctor --repair-patches-manifest` now rebuilds `patches.json` **under the patch-directory lock** — the repair used to write the manifest lock-free, so it could race a concurrent export/reorder and clobber the other writer (a lock-wait regression test pins the serialization, alongside the existing `stampPatchVersions` pin). `patch tier` and `patch lint-ignore` now follow the full destructive-operation contract via `confirmDestructive` (summary, `--dry-run`, confirmation or `--yes`, non-TTY refusal with `--yes` guidance) — both accepted `--yes` without ever prompting, so the flag only appeared in the history record; suppressing lint findings in particular is a mutation an operator should consciously approve. The root `--version` edge case flagged in the same pass (`fireforge --verbose --version` failing on the argv-length-3 fast path) was already fixed above; two CLI tests now pin both the multi-flag form and the non-hijacking of `source set --version`.
30
+ - **Infrastructure**: all GitHub Actions are SHA-pinned (release workflow included); `coverage.include: ['src/**']` closes the Vitest 4 hole where never-imported modules were invisible to thresholds (with new per-module pins for the fuzz/state-persist/step-error modules); the ~2-minute pack smoke test runs once via `pack:verify` (own config) instead of in every `npm test` and twice per release:check; CI has concurrency groups and a stable `quality (ubuntu)` job name that `setup-rulesets.sh` now matches (the Node version embedded in the name silently invalidated the required check on every bump); the full-integration runner's masked-build-failure detection works (its `ls obj-*` glob never expanded under spawn); the coverage-pin matcher requires a path-separator boundary; husky hooks only prepend Homebrew paths that exist; `docs/*.md` and `knip.json` are formatted; `isolatedModules` + `verbatimModuleSyntax` are on (zero fallout); the published tarball no longer ships dangling `sourceMappingURL` comments (build emits no maps); machine-mode logging and the lock races have dedicated unit suites; `docs/lifecycle-invariants.md` records the new lock-ownership, manifest-corruption, and state-persistence invariants with their enforcing tests.
31
+
3
32
  ## 0.35.0
4
33
 
5
34
  - Fixed the **patch symlink-escape guard being dead code**: the check meant to reject patches targeting a symlink that points outside `engine/` normalized the path text with `resolve()` but never followed the link, so it could not reject anything (the path had already passed the relative-containment check). Target validation now resolves the physical write destination with `realpath` — covering the direct symlink, a _dangling_ symlink (whose destination a write would still create), and a symlinked parent directory — and compares it against the real engine root. Regression tests pin all three escape shapes plus the two accept shapes (inside-tree link, plain new file).
package/README.md CHANGED
@@ -23,7 +23,7 @@ Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon
23
23
  - Node.js 22.22.1+
24
24
  - Python 3
25
25
  - Git
26
- - The normal Firefox platform build tools: Xcode command line tools on macOS, `build-essential`-style packages on Linux, Visual Studio Build Tools on Windows (never tested on Windows tbh)
26
+ - The normal Firefox platform build tools: Xcode command line tools on macOS, `build-essential`-style packages on Linux, Visual Studio Build Tools on Windows (never tested on Windows tbh — smoke-run process cleanup there uses `taskkill /T /F` and is best-effort)
27
27
  - Watchman, if you want `fireforge watch` (optional)
28
28
 
29
29
  ## Getting Started
@@ -88,14 +88,39 @@ Queue maintenance lives under `fireforge patch`: `patch compact` closes ordinal
88
88
  new one as a single transaction — including staged-dependency owner rewrites — with
89
89
  `--dry-run` support.
90
90
 
91
- `fireforge test <directory>` runs exactly that directory: the argument is normalized with a
92
- trailing `/` so mach's prefix-based path matching cannot silently sweep in sibling
93
- directories sharing the name prefix (excluded siblings are echoed with their test-file
94
- counts). Multiple test paths run as sequential per-file shards by default — announced with
95
- a notice, since isolated instances do not exercise cross-file state (`--no-shard` restores
96
- one combined invocation). Recognized harness crashes retry up to `--harness-retries <n>`
97
- times (default 2), and `--perf-samples <path>` publishes a perf-sample artifact path to the
98
- harness (exported as `<BINARYNAME>_PERF_SAMPLE_JSON`).
91
+ `fireforge test <directory>` runs exactly that directory: FireForge enumerates the
92
+ directory's test files and passes the explicit file list to mach in one invocation, so
93
+ mach's prefix-based path matching cannot silently sweep in sibling directories sharing the
94
+ name prefix (excluded siblings are echoed with their test-file counts). Multiple path
95
+ arguments run as sequential shards by default one browser instance per argument, with a
96
+ directory argument keeping its files together announced with a notice, since isolated
97
+ instances do not exercise cross-argument state (`--no-shard` restores one combined
98
+ invocation). Recognized harness crashes retry up to `--harness-retries <n>` times (default
99
+ 2), and `--perf-samples <path>` publishes a perf-sample artifact path to the harness
100
+ (exported as `<BINARYNAME>_PERF_SAMPLE_JSON`).
101
+ Pathless test runs must choose a mode: `--auto` forwards mach's own auto-selection,
102
+ `--doctor` runs the Marionette preflight only, and `--canary [path]` runs one short
103
+ browser-chrome canary (`test.canaryPath` / `test.canaryTimeoutSeconds` in `fireforge.json`
104
+ provide defaults). When packageable engine files changed since the last successful
105
+ FireForge build, `test` fails before launching stale artifacts; use `--build` to refresh
106
+ or `--allow-stale-build` only for intentional out-of-band rebuilds. If an interrupted
107
+ browser holds the Marionette port, `--kill-stale-marionette` terminates recognized browser
108
+ holders and still refuses unrelated listeners. In dev builds, files under `obj-*/dist/bin`
109
+ may be symlinks back into the source tree (notably prefs), so edit source prefs directly
110
+ and keep a backup before bisection experiments.
111
+
112
+ Patch names are normalized on export/rename: `window-chrome-tests`,
113
+ `ui-window-chrome-tests`, and `235-ui-window-chrome-tests` all resolve to the same
114
+ `<order>-ui-window-chrome-tests.patch` shape. For focused queue checks,
115
+ `lint --per-patch --patches` accepts repeated flags, comma lists, full filenames/stems,
116
+ manifest names, category-prefixed slugs, and bare slugs.
117
+
118
+ Projects with generated asset prerequisites can declare opt-in `externalToolchains` in
119
+ `fireforge.json`; `fireforge doctor` reports missing required tools as errors and missing
120
+ optional tools as warnings. Seasonal branding on macOS, for example, can declare Apple's
121
+ Icon Composer tool at
122
+ `/Applications/Xcode.app/Contents/Applications/Icon Composer.app/Contents/Executables/ictool`,
123
+ plus `actool` via `xcrun`, `sips`, and `iconutil`.
99
124
  Design tokens are managed with `fireforge token add`; pass `--create-category` to declare a
100
125
  new category banner and insert the token in one step.
101
126
 
@@ -16,6 +16,7 @@ import { installBrokenPipeHandler, main } from '../src/cli.js';
16
16
  import { forceReleaseFurnaceLocksForActiveOperations, isSignalRollbackInFlight, rollbackActiveOperationsForSignal, } from '../src/core/furnace-operation.js';
17
17
  import { waitForActiveCriticalSections } from '../src/core/signal-critical.js';
18
18
  import { CommandError } from '../src/errors/base.js';
19
+ import { waitForActiveChildShutdown } from '../src/utils/process.js';
19
20
  /**
20
21
  * Upper bound (ms) the signal handler will wait for any in-flight critical
21
22
  * section (e.g. rebase apply + session persist) to finish before calling
@@ -23,6 +24,16 @@ import { CommandError } from '../src/errors/base.js';
23
24
  * postpone the exit a user requested with Ctrl+C.
24
25
  */
25
26
  const SIGNAL_CRITICAL_SECTION_TIMEOUT_MS = 5_000;
27
+ /**
28
+ * Upper bound (ms) the signal handler waits for spawned children (Firefox
29
+ * under `run`/`test`, mach under `build`) to shut down after the signal was
30
+ * forwarded to them. Must exceed the largest child grace window
31
+ * (execSmokeRun's killGraceMs default of 10 s) so the SIGTERM → grace →
32
+ * SIGKILL escalation can actually complete — exiting earlier is what used
33
+ * to orphan hung Firefox trees. A second Ctrl+C escalates to SIGKILL
34
+ * immediately, so an impatient operator is never stuck waiting.
35
+ */
36
+ const CHILD_SHUTDOWN_TIMEOUT_MS = 12_000;
26
37
  installBrokenPipeHandler();
27
38
  process.on('unhandledRejection', (reason) => {
28
39
  console.error('Fatal error (unhandled rejection):', reason instanceof Error ? reason.message : reason);
@@ -45,16 +56,21 @@ function installFurnaceSignalHandler(signal, exitCode) {
45
56
  // rather than queueing another rollback that will race the first.
46
57
  process.exit(exitCode);
47
58
  }
48
- // Run furnace rollback and signal-critical-section drain in parallel.
49
- // Rebase-style operations register critical sections (apply + session
50
- // persist) via `runInSignalCriticalSection`; awaiting them here ensures
51
- // the CLI never exits with a patch applied to the engine but a stale
52
- // session file that would mis-track progress on `--continue`.
59
+ // Run furnace rollback, signal-critical-section drain, and child
60
+ // shutdown in parallel. Rebase-style operations register critical
61
+ // sections (apply + session persist) via `runInSignalCriticalSection`;
62
+ // awaiting them here ensures the CLI never exits with a patch applied
63
+ // to the engine but a stale session file that would mis-track progress
64
+ // on `--continue`. Waiting on child shutdown keeps the parent alive
65
+ // through the SIGTERM → grace → SIGKILL escalation that
66
+ // execInherit/execSmokeRun forward to Firefox/mach — exiting before it
67
+ // ran is what used to orphan hung child trees.
53
68
  void Promise.allSettled([
54
69
  rollbackActiveOperationsForSignal(signal).catch((error) => {
55
70
  console.error(`Furnace rollback after ${signal} failed:`, error instanceof Error ? error.message : error);
56
71
  }),
57
72
  waitForActiveCriticalSections(SIGNAL_CRITICAL_SECTION_TIMEOUT_MS),
73
+ waitForActiveChildShutdown(CHILD_SHUTDOWN_TIMEOUT_MS),
58
74
  ])
59
75
  // Force-release the furnace lock directory after rollback completes.
60
76
  // `withFileLock`'s `finally { rm }` never runs when we `process.exit`
@@ -79,4 +95,3 @@ main().catch((error) => {
79
95
  console.error('Fatal error:', error);
80
96
  process.exit(1);
81
97
  });
82
- //# sourceMappingURL=fireforge.js.map
package/dist/src/cli.js CHANGED
@@ -58,8 +58,9 @@ export function resetBrokenPipeHandlerForTests() {
58
58
  }
59
59
  /**
60
60
  * Maximum number of directory levels to walk when searching for
61
- * `fireforge.json`. Guards against symlink cycles and pathologically
62
- * deep trees.
61
+ * `fireforge.json`. `dirname()` walking is pure string manipulation and
62
+ * cannot cycle (the `parent === current` check already terminates at the
63
+ * root); this cap only bounds the cost on pathologically deep paths.
63
64
  */
64
65
  const MAX_PROJECT_ROOT_WALK_DEPTH = 50;
65
66
  /**
@@ -100,7 +101,10 @@ export function withErrorHandling(handler) {
100
101
  catch (error) {
101
102
  if (error instanceof CancellationError) {
102
103
  cancel('Operation cancelled');
103
- throw new CommandError(ExitCode.GENERAL_ERROR);
104
+ // 130 (128+SIGINT) is the conventional "user interrupted" code —
105
+ // scripts/CI can distinguish a deliberate prompt cancellation from
106
+ // a real failure (which exits 1).
107
+ throw new CommandError(ExitCode.USER_CANCELLED);
104
108
  }
105
109
  if (error instanceof FireForgeError) {
106
110
  logError(error.userMessage);
@@ -154,6 +158,11 @@ function buildGroupedHelpFormatter(manifest) {
154
158
  const desc = helper.optionDescription(opt);
155
159
  return formatHelpLine(term, desc, termWidth, helpWidth);
156
160
  });
161
+ // -V/--version is handled in main() before commander parses (a real
162
+ // root-level `.version()` option would claim `--version` ANYWHERE in
163
+ // argv under commander's default parsing, breaking subcommand flags
164
+ // like `source set --version <v>`); advertise it here so root help
165
+ // stays truthful.
157
166
  optionLines.unshift(formatHelpLine('-V, --version', 'output the version number', termWidth, helpWidth));
158
167
  if (optionLines.length > 0) {
159
168
  output.push('Options:');
@@ -256,11 +265,19 @@ export function createProgram() {
256
265
  * Main CLI entry point.
257
266
  */
258
267
  export async function main() {
259
- if (process.argv.length === 3 && (process.argv[2] === '--version' || process.argv[2] === '-V')) {
268
+ // Root-level --version/-V handling. Deliberately NOT a commander
269
+ // `.version()` option: under commander's default (non-positional)
270
+ // parsing, a root version option claims `--version` anywhere in argv and
271
+ // hijacks subcommand flags like `source set --version <v>`. The rule
272
+ // here: when NO subcommand was given, any -V/--version among the root
273
+ // flags prints the version — so `fireforge --verbose --version` works,
274
+ // and `fireforge source set --version 152` is untouched.
275
+ const userArgs = process.argv.slice(2);
276
+ const hasSubcommand = userArgs.some((arg) => !arg.startsWith('-'));
277
+ if (!hasSubcommand && userArgs.some((arg) => arg === '--version' || arg === '-V')) {
260
278
  process.stdout.write(`${getPackageVersion()}\n`);
261
279
  return;
262
280
  }
263
281
  const program = createProgram();
264
282
  await program.parseAsync(process.argv);
265
283
  }
266
- //# sourceMappingURL=cli.js.map
@@ -63,4 +63,3 @@ export async function runPostBootstrapChecks(issues) {
63
63
  }
64
64
  return checks;
65
65
  }
66
- //# sourceMappingURL=bootstrap-checks.js.map
@@ -1,9 +1,10 @@
1
1
  import { Command } from 'commander';
2
+ import { ExitCode } from '../errors/codes.js';
2
3
  import type { CommandContext } from '../types/cli.js';
3
4
  /**
4
5
  * Runs the bootstrap command.
5
6
  * @param projectRoot - Root directory of the project
6
7
  */
7
- export declare function bootstrapCommand(projectRoot: string): Promise<void>;
8
+ export declare function bootstrapCommand(projectRoot: string): Promise<ExitCode>;
8
9
  /** Registers the bootstrap command on the CLI program. */
9
10
  export declare function registerBootstrap(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
@@ -1,8 +1,11 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { Command } from 'commander';
1
3
  import { getProjectPaths } from '../core/config.js';
2
4
  import { ensureOriginRemote } from '../core/git.js';
3
5
  import { bootstrapWithOutput } from '../core/mach.js';
4
6
  import { GeneralError } from '../errors/base.js';
5
7
  import { BootstrapError } from '../errors/build.js';
8
+ import { ExitCode } from '../errors/codes.js';
6
9
  import { pathExists } from '../utils/fs.js';
7
10
  import { error, info, intro, outro, warn } from '../utils/logger.js';
8
11
  import { detectBootstrapIssues, runPostBootstrapChecks } from './bootstrap-checks.js';
@@ -72,16 +75,20 @@ export async function bootstrapCommand(projectRoot) {
72
75
  else {
73
76
  warn('Bootstrap completed with warnings:');
74
77
  }
75
- reportDoctorResults(checks);
78
+ const checksExitCode = reportDoctorResults(checks);
76
79
  if (hasErrors) {
77
80
  outro('Build dependencies installed with errors');
81
+ // Propagate the failure (mirroring doctor's exit-code handling)
82
+ // instead of exiting 0 — CI gating on bootstrap used to proceed to a
83
+ // build that could not succeed because error-severity check results
84
+ // were discarded here.
85
+ return checksExitCode;
78
86
  }
79
- else {
80
- outro('Build dependencies installed with warnings');
81
- }
82
- return;
87
+ outro('Build dependencies installed with warnings');
88
+ return ExitCode.SUCCESS;
83
89
  }
84
90
  outro('Build dependencies installed successfully!');
91
+ return ExitCode.SUCCESS;
85
92
  }
86
93
  /** Registers the bootstrap command on the CLI program. */
87
94
  export function registerBootstrap(program, { getProjectRoot, withErrorHandling }) {
@@ -89,7 +96,9 @@ export function registerBootstrap(program, { getProjectRoot, withErrorHandling }
89
96
  .command('bootstrap')
90
97
  .description('Install Firefox build dependencies')
91
98
  .action(withErrorHandling(async () => {
92
- await bootstrapCommand(getProjectRoot());
99
+ const exitCode = await bootstrapCommand(getProjectRoot());
100
+ if (exitCode !== ExitCode.SUCCESS) {
101
+ process.exitCode = exitCode;
102
+ }
93
103
  }));
94
104
  }
95
- //# sourceMappingURL=bootstrap.js.map
@@ -1,10 +1,11 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
- import { InvalidArgumentError as CommanderInvalidArgumentError } from 'commander';
2
+ import { Command, InvalidArgumentError as CommanderInvalidArgumentError } from 'commander';
3
3
  import { validateBrandOverride } from '../core/brand-validation.js';
4
4
  import { auditBuildArtifacts } from '../core/build-audit.js';
5
5
  import { readBuildBaseline, writeBuildBaseline } from '../core/build-baseline.js';
6
6
  import { prepareBuildEnvironment } from '../core/build-prepare.js';
7
7
  import { getProjectPaths, loadConfig } from '../core/config.js';
8
+ import { withEngineSessionLock } from '../core/engine-session-lock.js';
8
9
  import { attemptMozinfoRewrite, build, buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, hasRunnableBundle, runMach, withBuildLock, } from '../core/mach.js';
9
10
  import { buildHarnessCrashMessage } from '../core/test-harness-crash.js';
10
11
  import { formatToolchainMismatchMessage, runToolchainPreflight, } from '../core/toolchain-preflight.js';
@@ -361,7 +362,7 @@ export function registerBuild(program, { getProjectRoot, withErrorHandling }) {
361
362
  'move, in which case a clean rebuild is still required.',
362
363
  ].join('\n'))
363
364
  .action(withErrorHandling(async (options) => {
364
- await buildCommand(getProjectRoot(), pickDefined(options));
365
+ const projectRoot = getProjectRoot();
366
+ await withEngineSessionLock(projectRoot, 'build', () => buildCommand(projectRoot, pickDefined(options)));
365
367
  }));
366
368
  }
367
- //# sourceMappingURL=build.js.map
@@ -1,5 +1,8 @@
1
- import { configExists, loadConfig, loadRawConfigDocument, mutateConfig, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, withConfigFileLock, writeConfig, writeConfigDocument, } from '../core/config.js';
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { Command } from 'commander';
3
+ import { configExists, loadRawConfigDocument, mutateConfig, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, validateConfig, withConfigFileLock, writeConfigDocument, } from '../core/config.js';
2
4
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
5
+ import { ConfigError } from '../errors/config.js';
3
6
  import { toError } from '../utils/errors.js';
4
7
  import { info, intro, outro, success, warn } from '../utils/logger.js';
5
8
  import { pickDefined } from '../utils/options.js';
@@ -154,24 +157,42 @@ export async function configCommand(projectRoot, key, value, options = {}) {
154
157
  // for a *known* key. Apply strict validation whenever the key is
155
158
  // listed in SUPPORTED_CONFIG_PATHS, regardless of --force, and only
156
159
  // skip validation for genuinely unknown key paths.
160
+ //
161
+ // BOTH branches seed the mutation from the raw on-disk document.
162
+ // The known-key branch used to round-trip through `loadConfig` →
163
+ // `validateConfig`, which builds a typed clone containing only the
164
+ // known schema fields — so any ordinary `fireforge config <key>
165
+ // <value>` silently dropped every previously --force-written key
166
+ // from fireforge.json (2026-07-05 review, finding H4; the --force
167
+ // branch's comment described this exact hazard for its own path).
157
168
  if (options.force && !keyIsKnown) {
158
- // Seed mutation from the raw on-disk document so previously-forced
159
- // keys (which `validateConfig` would strip) survive the round-trip.
160
- // Without this, writing a second --force key would silently drop
161
- // every earlier forced key from fireforge.json.
162
169
  const updatedConfig = mutateConfig(rawConfig, key, parsedValue, true);
163
170
  await writeConfigDocument(projectRoot, updatedConfig);
164
171
  }
165
172
  else {
166
- const config = await loadConfig(projectRoot);
167
- const updatedConfig = mutateConfig(config, key, parsedValue);
168
- await writeConfig(projectRoot, updatedConfig);
173
+ // Mutate the raw document (preserving unknown keys), then run
174
+ // strict validation on the RESULT — validateConfig checks the
175
+ // known schema fields and ignores unknown keys, so this keeps
176
+ // exactly the old validation strength while writing the
177
+ // unstripped document.
178
+ const updatedConfig = mutateConfig(rawConfig, key, parsedValue, true);
179
+ validateConfig(updatedConfig);
180
+ await writeConfigDocument(projectRoot, updatedConfig);
169
181
  }
170
182
  return false;
171
183
  });
172
184
  }
173
185
  catch (error) {
174
- throw new InvalidArgumentError(`Invalid value for "${key}": ${toError(error).message}`, key);
186
+ // Only value/validation problems are the user's "invalid value".
187
+ // Lock-acquisition timeouts and I/O failures must keep their own
188
+ // types/messages — re-labelling a lock timeout as `Invalid value for
189
+ // "<key>"` used to point diagnosis at the value (and return the
190
+ // wrong exit-code class) when the actual problem was a concurrent
191
+ // fireforge process holding the config lock.
192
+ if (error instanceof ConfigError) {
193
+ throw new InvalidArgumentError(`Invalid value for "${key}": ${toError(error).message}`, key);
194
+ }
195
+ throw error;
175
196
  }
176
197
  if (unchanged) {
177
198
  info(`${key} = ${formatValue(parsedValue)} (unchanged)`);
@@ -225,4 +246,3 @@ export function registerConfig(program, { getProjectRoot, withErrorHandling }) {
225
246
  await configCommand(getProjectRoot(), key, value, pickDefined(options));
226
247
  }));
227
248
  }
228
- //# sourceMappingURL=config.js.map
@@ -1,8 +1,9 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { confirm } from '@clack/prompts';
3
+ import { Command } from 'commander';
3
4
  import { getProjectPaths } from '../core/config.js';
4
5
  import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
5
- import { isGitRepository } from '../core/git.js';
6
+ import { getHead, isGitRepository, isMissingHeadError } from '../core/git.js';
6
7
  import { discardStatusEntry } from '../core/git-file-ops.js';
7
8
  import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
8
9
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
@@ -104,6 +105,18 @@ export async function discardCommand(projectRoot, file, options = {}) {
104
105
  if (!(await isGitRepository(paths.engine))) {
105
106
  throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
106
107
  }
108
+ // Unborn-HEAD guard (mirrors status): an interrupted download leaves a
109
+ // repo with no baseline commit, where the entire ~300k-file tree reads
110
+ // as untracked — a discard against that state is never what the operator
111
+ // wants, and the guidance names the actual fix.
112
+ try {
113
+ await getHead(paths.engine);
114
+ }
115
+ catch (headError) {
116
+ if (!isMissingHeadError(headError))
117
+ throw headError;
118
+ throw new GeneralError('Engine repository has no baseline commit yet — a previous "fireforge download" was interrupted before git created the initial Firefox source commit. Re-run "fireforge download --force" to recreate the baseline repository cleanly.');
119
+ }
107
120
  // Check if the file has changes
108
121
  const statusEntries = await expandUntrackedDirectoryEntries(paths.engine, await getWorkingTreeStatus(paths.engine));
109
122
  const statusEntry = statusEntries.find((entry) => entry.file === file || entry.originalPath === file);
@@ -145,7 +158,10 @@ export async function discardCommand(projectRoot, file, options = {}) {
145
158
  }
146
159
  }
147
160
  if (options.dryRun) {
148
- const target = statusEntry.originalPath === file
161
+ // Show the rename pair regardless of which side the operator passed —
162
+ // passing the NEW path of a rename used to hide that discarding also
163
+ // resurrects the old path, exactly when the full picture matters most.
164
+ const target = statusEntry.originalPath
149
165
  ? `${statusEntry.originalPath} -> ${statusEntry.file}`
150
166
  : statusEntry.file;
151
167
  info(`Would discard changes to: ${target}`);
@@ -194,4 +210,3 @@ export function registerDiscard(program, { getProjectRoot, withErrorHandling })
194
210
  await discardCommand(getProjectRoot(), file, pickDefined(options));
195
211
  }));
196
212
  }
197
- //# sourceMappingURL=discard.js.map
@@ -83,4 +83,3 @@ export const POST_REBASE_AUDIT_CHECK = {
83
83
  dependsOn: ['fireforge.json is valid'],
84
84
  run: runPostRebaseAudit,
85
85
  };
86
- //# sourceMappingURL=post-rebase-audit.js.map
@@ -29,4 +29,3 @@ export function warning(name, message, fix) {
29
29
  export function failure(name, message, fix) {
30
30
  return { name, passed: false, severity: 'error', message, ...(fix ? { fix } : {}) };
31
31
  }
32
- //# sourceMappingURL=doctor-check-core.js.map
@@ -0,0 +1,3 @@
1
+ import type { DoctorCheckDefinition } from './doctor-check-core.js';
2
+ /** Doctor check for opt-in project-declared external asset/toolchain requirements. */
3
+ export declare const EXTERNAL_TOOLCHAIN_DOCTOR_CHECK: DoctorCheckDefinition;
@@ -0,0 +1,54 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { isAbsolute, resolve } from 'node:path';
3
+ import { pathExists } from '../utils/fs.js';
4
+ import { exec, findExecutable } from '../utils/process.js';
5
+ import { failure, warning } from './doctor-check-core.js';
6
+ async function resolveDeclaredTool(projectRoot, tool) {
7
+ if (tool.path !== undefined) {
8
+ const candidate = isAbsolute(tool.path) ? tool.path : resolve(projectRoot, tool.path);
9
+ return (await pathExists(candidate)) ? candidate : undefined;
10
+ }
11
+ if (tool.xcrun === true) {
12
+ try {
13
+ const found = await exec('xcrun', ['-find', tool.name], { timeout: 5000 });
14
+ const line = found.stdout.split(/\r?\n/).find((entry) => entry.trim().length > 0);
15
+ return found.exitCode === 0 ? line?.trim() : undefined;
16
+ }
17
+ catch {
18
+ return undefined;
19
+ }
20
+ }
21
+ return findExecutable(tool.name);
22
+ }
23
+ async function runExternalToolchainChecks(ctx) {
24
+ const toolchains = ctx.config?.externalToolchains;
25
+ if (!toolchains || toolchains.length === 0)
26
+ return [];
27
+ const rows = [];
28
+ for (const toolchain of toolchains) {
29
+ for (const tool of toolchain.tools) {
30
+ const label = `External toolchain: ${toolchain.name}/${tool.name}`;
31
+ const found = await resolveDeclaredTool(ctx.projectRoot, tool);
32
+ if (found !== undefined) {
33
+ rows.push({ name: label, passed: true, severity: 'ok', message: `OK (${found})` });
34
+ continue;
35
+ }
36
+ const required = tool.required !== false;
37
+ const location = tool.path
38
+ ? `at ${tool.path}`
39
+ : tool.xcrun
40
+ ? `via xcrun -find ${tool.name}`
41
+ : 'on PATH';
42
+ const message = `${tool.name} was not found ${location}.`;
43
+ const fix = `Install ${tool.name} or update fireforge.json externalToolchains for ${toolchain.name}.`;
44
+ rows.push(required ? failure(label, message, fix) : warning(label, message, fix));
45
+ }
46
+ }
47
+ return rows;
48
+ }
49
+ /** Doctor check for opt-in project-declared external asset/toolchain requirements. */
50
+ export const EXTERNAL_TOOLCHAIN_DOCTOR_CHECK = {
51
+ name: 'External toolchains',
52
+ dependsOn: ['fireforge.json is valid'],
53
+ run: runExternalToolchainChecks,
54
+ };
@@ -44,4 +44,3 @@ export const furnaceStaleJarRegistrationCheck = {
44
44
  }
45
45
  },
46
46
  };
47
- //# sourceMappingURL=doctor-furnace-jar.js.map
@@ -194,4 +194,3 @@ export const furnaceManifestSyncCheck = {
194
194
  'Nothing to repair (orphans surfaced but all were already recoverable).');
195
195
  },
196
196
  };
197
- //# sourceMappingURL=doctor-furnace-manifest-sync.js.map
@@ -6,6 +6,7 @@ import { hasCustomEngineDrift, hasOverrideEngineDrift } from '../core/furnace-ap
6
6
  import { getFurnacePaths, loadFurnaceConfig, loadFurnaceState, updateFurnaceState, } from '../core/furnace-config.js';
7
7
  import { CUSTOM_ELEMENTS_JS, JAR_MN, resolveFtlDir } from '../core/furnace-constants.js';
8
8
  import { getFurnaceLockPath, runFurnaceMutation } from '../core/furnace-operation.js';
9
+ import { hasBlockingStepErrors } from '../core/furnace-step-errors.js';
9
10
  import { validateAllComponents } from '../core/furnace-validate.js';
10
11
  import { toError } from '../utils/errors.js';
11
12
  import { pathExists } from '../utils/fs.js';
@@ -25,7 +26,7 @@ async function runRepairApply(projectRoot) {
25
26
  return runFurnaceMutation(projectRoot, 'apply-rollback', (ctx) => applyAllComponents(projectRoot, false, { operationContext: ctx }), { skipPendingRepairCheck: true });
26
27
  }
27
28
  function countApplyFailures(applyResult) {
28
- const appliedWithStepErrors = applyResult.applied.filter((entry) => (entry.stepErrors?.length ?? 0) > 0).length;
29
+ const appliedWithStepErrors = applyResult.applied.filter((entry) => hasBlockingStepErrors(entry)).length;
29
30
  return applyResult.errors.length + appliedWithStepErrors;
30
31
  }
31
32
  function firstApplyFailure(applyResult) {
@@ -512,4 +513,3 @@ export const FURNACE_DOCTOR_CHECKS = [
512
513
  furnaceComponentValidationCheck,
513
514
  furnaceManifestSyncCheck,
514
515
  ];
515
- //# sourceMappingURL=doctor-furnace.js.map
@@ -94,4 +94,3 @@ export async function inspectEngineWorkingTree(ctx) {
94
94
  : '';
95
95
  return warning('Engine working tree', `Engine working tree has ${counts.unmanaged} unmanaged change${counts.unmanaged === 1 ? '' : 's'}.${managedTail}`, 'Use "fireforge status --ownership" to separate patch-backed from unmanaged files, then export, discard, or reset only the unmanaged set.');
96
96
  }
97
- //# sourceMappingURL=doctor-working-tree.js.map
@@ -1,3 +1,5 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { Command } from 'commander';
1
3
  import { configExists, getProjectPaths, loadConfig, loadState, updateState, } from '../core/config.js';
2
4
  import { furnaceConfigExists as checkFurnaceConfigExists } from '../core/furnace-config.js';
3
5
  import { getCurrentBranch, getHead, isGitRepository, isMissingHeadError } from '../core/git.js';
@@ -12,6 +14,7 @@ import { error, info, intro, outro, success, warn } from '../utils/logger.js';
12
14
  import { findExecutable } from '../utils/process.js';
13
15
  import { POST_REBASE_AUDIT_CHECK } from './doctor/post-rebase-audit.js';
14
16
  import { failure, ok, warning } from './doctor-check-core.js';
17
+ import { EXTERNAL_TOOLCHAIN_DOCTOR_CHECK } from './doctor-external-toolchains.js';
15
18
  import { FURNACE_DOCTOR_CHECKS } from './doctor-furnace.js';
16
19
  import { inspectEngineWorkingTree } from './doctor-working-tree.js';
17
20
  import { collectPatchQueueHealth } from './verify.js';
@@ -173,6 +176,7 @@ const DOCTOR_CHECKS = [
173
176
  },
174
177
  fix: 'Check fireforge.json for syntax errors or missing fields',
175
178
  },
179
+ EXTERNAL_TOOLCHAIN_DOCTOR_CHECK,
176
180
  {
177
181
  name: 'Engine directory exists',
178
182
  run: (ctx) => {
@@ -476,4 +480,3 @@ export function registerDoctor(program, { getProjectRoot, withErrorHandling }) {
476
480
  }
477
481
  }));
478
482
  }
479
- //# sourceMappingURL=doctor.js.map