@hominis/fireforge 0.44.4 → 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 (378) hide show
  1. package/CHANGELOG.md +120 -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-source-pin.d.ts +29 -0
  18. package/dist/src/commands/doctor-source-pin.js +81 -0
  19. package/dist/src/commands/doctor.d.ts +2 -0
  20. package/dist/src/commands/doctor.js +9 -8
  21. package/dist/src/commands/download.js +35 -24
  22. package/dist/src/commands/export-all.js +21 -4
  23. package/dist/src/commands/export-flow.js +15 -60
  24. package/dist/src/commands/export-placement-conflicts.js +3 -3
  25. package/dist/src/commands/export-placement-gate.js +2 -8
  26. package/dist/src/commands/export-placement-policy.js +3 -5
  27. package/dist/src/commands/export-shared.d.ts +42 -23
  28. package/dist/src/commands/export-shared.js +4 -19
  29. package/dist/src/commands/export.js +10 -1
  30. package/dist/src/commands/furnace/authoring-config.d.ts +13 -0
  31. package/dist/src/commands/furnace/authoring-config.js +19 -0
  32. package/dist/src/commands/furnace/chrome-doc-remove.js +1 -1
  33. package/dist/src/commands/furnace/create-browser-test.js +3 -8
  34. package/dist/src/commands/furnace/create-dry-run.js +2 -5
  35. package/dist/src/commands/furnace/create-templates.d.ts +17 -1
  36. package/dist/src/commands/furnace/create-templates.js +2 -1
  37. package/dist/src/commands/furnace/create.js +25 -19
  38. package/dist/src/commands/furnace/deploy.js +11 -2
  39. package/dist/src/commands/furnace/init.js +2 -1
  40. package/dist/src/commands/furnace/override.js +28 -19
  41. package/dist/src/commands/furnace/refresh.js +9 -1
  42. package/dist/src/commands/furnace/remove-mutations.d.ts +6 -7
  43. package/dist/src/commands/furnace/remove-mutations.js +0 -1
  44. package/dist/src/commands/furnace/remove.js +2 -6
  45. package/dist/src/commands/furnace/rename-browser-test.js +1 -7
  46. package/dist/src/commands/furnace/rename.js +29 -29
  47. package/dist/src/commands/furnace/status.js +12 -9
  48. package/dist/src/commands/furnace/test-file-name.d.ts +28 -0
  49. package/dist/src/commands/furnace/test-file-name.js +39 -0
  50. package/dist/src/commands/furnace/validation-output.d.ts +21 -9
  51. package/dist/src/commands/furnace/validation-output.js +3 -8
  52. package/dist/src/commands/import.js +2 -9
  53. package/dist/src/commands/lint-per-patch.d.ts +2 -2
  54. package/dist/src/commands/lint-per-patch.js +19 -4
  55. package/dist/src/commands/lint-per-run-checkjs.d.ts +2 -2
  56. package/dist/src/commands/lint.d.ts +2 -1
  57. package/dist/src/commands/lint.js +1 -1
  58. package/dist/src/commands/manifest.js +1 -1
  59. package/dist/src/commands/package.js +2 -4
  60. package/dist/src/commands/patch/compact.js +14 -15
  61. package/dist/src/commands/patch/delete.js +15 -25
  62. package/dist/src/commands/patch/entry-projection.d.ts +13 -0
  63. package/dist/src/commands/patch/entry-projection.js +24 -0
  64. package/dist/src/commands/patch/lint-ignore.d.ts +0 -8
  65. package/dist/src/commands/patch/lint-ignore.js +19 -29
  66. package/dist/src/commands/patch/move-files-create.js +24 -10
  67. package/dist/src/commands/patch/move-files-into.js +38 -37
  68. package/dist/src/commands/patch/move-files.js +4 -13
  69. package/dist/src/commands/patch/rename.js +27 -41
  70. package/dist/src/commands/patch/reorder.js +25 -38
  71. package/dist/src/commands/patch/split-plan.d.ts +4 -5
  72. package/dist/src/commands/patch/split-plan.js +4 -11
  73. package/dist/src/commands/patch/split.js +45 -43
  74. package/dist/src/commands/patch/staged-dependency.js +16 -22
  75. package/dist/src/commands/patch/tier.js +15 -25
  76. package/dist/src/commands/patch-rename-map.d.ts +19 -0
  77. package/dist/src/commands/patch-rename-map.js +36 -0
  78. package/dist/src/commands/pending-resolution.d.ts +15 -0
  79. package/dist/src/commands/pending-resolution.js +25 -0
  80. package/dist/src/commands/re-export-adjacent.d.ts +32 -2
  81. package/dist/src/commands/re-export-adjacent.js +51 -10
  82. package/dist/src/commands/re-export-bulk-scan.js +6 -10
  83. package/dist/src/commands/re-export-drift.d.ts +8 -0
  84. package/dist/src/commands/re-export-drift.js +65 -4
  85. package/dist/src/commands/re-export-files.d.ts +2 -3
  86. package/dist/src/commands/re-export-files.js +38 -32
  87. package/dist/src/commands/re-export-lint.d.ts +3 -4
  88. package/dist/src/commands/re-export-lint.js +9 -2
  89. package/dist/src/commands/re-export-options.js +19 -5
  90. package/dist/src/commands/re-export-register.js +5 -1
  91. package/dist/src/commands/re-export-scan.js +8 -8
  92. package/dist/src/commands/re-export-single.d.ts +22 -3
  93. package/dist/src/commands/re-export-single.js +19 -11
  94. package/dist/src/commands/re-export.js +44 -27
  95. package/dist/src/commands/rebase/abort.js +3 -8
  96. package/dist/src/commands/rebase/continue.js +12 -12
  97. package/dist/src/commands/rebase/index.js +2 -2
  98. package/dist/src/commands/rebase/patch-loop.d.ts +2 -2
  99. package/dist/src/commands/rebase/patch-loop.js +2 -7
  100. package/dist/src/commands/resolve.js +18 -10
  101. package/dist/src/commands/run.d.ts +0 -13
  102. package/dist/src/commands/run.js +12 -25
  103. package/dist/src/commands/setup-support.d.ts +16 -4
  104. package/dist/src/commands/setup-support.js +16 -16
  105. package/dist/src/commands/setup.js +3 -9
  106. package/dist/src/commands/source.js +8 -1
  107. package/dist/src/commands/status-check.js +2 -2
  108. package/dist/src/commands/status-output.js +2 -2
  109. package/dist/src/commands/status.js +3 -4
  110. package/dist/src/commands/test-build-phase.d.ts +2 -2
  111. package/dist/src/commands/test-build-phase.js +33 -8
  112. package/dist/src/commands/test-diagnose.js +14 -5
  113. package/dist/src/commands/test-modes.d.ts +2 -2
  114. package/dist/src/commands/test-modes.js +0 -1
  115. package/dist/src/commands/test-register.js +6 -3
  116. package/dist/src/commands/test-run.js +29 -21
  117. package/dist/src/commands/test-stale-gate.js +8 -5
  118. package/dist/src/commands/test-verdict.d.ts +16 -2
  119. package/dist/src/commands/test-verdict.js +18 -3
  120. package/dist/src/commands/test.js +51 -7
  121. package/dist/src/commands/token-list.d.ts +26 -0
  122. package/dist/src/commands/token-list.js +153 -0
  123. package/dist/src/commands/token.js +17 -1
  124. package/dist/src/commands/tree.js +7 -2
  125. package/dist/src/commands/typecheck.js +9 -9
  126. package/dist/src/commands/wire.js +4 -3
  127. package/dist/src/core/branding.d.ts +0 -7
  128. package/dist/src/core/branding.js +3 -2
  129. package/dist/src/core/browser-wire.js +6 -3
  130. package/dist/src/core/build-audit-platform.d.ts +29 -0
  131. package/dist/src/core/build-audit-platform.js +140 -0
  132. package/dist/src/core/build-audit-registration.js +3 -2
  133. package/dist/src/core/build-audit-skip.d.ts +57 -0
  134. package/dist/src/core/build-audit-skip.js +112 -0
  135. package/dist/src/core/build-audit.d.ts +15 -2
  136. package/dist/src/core/build-audit.js +154 -12
  137. package/dist/src/core/build-baseline.d.ts +45 -28
  138. package/dist/src/core/build-baseline.js +6 -29
  139. package/dist/src/core/build-jar-escalation.d.ts +64 -0
  140. package/dist/src/core/build-jar-escalation.js +128 -0
  141. package/dist/src/core/build-overwrite-guard.js +1 -1
  142. package/dist/src/core/build-prepare.d.ts +5 -2
  143. package/dist/src/core/build-prepare.js +34 -6
  144. package/dist/src/core/config-paths.d.ts +2 -6
  145. package/dist/src/core/config-paths.js +4 -2
  146. package/dist/src/core/config-state.d.ts +0 -6
  147. package/dist/src/core/config-state.js +1 -19
  148. package/dist/src/core/config-validate-build-audit.d.ts +8 -0
  149. package/dist/src/core/config-validate-build-audit.js +80 -0
  150. package/dist/src/core/config-validate-file-size.d.ts +12 -0
  151. package/dist/src/core/config-validate-file-size.js +65 -0
  152. package/dist/src/core/config-validate-patch-policy.d.ts +2 -2
  153. package/dist/src/core/config-validate-patch-policy.js +3 -3
  154. package/dist/src/core/config-validate-test-toolchains.d.ts +2 -2
  155. package/dist/src/core/config-validate-test-toolchains.js +4 -5
  156. package/dist/src/core/config-validate.js +20 -3
  157. package/dist/src/core/config.d.ts +2 -2
  158. package/dist/src/core/config.js +11 -15
  159. package/dist/src/core/coverage-extend.js +4 -4
  160. package/dist/src/core/discard-baseline.js +7 -7
  161. package/dist/src/core/engine-precondition.d.ts +1 -9
  162. package/dist/src/core/engine-precondition.js +3 -8
  163. package/dist/src/core/engine-session-lock.js +13 -17
  164. package/dist/src/core/export-moved-code-shape.js +2 -2
  165. package/dist/src/core/file-lock-owner.d.ts +103 -0
  166. package/dist/src/core/file-lock-owner.js +259 -0
  167. package/dist/src/core/file-lock.d.ts +2 -7
  168. package/dist/src/core/file-lock.js +40 -91
  169. package/dist/src/core/firefox-cache.d.ts +13 -1
  170. package/dist/src/core/firefox-cache.js +59 -34
  171. package/dist/src/core/firefox-download.d.ts +15 -0
  172. package/dist/src/core/firefox-download.js +37 -21
  173. package/dist/src/core/firefox-extract.d.ts +6 -5
  174. package/dist/src/core/firefox-extract.js +36 -18
  175. package/dist/src/core/firefox.d.ts +30 -32
  176. package/dist/src/core/firefox.js +5 -36
  177. package/dist/src/core/furnace-apply-dry-run.d.ts +1 -6
  178. package/dist/src/core/furnace-apply-dry-run.js +1 -7
  179. package/dist/src/core/furnace-apply-helpers.d.ts +0 -11
  180. package/dist/src/core/furnace-apply-helpers.js +8 -19
  181. package/dist/src/core/furnace-apply-overwrite-warn.js +4 -2
  182. package/dist/src/core/furnace-apply.d.ts +1 -1
  183. package/dist/src/core/furnace-apply.js +30 -10
  184. package/dist/src/core/furnace-config.d.ts +1 -1
  185. package/dist/src/core/furnace-config.js +23 -36
  186. package/dist/src/core/furnace-constants.js +3 -2
  187. package/dist/src/core/furnace-dir-entry.d.ts +20 -0
  188. package/dist/src/core/furnace-dir-entry.js +21 -0
  189. package/dist/src/core/furnace-operation.d.ts +2 -0
  190. package/dist/src/core/furnace-operation.js +3 -1
  191. package/dist/src/core/furnace-refresh.d.ts +18 -6
  192. package/dist/src/core/furnace-refresh.js +3 -6
  193. package/dist/src/core/furnace-registration-ast.d.ts +0 -1
  194. package/dist/src/core/furnace-registration-ast.js +0 -3
  195. package/dist/src/core/furnace-registration.d.ts +2 -1
  196. package/dist/src/core/furnace-registration.js +22 -29
  197. package/dist/src/core/furnace-scanner.js +3 -6
  198. package/dist/src/core/furnace-state-persist.d.ts +2 -2
  199. package/dist/src/core/furnace-validate-registration.js +5 -7
  200. package/dist/src/core/git-base.d.ts +3 -1
  201. package/dist/src/core/git-base.js +25 -3
  202. package/dist/src/core/git-diff-new-file.d.ts +40 -0
  203. package/dist/src/core/git-diff-new-file.js +119 -0
  204. package/dist/src/core/git-diff.d.ts +9 -9
  205. package/dist/src/core/git-diff.js +88 -157
  206. package/dist/src/core/git-file-ops.d.ts +0 -7
  207. package/dist/src/core/git-file-ops.js +4 -28
  208. package/dist/src/core/git-readonly-index.d.ts +0 -2
  209. package/dist/src/core/git-readonly-index.js +0 -4
  210. package/dist/src/core/git-status.d.ts +7 -0
  211. package/dist/src/core/git-status.js +21 -9
  212. package/dist/src/core/git.d.ts +0 -6
  213. package/dist/src/core/git.js +14 -43
  214. package/dist/src/core/harness-orphans.d.ts +73 -0
  215. package/dist/src/core/harness-orphans.js +187 -0
  216. package/dist/src/core/history-log.d.ts +19 -0
  217. package/dist/src/core/history-log.js +29 -0
  218. package/dist/src/core/mach-error-hints.d.ts +0 -5
  219. package/dist/src/core/mach-error-hints.js +1 -1
  220. package/dist/src/core/mach-known-noise-filter.d.ts +25 -3
  221. package/dist/src/core/mach-known-noise-filter.js +31 -10
  222. package/dist/src/core/mach-mozconfig.js +2 -1
  223. package/dist/src/core/mach-python.d.ts +2 -0
  224. package/dist/src/core/mach-python.js +12 -7
  225. package/dist/src/core/mach-resource-shim.d.ts +13 -1
  226. package/dist/src/core/mach-resource-shim.js +112 -9
  227. package/dist/src/core/mach.d.ts +46 -53
  228. package/dist/src/core/mach.js +31 -83
  229. package/dist/src/core/marionette-port.d.ts +0 -17
  230. package/dist/src/core/marionette-port.js +12 -58
  231. package/dist/src/core/marionette-preflight.js +9 -27
  232. package/dist/src/core/mochitest-server-port.js +3 -3
  233. package/dist/src/core/moz-manifest-rules.js +37 -33
  234. package/dist/src/core/patch-apply-fuzz.js +4 -4
  235. package/dist/src/core/patch-apply.d.ts +4 -5
  236. package/dist/src/core/patch-apply.js +55 -32
  237. package/dist/src/core/patch-export-coverage.d.ts +0 -9
  238. package/dist/src/core/patch-export-coverage.js +1 -30
  239. package/dist/src/core/patch-export-update.d.ts +18 -1
  240. package/dist/src/core/patch-export-update.js +4 -3
  241. package/dist/src/core/patch-export.d.ts +11 -21
  242. package/dist/src/core/patch-export.js +44 -66
  243. package/dist/src/core/patch-files.d.ts +0 -4
  244. package/dist/src/core/patch-files.js +0 -11
  245. package/dist/src/core/patch-lint-binary.d.ts +6 -12
  246. package/dist/src/core/patch-lint-cache.d.ts +17 -1
  247. package/dist/src/core/patch-lint-cache.js +3 -6
  248. package/dist/src/core/patch-lint-checkjs.d.ts +33 -16
  249. package/dist/src/core/patch-lint-checkjs.js +43 -25
  250. package/dist/src/core/patch-lint-creators.d.ts +2 -6
  251. package/dist/src/core/patch-lint-cross.d.ts +16 -8
  252. package/dist/src/core/patch-lint-cross.js +49 -92
  253. package/dist/src/core/patch-lint-css.js +10 -6
  254. package/dist/src/core/patch-lint-file-size.d.ts +52 -0
  255. package/dist/src/core/patch-lint-file-size.js +53 -0
  256. package/dist/src/core/patch-lint-forward-registration.d.ts +58 -0
  257. package/dist/src/core/patch-lint-forward-registration.js +210 -0
  258. package/dist/src/core/patch-lint-jsdoc.js +36 -36
  259. package/dist/src/core/patch-lint-module-registration.d.ts +2 -8
  260. package/dist/src/core/patch-lint-prettier.d.ts +36 -0
  261. package/dist/src/core/patch-lint-prettier.js +126 -0
  262. package/dist/src/core/patch-lint-queue-types.d.ts +60 -0
  263. package/dist/src/core/patch-lint-queue-types.js +12 -0
  264. package/dist/src/core/patch-lint-staged-registration.d.ts +61 -0
  265. package/dist/src/core/patch-lint-staged-registration.js +90 -0
  266. package/dist/src/core/patch-lint-unmanaged-companion.d.ts +106 -0
  267. package/dist/src/core/patch-lint-unmanaged-companion.js +214 -0
  268. package/dist/src/core/patch-lint.d.ts +14 -9
  269. package/dist/src/core/patch-lint.js +27 -27
  270. package/dist/src/core/patch-lock.js +3 -3
  271. package/dist/src/core/patch-manifest-io.js +8 -7
  272. package/dist/src/core/patch-parse.d.ts +19 -8
  273. package/dist/src/core/patch-parse.js +25 -0
  274. package/dist/src/core/patch-policy.js +15 -27
  275. package/dist/src/core/patch-source-metadata.d.ts +1 -3
  276. package/dist/src/core/patch-source-metadata.js +0 -4
  277. package/dist/src/core/patch-transform.d.ts +21 -8
  278. package/dist/src/core/patch-transform.js +38 -10
  279. package/dist/src/core/process-cpu-time.js +1 -1
  280. package/dist/src/core/rebase-session.d.ts +2 -8
  281. package/dist/src/core/rebase-session.js +5 -17
  282. package/dist/src/core/register-browser-content.js +3 -2
  283. package/dist/src/core/register-browser-test.js +2 -1
  284. package/dist/src/core/register-module.js +2 -1
  285. package/dist/src/core/register-scaffold.js +5 -4
  286. package/dist/src/core/register-shared-css.js +2 -1
  287. package/dist/src/core/run-log-redact.d.ts +38 -0
  288. package/dist/src/core/run-log-redact.js +69 -0
  289. package/dist/src/core/run-log.d.ts +13 -2
  290. package/dist/src/core/run-log.js +67 -18
  291. package/dist/src/core/signal-critical.js +2 -4
  292. package/dist/src/core/smoke-patterns.d.ts +0 -1
  293. package/dist/src/core/smoke-patterns.js +18 -11
  294. package/dist/src/core/state-file.d.ts +12 -1
  295. package/dist/src/core/state-file.js +14 -1
  296. package/dist/src/core/status-classify.js +2 -1
  297. package/dist/src/core/test-changed-prefs.js +26 -11
  298. package/dist/src/core/test-harness-crash.js +61 -48
  299. package/dist/src/core/test-harness-verdict-notes.d.ts +55 -0
  300. package/dist/src/core/test-harness-verdict-notes.js +48 -0
  301. package/dist/src/core/test-stale-check.d.ts +23 -8
  302. package/dist/src/core/test-stale-check.js +77 -4
  303. package/dist/src/core/test-stall-triage.js +37 -19
  304. package/dist/src/core/test-xpcshell-retry.d.ts +26 -7
  305. package/dist/src/core/test-xpcshell-retry.js +14 -11
  306. package/dist/src/core/token-category.d.ts +33 -0
  307. package/dist/src/core/token-category.js +56 -48
  308. package/dist/src/core/token-dark-mode.js +2 -14
  309. package/dist/src/core/token-inventory.d.ts +56 -0
  310. package/dist/src/core/token-inventory.js +109 -0
  311. package/dist/src/core/token-manager.d.ts +1 -1
  312. package/dist/src/core/token-manager.js +1 -1
  313. package/dist/src/core/token-variant.js +21 -22
  314. package/dist/src/core/toolchain-preflight.js +1 -1
  315. package/dist/src/core/tree-cow.d.ts +0 -2
  316. package/dist/src/core/tree-cow.js +2 -2
  317. package/dist/src/core/tree-store.js +4 -5
  318. package/dist/src/core/typecheck.js +2 -2
  319. package/dist/src/core/wire-dom-fragment.js +6 -17
  320. package/dist/src/core/wire-utils.js +5 -17
  321. package/dist/src/core/xpcshell-appdir.js +2 -4
  322. package/dist/src/errors/base.d.ts +53 -1
  323. package/dist/src/errors/base.js +79 -11
  324. package/dist/src/errors/build.d.ts +28 -0
  325. package/dist/src/errors/build.js +51 -10
  326. package/dist/src/errors/config.d.ts +0 -14
  327. package/dist/src/errors/config.js +13 -34
  328. package/dist/src/errors/download.d.ts +13 -0
  329. package/dist/src/errors/download.js +53 -24
  330. package/dist/src/errors/furnace.js +8 -9
  331. package/dist/src/errors/git.d.ts +8 -8
  332. package/dist/src/errors/git.js +20 -30
  333. package/dist/src/errors/patch.js +8 -10
  334. package/dist/src/errors/rebase.js +7 -7
  335. package/dist/src/types/commands/options.d.ts +96 -123
  336. package/dist/src/types/commands/patches.d.ts +17 -6
  337. package/dist/src/types/config.d.ts +75 -2
  338. package/dist/src/types/index.d.ts +0 -1
  339. package/dist/src/utils/assert.d.ts +3 -15
  340. package/dist/src/utils/assert.js +1 -18
  341. package/dist/src/utils/build-info.d.ts +1 -1
  342. package/dist/src/utils/child-env.d.ts +2 -1
  343. package/dist/src/utils/elapsed.js +10 -1
  344. package/dist/src/utils/fs.d.ts +4 -2
  345. package/dist/src/utils/fs.js +11 -14
  346. package/dist/src/utils/hash.d.ts +12 -0
  347. package/dist/src/utils/hash.js +16 -0
  348. package/dist/src/utils/line-dispatch.d.ts +15 -0
  349. package/dist/src/utils/line-dispatch.js +48 -0
  350. package/dist/src/utils/options.d.ts +1 -7
  351. package/dist/src/utils/options.js +6 -7
  352. package/dist/src/utils/package-root.js +3 -12
  353. package/dist/src/utils/platform.d.ts +0 -4
  354. package/dist/src/utils/process-group.d.ts +2 -1
  355. package/dist/src/utils/process-group.js +20 -19
  356. package/dist/src/utils/process.d.ts +40 -26
  357. package/dist/src/utils/process.js +115 -166
  358. package/dist/src/utils/ps-duration.js +3 -3
  359. package/dist/src/utils/sleep.d.ts +26 -0
  360. package/dist/src/utils/sleep.js +31 -0
  361. package/dist/src/utils/validation.d.ts +2 -38
  362. package/dist/src/utils/validation.js +7 -16
  363. package/docs/configuration.md +136 -1
  364. package/docs/exit-codes.md +34 -20
  365. package/docs/lifecycle-invariants.md +18 -12
  366. package/docs/machine-output.md +31 -8
  367. package/docs/patch-workflow.md +2 -1
  368. package/docs/run-logs.md +44 -4
  369. package/docs/testing.md +70 -11
  370. package/package.json +8 -7
  371. package/dist/src/commands/rebase.d.ts +0 -4
  372. package/dist/src/commands/rebase.js +0 -5
  373. package/dist/src/core/patch-artifact-normalize.d.ts +0 -21
  374. package/dist/src/core/patch-artifact-normalize.js +0 -24
  375. package/dist/src/core/patch-lint-reexports.d.ts +0 -7
  376. package/dist/src/core/patch-lint-reexports.js +0 -8
  377. package/dist/src/core/wire-targets.d.ts +0 -7
  378. package/dist/src/core/wire-targets.js +0 -8
