@hominis/fireforge 0.43.1 → 0.44.1

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 (478) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +3 -3
  3. package/dist/bin/fireforge.d.ts +4 -9
  4. package/dist/bin/fireforge.js +42 -40
  5. package/dist/build-info.json +4 -4
  6. package/dist/src/cli.js +74 -8
  7. package/dist/src/commands/bootstrap.js +17 -20
  8. package/dist/src/commands/build.js +25 -32
  9. package/dist/src/commands/config.js +34 -41
  10. package/dist/src/commands/discard.js +8 -9
  11. package/dist/src/commands/doctor-check-core.d.ts +13 -18
  12. package/dist/src/commands/doctor-check-core.js +8 -35
  13. package/dist/src/commands/doctor-external-toolchains.js +2 -2
  14. package/dist/src/commands/doctor-furnace-config-sync.d.ts +18 -0
  15. package/dist/src/commands/{doctor-furnace-manifest-sync.js → doctor-furnace-config-sync.js} +27 -26
  16. package/dist/src/commands/doctor-furnace-jar.d.ts +7 -9
  17. package/dist/src/commands/doctor-furnace-jar.js +7 -9
  18. package/dist/src/commands/doctor-furnace.js +21 -24
  19. package/dist/src/commands/doctor-orphaned-harness.d.ts +16 -20
  20. package/dist/src/commands/doctor-orphaned-harness.js +18 -44
  21. package/dist/src/commands/doctor-patch-manifest.d.ts +8 -0
  22. package/dist/src/commands/doctor-patch-manifest.js +155 -0
  23. package/dist/src/commands/doctor-post-rebase-audit.d.ts +2 -0
  24. package/dist/src/commands/{doctor/post-rebase-audit.js → doctor-post-rebase-audit.js} +3 -3
  25. package/dist/src/commands/doctor-working-tree.d.ts +10 -12
  26. package/dist/src/commands/doctor-working-tree.js +11 -18
  27. package/dist/src/commands/doctor.d.ts +1 -1
  28. package/dist/src/commands/doctor.js +64 -96
  29. package/dist/src/commands/download.js +32 -48
  30. package/dist/src/commands/export-all.js +29 -33
  31. package/dist/src/commands/export-flow.js +6 -6
  32. package/dist/src/commands/export-placement-conflicts.js +10 -9
  33. package/dist/src/commands/export-placement-gate.js +5 -5
  34. package/dist/src/commands/export-shared.d.ts +30 -33
  35. package/dist/src/commands/export-shared.js +45 -43
  36. package/dist/src/commands/export.js +21 -27
  37. package/dist/src/commands/furnace/apply.js +14 -23
  38. package/dist/src/commands/furnace/chrome-doc-remove.js +14 -21
  39. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +23 -23
  40. package/dist/src/commands/furnace/chrome-doc-templates.js +23 -23
  41. package/dist/src/commands/furnace/chrome-doc-tests.js +26 -29
  42. package/dist/src/commands/furnace/chrome-doc.d.ts +16 -17
  43. package/dist/src/commands/furnace/chrome-doc.js +31 -41
  44. package/dist/src/commands/furnace/create-browser-test.d.ts +4 -4
  45. package/dist/src/commands/furnace/create-browser-test.js +8 -8
  46. package/dist/src/commands/furnace/create-mochikit.d.ts +6 -7
  47. package/dist/src/commands/furnace/create-mochikit.js +8 -9
  48. package/dist/src/commands/furnace/create-readback.d.ts +12 -13
  49. package/dist/src/commands/furnace/create-readback.js +12 -13
  50. package/dist/src/commands/furnace/create-templates.d.ts +22 -28
  51. package/dist/src/commands/furnace/create-templates.js +35 -30
  52. package/dist/src/commands/furnace/create-xpcshell.js +2 -2
  53. package/dist/src/commands/furnace/create.js +33 -23
  54. package/dist/src/commands/furnace/deploy.js +16 -24
  55. package/dist/src/commands/furnace/diff.js +15 -10
  56. package/dist/src/commands/furnace/init.js +33 -36
  57. package/dist/src/commands/furnace/override.js +23 -25
  58. package/dist/src/commands/furnace/preview.js +47 -67
  59. package/dist/src/commands/furnace/refresh.js +12 -16
  60. package/dist/src/commands/furnace/remove-mutations.d.ts +3 -6
  61. package/dist/src/commands/furnace/remove-mutations.js +14 -17
  62. package/dist/src/commands/furnace/remove-state.js +1 -1
  63. package/dist/src/commands/furnace/remove.js +33 -38
  64. package/dist/src/commands/furnace/rename-xpcshell.d.ts +8 -11
  65. package/dist/src/commands/furnace/rename-xpcshell.js +8 -11
  66. package/dist/src/commands/furnace/rename.js +59 -69
  67. package/dist/src/commands/furnace/scan.js +19 -33
  68. package/dist/src/commands/furnace/status.js +12 -9
  69. package/dist/src/commands/furnace/sync.js +9 -16
  70. package/dist/src/commands/furnace/validate.js +20 -20
  71. package/dist/src/commands/import.js +48 -37
  72. package/dist/src/commands/lint-per-patch.js +15 -10
  73. package/dist/src/commands/lint-per-run-checkjs.d.ts +13 -13
  74. package/dist/src/commands/lint-per-run-checkjs.js +13 -13
  75. package/dist/src/commands/lint-report.js +6 -7
  76. package/dist/src/commands/lint.d.ts +15 -16
  77. package/dist/src/commands/lint.js +86 -102
  78. package/dist/src/commands/package.js +6 -11
  79. package/dist/src/commands/patch/compact.js +1 -1
  80. package/dist/src/commands/patch/delete.js +24 -33
  81. package/dist/src/commands/patch/index.d.ts +5 -6
  82. package/dist/src/commands/patch/index.js +10 -12
  83. package/dist/src/commands/patch/lint-ignore.js +14 -13
  84. package/dist/src/commands/patch/move-files-create.d.ts +6 -6
  85. package/dist/src/commands/patch/move-files-create.js +8 -8
  86. package/dist/src/commands/patch/move-files-into.d.ts +6 -6
  87. package/dist/src/commands/patch/move-files-into.js +8 -8
  88. package/dist/src/commands/patch/patch-context.d.ts +2 -3
  89. package/dist/src/commands/patch/patch-context.js +2 -3
  90. package/dist/src/commands/patch/rename.d.ts +10 -11
  91. package/dist/src/commands/patch/rename.js +11 -12
  92. package/dist/src/commands/patch/reorder.js +8 -9
  93. package/dist/src/commands/patch/split-plan.js +1 -1
  94. package/dist/src/commands/patch/split.d.ts +8 -10
  95. package/dist/src/commands/patch/split.js +9 -11
  96. package/dist/src/commands/patch/staged-dependency-validate.js +8 -8
  97. package/dist/src/commands/patch/staged-dependency.js +7 -3
  98. package/dist/src/commands/patch/tier.d.ts +9 -9
  99. package/dist/src/commands/patch/tier.js +18 -15
  100. package/dist/src/commands/re-export-adjacent.js +9 -8
  101. package/dist/src/commands/re-export-bulk-scan.d.ts +9 -9
  102. package/dist/src/commands/re-export-bulk-scan.js +22 -21
  103. package/dist/src/commands/re-export-drift.js +10 -10
  104. package/dist/src/commands/re-export-files.d.ts +6 -7
  105. package/dist/src/commands/re-export-files.js +34 -44
  106. package/dist/src/commands/re-export-lint.d.ts +10 -13
  107. package/dist/src/commands/re-export-lint.js +16 -14
  108. package/dist/src/commands/re-export-options.d.ts +6 -6
  109. package/dist/src/commands/re-export-options.js +6 -6
  110. package/dist/src/commands/re-export-register.d.ts +4 -10
  111. package/dist/src/commands/re-export-register.js +5 -11
  112. package/dist/src/commands/re-export-scan.js +1 -1
  113. package/dist/src/commands/re-export-single.d.ts +3 -3
  114. package/dist/src/commands/re-export-single.js +33 -25
  115. package/dist/src/commands/re-export.js +13 -21
  116. package/dist/src/commands/rebase/abort.js +3 -3
  117. package/dist/src/commands/rebase/confirm.js +2 -1
  118. package/dist/src/commands/rebase/continue.d.ts +1 -1
  119. package/dist/src/commands/rebase/continue.js +14 -9
  120. package/dist/src/commands/rebase/index.js +11 -11
  121. package/dist/src/commands/rebase/patch-loop.d.ts +7 -1
  122. package/dist/src/commands/rebase/patch-loop.js +60 -39
  123. package/dist/src/commands/rebase/summary.js +1 -1
  124. package/dist/src/commands/register.js +5 -7
  125. package/dist/src/commands/reset.js +2 -1
  126. package/dist/src/commands/resolve.d.ts +16 -13
  127. package/dist/src/commands/resolve.js +30 -33
  128. package/dist/src/commands/run.js +33 -35
  129. package/dist/src/commands/setup-support.js +15 -19
  130. package/dist/src/commands/setup.js +13 -9
  131. package/dist/src/commands/status-check.d.ts +6 -6
  132. package/dist/src/commands/status-json.d.ts +6 -6
  133. package/dist/src/commands/status-json.js +5 -0
  134. package/dist/src/commands/status-output.d.ts +5 -5
  135. package/dist/src/commands/status-output.js +6 -6
  136. package/dist/src/commands/status-ownership.d.ts +16 -14
  137. package/dist/src/commands/status-ownership.js +16 -14
  138. package/dist/src/commands/status.js +42 -34
  139. package/dist/src/commands/test-build-phase.js +26 -13
  140. package/dist/src/commands/test-diagnose.js +44 -44
  141. package/dist/src/commands/test-register.js +1 -0
  142. package/dist/src/commands/test-run.d.ts +14 -16
  143. package/dist/src/commands/test-run.js +21 -13
  144. package/dist/src/commands/test-stale-gate.d.ts +15 -16
  145. package/dist/src/commands/test-stale-gate.js +15 -16
  146. package/dist/src/commands/test-verdict.d.ts +20 -3
  147. package/dist/src/commands/test-verdict.js +30 -0
  148. package/dist/src/commands/test.js +83 -76
  149. package/dist/src/commands/token-coverage.js +3 -4
  150. package/dist/src/commands/token.js +16 -18
  151. package/dist/src/commands/tree.js +47 -31
  152. package/dist/src/commands/typecheck.d.ts +10 -12
  153. package/dist/src/commands/typecheck.js +24 -27
  154. package/dist/src/commands/verify.js +13 -15
  155. package/dist/src/commands/watch.js +21 -23
  156. package/dist/src/commands/wire.js +52 -62
  157. package/dist/src/core/ast-utils.d.ts +8 -7
  158. package/dist/src/core/ast-utils.js +8 -7
  159. package/dist/src/core/branding.d.ts +9 -14
  160. package/dist/src/core/branding.js +7 -11
  161. package/dist/src/core/browser-wire.d.ts +1 -1
  162. package/dist/src/core/browser-wire.js +14 -19
  163. package/dist/src/core/build-audit-platform.js +25 -33
  164. package/dist/src/core/build-audit-registration.js +17 -25
  165. package/dist/src/core/build-audit-resolve.d.ts +15 -17
  166. package/dist/src/core/build-audit-resolve.js +39 -36
  167. package/dist/src/core/build-audit-transforms.js +18 -19
  168. package/dist/src/core/build-audit.js +28 -42
  169. package/dist/src/core/build-baseline-types.d.ts +29 -35
  170. package/dist/src/core/build-baseline.d.ts +5 -4
  171. package/dist/src/core/build-baseline.js +14 -14
  172. package/dist/src/core/build-overwrite-guard.d.ts +7 -14
  173. package/dist/src/core/build-overwrite-guard.js +17 -27
  174. package/dist/src/core/build-prepare.d.ts +2 -2
  175. package/dist/src/core/build-prepare.js +8 -9
  176. package/dist/src/core/config-mutate.js +20 -19
  177. package/dist/src/core/config-state.js +12 -4
  178. package/dist/src/core/config-validate.js +8 -5
  179. package/dist/src/core/config.d.ts +5 -8
  180. package/dist/src/core/config.js +7 -16
  181. package/dist/src/core/coverage-extend.d.ts +15 -13
  182. package/dist/src/core/coverage-extend.js +15 -13
  183. package/dist/src/core/destructive.d.ts +31 -9
  184. package/dist/src/core/destructive.js +29 -26
  185. package/dist/src/core/diff-hunks.js +10 -12
  186. package/dist/src/core/discard-baseline.d.ts +8 -8
  187. package/dist/src/core/discard-baseline.js +16 -15
  188. package/dist/src/core/display-state.d.ts +12 -14
  189. package/dist/src/core/display-state.js +12 -14
  190. package/dist/src/core/engine-changes.d.ts +2 -4
  191. package/dist/src/core/engine-changes.js +2 -4
  192. package/dist/src/core/engine-precondition.d.ts +28 -13
  193. package/dist/src/core/engine-precondition.js +25 -10
  194. package/dist/src/core/engine-session-lock.d.ts +47 -21
  195. package/dist/src/core/engine-session-lock.js +168 -33
  196. package/dist/src/core/export-moved-code-shape.d.ts +10 -10
  197. package/dist/src/core/export-moved-code-shape.js +10 -10
  198. package/dist/src/core/file-lock.d.ts +62 -5
  199. package/dist/src/core/file-lock.js +196 -59
  200. package/dist/src/core/firefox-cache.js +1 -3
  201. package/dist/src/core/firefox-extract.js +10 -14
  202. package/dist/src/core/firefox.d.ts +7 -5
  203. package/dist/src/core/firefox.js +4 -5
  204. package/dist/src/core/furnace-apply-dry-run.d.ts +2 -2
  205. package/dist/src/core/furnace-apply-dry-run.js +3 -2
  206. package/dist/src/core/furnace-apply-ftl.d.ts +7 -9
  207. package/dist/src/core/furnace-apply-ftl.js +16 -13
  208. package/dist/src/core/furnace-apply-helpers.d.ts +7 -7
  209. package/dist/src/core/furnace-apply-helpers.js +30 -21
  210. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +8 -9
  211. package/dist/src/core/furnace-apply-overwrite-warn.js +8 -9
  212. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +3 -4
  213. package/dist/src/core/furnace-apply-undeploy-actions.js +3 -4
  214. package/dist/src/core/furnace-apply.d.ts +5 -8
  215. package/dist/src/core/furnace-apply.js +19 -24
  216. package/dist/src/core/furnace-checksum-utils.d.ts +14 -0
  217. package/dist/src/core/furnace-checksum-utils.js +16 -0
  218. package/dist/src/core/furnace-config.d.ts +21 -30
  219. package/dist/src/core/furnace-config.js +32 -54
  220. package/dist/src/core/furnace-constants.d.ts +8 -0
  221. package/dist/src/core/furnace-constants.js +10 -2
  222. package/dist/src/core/furnace-css-fragments.d.ts +5 -5
  223. package/dist/src/core/furnace-css-fragments.js +5 -5
  224. package/dist/src/core/furnace-jsconfig.d.ts +11 -12
  225. package/dist/src/core/furnace-jsconfig.js +11 -12
  226. package/dist/src/core/furnace-marker.d.ts +7 -8
  227. package/dist/src/core/furnace-marker.js +7 -8
  228. package/dist/src/core/furnace-operation.d.ts +31 -0
  229. package/dist/src/core/furnace-operation.js +54 -17
  230. package/dist/src/core/furnace-precondition.d.ts +61 -0
  231. package/dist/src/core/furnace-precondition.js +67 -0
  232. package/dist/src/core/furnace-registration-remove.d.ts +5 -6
  233. package/dist/src/core/furnace-registration-remove.js +6 -7
  234. package/dist/src/core/furnace-registration-validate.d.ts +13 -0
  235. package/dist/src/core/furnace-registration-validate.js +20 -1
  236. package/dist/src/core/furnace-registration.d.ts +5 -6
  237. package/dist/src/core/furnace-registration.js +10 -12
  238. package/dist/src/core/furnace-rollback.js +7 -7
  239. package/dist/src/core/furnace-scanner.d.ts +1 -0
  240. package/dist/src/core/furnace-scanner.js +1 -2
  241. package/dist/src/core/furnace-stale-export.d.ts +10 -10
  242. package/dist/src/core/furnace-stale-export.js +10 -10
  243. package/dist/src/core/furnace-state-persist.d.ts +6 -8
  244. package/dist/src/core/furnace-state-persist.js +6 -8
  245. package/dist/src/core/furnace-validate-accessibility.d.ts +22 -0
  246. package/dist/src/core/furnace-validate-accessibility.js +310 -1
  247. package/dist/src/core/furnace-validate-compatibility.js +141 -1
  248. package/dist/src/core/furnace-validate-helpers.d.ts +5 -116
  249. package/dist/src/core/furnace-validate-helpers.js +0 -491
  250. package/dist/src/core/furnace-validate-registration.d.ts +7 -0
  251. package/dist/src/core/furnace-validate-registration.js +115 -41
  252. package/dist/src/core/furnace-validate.d.ts +16 -0
  253. package/dist/src/core/furnace-validate.js +188 -145
  254. package/dist/src/core/furnace-version-drift.d.ts +10 -11
  255. package/dist/src/core/furnace-version-drift.js +10 -11
  256. package/dist/src/core/git-base.d.ts +21 -9
  257. package/dist/src/core/git-base.js +24 -9
  258. package/dist/src/core/git-diff.d.ts +20 -9
  259. package/dist/src/core/git-diff.js +148 -63
  260. package/dist/src/core/git-file-ops.d.ts +19 -19
  261. package/dist/src/core/git-file-ops.js +19 -19
  262. package/dist/src/core/git-readonly-index.d.ts +53 -19
  263. package/dist/src/core/git-readonly-index.js +75 -31
  264. package/dist/src/core/git-status.d.ts +5 -6
  265. package/dist/src/core/git-status.js +5 -6
  266. package/dist/src/core/git.d.ts +9 -10
  267. package/dist/src/core/git.js +57 -71
  268. package/dist/src/core/license-headers.d.ts +17 -23
  269. package/dist/src/core/license-headers.js +17 -23
  270. package/dist/src/core/mach-build-artifacts.d.ts +9 -9
  271. package/dist/src/core/mach-build-artifacts.js +57 -19
  272. package/dist/src/core/mach-error-hints.js +42 -29
  273. package/dist/src/core/mach-known-noise-filter.d.ts +15 -16
  274. package/dist/src/core/mach-known-noise-filter.js +18 -20
  275. package/dist/src/core/mach-mozconfig.d.ts +14 -0
  276. package/dist/src/core/mach-mozconfig.js +30 -0
  277. package/dist/src/core/mach-resource-shim.d.ts +43 -58
  278. package/dist/src/core/mach-resource-shim.js +50 -67
  279. package/dist/src/core/mach.d.ts +37 -41
  280. package/dist/src/core/mach.js +50 -51
  281. package/dist/src/core/marionette-port.d.ts +36 -1
  282. package/dist/src/core/marionette-port.js +96 -31
  283. package/dist/src/core/marionette-preflight.d.ts +8 -12
  284. package/dist/src/core/marionette-preflight.js +86 -52
  285. package/dist/src/core/mochitest-server-port.d.ts +42 -0
  286. package/dist/src/core/mochitest-server-port.js +119 -0
  287. package/dist/src/core/module-resolution-preflight.d.ts +9 -11
  288. package/dist/src/core/module-resolution-preflight.js +9 -11
  289. package/dist/src/core/moz-manifest-helpers.d.ts +79 -0
  290. package/dist/src/core/moz-manifest-helpers.js +163 -0
  291. package/dist/src/core/{manifest-register.d.ts → moz-manifest-register.d.ts} +1 -1
  292. package/dist/src/core/{manifest-register.js → moz-manifest-register.js} +13 -28
  293. package/dist/src/core/{manifest-rules.d.ts → moz-manifest-rules.d.ts} +10 -1
  294. package/dist/src/core/{manifest-rules.js → moz-manifest-rules.js} +31 -24
  295. package/dist/src/core/{manifest-tokenizers.d.ts → moz-manifest-tokenizers.d.ts} +3 -5
  296. package/dist/src/core/{manifest-tokenizers.js → moz-manifest-tokenizers.js} +9 -12
  297. package/dist/src/core/ownership-table.d.ts +14 -16
  298. package/dist/src/core/ownership-table.js +14 -16
  299. package/dist/src/core/parser-fallback.d.ts +15 -1
  300. package/dist/src/core/parser-fallback.js +22 -3
  301. package/dist/src/core/patch-apply-fuzz.d.ts +10 -12
  302. package/dist/src/core/patch-apply-fuzz.js +15 -18
  303. package/dist/src/core/patch-apply.d.ts +13 -34
  304. package/dist/src/core/patch-apply.js +16 -76
  305. package/dist/src/core/patch-artifact-normalize.d.ts +13 -15
  306. package/dist/src/core/patch-artifact-normalize.js +13 -15
  307. package/dist/src/core/patch-export-metadata.d.ts +12 -2
  308. package/dist/src/core/patch-export-metadata.js +13 -4
  309. package/dist/src/core/patch-export-update.d.ts +13 -1
  310. package/dist/src/core/patch-export-update.js +25 -8
  311. package/dist/src/core/patch-export.d.ts +15 -8
  312. package/dist/src/core/patch-export.js +10 -11
  313. package/dist/src/core/patch-files.d.ts +5 -6
  314. package/dist/src/core/patch-files.js +5 -6
  315. package/dist/src/core/patch-identifier-suggest.d.ts +7 -12
  316. package/dist/src/core/patch-identifier-suggest.js +14 -19
  317. package/dist/src/core/patch-lint-binary.d.ts +51 -0
  318. package/dist/src/core/patch-lint-binary.js +39 -0
  319. package/dist/src/core/{lint-cache.d.ts → patch-lint-cache.d.ts} +5 -5
  320. package/dist/src/core/{lint-cache.js → patch-lint-cache.js} +21 -23
  321. package/dist/src/core/patch-lint-checkjs.d.ts +16 -16
  322. package/dist/src/core/patch-lint-checkjs.js +41 -20
  323. package/dist/src/core/patch-lint-chrome-jsdoc.d.ts +6 -9
  324. package/dist/src/core/patch-lint-chrome-jsdoc.js +10 -14
  325. package/dist/src/core/patch-lint-creators.d.ts +8 -8
  326. package/dist/src/core/patch-lint-creators.js +8 -8
  327. package/dist/src/core/patch-lint-cross.d.ts +17 -20
  328. package/dist/src/core/patch-lint-cross.js +16 -16
  329. package/dist/src/core/patch-lint-css.d.ts +3 -5
  330. package/dist/src/core/patch-lint-css.js +17 -22
  331. package/dist/src/core/patch-lint-diff-tag.d.ts +14 -16
  332. package/dist/src/core/patch-lint-diff-tag.js +14 -16
  333. package/dist/src/core/patch-lint-jsdoc.d.ts +7 -8
  334. package/dist/src/core/patch-lint-jsdoc.js +26 -19
  335. package/dist/src/core/patch-lint-mozbuild.js +20 -20
  336. package/dist/src/core/patch-lint-observer.d.ts +7 -7
  337. package/dist/src/core/patch-lint-observer.js +7 -7
  338. package/dist/src/core/patch-lint.d.ts +38 -29
  339. package/dist/src/core/patch-lint.js +89 -121
  340. package/dist/src/core/patch-lock.d.ts +1 -1
  341. package/dist/src/core/patch-lock.js +45 -9
  342. package/dist/src/core/patch-manifest-consistency.d.ts +57 -6
  343. package/dist/src/core/patch-manifest-consistency.js +111 -40
  344. package/dist/src/core/patch-manifest-files-affected.d.ts +43 -0
  345. package/dist/src/core/patch-manifest-files-affected.js +77 -0
  346. package/dist/src/core/patch-manifest-io.js +14 -18
  347. package/dist/src/core/patch-manifest-resolve.d.ts +9 -12
  348. package/dist/src/core/patch-manifest-resolve.js +9 -12
  349. package/dist/src/core/patch-manifest-validate.d.ts +6 -1
  350. package/dist/src/core/patch-manifest-validate.js +25 -13
  351. package/dist/src/core/patch-manifest.d.ts +2 -1
  352. package/dist/src/core/patch-manifest.js +2 -1
  353. package/dist/src/core/patch-parse.d.ts +24 -13
  354. package/dist/src/core/patch-parse.js +19 -14
  355. package/dist/src/core/patch-policy.d.ts +2 -2
  356. package/dist/src/core/patch-policy.js +2 -2
  357. package/dist/src/core/patch-registration-refs.d.ts +7 -8
  358. package/dist/src/core/patch-registration-refs.js +9 -9
  359. package/dist/src/core/process-cpu-time.d.ts +12 -0
  360. package/dist/src/core/process-cpu-time.js +52 -0
  361. package/dist/src/core/rebase-session.d.ts +38 -26
  362. package/dist/src/core/rebase-session.js +55 -28
  363. package/dist/src/core/register-browser-content.js +9 -74
  364. package/dist/src/core/{register-test-manifest.js → register-browser-test.js} +7 -42
  365. package/dist/src/core/register-module.js +10 -43
  366. package/dist/src/core/register-scaffold.d.ts +8 -8
  367. package/dist/src/core/register-scaffold.js +10 -10
  368. package/dist/src/core/register-shared-css.d.ts +9 -12
  369. package/dist/src/core/register-shared-css.js +17 -87
  370. package/dist/src/core/register-xpcshell-test.d.ts +6 -7
  371. package/dist/src/core/register-xpcshell-test.js +7 -8
  372. package/dist/src/core/shared-ftl.js +12 -4
  373. package/dist/src/core/state-file.d.ts +45 -0
  374. package/dist/src/core/state-file.js +34 -0
  375. package/dist/src/core/status-binary.d.ts +18 -11
  376. package/dist/src/core/status-binary.js +19 -12
  377. package/dist/src/core/status-classify.d.ts +30 -36
  378. package/dist/src/core/status-classify.js +21 -26
  379. package/dist/src/core/test-changed-prefs.d.ts +30 -0
  380. package/dist/src/core/test-changed-prefs.js +78 -0
  381. package/dist/src/core/test-harness-crash.d.ts +25 -63
  382. package/dist/src/core/test-harness-crash.js +121 -142
  383. package/dist/src/core/test-harness-signature.d.ts +18 -0
  384. package/dist/src/core/test-harness-signature.js +13 -0
  385. package/dist/src/core/test-path-scope.d.ts +12 -15
  386. package/dist/src/core/test-path-scope.js +15 -18
  387. package/dist/src/core/test-stale-check.d.ts +17 -17
  388. package/dist/src/core/test-stale-check.js +50 -43
  389. package/dist/src/core/test-stall-triage.d.ts +51 -0
  390. package/dist/src/core/test-stall-triage.js +126 -0
  391. package/dist/src/core/token-category.js +66 -14
  392. package/dist/src/core/token-coverage.js +9 -9
  393. package/dist/src/core/token-dark-mode.d.ts +9 -11
  394. package/dist/src/core/token-dark-mode.js +29 -13
  395. package/dist/src/core/token-docs.d.ts +2 -0
  396. package/dist/src/core/token-docs.js +3 -1
  397. package/dist/src/core/token-manager.d.ts +10 -2
  398. package/dist/src/core/token-manager.js +17 -8
  399. package/dist/src/core/token-scaffold.d.ts +7 -7
  400. package/dist/src/core/token-scaffold.js +7 -7
  401. package/dist/src/core/token-variant.d.ts +4 -4
  402. package/dist/src/core/token-variant.js +28 -6
  403. package/dist/src/core/toolchain-preflight.d.ts +20 -21
  404. package/dist/src/core/toolchain-preflight.js +28 -31
  405. package/dist/src/core/tree-guard.js +12 -2
  406. package/dist/src/core/tree-store.d.ts +41 -27
  407. package/dist/src/core/tree-store.js +68 -43
  408. package/dist/src/core/typecheck-shim.d.ts +23 -24
  409. package/dist/src/core/typecheck-shim.js +23 -24
  410. package/dist/src/core/typecheck.js +15 -9
  411. package/dist/src/core/wire-destroy.d.ts +3 -3
  412. package/dist/src/core/wire-destroy.js +7 -15
  413. package/dist/src/core/wire-dom-fragment.d.ts +9 -17
  414. package/dist/src/core/wire-dom-fragment.js +11 -44
  415. package/dist/src/core/wire-init.d.ts +2 -3
  416. package/dist/src/core/wire-init.js +8 -19
  417. package/dist/src/core/wire-subscript.d.ts +2 -3
  418. package/dist/src/core/wire-subscript.js +4 -13
  419. package/dist/src/core/wire-utils.d.ts +7 -8
  420. package/dist/src/core/wire-utils.js +7 -8
  421. package/dist/src/core/xpcshell-appdir.d.ts +38 -45
  422. package/dist/src/core/xpcshell-appdir.js +38 -45
  423. package/dist/src/errors/base.d.ts +37 -16
  424. package/dist/src/errors/base.js +37 -16
  425. package/dist/src/errors/build.d.ts +13 -1
  426. package/dist/src/errors/build.js +14 -2
  427. package/dist/src/errors/codes.d.ts +16 -0
  428. package/dist/src/errors/codes.js +16 -0
  429. package/dist/src/errors/furnace.d.ts +4 -4
  430. package/dist/src/errors/git.d.ts +6 -10
  431. package/dist/src/errors/git.js +6 -10
  432. package/dist/src/errors/patch.d.ts +7 -7
  433. package/dist/src/errors/patch.js +7 -7
  434. package/dist/src/errors/rebase.d.ts +4 -4
  435. package/dist/src/errors/rebase.js +4 -4
  436. package/dist/src/index.d.ts +7 -1
  437. package/dist/src/index.js +5 -0
  438. package/dist/src/types/commands/index.d.ts +2 -2
  439. package/dist/src/types/commands/options.d.ts +111 -70
  440. package/dist/src/types/commands/patches.d.ts +24 -34
  441. package/dist/src/types/commands/project.d.ts +5 -34
  442. package/dist/src/types/config.d.ts +25 -23
  443. package/dist/src/types/config.js +6 -1
  444. package/dist/src/types/furnace.d.ts +37 -13
  445. package/dist/src/types/furnace.js +19 -1
  446. package/dist/src/types/index.d.ts +3 -3
  447. package/dist/src/utils/build-info.d.ts +7 -8
  448. package/dist/src/utils/build-info.js +12 -11
  449. package/dist/src/utils/errors.d.ts +6 -8
  450. package/dist/src/utils/errors.js +6 -8
  451. package/dist/src/utils/logger.d.ts +27 -10
  452. package/dist/src/utils/logger.js +28 -16
  453. package/dist/src/utils/machine-output.d.ts +12 -0
  454. package/dist/src/utils/machine-output.js +33 -0
  455. package/dist/src/utils/options.d.ts +31 -14
  456. package/dist/src/utils/options.js +86 -30
  457. package/dist/src/utils/paths.d.ts +10 -8
  458. package/dist/src/utils/paths.js +10 -8
  459. package/dist/src/utils/platform.d.ts +0 -25
  460. package/dist/src/utils/platform.js +1 -42
  461. package/dist/src/utils/process-group.d.ts +9 -4
  462. package/dist/src/utils/process-group.js +19 -5
  463. package/dist/src/utils/process.d.ts +42 -7
  464. package/dist/src/utils/process.js +44 -17
  465. package/dist/src/utils/ps-duration.d.ts +31 -0
  466. package/dist/src/utils/ps-duration.js +70 -0
  467. package/dist/src/utils/regex.d.ts +9 -10
  468. package/dist/src/utils/regex.js +9 -10
  469. package/dist/src/utils/validation.d.ts +58 -28
  470. package/dist/src/utils/validation.js +60 -33
  471. package/package.json +3 -3
  472. package/dist/src/commands/doctor/post-rebase-audit.d.ts +0 -2
  473. package/dist/src/commands/doctor-furnace-manifest-sync.d.ts +0 -18
  474. package/dist/src/core/manifest-helpers.d.ts +0 -38
  475. package/dist/src/core/manifest-helpers.js +0 -120
  476. /package/dist/src/core/{lint-projection.d.ts → patch-lint-projection.d.ts} +0 -0
  477. /package/dist/src/core/{lint-projection.js → patch-lint-projection.js} +0 -0
  478. /package/dist/src/core/{register-test-manifest.d.ts → register-browser-test.d.ts} +0 -0
