@hominis/fireforge 0.40.0 → 0.42.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 (256) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +16 -10
  3. package/dist/bin/fireforge.js +25 -4
  4. package/dist/build-info.json +9 -0
  5. package/dist/src/cli.js +20 -3
  6. package/dist/src/commands/bootstrap.js +5 -1
  7. package/dist/src/commands/discard.js +2 -22
  8. package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
  9. package/dist/src/commands/doctor-check-core.d.ts +20 -0
  10. package/dist/src/commands/doctor-check-core.js +28 -0
  11. package/dist/src/commands/doctor-external-toolchains.js +2 -0
  12. package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
  13. package/dist/src/commands/doctor-furnace.js +13 -11
  14. package/dist/src/commands/doctor-orphaned-harness.js +2 -1
  15. package/dist/src/commands/doctor.js +2 -2
  16. package/dist/src/commands/export-all.js +9 -1
  17. package/dist/src/commands/export-flow.js +13 -3
  18. package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
  19. package/dist/src/commands/export-placement-conflicts.js +74 -0
  20. package/dist/src/commands/export-placement-gate.js +1 -1
  21. package/dist/src/commands/export-shared.d.ts +8 -1
  22. package/dist/src/commands/export-shared.js +16 -7
  23. package/dist/src/commands/export.js +2 -2
  24. package/dist/src/commands/furnace/apply.js +4 -3
  25. package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
  26. package/dist/src/commands/furnace/chrome-doc.js +13 -2
  27. package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
  28. package/dist/src/commands/furnace/create-browser-test.js +5 -0
  29. package/dist/src/commands/furnace/create.js +2 -2
  30. package/dist/src/commands/furnace/diff.js +2 -1
  31. package/dist/src/commands/furnace/init.js +4 -6
  32. package/dist/src/commands/furnace/override.js +2 -2
  33. package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
  34. package/dist/src/commands/furnace/remove-mutations.js +171 -0
  35. package/dist/src/commands/furnace/remove.js +21 -128
  36. package/dist/src/commands/furnace/rename-browser-test.js +54 -5
  37. package/dist/src/commands/furnace/scan.js +27 -36
  38. package/dist/src/commands/furnace/status.js +30 -28
  39. package/dist/src/commands/furnace/validate.js +25 -18
  40. package/dist/src/commands/import.js +6 -20
  41. package/dist/src/commands/lint-per-patch.js +12 -59
  42. package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
  43. package/dist/src/commands/lint-per-run-checkjs.js +102 -0
  44. package/dist/src/commands/lint.js +4 -4
  45. package/dist/src/commands/manifest.js +1 -1
  46. package/dist/src/commands/package.js +9 -16
  47. package/dist/src/commands/patch/compact.js +5 -5
  48. package/dist/src/commands/patch/delete.js +5 -5
  49. package/dist/src/commands/patch/lint-ignore.js +3 -2
  50. package/dist/src/commands/patch/move-files-create.js +22 -4
  51. package/dist/src/commands/patch/move-files-into.js +14 -10
  52. package/dist/src/commands/patch/move-files.js +6 -5
  53. package/dist/src/commands/patch/rename.d.ts +2 -2
  54. package/dist/src/commands/patch/rename.js +151 -50
  55. package/dist/src/commands/patch/reorder.d.ts +9 -0
  56. package/dist/src/commands/patch/reorder.js +14 -10
  57. package/dist/src/commands/patch/split-plan.d.ts +13 -6
  58. package/dist/src/commands/patch/split-plan.js +22 -12
  59. package/dist/src/commands/patch/split.d.ts +1 -1
  60. package/dist/src/commands/patch/split.js +23 -9
  61. package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
  62. package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
  63. package/dist/src/commands/patch/staged-dependency.js +10 -0
  64. package/dist/src/commands/re-export-adjacent.js +5 -6
  65. package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
  66. package/dist/src/commands/re-export-bulk-scan.js +142 -2
  67. package/dist/src/commands/re-export-drift.d.ts +74 -0
  68. package/dist/src/commands/re-export-drift.js +234 -0
  69. package/dist/src/commands/re-export-files.js +5 -3
  70. package/dist/src/commands/re-export-lint.d.ts +97 -0
  71. package/dist/src/commands/re-export-lint.js +210 -0
  72. package/dist/src/commands/re-export-options.js +8 -0
  73. package/dist/src/commands/re-export-register.d.ts +18 -0
  74. package/dist/src/commands/re-export-register.js +61 -0
  75. package/dist/src/commands/re-export-scan.js +13 -7
  76. package/dist/src/commands/re-export-single.d.ts +20 -0
  77. package/dist/src/commands/re-export-single.js +261 -0
  78. package/dist/src/commands/re-export.d.ts +0 -4
  79. package/dist/src/commands/re-export.js +113 -268
  80. package/dist/src/commands/rebase/abort.js +13 -4
  81. package/dist/src/commands/rebase/continue.js +11 -4
  82. package/dist/src/commands/rebase/index.js +19 -29
  83. package/dist/src/commands/rebase/patch-loop.js +12 -0
  84. package/dist/src/commands/reset.js +4 -23
  85. package/dist/src/commands/resolve.js +2 -9
  86. package/dist/src/commands/run.js +11 -21
  87. package/dist/src/commands/source.js +3 -9
  88. package/dist/src/commands/status-check.d.ts +13 -0
  89. package/dist/src/commands/status-check.js +31 -13
  90. package/dist/src/commands/status-json.d.ts +31 -0
  91. package/dist/src/commands/status-json.js +76 -0
  92. package/dist/src/commands/status-output.d.ts +1 -0
  93. package/dist/src/commands/status-output.js +10 -1
  94. package/dist/src/commands/status-ownership.d.ts +52 -0
  95. package/dist/src/commands/status-ownership.js +57 -0
  96. package/dist/src/commands/status.js +184 -175
  97. package/dist/src/commands/test-build-phase.d.ts +19 -0
  98. package/dist/src/commands/test-build-phase.js +168 -0
  99. package/dist/src/commands/test-diagnose.js +30 -2
  100. package/dist/src/commands/test-modes.d.ts +9 -1
  101. package/dist/src/commands/test-modes.js +46 -13
  102. package/dist/src/commands/test-register.js +59 -27
  103. package/dist/src/commands/test-run.d.ts +29 -3
  104. package/dist/src/commands/test-run.js +41 -4
  105. package/dist/src/commands/test-verdict.d.ts +32 -0
  106. package/dist/src/commands/test-verdict.js +48 -0
  107. package/dist/src/commands/test.d.ts +9 -0
  108. package/dist/src/commands/test.js +100 -123
  109. package/dist/src/commands/token-coverage.js +4 -9
  110. package/dist/src/commands/tree.d.ts +3 -0
  111. package/dist/src/commands/tree.js +171 -25
  112. package/dist/src/commands/verify.js +2 -2
  113. package/dist/src/commands/watch.js +22 -21
  114. package/dist/src/commands/wire.js +1 -1
  115. package/dist/src/core/ast-utils.d.ts +11 -0
  116. package/dist/src/core/ast-utils.js +11 -0
  117. package/dist/src/core/branding.d.ts +16 -0
  118. package/dist/src/core/branding.js +18 -2
  119. package/dist/src/core/build-audit-platform.js +6 -1
  120. package/dist/src/core/build-audit-registration.js +2 -0
  121. package/dist/src/core/build-audit-resolve.d.ts +13 -0
  122. package/dist/src/core/build-audit-resolve.js +34 -13
  123. package/dist/src/core/build-audit.js +4 -20
  124. package/dist/src/core/build-baseline-types.d.ts +29 -8
  125. package/dist/src/core/build-baseline-types.js +2 -1
  126. package/dist/src/core/build-baseline.d.ts +10 -2
  127. package/dist/src/core/build-baseline.js +39 -8
  128. package/dist/src/core/build-prepare.d.ts +4 -0
  129. package/dist/src/core/build-prepare.js +7 -1
  130. package/dist/src/core/config-paths.d.ts +1 -1
  131. package/dist/src/core/config-paths.js +2 -0
  132. package/dist/src/core/config-validate-test-toolchains.js +3 -2
  133. package/dist/src/core/config-validate.js +34 -36
  134. package/dist/src/core/config.js +16 -3
  135. package/dist/src/core/coverage-extend.d.ts +89 -0
  136. package/dist/src/core/coverage-extend.js +188 -0
  137. package/dist/src/core/destructive.js +3 -1
  138. package/dist/src/core/discard-baseline.js +18 -4
  139. package/dist/src/core/engine-precondition.d.ts +35 -0
  140. package/dist/src/core/engine-precondition.js +50 -0
  141. package/dist/src/core/engine-session-lock.d.ts +38 -1
  142. package/dist/src/core/engine-session-lock.js +78 -4
  143. package/dist/src/core/file-lock.d.ts +7 -0
  144. package/dist/src/core/file-lock.js +20 -30
  145. package/dist/src/core/firefox.js +5 -2
  146. package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
  147. package/dist/src/core/furnace-apply-helpers.js +12 -3
  148. package/dist/src/core/furnace-apply-output.js +9 -0
  149. package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
  150. package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
  151. package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
  152. package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
  153. package/dist/src/core/furnace-apply.d.ts +16 -4
  154. package/dist/src/core/furnace-apply.js +39 -43
  155. package/dist/src/core/furnace-config.js +2 -0
  156. package/dist/src/core/furnace-css-fragments.js +2 -0
  157. package/dist/src/core/furnace-jsconfig.js +14 -7
  158. package/dist/src/core/furnace-registration-ast.js +8 -8
  159. package/dist/src/core/furnace-registration-remove.js +6 -3
  160. package/dist/src/core/furnace-registration.js +9 -12
  161. package/dist/src/core/furnace-rollback.js +1 -1
  162. package/dist/src/core/furnace-scanner.js +2 -1
  163. package/dist/src/core/furnace-stale-export.js +2 -1
  164. package/dist/src/core/furnace-state-persist.d.ts +4 -6
  165. package/dist/src/core/furnace-validate-compatibility.js +4 -7
  166. package/dist/src/core/furnace-validate-helpers.js +33 -1
  167. package/dist/src/core/furnace-validate-registration.js +16 -0
  168. package/dist/src/core/furnace-validate.js +4 -2
  169. package/dist/src/core/git-diff.js +37 -2
  170. package/dist/src/core/lint-cache.d.ts +1 -1
  171. package/dist/src/core/lint-cache.js +33 -10
  172. package/dist/src/core/mach-build-artifacts.d.ts +63 -1
  173. package/dist/src/core/mach-build-artifacts.js +108 -2
  174. package/dist/src/core/mach-mozconfig.js +9 -1
  175. package/dist/src/core/mach-resource-shim.js +1 -0
  176. package/dist/src/core/marionette-port.d.ts +16 -0
  177. package/dist/src/core/marionette-port.js +93 -8
  178. package/dist/src/core/marionette-preflight.js +6 -5
  179. package/dist/src/core/patch-apply.d.ts +20 -13
  180. package/dist/src/core/patch-apply.js +32 -33
  181. package/dist/src/core/patch-export.js +3 -5
  182. package/dist/src/core/patch-lint-checkjs.d.ts +25 -3
  183. package/dist/src/core/patch-lint-checkjs.js +78 -7
  184. package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
  185. package/dist/src/core/patch-lint-creators.d.ts +30 -0
  186. package/dist/src/core/patch-lint-creators.js +45 -0
  187. package/dist/src/core/patch-lint-cross.d.ts +1 -17
  188. package/dist/src/core/patch-lint-cross.js +16 -32
  189. package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
  190. package/dist/src/core/patch-lint-jsdoc.js +15 -5
  191. package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
  192. package/dist/src/core/patch-lint-module-registration.js +88 -0
  193. package/dist/src/core/patch-lint-mozbuild.js +99 -11
  194. package/dist/src/core/patch-lint-reexports.d.ts +1 -0
  195. package/dist/src/core/patch-lint-reexports.js +1 -0
  196. package/dist/src/core/patch-lint.d.ts +8 -0
  197. package/dist/src/core/patch-lint.js +84 -6
  198. package/dist/src/core/patch-lock.d.ts +12 -1
  199. package/dist/src/core/patch-lock.js +40 -4
  200. package/dist/src/core/patch-manifest-consistency.js +15 -3
  201. package/dist/src/core/patch-parse.d.ts +8 -0
  202. package/dist/src/core/patch-parse.js +10 -0
  203. package/dist/src/core/patch-policy.d.ts +7 -0
  204. package/dist/src/core/patch-policy.js +4 -1
  205. package/dist/src/core/patch-transform.d.ts +10 -0
  206. package/dist/src/core/patch-transform.js +13 -2
  207. package/dist/src/core/rebase-session.d.ts +44 -2
  208. package/dist/src/core/rebase-session.js +114 -17
  209. package/dist/src/core/register-browser-content.js +2 -1
  210. package/dist/src/core/register-shared-css.js +2 -1
  211. package/dist/src/core/smoke-patterns.d.ts +0 -8
  212. package/dist/src/core/smoke-patterns.js +3 -2
  213. package/dist/src/core/status-binary.d.ts +55 -0
  214. package/dist/src/core/status-binary.js +124 -0
  215. package/dist/src/core/status-classify.d.ts +6 -1
  216. package/dist/src/core/status-classify.js +101 -73
  217. package/dist/src/core/test-harness-crash.d.ts +32 -1
  218. package/dist/src/core/test-harness-crash.js +59 -4
  219. package/dist/src/core/test-path-scope.js +2 -0
  220. package/dist/src/core/test-stale-check.js +1 -20
  221. package/dist/src/core/toolchain-preflight.js +24 -26
  222. package/dist/src/core/tree-guard.d.ts +1 -1
  223. package/dist/src/core/tree-guard.js +67 -12
  224. package/dist/src/core/tree-store.d.ts +86 -10
  225. package/dist/src/core/tree-store.js +310 -72
  226. package/dist/src/core/typecheck.js +2 -1
  227. package/dist/src/core/wire-destroy.js +14 -3
  228. package/dist/src/core/wire-init.js +37 -40
  229. package/dist/src/core/wire-subscript.js +11 -3
  230. package/dist/src/core/wire-utils.js +4 -4
  231. package/dist/src/core/xpcshell-appdir.js +3 -0
  232. package/dist/src/errors/base.d.ts +13 -0
  233. package/dist/src/errors/base.js +13 -0
  234. package/dist/src/errors/rebase.d.ts +13 -0
  235. package/dist/src/errors/rebase.js +17 -0
  236. package/dist/src/types/commands/options.d.ts +86 -1
  237. package/dist/src/types/commands/patches.d.ts +8 -0
  238. package/dist/src/types/furnace.d.ts +7 -0
  239. package/dist/src/utils/build-info.d.ts +31 -0
  240. package/dist/src/utils/build-info.js +98 -0
  241. package/dist/src/utils/concurrency.d.ts +8 -0
  242. package/dist/src/utils/concurrency.js +21 -0
  243. package/dist/src/utils/errors.d.ts +39 -0
  244. package/dist/src/utils/errors.js +55 -0
  245. package/dist/src/utils/fs.js +5 -12
  246. package/dist/src/utils/logger.d.ts +14 -2
  247. package/dist/src/utils/logger.js +43 -13
  248. package/dist/src/utils/options.d.ts +21 -4
  249. package/dist/src/utils/options.js +26 -6
  250. package/dist/src/utils/package-root.js +7 -4
  251. package/dist/src/utils/process-group.js +13 -2
  252. package/dist/src/utils/stdio-drain.d.ts +28 -0
  253. package/dist/src/utils/stdio-drain.js +54 -0
  254. package/dist/src/utils/validation.d.ts +23 -2
  255. package/dist/src/utils/validation.js +29 -3
  256. package/package.json +9 -9