@@ -9,8 +9,9 @@ import { loadPatchesManifest, resolvePatchIdentifier, stampPatchVersions, } from
9
9
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
10
10
  import { elapsedSince } from '../utils/elapsed.js';
11
11
  import { toError } from '../utils/errors.js';
12
- import { cancel, info, intro, isCancel, outro, spinner, success, warn } from '../utils/logger.js';
12
+ import { cancel, info, intro, isCancel, outro, spinner, success, warn, } from '../utils/logger.js';
13
13
  import { loadScanFilesAssignments, withDryRunPurityGuard, withDryRunReExportLock, } from './re-export-bulk-scan.js';
14
+ import { warnUnseenExpectedDrift } from './re-export-drift.js';
14
15
  import { reExportFilesInPlace } from './re-export-files.js';
15
16
  import { buildReExportLintContext, finishReExportLint } from './re-export-lint.js';
16
17
  import { applyReExportFilesPositionalFolding, validateReExportOptionCombinations, } from './re-export-options.js';
@@ -123,6 +124,8 @@ export async function reExportCommand(projectRoot, patches, options) {
123
124
  binaryName: config.binaryName,
124
125
  furnacePrefixes: await collectFurnaceManagedPrefixes(paths.root),
125
126
  refuseAdjacentUnmanaged: options.refuseAdjacentUnmanaged === true,
127
+ approvedUnmanaged: new Set((options.expectUnmanaged ?? []).map((file) => normalizeEngineRelativeInput(file, '--expect-unmanaged'))),
128
+ approvedSeen: new Set(),
126
129
  refusals: [],
127
130
  };