@@ -6,17 +6,20 @@ import { getCurrentBranch, getHead, isGitRepository, isMissingHeadError } from '
6
6
  import { ensureGit } from '../core/git-base.js';
7
7
  import { ensureMach, ensurePython } from '../core/mach.js';
8
8
  import { countPatches } from '../core/patch-apply.js';
9
- import { rebuildPatchesManifest, validatePatchesManifestConsistency, validatePatchIntegrity, } from '../core/patch-manifest.js';
9
+ import { validatePatchIntegrity } from '../core/patch-manifest.js';
10
+ import { InvalidArgumentError } from '../errors/base.js';
10
11
  import { ExitCode } from '../errors/codes.js';
11
12
  import { toError } from '../utils/errors.js';
12
13
  import { pathExists } from '../utils/fs.js';
13
14
  import { error, info, intro, outro, success, warn } from '../utils/logger.js';
15
+ import { addWaitLockOption } from '../utils/options.js';
14
16
  import { findExecutable } from '../utils/process.js';
15
- import { POST_REBASE_AUDIT_CHECK } from './doctor/post-rebase-audit.js';
16
17
  import { failure, ok, resolveDoctorSeverity, warning } from './doctor-check-core.js';
17
18
  import { EXTERNAL_TOOLCHAIN_DOCTOR_CHECK } from './doctor-external-toolchains.js';