package/CHANGELOG.md CHANGED
@@ -1,5 +1,90 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.42.0
4
+
5
+ ### macOS bundle identity
6
+
7
+ - `MOZ_MACBUNDLE_ID` in the generated branding `configure.sh` now carries only the LEAF segment of `appId`, and the generated mozconfig emits `ac_add_options --with-distribution-id=<appId prefix>` — upstream `toolkit/moz.configure` composes `CFBundleIdentifier` as `<distribution-id>.<MOZ_MACBUNDLE_ID>`, so writing the full reverse-domain `appId` double-prefixed the shipped bundle id (observed: `org.mozilla.org.hominis.browser`). A `configure.sh` carrying the full `appId` now reads as stale and is rewritten.
8
+
9
+ ## 0.41.0
10
+
11
+ ### Release and build integrity
12
+
13
+ - Release publishing commits version bumps before building and enforces a clean tree with `FIREFORGE_PACK_STRICT=1`.
14
+ - Builds stamp `dist/build-info.json`; installed `--version` output includes the commit and dirty-content hash.
15
+ - Build baselines retain deletion tombstones, so removed packageable files and `components.conf` entries cannot look fresh.
16
+ - `test --build` uses a full build for new `jar.mn` registrations; mixed-harness `--build-only` packages all requested paths once.
17
+ - `--extend-coverage` unions scoped packaging coverage only while HEAD, packageable inputs, and `mozconfig` remain unchanged.
18
+ - Shared build and engine preflights now reject incomplete builds and unborn engine repositories consistently.
19
+
20
+ ### Tests and verification trees
21
+
22
+ - Tests emit exactly one final `FIREFORGE-VERDICT:` line with the correct pass/failure reason, including sharded, lock-timeout, crash, and inconclusive runs.
23
+ - `tree exec` preserves the child verdict as the final stdout line; pre-spawn refusals emit no verdict.
24
+ - Engine-integrity checks run before successful verdicts.
25
+ - Browser preflight detects stale apps from the same objdir even after they release Marionette; `--kill-stale-marionette` can terminate them.
26
+ - Mach commands use process groups and reap surviving browser or harness descendants.
27
+ - `tree create --with-objdir` supports build-less tests after relocation and reconfiguration checks.
28
+ - Tree creation refuses symlinked objdirs, cleans failed clones, validates markers, and handles unreadable locks fail-closed.
29
+ - Tree freshness covers dirty engine content and patch bodies.
30
+ - Tree creation skips unwanted `obj-*` directories before cloning and uses bounded concurrency.
31
+ - `tree create --wait-lock` now matches its lock-refusal guidance.
32
+ - Verification trees permit `re-export --dry-run`; mutating operations remain blocked.
33
+ - Test assertion lint now recognizes real assertion calls, and critical modules have explicit coverage floors.
34
+
35
+ ### Status and diagnostics
36
+
37
+ - Status classification reuses one patch context and classifies files through a bounded pool.
38
+ - `status --json --summary` returns counts and offenders without the full file payload.
39
+ - `status --json --include-ownership` adds ownership data without another scan.
40
+ - JSON output drains fully through slow pipes, and machine-mode refusals route to stderr.
41
+ - Multi-owner files remain conflicts even when Furnace-managed or generated branding paths.
42
+ - Lock contention and unreadable/corrupt state now produce concise, accurate diagnostics.
43
+
44
+ ### Patch, export, and lint workflow
45
+
46
+ - `export-all --exclude-furnace` keeps its filtered scope after license-header repair.
47
+ - `re-export --expect <path>` permits intended drift and reports paths skipped by earlier refusals as not evaluated.
48
+ - Missing drift baselines fail closed, binary patches compare by content, and partial or fully refused re-exports exit non-zero.
49
+ - `re-export --dry-run` uses before/after fingerprints and cannot roll back another completed write.
50
+ - Re-export builds checkJs once per run and reuses the per-patch cache; `--patches` subsets narrow computation.
51
+ - Re-export probes and purity hashing use bounded concurrency; patch writes remain serial.
52
+ - Patch lint rejects new imported system modules without a projected `moz.build` registration.
53
+ - The checkJs cache hashes both configurable shims.
54
+ - Export placement refusals print projected errors and identify renumbering consequences.
55
+ - `patch rename` can recategorize and renumber atomically.
56
+ - `patch move-files` and `patch split` use whole-queue projection lint, normalize names, and provide split guidance.
57
+ - `patch move-files --create` names the required `--description` option.
58
+ - Queue-mutating patch commands support `--wait-lock` and avoid unsafe stale-lock advice.
59
+ - Staged-dependency declarations reject patch-name-shaped file arguments.
60
+ - `CommandError` passes through the top-level handler without duplicate or JSON-corrupting output.
61
+
62
+ ### Furnace and validation
63
+
64
+ - Furnace computes checksums once per apply and warns before overwriting divergent patch-owned files.
65
+ - Furnace jsconfig synchronization includes `register: false` components.
66
+ - Manifest-sync failures are visible, and repairs run under the Furnace lock.
67
+ - `furnace validate --fix` changes only reported components and detects missing custom-element registrations.
68
+ - Localization validation recognizes non-Latin text while exempting emoji and punctuation.
69
+ - `furnace rename` uses single-pass, boundary-aware substitutions.
70
+ - Rollback failures retain repair breadcrumbs for `doctor --repair-furnace`.
71
+ - The reported no-change/apply-state disagreement was not reproducible and is regression-tested.
72
+
73
+ ### Safety, performance, and maintenance
74
+
75
+ - Nano ID 3.3.18 resolves a high-severity audit finding.
76
+ - Rebase loading distinguishes absent and corrupt sessions; `rebase --abort` can recover invalid state.
77
+ - PID liveness treats `EPERM` as alive, preventing deletion through another user's lock.
78
+ - The moz.build parser handles comments, quoted brackets/apostrophes, and unterminated lists.
79
+ - Parser failures no longer silently certify validation success; internal invariant errors bypass fallback.
80
+ - Toolchain probes, baseline fingerprints, cache hashes, and manifest reads use bounded concurrency.
81
+ - Existing Git tuning remains enabled; fsmonitor stays disabled because of stale-daemon risk.
82
+ - A persistent checkJs daemon remains deferred until safe invalidation is cheaper; see `docs/lint-daemon-design.md`.
83
+ - Shared helpers replace duplicated preflights, error conversion, regex escaping, option parsing, object guards, and ESTree casts.
84
+ - Four local ESLint rules prevent unsafe error casts, duplicate regex escaping, and empty JSDoc.
85
+ - Oversized modules were split or simplified, silent degradations documented, and stale dead-code suppressions removed.
86
+ - The release gate pins weak critical modules so aggregate coverage cannot hide local regressions.
87
+
3
88
  ## 0.40.0
