@hominis/fireforge 0.45.0 → 0.46.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 (359) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/cli-usage-error.d.ts +46 -0
  5. package/dist/src/cli-usage-error.js +81 -0
  6. package/dist/src/cli.d.ts +5 -1
  7. package/dist/src/cli.js +20 -21
  8. package/dist/src/commands/bootstrap.js +1 -2
  9. package/dist/src/commands/build.js +15 -6
  10. package/dist/src/commands/destructive-decision.d.ts +19 -0
  11. package/dist/src/commands/destructive-decision.js +31 -0
  12. package/dist/src/commands/doctor-check-core.d.ts +0 -6
  13. package/dist/src/commands/doctor-check-core.js +0 -8
  14. package/dist/src/commands/doctor-orphaned-harness.d.ts +0 -6
  15. package/dist/src/commands/doctor-orphaned-harness.js +4 -12
  16. package/dist/src/commands/doctor-post-rebase-audit.js +1 -1
  17. package/dist/src/commands/doctor.d.ts +2 -0
  18. package/dist/src/commands/doctor.js +7 -8
  19. package/dist/src/commands/download.js +35 -24
  20. package/dist/src/commands/export-all.js +21 -4
  21. package/dist/src/commands/export-flow.js +15 -45
  22. package/dist/src/commands/export-placement-conflicts.js +3 -3
  23. package/dist/src/commands/export-placement-gate.js +2 -8
  24. package/dist/src/commands/export-placement-policy.js +3 -5
  25. package/dist/src/commands/export-shared.d.ts +42 -23
  26. package/dist/src/commands/export-shared.js +4 -19
  27. package/dist/src/commands/export.js +10 -1
  28. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  29. package/dist/src/commands/furnace/authoring-config.js +19 -0
  30. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  31. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  32. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  33. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  34. package/dist/src/commands/furnace/create-templates.js +2 -1
  35. package/dist/src/commands/furnace/create.js +25 -19
  36. package/dist/src/commands/furnace/deploy.js +11 -2
  37. package/dist/src/commands/furnace/init.js +2 -1
  38. package/dist/src/commands/furnace/override.js +28 -19
  39. package/dist/src/commands/furnace/refresh.js +9 -1
  40. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  41. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  42. package/dist/src/commands/furnace/remove.js +2 -6
  43. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  44. package/dist/src/commands/furnace/rename.js +29 -29
  45. package/dist/src/commands/furnace/status.js +12 -9
  46. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  47. package/dist/src/commands/furnace/test-file-name.js +39 -0
  48. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  49. package/dist/src/commands/furnace/validation-output.js +3 -8
  50. package/dist/src/commands/import.js +2 -9
  51. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  52. package/dist/src/commands/lint-per-patch.js +19 -4
  53. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  54. package/dist/src/commands/lint.d.ts +2 -1
  55. package/dist/src/commands/lint.js +1 -1
  56. package/dist/src/commands/manifest.js +1 -1
  57. package/dist/src/commands/package.js +2 -4
  58. package/dist/src/commands/patch/compact.js +14 -15
  59. package/dist/src/commands/patch/delete.js +15 -25
  60. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  61. package/dist/src/commands/patch/entry-projection.js +24 -0
  62. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  63. package/dist/src/commands/patch/lint-ignore.js +19 -29
  64. package/dist/src/commands/patch/move-files-create.js +24 -10
  65. package/dist/src/commands/patch/move-files-into.js +38 -34
  66. package/dist/src/commands/patch/move-files.js +4 -13
  67. package/dist/src/commands/patch/rename.js +27 -41
  68. package/dist/src/commands/patch/reorder.js +25 -38
  69. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  70. package/dist/src/commands/patch/split-plan.js +4 -8
  71. package/dist/src/commands/patch/split.js +45 -43
  72. package/dist/src/commands/patch/staged-dependency.js +16 -22
  73. package/dist/src/commands/patch/tier.js +15 -25
  74. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  75. package/dist/src/commands/patch-rename-map.js +36 -0
  76. package/dist/src/commands/pending-resolution.d.ts +15 -0
  77. package/dist/src/commands/pending-resolution.js +25 -0
  78. package/dist/src/commands/re-export-adjacent.d.ts +24 -2
  79. package/dist/src/commands/re-export-adjacent.js +32 -16
  80. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  81. package/dist/src/commands/re-export-drift.js +4 -4
  82. package/dist/src/commands/re-export-files.d.ts +2 -3
  83. package/dist/src/commands/re-export-files.js +38 -29
  84. package/dist/src/commands/re-export-lint.d.ts +3 -4
  85. package/dist/src/commands/re-export-lint.js +9 -2
  86. package/dist/src/commands/re-export-scan.js +5 -2
  87. package/dist/src/commands/re-export-single.d.ts +22 -3
  88. package/dist/src/commands/re-export-single.js +19 -11
  89. package/dist/src/commands/re-export.js +28 -11
  90. package/dist/src/commands/rebase/abort.js +3 -8
  91. package/dist/src/commands/rebase/continue.js +12 -12
  92. package/dist/src/commands/rebase/index.js +2 -2
  93. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  94. package/dist/src/commands/rebase/patch-loop.js +2 -7
  95. package/dist/src/commands/resolve.js +18 -10
  96. package/dist/src/commands/run.d.ts +0 -13
  97. package/dist/src/commands/run.js +12 -25
  98. package/dist/src/commands/setup-support.d.ts +16 -4
  99. package/dist/src/commands/setup-support.js +16 -16
  100. package/dist/src/commands/setup.js +3 -9
  101. package/dist/src/commands/source.js +8 -1
  102. package/dist/src/commands/status-check.js +2 -2
  103. package/dist/src/commands/status-output.js +2 -2
  104. package/dist/src/commands/status.js +3 -4
  105. package/dist/src/commands/test-build-phase.d.ts +2 -2
  106. package/dist/src/commands/test-build-phase.js +31 -7
  107. package/dist/src/commands/test-modes.d.ts +2 -2
  108. package/dist/src/commands/test-modes.js +0 -1
  109. package/dist/src/commands/test-register.js +5 -3
  110. package/dist/src/commands/test-run.js +25 -18
  111. package/dist/src/commands/test-stale-gate.js +4 -4
  112. package/dist/src/commands/test-verdict.d.ts +16 -2
  113. package/dist/src/commands/test-verdict.js +18 -3
  114. package/dist/src/commands/test.js +42 -5
  115. package/dist/src/commands/token-list.d.ts +26 -0
  116. package/dist/src/commands/token-list.js +153 -0
  117. package/dist/src/commands/token.js +17 -1
  118. package/dist/src/commands/tree.js +7 -2
  119. package/dist/src/commands/typecheck.js +9 -9
  120. package/dist/src/commands/wire.js +4 -3
  121. package/dist/src/core/branding.d.ts +0 -7
  122. package/dist/src/core/branding.js +3 -2
  123. package/dist/src/core/browser-wire.js +6 -3
  124. package/dist/src/core/build-audit-platform.d.ts +29 -0
  125. package/dist/src/core/build-audit-platform.js +140 -0
  126. package/dist/src/core/build-audit-registration.js +3 -2
  127. package/dist/src/core/build-audit-skip.d.ts +57 -0
  128. package/dist/src/core/build-audit-skip.js +112 -0
  129. package/dist/src/core/build-audit.d.ts +15 -2
  130. package/dist/src/core/build-audit.js +110 -10
  131. package/dist/src/core/build-baseline.d.ts +45 -28
  132. package/dist/src/core/build-baseline.js +6 -29
  133. package/dist/src/core/build-jar-escalation.js +3 -2
  134. package/dist/src/core/build-overwrite-guard.js +1 -1
  135. package/dist/src/core/build-prepare.d.ts +0 -8
  136. package/dist/src/core/build-prepare.js +7 -12
  137. package/dist/src/core/config-paths.d.ts +2 -6
  138. package/dist/src/core/config-paths.js +4 -2
  139. package/dist/src/core/config-state.d.ts +0 -6
  140. package/dist/src/core/config-state.js +1 -19
  141. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  142. package/dist/src/core/config-validate-build-audit.js +80 -0
  143. package/dist/src/core/config-validate-file-size.js +1 -1
  144. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  145. package/dist/src/core/config-validate-patch-policy.js +3 -3
  146. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  147. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  148. package/dist/src/core/config-validate.js +14 -3
  149. package/dist/src/core/config.d.ts +2 -2
  150. package/dist/src/core/config.js +11 -15
  151. package/dist/src/core/coverage-extend.js +4 -4
  152. package/dist/src/core/discard-baseline.js +7 -7
  153. package/dist/src/core/engine-precondition.d.ts +1 -9
  154. package/dist/src/core/engine-precondition.js +3 -8
  155. package/dist/src/core/engine-session-lock.js +13 -17
  156. package/dist/src/core/export-moved-code-shape.js +2 -2
  157. package/dist/src/core/file-lock-owner.d.ts +103 -0
  158. package/dist/src/core/file-lock-owner.js +259 -0
  159. package/dist/src/core/file-lock.d.ts +2 -7
  160. package/dist/src/core/file-lock.js +40 -91
  161. package/dist/src/core/firefox-cache.d.ts +13 -1
  162. package/dist/src/core/firefox-cache.js +59 -34
  163. package/dist/src/core/firefox-download.d.ts +15 -0
  164. package/dist/src/core/firefox-download.js +37 -21
  165. package/dist/src/core/firefox-extract.d.ts +6 -5
  166. package/dist/src/core/firefox-extract.js +36 -18
  167. package/dist/src/core/firefox.d.ts +30 -32
  168. package/dist/src/core/firefox.js +5 -36
  169. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  170. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  171. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  172. package/dist/src/core/furnace-apply-helpers.js +8 -19
  173. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  174. package/dist/src/core/furnace-apply.d.ts +1 -1
  175. package/dist/src/core/furnace-apply.js +30 -10
  176. package/dist/src/core/furnace-config.d.ts +1 -1
  177. package/dist/src/core/furnace-config.js +23 -36
  178. package/dist/src/core/furnace-constants.js +3 -2
  179. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  180. package/dist/src/core/furnace-dir-entry.js +21 -0
  181. package/dist/src/core/furnace-operation.d.ts +2 -0
  182. package/dist/src/core/furnace-operation.js +3 -1
  183. package/dist/src/core/furnace-refresh.d.ts +18 -6
  184. package/dist/src/core/furnace-refresh.js +3 -6
  185. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  186. package/dist/src/core/furnace-registration-ast.js +0 -3
  187. package/dist/src/core/furnace-registration.d.ts +2 -1
  188. package/dist/src/core/furnace-registration.js +22 -29
  189. package/dist/src/core/furnace-scanner.js +3 -6
  190. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  191. package/dist/src/core/furnace-validate-registration.js +5 -7
  192. package/dist/src/core/git-base.d.ts +3 -1
  193. package/dist/src/core/git-base.js +25 -3
  194. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  195. package/dist/src/core/git-diff-new-file.js +119 -0
  196. package/dist/src/core/git-diff.d.ts +9 -9
  197. package/dist/src/core/git-diff.js +88 -157
  198. package/dist/src/core/git-file-ops.d.ts +0 -7
  199. package/dist/src/core/git-file-ops.js +4 -28
  200. package/dist/src/core/git-readonly-index.d.ts +0 -2
  201. package/dist/src/core/git-readonly-index.js +0 -4
  202. package/dist/src/core/git-status.d.ts +7 -0
  203. package/dist/src/core/git-status.js +21 -9
  204. package/dist/src/core/git.d.ts +0 -6
  205. package/dist/src/core/git.js +14 -43
  206. package/dist/src/core/harness-orphans.js +8 -7
  207. package/dist/src/core/history-log.d.ts +19 -0
  208. package/dist/src/core/history-log.js +29 -0
  209. package/dist/src/core/mach-error-hints.d.ts +0 -5
  210. package/dist/src/core/mach-error-hints.js +1 -1
  211. package/dist/src/core/mach-known-noise-filter.d.ts +4 -3
  212. package/dist/src/core/mach-known-noise-filter.js +7 -9
  213. package/dist/src/core/mach-mozconfig.js +2 -1
  214. package/dist/src/core/mach-python.d.ts +2 -0
  215. package/dist/src/core/mach-python.js +12 -7
  216. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  217. package/dist/src/core/mach-resource-shim.js +112 -9
  218. package/dist/src/core/mach.d.ts +39 -53
  219. package/dist/src/core/mach.js +13 -81
  220. package/dist/src/core/marionette-port.d.ts +0 -17
  221. package/dist/src/core/marionette-port.js +12 -58
  222. package/dist/src/core/marionette-preflight.js +9 -27
  223. package/dist/src/core/mochitest-server-port.js +3 -3
  224. package/dist/src/core/moz-manifest-rules.js +37 -33
  225. package/dist/src/core/patch-apply-fuzz.js +4 -4
  226. package/dist/src/core/patch-apply.d.ts +4 -5
  227. package/dist/src/core/patch-apply.js +6 -12
  228. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  229. package/dist/src/core/patch-export-coverage.js +1 -30
  230. package/dist/src/core/patch-export-update.d.ts +18 -1
  231. package/dist/src/core/patch-export-update.js +4 -3
  232. package/dist/src/core/patch-export.d.ts +11 -21
  233. package/dist/src/core/patch-export.js +44 -66
  234. package/dist/src/core/patch-files.d.ts +0 -4
  235. package/dist/src/core/patch-files.js +0 -11
  236. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  237. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  238. package/dist/src/core/patch-lint-cache.js +3 -6
  239. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  240. package/dist/src/core/patch-lint-checkjs.js +37 -24
  241. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  242. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  243. package/dist/src/core/patch-lint-cross.js +49 -92
  244. package/dist/src/core/patch-lint-css.js +10 -6
  245. package/dist/src/core/patch-lint-file-size.js +3 -3
  246. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  247. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  248. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  249. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  250. package/dist/src/core/patch-lint-prettier.js +1 -1
  251. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  252. package/dist/src/core/patch-lint-queue-types.js +12 -0
  253. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  254. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  255. package/dist/src/core/patch-lint.d.ts +14 -9
  256. package/dist/src/core/patch-lint.js +4 -3
  257. package/dist/src/core/patch-lock.js +3 -3
  258. package/dist/src/core/patch-manifest-io.js +8 -7
  259. package/dist/src/core/patch-parse.d.ts +19 -8
  260. package/dist/src/core/patch-parse.js +25 -0
  261. package/dist/src/core/patch-policy.js +15 -27
  262. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  263. package/dist/src/core/patch-source-metadata.js +0 -4
  264. package/dist/src/core/patch-transform.d.ts +0 -8
  265. package/dist/src/core/patch-transform.js +0 -10
  266. package/dist/src/core/process-cpu-time.js +1 -1
  267. package/dist/src/core/rebase-session.d.ts +2 -8
  268. package/dist/src/core/rebase-session.js +5 -17
  269. package/dist/src/core/register-browser-content.js +3 -2
  270. package/dist/src/core/register-browser-test.js +2 -1
  271. package/dist/src/core/register-module.js +2 -1
  272. package/dist/src/core/register-scaffold.js +5 -4
  273. package/dist/src/core/register-shared-css.js +2 -1
  274. package/dist/src/core/run-log-redact.d.ts +38 -0
  275. package/dist/src/core/run-log-redact.js +69 -0
  276. package/dist/src/core/run-log.d.ts +13 -2
  277. package/dist/src/core/run-log.js +67 -18
  278. package/dist/src/core/signal-critical.js +2 -4
  279. package/dist/src/core/smoke-patterns.d.ts +0 -1
  280. package/dist/src/core/smoke-patterns.js +6 -11
  281. package/dist/src/core/state-file.d.ts +12 -1
  282. package/dist/src/core/state-file.js +14 -1
  283. package/dist/src/core/status-classify.js +2 -1
  284. package/dist/src/core/test-changed-prefs.js +26 -11
  285. package/dist/src/core/test-harness-crash.js +2 -2
  286. package/dist/src/core/test-stale-check.d.ts +0 -2
  287. package/dist/src/core/test-stale-check.js +2 -2
  288. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  289. package/dist/src/core/test-xpcshell-retry.js +14 -11
  290. package/dist/src/core/token-category.d.ts +33 -0
  291. package/dist/src/core/token-category.js +56 -48
  292. package/dist/src/core/token-dark-mode.js +2 -14
  293. package/dist/src/core/token-inventory.d.ts +56 -0
  294. package/dist/src/core/token-inventory.js +109 -0
  295. package/dist/src/core/token-manager.d.ts +1 -1
  296. package/dist/src/core/token-manager.js +1 -1
  297. package/dist/src/core/token-variant.js +21 -22
  298. package/dist/src/core/toolchain-preflight.js +1 -1
  299. package/dist/src/core/tree-cow.d.ts +0 -2
  300. package/dist/src/core/tree-cow.js +2 -2
  301. package/dist/src/core/tree-store.js +4 -5
  302. package/dist/src/core/typecheck.js +2 -2
  303. package/dist/src/core/wire-dom-fragment.js +6 -17
  304. package/dist/src/core/wire-utils.js +5 -17
  305. package/dist/src/core/xpcshell-appdir.js +2 -4
  306. package/dist/src/errors/base.d.ts +53 -1
  307. package/dist/src/errors/base.js +79 -11
  308. package/dist/src/errors/build.js +20 -28
  309. package/dist/src/errors/config.d.ts +0 -14
  310. package/dist/src/errors/config.js +13 -34
  311. package/dist/src/errors/download.d.ts +13 -0
  312. package/dist/src/errors/download.js +53 -24
  313. package/dist/src/errors/furnace.js +8 -9
  314. package/dist/src/errors/git.d.ts +8 -8
  315. package/dist/src/errors/git.js +20 -30
  316. package/dist/src/errors/patch.js +8 -10
  317. package/dist/src/errors/rebase.js +7 -7
  318. package/dist/src/types/commands/options.d.ts +77 -123
  319. package/dist/src/types/config.d.ts +34 -2
  320. package/dist/src/types/index.d.ts +0 -1
  321. package/dist/src/utils/assert.d.ts +3 -15
  322. package/dist/src/utils/assert.js +1 -18
  323. package/dist/src/utils/build-info.d.ts +1 -1
  324. package/dist/src/utils/child-env.d.ts +2 -1
  325. package/dist/src/utils/elapsed.js +10 -1
  326. package/dist/src/utils/fs.js +8 -13
  327. package/dist/src/utils/hash.d.ts +12 -0
  328. package/dist/src/utils/hash.js +16 -0
  329. package/dist/src/utils/line-dispatch.d.ts +15 -0
  330. package/dist/src/utils/line-dispatch.js +48 -0
  331. package/dist/src/utils/options.d.ts +1 -7
  332. package/dist/src/utils/options.js +6 -7
  333. package/dist/src/utils/package-root.js +3 -12
  334. package/dist/src/utils/platform.d.ts +0 -4
  335. package/dist/src/utils/process-group.d.ts +2 -1
  336. package/dist/src/utils/process-group.js +20 -19
  337. package/dist/src/utils/process.d.ts +40 -26
  338. package/dist/src/utils/process.js +115 -166
  339. package/dist/src/utils/ps-duration.js +3 -3
  340. package/dist/src/utils/sleep.d.ts +26 -0
  341. package/dist/src/utils/sleep.js +31 -0
  342. package/dist/src/utils/validation.d.ts +2 -38
  343. package/dist/src/utils/validation.js +7 -16
  344. package/docs/configuration.md +89 -1
  345. package/docs/exit-codes.md +31 -19
  346. package/docs/lifecycle-invariants.md +18 -12
  347. package/docs/machine-output.md +31 -8
  348. package/docs/patch-workflow.md +2 -1
  349. package/docs/run-logs.md +44 -4
  350. package/docs/testing.md +8 -1
  351. package/package.json +8 -7
  352. package/dist/src/commands/rebase.d.ts +0 -4
  353. package/dist/src/commands/rebase.js +0 -5
  354. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  355. package/dist/src/core/patch-artifact-normalize.js +0 -24
  356. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  357. package/dist/src/core/patch-lint-reexports.js +0 -8
  358. package/dist/src/core/wire-targets.d.ts +0 -7
  359. package/dist/src/core/wire-targets.js +0 -8
