@hominis/fireforge 0.30.0 → 0.31.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 (141) hide show
  1. package/CHANGELOG.md +26 -1
  2. package/README.md +22 -5
  3. package/dist/src/commands/export-all.js +5 -15
  4. package/dist/src/commands/export-flow.d.ts +6 -0
  5. package/dist/src/commands/export-flow.js +6 -1
  6. package/dist/src/commands/export-placement-gate.d.ts +38 -0
  7. package/dist/src/commands/export-placement-gate.js +105 -0
  8. package/dist/src/commands/export-shared.d.ts +28 -0
  9. package/dist/src/commands/export-shared.js +36 -0
  10. package/dist/src/commands/export.js +47 -112
  11. package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
  12. package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
  13. package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
  14. package/dist/src/commands/furnace/create.d.ts +1 -2
  15. package/dist/src/commands/furnace/deploy.js +36 -114
  16. package/dist/src/commands/furnace/refresh.js +52 -32
  17. package/dist/src/commands/furnace/sync.js +2 -0
  18. package/dist/src/commands/import.js +108 -73
  19. package/dist/src/commands/lint-per-patch.d.ts +1 -1
  20. package/dist/src/commands/lint-per-patch.js +119 -78
  21. package/dist/src/commands/lint.d.ts +1 -58
  22. package/dist/src/commands/lint.js +96 -84
  23. package/dist/src/commands/patch/compact.d.ts +5 -2
  24. package/dist/src/commands/patch/compact.js +85 -25
  25. package/dist/src/commands/patch/delete.js +17 -17
  26. package/dist/src/commands/patch/index.js +2 -0
  27. package/dist/src/commands/patch/lint-ignore.js +3 -16
  28. package/dist/src/commands/patch/move-files.js +2 -0
  29. package/dist/src/commands/patch/patch-context.d.ts +41 -0
  30. package/dist/src/commands/patch/patch-context.js +53 -0
  31. package/dist/src/commands/patch/rename.js +10 -15
  32. package/dist/src/commands/patch/reorder.d.ts +0 -2
  33. package/dist/src/commands/patch/reorder.js +18 -19
  34. package/dist/src/commands/patch/split-plan.d.ts +66 -0
  35. package/dist/src/commands/patch/split-plan.js +178 -0
  36. package/dist/src/commands/patch/split.d.ts +30 -0
  37. package/dist/src/commands/patch/split.js +283 -0
  38. package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
  39. package/dist/src/commands/patch/staged-dependency.js +4 -17
  40. package/dist/src/commands/patch/tier.js +4 -17
  41. package/dist/src/commands/re-export-scan.js +8 -1
  42. package/dist/src/commands/rebase/summary.d.ts +1 -5
  43. package/dist/src/commands/rebase/summary.js +1 -1
  44. package/dist/src/commands/status-output.js +77 -68
  45. package/dist/src/commands/test-diagnose.d.ts +23 -0
  46. package/dist/src/commands/test-diagnose.js +210 -0
  47. package/dist/src/commands/test-run.d.ts +58 -0
  48. package/dist/src/commands/test-run.js +88 -0
  49. package/dist/src/commands/test.js +169 -257
  50. package/dist/src/commands/token.js +15 -1
  51. package/dist/src/commands/wire.js +109 -78
  52. package/dist/src/core/build-audit.d.ts +1 -1
  53. package/dist/src/core/build-audit.js +2 -46
  54. package/dist/src/core/build-baseline-types.d.ts +38 -0
  55. package/dist/src/core/build-baseline-types.js +10 -0
  56. package/dist/src/core/build-baseline.d.ts +1 -31
  57. package/dist/src/core/build-prepare.d.ts +1 -1
  58. package/dist/src/core/build-prepare.js +2 -45
  59. package/dist/src/core/config-paths.d.ts +0 -8
  60. package/dist/src/core/config-paths.js +4 -4
  61. package/dist/src/core/config-state.d.ts +0 -6
  62. package/dist/src/core/config-state.js +1 -1
  63. package/dist/src/core/config-validate-patch-policy.js +12 -13
  64. package/dist/src/core/config-validate.js +48 -28
  65. package/dist/src/core/engine-changes.d.ts +24 -0
  66. package/dist/src/core/engine-changes.js +64 -0
  67. package/dist/src/core/firefox-cache.d.ts +0 -5
  68. package/dist/src/core/firefox-cache.js +1 -1
  69. package/dist/src/core/firefox-download.d.ts +0 -6
  70. package/dist/src/core/firefox-download.js +1 -1
  71. package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
  72. package/dist/src/core/furnace-apply-helpers.js +11 -20
  73. package/dist/src/core/furnace-apply.d.ts +1 -1
  74. package/dist/src/core/furnace-apply.js +1 -1
  75. package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
  76. package/dist/src/core/furnace-checksum-utils.js +15 -0
  77. package/dist/src/core/furnace-config-validate.d.ts +31 -0
  78. package/dist/src/core/furnace-config-validate.js +133 -0
  79. package/dist/src/core/furnace-config.d.ts +4 -32
  80. package/dist/src/core/furnace-config.js +15 -111
  81. package/dist/src/core/furnace-constants.d.ts +0 -10
  82. package/dist/src/core/furnace-constants.js +2 -2
  83. package/dist/src/core/furnace-css-fragments.d.ts +79 -0
  84. package/dist/src/core/furnace-css-fragments.js +243 -0
  85. package/dist/src/core/furnace-jsconfig.d.ts +63 -0
  86. package/dist/src/core/furnace-jsconfig.js +171 -0
  87. package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
  88. package/dist/src/core/furnace-validate-helpers.js +40 -1
  89. package/dist/src/core/furnace-validate-registration.js +16 -1
  90. package/dist/src/core/furnace-validate.js +54 -2
  91. package/dist/src/core/git-file-ops.d.ts +0 -12
  92. package/dist/src/core/git-file-ops.js +2 -2
  93. package/dist/src/core/lint-cache.d.ts +3 -13
  94. package/dist/src/core/lint-cache.js +11 -5
  95. package/dist/src/core/mach.d.ts +5 -1
  96. package/dist/src/core/mach.js +6 -2
  97. package/dist/src/core/manifest-register.d.ts +5 -16
  98. package/dist/src/core/manifest-register.js +3 -1
  99. package/dist/src/core/patch-lint-checkjs.js +53 -7
  100. package/dist/src/core/patch-lint-jsdoc.js +63 -4
  101. package/dist/src/core/patch-lint-observer.d.ts +37 -0
  102. package/dist/src/core/patch-lint-observer.js +168 -0
  103. package/dist/src/core/patch-lint.js +132 -125
  104. package/dist/src/core/patch-manifest-io.d.ts +16 -0
  105. package/dist/src/core/patch-manifest-io.js +44 -2
  106. package/dist/src/core/patch-manifest-validate.d.ts +1 -8
  107. package/dist/src/core/patch-manifest-validate.js +1 -1
  108. package/dist/src/core/patch-manifest.d.ts +1 -1
  109. package/dist/src/core/patch-manifest.js +1 -1
  110. package/dist/src/core/patch-policy.d.ts +0 -4
  111. package/dist/src/core/patch-policy.js +10 -4
  112. package/dist/src/core/register-browser-content.d.ts +1 -1
  113. package/dist/src/core/register-module.d.ts +1 -1
  114. package/dist/src/core/register-result.d.ts +21 -0
  115. package/dist/src/core/register-result.js +9 -0
  116. package/dist/src/core/register-shared-css.d.ts +1 -1
  117. package/dist/src/core/register-test-manifest.d.ts +1 -1
  118. package/dist/src/core/test-harness-crash.d.ts +61 -0
  119. package/dist/src/core/test-harness-crash.js +140 -0
  120. package/dist/src/core/test-stale-check.d.ts +1 -1
  121. package/dist/src/core/test-stale-check.js +2 -46
  122. package/dist/src/core/test-xpcshell-retry.d.ts +1 -1
  123. package/dist/src/core/test-xpcshell-retry.js +4 -2
  124. package/dist/src/core/token-dark-mode.js +14 -26
  125. package/dist/src/core/token-manager.d.ts +4 -0
  126. package/dist/src/core/token-manager.js +70 -16
  127. package/dist/src/core/typecheck-shim.d.ts +0 -21
  128. package/dist/src/core/typecheck-shim.js +26 -4
  129. package/dist/src/core/wire-utils.js +37 -44
  130. package/dist/src/types/commands/index.d.ts +1 -1
  131. package/dist/src/types/commands/options.d.ts +105 -0
  132. package/dist/src/types/furnace.d.ts +12 -1
  133. package/dist/src/utils/elapsed.d.ts +0 -2
  134. package/dist/src/utils/elapsed.js +1 -1
  135. package/dist/src/utils/fs.d.ts +0 -5
  136. package/dist/src/utils/fs.js +1 -1
  137. package/dist/src/utils/regex.d.ts +0 -6
  138. package/dist/src/utils/regex.js +3 -3
  139. package/dist/src/utils/validation.d.ts +0 -8
  140. package/dist/src/utils/validation.js +2 -2
  141. package/package.json +6 -4
