@hominis/fireforge 0.11.2 → 0.13.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 (49) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +79 -26
  3. package/dist/src/commands/bootstrap-checks.d.ts +16 -0
  4. package/dist/src/commands/bootstrap-checks.js +66 -0
  5. package/dist/src/commands/bootstrap.js +27 -9
  6. package/dist/src/commands/doctor.d.ts +8 -0
  7. package/dist/src/commands/doctor.js +7 -1
  8. package/dist/src/commands/export-flow.js +3 -11
  9. package/dist/src/commands/export-shared.d.ts +2 -1
  10. package/dist/src/commands/export-shared.js +7 -2
  11. package/dist/src/commands/furnace/create.js +1 -1
  12. package/dist/src/commands/furnace/deploy.js +1 -1
  13. package/dist/src/commands/furnace/override.js +1 -1
  14. package/dist/src/commands/furnace/refresh.js +1 -1
  15. package/dist/src/commands/furnace/remove.js +1 -1
  16. package/dist/src/commands/furnace/rename.js +1 -1
  17. package/dist/src/commands/furnace/scan.js +1 -1
  18. package/dist/src/commands/lint.js +12 -3
  19. package/dist/src/commands/patch/delete.js +1 -15
  20. package/dist/src/commands/patch/reorder.js +1 -9
  21. package/dist/src/commands/re-export.js +1 -17
  22. package/dist/src/commands/verify.js +2 -2
  23. package/dist/src/core/ast-utils.d.ts +10 -0
  24. package/dist/src/core/ast-utils.js +18 -0
  25. package/dist/src/core/config-paths.d.ts +2 -2
  26. package/dist/src/core/config-paths.js +3 -0
  27. package/dist/src/core/config-validate.js +21 -3
  28. package/dist/src/core/file-lock.js +39 -2
  29. package/dist/src/core/furnace-apply.js +2 -1
  30. package/dist/src/core/furnace-config.js +6 -2
  31. package/dist/src/core/patch-apply.js +26 -4
  32. package/dist/src/core/patch-lint-checkjs.d.ts +21 -0
  33. package/dist/src/core/patch-lint-checkjs.js +225 -0
  34. package/dist/src/core/patch-lint-cross.d.ts +1 -0
  35. package/dist/src/core/patch-lint-cross.js +7 -0
  36. package/dist/src/core/patch-lint-jsdoc.d.ts +21 -0
  37. package/dist/src/core/patch-lint-jsdoc.js +259 -0
  38. package/dist/src/core/patch-lint-ownership.d.ts +25 -0
  39. package/dist/src/core/patch-lint-ownership.js +43 -0
  40. package/dist/src/core/patch-lint.d.ts +14 -3
  41. package/dist/src/core/patch-lint.js +116 -47
  42. package/dist/src/core/patch-manifest-resolve.d.ts +5 -0
  43. package/dist/src/core/patch-manifest-resolve.js +12 -0
  44. package/dist/src/core/patch-manifest.d.ts +1 -0
  45. package/dist/src/core/patch-manifest.js +1 -0
  46. package/dist/src/types/commands/patches.d.ts +2 -2
  47. package/dist/src/types/config.d.ts +11 -0
  48. package/dist/src/utils/paths.js +3 -1
  49. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.13.0
