@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
@@ -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
  }
@@ -3,8 +3,8 @@
3
3
  * the per-file line budget): the incremental protected mach build with
4
4
  * its baseline write, and the `--build-only` union-build shape.
5
5
  */
6
- import type { getProjectPaths, loadConfig } from '../core/config.js';
7
6
  import type { TestOptions } from '../types/commands/index.js';
7
+ import type { FireForgeConfig, ProjectPaths } from '../types/config.js';
8
8
  import type { HarnessClassification } from './test-modes.js';
9
9
  /**
10
10
  * Runs the pre-test build (or the stale-build gate when no build was
@@ -13,7 +13,7 @@ import type { HarnessClassification } from './test-modes.js';
13
13
  * (mixed harnesses legal, nothing dispatches), prints the per-harness
14
14
  * next steps, and emits the run's single `PASS` verdict.
15
15
  */
16
- export declare function runTestBuildPhase(projectRoot: string, paths: ReturnType<typeof getProjectPaths>, projectConfig: Awaited<ReturnType<typeof loadConfig>>, harnessRetries: number, options: TestOptions, request: {
16
+ export declare function runTestBuildPhase(projectRoot: string, paths: ProjectPaths, projectConfig: FireForgeConfig, harnessRetries: number, options: TestOptions, request: {
17
17
  classification: HarnessClassification;
18
18
  normalizedPaths: string[];
19
19
  }): Promise<boolean>;
@@ -16,7 +16,14 @@ import { info, notice, spinner, verbose } from '../utils/logger.js';
16
16
  import { reportBuildOnlyCompletion } from './test-modes.js';
17
17
  import { enforceStaleBuildGate, enforceStaticComponentsGate } from './test-stale-gate.js';
18
18
  import { emitPassVerdict } from './test-verdict.js';
19
- async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries, testPackagingCoverage, refuseUnexportedDrift, extend) {
19
+ /**
20
+ * Runs the incremental pre-test build under the build lock and records the
21
+ * baseline it produced.
22
+ *
23
+ * @param options - See {@link RunPreTestBuildOptions}
24
+ */
25
+ async function runPreTestBuild(options) {
26
+ const { projectRoot, paths, projectConfig, harnessRetries, testPackagingCoverage, refuseUnexportedDrift, extend, } = options;
20
27
  await withBuildLock(projectRoot, async () => {
21
28
  // Pass the previous baseline exactly like `fireforge build` does, so
22
29
  // auto-configure runs under the same conditions on both paths. The
@@ -81,11 +88,20 @@ async function runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries
81
88
  const recordedCoverage = extend !== undefined
82
89
  ? unionTestPackagingCoverage(previousBaseline?.testPackagingCoverage, extend.requestedPaths)
83
90
  : testPackagingCoverage;
84
- await writeBuildBaseline(projectRoot, paths.engine, projectConfig.binaryName, recordedCoverage, previousBaseline, describeBuildInvocation(extend !== undefined, testPackagingCoverage), preparation.fullBuildRequired
85
- ? 'refresh'
86
- : extend !== undefined
87
- ? 'carry-forward'
88
- : 'auto', buildKind);
91
+ await writeBuildBaseline({
92
+ projectRoot,
93
+ engineDir: paths.engine,
94
+ binaryName: projectConfig.binaryName,
95
+ testPackagingCoverage: recordedCoverage,
96
+ previousBaseline,
97
+ recordedBy: describeBuildInvocation(extend !== undefined, testPackagingCoverage),
98
+ staticComponentsHandling: preparation.fullBuildRequired
99
+ ? 'refresh'
100
+ : extend !== undefined
101
+ ? 'carry-forward'
102
+ : 'auto',
103
+ buildKind,
104
+ });
89
105
  }
90
106
  catch (baselineError) {
91
107
  verbose(`Could not persist build baseline: ${toError(baselineError).message}`);
@@ -166,7 +182,15 @@ export async function runTestBuildPhase(projectRoot, paths, projectConfig, harne
166
182
  }
167
183
  }
168
184
  }
169
- await runPreTestBuild(projectRoot, paths, projectConfig, harnessRetries, coverage, options.refuseUnexportedDrift === true, extending ? { requestedPaths: normalizedPaths } : undefined);
185
+ await runPreTestBuild({
186
+ projectRoot,
187
+ paths,
188
+ projectConfig,
189
+ harnessRetries,
190
+ testPackagingCoverage: coverage,
191
+ refuseUnexportedDrift: options.refuseUnexportedDrift === true,
192
+ extend: extending ? { requestedPaths: normalizedPaths } : undefined,
193
+ });
170
194
  info('');
171
195
  if (options.buildOnly) {
172
196
  // Union build for mixed harnesses: the coverage claim
@@ -1,7 +1,7 @@
1
- import { loadConfig } from '../core/config.js';
2
1
  import type { TestOptions } from '../types/commands/index.js';
2
+ import type { FireForgeConfig } from '../types/config.js';
3
3
  import type { TestRunOutcome } from './test-run.js';
4
- type ProjectConfig = Awaited<ReturnType<typeof loadConfig>>;
4
+ type ProjectConfig = FireForgeConfig;
5
5
  /** Rejects pathless `fireforge test` unless the operator selected a pathless mode. */
6
6
  export declare function assertPathlessTestMode(testPaths: readonly string[], options: TestOptions): void;
7
7
  /** Rejects invalid combinations for mode-like `fireforge test` options. */
@@ -1,5 +1,4 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
- import { loadConfig } from '../core/config.js';
3
2
  import { findNearestXpcshellManifest } from '../core/xpcshell-appdir.js';
4
3
  import { GeneralError } from '../errors/base.js';
5
4
  import { info, success } from '../utils/logger.js';
@@ -28,7 +28,7 @@ const TEST_HELP_TEXT = [
28
28
  'Machine-readable verdict: every run (single, sharded, --canary,',
29
29
  '--doctor) ends with exactly one raw stdout line',
30
30
  ' FIREFORGE-VERDICT: PASS|FAIL [reason=crash|test-failures|',
31
- ' no-tests|preflight|inconclusive|lock-timeout] [checks=<n>]',
31
+ ' no-tests|preflight|inconclusive|lock-timeout|killed] [checks=<n>]',
32
32
  ' [unexpected=<n>] [shards=<p>/<t>] [(<note>)] [log=<path>]',
33
33
  "log=<path> names this run's complete output, written to",
34
34
  '.fireforge/logs/ as the run streams, so a piped or truncated run',
@@ -41,7 +41,9 @@ const TEST_HELP_TEXT = [
41
41
  'errors); reason=inconclusive means engine/ changed while the',
42
42
  'tests ran, so the harness result cannot be trusted;',
43
43
  'reason=lock-timeout means the run never started because another',
44
- 'engine-mutating command held the lock past the wait budget. A sharded',
44
+ 'engine-mutating command held the lock past the wait budget;',
45
+ 'reason=killed means a signal terminated the run before it reached',
46
+ 'any other outcome (written from the signal handler). A sharded',
45
47
  'aggregate reason is the most structural failing shard (crash,',
46
48
  'then no-tests, then test-failures). Count keys are omitted when',
47
49
  'the embedded summary did not print them. A trailing parenthetical',
@@ -71,7 +73,7 @@ export function registerTest(program, { getProjectRoot, withErrorHandling }) {
71
73
  .option('--canary [path]', 'Run one short browser-chrome harness canary. Uses test.canaryPath from fireforge.json when no path is supplied.')
72
74
  .option('--doctor', 'Run a marionette handshake preflight before tests (exit 1 on FAIL). With no paths, runs the preflight only.')
73
75
  .option('--mach-arg <arg>', 'Forward this argument verbatim to `mach test` (repeatable). Escape valve for upstream xpcshell/mochitest flags FireForge does not model.', ...stringListOption())
74
- .option('--harness-retries <n>', `Retry budget for recognized harness crashes (resource-monitor tracebacks, pre-test hangs, post-green shutdown re-entry). 0 disables retries. Default: ${String(DEFAULT_HARNESS_RETRIES)}.`, commanderArgParser((raw) => {
76
+ .option('--harness-retries <n>', `Retry budget for recognized harness crashes (resource-monitor tracebacks, pre-test hangs, post-green shutdown re-entry). 0 disables retries. Default: ${DEFAULT_HARNESS_RETRIES}.`, commanderArgParser((raw) => {
75
77
  const n = Number.parseInt(raw, 10);
76
78
  if (!Number.isFinite(n) || n < 0 || n > 10) {
77
79
  throw new GeneralError(`--harness-retries must be an integer in 0..10 (got "${raw}")`);
@@ -11,7 +11,7 @@
11
11
  * stays available via `--no-shard`.
12
12
  */
13
13
  import { probeDisplaySleepState } from '../core/display-state.js';
14
- import { mochitestWithOutput, testWithOutput, xpcshellTestWithOutput, } from '../core/mach.js';
14
+ import { runMachTestSuite } from '../core/mach.js';
15
15
  import { getActiveRunLogPath } from '../core/run-log.js';
16
16
  import { changedPrefNoiseVerdictNote } from '../core/test-changed-prefs.js';
17
17
  import { buildHarnessCrashMessage, classifyHarnessRun, headedDisplayAsleepVerdictNote, headedNoOutputTimeoutHint, } from '../core/test-harness-crash.js';
@@ -24,13 +24,13 @@ import { maybeInjectAppdirArg } from './test-appdir.js';
24
24
  import { emitHarnessVerdict } from './test-verdict.js';
25
25
  /** Default bounded retry budget for recognized harness crashes. */
26
26
  export const DEFAULT_HARNESS_RETRIES = 2;
27
- /** Resolves the capturing mach dispatcher for a suite. */
28
- function dispatchForSuite(suite) {
27
+ /** Resolves the mach test command a suite dispatches to. */
28
+ function machKindForSuite(suite) {
29
29
  if (suite === 'xpcshell')
30
- return xpcshellTestWithOutput;
30
+ return 'xpcshell-test';
31
31
  if (suite === 'mochitest')
32
- return mochitestWithOutput;
33
- return testWithOutput;
32
+ return 'mochitest';
33
+ return 'test';
34
34
  }
35
35
  /**
36
36
  * Runs one mach test invocation for `paths`, retrying recognized harness
@@ -54,24 +54,31 @@ export async function runTestsWithRetries(ctx, paths) {
54
54
  async function runTestsWithRetriesInner(ctx, paths, env) {
55
55
  const extraArgs = [...ctx.baseExtraArgs];
56
56
  const appdirInjectionAttempted = await maybeInjectAppdirArg(ctx.engineDir, paths, ctx.objDir, extraArgs);
57
- const dispatch = dispatchForSuite(ctx.suite);
57
+ const kind = machKindForSuite(ctx.suite);
58
58
  const maxAttempts = Math.max(1, ctx.harnessRetries + 1);
59
59
  let attempts = 0;
60
60
  let result;
61
61
  let verdict;
62
62
  for (;;) {
63
63
  attempts += 1;
64
- // Arity is kept minimal when there is nothing extra to pass: the
65
- // dispatchers' trailing parameters are optional, and appending explicit
66
- // `undefined`s would only make every call site's shape depend on
67
- // features the run is not using.
68
- result =
69
- ctx.fullOutput === true
70
- ? await dispatch(ctx.engineDir, paths, extraArgs, env, true)
71
- : env
72
- ? await dispatch(ctx.engineDir, paths, extraArgs, env)
73
- : await dispatch(ctx.engineDir, paths, extraArgs);
74
- result = await retryAfterXpcshellSymlinkRepair(ctx.engineDir, ctx.objDir, result, ctx.classification, paths, extraArgs, env, dispatch, ctx.fullOutput);
64
+ result = await runMachTestSuite(kind, {
65
+ engineDir: ctx.engineDir,
66
+ testPaths: paths,
67
+ args: extraArgs,
68
+ env,
69
+ fullOutput: ctx.fullOutput,
70
+ });
71
+ result = await retryAfterXpcshellSymlinkRepair({
72
+ engineDir: ctx.engineDir,
73
+ objDir: ctx.objDir,
74
+ result,
75
+ classification: ctx.classification,
76
+ normalizedPaths: paths,
77
+ extraArgs,
78
+ env,
79
+ kind,
80
+ fullOutput: ctx.fullOutput,
81
+ });
75
82
  const combined = `${result.stdout}\n${result.stderr}`;
76
83
  verdict = classifyHarnessRun(result.exitCode, combined, paths);
77
84
  if (verdict.kind !== 'harness-crash' || attempts >= maxAttempts)
@@ -8,7 +8,7 @@
8
8
  * `src/core/test-stale-check.ts`.
9
9
  */
10
10
  import { checkStaleBuildForTest, checkStaticComponentsStale, findChangedTestManifestsForPaths, findUncoveredRequestPaths, formatStaleBuildWarning, formatStaticComponentsRefusal, formatTestCoverageRefusal, } from '../core/test-stale-check.js';
11
- import { GeneralError } from '../errors/base.js';
11
+ import { PreflightRefusalError } from '../errors/base.js';
12
12
  import { warn } from '../utils/logger.js';
13
13
  /**
14
14
  * Stale-build preflight — when `--build` was NOT requested, detect packageable
@@ -40,7 +40,7 @@ export async function enforceStaleBuildGate(projectRoot, engineDir, options, nor
40
40
  // The refusal is correct on its own; naming the manifest that gained
41
41
  // an entry is the triage step it otherwise leaves to the reader.
42
42
  const changedManifests = await findChangedTestManifestsForPaths(engineDir, stale.baseline, uncovered);
43
- throw new GeneralError(formatTestCoverageRefusal(uncovered, Array.isArray(recordedCoverage) ? recordedCoverage : [], changedManifests));
43
+ throw new PreflightRefusalError(formatTestCoverageRefusal(uncovered, Array.isArray(recordedCoverage) ? recordedCoverage : [], changedManifests), 'coverage-replaced');
44
44
  }
45
45
  }
46
46
  await enforceStaticComponentsGate(engineDir, stale.baseline, options);
@@ -59,7 +59,7 @@ export async function enforceStaleBuildGate(projectRoot, engineDir, options, nor
59
59
  warn(staleMessage);
60
60
  }
61
61
  else {
62
- throw new GeneralError(staleMessage);
62
+ throw new PreflightRefusalError(staleMessage, 'stale-build');
63
63
  }
64
64
  }
65
65
  }
@@ -84,5 +84,5 @@ export async function enforceStaticComponentsGate(engineDir, baseline, options)
84
84
  warn(message);
85
85
  return;
86
86
  }
87
- throw new GeneralError(message);
87
+ throw new PreflightRefusalError(message, 'stale-components');
88
88
  }
@@ -30,8 +30,22 @@ export declare function emitHarnessVerdict(verdict: HarnessRunVerdict, shards?:
30
30
  }): void;
31
31
  /** Emits the pathless `--doctor` PASS verdict. */
32
32
  export declare function emitPassVerdict(): void;
33
- /** Emits a FAIL verdict carrying an emission-layer reason code. */
34
- export declare function emitFailVerdict(reason: FireforgeVerdictReason): void;
33
+ /**
34
+ * Emits a FAIL verdict carrying an emission-layer reason code, and
35
+ * optionally the refusal class that produced it.
36
+ *
37
+ * `note=` is ADDITIVE, like the `log=`, `signal=` and `shards=` keys already
38
+ * on this line: `reason=` is unchanged and still comes from the closed set
39
+ * in `docs/machine-output.md`, so a consumer that tokenises `key=value`
40
+ * pairs is unaffected. It exists because `reason=preflight` covers every
41
+ * gate before the harness — a missing build, a peer's packaging record, a
42
+ * developer browser holding the objdir — and an unattended reader whose
43
+ * pipe kept only the verdict line could not tell those apart.
44
+ *
45
+ * @param reason - Closed-set verdict reason
46
+ * @param note - Stable refusal class, when the thrower named one
47
+ */
48
+ export declare function emitFailVerdict(reason: FireforgeVerdictReason, note?: string): void;
35
49
  /**
36
50
  * Emits the terminal verdict for a run killed by a signal.
37
51
  *
@@ -61,9 +61,24 @@ export function emitHarnessVerdict(verdict, shards) {
61
61
  export function emitPassVerdict() {
62
62
  writeVerdictLine('FIREFORGE-VERDICT: PASS');
63
63
  }
64
- /** Emits a FAIL verdict carrying an emission-layer reason code. */
65
- export function emitFailVerdict(reason) {
66
- writeVerdictLine(`FIREFORGE-VERDICT: FAIL reason=${reason}`);
64
+ /**
65
+ * Emits a FAIL verdict carrying an emission-layer reason code, and
66
+ * optionally the refusal class that produced it.
67
+ *
68
+ * `note=` is ADDITIVE, like the `log=`, `signal=` and `shards=` keys already
69
+ * on this line: `reason=` is unchanged and still comes from the closed set
70
+ * in `docs/machine-output.md`, so a consumer that tokenises `key=value`
71
+ * pairs is unaffected. It exists because `reason=preflight` covers every
72
+ * gate before the harness — a missing build, a peer's packaging record, a
73
+ * developer browser holding the objdir — and an unattended reader whose
74
+ * pipe kept only the verdict line could not tell those apart.
75
+ *
76
+ * @param reason - Closed-set verdict reason
77
+ * @param note - Stable refusal class, when the thrower named one
78
+ */
79
+ export function emitFailVerdict(reason, note) {
80
+ const suffix = note === undefined ? '' : ` note=${note}`;
81
+ writeVerdictLine(`FIREFORGE-VERDICT: FAIL reason=${reason}${suffix}`);
67
82
  }
68
83
  /**
69
84
  * Emits the terminal verdict for a run killed by a signal.
@@ -9,12 +9,13 @@ import { assertBuildArtifacts } from '../core/mach-build-artifacts.js';
9
9
  import { assertMarionettePortAvailable, ensureLaunchableBrowserNotRunning, ensureMarionettePortAvailable, extractForwardedMarionettePort, forwardedMachArgsIncludeMarionetteClient, shouldAutoForwardMarionettePortToMach, } from '../core/marionette-port.js';
10
10
  import { formatMarionettePreflightLine, reportMarionettePreflight, runMarionettePreflight, } from '../core/marionette-preflight.js';
11
11
  import { ensureMochitestServerPortAvailable } from '../core/mochitest-server-port.js';
12
- import { closeActiveRunLog, openRunLog, setActiveRunLog } from '../core/run-log.js';
12
+ import { closeActiveRunLog, openRunLog, setActiveRunLog, writeToActiveRunLog, } from '../core/run-log.js';
13
13
  import { createPostRebuildFailureContext } from '../core/test-harness-output.js';
14
14
  import { analyzeTestPathScopes, formatScopeNotice, } from '../core/test-path-scope.js';
15
15
  import { assertObjdirMatchesTreeMarker } from '../core/tree-store.js';
16
- import { GeneralError } from '../errors/base.js';
16
+ import { FireForgeError, GeneralError, PreflightRefusalError } from '../errors/base.js';
17
17
  import { BuildError } from '../errors/build.js';
18
+ import { toError } from '../utils/errors.js';
18
19
  import { pathExists } from '../utils/fs.js';
19
20
  import { info, intro, notice, outro, verbose } from '../utils/logger.js';
20
21
  import { stripEnginePrefix } from '../utils/paths.js';
@@ -292,14 +293,50 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
292
293
  await runTestCommandBody(projectRoot, testPaths, options);
293
294
  }
294
295
  catch (error) {
295
- if (!verdictEmitted())
296
- emitFailVerdict('preflight');
296
+ if (!verdictEmitted()) {
297
+ renderPreflightRefusal(error);
298
+ emitFailVerdict('preflight', refusalNote(error));
299
+ }
297
300
  throw error;
298
301
  }
299
302
  finally {
300
303
  await closeActiveRunLog();
301
304
  }
302
305
  }
306
+ /** The refusal class a preflight gate named, when it named one. */
307
+ function refusalNote(error) {
308
+ return error instanceof PreflightRefusalError ? error.note : undefined;
309
+ }
310
+ /**
311
+ * Puts a preflight refusal's own text on the operator's channel BEFORE the
312
+ * verdict line, and into the run log.
313
+ *
314
+ * Both halves are load-bearing, and neither happened before:
315
+ *
316
+ * - `withErrorHandling` renders `error.userMessage` back in `cli.ts`, but
317
+ * by then `emitFailVerdict` has called `setStdoutSealed(true)` — the seal
318
+ * that keeps the verdict the LAST stdout write — so `logError` routes to
319
+ * stderr. A run captured with `> file` therefore kept the verdict and
320
+ * dropped the reason.
321
+ * - That same rendering happens after this function's caller returns, and
322
+ * the `finally` below has already closed the run log. So the artifact the
323
+ * verdict's own `log=` key points at did not contain the refusal either,
324
+ * which is what left `.fireforge/logs/test-*.log` holding only the
325
+ * pre-test build.
326
+ *
327
+ * Writing here, before the seal, fixes both without trading away the
328
+ * contract: the refusal lands on stdout and in the log, and the verdict
329
+ * line still comes last.
330
+ */
331
+ function renderPreflightRefusal(error) {
332
+ const text = error instanceof FireForgeError ? error.userMessage : toError(error).message;
333
+ const block = `Preflight refused:\n${text}\n`;
334
+ // Raw stdout rather than the clack logger: this must land on the captured
335
+ // stream verbatim, and clack's renderer can drop output under non-TTY
336
+ // capture — the same reason the verdict line is written this way.
337
+ process.stdout.write(block);
338
+ writeToActiveRunLog(block);
339
+ }
303
340
  /**
304
341
  * Verifies `engine/` did not change under the run before any PASS verdict
305
342
  * may print. On failure the run's verdict is `FAIL reason=inconclusive` —
@@ -392,7 +429,7 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}) {
392
429
  extraArgs.push('--auto');
393
430
  }
394
431
  if (canaryPath !== undefined) {
395
- extraArgs.push(`--timeout=${String(canaryTimeoutSeconds(projectConfig))}`);
432
+ extraArgs.push(`--timeout=${canaryTimeoutSeconds(projectConfig)}`);
396
433
  }
397
434
  // --mach-arg is a verbatim passthrough for upstream mach/xpcshell/mochitest
398
435
  // flags FireForge does not model directly (see the xpcshell appdir hint
@@ -0,0 +1,26 @@
1
+ /** Options accepted by {@link tokenListCommand}. */
2
+ export interface TokenListOptions {
3
+ /** Restrict the report to one category. */
4
+ category?: string;
5
+ /** Emit the machine-readable envelope instead of the human report. */
6
+ json?: boolean;
7
+ }
8
+ /**
9
+ * Lists the categories declared in the tokens CSS with their token names,
10
+ * in file order.
11
+ *
12
+ * @param projectRoot - Root directory of the project
13
+ * @param options - Filter and output-mode options
14
+ */
15
+ export declare function tokenListCommand(projectRoot: string, options?: TokenListOptions): Promise<void>;
16
+ /**
17
+ * Reports one token: the category that owns it and the value it takes in
18
+ * every block that declares it.
19
+ *
20
+ * @param projectRoot - Root directory of the project
21
+ * @param tokenName - Custom-property name, with or without the leading `--`
22
+ * @param options - Output-mode options
23
+ */
24
+ export declare function tokenShowCommand(projectRoot: string, tokenName: string, options?: {
25
+ json?: boolean;
26
+ }): Promise<void>;