@hominis/fireforge 0.10.1 → 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 +125 -238
  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
@@ -56,13 +56,35 @@ export interface ExportOptions {
56
56
  supersede?: boolean;
57
57
  /** Skip patch lint checks (downgrade errors to warnings) */
58
58
  skipLint?: boolean;
59
+ /**
60
+ * Print the computed export plan without writing anything. With
61
+ * `--supersede`, the dry-run output includes which existing patches would
62
+ * be superseded and which files caused the coverage.
63
+ */
64
+ dryRun?: boolean;
65
+ /** Place the new patch at a specific ordinal, shifting subsequent patches. */
66
+ order?: number;
67
+ /** Place the new patch immediately before the named patch. */
68
+ before?: string;
69
+ /** Place the new patch immediately after the named patch. */
70
+ after?: string;
71
+ /**
72
+ * Skip the confirmation prompt when placement forces a renumber of more
73
+ * than one existing patch. Required for non-TTY runs that use placement
74
+ * flags.
75
+ */
76
+ yes?: boolean;
77
+ /** Bypass cross-patch lint refusal for projected placement state. */
78
+ forceUnsafe?: boolean;
79
+ /** Exclude furnace-managed file paths from the export. */
80
+ excludeFurnace?: boolean;
59
81
  }
60
82
  /**
61
83
  * Options for the reset command.
62
84
  */
63
85
  export interface ResetOptions {
64
86
  /** Skip confirmation prompt */
65
- force?: boolean;
87
+ yes?: boolean;
66
88
  /** Show what would be reset without doing it */
67
89
  dryRun?: boolean;
68
90
  }
@@ -73,7 +95,7 @@ export interface DiscardOptions {
73
95
  /** Show what would be discarded without doing it */
74
96
  dryRun?: boolean;
75
97
  /** Skip confirmation prompt */
76
- force?: boolean;
98
+ yes?: boolean;
77
99
  }
78
100
  /**
79
101
  * Options for the package command.
@@ -92,6 +114,14 @@ export interface ImportOptions {
92
114
  continue?: boolean;
93
115
  /** Force import even when engine HEAD has drifted from base commit */
94
116
  force?: boolean;
117
+ /**
118
+ * Apply patches only up to and including this patch (by name or ordinal).
119
+ * Subsequent patches are left unapplied. Useful for bisection and curated
120
+ * rebuild workflows.
121
+ */
122
+ until?: string;
123
+ /** Preview which patches would be applied without modifying the engine */
124
+ dryRun?: boolean;
95
125
  }
96
126
  /**
97
127
  * Options for the re-export command.
@@ -101,10 +131,22 @@ export interface ReExportOptions {
101
131
  all?: boolean;
102
132
  /** Scan directories for new/removed files and update filesAffected */
103
133
  scan?: boolean;
134
+ /**
135
+ * Restrict the re-exported patch's filesAffected to this explicit list.
136
+ * Files currently in the patch but not in this list are dropped (shrink);
137
+ * files in this list but not currently in the patch are added. Mutually
138
+ * exclusive with `--scan` and `--all`; applies to a single target patch
139
+ * at a time.
140
+ */
141
+ files?: string[];
104
142
  /** Show what would change without writing */
105
143
  dryRun?: boolean;
106
144
  /** Skip patch lint checks (downgrade errors to warnings) */
107
145
  skipLint?: boolean;
146
+ /** Skip confirmation prompt on shrink (required for non-TTY) */
147
+ yes?: boolean;
148
+ /** Bypass cross-patch lint refusal on projected shrink state */
149
+ forceUnsafe?: boolean;
108
150
  }
109
151
  /**
110
152
  * Options for the rebase command.
@@ -119,7 +161,7 @@ export interface RebaseOptions {
119
161
  /** Maximum fuzz factor for git apply (default 3) */
120
162
  maxFuzz?: number;
121
163
  /** Skip dirty-tree confirmation prompt */
122
- force?: boolean;
164
+ yes?: boolean;
123
165
  }
124
166
  /**
125
167
  * Options for the run command.
@@ -143,6 +185,10 @@ export interface TestOptions {
143
185
  export interface FurnaceApplyOptions {
144
186
  /** Show what would be changed without writing */