@@ -69,39 +69,54 @@ function validateDarkValue(options) {
69
69
  throw new InvalidArgumentError('Override mode requires --dark-value to be specified.', 'darkValue');
70
70
  }
71
71
  }
72
- async function assertTokenCategoryExists(engineDir, tokensCssPath, category) {
73
- const filePath = join(engineDir, tokensCssPath);
74
- if (!(await pathExists(filePath))) {
75
- throw new GeneralError(`Token CSS file not found: ${tokensCssPath}`);
76
- }
77
- const content = await readText(filePath);
78
- const lines = content.split('\n');
72
+ /**
73
+ * True when `lines` contain a category header (single-line or multi-line
74
+ * banner shape) naming `category`. Shared by the pre-add assertion and the
75
+ * in-memory banner creation path so both agree on what "exists" means.
76
+ */
77
+ function categoryHeaderExists(lines, category) {
79
78
  const escapedCategory = escapeRegex(category);
80
79
  const singleLinePattern = new RegExp(`\\/\\*\\s*=.*${escapedCategory}.*=\\s*\\*\\/`);
81
80
  for (let i = 0; i < lines.length; i++) {
82
81
  const line = lines[i] ?? '';
83
82
  if (singleLinePattern.test(line)) {
84
- return;
83
+ return true;
85
84
  }
86
85
  if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
87
86
  for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
88
87
  const blockLine = lines[j] ?? '';
89
88
  if (new RegExp(escapedCategory).test(blockLine)) {
90
- return;
89
+ return true;
91
90
  }
92
91
  if (/\*\//.test(blockLine))
93
92
  break;
94
93
  }
95
94
  }
96
95
  }
96
+ return false;
97
+ }
98
+ async function assertTokenCategoryExists(engineDir, tokensCssPath, category, createCategory = false) {
99
+ const filePath = join(engineDir, tokensCssPath);
100
+ if (!(await pathExists(filePath))) {
101
+ throw new GeneralError(`Token CSS file not found: ${tokensCssPath}`);
102
+ }
103
+ const content = await readText(filePath);
104
+ const lines = content.split('\n');
105
+ if (categoryHeaderExists(lines, category))
106
+ return;
107
+ // The write path declares the banner in the same edit as the token
108
+ // insertion, so a missing category is fine when creation was requested.
109
+ if (createCategory)
110
+ return;
97
111
  const discoveredCategories = discoverCategoryHeaders(lines);
98
112
  const available = discoveredCategories.length > 0
99
113
  ? `Available categories in the file: ${discoveredCategories.map((name) => `"${name}"`).join(', ')}.`
100
- : 'The file currently has no category headers. Add one by hand near the top of the :root { … } block — the format is "/* = My Category = */" — or run "fireforge furnace init --force" to re-scaffold the default seed set.';
114
+ : 'The file currently has no category headers.';
101
115
  throw new GeneralError(`Category "${category}" not found in ${tokensCssPath}.\n\n` +
102
116
  `${available}\n\n` +
103
117
  'Categories are declared by comment headers. Single-line shape: /* = My Category = */. ' +
104
- 'Multi-line shape: /* =============\\n * My Category\\n * ============= */.');
118
+ 'Multi-line shape: /* =============\\n * My Category\\n * ============= */.\n\n' +
119
+ 'Re-run with --create-category to declare the banner and insert the token in one step.');
105
120
  }
