@hominis/fireforge 0.10.0 → 0.11.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 (174) hide show
  1. package/CHANGELOG.md +93 -1
  2. package/README.md +126 -239
  3. package/dist/bin/fireforge.js +26 -0
  4. package/dist/src/cli.d.ts +1 -1
  5. package/dist/src/cli.js +131 -52
  6. package/dist/src/commands/bootstrap.js +6 -2
  7. package/dist/src/commands/build.js +4 -2
  8. package/dist/src/commands/discard.js +16 -4
  9. package/dist/src/commands/doctor-furnace.d.ts +8 -0
  10. package/dist/src/commands/doctor-furnace.js +422 -0
  11. package/dist/src/commands/doctor.d.ts +115 -0
  12. package/dist/src/commands/doctor.js +327 -258
  13. package/dist/src/commands/download.js +16 -1
  14. package/dist/src/commands/export-all.js +15 -0
  15. package/dist/src/commands/export-flow.d.ts +91 -0
  16. package/dist/src/commands/export-flow.js +344 -0
  17. package/dist/src/commands/export.js +151 -5
  18. package/dist/src/commands/furnace/apply.d.ts +3 -2
  19. package/dist/src/commands/furnace/apply.js +169 -36
  20. package/dist/src/commands/furnace/create.js +162 -52
  21. package/dist/src/commands/furnace/deploy.js +156 -144
  22. package/dist/src/commands/furnace/diff.d.ts +8 -4
  23. package/dist/src/commands/furnace/diff.js +142 -73
  24. package/dist/src/commands/furnace/index.d.ts +6 -2
  25. package/dist/src/commands/furnace/index.js +76 -25
  26. package/dist/src/commands/furnace/init.d.ts +11 -0
  27. package/dist/src/commands/furnace/init.js +76 -0
  28. package/dist/src/commands/furnace/list.d.ts +4 -1
  29. package/dist/src/commands/furnace/list.js +35 -3
  30. package/dist/src/commands/furnace/override.d.ts +8 -0
  31. package/dist/src/commands/furnace/override.js +216 -26
  32. package/dist/src/commands/furnace/preview.js +184 -30
  33. package/dist/src/commands/furnace/refresh.d.ts +10 -0
  34. package/dist/src/commands/furnace/refresh.js +268 -0
  35. package/dist/src/commands/furnace/remove.js +285 -89
  36. package/dist/src/commands/furnace/rename.d.ts +5 -0
  37. package/dist/src/commands/furnace/rename.js +308 -0
  38. package/dist/src/commands/furnace/scan.d.ts +4 -1
  39. package/dist/src/commands/furnace/scan.js +72 -11
  40. package/dist/src/commands/furnace/status.js +85 -20
  41. package/dist/src/commands/furnace/sync.d.ts +12 -0
  42. package/dist/src/commands/furnace/sync.js +77 -0
  43. package/dist/src/commands/furnace/validate.d.ts +4 -1
  44. package/dist/src/commands/furnace/validate.js +99 -3
  45. package/dist/src/commands/furnace/validation-output.d.ts +24 -1
  46. package/dist/src/commands/furnace/validation-output.js +93 -1
  47. package/dist/src/commands/import.js +37 -4
  48. package/dist/src/commands/lint.js +11 -2
  49. package/dist/src/commands/manifest.d.ts +39 -0
  50. package/dist/src/commands/manifest.js +59 -0
  51. package/dist/src/commands/patch/delete.d.ts +28 -0
  52. package/dist/src/commands/patch/delete.js +209 -0
  53. package/dist/src/commands/patch/index.d.ts +17 -0
  54. package/dist/src/commands/patch/index.js +25 -0
  55. package/dist/src/commands/patch/reorder.d.ts +30 -0
  56. package/dist/src/commands/patch/reorder.js +377 -0
  57. package/dist/src/commands/re-export-files.d.ts +17 -0
  58. package/dist/src/commands/re-export-files.js +177 -0
  59. package/dist/src/commands/re-export.js +44 -0
  60. package/dist/src/commands/rebase/abort.d.ts +1 -1
  61. package/dist/src/commands/rebase/abort.js +12 -3
  62. package/dist/src/commands/rebase/confirm.d.ts +3 -3
  63. package/dist/src/commands/rebase/confirm.js +4 -4
  64. package/dist/src/commands/rebase/index.js +13 -4
  65. package/dist/src/commands/reset.js +20 -4
  66. package/dist/src/commands/run.js +46 -1
  67. package/dist/src/commands/setup-support.js +5 -5
  68. package/dist/src/commands/status.js +97 -6
  69. package/dist/src/commands/test.js +5 -37
  70. package/dist/src/commands/verify.d.ts +31 -0
  71. package/dist/src/commands/verify.js +126 -0
  72. package/dist/src/core/build-prepare.js +40 -16
  73. package/dist/src/core/destructive.d.ts +96 -0
  74. package/dist/src/core/destructive.js +137 -0
  75. package/dist/src/core/diff-hunks.d.ts +73 -0
  76. package/dist/src/core/diff-hunks.js +268 -0
  77. package/dist/src/core/firefox.d.ts +1 -1
  78. package/dist/src/core/firefox.js +1 -1
  79. package/dist/src/core/furnace-apply-helpers.d.ts +89 -6
  80. package/dist/src/core/furnace-apply-helpers.js +302 -57
  81. package/dist/src/core/furnace-apply-output.d.ts +16 -0
  82. package/dist/src/core/furnace-apply-output.js +57 -0
  83. package/dist/src/core/furnace-apply.d.ts +21 -3
  84. package/dist/src/core/furnace-apply.js +260 -29
  85. package/dist/src/core/furnace-checksum-utils.d.ts +4 -0
  86. package/dist/src/core/furnace-checksum-utils.js +24 -0
  87. package/dist/src/core/furnace-config.d.ts +28 -1
  88. package/dist/src/core/furnace-config.js +180 -17
  89. package/dist/src/core/furnace-constants.d.ts +22 -0
  90. package/dist/src/core/furnace-constants.js +36 -0
  91. package/dist/src/core/furnace-graph-utils.d.ts +11 -0
  92. package/dist/src/core/furnace-graph-utils.js +94 -0
  93. package/dist/src/core/furnace-operation.d.ts +108 -0
  94. package/dist/src/core/furnace-operation.js +220 -0
  95. package/dist/src/core/furnace-refresh.d.ts +20 -0
  96. package/dist/src/core/furnace-refresh.js +118 -0
  97. package/dist/src/core/furnace-registration-ast.d.ts +5 -0
  98. package/dist/src/core/furnace-registration-ast.js +134 -4
  99. package/dist/src/core/furnace-registration-remove.d.ts +25 -3
  100. package/dist/src/core/furnace-registration-remove.js +196 -62
  101. package/dist/src/core/furnace-registration-validate.d.ts +13 -1
  102. package/dist/src/core/furnace-registration-validate.js +15 -3
  103. package/dist/src/core/furnace-registration.d.ts +27 -4
  104. package/dist/src/core/furnace-registration.js +93 -11
  105. package/dist/src/core/furnace-rollback.d.ts +11 -0
  106. package/dist/src/core/furnace-rollback.js +78 -7
  107. package/dist/src/core/furnace-scanner.d.ts +8 -2
  108. package/dist/src/core/furnace-scanner.js +152 -55
  109. package/dist/src/core/furnace-stories.js +7 -5
  110. package/dist/src/core/furnace-validate-accessibility.js +7 -1
  111. package/dist/src/core/furnace-validate-compatibility.d.ts +1 -1
  112. package/dist/src/core/furnace-validate-compatibility.js +85 -1
  113. package/dist/src/core/furnace-validate-helpers.d.ts +4 -0
  114. package/dist/src/core/furnace-validate-helpers.js +31 -0
  115. package/dist/src/core/furnace-validate-registration.d.ts +17 -2
  116. package/dist/src/core/furnace-validate-registration.js +73 -3
  117. package/dist/src/core/furnace-validate-structure.d.ts +10 -2
  118. package/dist/src/core/furnace-validate-structure.js +45 -3
  119. package/dist/src/core/furnace-validate.d.ts +10 -1
  120. package/dist/src/core/furnace-validate.js +80 -6
  121. package/dist/src/core/furnace-version-drift.d.ts +55 -0
  122. package/dist/src/core/furnace-version-drift.js +101 -0
  123. package/dist/src/core/git-file-ops.d.ts +8 -0
  124. package/dist/src/core/git-file-ops.js +19 -6
  125. package/dist/src/core/lint-projection.d.ts +25 -0
  126. package/dist/src/core/lint-projection.js +44 -0
  127. package/dist/src/core/mach.d.ts +4 -2
  128. package/dist/src/core/mach.js +17 -2
  129. package/dist/src/core/markdown-table.d.ts +104 -0
  130. package/dist/src/core/markdown-table.js +266 -0
  131. package/dist/src/core/ownership-table.d.ts +53 -0
  132. package/dist/src/core/ownership-table.js +144 -0
  133. package/dist/src/core/patch-apply.d.ts +17 -3
  134. package/dist/src/core/patch-apply.js +86 -8
  135. package/dist/src/core/patch-export.d.ts +119 -5
  136. package/dist/src/core/patch-export.js +183 -25
  137. package/dist/src/core/patch-lint-cross.d.ts +195 -0
  138. package/dist/src/core/patch-lint-cross.js +428 -0
  139. package/dist/src/core/patch-lint-diff.d.ts +33 -0
  140. package/dist/src/core/patch-lint-diff.js +84 -0
  141. package/dist/src/core/patch-lint.d.ts +2 -4
  142. package/dist/src/core/patch-lint.js +12 -50
  143. package/dist/src/core/patch-lock.js +2 -1
  144. package/dist/src/core/patch-manifest-io.d.ts +102 -1
  145. package/dist/src/core/patch-manifest-io.js +270 -2
  146. package/dist/src/core/patch-manifest-query.d.ts +1 -1
  147. package/dist/src/core/patch-manifest-query.js +1 -1
  148. package/dist/src/core/patch-manifest.d.ts +1 -1
  149. package/dist/src/core/patch-manifest.js +1 -1
  150. package/dist/src/core/patch-transform.d.ts +12 -0
  151. package/dist/src/core/patch-transform.js +21 -7
  152. package/dist/src/core/token-manager.js +67 -69
  153. package/dist/src/core/wire-destroy.js +6 -3
  154. package/dist/src/core/wire-init.js +10 -4
  155. package/dist/src/core/wire-subscript.js +9 -3
  156. package/dist/src/core/wire-utils.d.ts +52 -5
  157. package/dist/src/core/wire-utils.js +69 -6
  158. package/dist/src/errors/base.d.ts +20 -0
  159. package/dist/src/errors/base.js +24 -0
  160. package/dist/src/errors/furnace.js +7 -1
  161. package/dist/src/errors/rebase.js +6 -1
  162. package/dist/src/types/commands/index.d.ts +1 -1
  163. package/dist/src/types/commands/options.d.ts +125 -4
  164. package/dist/src/types/commands/patches.d.ts +11 -1
  165. package/dist/src/types/config.d.ts +1 -1
  166. package/dist/src/types/furnace.d.ts +55 -1
  167. package/dist/src/utils/fs.d.ts +12 -0
  168. package/dist/src/utils/fs.js +30 -1
  169. package/dist/src/utils/package-root.d.ts +5 -0
  170. package/dist/src/utils/package-root.js +12 -0
  171. package/dist/src/utils/process.js +9 -4
  172. package/dist/src/utils/validation.d.ts +20 -2
  173. package/dist/src/utils/validation.js +26 -3
  174. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,97 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### New commands