145
187
  dryRun?: boolean;
188
+ /** Proceed despite baseVersion drift (stale overrides) */
189
+ force?: boolean;
190
+ /** Watch component directories and re-apply on changes */
191
+ watch?: boolean;
146
192
  }
147
193
  /**
148
194
  * Options for the furnace preview command.
@@ -157,6 +203,39 @@ export interface FurnacePreviewOptions {
157
203
  export interface FurnaceDeployOptions {
158
204
  /** Show what would be changed without writing */
159
205
  dryRun?: boolean;
206
+ /** Proceed despite baseVersion drift (stale overrides) */
207
+ force?: boolean;
208
+ /** Skip the validation step (apply only, no accessibility/compatibility checks) */
209
+ skipValidate?: boolean;
210
+ }
211
+ /**
212
+ * Options for the furnace refresh command.
213
+ */
214
+ export interface FurnaceRefreshOptions {
215
+ /** Show what would change without modifying files */
216
+ dryRun?: boolean;
217
+ /** Refresh all overrides in a single batch */
218
+ all?: boolean;
219
+ /** Conflict resolution strategy for automated use (ours = keep local, theirs = accept upstream) */
220
+ strategy?: 'ours' | 'theirs';
221
+ /** Reset the override's baseline to the current engine HEAD, skipping three-way merge */
222
+ resetBase?: boolean;
223
+ }
224
+ /**
225
+ * Options for the furnace sync command.
226
+ */
227
+ export interface FurnaceSyncOptions {
228
+ /** Show what would change without modifying files */
229
+ dryRun?: boolean;
230
+ /** Conflict resolution strategy for three-way merge (ours = keep local, theirs = accept upstream) */
231
+ strategy?: 'ours' | 'theirs';
232
+ }
233
+ /**
234
+ * Options for the furnace validate command.
235
+ */
236
+ export interface FurnaceValidateOptions {
237
+ /** Auto-fix registration issues (missing jar.mn entries, customElements.js registration) */
238
+ fix?: boolean;
160
239
  }
161
240
  /**
162
241
  * Options for the furnace override command.
@@ -172,7 +251,7 @@ export interface FurnaceOverrideOptions {
172
251
  */
173
252
  export interface FurnaceRemoveOptions {
174
253
  /** Skip confirmation prompt */
175
- force?: boolean;
254
+ yes?: boolean;
176
255
  }
177
256
  /**
178
257
  * Options for the furnace create command.
@@ -207,12 +286,43 @@ export interface RegisterOptions {
207
286
  dryRun?: boolean;
208
287
  after?: string;
209
288
  }
289
+ /**
290
+ * Options for the patch delete command.
291
+ */
292
+ export interface PatchDeleteOptions {
293
+ /** Skip confirmation prompt; required for non-TTY runs. */
294
+ yes?: boolean;
295
+ /** Print what would happen without writing anything. */
296
+ dryRun?: boolean;
297
+ /** Bypass the hard refusal when later patches depend on the target. */
298
+ forceUnsafe?: boolean;
299
+ }
300
+ /**
301
+ * Options for the patch reorder command.
302
+ */
303
+ export interface PatchReorderOptions {
304
+ to?: number;
305
+ before?: string;
306
+ after?: string;
307
+ yes?: boolean;
308
+ dryRun?: boolean;
309
+ forceUnsafe?: boolean;
310
+ }
210
311
  /**
211
312
  * Options for the status command.
212
313
  */
213
314
  export interface StatusOptions {
214
315
  raw?: boolean;
215
316
  unmanaged?: boolean;
317
+ /**
318
+ * Render a flat file→owning-patch ownership table instead of the three-
319
+ * bucket classification. Sources the path list from the manifest's
320
+ * `filesAffected` per patch and flags any path claimed by more than one
321
+ * patch as an ownership conflict.
322
+ */
323
+ ownership?: boolean;
324
+ /** Output machine-readable JSON instead of human-readable text. */
325
+ json?: boolean;
216
326
  }
217
327
  /**
218
328
  * Options for the token add command.
@@ -229,6 +339,17 @@ export interface TokenAddOptions {
229
339
  */
