@hominis/fireforge 0.12.0 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
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
+
3
20
  ## 0.12.0
4
21
 
5
22
  ### JSDoc validation (breaking)
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,30 +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-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 >650 lines | warning |
206
- | `observer-topic-naming` | Observer topics with binaryName | warning |
207
- | `large-patch-files` | Patches affecting >5 files | warning |
208
- | `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
209
 
210
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
211
 
212
- **Optional `checkJs` pass.** Enable TypeScript-based 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 the pass focuses on type errors within the patch-owned code itself (mismatched JSDoc types, wrong argument counts, unreachable code, etc.).
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.).
213
213
 
214
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.
215
215
 
@@ -217,7 +217,7 @@ The two cross-patch rules (`duplicate-new-file-creation` and `forward-import`) r
217
217
 
218
218
  ### Repairing a broken patch queue
219
219
 
220
- 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:
221
221
 
222
222
  ```bash
223
223
  fireforge verify # fsck: manifest + cross-patch lint
@@ -237,7 +237,7 @@ Then fix with the appropriate primitive:
237
237
  | Manifest references a missing patch file | `fireforge doctor --repair-patches-manifest` |
238
238
  | Unmanaged changes you want to discard | `fireforge discard <file>` or `fireforge reset` |
239
239
 
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` 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.
241
241
 
242
242
  ## Wiring Custom Code
243
243
 
@@ -278,7 +278,7 @@ fireforge register browser/modules/mybrowser/MyStore.sys.mjs
278
278
 
279
279
  ## Furnace (UI Component System)
280
280
 
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.
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.
282
282
 
283
283
  There are three component types:
284
284
 
@@ -297,11 +297,11 @@ fireforge furnace status # workspace vs engine drift
297
297
  fireforge furnace diff moz-button # unified diff against baseline
298
298
  ```
299
299
 