@@ -12,6 +12,7 @@ import { dirname, join, relative } from 'node:path';
12
12
  import { GeneralError } from '../errors/base.js';
13
13
  import { pathExists, readText, writeText } from '../utils/fs.js';
14
14
  import { toRootRelativePath } from '../utils/paths.js';
15
+ import { normalizePathSlashes } from '../utils/paths.js';
15
16
  import { escapeRegex } from '../utils/regex.js';
16
17
  import { tokenizeXhtml } from './wire-utils.js';
17
18
  export const DEFAULT_DOM_TARGET = 'browser/base/content/browser.xhtml';
@@ -22,23 +23,11 @@ export function addDomFragmentTokenized(content, includeDirective) {
22
23
  const lines = content.split('\n');
23
24
  const tokens = tokenizeXhtml(lines);
24
25
  // Find the #include browser-sets.inc token
25
- let insertIndex = -1;
26
- for (let i = 0; i < tokens.length; i++) {
27
- const token = tokens[i];
28
- if (token && token.type === 'macro' && token.raw.includes('browser-sets.inc')) {
29
- insertIndex = i;
30
- break;
31
- }
32
- }
26
+ let insertIndex = tokens.findIndex((token) => token.type === 'macro' && token.raw.includes('browser-sets.inc'));
33
27
  if (insertIndex === -1) {
34
28
  // Fallback: after <html:body>
35
- for (let i = 0; i < tokens.length; i++) {
36
- const token = tokens[i];
37
- if (token && token.type === 'xml' && /<html:body/.test(token.raw)) {
38
- insertIndex = i + 1;
39
- break;
40
- }
41
- }
29
+ const bodyIndex = tokens.findIndex((token) => token.type === 'xml' && /<html:body/.test(token.raw));
30
+ insertIndex = bodyIndex === -1 ? -1 : bodyIndex + 1;
42
31
  }
43
32
  if (insertIndex === -1) {
44
33
  throw new GeneralError('Could not find insertion point in chrome document');
@@ -69,7 +58,7 @@ export async function probeDomFragmentInsertionPoint(engineDir, domFilePath, tar
69
58
  }
70
59
  const safeDomFilePath = toRootRelativePath(engineDir, domFilePath);
71
60
  const targetDir = dirname(targetPath);
72
- const includePath = relative(targetDir, safeDomFilePath).replace(/\\/g, '/');
61
+ const includePath = normalizePathSlashes(relative(targetDir, safeDomFilePath));
73
62
  const includeDirective = `#include ${includePath}`;
74
63
  const content = await readText(targetAbsPath);
75
64
  if (new RegExp(`^${escapeRegex(includeDirective)}$`, 'm').test(content)) {
@@ -109,7 +98,7 @@ export async function addDomFragment(engineDir, domFilePath, targetPath = DEFAUL
109
98
  // contains it, so this must track the chrome doc's location, not a
110
99
  // hardcoded `browser/base/content/`.
111
100
  const targetDir = dirname(targetPath);
112
- const includePath = relative(targetDir, safeDomFilePath).replace(/\\/g, '/');
101
+ const includePath = normalizePathSlashes(relative(targetDir, safeDomFilePath));
113
102
  const includeDirective = `#include ${includePath}`;
114
103
  let content = await readText(targetAbsPath);
115
104
  // Idempotency: check if the #include directive already exists (line-anchored to avoid substring matches)
@@ -160,7 +160,7 @@ export function tokenizeXhtml(lines) {
160
160
  }
161
161
  // ---------------------------------------------------------------------------
162
162
  // Legacy (line-based) helpers — shared by fallback implementations in
163
- // wire-targets.ts. Extracted to reduce duplication and make the brace-
163
+ // the wire target modules. Extracted to reduce duplication and make the brace-
164
164
  // walking logic independently testable.
165
165
  // ---------------------------------------------------------------------------
166
166
  /**
@@ -179,24 +179,12 @@ export function tokenizeXhtml(lines) {
179
179
  * found (or, under `requireBrace`, no brace follows the signature).
180
180
  */
181
181
  export function findMethodBraceIndex(lines, pattern, options) {
182
- let methodLine = -1;
183
- for (let i = 0; i < lines.length; i++) {
184
- if (pattern.test(lines[i] ?? '')) {
185
- methodLine = i;
186
- break;
187
- }
188
- }
182
+ const methodLine = lines.findIndex((line) => pattern.test(line));
189
183
  if (methodLine === -1)
190
184
  return null;
191
- let braceIndex = methodLine;
192
- let braceFound = false;
193
- for (let i = methodLine; i < lines.length; i++) {
194
- if (lines[i]?.includes('{')) {
195
- braceIndex = i;
196
- braceFound = true;
197
- break;
198
- }
199
- }
185
+ const braceOffset = lines.slice(methodLine).findIndex((line) => line.includes('{'));
186
+ const braceFound = braceOffset !== -1;
187
+ const braceIndex = braceFound ? methodLine + braceOffset : methodLine;
200
188
  if (!braceFound && options?.requireBrace) {
201
189
  return null;
202
190
  }
@@ -64,8 +64,7 @@ export function parseAppdirFromToml(tomlText, key) {
64
64
  // comment further along the file is not mistaken for the directive.
65
65
  const escapedKey = escapeRegex(key);
66
66
  const keyPattern = new RegExp('^\\s*' + escapedKey + '\\s*=\\s*(.+?)\\s*(?:[#;].*)?$');
67
- for (let i = 0; i < lines.length; i += 1) {
68
- const line = lines[i] ?? '';
67
+ for (const [i, line] of lines.entries()) {
69
68
  const sectionMatch = /^\s*\[([^\]]+)\]\s*$/.exec(line);
70
69
  if (sectionMatch) {
71
70
  sectionSeen = true;
@@ -290,8 +289,7 @@ export async function resolveXpcshellAppdirArg(engineDir, testPaths, objDirName)
290
289
  * wins.
291
290
  */
292
291
  export function operatorAlreadySetAppPath(extraArgs) {
293
- for (let i = 0; i < extraArgs.length; i += 1) {
294
- const arg = extraArgs[i] ?? '';
292
+ for (const [i, arg] of extraArgs.entries()) {
295
293
  if (arg === '--app-path' && i + 1 < extraArgs.length)
296
294
  return true;
297
295
  if (arg.startsWith('--app-path='))
@@ -19,12 +19,45 @@ export declare abstract class FireForgeError extends Error {
19
19
  */
20
20
  get userMessage(): string;
21
21
  }
22
+ /**
23
+ * Renders the trailing "To fix this:" block shared by every FireForge error
24
+ * that offers remedies.
25
+ *
26
+ * Numbering is derived from the array rather than written into each string.
27
+ * Several classes vary their advice by context (`FurnaceError` drops the
28
+ * validate step, `ChecksumMismatchError` adds a Developer Edition note), and
29
+ * hand-numbered branches had to renumber every later step by hand — one
30
+ * conditional away from printing "2." twice.
31
+ *
32
+ * @param steps - The remedy lines, in order, without their numbers
33
+ * @returns The block, starting with the blank-line separator
34
+ */
35
+ export declare function remedies(steps: readonly string[]): string;
22
36
  /**
23
37
  * General error for unexpected failures.
24
38
  */
25
39
  export declare class GeneralError extends FireForgeError {
26
40
  readonly code: 1;
27
41
  }
42
+ /**
43
+ * A `fireforge test` preflight gate that refused before any harness ran.
44
+ *
45
+ * Exists to carry a `note`, not to change behaviour: it keeps
46
+ * {@link ExitCode.GENERAL_ERROR}, so `docs/exit-codes.md` and every CI
47
+ * consumer keying on the exit status are untouched. The note is a stable
48
+ * kebab-case refusal class that the `FIREFORGE-VERDICT:` line reports as an
49
+ * additive `note=` key. Without it every preflight refusal reached an
50
+ * unattended reader as the single token `reason=preflight`, which covers
51
+ * everything from a missing build to a peer's packaging record — so the one
52
+ * line that survives a truncating pipe could not say WHICH gate fired.
53
+ */
54
+ export declare class PreflightRefusalError extends GeneralError {
55
+ /** Stable refusal class, e.g. `stale-browser`. */
56
+ readonly note: string;
57
+ constructor(message: string,
58
+ /** Stable refusal class, e.g. `stale-browser`. */
59
+ note: string);
60
+ }
28
61
  /**
29
62
  * Raised when one of FireForge's own internal invariants did not hold —
30
63
  * the failure of a runtime assertion from `src/utils/assert.ts`.
@@ -92,14 +125,33 @@ export declare class ExecTimeoutError extends FireForgeError {
92
125
  /** Timeout that elapsed, in milliseconds. */
93
126
  readonly timeoutMs: number;
94
127
  readonly code: 1;
128
+ /** Standard output captured before the deadline fired (empty when the exec path does not capture). */
129
+ readonly stdout: string;
130
+ /** Standard error captured before the deadline fired (empty when the exec path does not capture). */
131
+ readonly stderr: string;
132
+ /** True when either captured stream was cut off at the exec layer's collector cap. */
133
+ readonly outputTruncated: boolean;
95
134
  constructor(
96
135
  /** Executable that was spawned (argv[0]). */
97
136
  command: string,
98
137
  /** Arguments the executable was spawned with. */
99
138
  args: readonly string[],
100
139
  /** Timeout that elapsed, in milliseconds. */
101
- timeoutMs: number, cause?: unknown);
140
+ timeoutMs: number, cause?: unknown, output?: ExecTimeoutOutput);
102
141
  get userMessage(): string;
142
+ /**
143
+ * The last few lines the command printed before it was killed, so an
144
+ * operator can see what it was doing (a `git add -A` stuck on one path,
145
+ * a build step that hung) instead of a bare budget number. Prefers
146
+ * stderr; falls back to stdout when stderr is silent.
147
+ */
148
+ private outputTailForUser;
149
+ }
150
+ /** Output captured before an exec timeout fired; see {@link ExecTimeoutError}. */
151
+ export interface ExecTimeoutOutput {
152
+ stdout: string;
153
+ stderr: string;
154
+ truncated: boolean;
103
155
  }
104
156
  /**
105
157
  * Error thrown when a file-lock wait times out because another process holds
@@ -17,10 +17,11 @@ export class FireForgeError extends Error {
17
17
  if (cause !== undefined) {
18
18
  this.cause = cause;
19
19
  }
20
- // Maintains proper stack trace in V8 environments
21
- if (typeof Error.captureStackTrace === 'function') {
22
- Error.captureStackTrace(this, this.constructor);
23
- }
20
+ // Trims the constructor frames off the stack. `engines` pins Node >= 22,
21
+ // so the V8-only `captureStackTrace` is always present — the former
22
+ // `typeof === 'function'` guard was an unreachable branch that coverage
23
+ // could never close.
24
+ Error.captureStackTrace(this, this.constructor);
24
25
  }
25
26
  /**
26
27
  * User-friendly error message with context and suggested fixes.
@@ -30,12 +31,49 @@ export class FireForgeError extends Error {
30
31
  return this.message;
31
32
  }
32
33
  }
34
+ /**
35
+ * Renders the trailing "To fix this:" block shared by every FireForge error
36
+ * that offers remedies.
37
+ *
38
+ * Numbering is derived from the array rather than written into each string.
39
+ * Several classes vary their advice by context (`FurnaceError` drops the
40
+ * validate step, `ChecksumMismatchError` adds a Developer Edition note), and
41
+ * hand-numbered branches had to renumber every later step by hand — one
42
+ * conditional away from printing "2." twice.
43
+ *
44
+ * @param steps - The remedy lines, in order, without their numbers
45
+ * @returns The block, starting with the blank-line separator
46
+ */
47
+ export function remedies(steps) {
48
+ return `\n\nTo fix this:\n${steps.map((step, index) => ` ${index + 1}. ${step}`).join('\n')}`;
49
+ }
33
50
  /**
34
51
  * General error for unexpected failures.
35
52
  */
36
53
  export class GeneralError extends FireForgeError {
37
54
  code = ExitCode.GENERAL_ERROR;
38
55
  }
56
+ /**
57
+ * A `fireforge test` preflight gate that refused before any harness ran.
58
+ *
59
+ * Exists to carry a `note`, not to change behaviour: it keeps
60
+ * {@link ExitCode.GENERAL_ERROR}, so `docs/exit-codes.md` and every CI
61
+ * consumer keying on the exit status are untouched. The note is a stable
62
+ * kebab-case refusal class that the `FIREFORGE-VERDICT:` line reports as an
63
+ * additive `note=` key. Without it every preflight refusal reached an
64
+ * unattended reader as the single token `reason=preflight`, which covers
65
+ * everything from a missing build to a peer's packaging record — so the one
66
+ * line that survives a truncating pipe could not say WHICH gate fired.
67
+ */
68
+ export class PreflightRefusalError extends GeneralError {
69
+ note;
70
+ constructor(message,
71
+ /** Stable refusal class, e.g. `stale-browser`. */
72
+ note) {
73
+ super(message);
74
+ this.note = note;
75
+ }
76
+ }
39
77
  /**
40
78
  * Raised when one of FireForge's own internal invariants did not hold —
41
79
  * the failure of a runtime assertion from `src/utils/assert.ts`.
@@ -102,11 +140,9 @@ export class InvalidArgumentError extends FireForgeError {
102
140
  this.argument = argument;
103
141
  }
104
142
  get userMessage() {
105
- let msg = `Invalid Argument: ${this.message}`;
106
- if (this.argument) {
107
- msg += `\n\nArgument: ${this.argument}`;
108
- }
109
- return msg;
143
+ return this.argument
144
+ ? `Invalid Argument: ${this.message}\n\nArgument: ${this.argument}`
145
+ : `Invalid Argument: ${this.message}`;
110
146
  }
111
147
  }
112
148
  /**
@@ -123,24 +159,56 @@ export class ExecTimeoutError extends FireForgeError {
123
159
  args;
124
160
  timeoutMs;
125
161
  code = ExitCode.GENERAL_ERROR;
162
+ /** Standard output captured before the deadline fired (empty when the exec path does not capture). */
163
+ stdout;
164
+ /** Standard error captured before the deadline fired (empty when the exec path does not capture). */
165
+ stderr;
166
+ /** True when either captured stream was cut off at the exec layer's collector cap. */
167
+ outputTruncated;
126
168
  constructor(
127
169
  /** Executable that was spawned (argv[0]). */
128
170
  command,
129
171
  /** Arguments the executable was spawned with. */
130
172
  args,
131
173
  /** Timeout that elapsed, in milliseconds. */
132
- timeoutMs, cause) {
174
+ timeoutMs, cause, output) {
133
175
  super(`Command timed out after ${Math.round(timeoutMs / 1000)}s: ${[command, ...args].join(' ')}`, cause);
134
176
  this.command = command;
135
177
  this.args = args;
136
178
  this.timeoutMs = timeoutMs;
179
+ this.stdout = output?.stdout ?? '';
180
+ this.stderr = output?.stderr ?? '';
181
+ this.outputTruncated = output?.truncated ?? false;
137
182
  }
138
183
  get userMessage() {
139
184
  return (`${this.message}\n\n` +
140
185
  'The command was killed because it exceeded its time budget, not because it failed on its own. ' +
141
- 'If the host is just slow or loaded, re-running may succeed.');
186
+ 'If the host is just slow or loaded, re-running may succeed.' +
187
+ this.outputTailForUser());
188
+ }
189
+ /**
190
+ * The last few lines the command printed before it was killed, so an
191
+ * operator can see what it was doing (a `git add -A` stuck on one path,
192
+ * a build step that hung) instead of a bare budget number. Prefers
193
+ * stderr; falls back to stdout when stderr is silent.
194
+ */
195
+ outputTailForUser() {
196
+ const source = this.stderr.trim().length > 0 ? this.stderr : this.stdout;
197
+ const lines = source
198
+ .trimEnd()
199
+ .split(/\r?\n/)
200
+ .filter((line) => line.length > 0);
201
+ if (lines.length === 0)
202
+ return '';
203
+ const tail = lines.slice(-EXEC_TIMEOUT_TAIL_LINES);
204
+ const label = this.stderr.trim().length > 0 ? 'stderr' : 'stdout';
205
+ const omitted = lines.length > tail.length ? ` (last ${tail.length} of ${lines.length} lines)` : '';
206
+ const truncatedNote = this.outputTruncated ? ' [captured output was truncated]' : '';
207
+ return `\n\nOutput before the timeout (${label}${omitted})${truncatedNote}:\n${tail.map((line) => ` ${line}`).join('\n')}`;
142
208
  }
143
209
  }
210
+ /** How many trailing output lines {@link ExecTimeoutError.userMessage} shows. */
211
+ const EXEC_TIMEOUT_TAIL_LINES = 20;
144
212
  /**
145
213
  * Error thrown when a file-lock wait times out because another process holds
146
214
  * the lock.
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
- import { FireForgeError } from './base.js';
2
+ import { FireForgeError, remedies } from './base.js';
3
3
  import { ExitCode } from './codes.js';
4
4
  /**
5
5
  * Error thrown when a build operation fails.
@@ -12,15 +12,13 @@ export class BuildError extends FireForgeError {
12
12
  this.command = command;
13
13
  }
14
14
  get userMessage() {
15
- let msg = `Build Error: ${this.message}`;
16
- if (this.command) {
17
- msg += `\n\nCommand: ${this.command}`;
18
- }
19
- msg += '\n\nTo fix this:\n';
20
- msg += ' 1. Check the build output above for specific errors\n';
21
- msg += ' 2. Ensure all dependencies are installed with "fireforge bootstrap"\n';
22
- msg += ' 3. Try a clean build by deleting obj-* directories';
23
- return msg;
15
+ return (`Build Error: ${this.message}` +
16
+ (this.command ? `\n\nCommand: ${this.command}` : '') +
17
+ remedies([
18
+ 'Check the build output above for specific errors',
19
+ 'Ensure all dependencies are installed with "fireforge bootstrap"',
20
+ 'Try a clean build by deleting obj-* directories',
21
+ ]));
24
22
  }
25
23
  }
26
24
  /**
@@ -52,24 +50,18 @@ export class TestFailureError extends BuildError {
52
50
  this.logPath = logPath;
53
51
  }
54
52
  get userMessage() {
55
- let msg = `Test Failure: ${this.message}`;
56
- if (this.command) {
57
- msg += `\n\nCommand: ${this.command}`;
58
- }
59
- if (this.failureBlocks) {
60
- msg += `\n\n${this.failureBlocks}`;
61
- }
62
- msg += '\n\nTo fix this:\n';
63
- msg += this.failureBlocks
64
- ? ' 1. Fix the failing assertions listed above, then re-run just those test paths\n'
65
- : ' 1. Find the TEST-UNEXPECTED-FAIL lines in the output above — they name what broke\n';
66
- msg +=
67
- ' 2. Read the "FIREFORGE-VERDICT:" line for the run\'s ground truth (checks, unexpected)\n';
68
- msg +=
69
- this.logPath !== undefined
70
- ? ` 3. The complete raw output is at ${this.logPath} — it survives a piped/tailed terminal`
71
- : ' 3. Re-run without piping the output, so the failure lines are not cut off by "tail"';
72
- return msg;
53
+ return (`Test Failure: ${this.message}` +
54
+ (this.command ? `\n\nCommand: ${this.command}` : '') +
55
+ (this.failureBlocks ? `\n\n${this.failureBlocks}` : '') +
56
+ remedies([
57
+ this.failureBlocks
58
+ ? 'Fix the failing assertions listed above, then re-run just those test paths'
59
+ : 'Find the TEST-UNEXPECTED-FAIL lines in the output above — they name what broke',
60
+ 'Read the "FIREFORGE-VERDICT:" line for the run\'s ground truth (checks, unexpected)',
61
+ this.logPath !== undefined
62
+ ? `The complete raw output is at ${this.logPath} — it survives a piped/tailed terminal`
63
+ : 'Re-run without piping the output, so the failure lines are not cut off by "tail"',
64
+ ]));
73
65
  }
74
66
  }
75
67
  /**
@@ -15,17 +15,3 @@ export declare class ConfigNotFoundError extends ConfigError {
15
15
  constructor(configPath: string);
16
16
  get userMessage(): string;
17
17
  }
18
- /**
19
- * Error thrown when a required field is missing from config.
20
- */
21
- export declare class MissingFieldError extends ConfigError {
22
- constructor(field: string);
23
- }
24
- /**
25
- * Error thrown when a field has an invalid value.
26
- */
27
- export declare class InvalidFieldError extends ConfigError {
28
- readonly expectedType: string;
29
- readonly actualValue: unknown;
30
- constructor(field: string, expectedType: string, actualValue: unknown);
31
- }
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
- import { FireForgeError } from './base.js';
2
+ import { FireForgeError, remedies } from './base.js';
3
3
  import { ExitCode } from './codes.js';
4
4
  /**
5
5
  * Error thrown when configuration is missing or invalid.
@@ -12,15 +12,13 @@ export class ConfigError extends FireForgeError {
12
12
  this.field = field;
13
13
  }
14
14
  get userMessage() {
15
- let msg = `Configuration Error: ${this.message}`;
16
- if (this.field) {
17
- msg += `\n\nField: ${this.field}`;
18
- }
19
- msg += '\n\nTo fix this:\n';
20
- msg += ' 1. Check your fireforge.json file for errors\n';
21
- msg += ' 2. Run "fireforge setup" to create a new configuration\n';
22
- msg += ' 3. See the documentation for the expected format';
23
- return msg;
15
+ return (`Configuration Error: ${this.message}` +
16
+ (this.field ? `\n\nField: ${this.field}` : '') +
17
+ remedies([
18
+ 'Check your fireforge.json file for errors',
19
+ 'Run "fireforge setup" to create a new configuration',
20
+ 'See the documentation for the expected format',
21
+ ]));
24
22
  }
25
23
  }
26
24
  /**
@@ -32,29 +30,10 @@ export class ConfigNotFoundError extends ConfigError {
32
30
  }
33
31
  get userMessage() {
34
32
  return (`Configuration Error: ${this.message}\n\n` +
35
- 'This directory does not appear to be a FireForge project.\n\n' +
36
- 'To fix this:\n' +
37
- ' 1. Navigate to your project root directory\n' +
38
- ' 2. Run "fireforge setup" to initialize a new project');
39
- }
40
- }
41
- /**
42
- * Error thrown when a required field is missing from config.
43
- */
44
- export class MissingFieldError extends ConfigError {
45
- constructor(field) {
46
- super(`Required field "${field}" is missing from fireforge.json`, field);
47
- }
48
- }
49
- /**
50
- * Error thrown when a field has an invalid value.
51
- */
52
- export class InvalidFieldError extends ConfigError {
53
- expectedType;
54
- actualValue;
55
- constructor(field, expectedType, actualValue) {
56
- super(`Field "${field}" has invalid value. Expected ${expectedType}, got ${typeof actualValue}`, field);
57
- this.expectedType = expectedType;
58
- this.actualValue = actualValue;
33
+ 'This directory does not appear to be a FireForge project.' +
34
+ remedies([
35
+ 'Navigate to your project root directory',
36
+ 'Run "fireforge setup" to initialize a new project',
37
+ ]));
59
38
  }
60
39
  }
@@ -19,6 +19,19 @@ export declare class ChecksumMismatchError extends DownloadError {
19
19
  constructor(product: FirefoxProduct, expectedSha256: string, actualSha256: string, url: string);
20
20
  get userMessage(): string;
21
21
  }
22
+ /**
23
+ * Error thrown when the default integrity check cannot obtain Mozilla's
24
+ * published SHA-256 for a freshly downloaded archive (checksum file
25
+ * unreachable, malformed, or not listing the archive) and no operator pin
26
+ * or explicit opt-out allows the download to be trusted on TLS alone.
27
+ */
28
+ export declare class ChecksumUnavailableError extends DownloadError {
29
+ readonly product: FirefoxProduct;
30
+ readonly checksumsUrl: string;
31
+ readonly reason: string;
32
+ constructor(product: FirefoxProduct, checksumsUrl: string, reason: string, url: string);
33
+ get userMessage(): string;
34
+ }
22
35
  /**
23
36
  * Error thrown when extraction of the downloaded archive fails.
24
37
  */
@@ -1,4 +1,4 @@
1
- import { FireForgeError } from './base.js';
1
+ import { FireForgeError, remedies } from './base.js';
2
2
  import { ExitCode } from './codes.js';
3
3
  /**
4
4
  * Error thrown when Firefox source download fails.
@@ -11,15 +11,13 @@ export class DownloadError extends FireForgeError {
11
11
  this.url = url;
12
12
  }
13
13
  get userMessage() {
14
- let msg = `Download Error: ${this.message}`;
15
- if (this.url) {
16
- msg += `\n\nURL: ${this.url}`;
17
- }
18
- msg += '\n\nTo fix this:\n';
19
- msg += ' 1. Check your internet connection\n';
20
- msg += ' 2. Verify the Firefox version in fireforge.json is valid\n';
21
- msg += ' 3. Try again with "fireforge download --force"';
22
- return msg;
14
+ return (`Download Error: ${this.message}` +
15
+ (this.url ? `\n\nURL: ${this.url}` : '') +
16
+ remedies([
17
+ 'Check your internet connection',
18
+ 'Verify the Firefox version in fireforge.json is valid',
19
+ 'Try again with "fireforge download --force"',
20
+ ]));
23
21
  }
24
22
  }
25
23
  /**
@@ -36,23 +34,54 @@ export class ChecksumMismatchError extends DownloadError {
36
34
  this.actualSha256 = actualSha256;
37
35
  }
38
36
  get userMessage() {
39
- let msg = `Download Error: Firefox source archive checksum mismatch.\n\n` +
37
+ return (`Download Error: Firefox source archive checksum mismatch.\n\n` +
40
38
  `Product: ${this.product}\n` +
41
39
  `URL: ${this.url}\n` +
42
40
  `Expected SHA-256: ${this.expectedSha256}\n` +
43
- `Actual SHA-256: ${this.actualSha256}`;
44
- msg += '\n\nTo fix this:\n';
45
- msg += ' 1. Verify firefox.product, firefox.version, and firefox.sha256 in fireforge.json\n';
46
- msg += ' 2. Compare the pinned hash with Mozilla SHA256SUMMARY for the resolved archive\n';
47
- if (this.product === 'firefox-devedition') {
48
- msg +=
49
- ' 3. Developer Edition archives should resolve under https://archive.mozilla.org/pub/devedition/releases/\n';
50
- msg += ' 4. Re-run "fireforge download --force" after correcting the source settings';
51
- }
52
- else {
53
- msg += ' 3. Re-run "fireforge download --force" after correcting the source settings';
54
- }
55
- return msg;
41
+ `Actual SHA-256: ${this.actualSha256}` +
42
+ remedies([
43
+ 'Verify firefox.product, firefox.version, and firefox.sha256 in fireforge.json',
44
+ 'Compare the pinned hash with Mozilla SHA256SUMMARY for the resolved archive',
45
+ ...(this.product === 'firefox-devedition'
46
+ ? [
47
+ 'Developer Edition archives should resolve under https://archive.mozilla.org/pub/devedition/releases/',
48
+ ]
49
+ : []),
50
+ 'Re-run "fireforge download --force" after correcting the source settings',
51
+ ]));
52
+ }
53
+ }
54
+ /**
55
+ * Error thrown when the default integrity check cannot obtain Mozilla's
56
+ * published SHA-256 for a freshly downloaded archive (checksum file
57
+ * unreachable, malformed, or not listing the archive) and no operator pin
58
+ * or explicit opt-out allows the download to be trusted on TLS alone.
59
+ */
60
+ export class ChecksumUnavailableError extends DownloadError {
61
+ product;
62
+ checksumsUrl;
63
+ reason;
64
+ constructor(product, checksumsUrl, reason, url) {
65
+ super(`Could not verify archive against published SHA256SUMS: ${reason}`, url);
66
+ this.product = product;
67
+ this.checksumsUrl = checksumsUrl;
68
+ this.reason = reason;
69
+ }
70
+ get userMessage() {
71
+ return (`Download Error: Firefox source archive integrity could not be verified.\n\n` +
72
+ `Product: ${this.product}\n` +
73
+ `Archive: ${this.url ?? ''}\n` +
74
+ `Checksums: ${this.checksumsUrl}\n` +
75
+ `Reason: ${this.reason}\n\n` +
76
+ 'The archive was downloaded but discarded: without the published digest it would be ' +
77
+ 'trusted on TLS alone, and it becomes the git baseline every patch is built on.\n\n' +
78
+ 'To fix this:\n' +
79
+ ' 1. Check connectivity to archive.mozilla.org (a captive portal or proxy may be ' +
80
+ 'answering the SHA256SUMS request) and retry\n' +
81
+ ' 2. Or pin the archive digest: set firefox.sha256 in fireforge.json ' +
82
+ '(from the release SHA256SUMS, obtained out of band)\n' +
83
+ ' 3. Or, for an offline mirror you trust, set firefox.allowUnverifiedDownload: true ' +
84
+ 'in fireforge.json to accept the download with a warning');
56
85
  }
57
86
  }
58
87
  /**
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
- import { FireForgeError } from './base.js';
2
+ import { FireForgeError, remedies } from './base.js';
3
3
  import { ExitCode } from './codes.js';
4
4
  /**
5
5
  * Error thrown when a furnace component operation fails.
@@ -15,15 +15,14 @@ export class FurnaceError extends FireForgeError {
15
15
  }
16
16
  omitValidateAdvice;
17
17
  get userMessage() {
18
- let msg = this.component
18
+ const head = this.component
19
19
  ? `Furnace Error (${this.component}): ${this.message}`
20
20
  : `Furnace Error: ${this.message}`;
21
- msg += '\n\nTo fix this:\n';
22
- msg += ' 1. Check the error message above for specifics\n';
23
- if (!this.omitValidateAdvice) {
24
- msg += ' 2. Run "fireforge furnace validate" to diagnose issues\n';
25
- }
26
- msg += ` ${this.omitValidateAdvice ? '2' : '3'}. Use "fireforge doctor --repair-furnace" if state is inconsistent`;
27
- return msg;
21
+ return (head +
22
+ remedies([
23
+ 'Check the error message above for specifics',
24
+ ...(this.omitValidateAdvice ? [] : ['Run "fireforge furnace validate" to diagnose issues']),
25
+ 'Use "fireforge doctor --repair-furnace" if state is inconsistent',
26
+ ]));
28
27
  }
29
28
  }