@hominis/fireforge 0.43.0 → 0.44.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 (470) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +10 -1
  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 +4 -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-post-rebase-audit.d.ts +2 -0
  22. package/dist/src/commands/{doctor/post-rebase-audit.js → doctor-post-rebase-audit.js} +3 -3
  23. package/dist/src/commands/doctor-working-tree.d.ts +10 -12
  24. package/dist/src/commands/doctor-working-tree.js +11 -18
  25. package/dist/src/commands/doctor.js +35 -53
  26. package/dist/src/commands/download.js +32 -48
  27. package/dist/src/commands/export-all.js +29 -33
  28. package/dist/src/commands/export-flow.js +6 -6
  29. package/dist/src/commands/export-placement-conflicts.js +10 -9
  30. package/dist/src/commands/export-placement-gate.js +5 -5
  31. package/dist/src/commands/export-shared.d.ts +30 -33
  32. package/dist/src/commands/export-shared.js +45 -43
  33. package/dist/src/commands/export.js +21 -27
  34. package/dist/src/commands/furnace/apply.js +14 -23
  35. package/dist/src/commands/furnace/chrome-doc-remove.js +14 -21
  36. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +23 -23
  37. package/dist/src/commands/furnace/chrome-doc-templates.js +23 -23
  38. package/dist/src/commands/furnace/chrome-doc-tests.js +26 -29
  39. package/dist/src/commands/furnace/chrome-doc.d.ts +16 -17
  40. package/dist/src/commands/furnace/chrome-doc.js +31 -41
  41. package/dist/src/commands/furnace/create-browser-test.d.ts +4 -4
  42. package/dist/src/commands/furnace/create-browser-test.js +8 -8
  43. package/dist/src/commands/furnace/create-mochikit.d.ts +6 -7
  44. package/dist/src/commands/furnace/create-mochikit.js +8 -9
  45. package/dist/src/commands/furnace/create-readback.d.ts +12 -13
  46. package/dist/src/commands/furnace/create-readback.js +12 -13
  47. package/dist/src/commands/furnace/create-templates.d.ts +22 -28
  48. package/dist/src/commands/furnace/create-templates.js +35 -30
  49. package/dist/src/commands/furnace/create-xpcshell.js +2 -2
  50. package/dist/src/commands/furnace/create.js +33 -23
  51. package/dist/src/commands/furnace/deploy.js +16 -24
  52. package/dist/src/commands/furnace/diff.js +9 -9
  53. package/dist/src/commands/furnace/init.js +33 -36
  54. package/dist/src/commands/furnace/override.js +23 -25
  55. package/dist/src/commands/furnace/preview.js +47 -67
  56. package/dist/src/commands/furnace/refresh.js +12 -16
  57. package/dist/src/commands/furnace/remove-mutations.d.ts +3 -6
  58. package/dist/src/commands/furnace/remove-mutations.js +9 -16
  59. package/dist/src/commands/furnace/remove-state.js +1 -1
  60. package/dist/src/commands/furnace/remove.js +33 -38
  61. package/dist/src/commands/furnace/rename-xpcshell.d.ts +8 -11
  62. package/dist/src/commands/furnace/rename-xpcshell.js +8 -11
  63. package/dist/src/commands/furnace/rename.js +59 -69
  64. package/dist/src/commands/furnace/scan.js +19 -33
  65. package/dist/src/commands/furnace/status.js +12 -9
  66. package/dist/src/commands/furnace/sync.js +9 -16
  67. package/dist/src/commands/furnace/validate.js +20 -20
  68. package/dist/src/commands/import.js +42 -35
  69. package/dist/src/commands/lint-per-patch.js +15 -10
  70. package/dist/src/commands/lint-per-run-checkjs.d.ts +13 -13
  71. package/dist/src/commands/lint-per-run-checkjs.js +13 -13
  72. package/dist/src/commands/lint-report.js +6 -7
  73. package/dist/src/commands/lint.d.ts +15 -16
  74. package/dist/src/commands/lint.js +86 -102
  75. package/dist/src/commands/package.js +6 -11
  76. package/dist/src/commands/patch/compact.js +1 -1
  77. package/dist/src/commands/patch/delete.js +24 -33
  78. package/dist/src/commands/patch/index.d.ts +5 -6
  79. package/dist/src/commands/patch/index.js +10 -12
  80. package/dist/src/commands/patch/lint-ignore.js +14 -13
  81. package/dist/src/commands/patch/move-files-create.d.ts +6 -6
  82. package/dist/src/commands/patch/move-files-create.js +8 -8
  83. package/dist/src/commands/patch/move-files-into.d.ts +6 -6
  84. package/dist/src/commands/patch/move-files-into.js +8 -8
  85. package/dist/src/commands/patch/patch-context.d.ts +2 -3
  86. package/dist/src/commands/patch/patch-context.js +2 -3
  87. package/dist/src/commands/patch/rename.d.ts +10 -11
  88. package/dist/src/commands/patch/rename.js +11 -12
  89. package/dist/src/commands/patch/reorder.js +8 -9
  90. package/dist/src/commands/patch/split-plan.js +1 -1
  91. package/dist/src/commands/patch/split.d.ts +8 -10
  92. package/dist/src/commands/patch/split.js +9 -11
  93. package/dist/src/commands/patch/staged-dependency-validate.js +8 -8
  94. package/dist/src/commands/patch/staged-dependency.js +7 -3
  95. package/dist/src/commands/patch/tier.d.ts +9 -9
  96. package/dist/src/commands/patch/tier.js +18 -15
  97. package/dist/src/commands/re-export-adjacent.js +9 -8
  98. package/dist/src/commands/re-export-bulk-scan.d.ts +9 -9
  99. package/dist/src/commands/re-export-bulk-scan.js +22 -21
  100. package/dist/src/commands/re-export-drift.js +10 -10
  101. package/dist/src/commands/re-export-files.d.ts +6 -7
  102. package/dist/src/commands/re-export-files.js +34 -44
  103. package/dist/src/commands/re-export-lint.d.ts +10 -13
  104. package/dist/src/commands/re-export-lint.js +16 -14
  105. package/dist/src/commands/re-export-options.d.ts +6 -6
  106. package/dist/src/commands/re-export-options.js +6 -6
  107. package/dist/src/commands/re-export-register.d.ts +4 -10
  108. package/dist/src/commands/re-export-register.js +5 -11
  109. package/dist/src/commands/re-export-scan.js +1 -1
  110. package/dist/src/commands/re-export-single.d.ts +3 -3
  111. package/dist/src/commands/re-export-single.js +33 -25
  112. package/dist/src/commands/re-export.js +13 -21
  113. package/dist/src/commands/rebase/abort.js +3 -3
  114. package/dist/src/commands/rebase/confirm.js +2 -1
  115. package/dist/src/commands/rebase/continue.d.ts +1 -1
  116. package/dist/src/commands/rebase/continue.js +14 -9
  117. package/dist/src/commands/rebase/index.js +11 -11
  118. package/dist/src/commands/rebase/patch-loop.d.ts +7 -1
  119. package/dist/src/commands/rebase/patch-loop.js +60 -39
  120. package/dist/src/commands/rebase/summary.js +1 -1
  121. package/dist/src/commands/register.js +5 -7
  122. package/dist/src/commands/reset.js +2 -1
  123. package/dist/src/commands/resolve.d.ts +16 -13
  124. package/dist/src/commands/resolve.js +30 -33
  125. package/dist/src/commands/run.js +33 -35
  126. package/dist/src/commands/setup-support.js +15 -19
  127. package/dist/src/commands/setup.js +13 -9
  128. package/dist/src/commands/status-check.d.ts +6 -6
  129. package/dist/src/commands/status-json.d.ts +6 -6
  130. package/dist/src/commands/status-json.js +5 -0
  131. package/dist/src/commands/status-output.d.ts +5 -5
  132. package/dist/src/commands/status-output.js +6 -6
  133. package/dist/src/commands/status-ownership.d.ts +16 -14
  134. package/dist/src/commands/status-ownership.js +16 -14
  135. package/dist/src/commands/status.js +42 -34
  136. package/dist/src/commands/test-build-phase.js +26 -13
  137. package/dist/src/commands/test-diagnose.js +44 -44
  138. package/dist/src/commands/test-register.js +1 -0
  139. package/dist/src/commands/test-run.d.ts +14 -16
  140. package/dist/src/commands/test-run.js +21 -13
  141. package/dist/src/commands/test-stale-gate.d.ts +15 -16
  142. package/dist/src/commands/test-stale-gate.js +15 -16
  143. package/dist/src/commands/test-verdict.d.ts +20 -3
  144. package/dist/src/commands/test-verdict.js +30 -0
  145. package/dist/src/commands/test.js +83 -76
  146. package/dist/src/commands/token-coverage.js +3 -4
  147. package/dist/src/commands/token.js +16 -18
  148. package/dist/src/commands/tree.js +47 -31
  149. package/dist/src/commands/typecheck.d.ts +10 -12
  150. package/dist/src/commands/typecheck.js +24 -27
  151. package/dist/src/commands/verify.js +13 -15
  152. package/dist/src/commands/watch.js +21 -23
  153. package/dist/src/commands/wire.js +52 -62
  154. package/dist/src/core/ast-utils.d.ts +8 -7
  155. package/dist/src/core/ast-utils.js +8 -7
  156. package/dist/src/core/branding.d.ts +9 -14
  157. package/dist/src/core/branding.js +7 -11
  158. package/dist/src/core/browser-wire.d.ts +1 -1
  159. package/dist/src/core/browser-wire.js +14 -19
  160. package/dist/src/core/build-audit-platform.js +25 -33
  161. package/dist/src/core/build-audit-registration.js +17 -25
  162. package/dist/src/core/build-audit-resolve.d.ts +15 -17
  163. package/dist/src/core/build-audit-resolve.js +27 -33
  164. package/dist/src/core/build-audit-transforms.js +18 -19
  165. package/dist/src/core/build-audit.js +28 -42
  166. package/dist/src/core/build-baseline-types.d.ts +29 -35
  167. package/dist/src/core/build-baseline.d.ts +5 -4
  168. package/dist/src/core/build-baseline.js +14 -14
  169. package/dist/src/core/build-overwrite-guard.d.ts +7 -14
  170. package/dist/src/core/build-overwrite-guard.js +62 -19
  171. package/dist/src/core/build-prepare.d.ts +2 -2
  172. package/dist/src/core/build-prepare.js +8 -9
  173. package/dist/src/core/config-state.js +12 -4
  174. package/dist/src/core/config-validate.js +8 -5
  175. package/dist/src/core/config.d.ts +5 -8
  176. package/dist/src/core/config.js +7 -16
  177. package/dist/src/core/coverage-extend.d.ts +15 -13
  178. package/dist/src/core/coverage-extend.js +15 -13
  179. package/dist/src/core/destructive.d.ts +31 -9
  180. package/dist/src/core/destructive.js +29 -26
  181. package/dist/src/core/diff-hunks.js +10 -12
  182. package/dist/src/core/discard-baseline.d.ts +8 -8
  183. package/dist/src/core/discard-baseline.js +16 -15
  184. package/dist/src/core/display-state.d.ts +12 -14
  185. package/dist/src/core/display-state.js +12 -14
  186. package/dist/src/core/engine-changes.d.ts +2 -4
  187. package/dist/src/core/engine-changes.js +2 -4
  188. package/dist/src/core/engine-precondition.d.ts +28 -13
  189. package/dist/src/core/engine-precondition.js +25 -10
  190. package/dist/src/core/engine-session-lock.d.ts +47 -21
  191. package/dist/src/core/engine-session-lock.js +168 -33
  192. package/dist/src/core/export-moved-code-shape.d.ts +10 -10
  193. package/dist/src/core/export-moved-code-shape.js +10 -10
  194. package/dist/src/core/file-lock.d.ts +62 -5
  195. package/dist/src/core/file-lock.js +196 -59
  196. package/dist/src/core/firefox-cache.js +1 -3
  197. package/dist/src/core/firefox-extract.js +10 -14
  198. package/dist/src/core/firefox.d.ts +7 -5
  199. package/dist/src/core/firefox.js +4 -5
  200. package/dist/src/core/furnace-apply-dry-run.d.ts +2 -2
  201. package/dist/src/core/furnace-apply-dry-run.js +3 -2
  202. package/dist/src/core/furnace-apply-ftl.d.ts +7 -9
  203. package/dist/src/core/furnace-apply-ftl.js +12 -12
  204. package/dist/src/core/furnace-apply-helpers.d.ts +7 -7
  205. package/dist/src/core/furnace-apply-helpers.js +19 -16
  206. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +8 -9
  207. package/dist/src/core/furnace-apply-overwrite-warn.js +8 -9
  208. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +3 -4
  209. package/dist/src/core/furnace-apply-undeploy-actions.js +3 -4
  210. package/dist/src/core/furnace-apply.d.ts +5 -8
  211. package/dist/src/core/furnace-apply.js +19 -24
  212. package/dist/src/core/furnace-checksum-utils.d.ts +14 -0
  213. package/dist/src/core/furnace-checksum-utils.js +16 -0
  214. package/dist/src/core/furnace-config.d.ts +21 -30
  215. package/dist/src/core/furnace-config.js +32 -54
  216. package/dist/src/core/furnace-constants.d.ts +8 -0
  217. package/dist/src/core/furnace-constants.js +10 -2
  218. package/dist/src/core/furnace-css-fragments.d.ts +5 -5
  219. package/dist/src/core/furnace-css-fragments.js +5 -5
  220. package/dist/src/core/furnace-jsconfig.d.ts +11 -12
  221. package/dist/src/core/furnace-jsconfig.js +11 -12
  222. package/dist/src/core/furnace-marker.d.ts +7 -8
  223. package/dist/src/core/furnace-marker.js +7 -8
  224. package/dist/src/core/furnace-operation.d.ts +31 -0
  225. package/dist/src/core/furnace-operation.js +54 -17
  226. package/dist/src/core/furnace-precondition.d.ts +61 -0
  227. package/dist/src/core/furnace-precondition.js +67 -0
  228. package/dist/src/core/furnace-registration-remove.d.ts +5 -6
  229. package/dist/src/core/furnace-registration-remove.js +6 -7
  230. package/dist/src/core/furnace-registration-validate.d.ts +13 -0
  231. package/dist/src/core/furnace-registration-validate.js +20 -1
  232. package/dist/src/core/furnace-registration.d.ts +5 -6
  233. package/dist/src/core/furnace-registration.js +10 -12
  234. package/dist/src/core/furnace-rollback.js +7 -7
  235. package/dist/src/core/furnace-scanner.d.ts +1 -0
  236. package/dist/src/core/furnace-scanner.js +1 -2
  237. package/dist/src/core/furnace-stale-export.d.ts +10 -10
  238. package/dist/src/core/furnace-stale-export.js +10 -10
  239. package/dist/src/core/furnace-state-persist.d.ts +6 -8
  240. package/dist/src/core/furnace-state-persist.js +6 -8
  241. package/dist/src/core/furnace-validate-accessibility.d.ts +22 -0
  242. package/dist/src/core/furnace-validate-accessibility.js +310 -1
  243. package/dist/src/core/furnace-validate-compatibility.js +141 -1
  244. package/dist/src/core/furnace-validate-helpers.d.ts +5 -116
  245. package/dist/src/core/furnace-validate-helpers.js +0 -491
  246. package/dist/src/core/furnace-validate-registration.d.ts +7 -0
  247. package/dist/src/core/furnace-validate-registration.js +115 -41
  248. package/dist/src/core/furnace-validate.d.ts +16 -0
  249. package/dist/src/core/furnace-validate.js +188 -145
  250. package/dist/src/core/furnace-version-drift.d.ts +10 -11
  251. package/dist/src/core/furnace-version-drift.js +10 -11
  252. package/dist/src/core/git-base.d.ts +7 -9
  253. package/dist/src/core/git-base.js +7 -9
  254. package/dist/src/core/git-diff.d.ts +20 -9
  255. package/dist/src/core/git-diff.js +148 -63
  256. package/dist/src/core/git-file-ops.d.ts +19 -19
  257. package/dist/src/core/git-file-ops.js +19 -19
  258. package/dist/src/core/git-readonly-index.d.ts +53 -19
  259. package/dist/src/core/git-readonly-index.js +75 -31
  260. package/dist/src/core/git-status.d.ts +5 -6
  261. package/dist/src/core/git-status.js +5 -6
  262. package/dist/src/core/git.d.ts +9 -10
  263. package/dist/src/core/git.js +54 -70
  264. package/dist/src/core/license-headers.d.ts +17 -23
  265. package/dist/src/core/license-headers.js +17 -23
  266. package/dist/src/core/mach-build-artifacts.d.ts +9 -9
  267. package/dist/src/core/mach-build-artifacts.js +57 -19
  268. package/dist/src/core/mach-error-hints.js +42 -29
  269. package/dist/src/core/mach-known-noise-filter.d.ts +15 -16
  270. package/dist/src/core/mach-known-noise-filter.js +18 -20
  271. package/dist/src/core/mach-mozconfig.d.ts +14 -0
  272. package/dist/src/core/mach-mozconfig.js +30 -0
  273. package/dist/src/core/mach-resource-shim.d.ts +43 -58
  274. package/dist/src/core/mach-resource-shim.js +50 -67
  275. package/dist/src/core/mach.d.ts +37 -41
  276. package/dist/src/core/mach.js +50 -51
  277. package/dist/src/core/marionette-port.d.ts +36 -1
  278. package/dist/src/core/marionette-port.js +96 -31
  279. package/dist/src/core/marionette-preflight.d.ts +8 -12
  280. package/dist/src/core/marionette-preflight.js +86 -52
  281. package/dist/src/core/mochitest-server-port.d.ts +42 -0
  282. package/dist/src/core/mochitest-server-port.js +119 -0
  283. package/dist/src/core/module-resolution-preflight.d.ts +9 -11
  284. package/dist/src/core/module-resolution-preflight.js +9 -11
  285. package/dist/src/core/moz-manifest-helpers.d.ts +79 -0
  286. package/dist/src/core/moz-manifest-helpers.js +163 -0
  287. package/dist/src/core/{manifest-register.d.ts → moz-manifest-register.d.ts} +1 -1
  288. package/dist/src/core/{manifest-register.js → moz-manifest-register.js} +13 -28
  289. package/dist/src/core/{manifest-rules.d.ts → moz-manifest-rules.d.ts} +10 -1
  290. package/dist/src/core/{manifest-rules.js → moz-manifest-rules.js} +31 -24
  291. package/dist/src/core/{manifest-tokenizers.d.ts → moz-manifest-tokenizers.d.ts} +3 -5
  292. package/dist/src/core/{manifest-tokenizers.js → moz-manifest-tokenizers.js} +9 -12
  293. package/dist/src/core/ownership-table.d.ts +14 -16
  294. package/dist/src/core/ownership-table.js +14 -16
  295. package/dist/src/core/parser-fallback.d.ts +15 -1
  296. package/dist/src/core/parser-fallback.js +22 -3
  297. package/dist/src/core/patch-apply-fuzz.d.ts +10 -12
  298. package/dist/src/core/patch-apply-fuzz.js +15 -18
  299. package/dist/src/core/patch-apply.d.ts +13 -34
  300. package/dist/src/core/patch-apply.js +16 -76
  301. package/dist/src/core/patch-artifact-normalize.d.ts +13 -15
  302. package/dist/src/core/patch-artifact-normalize.js +13 -15
  303. package/dist/src/core/patch-export-metadata.d.ts +12 -2
  304. package/dist/src/core/patch-export-metadata.js +13 -4
  305. package/dist/src/core/patch-export-update.d.ts +13 -1
  306. package/dist/src/core/patch-export-update.js +25 -8
  307. package/dist/src/core/patch-export.d.ts +15 -8
  308. package/dist/src/core/patch-export.js +10 -11
  309. package/dist/src/core/patch-files.d.ts +5 -6
  310. package/dist/src/core/patch-files.js +5 -6
  311. package/dist/src/core/patch-identifier-suggest.d.ts +7 -12
  312. package/dist/src/core/patch-identifier-suggest.js +14 -19
  313. package/dist/src/core/patch-lint-binary.d.ts +51 -0
  314. package/dist/src/core/patch-lint-binary.js +39 -0
  315. package/dist/src/core/{lint-cache.d.ts → patch-lint-cache.d.ts} +5 -5
  316. package/dist/src/core/{lint-cache.js → patch-lint-cache.js} +21 -23
  317. package/dist/src/core/patch-lint-checkjs.d.ts +16 -16
  318. package/dist/src/core/patch-lint-checkjs.js +16 -16
  319. package/dist/src/core/patch-lint-chrome-jsdoc.d.ts +6 -9
  320. package/dist/src/core/patch-lint-chrome-jsdoc.js +10 -14
  321. package/dist/src/core/patch-lint-creators.d.ts +8 -8
  322. package/dist/src/core/patch-lint-creators.js +8 -8
  323. package/dist/src/core/patch-lint-cross.d.ts +17 -20
  324. package/dist/src/core/patch-lint-cross.js +16 -16
  325. package/dist/src/core/patch-lint-css.d.ts +3 -5
  326. package/dist/src/core/patch-lint-css.js +17 -22
  327. package/dist/src/core/patch-lint-diff-tag.d.ts +14 -16
  328. package/dist/src/core/patch-lint-diff-tag.js +14 -16
  329. package/dist/src/core/patch-lint-jsdoc.d.ts +7 -8
  330. package/dist/src/core/patch-lint-jsdoc.js +26 -19
  331. package/dist/src/core/patch-lint-mozbuild.js +20 -20
  332. package/dist/src/core/patch-lint-observer.d.ts +7 -7
  333. package/dist/src/core/patch-lint-observer.js +7 -7
  334. package/dist/src/core/patch-lint.d.ts +38 -29
  335. package/dist/src/core/patch-lint.js +89 -121
  336. package/dist/src/core/patch-lock.d.ts +1 -1
  337. package/dist/src/core/patch-lock.js +45 -9
  338. package/dist/src/core/patch-manifest-consistency.d.ts +2 -1
  339. package/dist/src/core/patch-manifest-consistency.js +25 -27
  340. package/dist/src/core/patch-manifest-io.js +14 -18
  341. package/dist/src/core/patch-manifest-resolve.d.ts +9 -12
  342. package/dist/src/core/patch-manifest-resolve.js +9 -12
  343. package/dist/src/core/patch-manifest-validate.d.ts +6 -1
  344. package/dist/src/core/patch-manifest-validate.js +25 -13
  345. package/dist/src/core/patch-parse.d.ts +24 -13
  346. package/dist/src/core/patch-parse.js +19 -14
  347. package/dist/src/core/patch-policy.d.ts +2 -2
  348. package/dist/src/core/patch-policy.js +2 -2
  349. package/dist/src/core/patch-registration-refs.d.ts +7 -8
  350. package/dist/src/core/patch-registration-refs.js +9 -9
  351. package/dist/src/core/process-cpu-time.d.ts +12 -0
  352. package/dist/src/core/process-cpu-time.js +52 -0
  353. package/dist/src/core/rebase-session.d.ts +38 -26
  354. package/dist/src/core/rebase-session.js +55 -28
  355. package/dist/src/core/register-browser-content.js +9 -74
  356. package/dist/src/core/{register-test-manifest.js → register-browser-test.js} +7 -42
  357. package/dist/src/core/register-module.js +10 -43
  358. package/dist/src/core/register-scaffold.d.ts +8 -8
  359. package/dist/src/core/register-scaffold.js +10 -10
  360. package/dist/src/core/register-shared-css.d.ts +9 -12
  361. package/dist/src/core/register-shared-css.js +17 -87
  362. package/dist/src/core/register-xpcshell-test.d.ts +6 -7
  363. package/dist/src/core/register-xpcshell-test.js +7 -8
  364. package/dist/src/core/shared-ftl.js +12 -4
  365. package/dist/src/core/state-file.d.ts +45 -0
  366. package/dist/src/core/state-file.js +34 -0
  367. package/dist/src/core/status-binary.d.ts +18 -11
  368. package/dist/src/core/status-binary.js +19 -12
  369. package/dist/src/core/status-classify.d.ts +30 -36
  370. package/dist/src/core/status-classify.js +21 -26
  371. package/dist/src/core/test-changed-prefs.d.ts +30 -0
  372. package/dist/src/core/test-changed-prefs.js +78 -0
  373. package/dist/src/core/test-harness-crash.d.ts +25 -63
  374. package/dist/src/core/test-harness-crash.js +121 -142
  375. package/dist/src/core/test-harness-signature.d.ts +18 -0
  376. package/dist/src/core/test-harness-signature.js +13 -0
  377. package/dist/src/core/test-path-scope.d.ts +12 -15
  378. package/dist/src/core/test-path-scope.js +15 -18
  379. package/dist/src/core/test-stale-check.d.ts +17 -17
  380. package/dist/src/core/test-stale-check.js +50 -43
  381. package/dist/src/core/test-stall-triage.d.ts +51 -0
  382. package/dist/src/core/test-stall-triage.js +126 -0
  383. package/dist/src/core/token-category.js +5 -7
  384. package/dist/src/core/token-coverage.js +9 -9
  385. package/dist/src/core/token-dark-mode.d.ts +9 -11
  386. package/dist/src/core/token-dark-mode.js +9 -11
  387. package/dist/src/core/token-docs.d.ts +2 -0
  388. package/dist/src/core/token-docs.js +2 -0
  389. package/dist/src/core/token-manager.d.ts +10 -2
  390. package/dist/src/core/token-manager.js +17 -8
  391. package/dist/src/core/token-scaffold.d.ts +7 -7
  392. package/dist/src/core/token-scaffold.js +7 -7
  393. package/dist/src/core/token-variant.d.ts +4 -4
  394. package/dist/src/core/token-variant.js +4 -4
  395. package/dist/src/core/toolchain-preflight.d.ts +20 -21
  396. package/dist/src/core/toolchain-preflight.js +28 -31
  397. package/dist/src/core/tree-guard.js +12 -2
  398. package/dist/src/core/tree-store.d.ts +41 -27
  399. package/dist/src/core/tree-store.js +63 -42
  400. package/dist/src/core/typecheck-shim.d.ts +23 -24
  401. package/dist/src/core/typecheck-shim.js +23 -24
  402. package/dist/src/core/typecheck.js +8 -8
  403. package/dist/src/core/wire-destroy.d.ts +3 -3
  404. package/dist/src/core/wire-destroy.js +7 -15
  405. package/dist/src/core/wire-dom-fragment.d.ts +9 -17
  406. package/dist/src/core/wire-dom-fragment.js +11 -44
  407. package/dist/src/core/wire-init.d.ts +2 -3
  408. package/dist/src/core/wire-init.js +8 -19
  409. package/dist/src/core/wire-subscript.d.ts +2 -3
  410. package/dist/src/core/wire-subscript.js +4 -13
  411. package/dist/src/core/wire-utils.d.ts +7 -8
  412. package/dist/src/core/wire-utils.js +7 -8
  413. package/dist/src/core/xpcshell-appdir.d.ts +38 -45
  414. package/dist/src/core/xpcshell-appdir.js +38 -45
  415. package/dist/src/errors/base.d.ts +37 -16
  416. package/dist/src/errors/base.js +37 -16
  417. package/dist/src/errors/build.d.ts +13 -1
  418. package/dist/src/errors/build.js +14 -2
  419. package/dist/src/errors/codes.d.ts +16 -0
  420. package/dist/src/errors/codes.js +16 -0
  421. package/dist/src/errors/furnace.d.ts +4 -4
  422. package/dist/src/errors/git.d.ts +6 -10
  423. package/dist/src/errors/git.js +6 -10
  424. package/dist/src/errors/patch.d.ts +7 -7
  425. package/dist/src/errors/patch.js +7 -7
  426. package/dist/src/errors/rebase.d.ts +4 -4
  427. package/dist/src/errors/rebase.js +4 -4
  428. package/dist/src/index.d.ts +7 -1
  429. package/dist/src/index.js +5 -0
  430. package/dist/src/types/commands/index.d.ts +2 -2
  431. package/dist/src/types/commands/options.d.ts +88 -70
  432. package/dist/src/types/commands/patches.d.ts +24 -34
  433. package/dist/src/types/commands/project.d.ts +5 -34
  434. package/dist/src/types/config.d.ts +25 -23
  435. package/dist/src/types/config.js +6 -1
  436. package/dist/src/types/furnace.d.ts +37 -13
  437. package/dist/src/types/furnace.js +19 -1
  438. package/dist/src/types/index.d.ts +3 -3
  439. package/dist/src/utils/build-info.d.ts +7 -8
  440. package/dist/src/utils/build-info.js +12 -11
  441. package/dist/src/utils/errors.d.ts +6 -8
  442. package/dist/src/utils/errors.js +6 -8
  443. package/dist/src/utils/logger.d.ts +27 -10
  444. package/dist/src/utils/logger.js +28 -16
  445. package/dist/src/utils/machine-output.d.ts +12 -0
  446. package/dist/src/utils/machine-output.js +33 -0
  447. package/dist/src/utils/options.d.ts +31 -14
  448. package/dist/src/utils/options.js +86 -30
  449. package/dist/src/utils/paths.d.ts +10 -8
  450. package/dist/src/utils/paths.js +10 -8
  451. package/dist/src/utils/platform.d.ts +0 -25
  452. package/dist/src/utils/platform.js +1 -42
  453. package/dist/src/utils/process-group.d.ts +9 -4
  454. package/dist/src/utils/process-group.js +19 -5
  455. package/dist/src/utils/process.d.ts +42 -7
  456. package/dist/src/utils/process.js +44 -17
  457. package/dist/src/utils/ps-duration.d.ts +31 -0
  458. package/dist/src/utils/ps-duration.js +70 -0
  459. package/dist/src/utils/regex.d.ts +9 -10
  460. package/dist/src/utils/regex.js +9 -10
  461. package/dist/src/utils/validation.d.ts +58 -28
  462. package/dist/src/utils/validation.js +60 -33
  463. package/package.json +2 -2
  464. package/dist/src/commands/doctor/post-rebase-audit.d.ts +0 -2
  465. package/dist/src/commands/doctor-furnace-manifest-sync.d.ts +0 -18
  466. package/dist/src/core/manifest-helpers.d.ts +0 -38
  467. package/dist/src/core/manifest-helpers.js +0 -120
  468. /package/dist/src/core/{lint-projection.d.ts → patch-lint-projection.d.ts} +0 -0
  469. /package/dist/src/core/{lint-projection.js → patch-lint-projection.js} +0 -0
  470. /package/dist/src/core/{register-test-manifest.d.ts → register-browser-test.d.ts} +0 -0
