@hominis/fireforge 0.43.1 → 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 +126 -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 +17 -27
  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
@@ -10,14 +10,19 @@ const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
10
10
  const DEFAULT_LOCK_POLL_MS = 50;
11
11
  const DEFAULT_STALE_LOCK_MS = 5 * 60_000;
12
12
  /**
13
- * Minimum interval between stale-lock probes while a waiter polls. The probe
14
- * used to run exactly once per waiter: if the holder died *after* that single
15
- * probe, the waiter polled a permanently dead lock until `timeoutMs` — for
16
- * the build lock that meant up to 24 hours (see `withBuildLock` in mach.ts).
17
- * Re-probing on a small interval bounds that hang to seconds while keeping
18
- * the per-poll cost negligible (one stat + one small read every ~5 s).
13
+ * Minimum interval between stale-lock probes while a waiter polls. Probing
14
+ * exactly once per waiter means a holder that dies *after* that probe leaves
15
+ * the waiter polling a permanently dead lock until `timeoutMs` — up to 24
16
+ * hours for the build lock (see `withBuildLock` in mach.ts). Re-probing on a
17
+ * small interval bounds that to seconds while keeping the per-poll cost
18
+ * negligible: one stat plus one small read every ~5 s.
19
19
  */
20
20
  const STALE_REPROBE_INTERVAL_MS = 5_000;