4
89
 
5
90
  - **Dependencies: adopted all five open dependabot PRs (#48, #47, #45, #44, #42) after Go-based TypeScript 6 compatibility review** — the pinned `typescript ~6.0.0` (6.0.3 installed) constrained which upgrades were feasible; every bumped tool checked out: `typescript-eslint` declares peer `typescript >=4.8.4 <6.1.0`, `knip` carries no TypeScript dependency at all (oxc parser), and `dpdm` bundles its own nested TypeScript 5.9.3, so none touch the root compiler. Production: `acorn` 8.17.0→8.18.0 (lockfile-only) and `magic-string` 0.30.21→**1.1.0** — a major that only drops the CJS/UMD builds for pure ESM (FireForge is pure ESM; the five `MagicString` call sites use the unchanged core API). Dev: `@types/node` 26.1.2, `dpdm` 4.3.0, `eslint` 10.8.0, `eslint-plugin-jsdoc` 63.3.3, `knip` 6.29.0, `lint-staged` 17.3.0, `prettier` 3.9.6, `typescript-eslint` 8.66.0, `vite` 8.2.1, and `eslint-plugin-simple-import-sort` 13→**14** (breaking only for string-literal export names, which the codebase does not use); four of these resolved one patch/minor past PR #48's snapshot within the existing caret ranges (typescript-eslint 8.66.0 re-verified against the same `<6.1.0` peer window). CI: the SHA-pinned actions group (checkout 7.0.1 with `--unset`-escaping and branch-trim fixes, setup-node, codeql init/analyze). knip 6.29's stricter re-export tracking flagged seven type re-exports (`PatchCoverage`, `ClearablePatchMetadataField`, `PatchMetadataMutation{,Result}` in `patch-export.ts`; `ForwardImportEdge` in `patch-lint-reexports.ts`; `PatchManifestRowMutation{,Result}` in `patch-manifest.ts`) that no module, test, or public-API entry consumed via the barrel hop — a real 6.26 blind spot, so the dead re-exports were removed (the types stay exported from their defining modules). Verified by a full green `release:check`.
package/README.md CHANGED
@@ -86,7 +86,8 @@ Queue maintenance lives under `fireforge patch`: `patch compact` closes ordinal
86
86
  (range-aware when `patchPolicy.ranges` is configured), `patch reorder` moves a patch, and
87
87
  `patch split <source> --files <paths...> --name <name>` carves files out of a patch into a
88
88
  new one as a single transaction — including staged-dependency owner rewrites — with
89
- `--dry-run` support.
89
+ `--dry-run` support. `patch rename` also supports `--category` and `--order`; every
90
+ queue-mutating patch command accepts `--wait-lock [seconds]`.
90
91
 
91
92
  `fireforge test <directory>` runs exactly that directory: FireForge enumerates the
92
93
  directory's test files and passes the explicit file list to mach in one invocation, so
@@ -98,14 +99,18 @@ instances do not exercise cross-argument state (`--no-shard` restores one combin
98
99
  invocation). Recognized harness crashes retry up to `--harness-retries <n>` times (default
99
100
  2), and `--perf-samples <path>` publishes a perf-sample artifact path to the harness
100
101
  (exported as `<BINARYNAME>_PERF_SAMPLE_JSON`).
102
+ Every test run ends with one machine-readable `FIREFORGE-VERDICT: PASS|FAIL ...` line;
103
+ automation should use it instead of the raw process code and treat a missing verdict as
104
+ failure.
101
105
  Pathless test runs must choose a mode: `--auto` forwards mach's own auto-selection,
102
106
  `--doctor` runs the Marionette preflight only, and `--canary [path]` runs one short
103
107
  browser-chrome canary (`test.canaryPath` / `test.canaryTimeoutSeconds` in `fireforge.json`
104
108
  provide defaults). When packageable engine files changed since the last successful
105
109
  FireForge build, `test` fails before launching stale artifacts; use `--build` to refresh
106
- or `--allow-stale-build` only for intentional out-of-band rebuilds. If an interrupted
107
- browser holds the Marionette port, `--kill-stale-marionette` terminates recognized browser
108
- holders and still refuses unrelated listeners. In dev builds, files under `obj-*/dist/bin`
110
+ or `--allow-stale-build` only for intentional out-of-band rebuilds. `--build-only`
111
+ packages mixed-harness paths once; `--extend-coverage` safely retains prior scoped
112
+ coverage. `--kill-stale-marionette` terminates recognized stale browsers. In dev builds,
113
+ files under `obj-*/dist/bin`
109
114
  may be symlinks back into the source tree (notably prefs), so edit source prefs directly
110
115
  and keep a backup before bisection experiments.
111
116
 
@@ -141,7 +146,9 @@ override the loose baseline so calls to nonexistent harness members fail at expo
141
146
  For CI enforcement, `fireforge status --check` exits non-zero when any unmanaged,
142
147
  drifted, or conflicted file exists (`--fail-on <class,...>` tunes the policy set, and
143
148
  `--json` composes — the JSON stays parseable and its `files[]` entries name the owning
144
- `patch`). `lint --per-patch --report <path>` writes a machine-readable JSON report with
149
+ `patch`). `--include-ownership` adds ownership rows to JSON status output. Scan-less
150
+ `re-export` previews drift; `--refuse-foreign-drift --expect <path>` restricts it to
151
+ intended files. `lint --per-patch --report <path>` writes a machine-readable JSON report with
145
152
  each patch's line count, tier, active size thresholds, issues, and lintIgnore-suppressed
