@hominis/fireforge 0.10.1 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/CHANGELOG.md +93 -1
  2. package/README.md +125 -238
  3. package/dist/bin/fireforge.js +26 -0
  4. package/dist/src/cli.d.ts +1 -1
  5. package/dist/src/cli.js +131 -52
  6. package/dist/src/commands/bootstrap.js +6 -2
  7. package/dist/src/commands/build.js +4 -2
  8. package/dist/src/commands/discard.js +16 -4
  9. package/dist/src/commands/doctor-furnace.d.ts +8 -0
  10. package/dist/src/commands/doctor-furnace.js +422 -0
  11. package/dist/src/commands/doctor.d.ts +115 -0
  12. package/dist/src/commands/doctor.js +327 -258
  13. package/dist/src/commands/download.js +16 -1
  14. package/dist/src/commands/export-all.js +15 -0
  15. package/dist/src/commands/export-flow.d.ts +91 -0
  16. package/dist/src/commands/export-flow.js +344 -0
  17. package/dist/src/commands/export.js +151 -5
  18. package/dist/src/commands/furnace/apply.d.ts +3 -2
  19. package/dist/src/commands/furnace/apply.js +169 -36
  20. package/dist/src/commands/furnace/create.js +162 -52
  21. package/dist/src/commands/furnace/deploy.js +156 -144
  22. package/dist/src/commands/furnace/diff.d.ts +8 -4
  23. package/dist/src/commands/furnace/diff.js +142 -73
  24. package/dist/src/commands/furnace/index.d.ts +6 -2
  25. package/dist/src/commands/furnace/index.js +76 -25
  26. package/dist/src/commands/furnace/init.d.ts +11 -0
  27. package/dist/src/commands/furnace/init.js +76 -0
  28. package/dist/src/commands/furnace/list.d.ts +4 -1
  29. package/dist/src/commands/furnace/list.js +35 -3
  30. package/dist/src/commands/furnace/override.d.ts +8 -0
  31. package/dist/src/commands/furnace/override.js +216 -26
  32. package/dist/src/commands/furnace/preview.js +184 -30
  33. package/dist/src/commands/furnace/refresh.d.ts +10 -0
  34. package/dist/src/commands/furnace/refresh.js +268 -0
  35. package/dist/src/commands/furnace/remove.js +285 -89
  36. package/dist/src/commands/furnace/rename.d.ts +5 -0
  37. package/dist/src/commands/furnace/rename.js +308 -0
  38. package/dist/src/commands/furnace/scan.d.ts +4 -1
  39. package/dist/src/commands/furnace/scan.js +72 -11
  40. package/dist/src/commands/furnace/status.js +85 -20
  41. package/dist/src/commands/furnace/sync.d.ts +12 -0
  42. package/dist/src/commands/furnace/sync.js +77 -0
  43. package/dist/src/commands/furnace/validate.d.ts +4 -1
  44. package/dist/src/commands/furnace/validate.js +99 -3
  45. package/dist/src/commands/furnace/validation-output.d.ts +24 -1
  46. package/dist/src/commands/furnace/validation-output.js +93 -1
  47. package/dist/src/commands/import.js +37 -4
  48. package/dist/src/commands/lint.js +11 -2
  49. package/dist/src/commands/manifest.d.ts +39 -0
  50. package/dist/src/commands/manifest.js +59 -0
  51. package/dist/src/commands/patch/delete.d.ts +28 -0
  52. package/dist/src/commands/patch/delete.js +209 -0
  53. package/dist/src/commands/patch/index.d.ts +17 -0
  54. package/dist/src/commands/patch/index.js +25 -0
  55. package/dist/src/commands/patch/reorder.d.ts +30 -0
  56. package/dist/src/commands/patch/reorder.js +377 -0
  57. package/dist/src/commands/re-export-files.d.ts +17 -0
  58. package/dist/src/commands/re-export-files.js +177 -0
  59. package/dist/src/commands/re-export.js +44 -0
  60. package/dist/src/commands/rebase/abort.d.ts +1 -1
  61. package/dist/src/commands/rebase/abort.js +12 -3
  62. package/dist/src/commands/rebase/confirm.d.ts +3 -3
  63. package/dist/src/commands/rebase/confirm.js +4 -4
  64. package/dist/src/commands/rebase/index.js +13 -4
  65. package/dist/src/commands/reset.js +20 -4
  66. package/dist/src/commands/run.js +46 -1
  67. package/dist/src/commands/setup-support.js +6 -5
  68. package/dist/src/commands/status.js +97 -6
  69. package/dist/src/commands/test.js +5 -37
  70. package/dist/src/commands/verify.d.ts +31 -0
  71. package/dist/src/commands/verify.js +126 -0
  72. package/dist/src/core/build-prepare.js +40 -16
  73. package/dist/src/core/destructive.d.ts +96 -0
  74. package/dist/src/core/destructive.js +137 -0
  75. package/dist/src/core/diff-hunks.d.ts +73 -0
  76. package/dist/src/core/diff-hunks.js +268 -0
  77. package/dist/src/core/firefox.d.ts +1 -1
  78. package/dist/src/core/firefox.js +1 -1
  79. package/dist/src/core/furnace-apply-helpers.d.ts +89 -6
  80. package/dist/src/core/furnace-apply-helpers.js +302 -57
  81. package/dist/src/core/furnace-apply-output.d.ts +16 -0
  82. package/dist/src/core/furnace-apply-output.js +57 -0
  83. package/dist/src/core/furnace-apply.d.ts +21 -3
  84. package/dist/src/core/furnace-apply.js +260 -29
  85. package/dist/src/core/furnace-checksum-utils.d.ts +4 -0
  86. package/dist/src/core/furnace-checksum-utils.js +24 -0
  87. package/dist/src/core/furnace-config.d.ts +28 -1
  88. package/dist/src/core/furnace-config.js +180 -17
  89. package/dist/src/core/furnace-constants.d.ts +22 -0
  90. package/dist/src/core/furnace-constants.js +36 -0
  91. package/dist/src/core/furnace-graph-utils.d.ts +11 -0
  92. package/dist/src/core/furnace-graph-utils.js +94 -0
  93. package/dist/src/core/furnace-operation.d.ts +108 -0
  94. package/dist/src/core/furnace-operation.js +220 -0
  95. package/dist/src/core/furnace-refresh.d.ts +20 -0
  96. package/dist/src/core/furnace-refresh.js +118 -0
  97. package/dist/src/core/furnace-registration-ast.d.ts +5 -0
  98. package/dist/src/core/furnace-registration-ast.js +134 -4
  99. package/dist/src/core/furnace-registration-remove.d.ts +25 -3
  100. package/dist/src/core/furnace-registration-remove.js +196 -62
  101. package/dist/src/core/furnace-registration-validate.d.ts +13 -1
  102. package/dist/src/core/furnace-registration-validate.js +15 -3
  103. package/dist/src/core/furnace-registration.d.ts +27 -4
  104. package/dist/src/core/furnace-registration.js +93 -11
  105. package/dist/src/core/furnace-rollback.d.ts +11 -0
  106. package/dist/src/core/furnace-rollback.js +78 -7
  107. package/dist/src/core/furnace-scanner.d.ts +8 -2
  108. package/dist/src/core/furnace-scanner.js +152 -55
  109. package/dist/src/core/furnace-stories.js +7 -5
  110. package/dist/src/core/furnace-validate-accessibility.js +7 -1
  111. package/dist/src/core/furnace-validate-compatibility.d.ts +1 -1
  112. package/dist/src/core/furnace-validate-compatibility.js +85 -1
  113. package/dist/src/core/furnace-validate-helpers.d.ts +4 -0
  114. package/dist/src/core/furnace-validate-helpers.js +31 -0
  115. package/dist/src/core/furnace-validate-registration.d.ts +17 -2
  116. package/dist/src/core/furnace-validate-registration.js +73 -3
  117. package/dist/src/core/furnace-validate-structure.d.ts +10 -2
  118. package/dist/src/core/furnace-validate-structure.js +45 -3
  119. package/dist/src/core/furnace-validate.d.ts +10 -1
  120. package/dist/src/core/furnace-validate.js +80 -6
  121. package/dist/src/core/furnace-version-drift.d.ts +55 -0
  122. package/dist/src/core/furnace-version-drift.js +101 -0
  123. package/dist/src/core/git-file-ops.d.ts +8 -0
  124. package/dist/src/core/git-file-ops.js +19 -6
  125. package/dist/src/core/lint-projection.d.ts +25 -0
  126. package/dist/src/core/lint-projection.js +44 -0
  127. package/dist/src/core/mach.d.ts +4 -2
  128. package/dist/src/core/mach.js +17 -2
  129. package/dist/src/core/markdown-table.d.ts +104 -0
  130. package/dist/src/core/markdown-table.js +266 -0
  131. package/dist/src/core/ownership-table.d.ts +53 -0
  132. package/dist/src/core/ownership-table.js +144 -0
  133. package/dist/src/core/patch-apply.d.ts +17 -3
  134. package/dist/src/core/patch-apply.js +86 -8
  135. package/dist/src/core/patch-export.d.ts +119 -5
  136. package/dist/src/core/patch-export.js +183 -25
  137. package/dist/src/core/patch-lint-cross.d.ts +195 -0
  138. package/dist/src/core/patch-lint-cross.js +428 -0
  139. package/dist/src/core/patch-lint-diff.d.ts +33 -0
  140. package/dist/src/core/patch-lint-diff.js +84 -0
  141. package/dist/src/core/patch-lint.d.ts +2 -4
  142. package/dist/src/core/patch-lint.js +12 -50
  143. package/dist/src/core/patch-lock.js +2 -1
  144. package/dist/src/core/patch-manifest-io.d.ts +102 -1
  145. package/dist/src/core/patch-manifest-io.js +270 -2
  146. package/dist/src/core/patch-manifest-query.d.ts +1 -1
  147. package/dist/src/core/patch-manifest-query.js +1 -1
  148. package/dist/src/core/patch-manifest.d.ts +1 -1
  149. package/dist/src/core/patch-manifest.js +1 -1
  150. package/dist/src/core/patch-transform.d.ts +12 -0
  151. package/dist/src/core/patch-transform.js +21 -7
  152. package/dist/src/core/token-manager.js +67 -69
  153. package/dist/src/core/wire-destroy.js +6 -3
  154. package/dist/src/core/wire-init.js +10 -4
  155. package/dist/src/core/wire-subscript.js +9 -3
  156. package/dist/src/core/wire-utils.d.ts +52 -5
  157. package/dist/src/core/wire-utils.js +69 -6
  158. package/dist/src/errors/base.d.ts +20 -0
  159. package/dist/src/errors/base.js +24 -0
  160. package/dist/src/errors/furnace.js +7 -1
  161. package/dist/src/errors/rebase.js +6 -1
  162. package/dist/src/types/commands/index.d.ts +1 -1
  163. package/dist/src/types/commands/options.d.ts +125 -4
  164. package/dist/src/types/commands/patches.d.ts +11 -1
  165. package/dist/src/types/config.d.ts +1 -1
  166. package/dist/src/types/furnace.d.ts +55 -1
  167. package/dist/src/utils/fs.d.ts +12 -0
  168. package/dist/src/utils/fs.js +30 -1
  169. package/dist/src/utils/package-root.d.ts +5 -0
  170. package/dist/src/utils/package-root.js +12 -0
  171. package/dist/src/utils/process.js +9 -4
  172. package/dist/src/utils/validation.d.ts +20 -2
  173. package/dist/src/utils/validation.js +26 -3
  174. package/package.json +1 -1