21
+ /**
22
+ * Default interval between wait probes (holder + queue) when a caller wants
23
+ * deadline extension but no progress line of its own.
24
+ */
25
+ const WAIT_PROBE_INTERVAL_MS = 5_000;
21
26
  function sleep(ms) {
22
27
  return new Promise((resolve) => {
23
28
  setTimeout(resolve, ms);
@@ -41,9 +46,9 @@ export const LOCK_PID_FILE = 'pid';
41
46
  * missing or the PID does not parse as a finite integer (caller falls back
42
47
  * to the age-only staleness heuristic).
43
48
  *
44
- * File format note: external readers (`doctor-furnace.ts`, older FireForge
45
- * releases) do `parseInt(content.trim(), 10)`, which parses the leading
46
- * digits of a multi-line file — so adding the token line stays compatible.
49
+ * File format note: external readers do `parseInt(content.trim(), 10)`,
50
+ * which parses the leading digits of a multi-line file — so the token line
51
+ * stays compatible.
47
52
  */
48
53
  async function readLockOwner(lockPath) {
49
54
  try {
@@ -150,37 +155,34 @@ async function registerWaiter(lockPath, startedAt) {
150
155
  };
151
156
  }
152
157
  /**
153
- * Reads the contended lock's owner and reports wait progress to the caller's
154
- * `onWaitProgress` callback. Failure to read the owner degrades to
155
- * `holder: undefined` — progress reporting must never break the wait loop.
158
+ * Reads the contended lock's owner and queue in one go. One probe per cycle
159
+ * serves both the progress line and the deadline extension, so the two can
160
+ * never disagree about the position they saw. Failure to read the owner
161
+ * degrades to `holder: undefined` — a diagnostic must never break the wait
162
+ * loop.
156
163
  */
157
- async function reportWaitProgress(lockPath, waitedMs, timeoutMs, startedAt, onWaitProgress) {
164
+ async function probeWaitState(lockPath, startedAt) {
158
165
  const owner = await readLockOwner(lockPath);
159
166
  const holder = owner.present
160
167
  ? { pid: owner.pid, alive: isProcessAlive(owner.pid), metadata: owner.metadata }
161
168
  : undefined;
162
- onWaitProgress({
163
- waitedMs,
164
- timeoutMs,
165
- holder,
166
- queue: await readLockQueue(lockPath, startedAt),
167
- });
169
+ return { holder, queue: await readLockQueue(lockPath, startedAt) };
168
170
  }
169
171
  /**
170
172
  * Reaps a lock directory believed to be stale, safely against concurrent
171
173
  * reapers and a concurrent legitimate re-acquisition.
172
174
  *
173
- * The naive stat → read-PID → `rm` sequence had a TOCTOU hole: two waiters
174
- * could both observe the dead owner, one would remove the stale dir and
175
- * `mkdir` its own lock, and the other's already-decided `rm` then deleted
176
- * the *fresh* lock — two processes inside the critical section at once.
175
+ * A naive stat → read-PID → `rm` sequence has a TOCTOU hole: two waiters
176
+ * both observe the dead owner, one removes the stale dir and `mkdir`s its
177
+ * own lock, and the other's already-decided `rm` then deletes the *fresh*
178
+ * lock — two processes inside the critical section at once.
177
179
  *
178
180
  * Rename-aside closes it: `rename()` is atomic, so exactly one reaper wins
179
- * ownership of the directory (the loser gets ENOENT and simply re-polls),
180
- * and nothing ever deletes the live path directly. After winning the rename
181
- * we re-read the owner from the renamed dir — if it is now a *live* process
182
- * (the lock was released and re-acquired between our staleness read and the
183
- * rename), we rename it back instead of reaping.
181
+ * ownership of the directory (the loser gets ENOENT and re-polls), and
182
+ * nothing ever deletes the live path directly. After winning the rename the
183
+ * owner is re-read from the renamed dir — if it is now a *live* process (the
184
+ * lock was released and re-acquired between the staleness read and the
185
+ * rename), it is renamed back instead of reaped.
184
186
  *
185
187
  * @returns true when the caller should immediately retry `mkdir` (the lock
186
188
  * path is now free or was freed by someone else), false when the lock is
@@ -236,15 +238,12 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
236
238
  try {
237
239
  const lockStat = await stat(lockPath);
238
240
  const ageMs = Date.now() - lockStat.mtimeMs;
239
- // Check PID FIRST, independent of age. Before this ordering change the
240
- // function age-gated everything: a lock younger than `staleMs` (default
241
- // 5 minutes) was never removed even when its PID file pointed at a
242
- // process that had already exited. That's the exact situation an
243
- // operator lands in after SIGINT'ing `furnace preview` — the signal
244
- // handler calls `process.exit`, `withFileLock`'s release never runs,
245
- // and the next command has to either wait 5 minutes for the staleness
246
- // gate or manually remove the lock directory. With the PID-first check,
247
- // an explicitly-dead owner unblocks immediately.
241
+ // Check PID FIRST, independent of age. Age-gating everything means a
242
+ // lock younger than `staleMs` (default 5 minutes) is never removed even
243
+ // when its PID file points at a process that has already exited — the
244
+ // situation after SIGINT'ing `furnace preview`, where the signal handler
245
+ // calls `process.exit` and `withFileLock`'s release never runs. With the
246
+ // PID-first check an explicitly-dead owner unblocks immediately.
248
247
  const owner = await readLockOwner(lockPath);
249
248
  if (owner.present) {
250
249
  if (!isProcessAlive(owner.pid)) {
@@ -276,16 +275,15 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
276
275
  }
277
276
  /**
278
277
  * Releases a lock previously acquired by this process, verifying ownership
279
- * first. The unconditional `finally { rm }` this replaces would remove
280
- * *whichever* lock directory was present — if a stale-lock reaper had (in a
281
- * pathological race) replaced our lock, we deleted the new owner's lock and
282
- * compounded the double-acquisition. When the PID file no longer names us,
283
- * warn and leave the directory alone.
278
+ * first. An unconditional `finally { rm }` would remove *whichever* lock
279
+ * directory is present — if a stale-lock reaper had (in a pathological race)
280
+ * replaced our lock, that deletes the new owner's lock and compounds the
281
+ * double-acquisition. When the PID file no longer names us, warn and leave
282
+ * the directory alone.
284
283
  *
285
284
  * `ownerFileWritten` is false when our own owner-file write failed at
286
- * acquisition (non-fatal there); ownership then cannot be verified and we
287
- * fall back to unconditional removal — identical to the historical behavior
288
- * and strictly better than leaking the lock.
285
+ * acquisition (non-fatal there); ownership then cannot be verified and the
286
+ * removal is unconditional, which is strictly better than leaking the lock.
289
287
  */
290
288
  async function releaseLock(lockPath, ownerToken, ownerFileWritten) {
291
289
  if (ownerFileWritten) {
@@ -299,31 +297,130 @@ async function releaseLock(lockPath, ownerToken, ownerFileWritten) {
299
297
  }
300
298
  await rm(lockPath, { recursive: true, force: true });
301
299
  }
300
+ /**
301
+ * Removes a lock directory only if this process still owns it, for callers
302
+ * that hold no acquisition token.
303
+ *
304
+ * The ownership half of {@link releaseLock}, minus the token check — a
305
+ * signal-time sweeper knows which lock paths its own operations opened, but
306
+ * not the per-acquisition UUID those acquisitions minted. The PID check is
307
+ * still the one that matters: it stops the sweeper deleting a lock a
308
+ * DIFFERENT process acquired in the window between our operation dying and
309
+ * the sweep running.
310
+ *
311
+ * Never throws: callers are shutdown paths where a slow or failing I/O must
312
+ * not prevent the process from exiting. Returns whether the directory was
313
+ * removed so callers can log accurately.
314
+ *
315
+ * @param lockPath - Lock directory to remove
316
+ * @returns True when the lock was removed, false when it was left in place
317
+ */
318
+ export async function releaseLockIfOwned(lockPath) {
319
+ try {
320
+ const owner = await readLockOwner(lockPath);
321
+ if (owner.present && owner.pid !== process.pid) {
322
+ return false;
323
+ }
324
+ await rm(lockPath, { recursive: true, force: true });
325
+ return true;
326
+ }
327
+ catch {
328
+ return false;
329
+ }
330
+ }
302
331
  /**
303
332
  * Builds the contention refusal thrown when the wait budget expires.
304
333
  *
305
334
  * Typed as {@link LockContentionError} (a `FireForgeError`) so the CLI
306
335
  * boundary prints the refusal as one clean line instead of an "Unexpected
307
- * error" stack. Callers supplying `onTimeoutMessage` still get
308
- * the holder identification appended — the reason-first copy stays theirs,
309
- * the "who holds it" is ours.
336
+ * error" stack. Callers supplying `onTimeoutMessage` still get the holder
337
+ * identification appended — the reason-first copy stays theirs, the "who
338
+ * holds it" is ours.
310
339
  */
311
- async function buildLockTimeoutError(lockPath, onTimeoutMessage, cause) {
340
+ async function buildLockTimeoutError(lockPath, onTimeoutMessage, cause, queue) {
312
341
  const owner = await readLockOwner(lockPath);
313
342
  const liveHolder = owner.present && isProcessAlive(owner.pid) ? owner : undefined;
314
343
  const details = liveHolder !== undefined && liveHolder.metadata.length > 0
315
344
  ? ` (${liveHolder.metadata.join(', ')})`
316
345
  : '';
346
+ const position = formatQueuePositionReached(queue);
317
347
  if (onTimeoutMessage !== undefined) {
318
348
  const identifiedHolder = liveHolder !== undefined
319
349
  ? ` The lock is held by PID ${String(liveHolder.pid)}${details}.`
320
350
  : '';
321
- return new LockContentionError(`${onTimeoutMessage}${identifiedHolder}`, cause);
351
+ return new LockContentionError(`${onTimeoutMessage}${identifiedHolder}${position}`, cause);
322
352
  }
323
353
  const holderHint = liveHolder !== undefined
324
354
  ? ` It is held by running process ${String(liveHolder.pid)}${details} — wait for it to finish, or stop it and retry.`
325
355
  : ' If no other FireForge process is running, remove the lock directory and retry.';
326
- return new LockContentionError(`Timed out waiting for file lock ${lockPath}.${holderHint}`, cause);
356
+ return new LockContentionError(`Timed out waiting for file lock ${lockPath}.${holderHint}${position}`, cause);
357
+ }
358
+ /**
359
+ * States the queue position the expired wait actually reached.
360
+ *
361
+ * Silence here is what made the worst outcome invisible: a wait that expires
362
+ * one position from the head has paid the whole budget for nothing, and the
363
+ * operator cannot tell that from a queue that never moved. Naming the
364
+ * position turns "raise the budget" into an informed decision instead of a
365
+ * guess. Omitted when no queue was observed — an empty registry is not
366
+ * evidence of an empty queue.
367
+ */
368
+ function formatQueuePositionReached(queue) {
369
+ if (queue === undefined || queue.depth === 0)
370
+ return '';
371
+ if (queue.ahead === 0) {
372
+ return ` You were next in a queue of ${String(queue.depth)} when the wait expired — a larger --wait-lock would very likely have got a turn.`;
373
+ }
374
+ return ` You were still ${String(queue.ahead)} from the head of a queue of ${String(queue.depth)} when the wait expired; re-run with a larger --wait-lock.`;
375
+ }
376
+ /**
377
+ * Runs one wait probe: reads holder and queue, and reports progress when the
378
+ * caller asked for it. Split out of the wait loop so the loop stays a
379
+ * readable state machine.
380
+ */
381
+ async function runWaitProbe(lockPath, startedAt, timeoutMs, options) {
382
+ const { holder, queue } = await probeWaitState(lockPath, startedAt);
383
+ options.onWaitProgress?.({
384
+ waitedMs: Date.now() - startedAt,
385
+ timeoutMs,
386
+ holder,
387
+ queue,
388
+ });
389
+ return { queue };
390
+ }
391
+ /**
392
+ * Lock directories this process currently holds.
393
+ *
394
+ * `withFileLock`'s `finally` never runs across a `process.exit`, so a
395
+ * SIGTERM'd command leaves its lock on disk. The next contender reclaims it
396
+ * immediately (the PID-first staleness check sees a dead owner), but in the
397
+ * window between, `fireforge status --lock` reports the lock held by a
398
+ * process that is not running — which is exactly the question an operator
399
+ * asks after a kill. The signal sweep closes that window.
400
+ *
401
+ * Paths only: the per-acquisition token lives in `withFileLock`'s frame and
402
+ * a sweeper cannot see it. {@link releaseLockIfOwned} checks the PID, which
403
+ * is the check that matters — it stops the sweep deleting a lock a
404
+ * DIFFERENT process acquired in the meantime.
405
+ */
406
+ const heldLocks = new Set();
407
+ /**
408
+ * Releases every lock this process still holds, for the signal path.
409
+ *
410
+ * Never throws and never rejects: a shutdown path must not be able to keep
411
+ * the process alive. Returns the paths actually removed so the caller can
412
+ * report accurately.
413
+ */
414
+ export async function forceReleaseHeldLocksForSignal() {
415
+ const paths = [...heldLocks];
416
+ const removed = [];
417
+ for (const lockPath of paths) {
418
+ heldLocks.delete(lockPath);
419
+ if (await releaseLockIfOwned(lockPath)) {
420
+ removed.push(lockPath);
421
+ }
422
+ }
423
+ return removed;
327
424
  }
328
425
  /** Runs an async operation while holding a directory lock, with stale-lock recovery. */
329
426
  export async function withFileLock(lockPath, operation, options = {}) {
@@ -332,12 +429,24 @@ export async function withFileLock(lockPath, operation, options = {}) {
332
429
  const staleMs = options.staleMs ?? DEFAULT_STALE_LOCK_MS;
333
430
  const staleReprobeMs = options.staleReprobeMs ?? STALE_REPROBE_INTERVAL_MS;
334
431
  const startedAt = Date.now();
335
- const deadline = startedAt + timeoutMs;
432
+ const extend = options.extendWhileAdvancing;
433
+ const hardDeadline = extend !== undefined ? startedAt + extend.maxWaitMs : undefined;
434
+ let deadline = startedAt + timeoutMs;
336
435
  let lastStaleProbeAt;
337
436
  let currentPollMs = pollMs;
338
- let lastProgressAt = startedAt;
437
+ let lastProbeAt = startedAt;
438
+ /** Best (lowest) queue position seen so far; used to detect an advance. */
439
+ let bestAhead;
440
+ /** Last observed queue state, so the timeout refusal can name the position reached. */
441
+ let lastQueue;
339
442
  /** Set on first contention so an uncontended acquire costs nothing. */
340
443
  let deregisterWaiter;
444
+ // One cadence drives both the progress line and the deadline extension.
445
+ // Either feature alone is enough to want the probe; neither is worth a
446
+ // second timer.
447
+ const probeIntervalMs = options.waitProgressMs ?? WAIT_PROBE_INTERVAL_MS;
448
+ const probeWanted = (options.onWaitProgress !== undefined && options.waitProgressMs !== undefined) ||
449
+ extend !== undefined;
341
450
  await ensureDir(dirname(lockPath));
342
451
  try {
343
452
  for (;;) {
@@ -360,13 +469,39 @@ export async function withFileLock(lockPath, operation, options = {}) {
360
469
  }
361
470
  }
362
471
  if (Date.now() >= deadline) {
363
- throw await buildLockTimeoutError(lockPath, options.onTimeoutMessage, error);
472
+ throw await buildLockTimeoutError(lockPath, options.onTimeoutMessage, error, lastQueue);
364
473
  }
365
- if (options.onWaitProgress !== undefined &&
366
- options.waitProgressMs !== undefined &&
367
- Date.now() - lastProgressAt >= options.waitProgressMs) {
368
- lastProgressAt = Date.now();
369
- await reportWaitProgress(lockPath, Date.now() - startedAt, timeoutMs, startedAt, options.onWaitProgress);
474
+ if (probeWanted && Date.now() - lastProbeAt >= probeIntervalMs) {
475
+ lastProbeAt = Date.now();
476
+ // Report the budget that is CURRENTLY in force, not the one
477
+ // originally requested: once an extension has been granted, the
478
+ // requested figure is stale, and a progress line that keeps
479
+ // quoting it reads as a timeout that does not fire.
480
+ const probe = await runWaitProbe(lockPath, startedAt, deadline - startedAt, options);
481
+ lastQueue = probe.queue;
482
+ // An advance — someone ahead of us took their turn and finished —
483
+ // buys a fresh budget, because the wait is working. Only a queue
484
+ // that stops moving starves, which is the case the timeout is for.
485
+ //
486
+ // The FIRST observation is a seed, never an advance. `bestAhead`
487
+ // used to start `undefined`, so the opening probe always satisfied
488
+ // this test and handed out a free full budget: a `--wait-lock 300`
489
+ // run outlived its 300 s even against a queue that never moved
490
+ // once, which is exactly the "it does not reliably give up after N
491
+ // seconds" a downstream fork reported.
492
+ if (hardDeadline !== undefined) {
493
+ if (bestAhead === undefined) {
494
+ bestAhead = probe.queue.ahead;
495
+ }
496
+ else if (probe.queue.ahead < bestAhead) {
497
+ bestAhead = probe.queue.ahead;
498
+ deadline = Math.min(Date.now() + timeoutMs, hardDeadline);
499
+ options.onWaitExtended?.({
500
+ ahead: probe.queue.ahead,
501
+ budgetMs: deadline - startedAt,
502
+ });
503
+ }
504
+ }
370
505
  }
371
506
  await sleep(currentPollMs);
372
507
  if (options.pollMaxMs !== undefined) {
@@ -396,10 +531,12 @@ export async function withFileLock(lockPath, operation, options = {}) {
396
531
  // Non-fatal: stale recovery falls back to age-only heuristic and release
397
532
  // falls back to unconditional removal.
398
533
  }
534
+ heldLocks.add(lockPath);
399
535
  try {
400
536
  return await operation();
401
537
  }
402
538
  finally {
539
+ heldLocks.delete(lockPath);
403
540
  await releaseLock(lockPath, ownerToken, ownerFileWritten);
404
541
  }
405
542
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { createHash, randomUUID } from 'node:crypto';
6
6
  import { createReadStream } from 'node:fs';
7
- import { rename } from 'node:fs/promises';
7
+ import { readdir, rename, stat } from 'node:fs/promises';
8
8
  import { join } from 'node:path';
9
9
  import { pipeline } from 'node:stream/promises';
10
10
  import { ChecksumMismatchError } from '../errors/download.js';
@@ -108,7 +108,6 @@ async function validateCachedArchive(archive, cacheDir, expectedSha256) {
108
108
  return false;
109
109
  }
110
110
  if (metadata.contentLength !== undefined) {
111
- const { stat } = await import('node:fs/promises');
112
111
  const archiveStats = await stat(tarballPath);
113
112
  if (archiveStats.size !== metadata.contentLength) {
114
113
  return false;
@@ -213,7 +212,6 @@ export async function invalidateArchiveCache(archive, cacheDir) {
213
212
  async function removeArchivePartFiles(archive, cacheDir) {
214
213
  const partPrefix = `${archive.filename}.part`;
215
214
  try {
216
- const { readdir } = await import('node:fs/promises');
217
215
  const entries = await readdir(cacheDir);
218
216
  await Promise.all(entries
219
217
  .filter((name) => name.startsWith(partPrefix))
@@ -5,7 +5,7 @@
5
5
  import { join } from 'node:path';
6
6
  import { ExtractionError } from '../errors/download.js';
7
7
  import { elapsedSince } from '../utils/elapsed.js';
8
- import { ensureDir, pathExists } from '../utils/fs.js';
8
+ import { ensureDir, pathExists, readText } from '../utils/fs.js';
9
9
  import { exec, execStream, executableExists } from '../utils/process.js';
10
10
  /**
11
11
  * Returns true when an archive member path could land outside the
@@ -81,11 +81,8 @@ export function findUnsafeArchiveLink(verboseLines) {
81
81
  * so a date-shaped owner name could shift the column boundary and hide an
82
82
  * absolute member name from the check. Each listing pass costs one full
83
83
  * decompression, so the two run concurrently and overlap almost perfectly
84
- * (they are CPU-bound on separate cores). Measured on a real Firefox ESR
85
- * 140.9 source tarball (601 MB, bsdtar 3.5.3, macOS): -tf 21.7 s, -tvf
86
- * 22.1 s, both concurrent 22 s wall, extraction itself 58 s (file-creation
87
- * syscalls dominate it, not decompression) — so the preflight adds ~22 s
88
- * (~38%) to this one-time extraction step, versus ~44 s if run
84
+ * (CPU-bound on separate cores): on a 601 MB Firefox ESR source tarball the
85
+ * preflight adds ~22 s to a ~58 s extraction, versus ~44 s if run
89
86
  * sequentially.
90
87
  */
91
88
  /**
@@ -116,13 +113,13 @@ function createLineScanner(onLine) {
116
113
  * Runs one tar listing pass, streaming lines through `checkLine` and
117
114
  * returning the first unsafe finding (or null) plus the exit code.
118
115
  *
119
- * Streaming matters for safety, not just memory: the buffered `exec`
120
- * collector silently truncates at 50 MB, and a full Firefox `-tvf` listing
121
- * already sits in the 40–50 MB range — so the buffered implementation could
122
- * scan only the head of the listing while reporting the archive safe. A
123
- * crafted archive could exploit exactly that by padding benign entries
124
- * first and hiding a traversal name or absolute link target past the cap.
125
- * With per-line streaming EVERY entry is scanned, with bounded memory.
116
+ * Streaming matters for safety, not just memory: a buffered `exec` collector
117
+ * silently truncates at 50 MB, and a full Firefox `-tvf` listing already
118
+ * sits in the 40–50 MB range — so a buffered implementation scans only the
119
+ * head of the listing while reporting the archive safe. A crafted archive
120
+ * could exploit exactly that by padding benign entries first and hiding a
121
+ * traversal name or absolute link target past the cap. Per-line streaming
122
+ * scans EVERY entry with bounded memory.
126
123
  */
127
124
  async function runListingScan(archivePath, tarFlag, checkLine) {
128
125
  // LC_ALL=C keeps the -tvf column format stable for the link parse.
@@ -213,7 +210,6 @@ export async function getFirefoxVersion(engineDir) {
213
210
  if (!(await pathExists(versionPath))) {
214
211
  return undefined;
215
212
  }
216
- const { readText } = await import('../utils/fs.js');
217
213
  const version = await readText(versionPath);
218
214
  return version.trim();
219
215
  }
@@ -27,11 +27,11 @@ export declare function getDownloadUrl(version: string, product?: FirefoxProduct
27
27
  export declare function getTarballFilename(version: string, product?: FirefoxProduct, candidate?: string): string;
28
28
  /**
29
29
  * Lifecycle phase reported by {@link downloadFirefoxSource}. The download
30
- * CLI command uses this to swap spinners between the bytes-on-the-wire
31
- * phase and the silent tar-xz decompression phase that follows — before
32
- * this, a single spinner stuck at "Downloading Firefox … 100%" covered
33
- * both phases, making the first-run setup look hung precisely when the
34
- * archive was already on disk and `tar` was the long pole.
30
+ * CLI command uses this to swap spinners between the bytes-on-the-wire phase
31
+ * and the silent tar-xz decompression phase that follows: a single spinner
32
+ * covering both sticks at "Downloading Firefox … 100%" and makes first-run
33
+ * setup look hung precisely when the archive is already on disk and `tar` is
34
+ * the long pole.
35
35
  */
36
36
  export type FirefoxSourcePhase = 'download' | 'extract';
37
37
  /** Callback fired at phase transitions during {@link downloadFirefoxSource}. */
@@ -59,6 +59,8 @@ export declare function sweepOrphanedEngineWorkDirs(destDir: string): Promise<st
59
59
  * @param onPhase - Optional callback fired when the function transitions
60
60
  * between phases (`'download'` → `'extract'`). Fires exactly once per
61
61
  * phase even if the cached archive path skips the wire entirely.
62
+ * @param expectedSha256 - Expected archive checksum; verified after download when supplied
63
+ * @param onPhaseProgress - Called with byte progress within the current phase
62
64
  * @param candidate - Optional release-candidate build directory (e.g.
63
65
  * "build2") resolving the archive under `candidates/` instead of
64
66
  * `releases/`.
@@ -6,8 +6,8 @@
6
6
  * {@link downloadFirefoxSource} orchestrator.
7
7
  */
8
8
  import { randomUUID } from 'node:crypto';
9
- import { rename } from 'node:fs/promises';
10
- import { join } from 'node:path';
9
+ import { readdir, rename } from 'node:fs/promises';
10
+ import { basename, dirname, join } from 'node:path';
11
11
  import { getNodeErrorCode, toError } from '../utils/errors.js';
12
12
  import { ensureDir, removeDir } from '../utils/fs.js';
13
13
  import { resolveArchive } from './firefox-archive.js';
@@ -72,8 +72,6 @@ function isLikelyArchiveCorruptionError(error) {
72
72
  * @returns names of the directories that were removed
73
73
  */
74
74
  export async function sweepOrphanedEngineWorkDirs(destDir) {
75
- const { readdir } = await import('node:fs/promises');
76
- const { basename, dirname } = await import('node:path');
77
75
  const parent = dirname(destDir);
78
76
  const enginePrefixTmp = `${basename(destDir)}.tmp-`;
79
77
  const enginePrefixReplacement = `${basename(destDir)}.replacement-`;
@@ -105,6 +103,8 @@ export async function sweepOrphanedEngineWorkDirs(destDir) {
105
103
  * @param onPhase - Optional callback fired when the function transitions
106
104
  * between phases (`'download'` → `'extract'`). Fires exactly once per
107
105
  * phase even if the cached archive path skips the wire entirely.
106
+ * @param expectedSha256 - Expected archive checksum; verified after download when supplied
107
+ * @param onPhaseProgress - Called with byte progress within the current phase
108
108
  * @param candidate - Optional release-candidate build directory (e.g.
109
109
  * "build2") resolving the archive under `candidates/` instead of
110
110
  * `releases/`.
@@ -136,7 +136,6 @@ export async function downloadFirefoxSource(version, product, destDir, cacheDir,
136
136
  }
137
137
  // Firefox source extracts to a subdirectory (e.g., firefox-140.0/)
138
138
  // Find it dynamically since ESR versions may have different naming
139
- const { readdir } = await import('node:fs/promises');
140
139
  const entries = await readdir(tempDir, { withFileTypes: true });
141
140
  const extractedSubdir = entries.find((entry) => entry.isDirectory() && entry.name.startsWith('firefox-'));
142
141
  if (extractedSubdir) {
@@ -3,14 +3,14 @@
3
3
  * `furnace-apply-helpers.ts` so the apply path and its dry-run mirror each
4
4
  * stay within the per-file line budget. Consumed only by that module.
5
5
  */
6
- import type { CustomComponentConfig, DryRunAction, StepError } from '../types/furnace.js';
6
+ import type { ComponentApplyContext, CustomComponentConfig, DryRunAction, StepError } from '../types/furnace.js';
7
7
  interface DirectoryEntry {
8
8
  isFile(): boolean;
9
9
  isSymbolicLink?(): boolean;
10
10
  name: string;
11
11
  }
12
12
  /** Computes the planned dry-run actions (and pre-flight step errors) for a custom component. */
13
- export declare function buildCustomDryRunActions(name: string, componentDir: string, engineDir: string, config: CustomComponentConfig, targetDir: string, entries: DirectoryEntry[], ftlDir: string): Promise<{
13
+ export declare function buildCustomDryRunActions(ctx: Pick<ComponentApplyContext, 'engineDir' | 'name' | 'componentDir' | 'ftlDir'>, config: CustomComponentConfig, targetDir: string, entries: DirectoryEntry[]): Promise<{
14
14
  actions: DryRunAction[];
15
15
  stepErrors: StepError[];
16
16
  }>;
@@ -18,7 +18,8 @@ function isRegularFile(entry) {
18
18
  return true;
19
19
  }
20
20
  /** Computes the planned dry-run actions (and pre-flight step errors) for a custom component. */
21
- export async function buildCustomDryRunActions(name, componentDir, engineDir, config, targetDir, entries, ftlDir) {
21
+ export async function buildCustomDryRunActions(ctx, config, targetDir, entries) {
22
+ const { engineDir, name, componentDir, ftlDir } = ctx;
22
23
  const actions = [];
23
24
  const stepErrors = [];
24
25
  for (const entry of entries) {
@@ -60,7 +61,7 @@ export async function buildCustomDryRunActions(name, componentDir, engineDir, co
60
61
  // A sharedFtl widget owns its strings via the shared bundle; surface the
61
62
  // removal of any dangling per-widget locale jar.mn entry so the dry-run
62
63
  // plan matches what apply will do (and explains the unblocked build).
63
- const pruneAction = await describeSharedFtlPrune(engineDir, name, ftlDir, config);
64
+ const pruneAction = await describeSharedFtlPrune(ctx, config);
64
65
  if (pruneAction) {
65
66
  actions.push(pruneAction);
66
67
  }
@@ -6,26 +6,24 @@
6
6
  * Every helper here degrades gracefully: if the locale jar.mn is missing or
7
7
  * the FTL tree is non-standard, apply logs an ADVISORY `stepError`
8
8
  * (`advisory: true`) rather than aborting the whole command. Advisory step
9
- * errors are reported as warnings and never trigger rollback — missing
10
- * jar.mn on a fork without a locale package should not block a working
11
- * `.mjs`/`.css` from shipping (the blocking/advisory split exists because
12
- * this contract used to be contradicted: any FTL step error rolled back
13
- * the whole apply).
9
+ * errors are reported as warnings and never trigger rollback — a missing
10
+ * jar.mn on a fork without a locale package must not block a working
11
+ * `.mjs`/`.css` from shipping.
14
12
  */
15
- import type { CustomComponentConfig, DryRunAction, StepError } from '../types/furnace.js';
13
+ import type { ComponentApplyContext, CustomComponentConfig, DryRunAction, StepError } from '../types/furnace.js';
16
14
  import { type RollbackJournal } from './furnace-rollback.js';
17
15
  /**
18
16
  * Apply-path wrapper around {@link pruneSharedFtlPerWidgetLocaleEntry} that
19
17
  * records the affected path / step error in the caller's collectors, mirroring
20
18
  * {@link applyCustomFtlFile}'s contract so the main apply helper stays terse.
21
19
  */
22
- export declare function applySharedFtlPrune(engineDir: string, name: string, ftlDir: string, config: CustomComponentConfig, affectedPaths: string[], stepErrors: StepError[], rollbackJournal?: RollbackJournal): Promise<void>;
20
+ export declare function applySharedFtlPrune(ctx: Pick<ComponentApplyContext, 'engineDir' | 'name' | 'ftlDir'>, config: CustomComponentConfig, affectedPaths: string[], stepErrors: StepError[], rollbackJournal?: RollbackJournal): Promise<void>;
23
21
  /**
24
22
  * Read-only dry-run describer for {@link pruneSharedFtlPerWidgetLocaleEntry}:
25
23
  * returns an action when a dangling per-widget locale entry exists for a
26
24
  * `sharedFtl` widget, else `undefined`.
27
25
  */
28
- export declare function describeSharedFtlPrune(engineDir: string, name: string, ftlDir: string, config: CustomComponentConfig): Promise<DryRunAction | undefined>;
26
+ export declare function describeSharedFtlPrune(ctx: Pick<ComponentApplyContext, 'engineDir' | 'name' | 'ftlDir'>, config: CustomComponentConfig): Promise<DryRunAction | undefined>;
29
27
  /**
30
28
  * Copies a component's `.ftl` into the FTL tree and registers the chrome URI
31
29
  * in the locale jar.mn.
@@ -34,7 +32,7 @@ export declare function describeSharedFtlPrune(engineDir: string, name: string,
34
32
  * stepErrors rather than thrown — a well-formed `.mjs`/`.css` must never be
35
33
  * blocked by a broken locale path.
36
34
  */
37
- export declare function applyCustomFtlFile(engineDir: string, name: string, componentDir: string, ftlDir: string, affectedPaths: string[], stepErrors: StepError[], rollbackJournal?: RollbackJournal): Promise<void>;
35
+ export declare function applyCustomFtlFile(ctx: Pick<ComponentApplyContext, 'engineDir' | 'name' | 'componentDir' | 'ftlDir'>, affectedPaths: string[], stepErrors: StepError[], rollbackJournal?: RollbackJournal): Promise<void>;
38
36
  /**
39
37
  * Returns a dry-run action for registering a locale jar.mn entry for the
40
38
  * `.ftl` that `applyCustomFtlFile` would write. `undefined` when the FTL
@@ -7,11 +7,9 @@
7
7
  * Every helper here degrades gracefully: if the locale jar.mn is missing or
8
8
  * the FTL tree is non-standard, apply logs an ADVISORY `stepError`
9
9
  * (`advisory: true`) rather than aborting the whole command. Advisory step
10
- * errors are reported as warnings and never trigger rollback — missing
11
- * jar.mn on a fork without a locale package should not block a working
12
- * `.mjs`/`.css` from shipping (the blocking/advisory split exists because
13
- * this contract used to be contradicted: any FTL step error rolled back
14
- * the whole apply).
10
+ * errors are reported as warnings and never trigger rollback — a missing
11
+ * jar.mn on a fork without a locale package must not block a working
12
+ * `.mjs`/`.css` from shipping.
15
13
  */
16
14
  import { join, relative } from 'node:path';
17
15
  import { toError } from '../utils/errors.js';
@@ -46,10 +44,9 @@ function resolveSharedFtlPruneTarget(name, ftlDir) {
46
44
  * A `localized: true` widget that opts into a feature-scoped `sharedFtl`
47
45
  * bundle (its strings live under `browser/...` and load via
48
46
  * `insertFTLIfNeeded`) must NOT carry a per-widget
49
- * `locale/@AB_CD@/<chromeSubPath>/<name>.ftl` line. Such a line — written by
50
- * an older FireForge before the sharedFtl apply guard, or by a layout
51
- * migration — points at a `.ftl` that does not exist, so `mach build` fails
52
- * hard (`Cannot find <chromeSubPath>/<name>.ftl`) and blocks every build.
47
+ * `locale/@AB_CD@/<chromeSubPath>/<name>.ftl` line. Such a line points at a
48
+ * `.ftl` that does not exist, so `mach build` fails hard (`Cannot find
49
+ * <chromeSubPath>/<name>.ftl`) and blocks every build.
53
50
  *
54
51
  * The pruned line is the per-widget toolkit entry only; the shared bundle's
55
52
  * own line (a different chrome sub-path / base name) is never matched, so
@@ -83,7 +80,8 @@ async function pruneSharedFtlPerWidgetLocaleEntry(engineDir, name, ftlDir, confi
83
80
  * records the affected path / step error in the caller's collectors, mirroring
84
81
  * {@link applyCustomFtlFile}'s contract so the main apply helper stays terse.
85
82
  */
86
- export async function applySharedFtlPrune(engineDir, name, ftlDir, config, affectedPaths, stepErrors, rollbackJournal) {
83
+ export async function applySharedFtlPrune(ctx, config, affectedPaths, stepErrors, rollbackJournal) {
84
+ const { engineDir, name, ftlDir } = ctx;
87
85
  try {
88
86
  const prunedPath = await pruneSharedFtlPerWidgetLocaleEntry(engineDir, name, ftlDir, config, rollbackJournal);
89
87
  if (prunedPath)
@@ -98,7 +96,8 @@ export async function applySharedFtlPrune(engineDir, name, ftlDir, config, affec
98
96
  * returns an action when a dangling per-widget locale entry exists for a
99
97
  * `sharedFtl` widget, else `undefined`.
100
98
  */
101
- export async function describeSharedFtlPrune(engineDir, name, ftlDir, config) {
99
+ export async function describeSharedFtlPrune(ctx, config) {
100
+ const { engineDir, name, ftlDir } = ctx;
102
101
  if (!config.sharedFtl)
103
102
  return undefined;
104
103
  const target = resolveSharedFtlPruneTarget(name, ftlDir);
@@ -123,7 +122,8 @@ export async function describeSharedFtlPrune(engineDir, name, ftlDir, config) {
123
122
  * stepErrors rather than thrown — a well-formed `.mjs`/`.css` must never be
124
123
  * blocked by a broken locale path.
125
124
  */
126
- export async function applyCustomFtlFile(engineDir, name, componentDir, ftlDir, affectedPaths, stepErrors, rollbackJournal) {
125
+ export async function applyCustomFtlFile(ctx, affectedPaths, stepErrors, rollbackJournal) {
126
+ const { engineDir, name, componentDir, ftlDir } = ctx;
127
127
  const ftlFile = `${name}.ftl`;
128
128
  const ftlSrc = join(componentDir, ftlFile);
129
129
  if (!(await pathExists(ftlSrc)))