146
153
  issues; the size metrics (`countNonBinaryDiffLines`, `resolvePatchSizeTier`,
147
154
  `getPatchSizeThresholds`) are also exported on the programmatic API.
@@ -149,11 +156,10 @@ issues; the size metrics (`countNonBinaryDiffLines`, `resolvePatchSizeTier`,
149
156
  For concurrent read-mostly verification beside a busy primary checkout,
150
157
  `fireforge tree create <name>` snapshots the project — applied engine tree included,
151
158
  `obj-*` excluded — into `.fireforge/trees/<name>` using filesystem copy-on-write (APFS
152
- clonefile / btrfs-XFS reflink; ~3 s for a 156 MB applied browser tree). Inside a tree,
153
- `status`, `lint`, `typecheck`, `verify`, `doctor`, and `export --dry-run` work unmodified
154
- with their own locks, while every mutating command is refused — patches and exports stay
155
- strictly serial in the primary tree. `tree list` reports staleness, `tree remove` deletes
156
- (refusing while a live process holds a tree lock), and `tree exec <name> -- <cmd>` runs a
159
+ clonefile / btrfs-XFS reflink; ~3 s for a 156 MB applied browser tree). Read-only commands
160
+ and export/re-export dry runs work inside a tree; mutations are refused. `--with-objdir`
161
+ adds a safely relocated build for build-less tests. `tree list` reports staleness,
162
+ `tree remove` deletes (refusing active locks), and `tree exec <name> -- <cmd>` runs a
157
163
  command inside a tree. Filesystems without CoW support refuse unless `--force-copy`
