@hominis/fireforge 0.17.0 → 0.18.1

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 (75) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +60 -33
  3. package/dist/src/commands/build.js +18 -4
  4. package/dist/src/commands/doctor-furnace-manifest-sync.d.ts +18 -0
  5. package/dist/src/commands/doctor-furnace-manifest-sync.js +159 -0
  6. package/dist/src/commands/doctor-furnace.js +2 -0
  7. package/dist/src/commands/doctor-working-tree.d.ts +29 -0
  8. package/dist/src/commands/doctor-working-tree.js +93 -0
  9. package/dist/src/commands/doctor.js +22 -12
  10. package/dist/src/commands/export-all.js +74 -4
  11. package/dist/src/commands/export-shared.d.ts +7 -1
  12. package/dist/src/commands/export-shared.js +21 -3
  13. package/dist/src/commands/furnace/create-xpcshell.js +4 -2
  14. package/dist/src/commands/furnace/override.js +23 -13
  15. package/dist/src/commands/furnace/preview.js +38 -0
  16. package/dist/src/commands/furnace/remove.js +75 -1
  17. package/dist/src/commands/furnace/rename-xpcshell.d.ts +35 -0
  18. package/dist/src/commands/furnace/rename-xpcshell.js +97 -0
  19. package/dist/src/commands/furnace/rename.js +32 -4
  20. package/dist/src/commands/lint.js +19 -6
  21. package/dist/src/commands/patch/delete.js +4 -1
  22. package/dist/src/commands/patch/reorder.js +4 -1
  23. package/dist/src/commands/re-export-files.js +3 -1
  24. package/dist/src/commands/re-export.js +4 -1
  25. package/dist/src/commands/rebase/index.js +19 -1
  26. package/dist/src/commands/register.js +11 -0
  27. package/dist/src/commands/status.js +44 -5
  28. package/dist/src/commands/test.js +68 -16
  29. package/dist/src/commands/token-coverage.js +10 -3
  30. package/dist/src/commands/verify.js +81 -6
  31. package/dist/src/commands/watch.js +43 -7
  32. package/dist/src/commands/wire.js +16 -0
  33. package/dist/src/core/browser-wire.js +21 -4
  34. package/dist/src/core/build-audit.js +10 -0
  35. package/dist/src/core/furnace-constants.d.ts +14 -0
  36. package/dist/src/core/furnace-constants.js +16 -0
  37. package/dist/src/core/furnace-validate.js +67 -1
  38. package/dist/src/core/git-base.d.ts +27 -2
  39. package/dist/src/core/git-base.js +41 -3
  40. package/dist/src/core/git-diff.js +21 -2
  41. package/dist/src/core/git.js +53 -14
  42. package/dist/src/core/mach.d.ts +26 -8
  43. package/dist/src/core/mach.js +24 -8
  44. package/dist/src/core/manifest-rules.js +10 -1
  45. package/dist/src/core/manifest-tokenizers.d.ts +6 -0
  46. package/dist/src/core/manifest-tokenizers.js +28 -0
  47. package/dist/src/core/marionette-preflight.d.ts +16 -0
  48. package/dist/src/core/marionette-preflight.js +19 -0
  49. package/dist/src/core/patch-lint-diff-tag.d.ts +20 -0
  50. package/dist/src/core/patch-lint-diff-tag.js +25 -0
  51. package/dist/src/core/patch-lint.d.ts +47 -2
  52. package/dist/src/core/patch-lint.js +94 -18
  53. package/dist/src/core/patch-manifest-consistency.js +15 -2
  54. package/dist/src/core/patch-manifest-io.js +10 -0
  55. package/dist/src/core/patch-manifest-resolve.d.ts +20 -1
  56. package/dist/src/core/patch-manifest-resolve.js +29 -2
  57. package/dist/src/core/patch-manifest-validate.js +25 -1
  58. package/dist/src/core/patch-registration-refs.d.ts +42 -0
  59. package/dist/src/core/patch-registration-refs.js +117 -0
  60. package/dist/src/core/token-coverage.js +24 -0
  61. package/dist/src/core/wire-destroy.d.ts +7 -3
  62. package/dist/src/core/wire-destroy.js +11 -6
  63. package/dist/src/core/wire-init.d.ts +9 -3
  64. package/dist/src/core/wire-init.js +18 -6
  65. package/dist/src/core/wire-subscript.d.ts +7 -3
  66. package/dist/src/core/wire-subscript.js +11 -4
  67. package/dist/src/core/xpcshell-appdir.d.ts +19 -5
  68. package/dist/src/core/xpcshell-appdir.js +46 -20
  69. package/dist/src/errors/git.d.ts +20 -0
  70. package/dist/src/errors/git.js +39 -0
  71. package/dist/src/types/commands/patches.d.ts +23 -0
  72. package/dist/src/types/furnace.d.ts +9 -0
  73. package/dist/src/utils/parse.d.ts +7 -0
  74. package/dist/src/utils/parse.js +15 -0
  75. package/package.json +1 -1