18
19
  import { FURNACE_DOCTOR_CHECKS } from './doctor-furnace.js';
19
20
  import { ORPHANED_HARNESS_DOCTOR_CHECK } from './doctor-orphaned-harness.js';
21
+ import { PATCH_MANIFEST_CONSISTENCY_CHECK } from './doctor-patch-manifest.js';
22
+ import { POST_REBASE_AUDIT_CHECK } from './doctor-post-rebase-audit.js';
20
23
  import { inspectEngineWorkingTree } from './doctor-working-tree.js';
21
24
  import { collectPatchQueueHealth } from './verify.js';
22
25
  /**
@@ -236,31 +239,21 @@ const DOCTOR_CHECKS = [
236
239
  fix: 'Firefox source may be corrupted. Re-download with "fireforge download --force"',
237
240
  },
238
241
  {
239
- // `fireforge watch` has an undeclared hard dependency on watchman —
240
- // neither `bootstrap` nor `doctor` used to surface it, so operators
241
- // got through setup → download → build → and only discovered the gap
242
- // when they tried to start watch mode. A warning-severity doctor row
243
- // is the right shape: most projects never run watch, so a missing
244
- // watchman should not fail `doctor` outright, but the information
245
- // needs to be visible ahead of time rather than at the watch-mode
246
- // failure site.
242
+ // `fireforge watch` has an undeclared hard dependency on watchman, and
243
+ // without a doctor row operators only discover the gap when they try to
244
+ // start watch mode. Warning severity is the right shape: most projects
245
+ // never run watch, so a missing watchman should not fail `doctor`
246
+ // outright, but the information needs to be visible ahead of time.
247
247
  name: 'Watchman available',
248
248
  run: async () => {
249
249
  // Resolve the absolute path so the OK row names what doctor actually
250
- // found. The 2026-04-25 eval flagged a confusing case where the
251
- // operator's interactive shell returned no result for `which
252
- // watchman` but doctor still printed "OK" — the cause was a
253
- // PATH-export discrepancy between the shell and the spawned
254
- // subprocess, and surfacing the resolved path makes the discrepancy
255
- // visible without users having to re-run with a verbose flag.
250
+ // found. A PATH-export discrepancy between the operator's interactive
251
+ // shell and the spawned subprocess otherwise reads as doctor printing
252
+ // "OK" for a binary `which watchman` cannot find; surfacing the
253
+ // resolved path makes the discrepancy visible without a verbose flag.
256
254
  const path = await findExecutable('watchman');
257
255
  if (path) {
258
- return {
259
- name: 'Watchman available',
260
- passed: true,
261
- severity: 'ok',
262
- message: `OK (${path})`,
263
- };
256
+ return ok('Watchman available', `OK (${path})`);
264
257
  }
265
258
  return warning('Watchman available', 'watchman is not installed or not on PATH. "fireforge watch" requires it.', 'Install watchman (brew install watchman / dnf install watchman / https://facebook.github.io/watchman/), then re-run doctor.');
266
259
  },
@@ -270,12 +263,7 @@ const DOCTOR_CHECKS = [
270
263
  name: 'Patches directory exists',
271
264
  run: async (ctx) => {
272
265
  const patchesExist = await pathExists(ctx.paths.patches);
273
- return {
274
- name: 'Patches directory exists',
275
- passed: true,
276
- severity: 'ok',
277
- message: patchesExist ? 'OK' : 'No patches/ directory (optional)',
278
- };
266
+ return ok('Patches directory exists', patchesExist ? 'OK' : 'No patches/ directory (optional)');
279
267
  },
280
268
  },
281
269
  {
@@ -285,70 +273,10 @@ const DOCTOR_CHECKS = [
285
273
  return [];
286
274
  }
287
275
  const patchCount = await countPatches(ctx.paths.patches);
288
- return {
289
- name: 'Patches found',
290
- passed: true,
291
- severity: 'ok',
292
- message: `${patchCount} patch${patchCount === 1 ? '' : 'es'} found`,
293
- };
294
- },
295
- },
296
- {
297
- name: 'Patch manifest consistency',
298
- dependsOn: ['fireforge.json is valid'],
299
- run: async (ctx) => {
300
- if (!(await pathExists(ctx.paths.patches))) {
301
- return [];
302
- }
303
- const manifestConsistencyIssues = await validatePatchesManifestConsistency(ctx.paths.patches);
304
- if (manifestConsistencyIssues.length === 0) {
305
- return ok('Patch manifest consistency');
306
- }
307
- if (!ctx.options.repairPatchesManifest) {
308
- return failure('Patch manifest consistency', manifestConsistencyIssues.map((issue) => issue.message).join(' '), 'Run "fireforge doctor --repair-patches-manifest" to rebuild patches.json from patch files.');
309
- }
310
- // Repair stamps sourceEsrVersion into every recovered entry. If the
311
- // earlier "fireforge.json is valid" check failed, ctx.config is
312
- // undefined and we must refuse rather than fabricate a fallback —
313
- // persisting 'unknown' into manifest metadata is hard to reverse
314
- // and would mislead every later command that reads it.
315
- if (!ctx.config) {
316
- return failure('Patch manifest consistency', 'Cannot repair patches.json: fireforge.json could not be loaded, so the Firefox version to stamp into recovered manifest entries is unknown.', 'Fix the fireforge.json errors reported above and re-run "fireforge doctor --repair-patches-manifest".');
317
- }
318
- try {
319
- const repaired = await rebuildPatchesManifest(ctx.paths.patches, ctx.config.firefox.version);
320
- // 2026-04-21 eval (Finding #17): the repair path silently
321
- // overwrote useful human-written descriptions on recovered
322
- // entries, leaving the queue less trustworthy as an audit
323
- // trail. The rebuilder now returns the list of filenames
324
- // whose metadata was entirely invented, and we name them
325
- // explicitly here so the operator knows exactly which
326
- // patches to review. Names that DID have a preserved entry
327
- // (only `filesAffected` / ordering drifted) are not flagged.
328
- if (repaired.recoveredFilenames.length > 0) {
329
- for (const filename of repaired.recoveredFilenames) {
330
- // 2026-04-24 eval Finding 6: the repair path used to tell the
331
- // operator to hand-edit patches.json, which contradicts the
332
- // README + downstream docs that treat the manifest as
333
- // FireForge-owned. Point at the existing `re-export` /
334
- // `export` workflow instead so the fix stays inside the tool:
335
- // re-exporting the same files with an explicit `--description`
336
- // overwrites the recovered entry with operator-supplied
337
- // metadata and supersedes the mtime-based createdAt stamp.
338
- warn(`Recovered manifest entry for ${filename} with generic description and mtime-based createdAt. ` +
339
- 'Re-export the affected files with `fireforge re-export <filename> --description "<your description>"` ' +
340
- '(or `fireforge export <paths...> --name <name> --category <category> --description "<your description>"`) ' +
341
- 'to overwrite the reconstructed metadata, or accept the generic description if the original text is not recoverable. ' +
342
- 'Avoid hand-editing patches.json — FireForge owns that file and will regenerate it on the next manifest consistency pass.');
343
- }
344
- }
345
- return warning('Patch manifest consistency', `Rebuilt patches.json from ${repaired.manifest.patches.length} patch${repaired.manifest.patches.length === 1 ? '' : 'es'}${repaired.recoveredFilenames.length > 0 ? ` (${repaired.recoveredFilenames.length} with reconstructed metadata — see warnings above)` : ''}. Review recovered metadata before release.`);
346
- }
347
- catch (err) {
348
- return failure('Patch manifest consistency', toError(err).message, 'Repair failed. Fix the underlying patch metadata issue and retry the doctor command.');
349
- }
276
+ return ok('Patches found', `${patchCount} patch${patchCount === 1 ? '' : 'es'} found`);
350
277
  },
351
278
  },
279
+ PATCH_MANIFEST_CONSISTENCY_CHECK,
352
280
  {
353
281
  name: 'Patch integrity',
354
282
  skipIf: (ctx) => !ctx.engineExists,
@@ -400,7 +328,7 @@ export const DOCTOR_CHECK_ORDER = DOCTOR_CHECKS.map((check) => check.name);
400
328
  * @param checks - The check results to display
401
329
  * @returns The exit code reflecting the overall result
402
330
  */