4
+
5
+ ### Setup
6
+
7
+ - **`fireforge bootstrap` now runs targeted post-bootstrap checks** instead of pattern-matching output text. When `mach bootstrap` exits successfully but sub-downloads fail (e.g. HTTP 403 from Apple's CDN), FireForge validates actual system state — checking whether a macOS SDK is available via Xcode — and reports actionable results using the same `✓`/`!`/`✗` severity rendering as `fireforge doctor`. Non-critical issues (SDK download failed but Xcode provides one) are reported as warnings rather than alarming "did not complete successfully" errors.
8
+
9
+ ### Lint fixes
10
+
11
+ - **`file-too-large` now uses tiered severity thresholds.** The old single 650-line warning is replaced with a three-tier system (notice / warning / error) that distinguishes general files from test files. General files: 500–749 lines notice, 750–899 warning, 900+ error. Test files (paths containing `/test/`, or filenames matching `browser_*.js`, `test_*.js`, `xpcshell_*.js`): 1200–1399 notice, 1400–1599 warning, 1600+ error. Messages include the applicable thresholds so users know where they stand. The new `notice` severity is displayed but does not count toward warning or error totals and does not block export.
12
+ - **`observer-topic-naming` no longer matches across newlines.** The regex that extracts topic strings from `notifyObservers`/`addObserver`/`removeObserver` calls now anchors to a single line, preventing false positives when the call spans multiple lines and an unrelated string literal appears later.
13
+ - **`raw-color-value` now supports a file allowlist and inline suppression.** New `patchLint.rawColorAllowlist` config array in `fireforge.json` exempts file paths (exact or basename match) from the raw-color check — intended for design token files that must contain raw color values. Individual declarations can also be suppressed with an inline `/* fireforge-ignore: raw-color-value */` comment.
14
+ - **`large-patch-lines` now uses tiered severity thresholds.** The old single >300-line warning is replaced with a three-tier system matching the `file-too-large` pattern. General patches: 800+ lines notice, 1500+ warning, 3000+ error. Test-only patches (all files match test patterns): 1500+ notice, 3000+ warning, 6000+ error. The previous threshold was too restrictive relative to file LOC limits — creating a single new file at the `file-too-large` notice tier (500 LOC) already exceeded it. Messages now include the applicable soft and hard limits.
15
+
16
+ ### General Improvements
17
+
18
+ - **Minor Refactor**
19
+
20
+ ## 0.12.0
21
+
22
+ ### JSDoc validation (breaking)
23
+
24
+ - **JSDoc enforcement is now AST-based and severity `error`.** The previous heuristic (walk backwards from `export` to find `*/`) has been replaced with Acorn-based AST analysis. Exported functions must have a JSDoc block with `@param` for each parameter (names must match) and `@returns` when returning a value. Exported classes require a JSDoc block. Exported constants require `@type`. This is a breaking change: projects that previously passed with incomplete JSDoc will now see lint errors.
25
+ - **Patch-owned scope.** JSDoc enforcement now applies to all patch-owned `.sys.mjs` files, not just files new in the current diff. A file is patch-owned if it was created by the current diff or by any existing patch in the queue.
26
+ - New check: **`jsdoc-param-mismatch`** (error) — flags `@param` tags that are missing or have the wrong name.
27
+ - New check: **`jsdoc-missing-returns`** (error) — flags functions that return a value but lack `@returns`.
28
+ - Exported constants and classes require a JSDoc block but do not require specific tags like `@type`.
29
+
30
+ ### Optional checkJs pass
31
+
32
+ - **`patchLint.checkJs`** — new opt-in config field in `fireforge.json`. When enabled, runs TypeScript's `checkJs` pass (`allowJs + checkJs + noEmit`) on patch-owned `.sys.mjs` files only. Firefox globals are shimmed automatically. Diagnostics are filtered to patch-owned files so upstream noise is suppressed.
33
+ - New check: **`checkjs-type-error`** (error/warning) — surfaces type errors from the TypeScript compiler.
34
+
35
+ ### Hardening
36
+
37
+ - **Path validation.** `binaryName` in `fireforge.json` now rejects null bytes and absolute paths (including Windows drive letters). `isContainedRelativePath` and `isPathInsideRoot` reject null bytes. Furnace custom component `targetPath` rejects null bytes and absolute paths.
38
+ - **Symlink traversal protection.** Patch target validation now checks whether existing paths are symlinks resolving outside the engine tree before applying.
39
+ - **PID-aware stale lock recovery.** The file lock writes the owning PID into the lock directory. Stale lock recovery checks whether the PID is still alive before removing, preventing premature removal when a slow operation legitimately holds the lock.
40
+ - **Forward-import detection** now catches `ChromeUtils.importESModule()` calls in addition to static/dynamic ES imports and `defineESModuleGetters`.
41
+ - **Furnace rollback failure markers** now include the component name and operation context, improving diagnostics in `fireforge doctor`.
42
+ - New lint check in README: **`modified-file-missing-header`** (warning) was implemented but not documented; now listed in the lint checks table.
43
+
3
44
  ## 0.11.0
4
45
 
5
46
  ### New commands
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
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 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`.
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
13
  Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon](https://github.com/dothq/melon).
14
14
 
@@ -18,7 +18,7 @@ Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon
18
18
 
19
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
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.
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
22
 
23
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
24
 
@@ -26,7 +26,7 @@ Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon
26
26
 
27
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
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. Yes, we mock quite a bit, but when building a tool that modifies a separate code base, I think it's a solid compromise for the time being. Full end-to-end runs are currently run locally, as they require about 30 GB of disk and significant compute for multiple full builds. Full end-to-end via Github Actions will be added soonishlyTM.
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. Yes, we mock quite a bit, but when building a tool that modifies a separate code base, I think it's a solid compromise for the time being. Full end-to-end runs are currently run locally, as they require about 30 GB of disk and significant compute for multiple full builds. Full end-to-end via Github Actions will be added soonishlyTM.
30
30
 
31
31
  ## Quick Start
32
32
 
@@ -52,7 +52,7 @@ npx fireforge build # build the browser
52
52
  npx fireforge run # launch it
53
53
  ```
54
54
 
55
- Your project now has `fireforge.json`, an `engine/` directory with Firefox source, and a `patches/` directory with an empty `patches.json` manifest ready for your first customisation.
55
+ Your project now has `fireforge.json`, an `engine/` directory with Firefox source and a `patches/` directory with an empty `patches.json` manifest ready for your first customisation.
56
56
 
57
57
  ### Workflow Overview
58
58
 
@@ -71,14 +71,14 @@ npx fireforge export browser/base/content/browser.js \
71
71
  npx fireforge reset --yes
72
72
  npx fireforge import # --dry-run to preview without applying
73
73
 
74
- # 5. When Firefox releases a new ESR, update fireforge.json, re-download, and rebase
74
+ # 5. When Firefox releases a new ESR, update fireforge.json, re-download and rebase
75
75
  npx fireforge download --force
76
76
  npx fireforge rebase
77
77
  ```
78
78
 
79
79
  ## Patch Workflow
80
80
 
81
- Patches live in `patches/`, applied by numeric filename prefix, and tracked in `patches/patches.json`:
81
+ Patches live in `patches/`, applied by numeric filename prefix and tracked in `patches/patches.json`:
82
82
 
83
83
  ```
84
84
  patches/
@@ -186,22 +186,30 @@ If the manifest drifts after an interrupted export or manual edits, `fireforge i
186
186
  <details>
187
187
  <summary>Patch lint checks</summary>
188
188
 
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 |
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-jsdoc` | Exports in patch-owned `.sys.mjs` | error |
200
+ | `jsdoc-param-mismatch` | Exports in patch-owned `.sys.mjs` | error |
201
+ | `jsdoc-missing-returns` | Exports in patch-owned `.sys.mjs` | error |
202
+ | `checkjs-type-error` | Patch-owned `.sys.mjs` (opt-in) | error |
203
+ | `missing-modification-comment` | Modified upstream JS/MJS | warning |
204
+ | `modified-file-missing-header` | Modified upstream files (JS/CSS/FTL) | warning |
205
+ | `file-too-large` | New files (tiered: 500/750/900 general, 1200/1400/1600 test) | notice / warning / error |
206
+ | `observer-topic-naming` | Observer topics with binaryName | warning |
207
+ | `large-patch-files` | Patches affecting >5 files | warning |
208
+ | `large-patch-lines` | Patch line count (tiered: 800/1500/3000 general, 1500/3000/6000 test) | notice / warning / error |
209
+
210
+ **JSDoc validation** uses AST-based analysis (Acorn) to validate exported APIs in patch-owned `.sys.mjs` files. A file is "patch-owned" if it was newly created by the current diff or by an existing patch in the queue. Functions must document every `@param` (names must match) and include `@returns` when the function returns a value. Exported constants and classes require a JSDoc block.
211
+
212
+ **Optional `checkJs` pass.** Enable a TypeScript-esque bastardization of type checking for patch-owned `.sys.mjs` files by adding `"patchLint": { "checkJs": true }` to `fireforge.json`. This uses the TypeScript compiler API with `allowJs + checkJs + noEmit`, scoped only to patch-owned files. Firefox globals (`Services`, `ChromeUtils`, `lazy`, etc.) are shimmed automatically. Module-resolution errors from Firefox's `resource://` and `chrome://` URL schemes are suppressed since TypeScript cannot follow these. This pass solely focuses on type errors within the patch-owned code itself (mismatched JSDoc types, wrong argument counts, unreachable code, etc.).
205
213
 
206
214
  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.
207
215
 
@@ -209,7 +217,7 @@ The two cross-patch rules (`duplicate-new-file-creation` and `forward-import`) r
209
217
 
210
218
  ### Repairing a broken patch queue
211
219
 
212
- When a patch queue drifts overlapping new-file creations, forward imports, manifest desync start with diagnosis:
220
+ When a patch queue drifts, e.g. due to overlapping new-file creations, forward imports, manifest desync, etc. start with diagnosing the root cause:
213
221
 
214
222
  ```bash
215
223
  fireforge verify # fsck: manifest + cross-patch lint
@@ -229,7 +237,7 @@ Then fix with the appropriate primitive:
229
237
  | Manifest references a missing patch file | `fireforge doctor --repair-patches-manifest` |
230
238
  | Unmanaged changes you want to discard | `fireforge discard <file>` or `fireforge reset` |
231
239
 
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.
240
+ 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` as the file is owned by FireForge.
233
241
 
234
242
  ## Wiring Custom Code
235
243
 
@@ -270,7 +278,7 @@ fireforge register browser/modules/mybrowser/MyStore.sys.mjs
270
278
 
271
279
  ## Furnace (UI Component System)
272
280
 
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.
281
+ 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.
274
282
 
275
283
  There are three component types:
276
284
 
@@ -289,7 +297,52 @@ fireforge furnace status # workspace vs engine drift
289
297
  fireforge furnace diff moz-button # unified diff against baseline
290
298
  ```
291
299
 
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.
300
+ `furnace deploy` validates components before applying. As always, 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.
301
+
302
+ ## Additional Commands
303
+
304
+ The commands below cover project configuration, patch queue management, build packaging and development utilities. Run `fireforge <command> --help` for full option details.
305
+
306
+ ### Configuration
307
+
308
+ ```bash
309
+ # Read a config value
310
+ fireforge config firefox.version
311
+
312
+ # Set a config value
313
+ fireforge config firefox.version 145.0.0esr
314
+
315
+ # Set a value at a non-standard path (requires --force)
316
+ fireforge config customKey "value" --force
317
+ ```
318
+
319
+ ### Patch queue management
320
+
321
+ ```bash
322
+ # Delete a patch from the queue
323
+ fireforge patch delete 003-ui-sidebar-tweaks.patch
324
+
325
+ # Reorder a patch to a new position
326
+ fireforge patch reorder 003-ui-sidebar-tweaks.patch --to 1
327
+
328
+ # Move a patch before or after another
329
+ fireforge patch reorder 003-ui-sidebar.patch --before 001-branding-logo.patch
330
+ ```
331
+
332
+ Both subcommands support `--dry-run` and `--yes`.
333
+
334
+ ### Additional workflow commands
335
+
336
+ ```bash
337
+ # Package the built browser for distribution
338
+ fireforge package
339
+
340
+ # Watch for file changes and auto-rebuild
341
+ fireforge watch
342
+
343
+ # Add a CSS design token
344
+ fireforge token --name "--my-color" --value "light-dark(#fff, #000)"
345
+ ```
293
346
 
294
347
  ## Configuration
295
348
 
@@ -0,0 +1,16 @@
1
+ import type { DoctorCheck } from '../types/commands/index.js';
2
+ /** Tags representing distinct issues detected in bootstrap output. */
3
+ export type BootstrapIssue = 'sdk-fetch-403' | 'python-traceback' | 'missing-origin-remote';
4
+ /**
5
+ * Scans bootstrap output for known failure patterns and returns structured
6
+ * issue tags. A Python traceback paired with an HTTP 403 is collapsed into
7
+ * a single `sdk-fetch-403` tag since the traceback is just the stack trace
8
+ * from the HTTP error.
9
+ */
10
+ export declare function detectBootstrapIssues(output: string): BootstrapIssue[];
11
+ /**
12
+ * Runs targeted post-bootstrap checks based on the detected issues.
13
+ * Returns doctor-compatible check results so the caller can render them
14
+ * with the standard `reportDoctorResults` display.
15
+ */
16
+ export declare function runPostBootstrapChecks(issues: BootstrapIssue[]): Promise<DoctorCheck[]>;
@@ -0,0 +1,66 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { execFile } from 'node:child_process';
3
+ import { failure, warning } from './doctor.js';
4
+ /**
5
+ * Scans bootstrap output for known failure patterns and returns structured
6
+ * issue tags. A Python traceback paired with an HTTP 403 is collapsed into
7
+ * a single `sdk-fetch-403` tag since the traceback is just the stack trace
8
+ * from the HTTP error.
9
+ */
10
+ export function detectBootstrapIssues(output) {
11
+ const normalized = output.replace(/\r\n/g, '\n');
12
+ const issues = [];
13
+ const hasTraceback = /traceback \(most recent call last\):/i.test(normalized);
14
+ const has403 = /\bhttp(?:\s+error)?\s*403\b/i.test(normalized) || /\b403\b.*forbidden/i.test(normalized);
15
+ if (has403) {
16
+ // The traceback is just the stack trace from the HTTP error — report once.
17
+ issues.push('sdk-fetch-403');
18
+ }
19
+ else if (hasTraceback) {
20
+ issues.push('python-traceback');
21
+ }
22
+ if (/no such remote ['"]origin['"]/i.test(normalized) ||
23
+ /remote ['"]origin['"] does not exist/i.test(normalized) ||
24
+ /missing git remote ['"]origin['"]/i.test(normalized)) {
25
+ issues.push('missing-origin-remote');
26
+ }
27
+ return issues;
28
+ }
29
+ /** Checks whether `xcrun --show-sdk-path` returns a valid macOS SDK path. */
30
+ async function hasMacOsSdk() {
31
+ return new Promise((resolve) => {
32
+ execFile('xcrun', ['--show-sdk-path'], { timeout: 10_000 }, (err, stdout) => {
33
+ resolve(!err && stdout.trim().length > 0);
34
+ });
35
+ });
36
+ }
37
+ /**
38
+ * Runs targeted post-bootstrap checks based on the detected issues.
39
+ * Returns doctor-compatible check results so the caller can render them
40
+ * with the standard `reportDoctorResults` display.
41
+ */
42
+ export async function runPostBootstrapChecks(issues) {
43
+ const checks = [];
44
+ for (const issue of issues) {
45
+ switch (issue) {
46
+ case 'sdk-fetch-403': {
47
+ const sdkAvailable = await hasMacOsSdk();
48
+ if (sdkAvailable) {
49
+ checks.push(warning('macOS SDK download', "SDK download from Apple's CDN failed (HTTP 403), but a macOS SDK was found via Xcode. This is safe to ignore."));
50
+ }
51
+ else {
52
+ checks.push(failure('macOS SDK', 'SDK download failed and no macOS SDK found on your system.', 'Install Xcode Command Line Tools with "xcode-select --install"'));
53
+ }
54
+ break;
55
+ }
56
+ case 'python-traceback':
57
+ checks.push(warning('Python traceback', 'Bootstrap emitted a Python traceback. This may indicate a non-critical issue.', 'Review the bootstrap output above for details.'));
58
+ break;
59
+ case 'missing-origin-remote':
60
+ checks.push(failure('Git remote', 'Bootstrap expected an "origin" git remote in the Firefox source checkout.', 'Run "git remote add origin <url>" in the engine directory.'));
61
+ break;
62
+ }
63
+ }
64
+ return checks;
65
+ }
66
+ //# sourceMappingURL=bootstrap-checks.js.map
@@ -4,7 +4,13 @@ import { bootstrapWithOutput } from '../core/mach.js';
4
4
  import { GeneralError } from '../errors/base.js';
5
5
  import { BootstrapError } from '../errors/build.js';
6
6
  import { pathExists } from '../utils/fs.js';
7
- import { error, info, intro, outro } from '../utils/logger.js';
7
+ import { error, info, intro, outro, warn } from '../utils/logger.js';
8
+ import { detectBootstrapIssues, runPostBootstrapChecks } from './bootstrap-checks.js';
9
+ import { reportDoctorResults } from './doctor.js';
10
+ /**
11
+ * Builds a human-readable failure message for hard failures (non-zero exit).
12
+ * Used only when mach bootstrap itself reports failure.
13
+ */
8
14
  function buildBootstrapFailureMessage(output) {
9
15
  const normalized = output.replace(/\r\n/g, '\n');
10
16
  const issues = [];
@@ -52,15 +58,27 @@ export async function bootstrapCommand(projectRoot) {
52
58
  throw new BootstrapError();
53
59
  }
54
60
  // mach bootstrap may exit 0 even when sub-downloads fail (e.g. HTTP 403).
55
- // Scan output for known failure patterns and surface them prominently.
56
- const softFailure = buildBootstrapFailureMessage(`${result.stdout}\n${result.stderr}`);
57
- if (softFailure) {
61
+ // Instead of guessing from output text, detect what went wrong and run
62
+ // targeted checks to determine whether the issues are actually actionable.
63
+ const output = `${result.stdout}\n${result.stderr}`;
64
+ const issues = detectBootstrapIssues(output);
65
+ if (issues.length > 0) {
66
+ const checks = await runPostBootstrapChecks(issues);
67
+ const hasErrors = checks.some((c) => c.severity === 'error' || (!c.passed && !c.warning));
58
68
  info('');
59
- error('Bootstrap completed with issues:');
60
- info(softFailure);
61
- info('Run "fireforge doctor" to verify your build environment. ' +
62
- 'These issues may cause build failures if not resolved.');
63
- outro('Build dependencies installed with warnings');
69
+ if (hasErrors) {
70
+ warn('Bootstrap completed with issues:');
71
+ }
72
+ else {
73
+ warn('Bootstrap completed with warnings:');
74
+ }
75
+ reportDoctorResults(checks);
76
+ if (hasErrors) {
77
+ outro('Build dependencies installed with errors');
78
+ }
79
+ else {
80
+ outro('Build dependencies installed with warnings');
81
+ }
64
82
  return;
65
83
  }
66
84
  outro('Build dependencies installed successfully!');
@@ -1,4 +1,5 @@
1
1
  import { Command } from 'commander';
2
+ import { ExitCode } from '../errors/codes.js';
2
3
  import type { CommandContext } from '../types/cli.js';
3
4
  import type { DoctorCheck, DoctorOptions } from '../types/commands/index.js';
4
5
  import type { FireForgeConfig, FireForgeState, ProjectPaths } from '../types/config.js';
@@ -116,6 +117,13 @@ export declare function failure(name: string, message: string, fix?: string): Do
116
117
  * think through the consequences.
117
118
  */
118
119
  export declare const DOCTOR_CHECK_ORDER: readonly string[];
120
+ /**
121
+ * Renders a list of doctor checks to the console and returns the
122
+ * appropriate exit code (success when no errors, general error otherwise).
123
+ * @param checks - The check results to display
124
+ * @returns The exit code reflecting the overall result
125
+ */
126
+ export declare function reportDoctorResults(checks: DoctorCheck[]): ExitCode;
119
127
  /**
120
128
  * Result of the doctor command, carrying the exit code so the caller
121
129
  * (or test) can inspect it without relying on process.exitCode.
@@ -342,7 +342,13 @@ validateCheckDependencies(DOCTOR_CHECKS);
342
342
  * think through the consequences.
343
343
  */
344
344
  export const DOCTOR_CHECK_ORDER = DOCTOR_CHECKS.map((check) => check.name);
345
- function reportDoctorResults(checks) {
345
+ /**
346
+ * Renders a list of doctor checks to the console and returns the
347
+ * appropriate exit code (success when no errors, general error otherwise).
348
+ * @param checks - The check results to display
349
+ * @returns The exit code reflecting the overall result
350
+ */
351
+ export function reportDoctorResults(checks) {
346
352
  info('');
347
353
  let passedCount = 0;
348
354
  let warningCount = 0;
@@ -11,7 +11,7 @@ import { join } from 'node:path';
11
11
  import { findAllPatchesForFilesWithDetails, planExport } from '../core/patch-export.js';
12
12
  import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, detectNewFilesInDiff, lintPatchQueue, } from '../core/patch-lint.js';
13
13
  import { withPatchDirectoryLock } from '../core/patch-lock.js';
14
- import { addPatchToManifest, loadPatchesManifest, renumberPatchesInManifest, savePatchesManifest, } from '../core/patch-manifest.js';
14
+ import { addPatchToManifest, loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, savePatchesManifest, } from '../core/patch-manifest.js';
15
15
  import { extractNewFileContentFromDiff } from '../core/patch-transform.js';
16
16
  import { InvalidArgumentError } from '../errors/base.js';
17
17
  import { toError } from '../utils/errors.js';
@@ -32,14 +32,6 @@ function buildFilenameForPlacement(category, name, order, width) {
32
32
  const padded = String(order).padStart(Math.max(3, width), '0');
33
33
  return `${padded}-${category}-${sanitizeExportName(name)}.patch`;
34
34
  }
35
- function resolvePatchByIdentifier(identifier, patches) {
36
- if (/^\d+$/.test(identifier)) {
37
- const order = parseInt(identifier, 10);
38
- return patches.find((p) => p.order === order) ?? null;
39
- }
40
- const normalized = identifier.endsWith('.patch') ? identifier : `${identifier}.patch`;
41
- return patches.find((p) => p.filename === normalized) ?? null;
42
- }
43
35
  function getSortedRenameEntries(renameMap) {
44
36
  return Array.from(renameMap.entries()).sort((a, b) => a[1].newOrder - b[1].newOrder);
45
37
  }
@@ -115,7 +107,7 @@ export async function resolvePlacementPlan(patchesDir, options, category, name)
115
107
  targetOrder = options.order;
116
108
  }
117
109
  else if (options.before !== undefined) {
118
- const anchor = resolvePatchByIdentifier(options.before, existingPatches);
110
+ const anchor = resolvePatchIdentifier(options.before, existingPatches);
119
111
  if (!anchor) {
120
112
  throw new InvalidArgumentError(`--before anchor "${options.before}" not found.`, '--before');
121
113
  }
@@ -126,7 +118,7 @@ export async function resolvePlacementPlan(patchesDir, options, category, name)
126
118
  if (afterAnchorId === undefined) {
127
119
  throw new InvalidArgumentError('Placement flag resolver reached --after branch with no value set.', '--after');
128
120
  }
129
- const anchor = resolvePatchByIdentifier(afterAnchorId, existingPatches);
121
+ const anchor = resolvePatchIdentifier(afterAnchorId, existingPatches);
130
122
  if (!anchor) {
131
123
  throw new InvalidArgumentError(`--after anchor "${afterAnchorId}" not found.`, '--after');
132
124
  }
@@ -10,8 +10,9 @@ import type { SpinnerHandle } from '../utils/logger.js';
10
10
  * @param diffContent - Raw unified diff string
11
11
  * @param config - Project configuration
12
12
  * @param skipLint - If true, downgrade errors to warnings
13
+ * @param patchQueueCtx - Optional cross-patch context for ownership resolution
13
14
  */
14
- export declare function runPatchLint(engineDir: string, filesAffected: string[], diffContent: string, config: FireForgeConfig, skipLint?: boolean): Promise<void>;
15
+ export declare function runPatchLint(engineDir: string, filesAffected: string[], diffContent: string, config: FireForgeConfig, skipLint?: boolean, patchQueueCtx?: import('../core/patch-lint-cross.js').PatchQueueContext): Promise<void>;
15
16
  /**
16
17
  * Resolves patch metadata interactively or from flags, with shared validation.
17
18
  * @param options - Export command options
@@ -17,13 +17,18 @@ import { isValidPatchCategory, PATCH_CATEGORIES, validatePatchName } from '../ut
17
17
  * @param diffContent - Raw unified diff string
18
18
  * @param config - Project configuration
19
19
  * @param skipLint - If true, downgrade errors to warnings
20
+ * @param patchQueueCtx - Optional cross-patch context for ownership resolution
20
21
  */
21
- export async function runPatchLint(engineDir, filesAffected, diffContent, config, skipLint) {
22
- const issues = await lintExportedPatch(engineDir, filesAffected, diffContent, config);
22
+ export async function runPatchLint(engineDir, filesAffected, diffContent, config, skipLint, patchQueueCtx) {
23
+ const issues = await lintExportedPatch(engineDir, filesAffected, diffContent, config, patchQueueCtx);
23
24
  if (issues.length === 0)
24
25
  return;
25
26
  const errors = issues.filter((i) => i.severity === 'error');
26
27
  const warnings = issues.filter((i) => i.severity === 'warning');
28
+ const notices = issues.filter((i) => i.severity === 'notice');
29
+ for (const issue of notices) {
30
+ info(`NOTICE [${issue.check}] ${issue.file}: ${issue.message}`);
31
+ }
27
32
  for (const issue of warnings) {
28
33
  warn(`[${issue.check}] ${issue.file}: ${issue.message}`);
29
34
  }
@@ -315,7 +315,7 @@ async function performCreateMutations(args) {
315
315
  await restoreRollbackJournalOrThrow(journal, `Failed to create custom component "${args.componentName}"`);
316
316
  }
317
317
  catch (rollbackError) {
318
- await recordFurnaceRollbackFailure(args.projectRoot, 'create-rollback', toError(rollbackError).message);
318
+ await recordFurnaceRollbackFailure(args.projectRoot, 'create-rollback', `component "${args.componentName}": ${toError(rollbackError).message}`);
319
319
  throw rollbackError;
320
320
  }
321
321
  throw error;
@@ -125,7 +125,7 @@ async function restoreNamedDeployRollback(rollbackJournal, name, projectRoot) {
125
125
  }
126
126
  catch (rollbackError) {
127
127
  if (projectRoot) {
128
- await recordFurnaceRollbackFailure(projectRoot, 'deploy-rollback', toError(rollbackError).message);
128
+ await recordFurnaceRollbackFailure(projectRoot, 'deploy-rollback', `component "${name}": ${toError(rollbackError).message}`);
129
129
  }
130
130
  throw rollbackError;
131
131
  }
@@ -116,7 +116,7 @@ async function performOverrideMutations(args) {
116
116
  await restoreRollbackJournalOrThrow(journal, `Failed to override component "${args.componentName}"`);
117
117
  }
118
118
  catch (rollbackError) {
119
- await recordFurnaceRollbackFailure(args.projectRoot, 'override-rollback', toError(rollbackError).message);
119
+ await recordFurnaceRollbackFailure(args.projectRoot, 'override-rollback', `component "${args.componentName}": ${toError(rollbackError).message}`);
120
120
  throw rollbackError;
121
121
  }
122
122
  throw error;
@@ -149,7 +149,7 @@ async function refreshSingleOverride(projectRoot, name, options = {}) {
149
149
  await restoreRollbackJournalOrThrow(journal, `Failed to refresh override "${name}"`);
150
150
  }
151
151
  catch (rollbackError) {
152
- await recordFurnaceRollbackFailure(projectRoot, 'refresh-rollback', toError(rollbackError).message);
152
+ await recordFurnaceRollbackFailure(projectRoot, 'refresh-rollback', `override "${name}": ${toError(rollbackError).message}`);
153
153
  throw rollbackError;
154
154
  }
155
155
  }
@@ -343,7 +343,7 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
343
343
  await restoreRollbackJournalOrThrow(journal, `Failed to remove component "${name}"`);
344
344
  }
345
345
  catch (rollbackError) {
346
- await recordFurnaceRollbackFailure(projectRoot, 'remove-rollback', toError(rollbackError).message);
346
+ await recordFurnaceRollbackFailure(projectRoot, 'remove-rollback', `component "${name}": ${toError(rollbackError).message}`);
347
347
  throw rollbackError;
348
348
  }
349
349
  throw error;
@@ -185,7 +185,7 @@ async function performRenameMutations(args) {
185
185
  await restoreRollbackJournalOrThrow(journal, `Failed to rename component "${oldName}" to "${newName}"`);
186
186
  }
187
187
  catch (rollbackError) {
188
- await recordFurnaceRollbackFailure(projectRoot, 'rename-rollback', toError(rollbackError).message);
188
+ await recordFurnaceRollbackFailure(projectRoot, 'rename-rollback', `rename "${oldName}" → "${newName}": ${toError(rollbackError).message}`);
189
189
  throw rollbackError;
190
190
  }
191
191
  throw error;
@@ -67,7 +67,7 @@ async function promptAddComponents(components, tracked, projectRoot) {
67
67
  await restoreRollbackJournalOrThrow(journal, 'Failed to update furnace.json during scan');
68
68
  }
69
69
  catch (rollbackError) {
70
- await recordFurnaceRollbackFailure(projectRoot, 'scan-rollback', toError(rollbackError).message);
70
+ await recordFurnaceRollbackFailure(projectRoot, 'scan-rollback', `furnace.json update during scan: ${toError(rollbackError).message}`);
71
71
  throw rollbackError;
72
72
  }
73
73
  throw error;
@@ -85,14 +85,19 @@ export async function lintCommand(projectRoot, files) {
85
85
  }
86
86
  const config = await loadConfig(projectRoot);
87
87
  const filesAffected = extractAffectedFiles(diff);
88
+ // Build patch queue context once so it can be shared between the
89
+ // per-patch ownership resolver and the cross-patch rules.
90
+ let ctx;
91
+ if (await pathExists(paths.patches)) {
92
+ ctx = await buildPatchQueueContext(paths.patches);
93
+ }
88
94
  const issues = [
89
- ...(await lintExportedPatch(paths.engine, filesAffected, diff, config)),
95
+ ...(await lintExportedPatch(paths.engine, filesAffected, diff, config, ctx)),
90
96
  ];
91
97
  // Cross-patch rules operate over the whole queue, so run them whenever a
92
98
  // patches directory exists — they surface duplicate /dev/null creations
93
99
  // and forward-import chains that the per-patch orchestrator cannot see.
94
- if (await pathExists(paths.patches)) {
95
- const ctx = await buildPatchQueueContext(paths.patches);
100
+ if (ctx) {
96
101
  issues.push(...lintPatchQueue(ctx));
97
102
  }
98
103
  if (issues.length === 0) {
@@ -102,6 +107,10 @@ export async function lintCommand(projectRoot, files) {
102
107
  }
103
108
  const errors = issues.filter((i) => i.severity === 'error');
104
109
  const warnings = issues.filter((i) => i.severity === 'warning');
110
+ const notices = issues.filter((i) => i.severity === 'notice');
111
+ for (const issue of notices) {
112
+ info(`NOTICE [${issue.check}] ${issue.file}: ${issue.message}`);
113
+ }
105
114
  for (const issue of warnings) {
106
115
  warn(`[${issue.check}] ${issue.file}: ${issue.message}`);
107
116
  }
@@ -12,26 +12,12 @@ import { getProjectPaths } from '../../core/config.js';
12
12
  import { appendHistory, confirmDestructive } from '../../core/destructive.js';
13
13
  import { buildPatchQueueContext, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, isForwardImportableFile, } from '../../core/patch-lint.js';
14
14
  import { withPatchDirectoryLock } from '../../core/patch-lock.js';
15
- import { loadPatchesManifest, removePatchFileAndManifest } from '../../core/patch-manifest.js';
15
+ import { loadPatchesManifest, removePatchFileAndManifest, resolvePatchIdentifier, } from '../../core/patch-manifest.js';
16
16
  import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
17
17
  import { toError } from '../../utils/errors.js';
18
18
  import { pathExists } from '../../utils/fs.js';
19
19
  import { info, intro, outro, warn } from '../../utils/logger.js';
20
20
  import { pickDefined } from '../../utils/options.js';
21
- /**
22
- * Resolves `<name>` (ordinal number or filename) to a manifest entry.
23
- * Mirrors re-export's `resolvePatchIdentifier` so the two resolvers behave
24
- * consistently — future work can lift this into a shared helper once a
25
- * third consumer appears.
26
- */
27
- function resolvePatchIdentifier(identifier, patches) {
28
- if (/^\d+$/.test(identifier)) {
29
- const order = parseInt(identifier, 10);
30
- return patches.find((p) => p.order === order) ?? null;
31
- }
32
- const normalized = identifier.endsWith('.patch') ? identifier : `${identifier}.patch`;
33
- return patches.find((p) => p.filename === normalized) ?? null;
34
- }
35
21
  /**
36
22
  * Runs the `patch delete` command: removes a patch file and its manifest
37
23
  * row atomically, refusing when a later patch imports a leaf owned by the