158
164
  explicitly accepts a full physical copy.
159
165
 
@@ -17,6 +17,7 @@ import { forceReleaseFurnaceLocksForActiveOperations, isSignalRollbackInFlight,
17
17
  import { waitForActiveCriticalSections } from '../src/core/signal-critical.js';
18
18
  import { CommandError } from '../src/errors/base.js';
19
19
  import { waitForActiveChildShutdown } from '../src/utils/process.js';
20
+ import { waitForStdioDrain } from '../src/utils/stdio-drain.js';
20
21
  /**
21
22
  * Upper bound (ms) the signal handler will wait for any in-flight critical
22
23
  * section (e.g. rebase apply + session persist) to finish before calling
@@ -34,13 +35,32 @@ const SIGNAL_CRITICAL_SECTION_TIMEOUT_MS = 5_000;
34
35
  * immediately, so an impatient operator is never stuck waiting.
35
36
  */
36
37
  const CHILD_SHUTDOWN_TIMEOUT_MS = 12_000;
38
+ /**
39
+ * Upper bound (ms) a delayed exit waits for stdout/stderr to drain. When
40
+ * stdout is a pipe it is ASYNC, and `process.exit()` discards anything
41
+ * queued past the 64 KiB kernel buffer — which truncated the
42
+ * `status --json --fail-on` refusal payload for every piped consumer.
43
+ * Bounded so a stalled reader can never wedge a failing process; an
44
+ * EPIPE'd/closed pipe releases the wait immediately.
45
+ */
46
+ const STDIO_FLUSH_TIMEOUT_MS = 5_000;
47
+ /**
48
+ * Drains stdio, then exits. Every delayed exit routes through here — only
49
+ * the second-Ctrl+C force-exit (an explicit "out NOW") and this helper's
50
+ * own call terminate directly.
51
+ */
52
+ function exitAfterStdioFlush(code) {
53
+ void waitForStdioDrain(STDIO_FLUSH_TIMEOUT_MS).finally(() => {
54
+ process.exit(code);
55
+ });
56
+ }
37
57
  installBrokenPipeHandler();
38
58
  process.on('unhandledRejection', (reason) => {
39
59
  console.error('Fatal error (unhandled rejection):', reason instanceof Error ? reason.message : reason);
40
60
  if (reason instanceof Error && reason.stack) {
41
61
  console.error(reason.stack);
42
62
  }
43
- process.exit(1);
63
+ exitAfterStdioFlush(1);
44
64
  });
45
65
  // SIGINT / SIGTERM handlers run any in-flight furnace rollback before
46
66
  // terminating. The library cannot call process.exit itself (the
@@ -81,7 +101,7 @@ function installFurnaceSignalHandler(signal, exitCode) {
81
101
  // best-effort (errors are logged, not thrown).
82
102
  .then(() => forceReleaseFurnaceLocksForActiveOperations())
83
103
  .finally(() => {
84
- process.exit(exitCode);
104
+ exitAfterStdioFlush(exitCode);
85
105
  });
86
106
  });