128
131
  const driftCtx = buildForeignDriftContext(options);
@@ -140,7 +143,17 @@ export async function reExportCommand(projectRoot, patches, options) {
140
143
  const patchOptions = assignedScanFiles !== undefined ? { ...options, scanFiles: assignedScanFiles } : options;
141
144
  progress.message(`Re-exporting ${index + 1}/${selectedPatches.length}: ${patch.filename} (${patch.filesAffected.length} file(s), ${elapsedSince(startedAt)} elapsed)...`);
142
145
  try {
143
- const exported = await reExportSinglePatchWithIndexLockRetry(patch, paths, manifest, patchOptions, isDryRun, config, adjacentCtx, driftCtx, lintCtx);
146
+ const exported = await reExportSinglePatchWithIndexLockRetry({
147
+ patch,
148
+ paths,
149
+ manifest,
150
+ options: patchOptions,
151
+ isDryRun,
152
+ config,
153
+ adjacentCtx,
154
+ driftCtx,
155
+ lintCtx,
156
+ });
144
157
  if (exported) {
145
158
  reExported++;
146
159
  reExportedFilenames.push(patch.filename);
@@ -155,6 +168,7 @@ export async function reExportCommand(projectRoot, patches, options) {
155
168
  }));
156
169
  await finishReExportLint(lintCtx);
157
170
  warnUnseenExpectedDrift(driftCtx);
171
+ warnUnseenApprovedUnmanaged(adjacentCtx);
158
172
  throwRunLevelRefusals(adjacentCtx, driftCtx, progress);
159
173
  if (reExported === 0 && selectedPatches.length > 0) {
160
174
  progress.error('Re-export failed');
@@ -198,26 +212,22 @@ function buildForeignDriftContext(options) {
198
212
  };
199
213
  }
200
214
  /**
201
- * Names `--expect` paths that never drifted this run. A typo'd
202
- * `--expect` path silently degrades the flag back to refusing the slice it
203
- * was meant to admit, so surface the mismatch — but only as a warning: an
204
- * expected file legitimately shows no drift when the slice was already
205
- * captured by an earlier export.
215
+ * Names `--expect-unmanaged` paths that were never met this run. A typo'd
216
+ * carve-out silently leaves the refusal armed for the path it was meant to
217
+ * admit — and, worse, reads as an approval that is in force when it is not.
218
+ * A warning rather than a refusal: an approved path legitimately goes unmet
219
+ * when the reviewed file has since been adopted into a patch, which is the
220
+ * outcome the carve-out exists to be retired by.
206
221
  */
207
- function warnUnseenExpectedDrift(driftCtx) {
208
- const unseen = [...driftCtx.expectedDriftFiles].filter((file) => !driftCtx.expectedSeen.has(file));
222
+ function warnUnseenApprovedUnmanaged(adjacentCtx) {
223
+ const unseen = [...adjacentCtx.approvedUnmanaged].filter((file) => !adjacentCtx.approvedSeen.has(file));
209
224
  if (unseen.length === 0)
210
225
  return;
211
- if (driftCtx.evaluationRuns === 0) {
212
- warn(`--expect path(s) were not evaluated because no selected patch reached the drift check (for example, an earlier refusal): ${unseen.join(', ')}`);
213
- return;
214
- }
215
- warn('--expect path(s) showed no drift this run: ' +
226
+ warn('--expect-unmanaged path(s) were not met as adjacent unmanaged files this run: ' +
216
227
  `${unseen.join(', ')}. ` +
217
- 'Causes, in rough order of likelihood: a typo in the path; the slice was already ' +
218
- 'captured by an earlier export in this session; or the engine content already matches ' +
219
- 'the patch body — a concurrent session may have exported the same change, so the drift ' +
220
- 'converged before this run looked.');
228
+ 'Causes, in rough order of likelihood: a typo in the path; the file is now owned by a ' +
229
+ 'patch (the carve-out can be dropped); or no selected patch is adjacent to it. The ' +
230
+ 'refusal stayed armed for everything else either way.');
221
231
  }
222
232
  /**
223
233
  * Turns the run's collected `--refuse-adjacent-unmanaged` and
@@ -227,19 +237,26 @@ function warnUnseenExpectedDrift(driftCtx) {
227
237
  function throwRunLevelRefusals(adjacentCtx, driftCtx, progress) {
228
238
  if (adjacentCtx.refusals.length > 0) {
229
239
  progress.error('Re-export refused');
230
- const names = adjacentCtx.refusals.map((r) => r.patchFilename).join(', ');
231
- throw new GeneralError(`Refused ${String(adjacentCtx.refusals.length)} patch(es) with adjacent unmanaged files ` +
232
- `(--refuse-adjacent-unmanaged): ${names}. Adopt reviewed files with --scan --scan-file, ` +
233
- `or export them as a new patch.`);
240
+ // Each offender is named with the owned directory that made it
241
+ // adjacent: an unattended run reads only this line, and without the
242
+ // anchor it cannot tell which of a multi-directory patch's locations
243
+ // the file sits beside.
244
+ const offenders = adjacentCtx.refusals
245
+ .map((r) => `${r.patchFilename} (${r.anchored.join(', ')})`)
246
+ .join('; ');
247
+ throw new GeneralError(`Refused ${adjacentCtx.refusals.length} patch(es) with adjacent unmanaged files ` +
248
+ `(--refuse-adjacent-unmanaged): ${offenders}. Adjacent means the file sits in a ` +
249
+ `directory the patch already owns a file in. Adopt reviewed files with ` +
250
+ `--scan --scan-file, or export them as a new patch.`);
234
251
  }
235
252
  if (driftCtx.refusals.length > 0) {
236
253
  progress.error('Re-export refused');
237
254
  const names = driftCtx.refusals.map((r) => r.patchFilename).join(', ');
238
255
  const unreadable = driftCtx.refusals.filter((r) => r.reason === 'baseline-unreadable');
239
256
  const unreadablePart = unreadable.length > 0
240
- ? ` ${String(unreadable.length)} of these refused because the old patch body was missing or unreadable, so the drift comparison could not run (fail-closed).`
257
+ ? ` ${unreadable.length} of these refused because the old patch body was missing or unreadable, so the drift comparison could not run (fail-closed).`
241
258
  : '';
242
- throw new GeneralError(`Refused ${String(driftCtx.refusals.length)} patch(es) whose refreshed body would absorb ` +
259
+ throw new GeneralError(`Refused ${driftCtx.refusals.length} patch(es) whose refreshed body would absorb ` +
243
260
  `engine edits not present in the old patch body (--refuse-foreign-drift): ${names}.${unreadablePart}\n\n` +
244
261
  'The flag cannot tell WHO wrote those lines, and the two populations have opposite ' +
245
262
  'remedies:\n' +
@@ -283,9 +300,9 @@ function reportReExportOutcome(args) {
283
300
  if (reExported < selectedCount) {
284
301
  const skippedCount = selectedCount - reExported - args.failedFilenames.length;
285
302
  const failedPart = args.failedFilenames.length > 0 ? ` Failed: ${args.failedFilenames.join(', ')}.` : '';
286
- const skippedPart = skippedCount > 0 ? ` Skipped: ${String(skippedCount)} patch(es) (see warnings above).` : '';
287
- throw new GeneralError(`${isDryRun ? '[dry-run] Would re-export' : 'Re-exported'} only ${String(reExported)} of ` +
288
- `${String(selectedCount)} selected patch(es).${failedPart}${skippedPart} ` +
303
+ const skippedPart = skippedCount > 0 ? ` Skipped: ${skippedCount} patch(es) (see warnings above).` : '';
304
+ throw new GeneralError(`${isDryRun ? '[dry-run] Would re-export' : 'Re-exported'} only ${reExported} of ` +
305
+ `${selectedCount} selected patch(es).${failedPart}${skippedPart} ` +
289
306
  'Partial re-export exits non-zero so it cannot pass as success in scripts; ' +
290
307
  'the per-patch errors are listed above.');
291
308
  }
@@ -2,12 +2,13 @@
2
2
  /**
3
3
  * Rebase abort flow.
4
4
  */
5
- import { getProjectPaths, updateState } from '../../core/config.js';
5
+ import { getProjectPaths } from '../../core/config.js';
6
6
  import { clearAppliedFurnaceState } from '../../core/furnace-config.js';
7
7
  import { resetChanges } from '../../core/git.js';
8
8
  import { clearRebaseSession, getRebaseSessionPath, readRebaseSession, } from '../../core/rebase-session.js';
9
9
  import { NoRebaseSessionError } from '../../errors/rebase.js';
10
10
  import { intro, outro, spinner, success, warn } from '../../utils/logger.js';
11
+ import { clearPendingResolution } from '../pending-resolution.js';
11
12
  import { confirmDirtyEngineReset } from './confirm.js';
12
13
  /**
13
14
  * Handles `fireforge rebase --abort`.
@@ -57,13 +58,7 @@ export async function handleAbort(projectRoot, yes) {
57
58
  // idempotently re-clear furnace state.
58
59
  await clearAppliedFurnaceState(projectRoot);
59
60
  // Step 3: clear pending resolution transactionally.
60
- await updateState(projectRoot, (current) => {
61
- if (!current.pendingResolution)
62
- return current;
63
- const next = { ...current };
64
- delete next.pendingResolution;
65
- return next;
66
- });
61
+ await clearPendingResolution(projectRoot);
67
62
  // Step 4: clear the rebase session LAST so a failure in any prior step
68
63
  // preserves the session on disk and a retry of --abort can succeed.
69
64
  await clearRebaseSession(projectRoot);
@@ -3,7 +3,7 @@
3
3
  * Rebase --continue flow.
4
4
  */
5
5
  import { join } from 'node:path';
6
- import { getProjectPaths, updateState } from '../../core/config.js';
6
+ import { getProjectPaths } from '../../core/config.js';
7
7
  import { getStagedDiffForFiles } from '../../core/git-diff.js';
8
8
  import { stageFiles, unstageFiles } from '../../core/git-file-ops.js';
9
9
  import { updatePatchAndMetadata } from '../../core/patch-export.js';
@@ -14,6 +14,7 @@ import { GeneralError } from '../../errors/base.js';
14
14
  import { CorruptRebaseSessionError, NoRebaseSessionError, RebaseError, } from '../../errors/rebase.js';
15
15
  import { pathExists } from '../../utils/fs.js';
16
16
  import { info, intro, success, warn } from '../../utils/logger.js';
17
+ import { clearPendingResolution } from '../pending-resolution.js';
17
18
  import { runPatchLoop } from './patch-loop.js';
18
19
  /**
19
20
  * Handles `fireforge rebase --continue`.
@@ -77,10 +78,15 @@ export async function handleContinue(projectRoot, maxFuzz, waitLockSeconds) {
77
78
  // updatePatchMetadata sequence can interleave with a concurrent export /
78
79
  // re-export / patch reorder / patch compact and leave the manifest
79
80
  // disagreeing with the freshly-written patch body. Mirrors resolve.ts.
80
- await updatePatchAndMetadata(paths.patches, currentPatch.filename, diffContent, {
81
- sourceEsrVersion: session.toVersion,
82
- sourceVersion: session.toVersion,
83
- ...(session.toProduct !== undefined ? { sourceProduct: session.toProduct } : {}),
81
+ await updatePatchAndMetadata({
82
+ patchesDir: paths.patches,
83
+ filename: currentPatch.filename,
84
+ newContent: diffContent,
85
+ updates: {
86
+ sourceEsrVersion: session.toVersion,
87
+ sourceVersion: session.toVersion,
88
+ ...(session.toProduct !== undefined ? { sourceProduct: session.toProduct } : {}),
89
+ },
84
90
  });
85
91
  }
86
92
  finally {
@@ -104,13 +110,7 @@ export async function handleContinue(projectRoot, maxFuzz, waitLockSeconds) {
104
110
  await saveRebaseSession(projectRoot, session);
105
111
  // Clear pending resolution transactionally so concurrent state-file
106
112
  // writes to unrelated keys are not clobbered by a stale reload.
107
- await updateState(projectRoot, (current) => {
108
- if (!current.pendingResolution)
109
- return current;
110
- const next = { ...current };
111
- delete next.pendingResolution;
112
- return next;
113
- });
113
+ await clearPendingResolution(projectRoot);
114
114
  });
115
115
  success(`Resolved ${currentPatch.filename}`);
116
116
  // Continue applying remaining patches
@@ -17,7 +17,7 @@ import { clearAppliedFurnaceState } from '../../core/furnace-config.js';
17
17
  import { getHead, resetChanges } from '../../core/git.js';
18
18
  import { discoverPatches } from '../../core/patch-files.js';
19
19
  import { loadPatchesManifest } from '../../core/patch-manifest.js';
20
- import { getPatchSourceProduct, getPatchSourceVersion } from '../../core/patch-source-metadata.js';
20
+ import { getPatchSourceVersion } from '../../core/patch-source-metadata.js';
21
21
  import { getRebaseSessionPath, readRebaseSession, saveRebaseSession, } from '../../core/rebase-session.js';
22
22
  import { GeneralError } from '../../errors/base.js';
23
23
  import { CorruptRebaseSessionError, RebaseSessionExistsError } from '../../errors/rebase.js';
@@ -59,7 +59,7 @@ async function handleFreshStart(projectRoot, options) {
59
59
  }
60
60
  // Determine the "from" version from the patches
61
61
  const patchVersions = new Set(manifest.patches.map((p) => getPatchSourceVersion(p)));
62
- const patchProducts = new Set(manifest.patches.map((p) => getPatchSourceProduct(p)).filter(Boolean));
62
+ const patchProducts = new Set(manifest.patches.map((p) => p.sourceProduct).filter(Boolean));
63
63
  const sortedVersions = [...patchVersions].sort();
64
64
  const fromVersion = sortedVersions[0] ?? currentVersion;
65
65
  const fromProduct = [...patchProducts].sort()[0] ?? config.firefox.product;
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Patch application loop and re-export flow.
3
3
  */
4
- import type { getProjectPaths } from '../../core/config.js';
5
4
  import type { RebaseSession } from '../../core/rebase-session.js';
5
+ import type { ProjectPaths } from '../../types/config.js';
6
6
  /**
7
7
  * Runs the patch application loop, re-exports applied patches, and stamps versions.
8
8
  *
@@ -12,4 +12,4 @@ import type { RebaseSession } from '../../core/rebase-session.js';
12
12
  * @param maxFuzz - Maximum context-reduction steps for `git apply -C<n>`
13
13
  * @param waitLockSeconds - `--wait-lock` budget for the patch-directory lock
14
14
  */
15
- export declare function runPatchLoop(projectRoot: string, session: RebaseSession, paths: ReturnType<typeof getProjectPaths>, maxFuzz: number, waitLockSeconds?: number): Promise<void>;
15
+ export declare function runPatchLoop(projectRoot: string, session: RebaseSession, paths: ProjectPaths, maxFuzz: number, waitLockSeconds?: number): Promise<void>;
@@ -20,6 +20,7 @@ import { toError } from '../../utils/errors.js';
20
20
  import { pathExists } from '../../utils/fs.js';
21
21
  import { error, info, outro, spinner, success, warn } from '../../utils/logger.js';
22
22
  import { isValidFirefoxVersion } from '../../utils/validation.js';
23
+ import { clearPendingResolution } from '../pending-resolution.js';
23
24
  import { buildRebaseConflictSummary } from './conflict-summary.js';
24
25
  import { printSummary } from './summary.js';
25
26
  /**
@@ -208,13 +209,7 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz, waitLoc
208
209
  await clearRebaseSession(projectRoot);
209
210
  // Clear pending resolution if any (transactionally, so a concurrent
210
211
  // state write to an unrelated key is not clobbered by a stale reload).
211
- await updateState(projectRoot, (current) => {
212
- if (!current.pendingResolution)
213
- return current;
214
- const next = { ...current };
215
- delete next.pendingResolution;
216
- return next;
217
- });
212
+ await clearPendingResolution(projectRoot);
218
213
  info('');
219
214
  success(`All patches re-exported with sourceVersion=${session.toVersion}`);
220
215
  outro('Rebase complete!');
@@ -2,7 +2,7 @@
2
2
  import { join } from 'node:path';
3
3
  import { confirm } from '@clack/prompts';
4
4
  import { Command } from 'commander';
5
- import { getProjectPaths, loadConfig, loadState, updateState } from '../core/config.js';
5
+ import { getProjectPaths, loadConfig, loadState } from '../core/config.js';
6
6
  import { stdioIsInteractive } from '../core/destructive.js';
7
7
  import { assertEngineGitReady } from '../core/engine-precondition.js';
8
8
  import { getStagedDiffForFiles } from '../core/git-diff.js';
@@ -16,6 +16,7 @@ import { toError } from '../utils/errors.js';
16
16
  import { pathExists } from '../utils/fs.js';
17
17
  import { error as logError, info, intro, isCancel, outro, spinner, success, } from '../utils/logger.js';
18
18
  import { addWaitLockOption, resolveWaitLockSeconds } from '../utils/options.js';
19
+ import { clearPendingResolution } from './pending-resolution.js';
19
20
  /**
20
21
  * Runs the resolve command to fix broken patches.
21
22
  * @param projectRoot - Root directory of the project
@@ -123,20 +124,27 @@ export async function resolveCommand(projectRoot, options = {}) {
123
124
  // here keeps resolve in agreement with every other writer.
124
125
  const diffFilesAffected = extractAffectedFiles(diffContent);
125
126
  const config = await loadConfig(projectRoot);
126
- await updatePatchAndMetadata(paths.patches, patchFilename, diffContent, {
127
- filesAffected: diffFilesAffected,
128
- ...buildPatchSourceMetadata(config.firefox),
129
- }, undefined, undefined, { waitLockSeconds: resolveWaitLockSeconds(options.waitLock), command: 'resolve' });
127
+ await updatePatchAndMetadata({
128
+ patchesDir: paths.patches,
129
+ filename: patchFilename,
130
+ newContent: diffContent,
131
+ updates: {
132
+ filesAffected: diffFilesAffected,
133
+ ...buildPatchSourceMetadata(config.firefox),
134
+ },
135
+ onCommitted: undefined,
136
+ policyGate: undefined,
137
+ lockOptions: {
138
+ waitLockSeconds: resolveWaitLockSeconds(options.waitLock),
139
+ command: 'resolve',
140
+ },
141
+ });
130
142
  // Clear pendingResolution from state.json transactionally so a
131
143
  // concurrent update to unrelated keys (another command writing buildMode
132
144
  // or baseCommit between our load and save) is not clobbered. The updater
133
145
  // runs inside the state-file lock with the freshest on-disk state, so
134
146
  // only pendingResolution is affected.
135
- await updateState(projectRoot, (current) => {
136
- const next = { ...current };
137
- delete next.pendingResolution;
138
- return next;
139
- });
147
+ await clearPendingResolution(projectRoot);
140
148
  s.stop(`Updated ${patchFilename}`);
141
149
  success('Patch updated successfully and resolution state cleared.');
142
150
  info('Patch updated. Run "fireforge import" next to resume the queue from this point — resolve only refreshes the one broken patch, it does not continue applying the remaining patches itself.');
@@ -1,19 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import type { CommandContext } from '../types/cli.js';
3
3
  import type { RunOptions } from '../types/commands/index.js';
4
- /**
5
- * Exit code returned by smoke-run mode when the captured console stream
6
- * produced one or more error lines that did NOT match the operator's
7
- * allowlist.
8
- */
9
- export declare const SMOKE_EXIT_FAILURE: 12;
10
- /**
11
- * Exit code returned by smoke-run mode when the browser itself exited
12
- * with a non-clean status before the smoke window elapsed — i.e. a
13
- * launch-side failure we could NOT observe as a console error line
14
- * (crash before console wiring, missing profile, etc.).
15
- */
16
- export declare const SMOKE_LAUNCH_FAILURE: 13;
17
4
  /**
18
5
  * Runs the run command to launch the built browser.
19
6
  * @param projectRoot - Root directory of the project
@@ -17,19 +17,6 @@ import { toError } from '../utils/errors.js';
17
17
  import { pathExists, removeDir, removeFile } from '../utils/fs.js';
18
18
  import { info, intro, verbose, warn } from '../utils/logger.js';
19
19
  import { commanderArgParser, pickDefined, stringListOption } from '../utils/options.js';
20
- /**
21
- * Exit code returned by smoke-run mode when the captured console stream
22
- * produced one or more error lines that did NOT match the operator's
23
- * allowlist.
24
- */
25
- export const SMOKE_EXIT_FAILURE = ExitCode.SMOKE_EXIT_FAILURE;
26
- /**
27
- * Exit code returned by smoke-run mode when the browser itself exited
28
- * with a non-clean status before the smoke window elapsed — i.e. a
29
- * launch-side failure we could NOT observe as a console error line
30
- * (crash before console wiring, missing profile, etc.).
31
- */
32
- export const SMOKE_LAUNCH_FAILURE = ExitCode.SMOKE_LAUNCH_FAILURE;
33
20
  /** Recommendation surfaced when the smoke window is shorter than a typical cold start. */
34
21
  const SMOKE_COLD_START_THRESHOLD_MS = 30_000;
35
22
  /** Maximum number of unallowed error lines to surface in the terminal summary. */
@@ -157,7 +144,7 @@ async function runSmokeExit(engineDir, options) {
157
144
  // Not an error — cold starts just tend to exceed the window. Surfacing
158
145
  // the hint here instead of failing lets agents run shorter windows
159
146
  // intentionally (e.g. warm-cache smoke checks).
160
- verbose(`Smoke window is ${String(smokeExit)}s; cold starts on slow machines often exceed 30s.`);
147
+ verbose(`Smoke window is ${smokeExit}s; cold starts on slow machines often exceed 30s.`);
161
148
  }
162
149
  const allowlist = await buildAllowlist(options);
163
150
  const captureStream = options.captureConsole
@@ -216,7 +203,7 @@ async function runSmokeExit(engineDir, options) {
216
203
  'console capture and can fail the run. Pass --headless (or run on an unattended host) ' +
217
204
  'for reliable smoke checks.');
218
205
  }
219
- info(`Launching browser (smoke-exit after ${String(smokeExit)}s)...\n`);
206
+ info(`Launching browser (smoke-exit after ${smokeExit}s)...\n`);
220
207
  const startedAt = Date.now();
221
208
  let result;
222
209
  try {
@@ -248,7 +235,7 @@ async function runSmokeExit(engineDir, options) {
248
235
  });
249
236
  // Exit contract (precedence: unallowed errors dominate timed-out).
250
237
  if (findings.length > 0) {
251
- throw new SmokeRunError(`Smoke run observed ${String(findings.length)} unallowed console error(s).`, SMOKE_EXIT_FAILURE);
238
+ throw new SmokeRunError(`Smoke run observed ${findings.length} unallowed console error(s).`, ExitCode.SMOKE_EXIT_FAILURE);
252
239
  }
253
240
  if (result.timedOut) {
254
241
  // Clean window — SIGTERM from us. Treat as success.
@@ -257,7 +244,7 @@ async function runSmokeExit(engineDir, options) {
257
244
  if (result.exitCode === 0 || result.exitCode === 130 || result.exitCode === 143) {
258
245
  return;
259
246
  }
260
- throw new SmokeRunError(`Browser exited with code ${String(result.exitCode)} before smoke-exit window elapsed.`, SMOKE_LAUNCH_FAILURE);
247
+ throw new SmokeRunError(`Browser exited with code ${result.exitCode} before smoke-exit window elapsed.`, ExitCode.SMOKE_LAUNCH_FAILURE);
261
248
  }
262
249
  /**
263
250
  * Compiles the active allowlist from `--console-allow` CLI values and
@@ -298,15 +285,15 @@ function reportSmokeSummary(args) {
298
285
  const suffix = args.timedOut ? ' (deadline fired — SIGTERM sent to process group)' : '';
299
286
  info('');
300
287
  info(`Smoke run complete: ${seconds}s elapsed of ${windowSeconds}s window${suffix}`);
301
- info(` Unallowed errors: ${String(args.findings.length)}`);
288
+ info(` Unallowed errors: ${args.findings.length}`);
302
289
  // The "suppressed errors" count is what the exit contract cares about — the
303
290
  // subset of allowlisted hits that would otherwise have been tallied as
304
291
  // findings. The "all allowlisted lines" count answers the operator's mental
305
292
  // model ("my --console-allow pattern matched N console lines"), without
306
293
  // which a visibly matching regex reports 0 hits.
307
- info(` Allowlisted error hits (suppressed): ${String(args.allowlistedErrorHits)}`);
308
- info(` Allowlisted lines total: ${String(args.allowlistedTotalHits)}`);
309
- info(` Child exit code: ${String(args.exitCode)}`);
294
+ info(` Allowlisted error hits (suppressed): ${args.allowlistedErrorHits}`);
295
+ info(` Allowlisted lines total: ${args.allowlistedTotalHits}`);
296
+ info(` Child exit code: ${args.exitCode}`);
310
297
  // Per-entry attribution: first-match credit per line, with
311
298
  // zero-hit entries always visible — an allowlist entry whose suppressed
312
299
  // shape changed upstream is only detectable as a 0× row.
@@ -315,19 +302,19 @@ function reportSmokeSummary(args) {
315
302
  args.allowlist.forEach((entry, index) => {
316
303
  const hits = args.allowlistHits[index] ?? 0;
317
304
  const zeroSuffix = hits === 0 ? ' (never matched — candidate for removal)' : '';
318
- info(` ${String(hits)}× ${entry.origin} ${entry.source}${zeroSuffix}`);
305
+ info(` ${hits}× ${entry.origin} ${entry.source}${zeroSuffix}`);
319
306
  });
320
307
  }
321
308
  if (args.findings.length === 0)
322
309
  return;
323
310
  warn('');
324
- warn(`Unallowed console errors (first ${String(SMOKE_UNALLOWED_PREVIEW_MAX)}):`);
311
+ warn(`Unallowed console errors (first ${SMOKE_UNALLOWED_PREVIEW_MAX}):`);
325
312
  args.findings.slice(0, SMOKE_UNALLOWED_PREVIEW_MAX).forEach((finding, index) => {
326
- warn(` ${String(index + 1)}. [${finding.stream}] ${finding.line}`);
313
+ warn(` ${index + 1}. [${finding.stream}] ${finding.line}`);
327
314
  });
328
315
  if (args.findings.length > SMOKE_UNALLOWED_PREVIEW_MAX) {
329
316
  const remaining = args.findings.length - SMOKE_UNALLOWED_PREVIEW_MAX;
330
- warn(` …and ${String(remaining)} more.`);
317
+ warn(` …and ${remaining} more.`);
331
318
  }
332
319
  }
333
320
  /** Registers the run command on the CLI program. */
@@ -9,10 +9,22 @@ export interface ResolvedSetupInputs {
9
9
  finalProduct: FirefoxProduct;
10
10
  finalLicense: ProjectLicense;
11
11
  }
12
- /** Parses an optional Firefox product flag into a typed product value. */
13
- export declare function parseFirefoxProductOption(product: string | undefined): FirefoxProduct | undefined;
14
- /** Parses an optional license flag into a validated SPDX identifier. */
15
- export declare function parseProjectLicenseOption(license: string | undefined): ProjectLicense | undefined;
12
+ /**
13
+ * Narrows a raw CLI or prompt value to a supported Firefox product.
14
+ * @param value - Raw value from `--product` or the setup prompt.
15
+ * @param field - Field name quoted in the refusal (e.g. `--product`).
16
+ * @returns The validated product.
17
+ * @throws {@link InvalidArgumentError} when the value names no supported product.
18
+ */
19
+ export declare function resolveFirefoxProduct(value: unknown, field: string): FirefoxProduct;
20
+ /**
21
+ * Narrows a raw CLI or prompt value to a supported SPDX licence identifier.
22
+ * @param value - Raw value from `--license` or the setup prompt.
23
+ * @param field - Field name quoted in the refusal (e.g. `--license`).
24
+ * @returns The validated licence identifier.
25
+ * @throws {@link InvalidArgumentError} when the value names no supported licence.
26
+ */
27
+ export declare function resolveProjectLicense(value: unknown, field: string): ProjectLicense;
16
28
  /** Validates non-interactive setup options before project scaffolding begins. */
17
29
  export declare function validateSetupOptions(options: SetupOptions): void;
18
30
  /** Resolves setup inputs from CLI flags and optional interactive prompts. */
@@ -18,7 +18,14 @@ function renderLicenseTemplate(license, template, vendor, now = new Date()) {
18
18
  }
19
19
  return template.replace(/\[year\]/g, String(now.getFullYear())).replace(/\[fullname\]/g, vendor);
20
20
  }
21
- function resolveFirefoxProduct(value, field) {
21
+ /**
22
+ * Narrows a raw CLI or prompt value to a supported Firefox product.
23
+ * @param value - Raw value from `--product` or the setup prompt.
24
+ * @param field - Field name quoted in the refusal (e.g. `--product`).
25
+ * @returns The validated product.
26
+ * @throws {@link InvalidArgumentError} when the value names no supported product.
27
+ */
28
+ export function resolveFirefoxProduct(value, field) {
22
29
  // Matches the sibling `resolveProjectLicense`, which already delegates to
23
30
  // its guard. The `||` chain this replaced spelled the member list twice —
24
31
  // once as the predicate and once in the message.
@@ -27,26 +34,19 @@ function resolveFirefoxProduct(value, field) {
27
34
  }
28
35
  throw new InvalidArgumentError(`Invalid product (use: ${FIREFOX_PRODUCTS.join(', ')})`, field);
29
36
  }
30
- function resolveProjectLicense(value, field) {
37
+ /**
38
+ * Narrows a raw CLI or prompt value to a supported SPDX licence identifier.
39
+ * @param value - Raw value from `--license` or the setup prompt.
40
+ * @param field - Field name quoted in the refusal (e.g. `--license`).
41
+ * @returns The validated licence identifier.
42
+ * @throws {@link InvalidArgumentError} when the value names no supported licence.
43
+ */
44
+ export function resolveProjectLicense(value, field) {
31
45
  if (typeof value === 'string' && isValidProjectLicense(value)) {
32
46
  return value;
33
47
  }
34
48
  throw new InvalidArgumentError('Invalid license (use: EUPL-1.2, MPL-2.0, 0BSD, GPL-2.0-or-later)', field);
35
49
  }
36
- /** Parses an optional Firefox product flag into a typed product value. */
37
- export function parseFirefoxProductOption(product) {
38
- if (product === undefined) {
39
- return undefined;
40
- }
41
- return resolveFirefoxProduct(product, '--product');
42
- }
43
- /** Parses an optional license flag into a validated SPDX identifier. */
44
- export function parseProjectLicenseOption(license) {
45
- if (license === undefined) {
46
- return undefined;
47
- }
48
- return resolveProjectLicense(license, '--license');
49
- }
50
50
  /** Validates non-interactive setup options before project scaffolding begins. */
51
51
  export function validateSetupOptions(options) {
52
52
  if (options.name !== undefined) {
@@ -8,7 +8,7 @@ import { ConfigError } from '../errors/config.js';
8
8
  import { cancel, intro, isCancel, note, outro, spinner } from '../utils/logger.js';
9
9
  import { pickDefined } from '../utils/options.js';
10
10
  import { FIREFOX_PRODUCTS, PROJECT_LICENSES } from '../utils/validation.js';
11
- import { buildSetupConfig, parseFirefoxProductOption, parseProjectLicenseOption, resolveSetupInputs, validateSetupOptions, writeSetupProjectFiles, } from './setup-support.js';
11
+ import { buildSetupConfig, resolveFirefoxProduct, resolveProjectLicense, resolveSetupInputs, validateSetupOptions, writeSetupProjectFiles, } from './setup-support.js';
12
12
  /**
13
13
  * Runs the setup command.
14
14
  * @param projectRoot - Root directory for the project
@@ -83,16 +83,10 @@ export function registerSetup(program, { withErrorHandling }) {
83
83
  if (yes === true)
84
84
  setupOptions.force = true;
85
85
  if (product !== undefined) {
86
- const parsedProduct = parseFirefoxProductOption(product);
87
- if (parsedProduct !== undefined) {
88
- setupOptions.product = parsedProduct;
89
- }
86
+ setupOptions.product = resolveFirefoxProduct(product, '--product');
90
87
  }
91
88
  if (license !== undefined) {
92
- const parsedLicense = parseProjectLicenseOption(license);
93
- if (parsedLicense !== undefined) {
94
- setupOptions.license = parsedLicense;
95
- }
89
+ setupOptions.license = resolveProjectLicense(license, '--license');
96
90
  }
97
91
  await setupCommand(resolve(process.cwd()), setupOptions);
98
92
  }));
@@ -87,7 +87,14 @@ export async function sourceSetCommand(projectRoot, options) {
87
87
  }
88
88
  /** Registers the source command on the CLI program. */
89
89
  export function registerSource(program, { getProjectRoot, withErrorHandling }) {
90
- const source = program.command('source').description('Manage Firefox source configuration');
90
+ const source = program
91
+ .command('source')
92
+ .description('Manage Firefox source configuration')
93
+ // No-subcommand contract shared with `patch`/`token`/`furnace`: help on
94
+ // stdout, exit 0 (see `handleParseError` for the groups without an action).
95
+ .action(() => {
96
+ source.outputHelp();
97
+ });
91
98
  source
92
99
  .command('set')
93
100
  .description('Atomically set Firefox source version, product, and optional checksum')
@@ -95,9 +95,9 @@ export function runStatusCheck(classified, policy) {
95
95
  const offending = collectStatusCheckOffenders(classified, policy).map((offender) => {
96
96
  const preview = offender.files.slice(0, CHECK_FILE_PREVIEW_MAX).join(', ');
97
97
  const more = offender.count > CHECK_FILE_PREVIEW_MAX
98
- ? `, +${String(offender.count - CHECK_FILE_PREVIEW_MAX)} more`
98
+ ? `, +${offender.count - CHECK_FILE_PREVIEW_MAX} more`
99
99
  : '';
100
- return `${String(offender.count)} ${offender.classification} (${preview}${more})`;
100
+ return `${offender.count} ${offender.classification} (${preview}${more})`;
101
101
  });
102
102
  if (offending.length === 0)
103
103
  return;
@@ -219,7 +219,7 @@ export function renderTestCoverageStatus(baseline) {
219
219
  return;
220
220
  }
221
221
  const list = coverage.map((path) => ` - ${path}`).join('\n');
222
- info(`Test packaging coverage: scoped to ${String(coverage.length)} path(s):\n${list}\n` +
222
+ info(`Test packaging coverage: scoped to ${coverage.length} path(s):\n${list}\n` +
223
223
  'A "fireforge test" over paths outside this list will be refused as uncovered.');
224
- outro(`Coverage: scoped (${String(coverage.length)} paths)`);
224
+ outro(`Coverage: scoped (${coverage.length} paths)`);
225
225
  }
@@ -15,7 +15,7 @@ import { GeneralError, InvalidArgumentError } from '../errors/base.js';
15
15
  import { FIREFORGE_TMP_PATH_PATTERN, pathExists } from '../utils/fs.js';
16
16
  import { info, intro, isMachineOutputMode, outro, setMachineOutputMode, warn, } from '../utils/logger.js';
17
17
  import { emitMachineError } from '../utils/machine-output.js';
18
- import { resolveStatusCheckPolicy, runStatusCheck } from './status-check.js';
18
+ import { resolveStatusCheckPolicy, runStatusCheck, } from './status-check.js';
19
19
  import { renderJsonStatus, renderJsonSummaryStatus } from './status-json.js';
20
20
  import { renderDefaultStatus, renderTestCoverageStatus, renderUnmanagedOnly, } from './status-output.js';
21
21
  import { buildOwnershipJsonBlock, collectOwnershipRows, summarizeOwnership, } from './status-ownership.js';
@@ -253,7 +253,6 @@ async function runStatusCommandBody(projectRoot, options) {
253
253
  const config = await loadConfig(projectRoot);
254
254
  // Shared envelope: see src/utils/machine-output.ts and
255
255
  // docs/machine-output.md. `status` was the only command that had one.
256
- const emitJsonError = emitMachineError;
257
256
  // Ownership mode is a flat file→patch table; sources are the manifest's
258
257
  // filesAffected, any worktree drift, and the cross-patch
259
258
  // duplicate-new-file-creation map produced by walking each patch body. The
@@ -292,14 +291,14 @@ async function runStatusCommandBody(projectRoot, options) {
292
291
  // message is kept identical to the helper's on purpose.
293
292
  if (!(await pathExists(paths.engine))) {
294
293
  if (options.json) {
295
- emitJsonError('engine-missing', 'Firefox source not found. Run "fireforge download" first.');
294
+ emitMachineError('engine-missing', 'Firefox source not found. Run "fireforge download" first.');
296
295
  }
297
296
  throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
298
297
  }
299
298
  // Check if it's a git repository
300
299
  if (!(await isGitRepository(paths.engine))) {
301
300
  if (options.json) {
302
- emitJsonError('engine-not-git', 'Engine directory is not a git repository. Run "fireforge download" to initialize.');
301
+ emitMachineError('engine-not-git', 'Engine directory is not a git repository. Run "fireforge download" to initialize.');
303
302
  }
304
303
  throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
305
304
  }