403
- export function reportDoctorResults(checks) {
331
+ export function reportDoctorResults(checks, mutations = []) {
404
332
  info('');
405
333
  let passedCount = 0;
406
334
  let warningCount = 0;
@@ -427,6 +355,16 @@ export function reportDoctorResults(checks) {
427
355
  }
428
356
  }
429
357
  info('');
358
+ // Printed in every branch, including the failing one: a repair that wrote
359
+ // and then met an unrelated failing check must not be reported as a run
360
+ // where nothing happened.
361
+ if (mutations.length > 0) {
362
+ warn('Repairs applied this run:');
363
+ for (const mutation of mutations) {
364
+ warn(` ${mutation}`);
365
+ }
366
+ info('');
367
+ }
430
368
  if (failedCount === 0 && warningCount === 0) {
431
369
  outro(`All ${passedCount} checks passed!`);
432
370
  }
@@ -439,12 +377,38 @@ export function reportDoctorResults(checks) {
439
377
  }
440
378
  return ExitCode.SUCCESS;
441
379
  }
380
+ /**
381
+ * Rejects repair-flag combinations that cannot mean what they say.
382
+ *
383
+ * The two manifest repairs are different operations on the same file, and
384
+ * `--dry-run` with nothing to project is a flag that silently does nothing —
385
+ * the shape most likely to be mistaken for a preview that reported "no
386
+ * changes".
387
+ */
388
+ function assertRepairOptions(options) {
389
+ if (options.repairPatchesManifest === true && options.repairFilesAffected === true) {
390
+ throw new InvalidArgumentError('--repair-patches-manifest and --repair-files-affected are mutually exclusive. ' +
391
+ 'Use --repair-files-affected for filesAffected drift; use --repair-patches-manifest ' +
392
+ 'when rows are missing, untracked or duplicated.', '--repair-files-affected');
393
+ }
394
+ if (options.allowMetadataLoss === true && options.repairPatchesManifest !== true) {
395
+ throw new InvalidArgumentError('--allow-metadata-loss only applies to --repair-patches-manifest.', '--allow-metadata-loss');
396
+ }
397
+ if (options.dryRun === true &&
398
+ options.repairPatchesManifest !== true &&
399
+ options.repairFilesAffected !== true) {
400
+ throw new InvalidArgumentError('--dry-run needs a manifest repair to project. Pass it with ' +
401
+ '--repair-patches-manifest or --repair-files-affected. ' +
402
+ '(--repair-furnace has no dry-run: it reconciles engine state, not a manifest.)', '--dry-run');
403
+ }
404
+ }
442
405
  /**
443
406
  * Runs the doctor command to diagnose issues.
444
407
  * @param projectRoot - Root directory of the project
445
408
  */