230
340
  export interface DoctorOptions {
231
341
  repairPatchesManifest?: boolean;
342
+ /**
343
+ * Opt-in repair path for furnace-specific checks. When true, doctor will:
344
+ * - clear stale `.fireforge/furnace-state.json` entries whose component is
345
+ * no longer in `furnace.json`,
346
+ * - run `applyAllComponents` to reconcile any engine drift,
347
+ * - clear the `pendingRepair` marker on success.
348
+ * Mirrors `repairPatchesManifest` in that the repair is only attempted when
349
+ * the caller explicitly asks for it, so a read-only `doctor` run stays cheap
350
+ * and side-effect-free.
351
+ */
352
+ repairFurnace?: boolean;
232
353
  }
233
354
  /**
234
355
  * Global CLI options available to all commands.
@@ -47,7 +47,7 @@ export interface PatchMetadata {
47
47
  description: string;
48
48
  /** ISO timestamp of when the patch was created */
49
49
  createdAt: string;
50
- /** ESR version the patch was created against (e.g., "140.0esr") */
50
+ /** ESR version the patch was created against (e.g., "146.0esr") */
51
51
  sourceEsrVersion: string;
52
52
  /** Array of file paths affected by this patch */
53
53
  filesAffected: string[];
@@ -82,6 +82,16 @@ export interface PatchLintIssue {
82
82
  file: string;
83
83
  /** Check identifier (e.g. "raw-color-value", "token-prefix-violation") */
84
84
  check: string;
85
+ /**
86
+ * Stable machine-readable identity for this finding.
87
+ *
88
+ * Use when the human-readable `message` may drift between otherwise
89
+ * equivalent runs (for example because a rule embeds line numbers,
90
+ * rename-sensitive patch filenames, or other contextual detail).
91
+ * Consumers that diff lint outputs should prefer this over `message`
92
+ * when it is present.
93
+ */
94
+ fingerprint?: string;
85
95
  /** Human-readable description of the issue */
86
96
  message: string;
87
97
  /** Severity: errors block export, warnings are advisory */
@@ -6,7 +6,7 @@ export type FirefoxProduct = 'firefox' | 'firefox-esr' | 'firefox-beta';
6
6
  * Firefox version configuration.
7
7
  */
8
8
  export interface FirefoxConfig {
9
- /** Firefox release version (e.g., "140.0esr") */
9
+ /** Firefox release version (e.g., "146.0esr") */
10
10
  version: string;
11
11
  /** Firefox product type */
12
12
  product: FirefoxProduct;
@@ -33,6 +33,8 @@ export interface OverrideComponentConfig {
33
33
  basePath: string;
34
34
  /** Firefox version this override was based on */
35
35
  baseVersion: string;
36
+ /** Git commit SHA the override was based on. Older overrides may lack this field. */
37
+ baseCommit?: string;
36
38
  }
37
39
  /**
38
40
  * Metadata for a custom component in the workspace.
@@ -61,6 +63,16 @@ export interface FurnaceConfig {
61
63
  tokenPrefix?: string;
62
64
  /** Custom properties allowed even though they don't match tokenPrefix (e.g. ["--background-color-box"]) */
63
65
  tokenAllowlist?: string[];
66
+ /**
67
+ * Override the default Fluent (.ftl) base path within the engine.
68
+ * Defaults to `toolkit/locales/en-US/toolkit/global` when not set.
69
+ */
70
+ ftlBasePath?: string;
71
+ /**
72
+ * Additional directories to scan for components (relative to engine root).
73
+ * Always includes `toolkit/content/widgets` by default.
74
+ */
75
+ scanPaths?: string[];
64
76
  /** Stock components tracked for preview */
65
77
  stock: string[];
66
78
  /** Override components */
@@ -68,6 +80,28 @@ export interface FurnaceConfig {
68
80
  /** Custom components */
69
81
  custom: Record<string, CustomComponentConfig>;
70
82
  }
83
+ /**
84
+ * Operations that can leave a pending-repair marker when they fail to roll
85
+ * back cleanly. The marker is consumed by `fireforge doctor`, which either
86
+ * re-runs apply for engine-side failures or validates the current authoring
87
+ * state before clearing authoring markers. The string is surfaced in doctor's
88
+ * failure message verbatim, so new entries should be self-explanatory.
89
+ */
90
+ export type FurnacePendingRepairOperation = 'preview-teardown' | 'apply-rollback' | 'deploy-rollback' | 'remove-rollback' | 'create-rollback' | 'override-rollback' | 'scan-rollback' | 'rename-rollback' | 'refresh-rollback';
91
+ /**
92
+ * Marker persisted into `.fireforge/furnace-state.json` when a furnace
93
+ * mutation failed to roll back cleanly. Its presence tells the next
94
+ * `fireforge doctor` run that the engine and workspace may have drifted
95
+ * out-of-band from what the state file records.
96
+ */
97
+ export interface FurnacePendingRepair {
98
+ /** The operation that failed to clean up; used by doctor to route the fix. */
99
+ operation: FurnacePendingRepairOperation;
100
+ /** ISO timestamp of when the repair marker was written. */
101
+ timestamp: string;
102
+ /** Human-readable summary of the failure; shown by doctor. */
103
+ reason: string;
104
+ }
71
105
  /**
72
106
  * State tracking for apply operations (stored in .fireforge/furnace-state.json).
73
107
  */
@@ -76,6 +110,20 @@ export interface FurnaceState {
76
110
  lastApply?: string;
77
111
  /** Checksums of component files at last apply, keyed by relative path */
78
112
  appliedChecksums?: Record<string, string>;
113
+ /**
114
+ * SHA-256 hashes of engine-side files written during the last apply, keyed
115
+ * by engine-relative path. Used by drift detection to avoid byte-comparing
116
+ * engine files against workspace sources when the cached hash still matches
117
+ * the on-disk content. Populated alongside `appliedChecksums` on successful
118
+ * apply.
119
+ */
120
+ engineChecksums?: Record<string, string>;
121
+ /**
122
+ * Set when a furnace mutation failed to roll back cleanly and the engine
123
+ * may be in an inconsistent state. Cleared by `fireforge doctor` after
124
+ * reconciliation. See {@link FurnacePendingRepair}.
125
+ */
126
+ pendingRepair?: FurnacePendingRepair;
79
127
  }
80
128
  /**
81
129
  * A registration-step error captured while applying a component.
@@ -107,13 +155,19 @@ export interface ApplyResult {
107
155
  name: string;
108
156
  error: string;
109
157
  }>;
158
+ /**
159
+ * Set to true when the rollback journal was restored after a partial failure.
160
+ * When true, entries in `applied` reflect what was attempted, not what
161
+ * persisted — the engine has been restored to its pre-apply state.
162
+ */
163
+ rolledBack?: boolean;
110
164
  }
111
165
  /**
112
166
  * An action that would be performed during a dry-run deploy.
113
167
  */
114
168
  export interface DryRunAction {
115
169
  component: string;
116
- action: 'copy' | 'register-ce' | 'register-jar' | 'copy-ftl';
170
+ action: 'copy' | 'register-ce' | 'register-jar' | 'copy-ftl' | 'undeploy-remove' | 'undeploy-restore' | 'unregister-ce' | 'unregister-jar';
117
171
  source?: string;
118
172
  target?: string;
119
173
  description: string;
@@ -70,3 +70,15 @@ export declare function writeFileAtomic(path: string, content: string | Buffer):
70
70
  * @param dest - Destination directory path
71
71
  */
72
72
  export declare function copyDir(src: string, dest: string): Promise<void>;
73
+ /**
74
+ * Checks available disk space at a path and warns via the provided
75
+ * callback when it falls below `minBytes`.
76
+ *
77
+ * @param path - Directory to check (must exist)
78
+ * @param minBytes - Minimum free bytes before emitting a warning
79
+ * @param onLowSpace - Callback invoked with a human-readable message
80
+ * when available space is below the threshold
81
+ * @returns The available bytes, or `undefined` when the check could not
82
+ * be performed (unsupported platform, permission error, etc.)
83
+ */
84
+ export declare function checkDiskSpace(path: string, minBytes: number, onLowSpace: (message: string) => void): Promise<number | undefined>;
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { randomUUID } from 'node:crypto';
3
- import { access, copyFile as fsCopyFile, mkdir, open, readdir, readFile, rename, rm, } from 'node:fs/promises';
3
+ import { access, copyFile as fsCopyFile, mkdir, open, readdir, readFile, rename, rm, statfs, } from 'node:fs/promises';
4
4
  import { dirname, join } from 'node:path';
5
5
  const RETRIABLE_REMOVE_ERRORS = new Set(['ENOTEMPTY', 'EBUSY', 'EPERM']);
6
6
  function sleep(ms) {
@@ -176,4 +176,33 @@ function createAtomicTempPath(path) {
176
176
  const filename = path.slice(directory.length + 1);
177
177
  return join(directory, `.${filename}.fireforge-tmp-${process.pid}-${randomUUID()}`);
178
178
  }
179
+ /**
180
+ * Checks available disk space at a path and warns via the provided
181
+ * callback when it falls below `minBytes`.
182
+ *
183
+ * @param path - Directory to check (must exist)
184
+ * @param minBytes - Minimum free bytes before emitting a warning
185
+ * @param onLowSpace - Callback invoked with a human-readable message
186
+ * when available space is below the threshold
187
+ * @returns The available bytes, or `undefined` when the check could not
188
+ * be performed (unsupported platform, permission error, etc.)
189
+ */
190
+ export async function checkDiskSpace(path, minBytes, onLowSpace) {
191
+ try {
192
+ const stats = await statfs(path);
193
+ const availableBytes = stats.bfree * stats.bsize;
194
+ if (availableBytes < minBytes) {
195
+ const availableGB = (availableBytes / (1024 * 1024 * 1024)).toFixed(1);
196
+ const requiredGB = (minBytes / (1024 * 1024 * 1024)).toFixed(1);
197
+ onLowSpace(`Low disk space: ${availableGB} GB available, ${requiredGB} GB recommended. ` +
198
+ 'The operation may fail if the disk fills up.');
199
+ }
200
+ return availableBytes;
201
+ }
202
+ catch {
203
+ // statfs may not be available on all platforms or the path may
204
+ // not exist yet — silently degrade rather than blocking the operation.
205
+ return undefined;
206
+ }
207
+ }
179
208
  //# sourceMappingURL=fs.js.map
@@ -9,8 +9,13 @@ interface PackageMetadata {
9
9
  * Works from both the source tree (`src/utils/`) and the compiled
10
10
  * tree (`dist/src/utils/`) by looking for a `package.json` that exposes
11
11
  * the `fireforge` CLI entrypoint, regardless of the npm package scope.
12
+ *
13
+ * The result is cached after the first call since it is deterministic
14
+ * within a process.
12
15
  */
13
16
  export declare function getPackageRoot(): string;
17
+ /** Clears the cached package root for testing. */
18
+ export declare function resetPackageRootCacheForTests(): void;
14
19
  /** @internal */
15
20
  export declare function isFireForgePackageMetadata(pkg: PackageMetadata): boolean;
16
21
  /** Reads the current package version from the repository root package manifest. */
@@ -17,20 +17,28 @@ function readPackageMetadata(filePath) {
17
17
  const raw = readFileSync(filePath, 'utf-8');
18
18
  return validatePackageMetadata(JSON.parse(raw), filePath);
19
19
  }
20
+ let cachedPackageRoot;
20
21
  /**
21
22
  * Finds the fireforge package root by walking up from the current module.
22
23
  *
23
24
  * Works from both the source tree (`src/utils/`) and the compiled
24
25
  * tree (`dist/src/utils/`) by looking for a `package.json` that exposes
25
26
  * the `fireforge` CLI entrypoint, regardless of the npm package scope.
27
+ *
28
+ * The result is cached after the first call since it is deterministic
29
+ * within a process.
26
30
  */
27
31
  export function getPackageRoot() {
32
+ if (cachedPackageRoot !== undefined) {
33
+ return cachedPackageRoot;
34
+ }
28
35
  let current = dirname(fileURLToPath(import.meta.url));
29
36
  for (;;) {
30
37
  try {
31
38
  const packagePath = join(current, 'package.json');
32
39
  const pkg = readPackageMetadata(packagePath);
33
40
  if (isFireForgePackageMetadata(pkg)) {
41
+ cachedPackageRoot = current;
34
42
  return current;
35
43
  }
36
44
  }
@@ -45,6 +53,10 @@ export function getPackageRoot() {
45
53
  current = parent;
46
54
  }
47
55
  }
56
+ /** Clears the cached package root for testing. */
57
+ export function resetPackageRootCacheForTests() {
58
+ cachedPackageRoot = undefined;
59
+ }
48
60
  /** @internal */
49
61
  export function isFireForgePackageMetadata(pkg) {
50
62
  if (typeof pkg.bin !== 'object' || pkg.bin === null || Array.isArray(pkg.bin)) {
@@ -29,6 +29,11 @@ function createStreamCollector(mirror) {
29
29
  getText: () => chunks.join(''),
30
30
  };
31
31
  }
32
+ function buildSignalFromTimeout(timeout) {
33
+ if (timeout === undefined)
34
+ return undefined;
35
+ return AbortSignal.timeout(timeout);
36
+ }
32
37
  function exitCodeFromClose(code, signal) {
33
38
  if (code !== null) {
34
39
  return code;
@@ -54,7 +59,7 @@ export async function exec(command, args, options = {}) {
54
59
  cwd: options.cwd,
55
60
  env: { ...process.env, ...options.env },
56
61
  stdio: ['ignore', 'pipe', 'pipe'],
57
- timeout: options.timeout,
62
+ signal: buildSignalFromTimeout(options.timeout),
58
63
  });
59
64
  const out = createStreamCollector();
60
65
  const err = createStreamCollector();
@@ -85,7 +90,7 @@ export async function execStream(command, args, options = {}) {
85
90
  cwd: options.cwd,
86
91
  env: { ...process.env, ...options.env },
87
92
  stdio: ['ignore', 'pipe', 'pipe'],
88
- timeout: options.timeout,
93
+ signal: buildSignalFromTimeout(options.timeout),
89
94
  });
90
95
  child.stdout.on('data', (data) => {
91
96
  options.onStdout?.(data.toString());
@@ -114,7 +119,7 @@ export async function execInherit(command, args, options = {}) {
114
119
  cwd: options.cwd,
115
120
  env: { ...process.env, ...options.env },
116
121
  stdio: 'inherit',
117
- timeout: options.timeout,
122
+ signal: buildSignalFromTimeout(options.timeout),
118
123
  });
119
124
  child.on('error', (error) => {
120
125
  reject(error);
@@ -138,7 +143,7 @@ export async function execInheritCapture(command, args, options = {}) {
138
143
  cwd: options.cwd,
139
144
  env: { ...process.env, ...options.env },
140
145
  stdio: ['inherit', 'pipe', 'pipe'],
141
- timeout: options.timeout,
146
+ signal: buildSignalFromTimeout(options.timeout),
142
147
  });
143
148
  const out = createStreamCollector(process.stdout);
144
149
  const err = createStreamCollector(process.stderr);
@@ -16,6 +16,24 @@ export declare function isNumber(value: unknown): value is number;
16
16
  * @returns True if value is a positive integer
17
17
  */
18
18
  export declare function isPositiveInteger(value: unknown): value is number;
19
+ /**
20
+ * Parses a CLI flag value as a positive integer. Throws
21
+ * {@link InvalidArgumentError} on NaN, non-integer, or non-positive input.
22
+ *
23
+ * Intended for use inside Commander `argParser` bodies where the raw
24
+ * input arrives as a string. Without this wrapper the default pattern
25
+ * (`parseInt(v, 10)`) silently hands NaN to downstream planners, which
26
+ * then embed it into filenames / orders instead of failing fast.
27
+ *
28
+ * Rejects leading-zero forms ("01"), decimals ("1.5"), whitespace, and
29
+ * non-numeric garbage via a strict regex — we only accept the canonical
30
+ * representation so there is no ambiguity between what the user typed
31
+ * and what the value becomes on disk.
32
+ *
33
+ * @param flagName - Flag name to include in the error (e.g. `--order`)
34
+ * @param rawValue - Raw string value from Commander
35
+ */
36
+ export declare function parsePositiveIntegerFlag(flagName: string, rawValue: string): number;
19
37
  /**
20
38
  * Checks whether a value is a boolean.
21
39
  * @param value - Value to check
@@ -48,7 +66,7 @@ export declare function assertString(value: unknown, name: string): asserts valu
48
66
  export declare function assertObject(value: unknown, name: string): asserts value is Record<string, unknown>;
49
67
  /**
50
68
  * Validates a Firefox version string.
51
- * Accepts formats like "146.0", "146.0.1", "140.0esr", "147.0b1"
69
+ * Accepts formats like "146.0", "146.0.1", "146.0esr", "147.0b1"
52
70
  */
53
71
  export declare function isValidFirefoxVersion(version: string): boolean;
54
72
  /**
@@ -89,7 +107,7 @@ export declare function inferProductFromVersion(version: string): 'firefox' | 'f
89
107
  * Validates that a Firefox product and version are compatible.
90
108
  *
91
109
  * Rules:
92
- * - `firefox-esr` requires an ESR version (e.g. "140.0esr", "128.0.1esr").
110
+ * - `firefox-esr` requires an ESR version (e.g. "146.0esr", "128.0.1esr").
93
111
  * - `firefox-beta` requires a beta version (e.g. "147.0b1").
94
112
  * - `firefox` (stable) rejects both ESR and beta version strings.
95
113
  *
@@ -28,6 +28,29 @@ export function isNumber(value) {
28
28
  export function isPositiveInteger(value) {
29
29
  return isNumber(value) && Number.isInteger(value) && value > 0;
30
30
  }
31
+ /**
32
+ * Parses a CLI flag value as a positive integer. Throws
33
+ * {@link InvalidArgumentError} on NaN, non-integer, or non-positive input.
34
+ *
35
+ * Intended for use inside Commander `argParser` bodies where the raw
36
+ * input arrives as a string. Without this wrapper the default pattern
37
+ * (`parseInt(v, 10)`) silently hands NaN to downstream planners, which
38
+ * then embed it into filenames / orders instead of failing fast.
39
+ *
40
+ * Rejects leading-zero forms ("01"), decimals ("1.5"), whitespace, and
41
+ * non-numeric garbage via a strict regex — we only accept the canonical
42
+ * representation so there is no ambiguity between what the user typed
43
+ * and what the value becomes on disk.
44
+ *
45
+ * @param flagName - Flag name to include in the error (e.g. `--order`)
46
+ * @param rawValue - Raw string value from Commander
47
+ */
48
+ export function parsePositiveIntegerFlag(flagName, rawValue) {
49
+ if (!/^[1-9]\d*$/.test(rawValue)) {
50
+ throw new InvalidArgumentError(`${flagName} must be a positive integer, got "${rawValue}".`, flagName);
51
+ }
52
+ return Number.parseInt(rawValue, 10);
53
+ }
31
54
  /**
32
55
  * Checks whether a value is a boolean.
33
56
  * @param value - Value to check
@@ -74,10 +97,10 @@ export function assertObject(value, name) {
74
97
  }
75
98
  /**
76
99
  * Validates a Firefox version string.
77
- * Accepts formats like "146.0", "146.0.1", "140.0esr", "147.0b1"
100
+ * Accepts formats like "146.0", "146.0.1", "146.0esr", "147.0b1"
78
101
  */
79
102
  export function isValidFirefoxVersion(version) {
80
- // Stable/ESR: 146.0, 146.0.1, 140.0esr, 128.0.1esr
103
+ // Stable/ESR: 146.0, 146.0.1, 146.0esr, 128.0.1esr
81
104
  // Beta: 147.0b1, 147.0b2
82
105
  return /^[1-9]\d{0,2}\.\d+(?:b[1-9]\d*|\.\d+(?:esr)?|esr)?$/.test(version);
83
106
  }
@@ -137,7 +160,7 @@ export function inferProductFromVersion(version) {
137
160
  * Validates that a Firefox product and version are compatible.
138
161
  *
139
162
  * Rules:
140
- * - `firefox-esr` requires an ESR version (e.g. "140.0esr", "128.0.1esr").
163
+ * - `firefox-esr` requires an ESR version (e.g. "146.0esr", "128.0.1esr").
141
164
  * - `firefox-beta` requires a beta version (e.g. "147.0b1").
142
165
  * - `firefox` (stable) rejects both ESR and beta version strings.
143
166
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.10.1",
3
+ "version": "0.11.0",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",