@@ -5,12 +5,18 @@
5
5
  * AST-based implementation: finds onLoad() method body, locates existing
6
6
  * fireforge init blocks (TryStatements containing typeof guards), and inserts
7
7
  * after the correct position.
8
+ *
9
+ * `marker` is prepended (uppercased) to the generated comment line so the
10
+ * emitted block carries the patch-lint `// <MARKER>:` signature that
11
+ * `lintModificationComments` looks for. Otherwise the first export after
12
+ * `wire` trips `missing-modification-comment` on wire-generated edits —
13
+ * exactly the eval 1 Finding #9 regression.
8
14
  */
9
- export declare function addInitAST(content: string, expression: string, after?: string): string;
15
+ export declare function addInitAST(content: string, expression: string, after?: string, marker?: string): string;
10
16
  /**
11
17
  * Legacy regex/line-based implementation preserved as fallback.
12
18
  */
13
- export declare function legacyAddInit(content: string, expression: string, after?: string): string;
19
+ export declare function legacyAddInit(content: string, expression: string, after?: string, marker?: string): string;
14
20
  /**
15
21
  * Adds an init expression as the first statement(s) in gBrowserInit.onLoad()
16
22
  * in browser-init.js, after any previously-wired fireforge init blocks.
@@ -20,4 +26,4 @@ export declare function legacyAddInit(content: string, expression: string, after
20
26
  * @param after - Optional name to insert after (e.g., "MyComponent" to insert after its block)
21
27
  * @returns true if added, false if already present
22
28
  */
23
- export declare function addInitToBrowserInit(engineDir: string, expression: string, after?: string): Promise<boolean>;
29
+ export declare function addInitToBrowserInit(engineDir: string, expression: string, after?: string, marker?: string): Promise<boolean>;
@@ -12,12 +12,24 @@ import { detectIndent, getNodeSource, parseScript } from './ast-utils.js';
12
12
  import { withParserFallback } from './parser-fallback.js';
13
13
  import { assertBraceBalancePreserved, coerceToCall, extractNameFromExpression, findInsertionAfterFireforgeBlocks, findMethodBody, findMethodBraceIndex, validateWireName, walkToTryBlockEnd, } from './wire-utils.js';
14
14
  const BROWSER_INIT_JS = 'browser/base/content/browser-init.js';
15
+ /**
16
+ * Default patch-lint marker used when a caller does not supply a
17
+ * project-specific one. Kept as a constant so test fixtures and
18
+ * fallback code paths agree on the shape.
19
+ */
20
+ const DEFAULT_MARKER = 'FIREFORGE:';
15
21
  /**
16
22
  * AST-based implementation: finds onLoad() method body, locates existing
17
23
  * fireforge init blocks (TryStatements containing typeof guards), and inserts
18
24
  * after the correct position.
25
+ *
26
+ * `marker` is prepended (uppercased) to the generated comment line so the
27
+ * emitted block carries the patch-lint `// <MARKER>:` signature that
28
+ * `lintModificationComments` looks for. Otherwise the first export after
29
+ * `wire` trips `missing-modification-comment` on wire-generated edits —
30
+ * exactly the eval 1 Finding #9 regression.
19
31
  */