300
- `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
301
 
302
302
  ## Additional Commands
303
303
 
304
- The commands below cover project configuration, patch queue management, build packaging, and development utilities. Run `fireforge <command> --help` for full option details.
304
+ The commands below cover project configuration, patch queue management, build packaging and development utilities. Run `fireforge <command> --help` for full option details.
305
305
 
306
306
  ### Configuration
307
307
 
@@ -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
  }
@@ -25,6 +25,10 @@ export async function runPatchLint(engineDir, filesAffected, diffContent, config
25
25
  return;
26
26
  const errors = issues.filter((i) => i.severity === 'error');
27
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
+ }
28
32
  for (const issue of warnings) {
29
33
  warn(`[${issue.check}] ${issue.file}: ${issue.message}`);
30
34
  }
@@ -107,6 +107,10 @@ export async function lintCommand(projectRoot, files) {
107
107
  }
108
108
  const errors = issues.filter((i) => i.severity === 'error');
109
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
+ }
110
114
  for (const issue of warnings) {
111
115
  warn(`[${issue.check}] ${issue.file}: ${issue.message}`);
112
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
@@ -13,21 +13,13 @@ import { getProjectPaths } from '../../core/config.js';
13
13
  import { appendHistory, confirmDestructive, } from '../../core/destructive.js';
14
14
  import { buildPatchQueueContext, lintPatchQueue, } from '../../core/patch-lint.js';
15
15
  import { withPatchDirectoryLock } from '../../core/patch-lock.js';
16
- import { loadPatchesManifest, renumberPatchesInManifest, } from '../../core/patch-manifest.js';
16
+ import { loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, } from '../../core/patch-manifest.js';
17
17
  import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
18
18
  import { toError } from '../../utils/errors.js';
19
19
  import { pathExists } from '../../utils/fs.js';
20
20
  import { info, intro, outro, warn } from '../../utils/logger.js';
21
21
  import { pickDefined } from '../../utils/options.js';
22
22
  import { parsePositiveIntegerFlag } from '../../utils/validation.js';
23
- function resolvePatchIdentifier(identifier, patches) {
24
- if (/^\d+$/.test(identifier)) {
25
- const order = parseInt(identifier, 10);
26
- return patches.find((p) => p.order === order) ?? null;
27
- }
28
- const normalized = identifier.endsWith('.patch') ? identifier : `${identifier}.patch`;
29
- return patches.find((p) => p.filename === normalized) ?? null;
30
- }
31
23
  function padOrder(value, width) {
32
24
  return String(value).padStart(width, '0');
33
25
  }
@@ -6,7 +6,7 @@ import { isGitRepository } from '../core/git.js';
6
6
  import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
7
7
  import { getModifiedFilesInDir, getUntrackedFilesInDir } from '../core/git-status.js';
8
8
  import { updatePatch, updatePatchMetadata } from '../core/patch-export.js';
9
- import { getClaimedFiles, loadPatchesManifest } from '../core/patch-manifest.js';
9
+ import { getClaimedFiles, loadPatchesManifest, resolvePatchIdentifier, } from '../core/patch-manifest.js';
10
10
  import { GeneralError, InvalidArgumentError } from '../errors/base.js';
11
11
  import { toError } from '../utils/errors.js';
12
12
  import { pathExists } from '../utils/fs.js';
@@ -14,22 +14,6 @@ import { cancel, info, intro, isCancel, outro, spinner, success, warn } from '..
14
14
  import { pickDefined } from '../utils/options.js';
15
15
  import { runPatchLint } from './export-shared.js';
16
16
  import { reExportFilesInPlace } from './re-export-files.js';
17
- /**
18
- * Resolves patch identifiers (numbers or filenames) to manifest entries.
19
- * @param identifier - Patch number (e.g. "005") or filename (e.g. "005-ui-storage-modules.patch")
20
- * @param patches - All patches from the manifest
21
- * @returns Matching patch metadata
22
- */
23
- function resolvePatchIdentifier(identifier, patches) {
24
- // If all digits, match by order number
25
- if (/^\d+$/.test(identifier)) {
26
- const order = parseInt(identifier, 10);
27
- return patches.find((p) => p.order === order) ?? null;
28
- }
29
- // Match by filename (with or without .patch suffix)
30
- const normalized = identifier.endsWith('.patch') ? identifier : `${identifier}.patch`;
31
- return patches.find((p) => p.filename === normalized) ?? null;
32
- }
33
17
  async function scanPatchFiles(currentFilesAffected, engineDir, manifest, patchFilename, isDryRun) {
34
18
  const parentDirs = [...new Set(currentFilesAffected.map((f) => dirname(f)))];
35
19
  const claimedByOthers = getClaimedFiles(manifest, patchFilename);
@@ -89,11 +89,11 @@ export async function verifyCommand(projectRoot) {
89
89
  if (lintIssues.length > 0) {
90
90
  warn(`Cross-patch lint issues (${lintIssues.length}):`);
91
91
  for (const issue of lintIssues) {
92
- const label = issue.severity === 'error' ? 'ERROR' : 'WARN';
92
+ const label = issue.severity === 'error' ? 'ERROR' : issue.severity === 'warning' ? 'WARN' : 'NOTICE';
93
93
  warn(` ${label} [${issue.check}] ${issue.file}: ${issue.message}`);
94
94
  if (issue.severity === 'error')
95
95
  errorCount += 1;
96
- else
96
+ else if (issue.severity === 'warning')
97
97
  warningCount += 1;
98
98
  }
99
99
  }
@@ -6,6 +6,12 @@ export { buildPatchQueueContext, collectNewFileCreatorsByPath, type ExtractedSpe
6
6
  export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
7
7
  export { type JsDocCheck, type JsDocIssue, validateExportJsDoc } from './patch-lint-jsdoc.js';
8
8
  export { resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
9
+ /**
10
+ * Returns true if the file path looks like a test file.
11
+ * Matches paths containing `/test/` or filenames starting with
12
+ * `browser_`, `test_`, or `xpcshell_` (all `.js`).
13
+ */
14
+ export declare function isTestFile(file: string): boolean;
9
15
  /**
10
16
  * Detects comment style from file extension for license header checks.
11
17
  */
@@ -19,7 +25,7 @@ export declare function commentStyleForFile(file: string): CommentStyle | null;
19
25
  * @param diffContent - Optional unified diff used to scope raw color checks to introduced lines
20
26
  * @returns Array of lint issues found
21
27
  */
22
- export declare function lintPatchedCss(repoDir: string, affectedFiles: string[], diffContent?: string): Promise<PatchLintIssue[]>;
28
+ export declare function lintPatchedCss(repoDir: string, affectedFiles: string[], diffContent?: string, config?: FireForgeConfig): Promise<PatchLintIssue[]>;
23
29
  /**
24
30
  * Checks new files for required license headers.
25
31
  *
@@ -28,6 +28,14 @@ export { resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
28
28
  // Helpers
29
29
  // ---------------------------------------------------------------------------
30
30
  const JS_EXTENSIONS = ['.js', '.mjs', '.jsm'];
31
+ const FILE_SIZE_THRESHOLDS = {
32
+ general: { notice: 500, warning: 750, error: 900 },
33
+ test: { notice: 1200, warning: 1400, error: 1600 },
34
+ };
35
+ const PATCH_LINE_THRESHOLDS = {
36
+ general: { notice: 800, warning: 1500, error: 3000 },
37
+ test: { notice: 1500, warning: 3000, error: 6000 },
38
+ };
31
39
  /**
32
40
  * Returns true if the filename looks like a JS/MJS/JSM file.
33
41
  * Handles `.sys.mjs` as well.
@@ -35,6 +43,17 @@ const JS_EXTENSIONS = ['.js', '.mjs', '.jsm'];
35
43
  function isJsFile(file) {
36
44
  return JS_EXTENSIONS.some((ext) => file.endsWith(ext));
37
45
  }
46
+ /**
47
+ * Returns true if the file path looks like a test file.
48
+ * Matches paths containing `/test/` or filenames starting with
49
+ * `browser_`, `test_`, or `xpcshell_` (all `.js`).
50
+ */
51
+ export function isTestFile(file) {
52
+ if (file.includes('/test/'))
53
+ return true;
54
+ const basename = file.split('/').pop() ?? '';
55
+ return /^(?:browser_|test_|xpcshell_).*\.js$/.test(basename);
56
+ }
38
57
  /**
39
58
  * Detects comment style from file extension for license header checks.
40
59
  */
@@ -59,7 +78,7 @@ export function commentStyleForFile(file) {
59
78
  * @param diffContent - Optional unified diff used to scope raw color checks to introduced lines
60
79
  * @returns Array of lint issues found
61
80
  */
62
- export async function lintPatchedCss(repoDir, affectedFiles, diffContent) {
81
+ export async function lintPatchedCss(repoDir, affectedFiles, diffContent, config) {
63
82
  const cssFiles = affectedFiles.filter((f) => f.endsWith('.css'));
64
83
  if (cssFiles.length === 0)
65
84
  return [];
@@ -86,17 +105,29 @@ export async function lintPatchedCss(repoDir, affectedFiles, diffContent) {
86
105
  const rawCss = await readText(filePath);
87
106
  // Strip block comments before scanning
88
107
  const cssContent = rawCss.replace(/\/\*[\s\S]*?\*\//g, '');
89
- const rawColorContent = addedLinesByFile
90
- ? (addedLinesByFile.get(file) ?? []).join('\n').replace(/\/\*[\s\S]*?\*\//g, '')
91
- : cssContent;
92
108
  // Check only introduced raw color values when diff context is available.
93
- if (hasRawCssColors(rawColorContent)) {
94
- issues.push({
95
- file,
96
- check: 'raw-color-value',
97
- message: 'Raw color value found. Use CSS custom properties (var(--...)) for design token consistency.',
98
- severity: 'error',
99
- });
109
+ // Skip files on the raw-color allowlist (exact path or basename match).
110
+ const allowlist = config?.patchLint?.rawColorAllowlist;
111
+ const isAllowlisted = allowlist?.some((entry) => file === entry || file.endsWith('/' + entry));
112
+ if (!isAllowlisted) {
113
+ // Strip lines with inline fireforge-ignore: raw-color-value suppression.
114
+ // Check against rawCss (before comment stripping) so the CSS comment marker is still present.
115
+ const sourceForSuppression = addedLinesByFile
116
+ ? (addedLinesByFile.get(file) ?? []).join('\n')
117
+ : rawCss;
118
+ const suppressedContent = sourceForSuppression
119
+ .split('\n')
120
+ .filter((line) => !line.includes('fireforge-ignore: raw-color-value'))
121
+ .join('\n')
122
+ .replace(/\/\*[\s\S]*?\*\//g, '');
123
+ if (hasRawCssColors(suppressedContent)) {
124
+ issues.push({
125
+ file,
126
+ check: 'raw-color-value',
127
+ message: 'Raw color value found. Use CSS custom properties (var(--...)) for design token consistency.',
128
+ severity: 'error',
129
+ });
130
+ }
100
131
  }
101
132
  // Check for non-tokenized custom properties
102
133
  if (tokenPrefix) {
@@ -193,14 +224,34 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
193
224
  // 2. File size check (new files only)
194
225
  if (isNew) {
195
226
  const lineCount = content.split('\n').length;
196
- if (lineCount > 650) {
227
+ const isTest = isTestFile(file);
228
+ const thresholds = isTest ? FILE_SIZE_THRESHOLDS.test : FILE_SIZE_THRESHOLDS.general;
229
+ const label = isTest ? 'Test file' : 'New file';
230
+ const verb = isTest ? 'splitting' : 'decomposing';
231
+ if (lineCount >= thresholds.error) {
197
232
  issues.push({
198
233
  file,
199
234
  check: 'file-too-large',
200
- message: `New file has ${lineCount} lines (recommended max: 650). Consider decomposing.`,
235
+ message: `${label} has ${lineCount} lines (hard limit: ${thresholds.error}). Consider ${verb}.`,
236
+ severity: 'error',
237
+ });
238
+ }
239
+ else if (lineCount >= thresholds.warning) {
240
+ issues.push({
241
+ file,
242
+ check: 'file-too-large',
243
+ message: `${label} has ${lineCount} lines (soft limit: ${thresholds.warning}, hard limit: ${thresholds.error}). Consider ${verb}.`,
201
244
  severity: 'warning',
202
245
  });
203
246
  }
247
+ else if (lineCount >= thresholds.notice) {
248
+ issues.push({
249
+ file,
250
+ check: 'file-too-large',
251
+ message: `${label} has ${lineCount} lines (soft limit: ${thresholds.warning}, hard limit: ${thresholds.error}). Consider ${verb}.`,
252
+ severity: 'notice',
253
+ });
254
+ }
204
255
  }
205
256
  // 3. JSDoc on exports (patch-owned .sys.mjs files)
206
257
  const isOwned = patchOwnedFiles ? patchOwnedFiles.has(file) : isNew;
@@ -216,7 +267,7 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
216
267
  }
217
268
  }
218
269
  // 4. Observer topic naming
219
- const topicPattern = /(?:addObserver|removeObserver|notifyObservers)\s*\([^)]*["']([^"']+)["']/g;
270
+ const topicPattern = /(?:addObserver|removeObserver|notifyObservers)\s*\([^)\n]*["']([^"']+)["']/g;
220
271
  let topicMatch;
221
272
  while ((topicMatch = topicPattern.exec(strippedContent)) !== null) {
222
273
  const topic = topicMatch[1];
@@ -283,14 +334,32 @@ export function lintPatchSize(filesAffected, lineCount) {
283
334
  severity: 'warning',
284
335
  });
285
336
  }
286
- if (lineCount > 300) {
337
+ const allTests = filesAffected.length > 0 && filesAffected.every(isTestFile);
338
+ const thresholds = allTests ? PATCH_LINE_THRESHOLDS.test : PATCH_LINE_THRESHOLDS.general;
339
+ if (lineCount >= thresholds.error) {
340
+ issues.push({
341
+ file: '(patch)',
342
+ check: 'large-patch-lines',
343
+ message: `Patch is ${lineCount} lines (hard limit: ${thresholds.error}). Consider splitting into smaller, focused patches.`,
344
+ severity: 'error',
345
+ });
346
+ }
347
+ else if (lineCount >= thresholds.warning) {
287
348
  issues.push({
288
349
  file: '(patch)',
289
350
  check: 'large-patch-lines',
290
- message: `Patch is ${lineCount} lines (recommended: ≤300). Consider splitting into smaller, focused patches.`,
351
+ message: `Patch is ${lineCount} lines (soft limit: ${thresholds.warning}, hard limit: ${thresholds.error}). Consider splitting into smaller, focused patches.`,
291
352
  severity: 'warning',
292
353
  });
293
354
  }
355
+ else if (lineCount >= thresholds.notice) {
356
+ issues.push({
357
+ file: '(patch)',
358
+ check: 'large-patch-lines',
359
+ message: `Patch is ${lineCount} lines (soft limit: ${thresholds.warning}, hard limit: ${thresholds.error}). Consider splitting into smaller, focused patches.`,
360
+ severity: 'notice',
361
+ });
362
+ }
294
363
  return issues;
295
364
  }
296
365
  // ---------------------------------------------------------------------------
@@ -346,7 +415,7 @@ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, con
346
415
  const lineCount = diffContent.split('\n').length;
347
416
  const patchOwnedFiles = resolvePatchOwnedSysMjs(newFiles, patchQueueCtx);
348
417
  const [cssIssues, headerIssues, jsIssues, modifiedHeaderIssues] = await Promise.all([
349
- lintPatchedCss(repoDir, affectedFiles, diffContent),
418
+ lintPatchedCss(repoDir, affectedFiles, diffContent, config),
350
419
  lintNewFileHeaders(repoDir, [...newFiles], config),
351
420
  lintPatchedJs(repoDir, affectedFiles, newFiles, config, patchOwnedFiles),
352
421
  lintModifiedFileHeaders(repoDir, affectedFiles, newFiles),
@@ -0,0 +1,5 @@
1
+ import type { PatchMetadata } from '../types/commands/index.js';
2
+ /**
3
+ * Resolves a patch identifier (ordinal number or filename) to its manifest entry.
4
+ */
5
+ export declare function resolvePatchIdentifier(identifier: string, patches: PatchMetadata[]): PatchMetadata | null;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Resolves a patch identifier (ordinal number or filename) to its manifest entry.
3
+ */
4
+ export function resolvePatchIdentifier(identifier, patches) {
5
+ if (/^\d+$/.test(identifier)) {
6
+ const order = parseInt(identifier, 10);
7
+ return patches.find((p) => p.order === order) ?? null;
8
+ }
9
+ const normalized = identifier.endsWith('.patch') ? identifier : `${identifier}.patch`;
10
+ return patches.find((p) => p.filename === normalized) ?? null;
11
+ }
12
+ //# sourceMappingURL=patch-manifest-resolve.js.map
@@ -8,4 +8,5 @@ export type { PatchManifestConsistencyIssue } from './patch-manifest-consistency
8
8
  export { rebuildPatchesManifest, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
9
9
  export { addPatchToManifest, loadPatchesManifest, PatchDeleteRollbackError, PATCHES_MANIFEST, type PatchRenameEntry, removePatchFileAndManifest, removePatchFromManifest, renumberPatchesInManifest, savePatchesManifest, } from './patch-manifest-io.js';
10
10
  export { checkVersionCompatibility, findPatchesAffectingFile, getClaimedFiles, stampPatchVersions, validatePatchIntegrity, } from './patch-manifest-query.js';
11
+ export { resolvePatchIdentifier } from './patch-manifest-resolve.js';
11
12
  export { validatePatchesManifest } from './patch-manifest-validate.js';
@@ -8,5 +8,6 @@
8
8
  export { rebuildPatchesManifest, validatePatchesManifestConsistency, } from './patch-manifest-consistency.js';
9
9
  export { addPatchToManifest, loadPatchesManifest, PatchDeleteRollbackError, PATCHES_MANIFEST, removePatchFileAndManifest, removePatchFromManifest, renumberPatchesInManifest, savePatchesManifest, } from './patch-manifest-io.js';
10
10
  export { checkVersionCompatibility, findPatchesAffectingFile, getClaimedFiles, stampPatchVersions, validatePatchIntegrity, } from './patch-manifest-query.js';
11
+ export { resolvePatchIdentifier } from './patch-manifest-resolve.js';
11
12
  export { validatePatchesManifest } from './patch-manifest-validate.js';
12
13
  //# sourceMappingURL=patch-manifest.js.map
@@ -94,6 +94,6 @@ export interface PatchLintIssue {
94
94
  fingerprint?: string;
95
95
  /** Human-readable description of the issue */
96
96
  message: string;
97
- /** Severity: errors block export, warnings are advisory */
98
- severity: 'error' | 'warning';
97
+ /** Severity: errors block export, warnings are advisory, notices are informational (not counted) */
98
+ severity: 'error' | 'warning' | 'notice';
99
99
  }
@@ -58,6 +58,8 @@ export interface WireConfig {
58
58
  export interface PatchLintConfig {
59
59
  /** Enable TypeScript checkJs pass on patch-owned .sys.mjs files */
60
60
  checkJs?: boolean;
61
+ /** File paths exempt from the raw-color-value check (exact or basename match) */
62
+ rawColorAllowlist?: string[];
61
63
  }
62
64
  /**
63
65
  * Build mode for mach.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",