@@ -1,112 +1,92 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
2
  import { readdir } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- import { getProjectPaths } from '../../core/config.js';
4
+ import { getProjectPaths, loadState } from '../../core/config.js';
5
+ import { diffLines, renderHunks } from '../../core/diff-hunks.js';
6
+ import { getOverrideEngineTargetPath } from '../../core/furnace-apply-helpers.js';
5
7
  import { getFurnacePaths, loadFurnaceConfig } from '../../core/furnace-config.js';
8
+ import { isComponentSourceFile, resolveFtlDir } from '../../core/furnace-constants.js';
9
+ import { getFileContentAtRef } from '../../core/git-file-ops.js';
6
10
  import { FurnaceError } from '../../errors/furnace.js';
7
11
  import { pathExists, readText } from '../../utils/fs.js';
8
12
  import { formatErrorText, formatSuccessText, info, intro, outro } from '../../utils/logger.js';
9
13
  /**
10
- * Computes a simplified line-level diff between two strings.
11
- * Note: Uses a prefix/suffix matching algorithm that may combine
12
- * multiple scattered changes into a single change region.
13
- * For complex diffs with interleaved changes, the output may not
14
- * be optimal.
14
+ * Renders a multi-hunk unified diff between the two strings and returns a
15
+ * flat list of display-ready lines. Each line has already had its marker
16
+ * prefix and color applied by this function; the caller just emits them.
17
+ *
18
+ * Pure delegation to the `diff-hunks` module — kept here as a thin wrapper
19
+ * so the command file does not need to care about the hunk data shape.
15
20
  */