446
409
  export async function doctorCommand(projectRoot, options = {}) {
447
410
  intro('FireForge Doctor');
411
+ assertRepairOptions(options);
448
412
  const paths = getProjectPaths(projectRoot);
449
413
  const state = await loadState(projectRoot);
450
414
  const engineExists = await pathExists(paths.engine);
@@ -458,24 +422,28 @@ export async function doctorCommand(projectRoot, options = {}) {
458
422
  config: undefined,
459
423
  furnaceConfigExists: furnaceConfigExistsFlag,
460
424
  furnaceConfig: undefined,
425
+ mutations: [],
461
426
  };
462
427
  const checks = [];
463
428
  for (const definition of DOCTOR_CHECKS) {
464
429
  checks.push(...(await executeCheck(definition, ctx)));
465
430
  }
466
- const exitCode = reportDoctorResults(checks);
431
+ const exitCode = reportDoctorResults(checks, ctx.mutations);
467
432
  return { checks, exitCode };
468
433
  }
469
434
  /** Registers the doctor command on the CLI program. */
470
435
  export function registerDoctor(program, { getProjectRoot, withErrorHandling }) {
471
- program
436
+ const command = program
472
437
  .command('doctor')
473
438
  .description('Diagnose project issues')
474
- .option('--repair-patches-manifest', 'Rebuild patches/patches.json from the current patch files before reporting results')
439
+ .option('--repair-patches-manifest', 'Rebuild patches/patches.json from the current patch files before reporting results. Only filesAffected and order are recomputed; every other field on an existing entry is preserved')
440
+ .option('--repair-files-affected', 'Repair only the filesAffected lists that disagree with their patch body, leaving every other manifest field untouched')
441
+ .option('--allow-metadata-loss', 'With --repair-patches-manifest, rebuild even when patches.json cannot be parsed — every descriptive field on every entry is then reinvented')
442
+ .option('--dry-run', 'Show what a requested repair would change without writing anything')
475
443
  .option('--repair-furnace', 'Reconcile furnace state: clear stale furnace-state.json entries, re-run furnace apply to fix engine drift, and clear the pending-repair marker set by a failed preview teardown')
476
444
  .option('--clear-resolution', 'Clear stale pendingResolution state after the patch queue health check reports no errors')
477
- .option('--post-rebase-audit', 'Check common registration surfaces after a Firefox source rebase')
478
- .action(withErrorHandling(async (options) => {
445
+ .option('--post-rebase-audit', 'Check common registration surfaces after a Firefox source rebase');
446
+ addWaitLockOption(command).action(withErrorHandling(async (options) => {
479
447
  const result = await doctorCommand(getProjectRoot(), options);
480
448
  if (result.exitCode !== 0) {
481
449
  process.exitCode = result.exitCode;
@@ -76,13 +76,12 @@ async function cleanPatchTouchedFiles(engineDir, patchesDir, preExistingDirty) {
76
76
  }
77
77
  /**
78
78
  * Prints a one-line nudge pointing at `fireforge import` when the project
79
- * carries a non-empty patch queue but the just-downloaded engine has not
80
- * yet had any patches applied. The post-download spinner closes with
79
+ * carries a non-empty patch queue but the just-downloaded engine has not yet
80
+ * had any patches applied. The post-download spinner closes with
81
81
  * "Patch-touched files already match baseline" because a fresh tree IS at
82
- * baseline, but the 2026-04-25 eval saw operators read that as "patches
83
- * are restored" and skip the import step. The note is suppressed when
84
- * patches/ is missing or the manifest is empty so unconfigured projects
85
- * stay quiet.
82
+ * baseline, which reads as "patches are restored" and invites skipping the
83
+ * import step. Suppressed when patches/ is missing or the manifest is empty
84
+ * so unconfigured projects stay quiet.
86
85
  */
87
86
  async function noteUnappliedPatches(patchesDir) {
88
87
  if (!(await pathExists(patchesDir)))
@@ -97,12 +96,8 @@ async function noteUnappliedPatches(patchesDir) {
97
96
  * Stops `restoreSpinner` with a message that reflects what actually
98
97
  * happened. Three branches: empty queue → explicit no-op; queue present but
99
98
  * nothing dirty → "already clean"; queue with dirty files → the usual
100
- * "Patch-touched files restored" success line.
101
- *
102
- * Before 0.16.0 the spinner always closed with "Patch-touched files
103
- * restored", so a fresh project with zero patches saw a claim of restore
104
- * work that had not happened — misleading and easy to mistake for a
105
- * silent retry.
99
+ * "Patch-touched files restored" success line. Always closing with the third
100
+ * claims restore work that did not happen on a project with zero patches.
106
101
  */
107
102
  function closeRestoreSpinner(restoreSpinner, result) {
108
103
  if (!result.hadQueue) {
@@ -192,12 +187,11 @@ async function downloadAndExtractFirefox(args) {
192
187
  }
193
188
  }
194
189
  /**
195
- * Prints the major-version-hop toolchain nudge when this download moved
196
- * the engine across a Firefox MAJOR version (152.0b7 → 153.0b8 drill:
197
- * the first post-hop build died in `mach configure` on a moved cbindgen
198
- * minimum, and nothing in the download output suggested re-running
199
- * `fireforge bootstrap`). Quiet on first downloads and same-major
200
- * re-downloads.
190
+ * Prints the major-version-hop toolchain nudge when this download moved the
191
+ * engine across a Firefox MAJOR version — the first post-hop build otherwise
192
+ * dies in `mach configure` on a moved cbindgen minimum with nothing in the
193
+ * download output suggesting `fireforge bootstrap`. Quiet on first downloads
194
+ * and same-major re-downloads.
201
195
  */
202
196
  function noteMajorVersionHop(previousVersion, version) {
203
197
  const hopNotice = formatMajorVersionHopNotice(previousVersion, version);
@@ -207,9 +201,9 @@ function noteMajorVersionHop(previousVersion, version) {
207
201
  }
208
202
  async function initializeDownloadedEngine(args) {
209
203
  const { projectRoot, patchesDir, version, previousVersion, engineDir, replacementActivated, backupEngineDir, } = args;
210
- // Finding #17: the git indexing phase of `download` can block for
211
- // minutes on a ~600 MB Firefox tree. Emit a one-line heads-up banner
212
- // before the spinner starts so CI logs show the expected duration.
204
+ // The git indexing phase of `download` can block for minutes on a ~600 MB
205
+ // Firefox tree. Emit a one-line heads-up banner before the spinner starts
206
+ // so CI logs show the expected duration.
213
207
  try {
214
208
  info('Indexing downloaded source into git (one-time; typically 3–5 minutes on a ~600 MB Firefox tree)...');
215
209
  info('Git phase: initializing/resetting source repository metadata.');
@@ -280,11 +274,11 @@ export async function downloadCommand(projectRoot, options) {
280
274
  info(`Firefox version: ${version}`);
281
275
  await checkDiskSpace(projectRoot, 5 * 1024 * 1024 * 1024, warn);
282
276
  // A legitimate holder of this lock runs for 10+ minutes (download +
283
- // extract + git indexing). The previous default 30 s timeout failed the
284
- // second invocation with "remove the lock directory if it is stale" advice
285
- // while the lock was actively held — dangerous guidance mid-extraction.
286
- // Wait generously instead: a dead holder is reaped within seconds by the
287
- // PID-based stale probe, so a long timeout only ever waits on real work.
277
+ // extract + git indexing). A 30 s timeout fails the second invocation with
278
+ // "remove the lock directory if it is stale" advice while the lock is
279
+ // actively held — dangerous guidance mid-extraction. Wait generously
280
+ // instead: a dead holder is reaped within seconds by the PID-based stale
281
+ // probe, so a long timeout only ever waits on real work.
288
282
  const downloadLockOptions = {
289
283
  timeoutMs: 30 * 60_000,
290
284
  onTimeoutMessage: 'Timed out waiting for another FireForge download to finish. ' +
@@ -329,11 +323,9 @@ export async function downloadCommand(projectRoot, options) {
329
323
  // The non-TTY spinner fallback in `src/utils/logger.ts`
330
324
  // already calls `p.log.step(msg)` from `message()`, so
331
325
  // forwarding the progress message is the single authority
332
- // in both TTY and non-TTY modes. Before 0.16.0 this
333
- // callback also invoked `step(message)` explicitly when
334
- // stdio was not a TTY, which printed the same step line
335
- // twice in CI logs (once from the fallback, once from
336
- // the explicit call).
326
+ // in both TTY and non-TTY modes. Calling `step(message)`
327
+ // explicitly here as well prints the same line twice in
328
+ // CI logs.
337
329
  onProgress: (message) => {
338
330
  resumeSpinner.message(message);
339
331
  },
@@ -359,17 +351,14 @@ export async function downloadCommand(projectRoot, options) {
359
351
  }
360
352
  catch (error) {
361
353
  resumeSpinner.error('Resume failed');
362
- // Preserve the underlying cause so the user sees *why* the
363
- // resume failed (timeout, permission denied, corrupted object,
364
- // disk full, …) instead of only the generic "partial engine
365
- // exists" story. Verbose mode prints the stack for deeper
366
- // triage.
367
- const cause = toError(error);
368
- verbose(`Resume failure detail: ${cause.message}`);
369
- if (cause.stack) {
370
- verbose(cause.stack);
371
- }
372
- throw new PartialEngineExistsError(paths.engine, cause);
354
+ // Preserve the underlying cause so the operator sees *why*
355
+ // the resume failed (timeout, permission denied, corrupted
356
+ // object, disk full) instead of only the generic "partial
357
+ // engine exists" story. The cause reaches them through the
358
+ // CLI boundary's chain walk under --verbose (src/cli.ts),
359
+ // which is what makes PartialEngineExistsError's "re-run
360
+ // with --verbose" promise true.
361
+ throw new PartialEngineExistsError(paths.engine, toError(error));
373
362
  }
374
363
  }
375
364
  // Re-throw unexpected git errors (corrupted objects, permission
@@ -377,12 +366,7 @@ export async function downloadCommand(projectRoot, options) {
377
366
  // both narratives: "we detected a partial engine and attempted
378
367
  // resume" AND the underlying git failure. Without the wrap the
379
368
  // raw git error loses the context that resume was in flight.
380
- const cause = toError(error);
381
- verbose(`Partial-engine probe failed with unexpected error: ${cause.message}`);
382
- if (cause.stack) {
383
- verbose(cause.stack);
384
- }
385
- throw new PartialEngineExistsError(paths.engine, cause);
369
+ throw new PartialEngineExistsError(paths.engine, toError(error));
386
370
  }
387
371
  }
388
372
  throw new EngineExistsError(paths.engine);
@@ -2,8 +2,10 @@
2
2
  import { Command } from 'commander';
3
3
  import { isBrandingManagedPath } from '../core/branding.js';
4
4
  import { getProjectPaths, loadConfig } from '../core/config.js';
5
+ import { stdioIsInteractive } from '../core/destructive.js';
6
+ import { assertEngineGitReady } from '../core/engine-precondition.js';
5
7
  import { collectFurnaceManagedPrefixes, furnaceConfigExists, loadFurnaceConfig, } from '../core/furnace-config.js';
6
- import { hasChanges, isGitRepository } from '../core/git.js';
8
+ import { hasChanges } from '../core/git.js';
7
9
  import { getAllDiff, getDiffForFilesAgainstHead } from '../core/git-diff.js';
8
10
  import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
9
11
  import { extractAffectedFiles } from '../core/patch-apply.js';
@@ -14,7 +16,7 @@ import { buildPatchSourceMetadata } from '../core/patch-source-metadata.js';
14
16
  import { GeneralError } from '../errors/base.js';
15
17
  import { ensureDir, pathExists } from '../utils/fs.js';
16
18
  import { info, intro, outro, spinner } from '../utils/logger.js';
17
- import { pickDefined } from '../utils/options.js';
19
+ import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../utils/options.js';
18
20
  import { renderDryRunPreview } from './export-flow.js';
19
21
  import { autoFixLicenseHeaders, promptExportPatchMetadata, runPatchLint, runSupersedeAndOverlapGates, } from './export-shared.js';
20
22
  async function checkBrandingManagedFiles(paths, config) {
@@ -46,9 +48,7 @@ async function resolveFurnaceExclusionPolicy(paths, projectRoot, excludeFurnace)
46
48
  // Expand collapsed `?? dir/` entries before matching against Furnace
47
49
  // prefixes — otherwise a Furnace-introduced directory slips past the
48
50
  // filter and later lands in the non-Furnace path list that feeds the
49
- // aggregate diff, where `getDiffForFilesAgainstHead` crashes with
50
- // EISDIR (eval finding: export-all unusable on a fresh project with
51
- // Furnace scaffolding).
51
+ // aggregate diff, where `getDiffForFilesAgainstHead` crashes with EISDIR.
52
52
  const rawStatus = await getWorkingTreeStatus(paths.engine);
53
53
  const changedFiles = await expandUntrackedDirectoryEntries(paths.engine, rawStatus);
54
54
  const furnaceManagedFiles = changedFiles
@@ -66,20 +66,19 @@ async function resolveFurnaceExclusionPolicy(paths, projectRoot, excludeFurnace)
66
66
  }
67
67
  /**
68
68
  * Refuses the export when the resulting patch would register furnace
69
- * component source files it does not itself carry. 2026-04-24 eval
70
- * Finding 1: operators running `export-all --exclude-furnace` after
71
- * `furnace create --localized --with-tests` ended up with patches that
72
- * added `toolkit/content/widgets/moz-qa-panel/*` via jar.mn /
73
- * customElements.js / locale jar.mn but excluded the component source
74
- * files themselves. The resulting patch queue was structurally broken
75
- * and `fireforge verify` stayed silent. We now detect the condition
76
- * pre-write and ask the operator to either include the component
77
- * sources (skip `--exclude-furnace`) or revert the furnace changes
78
- * before exporting.
69
+ * component source files it does not itself carry.
70
+ *
71
+ * `export-all --exclude-furnace` after `furnace create --localized
72
+ * --with-tests` otherwise produces a patch that adds
73
+ * `toolkit/content/widgets/<tag>/*` via jar.mn / customElements.js / locale
74
+ * jar.mn while excluding the component source files themselves — a
75
+ * structurally broken queue that `fireforge verify` stays silent about. The
76
+ * operator is asked to either include the component sources (skip
77
+ * `--exclude-furnace`) or revert the furnace changes before exporting.
79
78
  *
80
79
  * The check runs against the synthesised patch body before
81
- * `commitExportedPatch` writes anything, so no broken patch is left on
82
- * disk when the refusal fires.
80
+ * `commitExportedPatch` writes anything, so no broken patch is left on disk
81
+ * when the refusal fires.
83
82
  */
84
83
  async function checkDanglingFurnaceRegistrations(projectRoot, diff, furnaceExcluded) {
85
84
  if (furnaceExcluded.size === 0)
@@ -171,14 +170,7 @@ export async function exportAllCommand(projectRoot, options = {}) {
171
170
  const isDryRun = options.dryRun === true;
172
171
  intro(isDryRun ? 'FireForge Export All (dry run)' : 'FireForge Export All');
173
172
  const paths = getProjectPaths(projectRoot);
174
- // Check if engine exists
175
- if (!(await pathExists(paths.engine))) {
176
- throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
177
- }
178
- // Check if it's a git repository
179
- if (!(await isGitRepository(paths.engine))) {
180
- throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
181
- }
173
+ await assertEngineGitReady(paths.engine);
182
174
  // Check for changes
183
175
  if (!(await hasChanges(paths.engine))) {
184
176
  info('No changes to export');
@@ -228,13 +220,13 @@ export async function exportAllCommand(projectRoot, options = {}) {
228
220
  // the aggregate would newly create, so it runs here instead of alongside
229
221
  // the branding / furnace guards that operate on the raw status list.
230
222
  await checkDuplicateNewFileCreations(paths, diff);
231
- // Dangling-furnace-registration preflight (Finding 1). Runs after the
232
- // diff is assembled so we can inspect the exact hunks the operator is
233
- // about to land; runs BEFORE any write so a refusal leaves the
234
- // patches directory untouched.
223
+ // Dangling-furnace-registration preflight. Runs after the diff is
224
+ // assembled so it can inspect the exact hunks the operator is about to
225
+ // land, and BEFORE any write so a refusal leaves the patches directory
226
+ // untouched.
235
227
  await checkDanglingFurnaceRegistrations(projectRoot, diff, furnaceExcluded);
236
228
  // Check for non-interactive mode
237
- const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
229
+ const isInteractive = stdioIsInteractive();
238
230
  // Auto-fix missing license headers on new files (interactive only;
239
231
  // report-only under --dry-run so the preview never mutates engine/)
240
232
  const headersAdded = await autoFixLicenseHeaders(paths.engine, diff, config, isInteractive, isDryRun);
@@ -308,6 +300,10 @@ export async function exportAllCommand(projectRoot, options = {}) {
308
300
  config,
309
301
  policyCommand: 'export-all',
310
302
  forceUnsafe: options.forceUnsafe === true,
303
+ lockOptions: {
304
+ waitLockSeconds: resolveWaitLockSeconds(options.waitLock),
305
+ command: 'export-all',
306
+ },
311
307
  });
312
308
  for (const oldPatch of superseded) {
313
309
  info(`Superseded: ${oldPatch.filename}`);
@@ -326,7 +322,7 @@ export async function exportAllCommand(projectRoot, options = {}) {
326
322
  }
327
323
  /** Registers the export-all command on the CLI program. */
328
324
  export function registerExportAll(program, { getProjectRoot, withErrorHandling }) {
329
- program
325
+ const command = program
330
326
  .command('export-all')
331
327
  .description('Export all changes as a patch')
332
328
  .option('--name <name>', 'Name for the patch')
@@ -337,8 +333,8 @@ export function registerExportAll(program, { getProjectRoot, withErrorHandling }
337
333
  .option('--exclude-furnace', 'Export the non-Furnace subset of the aggregate diff instead of refusing when Furnace-managed files are modified. Furnace-managed files are still deployed by "fireforge furnace apply"; this flag only changes whether export-all aborts or filters in their presence.')
338
334
  .option('--allow-overlap', 'Acknowledge cross-patch ownership overlap with non-superseded patches (the resulting queue fails verify). Does not bypass the new-file creation guard — two patches creating the same path is structurally unrecoverable, so that case still refuses regardless of this flag.')
339
335
  .option('--dry-run', 'Print the export-all plan (filename, metadata, files affected, supersede preview) without writing anything to patches/. Lint still runs so the operator sees the same lint output a real run would produce.')
340
- .option('--force-unsafe', 'Bypass force-mode patchPolicy refusals')
341
- .action(withErrorHandling(async (options) => {
336
+ .option('--force-unsafe', 'Bypass force-mode patchPolicy refusals');
337
+ addWaitLockOption(command).action(withErrorHandling(async (options) => {
342
338
  const { category, ...rest } = options;
343
339
  await exportAllCommand(getProjectRoot(), {
344
340
  ...pickDefined(rest),
@@ -195,12 +195,12 @@ export async function projectPlacementForLint(patchesDir, plan, diff, config) {
195
195
  return entry;
196
196
  return { ...entry, filename: rename.newFilename, order: rename.newOrder };
197
197
  });
198
- // Synthetic entry for the pending patch, populated with both its
199
- // new-file content AND its added-line content for files it modifies
200
- // so the forward-import rule can inspect imports the patch *itself*
201
- // authors — whether they live in a brand-new file or are added to an
202
- // existing file. Leaving either map empty lets a patch land before
203
- // one of its own dependencies and still pass the gate.
198
+ // Synthetic entry for the pending patch, populated with both its new-file
199
+ // content AND its added-line content for files it modifies, so the
200
+ // forward-import rule can inspect imports the patch *itself* authors —
201
+ // whether they live in a brand-new file or are added to an existing one.
202
+ // Leaving either map empty lets a patch land before one of its own
203
+ // dependencies and still pass the gate.
204
204
  projectedEntries.push({
205
205
  filename: plan.newFilename,
206
206
  order: plan.insertionOrder,
@@ -1,16 +1,17 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /**
3
3
  * Attribution of projected placement lint errors, split out of
4
- * export-flow.ts for the line budget. A placement projection lints one
5
- * merged queue — the renumbered existing patches plus a synthetic entry
6
- * for the pending patch — so its errors used to render as one flat list
7
- * that read as though the NEW patch caused every one of them. A
8
- * renumbering that re-evaluates pre-existing staged relationships (455
9
- * importing a module created by 456, legal only at their current
10
- * ordinals) then sent the operator auditing a perfectly fine export.
4
+ * export-flow.ts for the line budget.
11
5
  *
12
- * Consumed by export-flow.ts; no top-level registrar is exported and none
13
- * is wanted.
6
+ * A placement projection lints one merged queue — the renumbered existing
7
+ * patches plus a synthetic entry for the pending patch — so a flat error
8
+ * list reads as though the NEW patch caused every one of them. A renumbering
9
+ * that re-evaluates pre-existing staged relationships (patch 455 importing a
10
+ * module created by 456, legal only at their current ordinals) then sends
11
+ * the operator auditing a perfectly fine export.
12
+ *
13
+ * Consumed by export-flow.ts; no top-level registrar is exported and none is
14
+ * wanted.
14
15
  */
15
16
  import { formatPatchLintIssue } from '../core/patch-lint.js';
16
17
  /**
@@ -70,10 +70,10 @@ export async function gatePlacementPlan(args) {
70
70
  const summary = placementSummary(placementPlan);
71
71
  const renameCount = placementPlan.renameMap.size;
72
72
  // Route through confirmDestructive when the operation is destructive
73
- // enough to warrant a prompt (more than one rename) OR when the user
74
- // asked for a dry-run. The dry-run branch must always print the
75
- // placement summary — previously, single-rename/no-rename dry-runs
76
- // exited silently with no filename or projected layout.
73
+ // enough to warrant a prompt (more than one rename) OR when the user asked
74
+ // for a dry-run. The dry-run branch must always print the placement
75
+ // summary; otherwise single-rename and no-rename dry-runs exit silently
76
+ // with no filename or projected layout.
77
77
  if (renameCount > 1 || isDryRun) {
78
78
  s.stop();
79
79
  const decision = await confirmDestructive({
@@ -89,7 +89,7 @@ export async function gatePlacementPlan(args) {
89
89
  outro('Dry run complete — no changes made');
90
90
  return 'stop';
91
91
  }
92
- if (decision === 'cancelled') {
92
+ if (decision === 'declined') {
93
93
  outro('Export cancelled');
94
94
  return 'stop';
95
95
  }