20
- export function addInitAST(content, expression, after) {
32
+ export function addInitAST(content, expression, after, marker = DEFAULT_MARKER) {
21
33
  const name = extractNameFromExpression(expression);
22
34
  // `validateWireName` accepts both `Foo.bar` and `Foo.bar()` shapes. The
23
35
  // template below interpolates the value verbatim, so a bare property
@@ -99,7 +111,7 @@ export function addInitAST(content, expression, after) {
99
111
  }
100
112
  }
101
113
  const block = [
102
- `${indent}// ${name} init — must be first, before Firefox subsystem`,
114
+ `${indent}// ${marker} wire-init ${name} — must be first, before Firefox subsystem`,
103
115
  `${indent}// inits that reference native UI elements we hide.`,
104
116
  `${indent}try {`,
105
117
  `${indent} if (typeof ${name} !== "undefined") {`,
@@ -115,7 +127,7 @@ export function addInitAST(content, expression, after) {
115
127
  /**
116
128
  * Legacy regex/line-based implementation preserved as fallback.
117
129
  */
118
- export function legacyAddInit(content, expression, after) {
130
+ export function legacyAddInit(content, expression, after, marker = DEFAULT_MARKER) {
119
131
  const name = extractNameFromExpression(expression);
120
132
  // See `addInitAST` for the rationale — the AST and fallback paths must
121
133
  // agree on whether the emitted block is a function call, otherwise
@@ -174,7 +186,7 @@ export function legacyAddInit(content, expression, after) {
174
186
  const inner = baseIndent + ' ';
175
187
  const inner2 = inner + ' ';
176
188
  const block = [
177
- `${baseIndent}// ${name} init — must be first, before Firefox subsystem`,
189
+ `${baseIndent}// ${marker} wire-init ${name} — must be first, before Firefox subsystem`,
178
190
  `${baseIndent}// inits that reference native UI elements we hide.`,
179
191
  `${baseIndent}try {`,
180
192
  `${inner}if (typeof ${name} !== "undefined") {`,
@@ -196,7 +208,7 @@ export function legacyAddInit(content, expression, after) {
196
208
  * @param after - Optional name to insert after (e.g., "MyComponent" to insert after its block)
197
209
  * @returns true if added, false if already present
198
210
  */
199
- export async function addInitToBrowserInit(engineDir, expression, after) {
211
+ export async function addInitToBrowserInit(engineDir, expression, after, marker = DEFAULT_MARKER) {
200
212
  validateWireName(expression, 'init expression');
201
213
  const filePath = join(engineDir, BROWSER_INIT_JS);
202
214
  if (!(await pathExists(filePath))) {
@@ -212,7 +224,7 @@ export async function addInitToBrowserInit(engineDir, expression, after) {
212
224
  if (initPattern.test(content)) {
213
225
  return false;
214
226
  }
215
- const { value, usedFallback } = withParserFallback(() => addInitAST(content, expression, after), () => legacyAddInit(content, expression, after), BROWSER_INIT_JS);
227
+ const { value, usedFallback } = withParserFallback(() => addInitAST(content, expression, after, marker), () => legacyAddInit(content, expression, after, marker), BROWSER_INIT_JS);
216
228
  if (usedFallback) {
217
229
  assertBraceBalancePreserved(content, value, BROWSER_INIT_JS);
218
230
  }
@@ -4,12 +4,16 @@
4
4
  /**
5
5
  * AST-based implementation: finds the last try/catch containing
6
6
  * `loadSubScript` and inserts a new try/catch block after it.
7
+ *
8
+ * The inserted block carries a `// <MARKER>: wire-subscript ...` comment
9
+ * so the emitted edit satisfies `lintModificationComments` (eval 1
10
+ * Finding #9).
7
11
  */
8
- export declare function addSubscriptAST(content: string, name: string): string;
12
+ export declare function addSubscriptAST(content: string, name: string, marker?: string): string;
9
13
  /**
10
14
  * Legacy regex/line-based implementation preserved as fallback.
11
15
  */
12
- export declare function legacyAddSubscript(content: string, name: string): string;
16
+ export declare function legacyAddSubscript(content: string, name: string, marker?: string): string;
13
17
  /**
14
18
  * Adds a loadSubScript entry to browser-main.js with try/catch error handling.
15
19
  *
@@ -17,4 +21,4 @@ export declare function legacyAddSubscript(content: string, name: string): strin
17
21
  * @param name - Subscript name (without .js extension)
18
22
  * @returns true if added, false if already present
19
23
  */
20
- export declare function addSubscriptToBrowserMain(engineDir: string, name: string): Promise<boolean>;
24
+ export declare function addSubscriptToBrowserMain(engineDir: string, name: string, marker?: string): Promise<boolean>;
@@ -11,11 +11,16 @@ import { detectIndent, getNodeSource, parseScript, walkAST, } from './ast-utils.
11
11
  import { withParserFallback } from './parser-fallback.js';
12
12
  import { assertBraceBalancePreserved, findNearestTryLine, validateWireName, walkToTryBlockEnd, } from './wire-utils.js';
13
13
  const BROWSER_MAIN_JS = 'browser/base/content/browser-main.js';
14
+ const DEFAULT_MARKER = 'FIREFORGE:';
14
15
  /**
15
16
  * AST-based implementation: finds the last try/catch containing
16
17
  * `loadSubScript` and inserts a new try/catch block after it.
18
+ *
19
+ * The inserted block carries a `// <MARKER>: wire-subscript ...` comment
20
+ * so the emitted edit satisfies `lintModificationComments` (eval 1
21
+ * Finding #9).
17
22
  */
18
- export function addSubscriptAST(content, name) {
23
+ export function addSubscriptAST(content, name, marker = DEFAULT_MARKER) {
19
24
  const ast = parseScript(content);
20
25
  const ms = new MagicString(content);
21
26
  // Collect all TryStatements containing loadSubScript
@@ -58,6 +63,7 @@ export function addSubscriptAST(content, name) {
58
63
  indent = detectIndent(content, lastBrace);
59
64
  }
60
65
  const block = [
66
+ `${indent}// ${marker} wire-subscript ${name}`,
61
67
  `${indent}try {`,
62
68
  `${indent} Services.scriptloader.loadSubScript("chrome://browser/content/${name}.js", this);`,
63
69
  `${indent}} catch (e) {`,
@@ -70,7 +76,7 @@ export function addSubscriptAST(content, name) {
70
76
  /**
71
77
  * Legacy regex/line-based implementation preserved as fallback.
72
78
  */
73
- export function legacyAddSubscript(content, name) {
79
+ export function legacyAddSubscript(content, name, marker = DEFAULT_MARKER) {
74
80
  const lines = content.split('\n');
75
81
  let lastSubScriptLine = -1;
76
82
  for (let i = 0; i < lines.length; i++) {
@@ -103,6 +109,7 @@ export function legacyAddSubscript(content, name) {
103
109
  const ind = refLine?.match(/^(\s*)/)?.[1] ?? ' ';
104
110
  const inner = ind + ' ';
105
111
  const block = [
112
+ `${ind}// ${marker} wire-subscript ${name}`,
106
113
  `${ind}try {`,
107
114
  `${inner}Services.scriptloader.loadSubScript("chrome://browser/content/${name}.js", this);`,
108
115
  `${ind}} catch (e) {`,
@@ -119,7 +126,7 @@ export function legacyAddSubscript(content, name) {
119
126
  * @param name - Subscript name (without .js extension)
120
127
  * @returns true if added, false if already present
121
128
  */
122
- export async function addSubscriptToBrowserMain(engineDir, name) {
129
+ export async function addSubscriptToBrowserMain(engineDir, name, marker = DEFAULT_MARKER) {
123
130
  validateWireName(name, 'subscript name');
124
131
  const filePath = join(engineDir, BROWSER_MAIN_JS);
125
132
  if (!(await pathExists(filePath))) {
@@ -130,7 +137,7 @@ export async function addSubscriptToBrowserMain(engineDir, name) {
130
137
  if (content.includes(`content/${name}.js"`)) {
131
138
  return false;
132
139
  }
133
- const { value, usedFallback } = withParserFallback(() => addSubscriptAST(content, name), () => legacyAddSubscript(content, name), BROWSER_MAIN_JS);
140
+ const { value, usedFallback } = withParserFallback(() => addSubscriptAST(content, name, marker), () => legacyAddSubscript(content, name, marker), BROWSER_MAIN_JS);
134
141
  if (usedFallback) {
135
142
  assertBraceBalancePreserved(content, value, BROWSER_MAIN_JS);
136
143
  }
@@ -96,11 +96,25 @@ export declare function readMozinfoAppname(objDirPath: string): Promise<string>;
96
96
  * (which fails with a different error than the original `firefox-appdir`
97
97
  * symptom and confuses triage).
98
98
  *
99
- * Probe order matches the on-disk layouts FireForge supports today:
100
- * 1. `<objDir>/dist/bin/<value>` — Linux primary, also macOS via the
101
- * `dist/bin -> dist/<App>.app/Contents/MacOS/` symlink.
102
- * 2. `<objDir>/dist/<bundle>.app/Contents/Resources/<value>` — macOS
103
- * packaged layout, where `dist/bin/` may not exist as a directory.
99
+ * Probe order differs by host platform:
100
+ *
101
+ * - **macOS (`darwin`)**: prefer `<objDir>/dist/<App>.app/Contents/Resources/
102
+ * <value>` FIRST, then fall back to `<objDir>/dist/bin/<value>`.
103
+ * 2026-04-24 eval Finding 8: on macOS `dist/bin` is symlinked to
104
+ * `dist/<App>.app/Contents/MacOS/` (the *binaries* directory), so
105
+ * `dist/bin/browser` actually resolves to `<App>.app/Contents/MacOS/
106
+ * browser/`. That is NOT where `resource:///modules/` is rooted — on
107
+ * macOS, `-a` for xpcshell must point at the `.app/Contents/Resources/
108
+ * <value>` subtree where modules / chrome.manifest live. Returning
109
+ * `dist/bin/browser` caused the injected `--app-path` to look
110
+ * successful (the info log showed it) but pointed at a directory
111
+ * without the modules tree, so every `resource:///modules/…` import
112
+ * still threw.
113
+ * - **non-macOS**: keep the historical order — `dist/bin/<value>` first,
114
+ * `.app/Contents/Resources/<value>` as fallback.
115
+ *
116
+ * On both platforms the final `.app` fallback iterates every `*.app`
117
+ * entry because a rebranded fork may pick an arbitrary app name.
104
118
  */
105
119
  export declare function resolveAbsoluteAppPath(objDirAbs: string, relativeAppdir: string): Promise<string | null>;
106
120
  /**
@@ -164,34 +164,60 @@ export async function readMozinfoAppname(objDirPath) {
164
164
  * (which fails with a different error than the original `firefox-appdir`
165
165
  * symptom and confuses triage).
166
166
  *
167
- * Probe order matches the on-disk layouts FireForge supports today:
168
- * 1. `<objDir>/dist/bin/<value>` — Linux primary, also macOS via the
169
- * `dist/bin -> dist/<App>.app/Contents/MacOS/` symlink.
170
- * 2. `<objDir>/dist/<bundle>.app/Contents/Resources/<value>` — macOS
171
- * packaged layout, where `dist/bin/` may not exist as a directory.
167
+ * Probe order differs by host platform:
168
+ *
169
+ * - **macOS (`darwin`)**: prefer `<objDir>/dist/<App>.app/Contents/Resources/
170
+ * <value>` FIRST, then fall back to `<objDir>/dist/bin/<value>`.
171
+ * 2026-04-24 eval Finding 8: on macOS `dist/bin` is symlinked to
172
+ * `dist/<App>.app/Contents/MacOS/` (the *binaries* directory), so
173
+ * `dist/bin/browser` actually resolves to `<App>.app/Contents/MacOS/
174
+ * browser/`. That is NOT where `resource:///modules/` is rooted — on
175
+ * macOS, `-a` for xpcshell must point at the `.app/Contents/Resources/
176
+ * <value>` subtree where modules / chrome.manifest live. Returning
177
+ * `dist/bin/browser` caused the injected `--app-path` to look
178
+ * successful (the info log showed it) but pointed at a directory
179
+ * without the modules tree, so every `resource:///modules/…` import
180
+ * still threw.
181
+ * - **non-macOS**: keep the historical order — `dist/bin/<value>` first,
182
+ * `.app/Contents/Resources/<value>` as fallback.
183
+ *
184
+ * On both platforms the final `.app` fallback iterates every `*.app`
185
+ * entry because a rebranded fork may pick an arbitrary app name.
172
186
  */
173
187
  export async function resolveAbsoluteAppPath(objDirAbs, relativeAppdir) {
174
188
  const distBinCandidate = join(objDirAbs, 'dist', 'bin', relativeAppdir);
175
- if (await pathExists(distBinCandidate))
176
- return distBinCandidate;
177
189
  const distDir = join(objDirAbs, 'dist');
178
- if (!(await pathExists(distDir)))
190
+ const isMacos = process.platform === 'darwin';
191
+ async function probeMacAppBundle() {
192
+ if (!(await pathExists(distDir)))
193
+ return null;
194
+ let entries;
195
+ try {
196
+ entries = await readdir(distDir);
197
+ }
198
+ catch {
199
+ return null;
200
+ }
201
+ for (const entry of entries) {
202
+ if (!entry.endsWith('.app'))
203
+ continue;
204
+ const candidate = join(distDir, entry, 'Contents', 'Resources', relativeAppdir);
205
+ if (await pathExists(candidate))
206
+ return candidate;
207
+ }
179
208
  return null;
180
- let entries;
181
- try {
182
- entries = await readdir(distDir);
183
209
  }
184
- catch {
210
+ if (isMacos) {
211
+ const appBundle = await probeMacAppBundle();
212
+ if (appBundle)
213
+ return appBundle;
214
+ if (await pathExists(distBinCandidate))
215
+ return distBinCandidate;
185
216
  return null;
186
217
  }
187
- for (const entry of entries) {
188
- if (!entry.endsWith('.app'))
189
- continue;
190
- const candidate = join(distDir, entry, 'Contents', 'Resources', relativeAppdir);
191
- if (await pathExists(candidate))
192
- return candidate;
193
- }
194
- return null;
218
+ if (await pathExists(distBinCandidate))
219
+ return distBinCandidate;
220
+ return probeMacAppBundle();
195
221
  }
196
222
  /**
197
223
  * Top-level resolver. Walks every test path, reads the nearest
@@ -39,3 +39,23 @@ export declare class GitIndexLockError extends GitError {
39
39
  constructor(lockPath: string, ageMs?: number | undefined);
40
40
  get userMessage(): string;
41
41
  }
42
+ /**
43
+ * Error thrown when `git add` (monolithic or chunked) exceeds the
44
+ * configured timeout while indexing the Firefox source tree.
45
+ *
46
+ * 2026-04-24 eval Finding 10: a 140.10.0esr bump on a previously-working
47
+ * 140.9.0esr workspace aborted after ~854s with a generic
48
+ * `AbortError: The operation was aborted`. The root cause was the
49
+ * `git add` timeout firing, but the surfaced error was indistinguishable
50
+ * from any other AbortError and gave the operator no actionable
51
+ * direction. This typed error carries the elapsed budget and the
52
+ * environment-variable override so the recovery path is
53
+ * self-documenting.
54
+ */
55
+ export declare class GitIndexingTimeoutError extends GitError {
56
+ readonly phase: 'monolithic' | 'chunked';
57
+ readonly timeoutMs: number;
58
+ readonly envVar: string;
59
+ constructor(phase: 'monolithic' | 'chunked', timeoutMs: number, envVar: string, cause?: Error);
60
+ get userMessage(): string;
61
+ }
@@ -96,4 +96,43 @@ export class GitIndexLockError extends GitError {
96
96
  ' 3. Re-run "fireforge download --force"');
97
97
  }
98
98
  }
99
+ /**
100
+ * Error thrown when `git add` (monolithic or chunked) exceeds the
101
+ * configured timeout while indexing the Firefox source tree.
102
+ *
103
+ * 2026-04-24 eval Finding 10: a 140.10.0esr bump on a previously-working
104
+ * 140.9.0esr workspace aborted after ~854s with a generic
105
+ * `AbortError: The operation was aborted`. The root cause was the
106
+ * `git add` timeout firing, but the surfaced error was indistinguishable
107
+ * from any other AbortError and gave the operator no actionable
108
+ * direction. This typed error carries the elapsed budget and the
109
+ * environment-variable override so the recovery path is
110
+ * self-documenting.
111
+ */
112
+ export class GitIndexingTimeoutError extends GitError {
113
+ phase;
114
+ timeoutMs;
115
+ envVar;
116
+ constructor(phase, timeoutMs, envVar, cause) {
117
+ super(`Git ${phase} indexing exceeded the ${Math.round(timeoutMs / 1000)}s timeout`, 'add -A', cause);
118
+ this.phase = phase;
119
+ this.timeoutMs = timeoutMs;
120
+ this.envVar = envVar;
121
+ }
122
+ get userMessage() {
123
+ const minutes = Math.max(1, Math.round(this.timeoutMs / 60_000));
124
+ const phaseDescription = this.phase === 'monolithic'
125
+ ? 'the monolithic `git add -A` pass'
126
+ : 'one of the chunked `git add -- <dir>` passes';
127
+ return (`Git Error: ${phaseDescription} exceeded the ${minutes}-minute timeout while indexing the Firefox source tree.\n\n` +
128
+ 'Common triggers:\n' +
129
+ ' - Slow or loaded disk (an external volume, encrypted filesystem, or heavily-used SSD under load).\n' +
130
+ ' - A Firefox source tree that has grown beyond what the default timeout accommodates.\n' +
131
+ ' - A background process (antivirus, backup, indexing) holding the working directory.\n\n' +
132
+ 'To recover:\n' +
133
+ ` 1. Extend the timeout via the ${this.envVar} environment variable (milliseconds; e.g. "export ${this.envVar}=1800000" for 30 minutes).\n` +
134
+ ' 2. Re-run "fireforge download --force" — the resume path resumes from the partial initialisation, so the repeat pass is not wasted work.\n' +
135
+ ' 3. If the problem persists, check disk throughput and free space; Firefox source indexing on a cold SSD typically completes in 1–3 minutes.');
136
+ }
137
+ }
99
138
  //# sourceMappingURL=git.js.map
@@ -73,6 +73,29 @@ export interface PatchMetadata {
73
73
  * renamed or removed.
74
74
  */
75
75
  lintIgnore?: string[];
76
+ /**
77
+ * Optional per-patch threshold-tier override for the `large-patch-lines`
78
+ * rule. Exists for branding patches that must touch a small number of
79
+ * cross-cutting registration files alongside `browser/branding/<name>/`
80
+ * (notably `browser/moz.configure` to register the new branding flavor
81
+ * with the top-level configure). The narrow auto-detect allowlist in
82
+ * `isBrandingOnlyPatch` covers the canonical shape, but a fork whose
83
+ * branding patch also touches an unlisted sibling (for example a
84
+ * `browser/themes/<name>/` override or a vendor-specific icon
85
+ * resource) falls through to the general tier and trips the hard
86
+ * limit on what is legitimately one branding diff.
87
+ *
88
+ * Declaring `tier: "branding"` here forces the branding thresholds
89
+ * (notice 3000 / warning 8000 / error 20000) regardless of
90
+ * `filesAffected`. The tier is the weaker claim than test — a patch
91
+ * of all-tests still lands in the test tier even if this field is
92
+ * set, because the test-tier thresholds are already more permissive
93
+ * and a test that is also branding-shaped is vanishingly rare.
94
+ *
95
+ * Only `"branding"` is currently recognised. Unknown values are
96
+ * rejected by the manifest validator, not silently stripped.
97
+ */
98
+ tier?: 'branding';
76
99
  }
77
100
  /**
78
101
  * Schema for patches/patches.json file.
@@ -102,6 +102,15 @@ export interface FurnaceConfig {
102
102
  tokenPrefix?: string;
103
103
  /** Custom properties allowed even though they don't match tokenPrefix (e.g. ["--background-color-box"]) */
104
104
  tokenAllowlist?: string[];
105
+ /**
106
+ * CSS custom-property prefixes that identify upstream / platform
107
+ * variables the fork does not own. `token coverage` counts matches
108
+ * as `allowlisted` rather than `unknown` so a copied upstream
109
+ * baseline doesn't drag fork-owned coverage percentages down.
110
+ * Defaults to `['--moz-']` when unset. Pass an explicit empty array
111
+ * to restore the pre-0.18.0 strict contract.
112
+ */
113
+ platformPrefixes?: string[];
105
114
  /**
106
115
  * Custom properties used as runtime state channels — written and read by the
107
116
  * component itself (e.g. per-frame camera/tile positions) rather than
@@ -76,6 +76,13 @@ export declare class ParsedRecord {
76
76
  * @throws Error if the field is missing or not an array of strings
77
77
  */
78
78
  stringArray(key: string): string[];
79
+ /**
80
+ * Extracts an optional array-of-strings field.
81
+ * @param key - Field name
82
+ * @returns The string array (fresh copy) or undefined when absent
83
+ * @throws Error if the field is present but not an array of strings
84
+ */
85
+ optionalStringArray(key: string): string[] | undefined;
79
86
  /**
80
87
  * Extracts a required nested object field.
81
88
  * @param key - Field name
@@ -142,6 +142,21 @@ export class ParsedRecord {
142
142
  }
143
143
  return [...value];
144
144
  }
145
+ /**
146
+ * Extracts an optional array-of-strings field.
147
+ * @param key - Field name
148
+ * @returns The string array (fresh copy) or undefined when absent
149
+ * @throws Error if the field is present but not an array of strings
150
+ */
151
+ optionalStringArray(key) {
152
+ const value = this.#data[key];
153
+ if (value === undefined)
154
+ return undefined;
155
+ if (!isArray(value) || !value.every(isString)) {
156
+ throw new Error(`${this.#label}.${key} must be an array of strings`);
157
+ }
158
+ return [...value];
159
+ }
145
160
  /**
146
161
  * Extracts a required nested object field.
147
162
  * @param key - Field name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.17.0",
3
+ "version": "0.18.1",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",