16
- function lineDiff(original, modified) {
17
- const oldLines = original.split('\n');
18
- const newLines = modified.split('\n');
19
- // Remove trailing empty element from trailing newline
20
- if (oldLines[oldLines.length - 1] === '')
21
- oldLines.pop();
22
- if (newLines[newLines.length - 1] === '')
23
- newLines.pop();
24
- const output = [];
25
- // Simple diff: find common prefix, common suffix, then show changed region
26
- let firstDiff = 0;
27
- while (firstDiff < oldLines.length && firstDiff < newLines.length) {
28
- if (oldLines[firstDiff] !== newLines[firstDiff])
29
- break;
30
- firstDiff++;
31
- }
32
- let lastOldDiff = oldLines.length - 1;
33
- let lastNewDiff = newLines.length - 1;
34
- while (lastOldDiff > firstDiff && lastNewDiff > firstDiff) {
35
- if (oldLines[lastOldDiff] !== newLines[lastNewDiff])
36
- break;
37
- lastOldDiff--;
38
- lastNewDiff--;
39
- }
40
- // Context lines before the change
41
- const contextLines = 3;
42
- const contextStart = Math.max(0, firstDiff - contextLines);
43
- const contextEndOld = Math.min(oldLines.length - 1, lastOldDiff + contextLines);
44
- const contextEndNew = Math.min(newLines.length - 1, lastNewDiff + contextLines);
45
- // Leading context
46
- for (let i = contextStart; i < firstDiff; i++) {
47
- output.push(` ${oldLines[i]}`);
48
- }
49
- // Removed lines
50
- for (let i = firstDiff; i <= lastOldDiff; i++) {
51
- output.push(formatErrorText(`- ${oldLines[i]}`));
52
- }
53
- // Added lines
54
- for (let i = firstDiff; i <= lastNewDiff; i++) {
55
- output.push(formatSuccessText(`+ ${newLines[i]}`));
56
- }
57
- // Trailing context
58
- const trailingStart = Math.max(lastOldDiff + 1, lastNewDiff + 1);
59
- const trailingEnd = Math.max(contextEndOld, contextEndNew);
60
- // Use the new lines for trailing context (they should match old lines here)
61
- for (let i = trailingStart; i <= trailingEnd && i < newLines.length; i++) {
62
- output.push(` ${newLines[i]}`);
63
- }
64
- return output;
21
+ function formatUnifiedDiff(original, modified) {
22
+ const hunks = diffLines(original, modified, 3);
23
+ return renderHunks(hunks).map((line) => {
24
+ switch (line.kind) {
25
+ case 'removed':
26
+ return formatErrorText(line.text);
27
+ case 'added':
28
+ return formatSuccessText(line.text);
29
+ default:
30
+ return line.text;
31
+ }
32
+ });
65
33
  }
66
34
  /**
67
- * Runs the furnace diff command to show changes vs the Firefox original.
68
- * Only works for override components.
69
- * @param projectRoot - Root directory of the project
70
- * @param name - Component name to diff
35
+ * Diffs an override component against its Firefox baseline at baseCommit.
71
36
  */
72
- export async function furnaceDiffCommand(projectRoot, name) {
73
- intro('Furnace Diff');
74
- const config = await loadFurnaceConfig(projectRoot);
75
- const paths = getProjectPaths(projectRoot);
76
- const furnacePaths = getFurnacePaths(projectRoot);
77
- // Verify the component is an override
37
+ async function diffOverride(name, projectRoot, config) {
78
38
  const overrideConfig = config.overrides[name];
79
39
  if (!overrideConfig) {
80
- throw new FurnaceError(`"${name}" is not an override component. The diff command only works for overrides.`, name);
40
+ throw new FurnaceError(`Override "${name}" not found in furnace.json.`, name);
81
41
  }
42
+ const paths = getProjectPaths(projectRoot);
43
+ const furnacePaths = getFurnacePaths(projectRoot);
44
+ const ftlDir = resolveFtlDir(config.ftlBasePath);
82
45
  const overrideDir = join(furnacePaths.overridesDir, name);
83
46
  if (!(await pathExists(overrideDir))) {
84
47
  throw new FurnaceError(`Override directory not found: components/overrides/${name}`, name);
85
48
  }
49
+ // Prefer the per-override baseCommit (survives download --force); fall back
50
+ // to the project-wide value for overrides created before this field existed.
51
+ const state = await loadState(projectRoot);
52
+ const baseCommit = overrideConfig.baseCommit ?? state.baseCommit;
53
+ if (!baseCommit) {
54
+ throw new FurnaceError('Cannot diff: baseCommit not found. Re-run "fireforge download" to establish a baseline.', name);
55
+ }
86
56
  const entries = await readdir(overrideDir, { withFileTypes: true });
87
57
  let hasDifferences = false;
88
58
  for (const entry of entries) {
89
59
  if (!entry.isFile())
90
60
  continue;
91
- if (!entry.name.endsWith('.mjs') && !entry.name.endsWith('.css'))
61
+ if (!isComponentSourceFile(entry.name))
92
62
  continue;
93
- const originalPath = join(paths.engine, overrideConfig.basePath, entry.name);
63
+ // git show takes a repo-relative path. paths.engine IS the repo root.
64
+ const enginePath = getOverrideEngineTargetPath(paths.engine, overrideConfig, entry.name, ftlDir).slice(paths.engine.length + 1);
94
65
  const modifiedPath = join(overrideDir, entry.name);
95
- if (!(await pathExists(originalPath))) {
66
+ const baselineDisplayPath = enginePath;
67
+ let originalContent;
68
+ try {
69
+ originalContent = await getFileContentAtRef(paths.engine, enginePath, baseCommit);
70
+ }
71
+ catch (error) {
72
+ throw new FurnaceError(`Cannot read baseline for "${entry.name}" at commit ${baseCommit.slice(0, 8)}: ` +
73
+ `${error instanceof Error ? error.message : String(error)}. ` +
74
+ `The commit may no longer exist in the engine history (e.g. after a re-download). ` +
75
+ `Run "fireforge furnace refresh --reset-base ${name}" to establish a new baseline.`, name);
76
+ }
77
+ if (originalContent === null) {
96
78
  info(`${entry.name}: original not found in engine (new file)`);
97
79
  hasDifferences = true;
98
80
  continue;
99
81
  }
100
- const originalContent = await readText(originalPath);
101
82
  const modifiedContent = await readText(modifiedPath);
102
83
  if (originalContent === modifiedContent) {
103
84
  continue;
104
85
  }
105
86
  hasDifferences = true;
106
- info(`--- ${overrideConfig.basePath}/${entry.name}`);
87
+ info(`--- ${baselineDisplayPath}`);
107
88
  info(`+++ components/overrides/${name}/${entry.name}`);
108
- const diffLines = lineDiff(originalContent, modifiedContent);
109
- for (const line of diffLines) {
89
+ for (const line of formatUnifiedDiff(originalContent, modifiedContent)) {
110
90
  info(line);
111
91
  }
112
92
  info('');
@@ -114,6 +94,95 @@ export async function furnaceDiffCommand(projectRoot, name) {
114
94
  if (!hasDifferences) {
115
95
  info('No modifications found');
116
96
  }
97
+ }
98
+ /**
99
+ * Diffs a custom component's workspace files against the engine-deployed copy.
100
+ * Shows what would change (or has changed) on the next `furnace apply`.
101
+ */
102
+ async function diffCustom(name, projectRoot, config) {
103
+ const customConfig = config.custom[name];
104
+ if (!customConfig) {
105
+ throw new FurnaceError(`Custom component "${name}" not found in furnace.json.`, name);
106
+ }
107
+ const paths = getProjectPaths(projectRoot);
108
+ const furnacePaths = getFurnacePaths(projectRoot);
109
+ const customDir = join(furnacePaths.customDir, name);
110
+ if (!(await pathExists(customDir))) {
111
+ throw new FurnaceError(`Custom component directory not found: components/custom/${name}`, name);
112
+ }
113
+ const engineDir = join(paths.engine, customConfig.targetPath);
114
+ const entries = await readdir(customDir, { withFileTypes: true });
115
+ let hasDifferences = false;
116
+ for (const entry of entries) {
117
+ if (!entry.isFile())
118
+ continue;
119
+ if (!isComponentSourceFile(entry.name))
120
+ continue;
121
+ const workspacePath = join(customDir, entry.name);
122
+ const deployedPath = join(engineDir, entry.name);
123
+ const workspaceContent = await readText(workspacePath);
124
+ if (!(await pathExists(deployedPath))) {
125
+ info(`${entry.name}: not yet deployed to engine (new file)`);
126
+ hasDifferences = true;
127
+ continue;
128
+ }
129
+ const deployedContent = await readText(deployedPath);
130
+ if (workspaceContent === deployedContent) {
131
+ continue;
132
+ }
133
+ hasDifferences = true;
134
+ info(`--- engine/${customConfig.targetPath}/${entry.name}`);
135
+ info(`+++ components/custom/${name}/${entry.name}`);
136
+ for (const line of formatUnifiedDiff(deployedContent, workspaceContent)) {
137
+ info(line);
138
+ }
139
+ info('');
140
+ }
141
+ if (!hasDifferences) {
142
+ info('No differences between workspace and engine');
143
+ }
144
+ }
145
+ /**
146
+ * Runs the furnace diff command.
147
+ *
148
+ * For overrides: shows changes vs the Firefox original at baseCommit.
149
+ * For custom components: shows workspace vs engine-deployed copy.
150
+ * When no name is provided, diffs all override and custom components.
151
+ *
152
+ * @param projectRoot - Root directory of the project
153
+ * @param name - Optional component name to diff (diffs all when omitted)
154
+ */
155
+ export async function furnaceDiffCommand(projectRoot, name) {
156
+ intro('Furnace Diff');
157
+ const config = await loadFurnaceConfig(projectRoot);
158
+ if (name) {
159
+ if (name in config.overrides) {
160
+ await diffOverride(name, projectRoot, config);
161
+ }
162
+ else if (name in config.custom) {
163
+ await diffCustom(name, projectRoot, config);
164
+ }
165
+ else {
166
+ throw new FurnaceError(`"${name}" is not found in furnace.json. Run "fireforge furnace list" to see registered components.`, name);
167
+ }
168
+ }
169
+ else {
170
+ const overrideNames = Object.keys(config.overrides);
171
+ const customNames = Object.keys(config.custom);
172
+ if (overrideNames.length === 0 && customNames.length === 0) {
173
+ info('No components to diff.');
174
+ outro('Diff complete');
175
+ return;
176
+ }
177
+ for (const overrideName of overrideNames) {
178
+ info(`\n── ${overrideName} (override) ──`);
179
+ await diffOverride(overrideName, projectRoot, config);
180
+ }
181
+ for (const customName of customNames) {
182
+ info(`\n── ${customName} (custom) ──`);
183
+ await diffCustom(customName, projectRoot, config);
184
+ }
185
+ }
117
186
  outro('Diff complete');
118
187
  }
119
188
  //# sourceMappingURL=diff.js.map
@@ -4,13 +4,17 @@ import { furnaceApplyCommand } from './apply.js';
4
4
  import { furnaceCreateCommand } from './create.js';
5
5
  import { furnaceDeployCommand } from './deploy.js';
6
6
  import { furnaceDiffCommand } from './diff.js';
7
+ import { furnaceInitCommand } from './init.js';
7
8
  import { furnaceListCommand } from './list.js';
8
- import { furnaceOverrideCommand } from './override.js';
9
+ import { furnaceBatchOverrideCommand, furnaceOverrideCommand } from './override.js';
9
10
  import { furnacePreviewCommand } from './preview.js';
11
+ import { furnaceRefreshCommand } from './refresh.js';
10
12
  import { furnaceRemoveCommand } from './remove.js';
13
+ import { furnaceRenameCommand } from './rename.js';
11
14
  import { furnaceScanCommand } from './scan.js';
12
15
  import { furnaceStatusCommand } from './status.js';
16
+ import { furnaceSyncCommand } from './sync.js';
13
17
  import { furnaceValidateCommand } from './validate.js';
14
- export { furnaceApplyCommand, furnaceCreateCommand, furnaceDeployCommand, furnaceDiffCommand, furnaceListCommand, furnaceOverrideCommand, furnacePreviewCommand, furnaceRemoveCommand, furnaceScanCommand, furnaceStatusCommand, furnaceValidateCommand, };
18
+ export { furnaceApplyCommand, furnaceBatchOverrideCommand, furnaceCreateCommand, furnaceDeployCommand, furnaceDiffCommand, furnaceInitCommand, furnaceListCommand, furnaceOverrideCommand, furnacePreviewCommand, furnaceRefreshCommand, furnaceRemoveCommand, furnaceRenameCommand, furnaceScanCommand, furnaceStatusCommand, furnaceSyncCommand, furnaceValidateCommand, };
15
19
  /** Registers the furnace command on the CLI program. */
16
20
  export declare function registerFurnace(program: Command, context: CommandContext): void;
@@ -5,16 +5,21 @@ import { furnaceApplyCommand } from './apply.js';
5
5
  import { furnaceCreateCommand } from './create.js';
6
6
  import { furnaceDeployCommand } from './deploy.js';
7
7
  import { furnaceDiffCommand } from './diff.js';
8
+ import { furnaceInitCommand } from './init.js';
8
9
  import { furnaceListCommand } from './list.js';
9
- import { furnaceOverrideCommand } from './override.js';
10
+ import { furnaceBatchOverrideCommand, furnaceOverrideCommand } from './override.js';
10
11
  import { furnacePreviewCommand } from './preview.js';
12
+ import { furnaceRefreshCommand } from './refresh.js';
11
13
  import { furnaceRemoveCommand } from './remove.js';
14
+ import { furnaceRenameCommand } from './rename.js';
12
15
  import { furnaceScanCommand } from './scan.js';
13
16
  import { furnaceStatusCommand } from './status.js';
17
+ import { furnaceSyncCommand } from './sync.js';
14
18
  import { furnaceValidateCommand } from './validate.js';
15
- export { furnaceApplyCommand, furnaceCreateCommand, furnaceDeployCommand, furnaceDiffCommand, furnaceListCommand, furnaceOverrideCommand, furnacePreviewCommand, furnaceRemoveCommand, furnaceScanCommand, furnaceStatusCommand, furnaceValidateCommand, };
19
+ export { furnaceApplyCommand, furnaceBatchOverrideCommand, furnaceCreateCommand, furnaceDeployCommand, furnaceDiffCommand, furnaceInitCommand, furnaceListCommand, furnaceOverrideCommand, furnacePreviewCommand, furnaceRefreshCommand, furnaceRemoveCommand, furnaceRenameCommand, furnaceScanCommand, furnaceStatusCommand, furnaceSyncCommand, furnaceValidateCommand, };
16
20
  /**
17
- * Registers read-only Furnace commands such as status, apply, deploy, and scan.
21
+ * Registers Furnace commands for querying component state: status, scan,
22
+ * and action commands like apply, deploy, and create.
18
23
  * @param furnace - Parent Furnace command
19
24
  * @param context - Shared CLI registration context
20
25
  */
@@ -27,24 +32,38 @@ function registerFurnaceInfoCommands(furnace, context) {
27
32
  await furnaceStatusCommand(getProjectRoot(), name);
28
33
  }));
29
34
  furnace
30
- .command('apply')
31
- .description('Apply all components to the engine')
32
- .option('--dry-run', 'Show what would be changed without writing')
33
- .action(withErrorHandling(async (options) => {
34
- await furnaceApplyCommand(getProjectRoot(), pickDefined(options));
35
+ .command('apply [name]')
36
+ .description('Apply components to the engine (optionally a single component)')
37
+ .option('--dry-run', 'Show what would be changed without writing (reads may overlap concurrent mutations)')
38
+ .option('--force', 'Proceed despite baseVersion drift (stale overrides)')
39
+ .option('-w, --watch', 'Watch component directories and re-apply on changes')
40
+ .action(withErrorHandling(async (name, options) => {
41
+ await furnaceApplyCommand(getProjectRoot(), name, pickDefined(options ?? {}));
35
42
  }));
36
43
  furnace
37
44
  .command('deploy [name]')
38
45
  .description('Apply components and validate in one step')
39
- .option('--dry-run', 'Show what would be changed without writing')
46
+ .option('--dry-run', 'Show what would be changed without writing (reads may overlap concurrent mutations)')
47
+ .option('--force', 'Proceed despite baseVersion drift (stale overrides)')
48
+ .option('--skip-validate', 'Skip the validation step (apply only)')
40
49
  .action(withErrorHandling(async (name, options) => {
41
50
  await furnaceDeployCommand(getProjectRoot(), name, pickDefined(options ?? {}));
42
51
  }));
43
52
  furnace
44
53
  .command('scan')
45
54
  .description('Scan engine for available components')
46
- .action(withErrorHandling(async () => {
47
- await furnaceScanCommand(getProjectRoot());
55
+ .option('--deep', 'Search additional Firefox directories beyond the default widgets path')
56
+ .action(withErrorHandling(async (options) => {
57
+ await furnaceScanCommand(getProjectRoot(), pickDefined(options));
58
+ }));
59
+ furnace
60
+ .command('init')
61
+ .description('Initialize furnace.json with project settings')
62
+ .option('-p, --prefix <prefix>', 'Component prefix (e.g. "moz-", "ff-")')
63
+ .option('--ftl-base-path <path>', 'Fluent l10n base path')
64
+ .option('--force', 'Overwrite existing furnace.json')
65
+ .action(withErrorHandling(async (options) => {
66
+ await furnaceInitCommand(getProjectRoot(), options);
48
67
  }));
49
68
  furnace
50
69
  .command('create [name]')
@@ -53,36 +72,43 @@ function registerFurnaceInfoCommands(furnace, context) {
53
72
  .option('--localized', 'Include Fluent l10n support')
54
73
  .option('--no-register', 'Skip customElements.js registration')
55
74
  .option('--with-tests', 'Scaffold Mochitest directory and register in moz.build')
56
- .option('--compose <tags>', 'Stock component tags composed internally (comma-separated)', (val) => val.split(',').map((s) => s.trim()))
75
+ .option('--compose <tags>', 'Record stock tags composed internally (metadata only, comma-separated)', (val) => val.split(',').map((s) => s.trim()))
57
76
  .action(withErrorHandling(async (name, options) => {
58
77
  await furnaceCreateCommand(getProjectRoot(), name, options);
59
78
  }));
60
79
  }
61
80
  /**
62
- * Registers modifying Furnace commands such as override, remove, preview, and diff.
81
+ * Registers Furnace commands for authoring, inspection, and maintenance:
82
+ * override, list, remove, preview, validate, diff, refresh, and rename.
63
83
  * @param furnace - Parent Furnace command
64
84
  * @param context - Shared CLI registration context
65
85
  */
66
86
  function registerFurnaceModifyCommands(furnace, context) {
67
87
  const { getProjectRoot, withErrorHandling } = context;
68
88
  furnace
69
- .command('override [name]')
70
- .description('Fork an existing component for modification')
89
+ .command('override [names...]')
90
+ .description('Fork one or more existing components for modification')
71
91
  .addOption(new Option('-t, --type <type>', 'Override type').choices(['css-only', 'full']))
72
92
  .option('-d, --description <desc>', 'Description')
73
- .action(withErrorHandling(async (name, options) => {
74
- await furnaceOverrideCommand(getProjectRoot(), name, options);
93
+ .action(withErrorHandling(async (names, options) => {
94
+ if (names.length <= 1) {
95
+ await furnaceOverrideCommand(getProjectRoot(), names[0], options);
96
+ }
97
+ else {
98
+ await furnaceBatchOverrideCommand(getProjectRoot(), names, options);
99
+ }
75
100
  }));
76
101
  furnace
77
102
  .command('list')
78
103
  .description('List all registered components')
79
- .action(withErrorHandling(async () => {
80
- await furnaceListCommand(getProjectRoot());
104
+ .option('-v, --verbose', 'Show per-component health indicators (clean/modified/not applied)')
105
+ .action(withErrorHandling(async (options) => {
106
+ await furnaceListCommand(getProjectRoot(), options);
81
107
  }));
82
108
  furnace
83
109
  .command('remove <name>')
84
110
  .description('Remove a component from the workspace')
85
- .option('-f, --force', 'Skip confirmation')
111
+ .option('-y, --yes', 'Skip confirmation')
86
112
  .action(withErrorHandling(async (name, options) => {
87
113
  await furnaceRemoveCommand(getProjectRoot(), name, options);
88
114
  }));
@@ -96,22 +122,47 @@ function registerFurnaceModifyCommands(furnace, context) {
96
122
  furnace
97
123
  .command('validate [name]')
98
124
  .description('Run accessibility and compatibility checks')
99
- .action(withErrorHandling(async (name) => {
100
- await furnaceValidateCommand(getProjectRoot(), name);
125
+ .option('--fix', 'Auto-fix registration issues (missing jar.mn entries, customElements.js)')
126
+ .action(withErrorHandling(async (name, options) => {
127
+ await furnaceValidateCommand(getProjectRoot(), name, pickDefined(options ?? {}));
101
128
  }));
102
129
  furnace
103
- .command('diff <name>')
104
- .description('Show changes vs Firefox original (overrides only)')
130
+ .command('diff [name]')
131
+ .description('Show changes vs baseline (overrides: vs Firefox original, custom: vs engine). Shows all components when name is omitted.')
105
132
  .action(withErrorHandling(async (name) => {
106
133
  await furnaceDiffCommand(getProjectRoot(), name);
107
134
  }));
135
+ furnace
136
+ .command('refresh [name]')
137
+ .description('Merge upstream Firefox changes into overrides (three-way merge)')
138
+ .option('--dry-run', 'Show what would change without modifying files')
139
+ .option('-a, --all', 'Refresh all overrides in a single batch')
140
+ .addOption(new Option('-s, --strategy <strategy>', 'Auto-resolve conflicts (ours = keep local, theirs = accept upstream)').choices(['ours', 'theirs']))
141
+ .option('--reset-base', 'Reset baseline to current engine HEAD (skips three-way merge, recovers from missing baseCommit)')
142
+ .action(withErrorHandling(async (name, options) => {
143
+ await furnaceRefreshCommand(getProjectRoot(), name, pickDefined(options));
144
+ }));
145
+ furnace
146
+ .command('rename <old-name> <new-name>')
147
+ .description('Rename a component (updates files, config, and registrations)')
148
+ .action(withErrorHandling(async (oldName, newName) => {
149
+ await furnaceRenameCommand(getProjectRoot(), oldName, newName);
150
+ }));
151
+ furnace
152
+ .command('sync')
153
+ .description('Refresh drifted overrides and re-apply all components (recommended after fireforge download)')
154
+ .option('--dry-run', 'Show what would change without modifying files (reads may overlap concurrent mutations)')
155
+ .addOption(new Option('-s, --strategy <strategy>', 'Auto-resolve merge conflicts (ours = keep local, theirs = accept upstream)').choices(['ours', 'theirs']))
156
+ .action(withErrorHandling(async (options) => {
157
+ await furnaceSyncCommand(getProjectRoot(), pickDefined(options));
158
+ }));
108
159
  }
109
160
  /** Registers the furnace command on the CLI program. */
110
161
  export function registerFurnace(program, context) {
111
162
  const { getProjectRoot, withErrorHandling } = context;
112
163
  const furnace = program
113
164
  .command('furnace')
114
- .description('Component management (Furnace)')
165
+ .description('Component management — create, override, apply, deploy, diff, validate, sync (run furnace --help for all subcommands)')
115
166
  .action(withErrorHandling(async () => {
116
167
  await furnaceStatusCommand(getProjectRoot());
117
168
  }));
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Runs the furnace init command to create a default furnace.json with
3
+ * user-specified settings.
4
+ * @param projectRoot - Root directory of the project
5
+ * @param options - Init options
6
+ */
7
+ export declare function furnaceInitCommand(projectRoot: string, options?: {
8
+ prefix?: string;
9
+ ftlBasePath?: string;
10
+ force?: boolean;
11
+ }): Promise<void>;
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: EUPL-1.2
2
+ import { text } from '@clack/prompts';
3
+ import { createDefaultFurnaceConfig, furnaceConfigExists, writeFurnaceConfig, } from '../../core/furnace-config.js';
4
+ import { FurnaceError } from '../../errors/furnace.js';
5
+ import { cancel, info, intro, isCancel, note, outro, success } from '../../utils/logger.js';
6
+ /**
7
+ * Runs the furnace init command to create a default furnace.json with
8
+ * user-specified settings.
9
+ * @param projectRoot - Root directory of the project
10
+ * @param options - Init options
11
+ */
12
+ export async function furnaceInitCommand(projectRoot, options = {}) {
13
+ intro('Furnace Init');
14
+ if ((await furnaceConfigExists(projectRoot)) && !options.force) {
15
+ throw new FurnaceError('furnace.json already exists. Use --force to overwrite it.');
16
+ }
17
+ const config = createDefaultFurnaceConfig();
18
+ const isInteractive = process.stdin.isTTY;
19
+ // Resolve componentPrefix
20
+ if (options.prefix !== undefined) {
21
+ config.componentPrefix = options.prefix;
22
+ }
23
+ else if (isInteractive) {
24
+ const prefixResult = await text({
25
+ message: 'Component prefix (e.g. "moz-", "ff-")',
26
+ initialValue: config.componentPrefix,
27
+ validate: (value) => {
28
+ if (!value)
29
+ return 'Prefix is required';
30
+ return undefined;
31
+ },
32
+ });
33
+ if (isCancel(prefixResult)) {
34
+ cancel('Init cancelled');
35
+ return;
36
+ }
37
+ config.componentPrefix = prefixResult;
38
+ }
39
+ // Resolve ftlBasePath
40
+ if (options.ftlBasePath !== undefined) {
41
+ if (options.ftlBasePath.includes('..')) {
42
+ throw new FurnaceError('ftlBasePath must not contain ".." (path traversal)');
43
+ }
44
+ config.ftlBasePath = options.ftlBasePath;
45
+ }
46
+ else if (isInteractive) {
47
+ const ftlResult = await text({
48
+ message: 'Fluent l10n base path (leave empty for default)',
49
+ placeholder: 'toolkit/locales/en-US/toolkit/global',
50
+ });
51
+ if (isCancel(ftlResult)) {
52
+ cancel('Init cancelled');
53
+ return;
54
+ }
55
+ const ftlValue = ftlResult.trim();
56
+ if (ftlValue) {
57
+ if (ftlValue.includes('..')) {
58
+ throw new FurnaceError('ftlBasePath must not contain ".." (path traversal)');
59
+ }
60
+ config.ftlBasePath = ftlValue;
61
+ }
62
+ }
63
+ await writeFurnaceConfig(projectRoot, config);
64
+ success('Created furnace.json');
65
+ const lines = [`Component prefix: ${config.componentPrefix}`];
66
+ if (config.ftlBasePath) {
67
+ lines.push(`FTL base path: ${config.ftlBasePath}`);
68
+ }
69
+ note(lines.join('\n'), 'Configuration');
70
+ info('Next steps:\n' +
71
+ ' fireforge furnace scan — discover engine components\n' +
72
+ ' fireforge furnace create — create a new custom component\n' +
73
+ ' fireforge furnace override — fork an existing component');
74
+ outro('Init complete');
75
+ }
76
+ //# sourceMappingURL=init.js.map
@@ -1,5 +1,8 @@
1
1
  /**
2
2
  * Runs the furnace list command to display all registered components.
3
3
  * @param projectRoot - Root directory of the project
4
+ * @param options - List options
4
5
  */
5
- export declare function furnaceListCommand(projectRoot: string): Promise<void>;
6
+ export declare function furnaceListCommand(projectRoot: string, options?: {
7
+ verbose?: boolean;
8
+ }): Promise<void>;
@@ -1,11 +1,30 @@
1
1
  // SPDX-License-Identifier: EUPL-1.2
2
- import { furnaceConfigExists, loadFurnaceConfig } from '../../core/furnace-config.js';
3
- import { info, intro, note, outro } from '../../utils/logger.js';
2
+ import { join } from 'node:path';
3
+ import { extractComponentChecksums, hasComponentChanged, } from '../../core/furnace-apply-helpers.js';
4
+ import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from '../../core/furnace-config.js';
5
+ import { pathExists } from '../../utils/fs.js';
6
+ import { formatErrorText, formatSuccessText, info, intro, note, outro, } from '../../utils/logger.js';
7
+ /**
8
+ * Returns a short health indicator for a component directory based on whether
9
+ * its workspace checksums have changed since the last apply.
10
+ */
11
+ async function getHealthIndicator(componentDir, type, name, appliedChecksums) {
12
+ if (!(await pathExists(componentDir))) {
13
+ return formatErrorText('missing');
14
+ }
15
+ const previous = extractComponentChecksums(appliedChecksums, type, name);
16
+ if (Object.keys(previous).length === 0) {
17
+ return formatErrorText('not applied');
18
+ }
19
+ const changed = await hasComponentChanged(componentDir, previous);
20
+ return changed ? formatErrorText('modified') : formatSuccessText('clean');
21
+ }
4
22
  /**
5
23
  * Runs the furnace list command to display all registered components.
6
24
  * @param projectRoot - Root directory of the project
25
+ * @param options - List options
7
26
  */
8
- export async function furnaceListCommand(projectRoot) {
27
+ export async function furnaceListCommand(projectRoot, options = {}) {
9
28
  intro('Furnace List');
10
29
  if (!(await furnaceConfigExists(projectRoot))) {
11
30
  info('No components configured. Run "fireforge furnace create" or "fireforge furnace override" to get started.');
@@ -22,6 +41,9 @@ export async function furnaceListCommand(projectRoot) {
22
41
  outro('Done');
23
42
  return;
24
43
  }
44
+ const showHealth = options.verbose ?? false;
45
+ const furnacePaths = showHealth ? getFurnacePaths(projectRoot) : undefined;
46
+ const state = showHealth ? await loadFurnaceState(projectRoot) : undefined;
25
47
  // --- Stock ---
26
48
  if (stockCount > 0) {
27
49
  info('Stock:');
@@ -37,6 +59,11 @@ export async function furnaceListCommand(projectRoot) {
37
59
  if (entry.description) {
38
60
  line += ` — ${entry.description}`;
39
61
  }
62
+ if (showHealth && furnacePaths && state) {
63
+ const componentDir = join(furnacePaths.overridesDir, name);
64
+ const health = await getHealthIndicator(componentDir, 'override', name, state.appliedChecksums);
65
+ line += ` [${health}]`;
66
+ }
40
67
  info(line);
41
68
  }
42
69
  }
@@ -56,6 +83,11 @@ export async function furnaceListCommand(projectRoot) {
56
83
  if (flags.length > 0) {
57
84
  line += ` [${flags.join(', ')}]`;
58
85
  }
86
+ if (showHealth && furnacePaths && state) {
87
+ const componentDir = join(furnacePaths.customDir, name);
88
+ const health = await getHealthIndicator(componentDir, 'custom', name, state.appliedChecksums);
89
+ line += ` [${health}]`;
90
+ }
59
91
  info(line);
60
92
  }
61
93
  }
@@ -6,3 +6,11 @@ import type { FurnaceOverrideOptions } from '../../types/commands/index.js';
6
6
  * @param options - CLI options for non-interactive mode
7
7
  */
8
8
  export declare function furnaceOverrideCommand(projectRoot: string, name?: string, options?: FurnaceOverrideOptions): Promise<void>;
9
+ /**
10
+ * Creates multiple overrides in a single invocation. Each component is validated
11
+ * and created sequentially; failures on one component do not block the rest.
12
+ * @param projectRoot - Root directory of the project
13
+ * @param names - Component tag names to override
14
+ * @param options - CLI options applied to all overrides
15
+ */
16
+ export declare function furnaceBatchOverrideCommand(projectRoot: string, names: string[], options?: FurnaceOverrideOptions): Promise<void>;