@@ -16,9 +16,9 @@ export async function handleAbort(projectRoot, yes) {
16
16
  intro('FireForge Rebase — Abort');
17
17
  // Abort is the escape hatch, so it deliberately does NOT require a *valid*
18
18
  // session — only that one is present. Nothing below reads the session
19
- // object (the restore works off `paths.engine` and `resetChanges`), and the
20
- // pre-0.41.0 `if (!session) throw` refused to run against a corrupt file,
21
- // which was the only command that could have cleared it.
19
+ // object (the restore works off `paths.engine` and `resetChanges`), and an
20
+ // `if (!session) throw` would refuse to run against a corrupt file, which
21
+ // is the only thing that could have cleared it.
22
22
  const read = await readRebaseSession(projectRoot);
23
23
  if (!read.present)
24
24
  throw new NoRebaseSessionError();
@@ -3,6 +3,7 @@
3
3
  * Dirty-engine confirmation/reset flow.
4
4
  */
5
5
  import { confirm } from '@clack/prompts';
6
+ import { stdioIsInteractive } from '../../core/destructive.js';
6
7
  import { hasChanges } from '../../core/git.js';
7
8
  import { InvalidArgumentError } from '../../errors/base.js';
8
9
  import { cancel, isCancel, warn } from '../../utils/logger.js';
@@ -15,7 +16,7 @@ export async function confirmDirtyEngineReset({ engineDir, yes, nonInteractiveCo
15
16
  if (!(await hasChanges(engineDir)) || yes) {
16
17
  return true;
17
18
  }
18
- const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
19
+ const isInteractive = stdioIsInteractive();
19
20
  if (!isInteractive) {
20
21
  throw new InvalidArgumentError(`Engine has uncommitted changes and interactive confirmation is not available. Run: ${nonInteractiveCommand}`, argumentName);
21
22
  }
@@ -4,4 +4,4 @@
4
4
  /**
5
5
  * Handles `fireforge rebase --continue`.
6
6
  */
7
- export declare function handleContinue(projectRoot: string, maxFuzz: number): Promise<void>;
7
+ export declare function handleContinue(projectRoot: string, maxFuzz: number, waitLockSeconds?: number): Promise<void>;
@@ -18,7 +18,7 @@ import { runPatchLoop } from './patch-loop.js';
18
18
  /**
19
19
  * Handles `fireforge rebase --continue`.
20
20
  */
21
- export async function handleContinue(projectRoot, maxFuzz) {
21
+ export async function handleContinue(projectRoot, maxFuzz, waitLockSeconds) {
22
22
  intro('FireForge Rebase — Continue');
23
23
  // A present-but-unreadable session is NOT "no session in progress" — that
24
24
  // conflation is what made a corrupt file unrecoverable. Name the file and
@@ -39,7 +39,7 @@ export async function handleContinue(projectRoot, maxFuzz) {
39
39
  // there is no failed patch to resolve, but the session is still active.
40
40
  if (session.currentIndex >= session.patches.length) {
41
41
  info('All patches already applied; retrying post-apply re-export and version stamping.');
42
- await runPatchLoop(projectRoot, session, paths, maxFuzz);
42
+ await runPatchLoop(projectRoot, session, paths, maxFuzz, waitLockSeconds);
43
43
  return;
44
44
  }
45
45
  // The current patch should be in 'failed' state
@@ -73,11 +73,10 @@ export async function handleContinue(projectRoot, maxFuzz) {
73
73
  return;
74
74
  }
75
75
  // Write the patch body and the manifest metadata atomically under the
76
- // shared patch-directory lock. The previous lock-free sequence of
77
- // updatePatch + updatePatchMetadata could interleave with a concurrent
78
- // export / re-export / patch reorder / patch compact and leave the
79
- // manifest disagreeing with the freshly-written patch body. Mirrors the
80
- // v0.14.0 resolve.ts fix.
76
+ // shared patch-directory lock. A lock-free updatePatch +
77
+ // updatePatchMetadata sequence can interleave with a concurrent export /
78
+ // re-export / patch reorder / patch compact and leave the manifest
79
+ // disagreeing with the freshly-written patch body. Mirrors resolve.ts.
81
80
  await updatePatchAndMetadata(paths.patches, currentPatch.filename, diffContent, {
82
81
  sourceEsrVersion: session.toVersion,
83
82
  sourceVersion: session.toVersion,
@@ -94,7 +93,13 @@ export async function handleContinue(projectRoot, maxFuzz) {
94
93
  // clear is held until both writes land, matching the guarantee the apply
95
94
  // loop in patch-loop.ts provides.
96
95
  await runInSignalCriticalSection(`rebase-continue:${currentPatch.filename}`, async () => {
97
- currentPatch.status = 'resolved';
96
+ // Replaced, not mutated: flipping `status` in place leaves the
97
+ // failure's `error` and `conflictingFiles` on a now-resolved entry, and
98
+ // persist that to the session file. The union makes it impossible.
99
+ session.patches[session.currentIndex] = {
100
+ filename: currentPatch.filename,
101
+ status: 'resolved',
102
+ };
98
103
  session.currentIndex++;
99
104
  await saveRebaseSession(projectRoot, session);
100
105
  // Clear pending resolution transactionally so concurrent state-file
@@ -109,5 +114,5 @@ export async function handleContinue(projectRoot, maxFuzz) {
109
114
  });
110
115
  success(`Resolved ${currentPatch.filename}`);
111
116
  // Continue applying remaining patches
112
- await runPatchLoop(projectRoot, session, paths, maxFuzz);
117
+ await runPatchLoop(projectRoot, session, paths, maxFuzz, waitLockSeconds);
113
118
  }
@@ -22,7 +22,7 @@ import { getRebaseSessionPath, readRebaseSession, saveRebaseSession, } from '../
22
22
  import { GeneralError } from '../../errors/base.js';
23
23
  import { CorruptRebaseSessionError, RebaseSessionExistsError } from '../../errors/rebase.js';
24
24
  import { info, intro, outro, spinner } from '../../utils/logger.js';
25
- import { commanderArgParser, pickDefined } from '../../utils/options.js';
25
+ import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../../utils/options.js';
26
26
  import { parsePositiveIntegerFlag } from '../../utils/validation.js';
27
27
  import { handleAbort } from './abort.js';
28
28
  import { confirmDirtyEngineReset } from './confirm.js';
@@ -39,17 +39,17 @@ async function handleFreshStart(projectRoot, options) {
39
39
  const existing = await readRebaseSession(projectRoot);
40
40
  if (existing.present) {
41
41
  // A corrupt session must not be reported as "already in progress": that
42
- // message tells the operator to run --continue or --abort, and before
43
- // 0.41.0 both of those then reported "no rebase session in progress".
42
+ // message tells the operator to run --continue or --abort, and both of
43
+ // those then report "no rebase session in progress".
44
44
  if (!existing.valid) {
45
45
  throw new CorruptRebaseSessionError(getRebaseSessionPath(projectRoot), existing.reason);
46
46
  }
47
47
  throw new RebaseSessionExistsError();
48
48
  }
49
49
  const paths = getProjectPaths(projectRoot);
50
- // Keeps the rebase-specific tail on the unborn-HEAD remediation (2026-04-24
51
- // eval Finding 11: --dry-run used to skip this check entirely, so the real
52
- // run then failed on `git rev-parse HEAD`).
50
+ // Keeps the rebase-specific tail on the unborn-HEAD remediation. Skipping
51
+ // this check in --dry-run leaves the real run to fail on
52
+ // `git rev-parse HEAD`.
53
53
  await assertEngineGitReady(paths.engine, { unbornHeadSuffix: ', then retry the rebase.' });
54
54
  const config = await loadConfig(projectRoot);
55
55
  const currentVersion = config.firefox.version;
@@ -112,7 +112,7 @@ async function handleFreshStart(projectRoot, options) {
112
112
  };
113
113
  await saveRebaseSession(projectRoot, session);
114
114
  // Run the patch loop
115
- await runPatchLoop(projectRoot, session, paths, maxFuzz);
115
+ await runPatchLoop(projectRoot, session, paths, maxFuzz, resolveWaitLockSeconds(options.waitLock));
116
116
  }
117
117
  // ── Public API ──
118
118
  /**
@@ -125,21 +125,21 @@ export async function rebaseCommand(projectRoot, options = {}) {
125
125
  return handleAbort(projectRoot, options.yes);
126
126
  }
127
127
  if (options.continue) {
128
- return handleContinue(projectRoot, options.maxFuzz ?? 3);
128
+ return handleContinue(projectRoot, options.maxFuzz ?? 3, resolveWaitLockSeconds(options.waitLock));
129
129
  }
130
130
  return handleFreshStart(projectRoot, options);
131
131
  }
132
132
  /** Registers the rebase command on the CLI program. */
133
133
  export function registerRebase(program, { getProjectRoot, withErrorHandling }) {
134
- program
134
+ const command = program
135
135
  .command('rebase')
136
136
  .description('Semi-automated Firefox source version upgrade — apply patches with drift tolerance and re-export')
137
137
  .option('--continue', 'Resume after manually resolving a failed patch')
138
138
  .option('--abort', 'Cancel the rebase and restore engine to pre-rebase state')
139
139
  .option('--dry-run', 'Show what would happen without modifying anything')
140
140
  .option('--max-fuzz <n>', 'Maximum context-reduction steps for git apply -C<n> (fuzz-like drift tolerance; default: 3)', commanderArgParser((v) => parsePositiveIntegerFlag('--max-fuzz', v)))
141
- .option('-y, --yes', 'Skip dirty-tree confirmation prompt')
142
- .action(withErrorHandling(async (options) => {
141
+ .option('-y, --yes', 'Skip dirty-tree confirmation prompt');
142
+ addWaitLockOption(command).action(withErrorHandling(async (options) => {
143
143
  await rebaseCommand(getProjectRoot(), pickDefined(options));
144
144
  }));
145
145
  }
@@ -5,5 +5,11 @@ import type { getProjectPaths } from '../../core/config.js';
5
5
  import type { RebaseSession } from '../../core/rebase-session.js';
6
6
  /**
7
7
  * Runs the patch application loop, re-exports applied patches, and stamps versions.
8
+ *
9
+ * @param projectRoot - Project root directory
10
+ * @param session - The active rebase session, mutated and persisted as it runs
11
+ * @param paths - Resolved project paths
12
+ * @param maxFuzz - Maximum context-reduction steps for `git apply -C<n>`
13
+ * @param waitLockSeconds - `--wait-lock` budget for the patch-directory lock
8
14
  */
9
- export declare function runPatchLoop(projectRoot: string, session: RebaseSession, paths: ReturnType<typeof getProjectPaths>, maxFuzz: number): Promise<void>;
15
+ export declare function runPatchLoop(projectRoot: string, session: RebaseSession, paths: ReturnType<typeof getProjectPaths>, maxFuzz: number, waitLockSeconds?: number): Promise<void>;
@@ -12,7 +12,7 @@ import { discoverPatches } from '../../core/patch-files.js';
12
12
  import { withPatchDirectoryLock } from '../../core/patch-lock.js';
13
13
  import { loadPatchesManifest, stampPatchVersions } from '../../core/patch-manifest.js';
14
14
  import { extractConflictingFiles } from '../../core/patch-parse.js';
15
- import { clearRebaseSession, saveRebaseSession } from '../../core/rebase-session.js';
15
+ import { clearRebaseSession, saveRebaseSession, } from '../../core/rebase-session.js';
16
16
  import { runInSignalCriticalSection } from '../../core/signal-critical.js';
17
17
  import { RebaseError } from '../../errors/rebase.js';
18
18
  import { elapsedSince } from '../../utils/elapsed.js';
@@ -22,10 +22,42 @@ import { error, info, outro, spinner, success, warn } from '../../utils/logger.j
22
22
  import { isValidFirefoxVersion } from '../../utils/validation.js';
23
23
  import { buildRebaseConflictSummary } from './conflict-summary.js';
24
24
  import { printSummary } from './summary.js';
25
+ /**
26
+ * Builds the session entry for a patch-apply outcome.
27
+ *
28
+ * Constructing a whole entry rather than assigning fields is what the
29
+ * discriminated union buys: a status can never be flipped while the previous
30
+ * status's payload stays behind, which is how
31
+ * `{ status: 'resolved', error, conflictingFiles }` reaches the session file.
32
+ *
33
+ * @param filename - Patch filename the entry describes
34
+ * @param applyResult - Outcome from `applyPatchWithFuzz`
35
+ * @returns The entry to store at this patch's index
36
+ */
37
+ function entryForApplyResult(filename, applyResult) {
38
+ if (!applyResult.success) {
39
+ return {
40
+ filename,
41
+ status: 'failed',
42
+ ...(applyResult.error !== undefined ? { error: applyResult.error } : {}),
43
+ conflictingFiles: extractConflictingFiles(applyResult.error),
44
+ };
45
+ }
46
+ if (applyResult.fuzzFactor === 0 || applyResult.fuzzFactor === undefined) {
47
+ return { filename, status: 'applied-clean' };
48
+ }
49
+ return { filename, status: 'applied-fuzz', fuzzFactor: applyResult.fuzzFactor };
50
+ }
25
51
  /**
26
52
  * Runs the patch application loop, re-exports applied patches, and stamps versions.
53
+ *
54
+ * @param projectRoot - Project root directory
55
+ * @param session - The active rebase session, mutated and persisted as it runs
56
+ * @param paths - Resolved project paths
57
+ * @param maxFuzz - Maximum context-reduction steps for `git apply -C<n>`
58
+ * @param waitLockSeconds - `--wait-lock` budget for the patch-directory lock
27
59
  */
28
- export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
60
+ export async function runPatchLoop(projectRoot, session, paths, maxFuzz, waitLockSeconds) {
29
61
  const allPatches = await discoverPatches(paths.patches);
30
62
  const s = spinner('Applying patches...');
31
63
  for (let i = session.currentIndex; i < session.patches.length; i++) {
@@ -35,7 +67,10 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
35
67
  const patchFile = allPatches.find((p) => p.filename === entry.filename);
36
68
  if (!patchFile) {
37
69
  warn(`Patch file not found for ${entry.filename}, skipping.`);
38
- entry.status = 'skipped';
70
+ // Replaced, not mutated: the entry is a discriminated union, so a status
71
+ // change is a new value rather than a field assignment. That is what
72
+ // stops a status flip stranding the previous status's payload.
73
+ session.patches[i] = { filename: entry.filename, status: 'skipped' };
39
74
  session.currentIndex = i + 1;
40
75
  await saveRebaseSession(projectRoot, session);
41
76
  continue;
@@ -50,26 +85,9 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
50
85
  // divergent results).
51
86
  const result = await runInSignalCriticalSection(`rebase-apply:${entry.filename}`, async () => {
52
87
  const applyResult = await applyPatchWithFuzz(patchFile.path, paths.engine, maxFuzz);
53
- if (applyResult.success) {
54
- if (applyResult.fuzzFactor === 0) {
55
- entry.status = 'applied-clean';
56
- }
57
- else {
58
- entry.status = 'applied-fuzz';
59
- entry.fuzzFactor = applyResult.fuzzFactor;
60
- }
61
- session.currentIndex = i + 1;
62
- await saveRebaseSession(projectRoot, session);
63
- }
64
- else {
65
- entry.status = 'failed';
66
- if (applyResult.error) {
67
- entry.error = applyResult.error;
68
- }
69
- entry.conflictingFiles = extractConflictingFiles(applyResult.error);
70
- session.currentIndex = i;
71
- await saveRebaseSession(projectRoot, session);
72
- }
88
+ session.patches[i] = entryForApplyResult(entry.filename, applyResult);
89
+ session.currentIndex = applyResult.success ? i + 1 : i;
90
+ await saveRebaseSession(projectRoot, session);
73
91
  return applyResult;
74
92
  });
75
93
  if (result.success) {
@@ -130,7 +148,7 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
130
148
  // patch was tested against if we proceeded to stamp. Refuse to claim
131
149
  // success and leave the session in place so the user can recover via
132
150
  // `fireforge rebase --continue` after fixing the underlying cause.
133
- const { failures: reExportFailures, overlapSkipped } = await reExportAppliedPatches(session, paths);
151
+ const { failures: reExportFailures, overlapSkipped } = await reExportAppliedPatches(session, paths, waitLockSeconds);
134
152
  if (reExportFailures.length > 0) {
135
153
  for (const f of reExportFailures) {
136
154
  error(` ${f.filename}: ${f.error}`);
@@ -157,21 +175,21 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
157
175
  await stampPatchVersions(paths.patches, appliedFilenames, session.toVersion, session.toProduct);
158
176
  }
159
177
  // Stamp every Furnace override's `baseVersion` to match the rebased
160
- // Firefox source version. Before this stamp, a successful source bump left
161
- // overrides in a doctor-failing drift state (each override still
162
- // claimed the pre-rebase source as its baseline) and every subsequent
163
- // `fireforge doctor` failed `Furnace component validation`. The
164
- // stamp is unconditional per the helper's contract: rebase already
165
- // succeeded on the patch side, so the operator is committing to the
166
- // new source baseline; per-component health checking stays with
178
+ // Firefox source version. Without it, a successful source bump leaves
179
+ // overrides in a doctor-failing drift state — each still claiming the
180
+ // pre-rebase source as its baseline — and every subsequent
181
+ // `fireforge doctor` fails `Furnace component validation`. The stamp is
182
+ // unconditional per the helper's contract: rebase already succeeded on
183
+ // the patch side, so the operator is committing to the new source
184
+ // baseline; per-component health checking stays with
167
185
  // `fireforge furnace validate` / `doctor --repair-furnace`.
168
186
  //
169
- // "Unconditional" means unconditional on component health, not on the value:
170
- // `stampFurnaceOverrideBaseVersions` assigns and persists whatever string it
171
- // is handed, so a session whose `toVersion` is not a real Firefox version
172
- // would rewrite every override's baseline to that value. `isValidSession`
173
- // now rejects such a session at read time; this refuses to persist it even
174
- // if a future caller reaches here another way.
187
+ // "Unconditional" means unconditional on component health, not on the
188
+ // value: `stampFurnaceOverrideBaseVersions` assigns and persists whatever
189
+ // string it is handed, so a session whose `toVersion` is not a real
190
+ // Firefox version would rewrite every override's baseline to that value.
191
+ // `isValidSession` rejects such a session at read time; this refuses to
192
+ // persist it even if a future caller reaches here another way.
175
193
  if (!isValidFirefoxVersion(session.toVersion)) {
176
194
  throw new RebaseError(`Refusing to stamp Furnace override baseVersion(s): the rebase session's target version ` +
177
195
  `("${session.toVersion}") is not a valid Firefox version.`);
@@ -201,7 +219,7 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
201
219
  success(`All patches re-exported with sourceVersion=${session.toVersion}`);
202
220
  outro('Rebase complete!');
203
221
  }
204
- async function reExportAppliedPatches(session, paths) {
222
+ async function reExportAppliedPatches(session, paths, waitLockSeconds) {
205
223
  const failures = [];
206
224
  const overlapSkipped = [];
207
225
  const manifest = await loadPatchesManifest(paths.patches);
@@ -253,7 +271,10 @@ async function reExportAppliedPatches(session, paths) {
253
271
  // Rebase sessions are serialized against each other by
254
272
  // `rebase-session.ts`, so this lock is only defending against other
255
273
  // command classes, not peer rebases.
256
- await withPatchDirectoryLock(paths.patches, () => updatePatch(patchPath, diffContent));
274
+ await withPatchDirectoryLock(paths.patches, () => updatePatch(patchPath, diffContent), {
275
+ waitLockSeconds,
276
+ command: 'rebase',
277
+ });
257
278
  }
258
279
  }
259
280
  catch (err) {
@@ -34,7 +34,7 @@ export function printSummary(session) {
34
34
  info(`Source Rebase Summary: ${from} → ${to}`);
35
35
  info('='.repeat(55));
36
36
  for (const patch of session.patches) {
37
- const label = statusLabel(patch.status, patch.fuzzFactor);
37
+ const label = statusLabel(patch.status, patch.status === 'applied-fuzz' ? patch.fuzzFactor : undefined);
38
38
  info(` ${patch.filename} ${'·'.repeat(Math.max(1, 45 - patch.filename.length - label.length))} ${label}`);
39
39
  }
40
40
  const clean = session.patches.filter((p) => p.status === 'applied-clean').length;
@@ -2,7 +2,7 @@
2
2
  import { join } from 'node:path';
3
3
  import { Command } from 'commander';
4
4
  import { getProjectPaths } from '../core/config.js';
5
- import { registerFile } from '../core/manifest-rules.js';
5
+ import { registerFile } from '../core/moz-manifest-rules.js';
6
6
  import { InvalidArgumentError } from '../errors/base.js';
7
7
  import { pathExists } from '../utils/fs.js';
8
8
  import { info, intro, outro, success, warn } from '../utils/logger.js';
@@ -48,12 +48,10 @@ export async function registerCommand(projectRoot, filePath, options = {}) {
48
48
  ...(options.createManifest !== undefined ? { createManifest: options.createManifest } : {}),
49
49
  });
50
50
  if (options.dryRun) {
51
- // 2026-04-21 eval (Finding #8): dry-run always said "Would
52
- // register" even when the rule's idempotency check already knew
53
- // the entry was present, so automation read the plan as "work to
54
- // do" and the following real run then reported "Already
55
- // registered". Surface the idempotency decision in dry-run too so
56
- // the plan mirrors the real command's outcome.
51
+ // Dry-run must surface the idempotency decision too. Always saying
52
+ // "Would register" — even when the rule already knows the entry is
53
+ // present — makes automation read the plan as "work to do" while the
54
+ // following real run reports "Already registered".
57
55
  if (result.skipped) {
58
56
  info(`[dry-run] Already registered: ${engineRelativePath} in ${result.manifest}`);
59
57
  outro('Dry run complete');
@@ -2,6 +2,7 @@
2
2
  import { confirm } from '@clack/prompts';
3
3
  import { Command } from 'commander';
4
4
  import { getProjectPaths } from '../core/config.js';
5
+ import { stdioIsInteractive } from '../core/destructive.js';
5
6
  import { assertEngineGitReady } from '../core/engine-precondition.js';
6
7
  import { clearAppliedFurnaceState } from '../core/furnace-config.js';
7
8
  import { hasChanges, resetChanges } from '../core/git.js';
@@ -40,7 +41,7 @@ export async function resetCommand(projectRoot, options) {
40
41
  // Confirm reset unless --yes is specified
41
42
  if (!options.yes) {
42
43
  // Check for non-interactive mode
43
- const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
44
+ const isInteractive = stdioIsInteractive();
44
45
  if (!isInteractive) {
45
46
  throw new InvalidArgumentError('Interactive confirmation not available. Use --yes flag to reset without confirmation.', 'Use: fireforge reset --yes');
46
47
  }
@@ -5,23 +5,26 @@ import type { CommandContext } from '../types/cli.js';
5
5
  */
6
6
  export interface ResolveCommandOptions {
7
7
  /**
8
- * Skip the interactive "Have you finished fixing the files?"
9
- * confirmation prompt and treat the resolution as complete.
8
+ * Skip the interactive "Have you finished fixing the files?" confirmation
9
+ * prompt and treat the resolution as complete.
10
10
  *
11
- * Motivating case (2026-04-21 eval, Finding #18): a scripted or
12
- * CI-assisted recovery flow that has already completed the manual
13
- * merge step cannot advance through `fireforge resolve` because the
14
- * TTY guard refuses non-interactive invocations outright. `--yes`
15
- * is the explicit opt-in for those flows: the operator is asserting
16
- * they have already done the merge, and the command proceeds
17
- * straight to the patch-refresh + state-clear path.
11
+ * A scripted or CI-assisted recovery flow that has already completed the
12
+ * manual merge step cannot otherwise advance through `fireforge resolve`,
13
+ * because the TTY guard refuses non-interactive invocations outright.
14
+ * `--yes` is the explicit opt-in: the operator asserts the merge is done,
15
+ * and the command proceeds straight to the patch-refresh + state-clear
16
+ * path.
18
17
  *
19
- * The guard without `--yes` is preserved — running `resolve` with
20
- * no TTY and no `--yes` still refuses so an accidental pipe-into
21
- * invocation doesn't silently commit whatever the engine happens
22
- * to contain.
18
+ * The guard without `--yes` is preserved — running `resolve` with no TTY
19
+ * and no `--yes` still refuses, so an accidental pipe-into invocation does
20
+ * not silently commit whatever the engine happens to contain.
23
21
  */
24
22
  yes?: boolean;
23
+ /**
24
+ * Seconds to wait for the patch directory lock. `resolve` rewrites a patch
25
+ * body and its metadata under that lock.
26
+ */
27
+ waitLock?: number | boolean;
25
28
  }
26
29
  /**
27
30
  * Runs the resolve command to fix broken patches.
@@ -3,6 +3,7 @@ import { join } from 'node:path';
3
3
  import { confirm } from '@clack/prompts';
4
4
  import { Command } from 'commander';
5
5
  import { getProjectPaths, loadConfig, loadState, updateState } from '../core/config.js';
6
+ import { stdioIsInteractive } from '../core/destructive.js';
6
7
  import { assertEngineGitReady } from '../core/engine-precondition.js';
7
8
  import { getStagedDiffForFiles } from '../core/git-diff.js';
8
9
  import { stageFiles, unstageFiles } from '../core/git-file-ops.js';
@@ -14,6 +15,7 @@ import { GeneralError, ResolutionError } from '../errors/base.js';
14
15
  import { toError } from '../utils/errors.js';
15
16
  import { pathExists } from '../utils/fs.js';
16
17
  import { error as logError, info, intro, isCancel, outro, spinner, success, } from '../utils/logger.js';
18
+ import { addWaitLockOption, resolveWaitLockSeconds } from '../utils/options.js';
17
19
  /**
18
20
  * Runs the resolve command to fix broken patches.
19
21
  * @param projectRoot - Root directory of the project
@@ -31,13 +33,12 @@ export async function resolveCommand(projectRoot, options = {}) {
31
33
  const { patchFilename } = state.pendingResolution;
32
34
  info(`Resolving conflict for patch: ${patchFilename}`);
33
35
  await assertEngineGitReady(paths.engine);
34
- // Non-interactive mode requires an explicit `--yes` to proceed: the
35
- // operator is asserting the manual merge is complete and the
36
- // refreshed diff is the one to record. Without `--yes`, an accidental
37
- // pipe / CI shell could otherwise commit whatever the engine
38
- // currently contains. 2026-04-21 eval (Finding #18): a scripted
39
- // recovery flow was dead-ended by the unconditional TTY refusal.
40
- if (!(process.stdin.isTTY && process.stdout.isTTY) && !options.yes) {
36
+ // Non-interactive mode requires an explicit `--yes`: the operator is
37
+ // asserting the manual merge is complete and the refreshed diff is the one
38
+ // to record. Without it, an accidental pipe or CI shell could commit
39
+ // whatever the engine currently contains — but an unconditional TTY
40
+ // refusal dead-ends a scripted recovery flow.
41
+ if (!stdioIsInteractive() && !options.yes) {
41
42
  throw new GeneralError('Cannot run "fireforge resolve" in non-interactive mode. Use a terminal with TTY support, or pass "--yes" to skip the interactive confirmation once the manual merge is complete.');
42
43
  }
43
44
  if (!options.yes) {
@@ -106,35 +107,31 @@ export async function resolveCommand(projectRoot, options = {}) {
106
107
  return;
107
108
  }
108
109
  // Atomically write the patch body and the metadata update under the
109
- // shared patch-directory lock. Replaces the previous lock-free
110
- // sequence of updatePatch + updatePatchMetadata, which a concurrent
111
- // import / export / re-export / patch reorder / patch compact could
112
- // interleave with and leave the manifest disagreeing with the
113
- // freshly-written patch body.
110
+ // shared patch-directory lock. A lock-free updatePatch +
111
+ // updatePatchMetadata sequence can be interleaved by a concurrent
112
+ // import / export / re-export / patch reorder / patch compact, leaving
113
+ // the manifest disagreeing with the freshly-written patch body.
114
114
  //
115
- // Always recompute `filesAffected` from the diff content itself. The
116
- // eval finding #16 scenario: the user's manual fix removed every
117
- // hunk for one file while the file still existed on disk, so the
118
- // pre-0.16.0 gate of "update filesAffected only when files were
119
- // deleted from disk" left the manifest claiming a file the patch
120
- // body no longer targeted. The next `fireforge import` then failed
121
- // the patch-manifest consistency check even though resolve reported
122
- // success. `extractAffectedFiles` already owns the canonical
123
- // "parse a diff, return its target paths" logic used by export and
124
- // consistency — using it here keeps resolve in agreement with every
125
- // other writer.
115
+ // Always recompute `filesAffected` from the diff content itself. When a
116
+ // manual fix removes every hunk for one file while the file still exists
117
+ // on disk, a gate of "update filesAffected only when files were deleted
118
+ // from disk" leaves the manifest claiming a file the patch body no longer
119
+ // targets, and the next `fireforge import` fails the patch-manifest
120
+ // consistency check even though resolve reported success.
121
+ // `extractAffectedFiles` already owns the canonical "parse a diff, return
122
+ // its target paths" logic used by export and consistency, so using it
123
+ // here keeps resolve in agreement with every other writer.
126
124
  const diffFilesAffected = extractAffectedFiles(diffContent);
127
125
  const config = await loadConfig(projectRoot);
128
126
  await updatePatchAndMetadata(paths.patches, patchFilename, diffContent, {
129
127
  filesAffected: diffFilesAffected,
130
128
  ...buildPatchSourceMetadata(config.firefox),
131
- });
132
- // Cleanup: Clear pendingResolution from state.json transactionally so
133
- // we don't clobber concurrent updates to unrelated keys (e.g. another
134
- // command writing buildMode or baseCommit between our loadState and
135
- // saveState). The updater function runs inside the state-file lock
136
- // with the freshest on-disk state, so only pendingResolution is
137
- // affected.
129
+ }, undefined, undefined, { waitLockSeconds: resolveWaitLockSeconds(options.waitLock), command: 'resolve' });
130
+ // Clear pendingResolution from state.json transactionally so a
131
+ // concurrent update to unrelated keys (another command writing buildMode
132
+ // or baseCommit between our load and save) is not clobbered. The updater
133
+ // runs inside the state-file lock with the freshest on-disk state, so
134
+ // only pendingResolution is affected.
138
135
  await updateState(projectRoot, (current) => {
139
136
  const next = { ...current };
140
137
  delete next.pendingResolution;
@@ -153,11 +150,11 @@ export async function resolveCommand(projectRoot, options = {}) {
153
150
  }
154
151
  /** Registers the resolve command on the CLI program. */
155
152
  export function registerResolve(program, { getProjectRoot, withErrorHandling }) {
156
- program
153
+ const command = program
157
154
  .command('resolve')
158
155
  .description('Update a broken patch with manual fixes (then run "fireforge import" to resume the queue)')
159
- .option('-y, --yes', 'Skip the interactive confirmation prompt. Use for non-interactive automation flows (CI, scripted recovery) after the manual merge is complete.')
160
- .action(withErrorHandling(async (options) => {
156
+ .option('-y, --yes', 'Skip the interactive confirmation prompt. Use for non-interactive automation flows (CI, scripted recovery) after the manual merge is complete.');
157
+ addWaitLockOption(command).action(withErrorHandling(async (options) => {
161
158
  await resolveCommand(getProjectRoot(), options);
162
159
  }));
163
160
  }