6
+
7
+ - **`fireforge verify`** — read-only integrity check for the patch queue. Reports duplicate file creations across patches, forward imports, orphaned patch files, and manifest inconsistencies. Exits non-zero on any error, making it usable as a CI pre-flight gate.
8
+ - **`fireforge patch delete <name>`** — removes a patch file and its manifest entry atomically. Refuses when a later patch imports from a file the deleted patch owns (bypassable with `--force-unsafe`).
9
+ - **`fireforge patch reorder <name> --to <N> | --before <anchor> | --after <anchor>`** — moves a patch to a new position, renumbers surrounding patches, and runs cross-patch lint against the projected order before writing.
10
+
11
+ ### New flags and options
12
+
13
+ - `fireforge export --dry-run` previews the full export plan (filename, metadata, affected files) without writing. With `--supersede`, shows which existing patches would be absorbed and why.
14
+ - `fireforge export --order <N> | --before <anchor> | --after <anchor>` places a new patch at a specific position and shifts subsequent patches up.
15
+ - `fireforge re-export --files <paths> <patch>` restricts a re-export to an explicit file subset, useful for splitting or shrinking a patch's scope.
16
+ - `fireforge import --until <patch>` (alias `--stop-at`) applies patches only up to the named patch, useful for bisection.
17
+ - `fireforge status --ownership` prints a flat table mapping every managed path to its owning patch and flags ownership conflicts.
18
+ - `fireforge furnace apply --force` and `furnace deploy --force` proceed despite `baseVersion` drift between `furnace.json` and the Firefox version.
19
+ - `fireforge furnace deploy --skip-validate` skips the validation suite during deploy.
20
+ - `fireforge furnace override` now accepts multiple tag names in a single invocation for batch creation.
21
+ - `fireforge import --dry-run` previews which patches would be applied, in order, without modifying the engine.
22
+ - `fireforge status --json` outputs classified file status as machine-readable JSON for CI scripting.
23
+
24
+ ### Furnace improvements
25
+
26
+ - **`furnace refresh <name>`** merges upstream Firefox changes into an override workspace via three-way merge. Clean merges update `baseVersion` automatically; conflicts leave standard markers for manual resolution. Supports `--dry-run` and `--reset-base` (skip merge, just update the baseline).
27
+ - **Full overrides now include shared Fluent files.** Localized widgets (those with a `.ftl` file) are now copied, applied, removed, and diffed end-to-end instead of silently dropping the locale payload.
28
+ - **`furnace diff` rewritten with proper multi-hunk output.** Scattered edits across a file now render as separate hunks with context lines instead of one giant block.
29
+ - **`furnace apply` detects and undeploys deleted workspace files.** If you remove a file from a component's workspace directory and re-run apply, the corresponding engine copy is cleaned up and registrations are adjusted.
30
+ - **`furnace status` now distinguishes workspace edits from engine drift.** These have different remediation paths and are reported separately instead of collapsed into one message.
31
+ - **`furnace scan` offers to override just-added stock components** in the same interactive session.
32
+ - **Preview stages workspace files into the engine** before launching Storybook so fresh edits actually appear, then rolls them back on teardown.
33
+ - **FTL base path is now configurable** via `ftlBasePath` in `furnace.json` for projects with non-standard locale paths.
34
+ - **`scanPaths` in `furnace.json`** lets `furnace scan` discover components outside the default `toolkit/content/widgets`.
35
+ - File copies during apply are now parallelized within each component.
36
+
37
+ ### New lint rules
38
+
39
+ - **`duplicate-new-file-creation`** (error) — flags any path that appears as a new-file creation in more than one patch.
40
+ - **`forward-import`** (error) — flags imports that reference a file owned by a later-ordered patch. Supports an inline suppression marker (`// fireforge-ignore: forward-import`) for false positives from basename collisions.
41
+
42
+ ### Doctor and diagnostics
43
+
44
+ - `fireforge doctor` now runs the full Furnace component validation suite (structure, accessibility, compatibility, registration) and reports issues without needing a separate `furnace validate` run.
45
+ - New `--repair-furnace` flag reconciles the engine when a furnace operation was interrupted or left inconsistent state.
46
+ - `fireforge doctor` checks that Firefox-internal paths Furnace depends on still exist and reports targeted warnings when they are missing.
47
+ - `furnace validate` now enforces `.ftl` presence for `localized: true` custom components and no longer false-warns about missing CSS jar entries when the component has no CSS file.
48
+
49
+ ### Reliability
50
+
51
+ - All furnace mutations now serialize on a project-wide lock, preventing concurrent operations from racing on engine state.
52
+ - Ctrl+C and SIGTERM trigger clean rollback across all furnace commands. A `pendingRepair` marker is only written when rollback was actually incomplete, so normal interrupts do not leave false-positive repair flags.
53
+ - Override `baseVersion` drift now blocks `apply` and `deploy` by default instead of warning and continuing. Pass `--force` to override, or use `furnace refresh` to update the baseline.
54
+ - Post-apply consistency check verifies that `customElements.js` and `jar.mn` entries match what was deployed.
55
+ - Engine-side content hashes are cached in the furnace state file, making drift detection faster for the common no-change case.
56
+ - `build` and `test --build` now share the same preparation pipeline including Furnace apply, so incremental test builds no longer run against stale component state.
57
+ - `furnace remove` on an override now restores every overridden engine file to its Firefox baseline instead of leaving deployed files behind.
58
+ - Scanner results are cached by content hash within a process, avoiding redundant parsing during scan-status-apply sequences.
59
+ - `download` and `build` now check available disk space before starting and warn when free space is low (Firefox source ~5 GB, full build ~20 GB).
60
+ - `getProjectRoot()` now throws instead of silent fallback.
61
+ - `getPackageRoot()` caches its result after the first call, avoiding repeated filesystem walks.
62
+ - Process spawn timeout is now enforced via `AbortSignal.timeout()` instead of the unreliable `timeout` option on `child_process.spawn()`.
63
+
64
+ ### Bug fixes
65
+
66
+ - `furnace refresh` now correctly advances the per-override `baseCommit` to the engine HEAD after a successful merge, preventing phantom conflicts on subsequent refreshes.
67
+ - `furnace rename` uses the correct file-removal function for FTL files.
68
+ - `furnace remove` now parses browser.toml sections properly, cleaning up metadata keys below the section header instead of leaving stale fragments.
69
+ - Registration duplicate detection now uses exact path matching so `moz-card` no longer collides with `moz-card-group`.
70
+ - The `customElements.js` parser now accepts `const` and `var` loop declarations alongside `let`.
71
+ - `re-export --files` refuses to write when a requested path would produce no hunks, preventing manifest/patch-body desynchronisation.
72
+ - `patch delete` now respects the `fireforge-ignore: forward-import` suppression marker, matching the behavior of `verify` and `lint`.
73
+ - Furnace apply no longer reports "up to date" after `reset --yes` or `download --force` wiped the engine. Both commands now clear the furnace state, and the skip logic checks engine-side drift before trusting cached checksums.
74
+ - `status` now classifies Furnace-managed engine paths as `furnace` instead of `unmanaged`, and `export-all` refuses to capture them.
75
+ - AST parser fallback in the scanner now emits a warning instead of failing silently.
76
+ - `stock` entries in `furnace.json` are validated against a safe character set, rejecting path-traversal strings.
77
+
78
+ ### Internal
79
+
80
+ - CLI command registration is now driven by a declarative manifest instead of hand-listed calls.
81
+ - Doctor checks are a declarative registry with per-check `run`, `skipIf`, and `fix` fields.
82
+ - New shared destructive-op framework handles confirmation, `--dry-run`, `--yes`/`--force-unsafe`, and audit logging for the patch mutation commands.
83
+ - Export internals factored into `planExport` / `executeExportPlan` so dry-run and real writes share one code path.
84
+ - Ownership table builder extracted from `status.ts` into `src/core/ownership-table.ts`.
85
+ - Cross-patch lint regression calculator extracted from `re-export.ts` into `src/core/lint-projection.ts`.
86
+ - The `re-export --files` path extracted into `src/commands/re-export-files.ts` to keep `re-export.ts` under the line limit.
87
+ - `max-lines` and `max-lines-per-function` ESLint rules promoted from `warn` to `error`.
88
+ - Doctor check ordering dependencies documented in the registry comment.
89
+ - Default Firefox version bumped to ESR 146.
90
+
91
+ ### Packaging
92
+
93
+ - Package metadata and lockfile updated to 0.11.0.
94
+
3
95
  ## 0.10.0