87
107
  }
@@ -89,9 +109,10 @@ installFurnaceSignalHandler('SIGINT', 130);
89
109
  installFurnaceSignalHandler('SIGTERM', 143);
90
110
  main().catch((error) => {
91
111
  if (error instanceof CommandError) {
92
- process.exit(error.exitCode);
112
+ exitAfterStdioFlush(error.exitCode);
113
+ return;
93
114
  }
94
115
  // Truly unexpected — CommandError should have been thrown by withErrorHandling
95
116
  console.error('Fatal error:', error);
96
- process.exit(1);
117
+ exitAfterStdioFlush(1);
97
118
  });
@@ -0,0 +1,9 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "version": "0.42.0",
4
+ "commit": "8200882a267f82974cd34356112990be0650c2ee",
5
+ "shortCommit": "8200882a267f",
6
+ "dirty": false,
7
+ "dirtyHash": null,
8
+ "buildTime": "2026-08-16T18:03:30.697Z"
9
+ }
package/dist/src/cli.js CHANGED
@@ -7,9 +7,9 @@ import { runTreeGuardHook } from './core/tree-guard.js';
7
7
  import { CancellationError, CommandError, FireForgeError } from './errors/base.js';
8
8
  import { ExitCode } from './errors/codes.js';
9
9
  import { ConfigNotFoundError } from './errors/config.js';
10
+ import { getCliVersion } from './utils/build-info.js';
10
11
  import { toError } from './utils/errors.js';
11
- import { cancel, error as logError, setVerbose } from './utils/logger.js';
12
- import { getPackageVersion } from './utils/package-root.js';
12
+ import { cancel, error as logError, setMachineOutputMode, setStdoutSealed, setVerbose, } from './utils/logger.js';
13
13
  const brokenPipeInstalledKey = Symbol.for('fireforge.cli.brokenPipeHandlerInstalled');
14
14
  const brokenPipeListenerKey = Symbol.for('fireforge.cli.brokenPipeHandlerListener');
15
15
  function getProcessState() {
@@ -100,6 +100,12 @@ export function withErrorHandling(handler) {
100
100
  await handler(...args);
101
101
  }
102
102
  catch (error) {
103
+ // Sentinel errors have already been rendered by the command. Passing
104
+ // them through prevents machine-readable refusal paths from acquiring
105
+ // an "Unexpected error" banner and stack trace on stderr.
106
+ if (error instanceof CommandError) {
107
+ throw error;
108
+ }
103
109
  if (error instanceof CancellationError) {
104
110
  cancel('Operation cancelled');
105
111
  // 130 (128+SIGINT) is the conventional "user interrupted" code —
@@ -118,6 +124,16 @@ export function withErrorHandling(handler) {
118
124
  }
119
125
  throw new CommandError(ExitCode.GENERAL_ERROR);
120
126
  }
127
+ finally {
128
+ // Central machine-mode and stdout-seal reset. Commands leave both
129
+ // ENGAGED while an error propagates (a mid-throw restore would route
130
+ // the styled error to stdout, corrupting the `--json`/`--raw` payload
131
+ // stream or displacing the FIREFORGE-VERDICT line); the reset happens
132
+ // here, after logError has picked its stream, so no state leaks into
133
+ // a subsequent in-process invocation.
134
+ setMachineOutputMode(false);
135
+ setStdoutSealed(false);
136
+ }
121
137
  };
122
138
  }