106
121
  /**
107
122
  * Scans a tokens CSS file for category header comments and returns the
@@ -156,7 +171,7 @@ export async function validateTokenAdd(root, options) {
156
171
  validateTokenNameSyntax(options.tokenName);
157
172
  await validateTokenPrefix(root, options);
158
173
  validateDarkValue(options);
159
- await assertTokenCategoryExists(engineDir, tokensCssPath, options.category);
174
+ await assertTokenCategoryExists(engineDir, tokensCssPath, options.category, options.createCategory === true);
160
175
  }
161
176
  /**
162
177
  * Adds a design token to the CSS file and documentation.
@@ -185,7 +200,7 @@ export async function addToken(root, options) {
185
200
  };
186
201
  }
187
202
  // --- CSS file ---
188
- const cssAdded = await addTokenToCSS(engineDir, options, tokensCssPath);
203
+ const { added: cssAdded, categoryCreated } = await addTokenToCSS(engineDir, options, tokensCssPath);
189
204
  if (!cssAdded) {
190
205
  return {
191
206
  cssAdded: false,
@@ -203,8 +218,39 @@ export async function addToken(root, options) {
203
218
  unmappedAdded: docsResult.unmappedAdded,
204
219
  countUpdated: docsResult.countUpdated,
205
220
  skipped: false,
221
+ categoryCreated,
206
222
  };
207
223
  }
224
+ /**
225
+ * Splices a new single-line category banner ("= Name =" comment shape, the
226
+ * same format `discoverCategoryHeaders` recognises) just before the closing
227
+ * brace of the `:root` block, making the new category the last section.
228
+ * Mutates `lines` in place.
229
+ */
230
+ function declareCategoryBanner(lines, category, tokensCssPath) {
231
+ let rootOpen = -1;
232
+ for (let i = 0; i < lines.length; i++) {
233
+ if (/:root\s*\{/.test(lines[i] ?? '')) {
234
+ rootOpen = i;
235
+ break;
236
+ }
237
+ }
238
+ if (rootOpen === -1) {
239
+ throw new GeneralError(`Cannot create category "${category}": no :root block found in ${tokensCssPath}. ` +
240
+ 'Run "fireforge furnace init --force" to re-scaffold the tokens CSS file.');
241
+ }
242
+ let rootClose = -1;
243
+ for (let i = rootOpen + 1; i < lines.length; i++) {
244
+ if (/^\s*\}/.test(lines[i] ?? '')) {
245
+ rootClose = i;
246
+ break;
247
+ }
248
+ }
249
+ if (rootClose === -1) {
250
+ throw new GeneralError(`Cannot create category "${category}": the :root block in ${tokensCssPath} never closes.`);
251
+ }
252
+ lines.splice(rootClose, 0, '', ` /* = ${category} = */`);
253
+ }
208
254
  function findCategorySection(lines, category, tokensCssPath) {
209
255
  const escapedCategory = escapeRegex(category);
210
256
  const singleLinePattern = new RegExp(`\\/\\*\\s*=.*${escapedCategory}.*=\\s*\\*\\/`);
@@ -296,15 +342,23 @@ function insertDarkModeOverride(lines, options) {
296
342
  */
297
343
  async function addTokenToCSS(engineDir, options, tokensCssPath) {
298
344
  const filePath = join(engineDir, tokensCssPath);
299
- await assertTokenCategoryExists(engineDir, tokensCssPath, options.category);
345
+ await assertTokenCategoryExists(engineDir, tokensCssPath, options.category, options.createCategory === true);
300
346
  let content = await readText(filePath);
301
347
  // Idempotency check — strip CSS block comments so we don't match inside them
302
348
  const stripped = content.replace(/\/\*[\s\S]*?\*\//g, '');
303
349
  if (stripped.includes(options.tokenName + ':')) {
304
- return false;
350
+ return { added: false, categoryCreated: false };
305
351
  }
306
352
  const lines = content.split('\n');
307
353
  const annotation = getModeAnnotation(options.mode, options.value);
354
+ // Declare a missing category banner in the same in-memory edit as the
355
+ // token insertion — the file is written exactly once, so a failure
356
+ // between "banner declared" and "token inserted" cannot occur.
357
+ let categoryCreated = false;
358
+ if (options.createCategory === true && !categoryHeaderExists(lines, options.category)) {
359
+ declareCategoryBanner(lines, options.category, tokensCssPath);
360
+ categoryCreated = true;
361
+ }
308
362
  const { categoryLine, sectionEnd } = findCategorySection(lines, options.category, tokensCssPath);
309
363
  // Build the insertion lines
310
364
  const insertLines = [];
@@ -325,7 +379,7 @@ async function addTokenToCSS(engineDir, options, tokensCssPath) {
325
379
  insertDarkModeOverride(lines, options);
326
380
  content = lines.join('\n');
327
381
  await writeText(filePath, content);
328
- return true;
382
+ return { added: true, categoryCreated };
329
383
  }
330
384
  /**
331
385
  * Strips surrounding backticks from a cell, if present. Token cells are
@@ -11,27 +11,6 @@
11
11
  */
12
12
  /** Filename used for the synthetic Firefox-globals shim source file. */
13
13
  export declare const SHIM_FILENAME = "__fireforge_firefox_globals.d.ts";
14
- /**
15
- * Minimal `.d.ts` shim for Firefox privileged-scope globals.
16
- *
17
- * Firefox source is plain JS — no TypeScript allowed. The shim lets
18
- * TS-driven type checking run without reporting "cannot find name"
19
- * for the most common Mozilla APIs. Types are intentionally loose
20
- * (`any`) because full Firefox type coverage is out of scope.
21
- *
22
- * Notable patterns that require shimming:
23
- * - `const lazy = {};` + `ChromeUtils.defineESModuleGetters(lazy, { ... })`
24
- * populates `lazy` at runtime; we declare it as `Record<string, any>`.
25
- * - `Services.obs`, `Services.prefs`, etc. are XPCOM service accessors.
26
- * - `Ci`, `Cc`, `Cr`, `Cu` are XPCOM component shortcuts.
27
- * - Browser chrome globals like `gBrowser`, `gURLBar` are common in
28
- * content scripts wired via `browser.js`.
29
- * - Dynamic `import("resource:-…")` / `import("chrome:-…")` under patch
30
- * checkJs: the compiler sees empty stubs (`noResolve`); without URL
31
- * ambient modules namespaces degrade to unusable typings. Wildcards
32
- * keep Firefox URL imports pragmatically loose, same posture as globals.
33
- */
34
- export declare const FIREFOX_GLOBALS_SHIM = "\ndeclare var Services: any;\ndeclare var ChromeUtils: {\n defineESModuleGetters(target: any, modules: Record<string, string>): void;\n importESModule(specifier: string): any;\n import(specifier: string): any;\n defineModuleGetter(target: any, name: string, specifier: string): void;\n generateQI(interfaces: any[]): Function;\n isClassInfo(obj: any): boolean;\n};\ndeclare var Cu: any;\ndeclare var Ci: any;\ndeclare var Cc: any;\ndeclare var Cr: any;\ndeclare var Components: any;\ndeclare var XPCOMUtils: any;\ndeclare var lazy: Record<string, any>;\ndeclare var PathUtils: any;\ndeclare var IOUtils: any;\ndeclare var FileUtils: any;\ndeclare var gBrowser: any;\ndeclare var gURLBar: any;\ndeclare var gNavigatorBundle: any;\ndeclare var AppConstants: any;\n\n// Shorthand ambient modules \u2014 exports from matching URL imports are loosely typed,\n// avoiding noResolve empty-graph namespaces. (Named member access broke when we tried\n// export= Record under moduleResolution Bundler.)\ndeclare module 'resource:*';\ndeclare module 'chrome:*';\n\n";
35
14
  /**
36
15
  * TS diagnostic codes suppressed by both the patch-lint checkJs pass
37
16
  * and the whole-project typecheck command. Each is a known false
@@ -30,18 +30,21 @@ export const SHIM_FILENAME = '__fireforge_firefox_globals.d.ts';
30
30
  * - Browser chrome globals like `gBrowser`, `gURLBar` are common in
31
31
  * content scripts wired via `browser.js`.
32
32
  * - Dynamic `import("resource:-…")` / `import("chrome:-…")` under patch
33
- * checkJs: the compiler sees empty stubs (`noResolve`); without URL
34
- * ambient modules namespaces degrade to unusable typings. Wildcards
35
- * keep Firefox URL imports pragmatically loose, same posture as globals.
33
+ * checkJs: imports of *patch-owned* modules resolve to their real
34
+ * sources (see `patch-lint-checkjs.ts`); everything else fails host
35
+ * resolution and lands on these URL ambient wildcards, keeping
36
+ * upstream Firefox imports pragmatically loose, same posture as globals.
36
37
  */
37
- export const FIREFOX_GLOBALS_SHIM = `
38
+ const FIREFOX_GLOBALS_SHIM = `
38
39
  declare var Services: any;
39
40
  declare var ChromeUtils: {
40
41
  defineESModuleGetters(target: any, modules: Record<string, string>): void;
41
42
  importESModule(specifier: string): any;
42
43
  import(specifier: string): any;
43
44
  defineModuleGetter(target: any, name: string, specifier: string): void;
45
+ defineLazyGetter(target: any, name: string, getter: () => any): void;
44
46
  generateQI(interfaces: any[]): Function;
47
+ getClassName(obj: any, unwrap?: boolean): string;
45
48
  isClassInfo(obj: any): boolean;
46
49
  };
47
50
  declare var Cu: any;
@@ -58,6 +61,25 @@ declare var gBrowser: any;
58
61
  declare var gURLBar: any;
59
62
  declare var gNavigatorBundle: any;
60
63
  declare var AppConstants: any;
64
+ // Fluent localization — a stable chrome global. Members stay loose (any),
65
+ // but the constructor shape is declared so "new Localization([...])" and
66
+ // "new Localization([...], true)" typecheck without a local cast.
67
+ declare var Localization: {
68
+ new (
69
+ resourceIds: Array<string | { path: string; optional?: boolean }>,
70
+ sync?: boolean
71
+ ): {
72
+ formatValue(id: string, args?: Record<string, unknown>): any;
73
+ formatValues(keys: any[]): any;
74
+ formatMessages(keys: any[]): any;
75
+ formatValueSync(id: string, args?: Record<string, unknown>): any;
76
+ formatValuesSync(keys: any[]): any;
77
+ formatMessagesSync(keys: any[]): any;
78
+ addResourceIds(ids: Array<string | { path: string; optional?: boolean }>): void;
79
+ removeResourceIds(ids: string[]): number;
80
+ setAsync(): void;
81
+ };
82
+ };
61
83
 
62
84
  // Shorthand ambient modules — exports from matching URL imports are loosely typed,
63
85
  // avoiding noResolve empty-graph namespaces. (Named member access broke when we tried
@@ -60,9 +60,7 @@ export function coerceToCall(expression) {
60
60
  */
61
61
  export function countBraceDepth(line, inBlockComment) {
62
62
  let depth = 0;
63
- let inSingle = false;
64
- let inDouble = false;
65
- let inTemplate = false;
63
+ let quote = null;
66
64
  let inLine = false;
67
65
  let inBlock = inBlockComment;
68
66
  for (let i = 0; i < line.length; i++) {
@@ -77,23 +75,13 @@ export function countBraceDepth(line, inBlockComment) {
77
75
  }
78
76
  continue;
79
77
  }
80
- if (ch === '\\' && (inSingle || inDouble || inTemplate)) {
78
+ if (ch === '\\' && quote !== null) {
81
79
  i++;
82
80
  continue;
83
81
  }
84
- if (inSingle) {
85
- if (ch === "'")
86
- inSingle = false;
87
- continue;
88
- }
89
- if (inDouble) {
90
- if (ch === '"')
91
- inDouble = false;
92
- continue;
93
- }
94
- if (inTemplate) {
95
- if (ch === '`')
96
- inTemplate = false;
82
+ if (quote !== null) {
83
+ if (ch === quote)
84
+ quote = null;
97
85
  continue;
98
86
  }
99
87
  if (ch === '/' && next === '/') {
@@ -105,35 +93,12 @@ export function countBraceDepth(line, inBlockComment) {
105
93
  i++;
106
94
  continue;
107
95
  }
108
- // Heuristic for regex literals: if / follows an operator or keyword boundary,
109
- // treat it as a regex start and skip to the closing /
110
- if (ch === '/' && next !== undefined) {
111
- const prev = i > 0 ? line[i - 1] : undefined;
112
- if (prev === undefined || /[=(:,!|&?;~^{[\n+\-*%<>]/.test(prev)) {
113
- // Skip to closing /
114
- i++;
115
- while (i < line.length) {
116
- if (line[i] === '\\') {
117
- i++; // skip escaped character
118
- }
119
- else if (line[i] === '/') {
120
- break;
121
- }
122
- i++;
123
- }
124
- continue;
125
- }
126
- }
127
- if (ch === "'") {
128
- inSingle = true;
129
- continue;
130
- }
131
- if (ch === '"') {
132
- inDouble = true;
96
+ if (ch === '/' && next !== undefined && isRegexLiteralStart(line, i)) {
97
+ i = scanToRegexLiteralEnd(line, i);
133
98
  continue;
134
99
  }
135
- if (ch === '`') {
136
- inTemplate = true;
100
+ if (ch === "'" || ch === '"' || ch === '`') {
101
+ quote = ch;
137
102
  continue;
138
103
  }
139
104
  if (ch === '{')
@@ -143,6 +108,34 @@ export function countBraceDepth(line, inBlockComment) {
143
108
  }
144
109
  return { depth, inBlockComment: inBlock };
145
110
  }
111
+ /**
112
+ * Regex-literal opener heuristic for the fallback scanner: a `/` is
113
+ * treated as starting a regex when it follows an operator or
114
+ * keyword-boundary character (or starts the line). See the misfire notes
115
+ * on {@link countBraceDepth}.
116
+ */
117
+ function isRegexLiteralStart(line, i) {
118
+ const prev = i > 0 ? line[i - 1] : undefined;
119
+ return prev === undefined || /[=(:,!|&?;~^{[\n+\-*%<>]/.test(prev);
120
+ }
121
+ /**
122
+ * Scans from the opening `/` of a regex literal to its closing `/`
123
+ * (honouring escapes), returning the index of the closing slash — or the
124
+ * end of the line when the literal never closes.
125
+ */
126
+ function scanToRegexLiteralEnd(line, start) {
127
+ let i = start + 1;
128
+ while (i < line.length) {
129
+ if (line[i] === '\\') {
130
+ i++; // skip escaped character
131
+ }
132
+ else if (line[i] === '/') {
133
+ break;
134
+ }
135
+ i++;
136
+ }
137
+ return i;
138
+ }
146
139
  /**
147
140
  * Extracts the class/object name from an expression like "MyComponent.init()".
148
141
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Re-exports all command-related types from focused sub-modules.
3
3
  */
4
- export type { BuildOptions, DiscardOptions, DoctorOptions, DownloadOptions, ExportOptions, FurnaceApplyOptions, FurnaceCreateOptions, FurnaceDeployOptions, FurnaceOverrideOptions, FurnacePreviewOptions, FurnaceRefreshOptions, FurnaceRemoveOptions, FurnaceSyncOptions, FurnaceValidateOptions, GlobalOptions, ImportOptions, PackageOptions, PatchCompactOptions, PatchDeleteOptions, PatchLintIgnoreOptions, PatchMoveFilesOptions, PatchRenameOptions, PatchReorderOptions, PatchStagedDependencyOptions, PatchTierOptions, RebaseOptions, ReExportOptions, RegisterOptions, ResetOptions, RunOptions, SetupOptions, SourceSetOptions, StatusOptions, TestOptions, TokenAddOptions, WireOptions, } from './options.js';
4
+ export type { BuildOptions, DiscardOptions, DoctorOptions, DownloadOptions, ExportOptions, FurnaceApplyOptions, FurnaceCreateOptions, FurnaceDeployOptions, FurnaceOverrideOptions, FurnacePreviewOptions, FurnaceRefreshOptions, FurnaceRemoveOptions, FurnaceSyncOptions, FurnaceValidateOptions, GlobalOptions, ImportOptions, LintCommandOptions, PackageOptions, PatchCompactOptions, PatchDeleteOptions, PatchLintIgnoreOptions, PatchMoveFilesOptions, PatchRenameOptions, PatchReorderOptions, PatchSplitOptions, PatchStagedDependencyOptions, PatchTierOptions, RebaseOptions, ReExportOptions, RegisterOptions, ResetOptions, RunOptions, SetupOptions, SourceSetOptions, StatusOptions, TestOptions, TokenAddOptions, WireOptions, } from './options.js';
5
5
  export type { ImportSummary, PatchCategory, PatchesManifest, PatchInfo, PatchLintIssue, PatchMetadata, PatchResult, PatchStagedDependencies, PatchStagedForwardImport, } from './patches.js';
6
6
  export type { DoctorCheck, ProjectStatus, TokenCoverageFileEntry, TokenCoverageReport, } from './project.js';
@@ -304,6 +304,33 @@ export interface PatchStagedDependencyOptions {
304
304
  * It validates an ownership transfer and prints the explicit
305
305
  * `re-export --files` commands needed to perform it.
306
306
  */
307
+ /**
308
+ * Options for the `patch split` command.
309
+ */
310
+ export interface PatchSplitOptions {
311
+ /** Files to move out of the source patch (engine-relative). */
312
+ files: string[];
313
+ /** Name for the new patch (used in its filename slug). */
314
+ name: string;
315
+ /** Category for the new patch; defaults to the source patch's category. */
316
+ category?: string;
317
+ /** Description for the new patch. */
318
+ description?: string;
319
+ /** Exact sparse order for the new patch (mutually exclusive with before/after). */
320
+ order?: number;
321
+ /** Place the new patch before this patch identifier. */
322
+ before?: string;
323
+ /** Place the new patch after this patch identifier (default: the source). */
324
+ after?: string;
325
+ /** Preview without writing. */
326
+ dryRun?: boolean;
327
+ /** Skip interactive confirmation (required for non-TTY). */
328
+ yes?: boolean;
329
+ /** Bypass projected-lint refusals. */
330
+ forceUnsafe?: boolean;
331
+ /** Skip per-patch lint of the projected bodies. */
332
+ skipLint?: boolean;
333
+ }
307
334
  export interface PatchMoveFilesOptions {
308
335
  /** File paths relative to engine/ to move from the source patch to the target patch. */
309
336
  file?: string[];
@@ -394,6 +421,25 @@ export interface TestOptions {
394
421
  * stale process holds the default port or CI uses another port.
395
422
  */
396
423
  marionettePort?: number;
424
+ /**
425
+ * Retry budget for recognized harness crashes (resource-monitor
426
+ * tracebacks, pre-test no-output hangs, post-green shutdown re-entry).
427
+ * 0 disables retries. Defaults to {@link DEFAULT_HARNESS_RETRIES} at the
428
+ * command layer.
429
+ */
430
+ harnessRetries?: number;
431
+ /**
432
+ * Commander negation flag for `--no-shard`. When false, multiple test
433
+ * paths run in one combined mach invocation; by default they shard into
434
+ * sequential single-file harness runs with an aggregate report.
435
+ */
436
+ shard?: boolean;
437
+ /**
438
+ * Project-relative (or absolute) artifact path published to the harness
439
+ * run as `<BINARYNAME>_PERF_SAMPLE_JSON`, for downstream perf-budget
440
+ * checkers that consume a sample artifact after the run.
441
+ */
442
+ perfSamples?: string;
397
443
  }
398
444
  /**
399
445
  * Options for the furnace apply command.
@@ -648,6 +694,8 @@ export interface TokenAddOptions {
648
694
  description?: string;
649
695
  darkValue?: string;
650
696
  dryRun?: boolean;
697
+ /** Declare the category banner in the tokens CSS when it does not exist yet. */
698
+ createCategory?: boolean;
651
699
  }
652
700
  /**
653
701
  * Options for the doctor command.
@@ -681,3 +729,60 @@ export interface GlobalOptions {
681
729
  /** Enable verbose/debug output */
682
730
  verbose?: boolean;
683
731
  }
732
+ /** Options controlling how the lint command filters and tags its output. */
733
+ export interface LintCommandOptions {
734
+ /**
735
+ * When set, tag each issue as `introduced` or `cumulative` based on
736
+ * whether its file changed since this git revision (e.g. `HEAD`, a
737
+ * branch name, or a SHA). Issues are not filtered — the full set still
738
+ * prints — but a diff-scoped summary makes it trivial to see which
739
+ * errors the current task introduced.
740
+ */
741
+ since?: string;
742
+ /**
743
+ * When set together with {@link since}, scope the exit code to issues
744
+ * tagged `introduced`. Cumulative pre-existing errors still print (so
745
+ * the operator can still see the full queue state) but do not fail
746
+ * lint. Motivating case: a branch whose diff is clean but whose repo
747
+ * already carries unrelated `raw-color` / license-header errors from
748
+ * older patches. Without this flag, CI treats the clean branch as
749
+ * failing; with it, a branch "breaks the build" only when its own diff
750
+ * introduced a new error.
751
+ *
752
+ * Requires {@link since}: without a revision to diff against there is
753
+ * no distinction between introduced and cumulative, so the flag is
754
+ * rejected up-front rather than silently ignored.
755
+ */
756
+ onlyIntroduced?: boolean;
757
+ /**
758
+ * Lint each patch in the queue as its own isolated diff, rather than
759
+ * the aggregate `git diff HEAD` across all applied patches.
760
+ *
761
+ * Motivating case: running `fireforge lint` (no args) on a repo where
762
+ * `fireforge import` or `fireforge rebase` has just applied the full
763
+ * patch queue produces an aggregate diff (every patch's changes
764
+ * summed). The patch-size advisory rules (`large-patch-lines`,
765
+ * `large-patch-files`) then fire against the sum — e.g. "Patch is
766
+ * 37529 lines" on a queue of 22 individually-fine patches — which
767
+ * reads as a task-specific regression when it is really an artefact
768
+ * of the aggregation. `--per-patch` rescopes the diff to each patch's
769
+ * own `filesAffected`, honours the patch's own `lintIgnore`, and runs
770
+ * the cross-patch rules once over the whole queue so queue-level
771
+ * findings (duplicate creations, forward imports) still surface.
772
+ *
773
+ * Mutually exclusive with passing explicit file paths — the two
774
+ * scope contracts are different.
775
+ */
776
+ perPatch?: boolean;
777
+ /**
778
+ * Maximum warning count tolerated before lint exits non-zero. Mirrors
779
+ * ESLint's `--max-warnings` shape for release gates that want advisory
780
+ * findings to become blocking without changing default CLI behavior.
781
+ */
782
+ maxWarnings?: number;
783
+ /**
784
+ * Bypass per-patch lint cache reads and writes. Accepted in aggregate mode
785
+ * for CLI consistency, but only `--per-patch` currently uses the cache.
786
+ */
787
+ noCache?: boolean;
788
+ }
@@ -140,6 +140,15 @@ export interface FurnaceConfig {
140
140
  * Always includes `toolkit/content/widgets` by default.
141
141
  */
142
142
  scanPaths?: string[];
143
+ /**
144
+ * Project-relative path to a consumer-owned jsconfig/tsconfig whose
145
+ * `compilerOptions.paths` entries for deployed component modules
146
+ * (`chrome://global/content/elements/<file>.mjs`) Furnace keeps in sync
147
+ * on deploy. Only entries under that chrome prefix that map into
148
+ * `components/custom/` are managed; everything else is preserved.
149
+ * Unset disables jsconfig maintenance.
150
+ */
151
+ typecheckJsconfig?: string;
143
152
  /** Stock components tracked for preview */
144
153
  stock: string[];
145
154
  /** Override components */
@@ -234,7 +243,7 @@ export interface ApplyResult {
234
243
  */
235
244
  export interface DryRunAction {
236
245
  component: string;
237
- action: 'copy' | 'register-ce' | 'register-jar' | 'copy-ftl' | 'undeploy-remove' | 'undeploy-restore' | 'unregister-ce' | 'unregister-jar';
246
+ action: 'copy' | 'expand-fragments' | 'register-ce' | 'register-jar' | 'copy-ftl' | 'undeploy-remove' | 'undeploy-restore' | 'unregister-ce' | 'unregister-jar';
238
247
  source?: string;
239
248
  target?: string;
240
249
  description: string;
@@ -277,3 +286,5 @@ export interface ValidationIssue {
277
286
  /** Human-readable description of the issue */
278
287
  message: string;
279
288
  }
289
+ /** Resolved test-harness selection for a `furnace create` run. */
290
+ export type ResolvedTestStyle = 'mochikit' | 'browser-chrome' | 'xpcshell' | 'none';
@@ -1,4 +1,2 @@
1
- /** Formats elapsed milliseconds for progress messages. */
2
- export declare function formatElapsed(ms: number): string;
3
1
  /** Returns formatted elapsed time since a start timestamp from Date.now(). */
4
2
  export declare function elapsedSince(startedAt: number): string;
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  /** Formats elapsed milliseconds for progress messages. */
3
- export function formatElapsed(ms) {
3
+ function formatElapsed(ms) {
4
4
  const totalSeconds = Math.max(0, Math.round(ms / 1000));
5
5
  const minutes = Math.floor(totalSeconds / 60);
6
6
  const seconds = totalSeconds % 60;
@@ -13,11 +13,6 @@ export declare function pathExistsStrict(path: string): Promise<boolean>;
13
13
  * @param path - Directory path to ensure
14
14
  */
15
15
  export declare function ensureDir(path: string): Promise<void>;
16
- /**
17
- * Ensures the parent directory of a file exists.
18
- * @param filePath - Path to a file
19
- */
20
- export declare function ensureParentDir(filePath: string): Promise<void>;
21
16
  /**
22
17
  * Removes a directory recursively.
23
18
  * @param path - Directory path to remove
@@ -52,7 +52,7 @@ export async function ensureDir(path) {
52
52
  * Ensures the parent directory of a file exists.
53
53
  * @param filePath - Path to a file
54
54
  */
55
- export async function ensureParentDir(filePath) {
55
+ async function ensureParentDir(filePath) {
56
56
  const parent = dirname(filePath);
57
57
  await ensureDir(parent);
58
58
  }
@@ -2,12 +2,6 @@
2
2
  * Escapes special regex characters in a string.
3
3
  */
4
4
  export declare function escapeRegex(str: string): string;
5
- /** Matches hex color values like #fff, #ff00ff, #ff00ff80 (longest-first alternation) */
6
- export declare const CSS_HEX_COLOR: RegExp;
7
- /** Matches rgb() or rgba() function calls */
8
- export declare const CSS_RGB_COLOR: RegExp;
9
- /** Matches hsl() or hsla() function calls */
10
- export declare const CSS_HSL_COLOR: RegExp;
11
5
  /**
12
6
  * Returns true if the content contains any raw CSS color values (hex, rgb, hsl).
13
7
  */
@@ -6,11 +6,11 @@ export function escapeRegex(str) {
6
6
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
7
  }
8
8
  /** Matches hex color values like #fff, #ff00ff, #ff00ff80 (longest-first alternation) */
9
- export const CSS_HEX_COLOR = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})\b/;
9
+ const CSS_HEX_COLOR = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})\b/;
10
10
  /** Matches rgb() or rgba() function calls */
11
- export const CSS_RGB_COLOR = /\brgba?\s*\(/;
11
+ const CSS_RGB_COLOR = /\brgba?\s*\(/;
12
12
  /** Matches hsl() or hsla() function calls */
13
- export const CSS_HSL_COLOR = /\bhsla?\s*\(/;
13
+ const CSS_HSL_COLOR = /\bhsla?\s*\(/;
14
14
  // Global variants for counting (cached to avoid re-creation per call)
15
15
  const CSS_HEX_COLOR_G = new RegExp(CSS_HEX_COLOR.source, 'g');
16
16
  const CSS_RGB_COLOR_G = new RegExp(CSS_RGB_COLOR.source, 'g');
@@ -90,14 +90,6 @@ export declare const PATCH_CATEGORIES: readonly ["branding", "ui", "privacy", "s
90
90
  * Validates a patch category string.
91
91
  */
92
92
  export declare function isValidPatchCategory(category: string): category is (typeof PATCH_CATEGORIES)[number];
93
- /**
94
- * Checks whether a Firefox version string has an ESR suffix.
95
- */
96
- export declare function isEsrVersion(version: string): boolean;
97
- /**
98
- * Checks whether a Firefox version string is a beta version (e.g. "147.0b1").
99
- */
100
- export declare function isBetaVersion(version: string): boolean;
101
93
  /**
102
94
  * Infers the Firefox product type from a version string.
103
95
  * Returns undefined if no clear inference can be made.
@@ -134,13 +134,13 @@ export function isValidPatchCategory(category) {
134
134
  /**
135
135
  * Checks whether a Firefox version string has an ESR suffix.
136
136
  */
137
- export function isEsrVersion(version) {
137
+ function isEsrVersion(version) {
138
138
  return /esr$/i.test(version);
139
139
  }
140
140
  /**
141
141
  * Checks whether a Firefox version string is a beta version (e.g. "147.0b1").
142
142
  */
143
- export function isBetaVersion(version) {
143
+ function isBetaVersion(version) {
144
144
  return /b\d+$/.test(version);
145
145
  }
146
146
  /**