4
96
 
5
97
  ### Patch workflow validation
@@ -22,7 +114,7 @@
22
114
 
23
115
  ### Packaging
24
116
 
25
- - Package metadata and smoke tests now use version `0.10.0`.
117
+ - Package metadata and smoke tests now use version 0.10.0.
26
118
  - npm install instructions use the scoped `@hominis/fireforge` package name.
27
119
  - Packaging and full Firefox integration helpers now handle platform-specific npm and mozconfig names more consistently.
28
120
 
package/README.md CHANGED
@@ -6,16 +6,39 @@
6
6
  [![types](https://img.shields.io/npm/types/@hominis/fireforge)](https://www.npmjs.com/package/@hominis/fireforge)
7
7
  [![npm downloads](https://img.shields.io/npm/dm/@hominis/fireforge)](https://www.npmjs.com/package/@hominis/fireforge)
8
8
 
9
- **Build and maintain your own Firefox-based browser with a patch-first workflow.**
9
+ **Build and maintain your own Firefox-based browser with a patch-first workflow**
10
10
 
11
- FireForge gives you a toolkit for forking Firefox: download a specific ESR release, manage your customisations as an ordered stack of contextual patches, survive version upgrades with semi-automated rebase, wire custom code into Mozilla's startup paths, and build the result. It also ships **Furnace**, a component system for creating and overriding Firefox custom elements.
11
+ FireForge gives you a toolkit for forking Firefox: download a specific ESR release, manage your customisations as a series of patches, survive version upgrades with semi-automated rebase, wire custom code into Mozilla's startup paths, and build the result. It also ships **Furnace**, a component system for creating and overriding Firefox custom elements under `toolkit/content/widgets`.
12
12
 
13
- Inspired by [fern.js](https://github.com/nicktrosper/user-agent-desktop?tab=readme-ov-file#user-agent-desktop) and [Melon](https://github.com/nicktrosper/nicktrosper-melon).
13
+ Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon](https://github.com/dothq/melon).
14
14
 
15
- ---
15
+ ## Features
16
+
17
+ - **Patch-based fork management** Your customisations live as portable, ordered `.patch` files. Export single files, multiple paths, or everything at once. Contextual diffs mean upstream security fixes are not silently dropped when you rebase.
18
+
19
+ - **Semi-automated ESR rebase** `fireforge rebase` replays your patch stack onto new Firefox source with escalating fuzz matching. When a patch fails, you fix it manually and `--continue`. The full stack gets re-exported with updated version stamps.
20
+
21
+ - **Wiring and registration** `fireforge wire` and `fireforge register` inject your code into Mozilla's startup paths, build manifests, and JAR files with a single command. The injection is AST-based (via Acorn), so it survives formatting changes applied between versions.
22
+
23
+ - **Furnace component system** Override existing Firefox custom elements or create new ones under `toolkit/content/widgets` (CSS-only restyles, full behavioural forks, or entirely new widgets).
24
+
25
+ - **Design token management** Track CSS custom property coverage across your modified files.
26
+
27
+ - **Quality checks** `fireforge lint` catches fork-specific issues (raw colours, missing licence headers, relative imports, large patches, cross-patch ordering problems) before you export. `fireforge verify` runs a read-only integrity check over the whole patch queue. `fireforge doctor` diagnoses project health including Furnace component validation.
28
+
29
+ - **Built and validated against real Firefox code** Developed by editing a real Firefox ESR codebase, learning from existing patch tools, observing the breakages and edge cases that surfaced, and turning those findings into a realistic test suite. In-repo tests are thus grounded in actual development scenarios. Full end-to-end runs are currently only run locally, as they require about 30 GB of disk and significant compute for the full build.
16
30
 
17
31
  ## Quick Start
18
32
 
33
+ ### Requirements
34
+
35
+ - **Node.js 20+**
36
+ - **Python 3** (required by Firefox's `mach` build system).
37
+ - **Git**
38
+ - Platform build tools: Xcode on macOS, `build-essential` on Linux, Visual Studio Build Tools on Windows.
39
+
40
+ ### Setup
41
+
19
42
  ```bash
20
43
  mkdir mybrowser && cd mybrowser
21
44
  npm init -y
@@ -31,11 +54,7 @@ npx fireforge run # launch it
31
54
 
32
55
  Your project now has `fireforge.json`, an `engine/` directory with Firefox source, and a `patches/` directory ready for your first customisation.
33
56
 
34
- A few things worth noting here: `bootstrap` may prompt for elevated permissions depending on your platform and what build dependencies are already present. This is not something we can avoid, since Mozilla's own `mach bootstrap` requires it, and wrapping that in our own sudo logic would be worse in every way that matters.
35
-
36
- ---
37
-
38
- ## Core Workflow
57
+ ### Workflow Overview
39
58
 
40
59
  ```bash
41
60
  # 1. Make changes inside engine/
@@ -49,45 +68,14 @@ npx fireforge export browser/base/content/browser.js \
49
68
  # with metadata tracked in patches/patches.json
50
69
 
51
70
  # 4. Later, reset and replay to verify everything applies cleanly
52
- npx fireforge reset --force
53
- npx fireforge import
71
+ npx fireforge reset --yes
72
+ npx fireforge import # --dry-run to preview without applying
54
73
 
55
74
  # 5. When Firefox releases a new ESR, update fireforge.json, re-download, and rebase
56
75
  npx fireforge download --force
57
76
  npx fireforge rebase
58
77
  ```
59
78
 
60
- The reason your customisations live as patches rather than as a permanent fork branch is, in fairness, a trade-off. Branches are more familiar, but they make upstream merges progressively harder as your changes grow, and they obscure which modifications are intentional versus which are artefacts of merge resolution. Patches are explicit. Each one is a discrete, portable unit of intent. The cost is that you need tooling to manage the stack, which is what FireForge exists to provide.
61
-
62
- ---
63
-
64
- ## What You Get
65
-
66
- - **Patch-based fork management.** Your customisations live as portable, ordered `.patch` files. Export single files, multiple paths, or everything at once. Contextual diffs mean upstream security fixes are not silently dropped when you rebase. This matters more then it might seem at first, because silent patch drift in a browser fork is the sort of bug that only surfaces when someone audits your security posture six months later.
67
-
68
- - **Semi-automated ESR rebase.** `fireforge rebase` replays your patch stack onto new Firefox source with escalating fuzz matching. When a patch fails, you fix it manually and `--continue`. The full stack gets re-exported with updated version stamps. It would be cleaner to make this fully automatic, of course, but patches that touch the same code as upstream changes genuinely require human judgement about which intent should win. Pretending otherwise would be worse.
69
-
70
- - **Wiring and registration.** `fireforge wire` and `fireforge register` inject your code into Mozilla's startup paths, build manifests, and JAR files with a single command. The injection is AST-based (via Acorn), not regex-based, which means it survives the formatting changes that Mozilla applies between versions. This is less about elegance then about not breaking on every minor release.
71
-
72
- - **Furnace component system.** Override existing Firefox custom elements (CSS-only or full fork) or create new ones. Storybook preview included. The component types are `stock` (tracked for preview, no local files), `override` (CSS-only restyle or full behavioural fork), and `custom` (entirely new elements).
73
-
74
- - **Design token management.** Track CSS custom property coverage across your modified files. This exists because raw colour values in a browser fork become a maintenance problem faster then you would expect, and catching them at export time is considerably cheaper then catching them during a visual regression review.
75
-
76
- - **Quality checks.** `fireforge lint` catches fork-specific issues (raw colours, missing licence headers, relative imports, large patches) before you export. `fireforge doctor` diagnoses project health. These are not redundant with Mozilla's own `./mach lint`, which does not know about your fork-specific conventions.
77
-
78
- - **Tested against real Firefox source.** We run end-to-end test passes against actual Firefox ESR 140 source code as part of development. The in-repo test suite is derived from those real-world runs, reflecting actual developer scenarios (multi-file patches, conflict resolution, manifest recovery, binary assets) without requiring 30 GB of Firefox source to execute. 1400+ tests run in under 15 seconds.
79
-
80
- ---
81
-
82
- ## Requirements
83
-
84
- - **Node.js 20+.** Version 18 will appear to work initially, but the native fetch usage in the request layer will fail silently in certain edge cases, which is the sort of thing you would rather discover now.
85
- - **Python 3** (required by Firefox's `mach` build system).
86
- - **Git.**
87
- - Platform build tools: Xcode on macOS, `build-essential` on Linux, Visual Studio on Windows. If you are on an M-series Mac and encounter native module compilation errors, this is almost certainly the `sharp` dependency in your broader toolchain, not FireForge itself.
88
-
89
- ---
90
-
91
79
  ## Patch Workflow
92
80
 
93
81
  Patches live in `patches/`, applied by numeric filename prefix, and tracked in `patches/patches.json`:
@@ -102,7 +90,26 @@ patches/
102
90
 
103
91
  **Categories:** `branding` | `ui` | `privacy` | `security` | `infra`
104
92
 
105
- The category system is intentionally coarse. Finer-grained categorisation sounds appealing in theory, but in practice it creates more classification arguments then it resolves, and the numeric ordering already handles sequencing.
93
+ The category system is intentionally broad. The numeric ordering provides sequencing.
94
+
95
+ ### Importing patches
96
+
97
+ ```bash
98
+ # Apply all patches from patches/ to the engine
99
+ fireforge import
100
+
101
+ # Preview what would be applied without modifying the engine
102
+ fireforge import --dry-run
103
+
104
+ # Apply patches up to (and including) a specific one
105
+ fireforge import --until 003-ui-sidebar-tweaks.patch
106
+
107
+ # Keep going if a patch fails instead of stopping
108
+ fireforge import --continue
109
+
110
+ # Force-apply even when the engine has drifted or has unmanaged changes
111
+ fireforge import --force
112
+ ```
106
113
 
107
114
  ### Exporting changes
108
115
 
@@ -119,9 +126,19 @@ fireforge export-all --name "all-changes" --category ui
119
126
 
120
127
  # Regenerate patches after further edits
121
128
  fireforge re-export --all --scan
129
+
130
+ # Preview what an export would do without writing
131
+ fireforge export browser/base/content/browser.js --dry-run
132
+
133
+ # Insert a new patch at a specific position
134
+ fireforge export browser/base/content/browser.js --order 3 --name "inserted" --category ui
135
+ fireforge export browser/base/content/browser.js --before 005-ui-sidebar.patch --name "prelim"
136
+
137
+ # Restrict a re-export to a specific file subset
138
+ fireforge re-export --files browser/base/content/browser.js 002-ui-toolbar
122
139
  ```
123
140
 
124
- ### Rebasing onto a new Firefox version
141
+ ### Rebasing on top of a new Firefox version
125
142
 
126
143
  1. Update `firefox.version` in `fireforge.json`
127
144
  2. `fireforge download --force`
@@ -129,8 +146,6 @@ fireforge re-export --all --scan
129
146
  4. Fix any rejects, then `fireforge rebase --continue`
130
147
  5. If stuck, `fireforge rebase --abort` to restore the pre-rebase state
131
148
 
132
- Mind you, the rebase process is deliberately conservative: it will stop at the first patch that cannot be applied cleanly rather then guessing at a resolution and potentially corrupting your intent. This is slower but considerably safer, especially for security-sensitive patches where a misapplied hunk could silently undo a fix.
133
-
134
149
  ### Resolving conflicts
135
150
 
136
151
  When `fireforge import` fails on a patch, fix the `.rej` files in `engine/`, then:
@@ -157,40 +172,64 @@ This re-exports the fixed patch and continues applying the remaining stack.
157
172
  "name": "custom-logo",
158
173
  "description": "Replaces default Firefox branding with custom logo",
159
174
  "createdAt": "2025-01-15T10:30:00Z",
160
- "sourceEsrVersion": "140.0esr",
175
+ "sourceEsrVersion": "146.0esr",
161
176
  "filesAffected": ["browser/branding/official/logo.png"]
162
177
  }
163
178
  ]
164
179
  }
165
180
  ```
166
181
 
167
- If the manifest drifts after an interrupted export or manual edits, `fireforge import` will stop rather then silently applying a stale stack. Use `fireforge doctor --repair-patches-manifest` to rebuild it from disk. The rebuild is deterministic: it reads patch headers, not cached state, so the result is always consistent with what is actually on the filesystem.
182
+ If the manifest drifts after an interrupted export or manual edits, `fireforge import` will stop rather then silently applying a stale stack. Use `fireforge doctor --repair-patches-manifest` to rebuild it from disk. Because the rebuild is deterministic, the result will always be consistent with what is actually on the filesystem.
168
183
 
169
184
  </details>
170
185
 
171
186
  <details>
172
187
  <summary>Patch lint checks</summary>
173
188
 
174
- `fireforge lint` runs automatically during export, export-all, and re-export. Use `--skip-lint` to downgrade errors to warnings, though I would recommend against making that a habit.
189
+ `fireforge lint` runs automatically during export, export-all, and re-export. Use `--skip-lint` to downgrade errors to warnings. Errors block the export; warnings are printed but do not block.
190
+
191
+ | Check | Scope | Severity |
192
+ | ------------------------------ | ------------------------------------- | -------- |
193
+ | `missing-license-header` | New files (JS/CSS/FTL) | error |
194
+ | `relative-import` | JS/MJS files | error |
195
+ | `token-prefix-violation` | CSS files (with furnace) | error |
196
+ | `raw-color-value` | Introduced CSS color values | error |
197
+ | `duplicate-new-file-creation` | Same path created by multiple patches | error |
198
+ | `forward-import` | Patch imports from a later-patch file | error |
199
+ | `missing-modification-comment` | Modified upstream JS/MJS | warning |
200
+ | `file-too-large` | New files >650 lines | warning |
201
+ | `missing-jsdoc` | Exports in new `.sys.mjs` | warning |
202
+ | `observer-topic-naming` | Observer topics with binaryName | warning |
203
+ | `large-patch-files` | Patches affecting >5 files | warning |
204
+ | `large-patch-lines` | Patches >300 lines | warning |
205
+
206
+ The two cross-patch rules (`duplicate-new-file-creation` and `forward-import`) run over the whole patch queue rather than a single diff, catching ordering issues that only surface during `import`. Forward-import detection compares leaf filenames, so a false positive is theoretically possible when two patches create files with the same basename in different directories. Suppress with an inline `// fireforge-ignore: forward-import` comment on or above the import line. This is currently the only lint rule that supports inline suppression.
175
207
 
176
- | Check | Scope | Severity |
177
- | ------------------------------ | ------------------------------- | -------- |
178
- | `missing-license-header` | New files (JS/CSS/FTL) | error |
179
- | `relative-import` | JS/MJS files | error |
180
- | `token-prefix-violation` | CSS files (with furnace) | error |
181
- | `raw-color-value` | Introduced CSS color values | error |
182
- | `missing-modification-comment` | Modified upstream JS/MJS | warning |
183
- | `file-too-large` | New files >650 lines | warning |
184
- | `missing-jsdoc` | Exports in new `.sys.mjs` | warning |
185
- | `observer-topic-naming` | Observer topics with binaryName | warning |
186
- | `large-patch-files` | Patches affecting >5 files | warning |
187
- | `large-patch-lines` | Patches >300 lines | warning |
208
+ </details>
188
209
 
189
- These catch fork-specific issues that Mozilla's `./mach lint` does not cover. The severity levels are not arbitrary: errors block export because they indicate structural problems that will cause failures downstream, while warnings flag things that are suboptimal but not immediately dangerous.
210
+ ### Repairing a broken patch queue
190
211
 
191
- </details>
212
+ When a patch queue drifts — overlapping new-file creations, forward imports, manifest desync — start with diagnosis:
213
+
214
+ ```bash
215
+ fireforge verify # fsck: manifest + cross-patch lint
216
+ fireforge lint # includes the same cross-patch rules
217
+ fireforge status --ownership # flat path → owning patch table
218
+ fireforge status --json # machine-readable classified output
219
+ ```
192
220
 
193
- ---
221
+ Then fix with the appropriate primitive:
222
+
223
+ | Problem | Fix |
224
+ | ---------------------------------------------- | --------------------------------------------------------------------- |
225
+ | Two patches each creating the same file | `fireforge patch delete <duplicate>` or `fireforge re-export --files` |
226
+ | A patch imports from a module in a later patch | `fireforge patch reorder <later> --before <importer>` |
227
+ | Wrong patch ordering | `fireforge patch reorder <patch> --to <N>` |
228
+ | A patch claims files that belong elsewhere | `fireforge re-export --files <subset> <patch>` |
229
+ | Manifest references a missing patch file | `fireforge doctor --repair-patches-manifest` |
230
+ | Unmanaged changes you want to discard | `fireforge discard <file>` or `fireforge reset` |
231
+
232
+ Every destructive command defaults to an interactive confirmation with a change summary. `--dry-run` previews without writing; `--yes` skips the prompt for CI; `--force-unsafe` bypasses structural refusals when you have context the linter cannot see. Do not hand-edit `patches.json` — it is owned by FireForge.
194
233
 
195
234
  ## Wiring Custom Code
196
235
 
@@ -229,81 +268,28 @@ fireforge register browser/modules/mybrowser/MyStore.sys.mjs
229
268
 
230
269
  </details>
231
270
 
232
- ---
233
-
234
- ## Furnace (Component System)
235
-
236
- ```bash
237
- fireforge furnace scan # discover available components
238
- fireforge furnace override moz-button -t css-only # fork an existing one
239
- fireforge furnace create moz-my-widget # create a new one
240
- fireforge furnace deploy --dry-run # preview
241
- fireforge furnace deploy # apply + validate
242
- ```
243
-
244
- Furnace manages Firefox custom elements (`MozLitElement`). Override stock components with CSS-only restyles or full forks, or scaffold entirely new ones. Changes are applied to `engine/` and then captured by the patch system, which means Furnace is not a separate persistence layer; it feeds into the same patch workflow as everything else.
245
-
246
- <details>
247
- <summary>Component types</summary>
248
-
249
- | Type | Description | Local files |
250
- | ------------ | ----------------------------------------------------------------- | ------------------------------ |
251
- | **Stock** | Engine components tracked for Storybook preview | None |
252
- | **Override** | Forked copies: `css-only` (restyle) or `full` (behaviour + style) | `components/overrides/<name>/` |
253
- | **Custom** | New elements that do not exist in Firefox | `components/custom/<name>/` |
271
+ ## Furnace (UI Component System)
254
272
 
255
- </details>
273
+ Furnace manages Firefox custom elements (`MozLitElement`) under `toolkit/content/widgets`. You can override existing components or create new ones. Changes feed into the same patch workflow as everything else — Furnace is not a separate persistence layer.
256
274
 
257
- <details>
258
- <summary>Validation checks</summary>
259
-
260
- Furnace validates components on deploy. Errors block apply; warnings are advisory. The distinction is not about severity in the abstract but about whether a violation will cause a runtime failure versus a maintenance headache.
261
-
262
- | Check | Severity | Description |
263
- | ------------------------ | -------- | ------------------------------------------- |
264
- | `missing-mjs` | error | Custom component missing `.mjs` file |
265
- | `missing-css` | warning | No `.css` file |
266
- | `filename-mismatch` | error | File name does not match tag name |
267
- | `missing-override-json` | error | Override missing `override.json` |
268
- | `no-aria-role` | warning | Generic interactive markup lacks semantics |
269
- | `no-keyboard-handler` | warning | Has `@click` but no keyboard handler |
270
- | `relative-import` | error | Imports must use `chrome://` URIs |
271
- | `raw-color-value` | error | Raw hex/rgb/hsl (use CSS custom properties) |
272
- | `token-prefix-violation` | error | CSS variable does not match `tokenPrefix` |
275
+ There are three component types:
273
276
 
274
- </details>
277
+ | Type | What it is | Local files |
278
+ | ------------ | ------------------------------------------------------ | ------------------------------ |
279
+ | **Stock** | Engine components tracked for Storybook preview | None |
280
+ | **Override** | Forked copy: `css-only` (restyle) or `full` (JS + CSS) | `components/overrides/<name>/` |
281
+ | **Custom** | New element that does not exist in Firefox | `components/custom/<name>/` |
275
282
 
276
- <details>
277
- <summary>furnace.json schema</summary>
278
-
279
- ```jsonc
280
- {
281
- "version": 1,
282
- "componentPrefix": "moz-",
283
- "stock": ["moz-button", "moz-toggle"],
284
- "overrides": {
285
- "moz-button": {
286
- "type": "css-only",
287
- "description": "Custom button styles",
288
- "basePath": "toolkit/content/widgets/moz-button",
289
- "baseVersion": "134.0",
290
- },
291
- },
292
- "custom": {
293
- "moz-my-widget": {
294
- "description": "A new widget",
295
- "targetPath": "toolkit/content/widgets/moz-my-widget",
296
- "register": true,
297
- "localized": false,
298
- "composes": ["moz-button"],
299
- },
300
- },
301
- }
283
+ ```bash
284
+ fireforge furnace scan # discover components in the engine
285
+ fireforge furnace override moz-button -t css-only # fork with CSS-only restyle
286
+ fireforge furnace create moz-my-widget # scaffold a new component
287
+ fireforge furnace deploy # apply to engine/ + validate
288
+ fireforge furnace status # workspace vs engine drift
289
+ fireforge furnace diff moz-button # unified diff against baseline
302
290
  ```
303
291
 
304
- </details>
305
-
306
- ---
292
+ `furnace deploy` validates components before applying — errors block, warnings are advisory. `fireforge build` and `fireforge test --build` run apply automatically. Use `fireforge doctor --repair-furnace` if the engine gets out of sync.
307
293
 
308
294
  ## Configuration
309
295
 
@@ -317,7 +303,7 @@ Furnace validates components on deploy. Errors block apply; warnings are advisor
317
303
  "binaryName": "mybrowser",
318
304
  "license": "EUPL-1.2",
319
305
  "firefox": {
320
- "version": "140.0esr",
306
+ "version": "146.0esr",
321
307
  "product": "firefox-esr"
322
308
  },
323
309
  "build": { "jobs": 8 },
@@ -325,114 +311,15 @@ Furnace validates components on deploy. Errors block apply; warnings are advisor
325
311
  }
326
312
  ```
327
313
 
328
- Use `fireforge config <key> [value]` to read or update values. Run `fireforge --help` and `fireforge <command> --help` for the full option reference. The `jobs` value defaults to your CPU core count, which is usually reasonable but not always optimal; Firefox's build system has enough sequential bottlenecks that doubling your core count does not halve your build time, for what it is worth.
329
-
330
- ---
331
-
332
- ## Testing Methodology
333
-
334
- FireForge's test suite is designed around a constraint that, at least in my experience, does not get enough attention: realistic tests do not have to be slow.
335
-
336
- ### Real Firefox validation
337
-
338
- We run full end-to-end test passes against real Firefox ESR 140 source code in a production fork setup. These validate the entire workflow: setup, download, bootstrap, build, export, import, discard, and recovery.
339
-
340
- ### Derived in-repo tests
341
-
342
- The 1400+ in-repo tests are not idealised mocks. They are derived from those real Firefox runs. Every fixture, edge case, and scenario was first observed against actual Firefox source, then distilled into a deterministic test that runs in seconds. Examples:
343
-
344
- - CSS design tokens with `light-dark(#hex)` from a real 348-line tokens file
345
- - BrowserGlue lazy import with `// BRAND:` markers from a real 2-hunk modification
346
- - Multi-file theme patches spanning CSS + manifest + build system from real patches
347
- - Observer topic regex edge cases from a real `notifyObservers` call
348
-
349
- This means the fast test suite covers the same behavioural surface as the full-tree runs, without requiring 30 GB of Firefox source. It would be fair to call the fixtures "synthetic" in the sense that they are not the original files, but the scenarios they encode are not invented; they are reproductions of real behaviour we observed during development.
350
-
351
- <details>
352
- <summary>Running the full-tree suite</summary>
353
-
354
- The opt-in full-tree suite exercises a connected workflow against a prepared Firefox project:
355
-
356
- ```bash
357
- FIREFORGE_FULL_PROJECT_ROOT=/path/to/project npm run test:firefox-full
358
- ```
359
-
360
- Optional environment variables:
361
-
362
- - `FIREFORGE_FULL_BUILD_MODE=ui|full` (defaults to `ui`)
363
- - `FIREFORGE_FULL_TARGET_FILE=browser/base/content/browser.js` (override the target file for export/import)
364
- - `FIREFORGE_FULL_KEEP_PATCH=1` (keep the temporary patch instead of cleaning up)
365
- - `FIREFORGE_FULL_SKIP_SETUP=1` (skip `setup --force` for an already-prepared project)
366
-
367
- Each run writes artefacts under `.fireforge/full-integration-artifacts/<timestamp>/` in the target project.
368
-
369
- </details>
370
-
371
- ---
372
-
373
- <details>
374
- <summary>Programmatic API</summary>
375
-
376
- > **Pre-1.0 stability notice.** FireForge is at v0.9.x. The programmatic API
377
- > exported from the main package entry point is functional and tested, but
378
- > may change between minor versions until 1.0. Pin your dependency to an
379
- > exact version if you rely on it. I would rather be honest about this then
380
- > pretend the API surface is frozen when it is not.
381
-
382
- FireForge can be used as a library in addition to the CLI:
383
-
384
- ```typescript
385
- import { loadConfig, validateConfig, applyAllComponents, loadFurnaceConfig } from 'fireforge';
386
- ```
387
-
388
- ### Exported functions
389
-
390
- | Function | Module | Purpose |
391
- | ----------------------- | ---------------- | ------------------------------------------ |
392
- | `loadConfig` | config | Load and parse `fireforge.json` |
393
- | `validateConfig` | config | Validate a config object |
394
- | `applyAllComponents` | furnace-apply | Apply all Furnace components to the engine |
395
- | `ensureFurnaceConfig` | furnace-config | Create `furnace.json` if missing |
396
- | `loadFurnaceConfig` | furnace-config | Load and parse `furnace.json` |
397
- | `loadFurnaceState` | furnace-config | Load Furnace runtime state |
398
- | `saveFurnaceState` | furnace-config | Persist Furnace runtime state |
399
- | `validateFurnaceConfig` | furnace-config | Validate a Furnace config |
400
- | `validateAllComponents` | furnace-validate | Validate all registered components |
401
- | `validateComponent` | furnace-validate | Validate a single component |
402
- | `addToken` | token-manager | Add a design token |
403
- | `getTokensCssPath` | token-manager | Get the path to the tokens CSS file |
404
- | `validateTokenAdd` | token-manager | Validate a token before adding |
405
-
406
- ### Exported types
407
-
408
- All configuration and result types are exported (`FireForgeConfig`, `FurnaceConfig`, `BuildConfig`, `ApplyResult`, `PatchInfo`, etc.). See `src/types/index.ts` for the full list.
409
-
410
- ### Error classes
411
-
412
- All error classes extend `FireForgeError`:
413
-
414
- - `CancellationError` (user-initiated cancellation)
415
- - `CommandError` (CLI command failure)
416
- - `GeneralError` (catch-all for unexpected failures)
417
- - `InvalidArgumentError` (bad input)
418
- - `ResolutionError` (dependency resolution failure)
419
-
420
- Use `ExitCode` for programmatic exit code handling.
421
-
422
- </details>
423
-
424
- ---
425
-
426
314
  ## Roadmap
427
315
 
428
- These are planned but not yet implemented. I mention them here for transparency rather then as a promise, since priorities shift and some of these may prove harder then they look.
429
-
430
- - **Docker builds.** Reproducible builds using Docker containers. The main challenge is keeping the image size reasonable given Firefox's build dependency tree.
431
- - **CI mode.** Automated setup for continuous integration pipelines.
432
- - **Update manifests.** Generate update server manifests for auto-updates.
433
- - **Nightly support.** This requires `hg clone` from mozilla-central rather then the archive download path, which is a meaningfully different code path.
316
+ Planned but not yet implemented:
434
317
 
435
- ---
318
+ - **Docker builds** Reproducible builds using Docker containers.
319
+ - **CI mode** Automated setup for continuous integration pipelines.
320
+ - **Update manifests** Generate update server manifests for auto-updates.
321
+ - **Nightly support** Requires implementing `hg clone` support via mozilla-central. Currently fireforge only downloads from the archive.
322
+ - **E2E Github Actions** Requires either a higher tier of Github offering, an external VPS or similar, or another provider entirely. In either case, full end-to-end testing is currently run solely locally.
436
323
 
437
324
  ## Licence
438
325
 
@@ -9,6 +9,7 @@
9
9
  *
10
10
  */
11
11
  import { installBrokenPipeHandler, main } from '../src/cli.js';
12
+ import { isSignalRollbackInFlight, rollbackActiveOperationsForSignal, } from '../src/core/furnace-operation.js';
12
13
  import { CommandError } from '../src/errors/base.js';
13
14
  installBrokenPipeHandler();
14
15
  process.on('unhandledRejection', (reason) => {
@@ -18,6 +19,31 @@ process.on('unhandledRejection', (reason) => {
18
19
  }
19
20
  process.exit(1);
20
21
  });
22
+ // SIGINT / SIGTERM handlers run any in-flight furnace rollback before
23
+ // terminating. The library cannot call process.exit itself (the
24
+ // process-boundary test enforces that invariant), so the bin entry point owns
25
+ // both the rollback dispatch and the exit. The handler is a no-op when no
26
+ // furnace mutation is currently registered with the lifecycle wrapper, so
27
+ // patch-only commands behave exactly as before.
28
+ function installFurnaceSignalHandler(signal, exitCode) {
29
+ process.on(signal, () => {
30
+ if (isSignalRollbackInFlight()) {
31
+ // A second Ctrl+C while we're already rolling back is a noisy "I want
32
+ // out now" — let the second signal terminate the process forcefully
33
+ // rather than queueing another rollback that will race the first.
34
+ process.exit(exitCode);
35
+ }
36
+ rollbackActiveOperationsForSignal(signal)
37
+ .catch((error) => {
38
+ console.error(`Furnace rollback after ${signal} failed:`, error instanceof Error ? error.message : error);
39
+ })
40
+ .finally(() => {
41
+ process.exit(exitCode);
42
+ });
43
+ });
44
+ }
45
+ installFurnaceSignalHandler('SIGINT', 130);
46
+ installFurnaceSignalHandler('SIGTERM', 143);
21
47
  main().catch((error) => {
22
48
  if (error instanceof CommandError) {
23
49
  process.exit(error.exitCode);
package/dist/src/cli.d.ts CHANGED
@@ -11,7 +11,7 @@ export declare function resetBrokenPipeHandlerForTests(): void;
11
11
  /**
12
12
  * Gets the project root directory.
13
13
  * Walks up from the current working directory until a fireforge.json is found.
14
- * Falls back to the current working directory when no project root is found.
14
+ * Throws when no fireforge.json is found within the walk depth limit.
15
15
  */
16
16
  export declare function getProjectRoot(): string;
17
17
  /**