123
139
  /** Human-readable labels for command groups, in display order. */
@@ -246,6 +262,7 @@ export function createProgram() {
246
262
  .name('fireforge')
247
263
  .description('A build tool for customizing Firefox')
248
264
  .option('-v, --verbose', 'Enable debug output')
265
+ .option('--ignore-corrupt-tree-marker', 'Run even when .fireforge/tree.json exists but cannot be parsed. Without this the tree guard refuses, because an unreadable marker leaves it unknown whether this is a snapshot or the primary tree.')
249
266
  .hook('preAction', (thisCommand) => {
250
267
  const opts = thisCommand.opts();
251
268
  if (opts['verbose']) {
@@ -282,7 +299,7 @@ export async function main() {
282
299
  const userArgs = process.argv.slice(2);
283
300
  const hasSubcommand = userArgs.some((arg) => !arg.startsWith('-'));
284
301
  if (!hasSubcommand && userArgs.some((arg) => arg === '--version' || arg === '-V')) {
285
- process.stdout.write(`${getPackageVersion()}\n`);
302
+ process.stdout.write(`${getCliVersion()}\n`);
286
303
  return;
287
304
  }
288
305
  const program = createProgram();
@@ -10,6 +10,7 @@ import { pathExists } from '../utils/fs.js';
10
10
  import { error, info, intro, outro, warn } from '../utils/logger.js';
11
11
  import { detectBootstrapIssues, runPostBootstrapChecks } from './bootstrap-checks.js';
12
12
  import { reportDoctorResults } from './doctor.js';
13
+ import { resolveDoctorSeverity } from './doctor-check-core.js';
13
14
  /**
14
15
  * Builds a human-readable failure message for hard failures (non-zero exit).
15
16
  * Used only when mach bootstrap itself reports failure.
@@ -67,7 +68,10 @@ export async function bootstrapCommand(projectRoot) {
67
68
  const issues = detectBootstrapIssues(output);
68
69
  if (issues.length > 0) {
69
70
  const checks = await runPostBootstrapChecks(issues);
70
- const hasErrors = checks.some((c) => c.severity === 'error' || (!c.passed && !c.warning));
71
+ // Shares one resolver with reportDoctorResults so the two consumers of
72
+ // this same array cannot drift apart again — before 0.41.0 they
73
+ // disagreed on `passed: false, warning: true` with no severity.
74
+ const hasErrors = checks.some((c) => resolveDoctorSeverity(c) === 'error');
71
75
  info('');
72
76
  if (hasErrors) {
73
77
  warn('Bootstrap completed with issues:');
@@ -3,13 +3,12 @@ import { confirm } from '@clack/prompts';
3
3
  import { Command } from 'commander';
4
4
  import { getProjectPaths } from '../core/config.js';
5
5
  import { applyDiscardBaseline, describeConflictWarning, describeDiscardBaseline, describeDiscardOutcome, planDiscardBaselines, planUpstreamDiscards, summarizeDiscardBaselines, } from '../core/discard-baseline.js';
6
+ import { assertEngineGitReady } from '../core/engine-precondition.js';
6
7
  import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
7
- import { getHead, isGitRepository, isMissingHeadError } from '../core/git.js';
8
8
  import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
9
9
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
10
10
  import { GitError } from '../errors/git.js';
11
11
  import { toError } from '../utils/errors.js';
12
- import { pathExists } from '../utils/fs.js';
13
12
  import { info, intro, isCancel, outro, spinner, warn } from '../utils/logger.js';
14
13
  import { pickDefined } from '../utils/options.js';
15
14
  /**
@@ -126,26 +125,7 @@ async function discardDirectoryEntries(projectRoot, engineDir, patchesDir, dirPa
126
125
  export async function discardCommand(projectRoot, file, options = {}) {
127
126
  intro('FireForge Discard');
128
127
  const paths = getProjectPaths(projectRoot);
129
- // Check if engine exists
130
- if (!(await pathExists(paths.engine))) {
131
- throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
132
- }
133
- // Check if it's a git repository
134
- if (!(await isGitRepository(paths.engine))) {
135
- throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
136
- }
137
- // Unborn-HEAD guard (mirrors status): an interrupted download leaves a
138
- // repo with no baseline commit, where the entire ~300k-file tree reads
139
- // as untracked — a discard against that state is never what the operator
140
- // wants, and the guidance names the actual fix.
141
- try {
142
- await getHead(paths.engine);
143
- }
144
- catch (headError) {
145
- if (!isMissingHeadError(headError))
146
- throw headError;
147
- throw new GeneralError('Engine repository has no baseline commit yet — a previous "fireforge download" was interrupted before git created the initial Firefox source commit. Re-run "fireforge download --force" to recreate the baseline repository cleanly.');
148
- }
128
+ await assertEngineGitReady(paths.engine);
149
129
  // Check if the file has changes
150
130
  const statusEntries = await expandUntrackedDirectoryEntries(paths.engine, await getWorkingTreeStatus(paths.engine));
151
131
  const statusEntry = statusEntries.find((entry) => entry.file === file || entry.originalPath === file);
@@ -1,6 +1,7 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { readdir } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
+ import { toError } from '../../utils/errors.js';
4
5
  import { pathExists, readText } from '../../utils/fs.js';
5
6
  import { ok, warning } from '../doctor-check-core.js';
6
7
  async function readEngineText(engineDir, relativePath) {
@@ -9,17 +10,28 @@ async function readEngineText(engineDir, relativePath) {
9
10
  return null;
10
11
  return readText(fullPath);
11
12
  }
13
+ /**
14
+ * Collects every `browser.toml` under the engine's browser-chrome test tree.
15
+ *
16
+ * Reports unreadable directories separately from "none found": the consumer
17
+ * turns an empty result into the issue "no browser.toml files found", which is
18
+ * the opposite diagnosis from "the tree could not be read". A swallowed
19
+ * mid-walk EACCES also silently *shrank* the result set, so one unreadable
20
+ * subdirectory produced a false clean for that subtree.
21
+ */
12
22
  async function collectBrowserTomlFiles(root) {
13
23
  const testRoot = join(root, 'browser/base/content/test');
14
24
  if (!(await pathExists(testRoot)))
15
- return [];
25
+ return { files: [], unreadable: [] };
16
26
  const result = [];
27
+ const unreadable = [];
17
28
  async function walk(absDir, relDir) {
18
29
  let entries;
19
30
  try {
20
31
  entries = await readdir(absDir, { withFileTypes: true });
21
32
  }
22
- catch {
33
+ catch (error) {
34
+ unreadable.push(`${relDir || '.'} (${toError(error).message})`);
23
35
  return;
24
36
  }
25
37
  for (const entry of entries) {
@@ -34,11 +46,17 @@ async function collectBrowserTomlFiles(root) {
34
46
  }
35
47
  }
36
48
  await walk(testRoot, '');
37
- return result.sort();
49
+ return { files: result.sort(), unreadable };
38
50
  }
39
51
  async function runPostRebaseAudit(ctx) {
40
52
  const issues = [];
41
53
  const engineDir = ctx.paths.engine;
54
+ // The four probes below are upstream-Firefox shape markers: each names a
55
+ // file and a token that a healthy post-rebase tree must still contain. A
56
+ // rebase that dropped a fork patch, or an upstream reorganisation, shows up
57
+ // as one of these going missing. The tokens are deliberately coarse — this
58
+ // is a smoke check that runs warn-only behind an opt-in flag, not a
59
+ // structural validator.
42
60
  const mozConfigure = await readEngineText(engineDir, 'browser/moz.configure');
43
61
  if (mozConfigure === null) {
44
62
  issues.push('browser/moz.configure is missing');
@@ -69,7 +87,12 @@ async function runPostRebaseAudit(ctx) {
69
87
  issues.push('toolkit/content/jar.mn has no widget/element exposure entries');
70
88
  }
71
89
  const browserTomls = await collectBrowserTomlFiles(engineDir);
72
- if (browserTomls.length === 0) {
90
+ if (browserTomls.unreadable.length > 0) {
91
+ issues.push(`could not read ${String(browserTomls.unreadable.length)} ` +
92
+ `${browserTomls.unreadable.length === 1 ? 'directory' : 'directories'} under ` +
93
+ `browser/base/content/test: ${browserTomls.unreadable.join(', ')}`);
94
+ }
95
+ else if (browserTomls.files.length === 0) {
73
96
  issues.push('no browser.toml files found under browser/base/content/test');
74
97
  }
75
98
  if (issues.length === 0) {
@@ -87,6 +87,26 @@ export interface DoctorCheckDefinition {
87
87
  */
88
88
  fix?: string;
89
89
  }
90
+ /**
91
+ * Resolves a {@link DoctorCheck} to its effective severity, implementing the
92
+ * field-precedence contract documented on the interface
93
+ * (`src/types/commands/project.ts`):
94
+ *
95
+ * 1. `severity`, when present, is authoritative — regardless of `passed`.
96
+ * 2. Without `severity`, `passed: false` is an error unless `warning: true`
97
+ * downgrades it to a warning.
98
+ *
99
+ * This is the single implementation on purpose. Before 0.41.0 there were two
100
+ * and they disagreed on exactly one cell: `reportDoctorResults` consulted
101
+ * `warning` only inside the `passed === true` branch, so the documented
102
+ * downgrade produced an error and a non-zero doctor exit, while
103
+ * `bootstrap.ts` — reading the same array one line earlier — classified it as
104
+ * a warning. No in-tree producer emitted that combination (`failure()` always
105
+ * sets `severity: 'error'`), so the contradiction was latent rather than
106
+ * observable; the interface is exported, so the next hand-rolled check would
107
+ * have hit it.
108
+ */
109
+ export declare function resolveDoctorSeverity(check: DoctorCheck): 'ok' | 'warning' | 'error';
90
110
  /**
91
111
  * Builds a DoctorCheck object representing a successful "OK" check.
92
112
  */
@@ -4,6 +4,34 @@
4
4
  * `doctor.ts` and sibling check modules. Kept separate so registries can import
5
5
  * these symbols without creating cycles through `doctor.ts`.
6
6
  */
7
+ /**
8
+ * Resolves a {@link DoctorCheck} to its effective severity, implementing the
9
+ * field-precedence contract documented on the interface
10
+ * (`src/types/commands/project.ts`):
11
+ *
12
+ * 1. `severity`, when present, is authoritative — regardless of `passed`.
13
+ * 2. Without `severity`, `passed: false` is an error unless `warning: true`
14
+ * downgrades it to a warning.
15
+ *
16
+ * This is the single implementation on purpose. Before 0.41.0 there were two
17
+ * and they disagreed on exactly one cell: `reportDoctorResults` consulted
18
+ * `warning` only inside the `passed === true` branch, so the documented
19
+ * downgrade produced an error and a non-zero doctor exit, while
20
+ * `bootstrap.ts` — reading the same array one line earlier — classified it as
21
+ * a warning. No in-tree producer emitted that combination (`failure()` always
22
+ * sets `severity: 'error'`), so the contradiction was latent rather than
23
+ * observable; the interface is exported, so the next hand-rolled check would
24
+ * have hit it.
25
+ */
26
+ export function resolveDoctorSeverity(check) {
27
+ if (check.severity !== undefined) {
28
+ return check.severity;
29
+ }
30
+ if (check.warning) {
31
+ return 'warning';
32
+ }
33
+ return check.passed ? 'ok' : 'error';
34
+ }
7
35
  /**
8
36
  * Builds a DoctorCheck object representing a successful "OK" check.
9
37
  */
@@ -15,6 +15,8 @@ async function resolveDeclaredTool(projectRoot, tool) {
15
15
  return found.exitCode === 0 ? line?.trim() : undefined;
16
16
  }
17
17
  catch {
18
+ // `xcrun -find` is absent or the tool is unknown — either way the tool has
19
+ // no resolvable path, which is what `undefined` means to the caller.
18
20
  return undefined;
19
21
  }
20
22
  }