@hominis/fireforge 0.10.1 → 0.11.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 +93 -1
- package/README.md +125 -238
- package/dist/bin/fireforge.js +26 -0
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +131 -52
- package/dist/src/commands/bootstrap.js +6 -2
- package/dist/src/commands/build.js +4 -2
- package/dist/src/commands/discard.js +16 -4
- package/dist/src/commands/doctor-furnace.d.ts +8 -0
- package/dist/src/commands/doctor-furnace.js +422 -0
- package/dist/src/commands/doctor.d.ts +115 -0
- package/dist/src/commands/doctor.js +327 -258
- package/dist/src/commands/download.js +16 -1
- package/dist/src/commands/export-all.js +15 -0
- package/dist/src/commands/export-flow.d.ts +91 -0
- package/dist/src/commands/export-flow.js +344 -0
- package/dist/src/commands/export.js +151 -5
- package/dist/src/commands/furnace/apply.d.ts +3 -2
- package/dist/src/commands/furnace/apply.js +169 -36
- package/dist/src/commands/furnace/create.js +162 -52
- package/dist/src/commands/furnace/deploy.js +156 -144
- package/dist/src/commands/furnace/diff.d.ts +8 -4
- package/dist/src/commands/furnace/diff.js +142 -73
- package/dist/src/commands/furnace/index.d.ts +6 -2
- package/dist/src/commands/furnace/index.js +76 -25
- package/dist/src/commands/furnace/init.d.ts +11 -0
- package/dist/src/commands/furnace/init.js +76 -0
- package/dist/src/commands/furnace/list.d.ts +4 -1
- package/dist/src/commands/furnace/list.js +35 -3
- package/dist/src/commands/furnace/override.d.ts +8 -0
- package/dist/src/commands/furnace/override.js +216 -26
- package/dist/src/commands/furnace/preview.js +184 -30
- package/dist/src/commands/furnace/refresh.d.ts +10 -0
- package/dist/src/commands/furnace/refresh.js +268 -0
- package/dist/src/commands/furnace/remove.js +285 -89
- package/dist/src/commands/furnace/rename.d.ts +5 -0
- package/dist/src/commands/furnace/rename.js +308 -0
- package/dist/src/commands/furnace/scan.d.ts +4 -1
- package/dist/src/commands/furnace/scan.js +72 -11
- package/dist/src/commands/furnace/status.js +85 -20
- package/dist/src/commands/furnace/sync.d.ts +12 -0
- package/dist/src/commands/furnace/sync.js +77 -0
- package/dist/src/commands/furnace/validate.d.ts +4 -1
- package/dist/src/commands/furnace/validate.js +99 -3
- package/dist/src/commands/furnace/validation-output.d.ts +24 -1
- package/dist/src/commands/furnace/validation-output.js +93 -1
- package/dist/src/commands/import.js +37 -4
- package/dist/src/commands/lint.js +11 -2
- package/dist/src/commands/manifest.d.ts +39 -0
- package/dist/src/commands/manifest.js +59 -0
- package/dist/src/commands/patch/delete.d.ts +28 -0
- package/dist/src/commands/patch/delete.js +209 -0
- package/dist/src/commands/patch/index.d.ts +17 -0
- package/dist/src/commands/patch/index.js +25 -0
- package/dist/src/commands/patch/reorder.d.ts +30 -0
- package/dist/src/commands/patch/reorder.js +377 -0
- package/dist/src/commands/re-export-files.d.ts +17 -0
- package/dist/src/commands/re-export-files.js +177 -0
- package/dist/src/commands/re-export.js +44 -0
- package/dist/src/commands/rebase/abort.d.ts +1 -1
- package/dist/src/commands/rebase/abort.js +12 -3
- package/dist/src/commands/rebase/confirm.d.ts +3 -3
- package/dist/src/commands/rebase/confirm.js +4 -4
- package/dist/src/commands/rebase/index.js +13 -4
- package/dist/src/commands/reset.js +20 -4
- package/dist/src/commands/run.js +46 -1
- package/dist/src/commands/setup-support.js +5 -5
- package/dist/src/commands/status.js +97 -6
- package/dist/src/commands/test.js +5 -37
- package/dist/src/commands/verify.d.ts +31 -0
- package/dist/src/commands/verify.js +126 -0
- package/dist/src/core/build-prepare.js +40 -16
- package/dist/src/core/destructive.d.ts +96 -0
- package/dist/src/core/destructive.js +137 -0
- package/dist/src/core/diff-hunks.d.ts +73 -0
- package/dist/src/core/diff-hunks.js +268 -0
- package/dist/src/core/firefox.d.ts +1 -1
- package/dist/src/core/firefox.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +89 -6
- package/dist/src/core/furnace-apply-helpers.js +302 -57
- package/dist/src/core/furnace-apply-output.d.ts +16 -0
- package/dist/src/core/furnace-apply-output.js +57 -0
- package/dist/src/core/furnace-apply.d.ts +21 -3
- package/dist/src/core/furnace-apply.js +260 -29
- package/dist/src/core/furnace-checksum-utils.d.ts +4 -0
- package/dist/src/core/furnace-checksum-utils.js +24 -0
- package/dist/src/core/furnace-config.d.ts +28 -1
- package/dist/src/core/furnace-config.js +180 -17
- package/dist/src/core/furnace-constants.d.ts +22 -0
- package/dist/src/core/furnace-constants.js +36 -0
- package/dist/src/core/furnace-graph-utils.d.ts +11 -0
- package/dist/src/core/furnace-graph-utils.js +94 -0
- package/dist/src/core/furnace-operation.d.ts +108 -0
- package/dist/src/core/furnace-operation.js +220 -0
- package/dist/src/core/furnace-refresh.d.ts +20 -0
- package/dist/src/core/furnace-refresh.js +118 -0
- package/dist/src/core/furnace-registration-ast.d.ts +5 -0
- package/dist/src/core/furnace-registration-ast.js +134 -4
- package/dist/src/core/furnace-registration-remove.d.ts +25 -3
- package/dist/src/core/furnace-registration-remove.js +196 -62
- package/dist/src/core/furnace-registration-validate.d.ts +13 -1
- package/dist/src/core/furnace-registration-validate.js +15 -3
- package/dist/src/core/furnace-registration.d.ts +27 -4
- package/dist/src/core/furnace-registration.js +93 -11
- package/dist/src/core/furnace-rollback.d.ts +11 -0
- package/dist/src/core/furnace-rollback.js +78 -7
- package/dist/src/core/furnace-scanner.d.ts +8 -2
- package/dist/src/core/furnace-scanner.js +152 -55
- package/dist/src/core/furnace-stories.js +7 -5
- package/dist/src/core/furnace-validate-accessibility.js +7 -1
- package/dist/src/core/furnace-validate-compatibility.d.ts +1 -1
- package/dist/src/core/furnace-validate-compatibility.js +85 -1
- package/dist/src/core/furnace-validate-helpers.d.ts +4 -0
- package/dist/src/core/furnace-validate-helpers.js +31 -0
- package/dist/src/core/furnace-validate-registration.d.ts +17 -2
- package/dist/src/core/furnace-validate-registration.js +73 -3
- package/dist/src/core/furnace-validate-structure.d.ts +10 -2
- package/dist/src/core/furnace-validate-structure.js +45 -3
- package/dist/src/core/furnace-validate.d.ts +10 -1
- package/dist/src/core/furnace-validate.js +80 -6
- package/dist/src/core/furnace-version-drift.d.ts +55 -0
- package/dist/src/core/furnace-version-drift.js +101 -0
- package/dist/src/core/git-file-ops.d.ts +8 -0
- package/dist/src/core/git-file-ops.js +19 -6
- package/dist/src/core/lint-projection.d.ts +25 -0
- package/dist/src/core/lint-projection.js +44 -0
- package/dist/src/core/mach.d.ts +4 -2
- package/dist/src/core/mach.js +17 -2
- package/dist/src/core/markdown-table.d.ts +104 -0
- package/dist/src/core/markdown-table.js +266 -0
- package/dist/src/core/ownership-table.d.ts +53 -0
- package/dist/src/core/ownership-table.js +144 -0
- package/dist/src/core/patch-apply.d.ts +17 -3
- package/dist/src/core/patch-apply.js +86 -8
- package/dist/src/core/patch-export.d.ts +119 -5
- package/dist/src/core/patch-export.js +183 -25
- package/dist/src/core/patch-lint-cross.d.ts +195 -0
- package/dist/src/core/patch-lint-cross.js +428 -0
- package/dist/src/core/patch-lint-diff.d.ts +33 -0
- package/dist/src/core/patch-lint-diff.js +84 -0
- package/dist/src/core/patch-lint.d.ts +2 -4
- package/dist/src/core/patch-lint.js +12 -50
- package/dist/src/core/patch-lock.js +2 -1
- package/dist/src/core/patch-manifest-io.d.ts +102 -1
- package/dist/src/core/patch-manifest-io.js +270 -2
- package/dist/src/core/patch-manifest-query.d.ts +1 -1
- package/dist/src/core/patch-manifest-query.js +1 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -1
- package/dist/src/core/patch-transform.d.ts +12 -0
- package/dist/src/core/patch-transform.js +21 -7
- package/dist/src/core/token-manager.js +67 -69
- package/dist/src/core/wire-destroy.js +6 -3
- package/dist/src/core/wire-init.js +10 -4
- package/dist/src/core/wire-subscript.js +9 -3
- package/dist/src/core/wire-utils.d.ts +52 -5
- package/dist/src/core/wire-utils.js +69 -6
- package/dist/src/errors/base.d.ts +20 -0
- package/dist/src/errors/base.js +24 -0
- package/dist/src/errors/furnace.js +7 -1
- package/dist/src/errors/rebase.js +6 -1
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +125 -4
- package/dist/src/types/commands/patches.d.ts +11 -1
- package/dist/src/types/config.d.ts +1 -1
- package/dist/src/types/furnace.d.ts +55 -1
- package/dist/src/utils/fs.d.ts +12 -0
- package/dist/src/utils/fs.js +30 -1
- package/dist/src/utils/package-root.d.ts +5 -0
- package/dist/src/utils/package-root.js +12 -0
- package/dist/src/utils/process.js +9 -4
- package/dist/src/utils/validation.d.ts +20 -2
- package/dist/src/utils/validation.js +26 -3
- package/package.json +1 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
5
|
+
import { getFurnacePaths, loadFurnaceConfig, updateFurnaceState, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
6
|
+
import { isComponentSourceFile, resolveFtlDir, tagNameToClassName, } from '../../core/furnace-constants.js';
|
|
7
|
+
import { recordFurnaceRollbackFailure, runFurnaceMutation } from '../../core/furnace-operation.js';
|
|
8
|
+
import { addCustomElementRegistration, addJarMnEntries, removeCustomElementRegistration, removeJarMnEntries, } from '../../core/furnace-registration.js';
|
|
9
|
+
import { CUSTOM_ELEMENT_TAG_PATTERN, CUSTOM_ELEMENT_TAG_RULES, } from '../../core/furnace-registration-validate.js';
|
|
10
|
+
import { createRollbackJournal, restoreRollbackJournalOrThrow, snapshotDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
11
|
+
import { getStoriesDir } from '../../core/furnace-stories.js';
|
|
12
|
+
import { InvalidArgumentError } from '../../errors/base.js';
|
|
13
|
+
import { FurnaceError } from '../../errors/furnace.js';
|
|
14
|
+
import { toError } from '../../utils/errors.js';
|
|
15
|
+
import { copyFile, ensureDir, pathExists, readText, removeDir, removeFile, writeText, } from '../../utils/fs.js';
|
|
16
|
+
import { info, intro, note, outro, warn } from '../../utils/logger.js';
|
|
17
|
+
function updateConfigForCustomRename(config, oldName, newName) {
|
|
18
|
+
const oldConfig = config.custom[oldName];
|
|
19
|
+
if (!oldConfig)
|
|
20
|
+
return;
|
|
21
|
+
config.custom[newName] = {
|
|
22
|
+
...oldConfig,
|
|
23
|
+
targetPath: oldConfig.targetPath.replace(new RegExp(`(^|/)${oldName}$`), `$1${newName}`),
|
|
24
|
+
};
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- idiomatic key removal from config record
|
|
26
|
+
delete config.custom[oldName];
|
|
27
|
+
// Update composes references in other components
|
|
28
|
+
for (const customConfig of Object.values(config.custom)) {
|
|
29
|
+
if (customConfig.composes) {
|
|
30
|
+
customConfig.composes = customConfig.composes.map((ref) => (ref === oldName ? newName : ref));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function updateConfigForOverrideRename(config, oldName, newName) {
|
|
35
|
+
const oldConfig = config.overrides[oldName];
|
|
36
|
+
if (!oldConfig)
|
|
37
|
+
return;
|
|
38
|
+
config.overrides[newName] = { ...oldConfig };
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- idiomatic key removal from config record
|
|
40
|
+
delete config.overrides[oldName];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Derives the test file name for a component, matching the convention used by
|
|
44
|
+
* `furnace create --with-tests`.
|
|
45
|
+
*/
|
|
46
|
+
function deriveTestFileName(componentName, binaryName) {
|
|
47
|
+
const strippedName = componentName.startsWith('moz-') ? componentName.slice(4) : componentName;
|
|
48
|
+
const withoutBinaryPrefix = strippedName.startsWith(binaryName + '-')
|
|
49
|
+
? strippedName.slice(binaryName.length + 1)
|
|
50
|
+
: strippedName;
|
|
51
|
+
const underscored = withoutBinaryPrefix.replace(/-/g, '_');
|
|
52
|
+
return `browser_${binaryName}_${underscored}.js`;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Renames test files created by `furnace create --with-tests` in the engine
|
|
56
|
+
* test directory. Best-effort: failures are logged as warnings but do not
|
|
57
|
+
* block the rename.
|
|
58
|
+
*/
|
|
59
|
+
async function renameTestFiles(engineDir, projectRoot, oldName, newName, journal) {
|
|
60
|
+
let forgeConfig;
|
|
61
|
+
try {
|
|
62
|
+
forgeConfig = await loadConfig(projectRoot);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return; // Cannot determine test paths without config.
|
|
66
|
+
}
|
|
67
|
+
const binaryName = forgeConfig.binaryName;
|
|
68
|
+
const oldTestFileName = deriveTestFileName(oldName, binaryName);
|
|
69
|
+
const newTestFileName = deriveTestFileName(newName, binaryName);
|
|
70
|
+
const testDir = join(engineDir, 'browser/base/content/test', binaryName);
|
|
71
|
+
if (!(await pathExists(testDir)))
|
|
72
|
+
return;
|
|
73
|
+
// Rename the test JS file
|
|
74
|
+
const oldTestPath = join(testDir, oldTestFileName);
|
|
75
|
+
const newTestPath = join(testDir, newTestFileName);
|
|
76
|
+
if (await pathExists(oldTestPath)) {
|
|
77
|
+
try {
|
|
78
|
+
await snapshotFile(journal, oldTestPath);
|
|
79
|
+
const content = await readText(oldTestPath);
|
|
80
|
+
await writeText(newTestPath, content);
|
|
81
|
+
await removeFile(oldTestPath);
|
|
82
|
+
info(`Renamed test file: ${oldTestFileName} → ${newTestFileName}`);
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
warn(`Could not rename test file — ${toError(error).message}. Rename it manually if needed.`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Update browser.toml entry
|
|
89
|
+
const tomlPath = join(testDir, 'browser.toml');
|
|
90
|
+
if (await pathExists(tomlPath)) {
|
|
91
|
+
try {
|
|
92
|
+
const toml = await readText(tomlPath);
|
|
93
|
+
if (toml.includes(`["${oldTestFileName}"]`)) {
|
|
94
|
+
await snapshotFile(journal, tomlPath);
|
|
95
|
+
const updated = toml.replace(`["${oldTestFileName}"]`, `["${newTestFileName}"]`);
|
|
96
|
+
await writeText(tomlPath, updated);
|
|
97
|
+
info(`Updated browser.toml: ${oldTestFileName} → ${newTestFileName}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
warn(`Could not update browser.toml — ${toError(error).message}. Update it manually if needed.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Performs the transactional rename mutation inside a furnace lock.
|
|
107
|
+
*/
|
|
108
|
+
async function performRenameMutations(args) {
|
|
109
|
+
const { projectRoot, oldName, newName, oldDir, newDir, isCustom, componentType, config } = args;
|
|
110
|
+
const oldClassName = tagNameToClassName(oldName);
|
|
111
|
+
const newClassName = tagNameToClassName(newName);
|
|
112
|
+
await runFurnaceMutation(projectRoot, 'rename-rollback', async (ctx) => {
|
|
113
|
+
const journal = createRollbackJournal();
|
|
114
|
+
ctx.registerJournal(journal);
|
|
115
|
+
try {
|
|
116
|
+
await snapshotDir(journal, oldDir);
|
|
117
|
+
await snapshotFile(journal, args.furnaceConfigPath);
|
|
118
|
+
// 1. Create new directory with renamed files and updated content
|
|
119
|
+
await ensureDir(newDir);
|
|
120
|
+
const entries = await readdir(oldDir, { withFileTypes: true });
|
|
121
|
+
for (const entry of entries) {
|
|
122
|
+
if (!entry.isFile())
|
|
123
|
+
continue;
|
|
124
|
+
const oldFileName = entry.name;
|
|
125
|
+
const newFileName = oldFileName.replace(oldName, newName);
|
|
126
|
+
const oldPath = join(oldDir, oldFileName);
|
|
127
|
+
const newPath = join(newDir, newFileName);
|
|
128
|
+
if (isComponentSourceFile(oldFileName)) {
|
|
129
|
+
let content = await readText(oldPath);
|
|
130
|
+
// Use word-boundary-aware patterns so substrings in other
|
|
131
|
+
// identifiers (e.g. "moz-panel" inside "moz-panel-group") are
|
|
132
|
+
// not replaced.
|
|
133
|
+
const tagPattern = new RegExp(`(?<![\\w-])${oldName.replace(/-/g, '\\-')}(?![\\w-])`, 'g');
|
|
134
|
+
const classPattern = new RegExp(`\\b${oldClassName}\\b`, 'g');
|
|
135
|
+
content = content.replace(tagPattern, newName);
|
|
136
|
+
content = content.replace(classPattern, newClassName);
|
|
137
|
+
await writeText(newPath, content);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
await copyFile(oldPath, newPath);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// 2. Update furnace.json
|
|
144
|
+
if (isCustom) {
|
|
145
|
+
updateConfigForCustomRename(config, oldName, newName);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
updateConfigForOverrideRename(config, oldName, newName);
|
|
149
|
+
}
|
|
150
|
+
await writeFurnaceConfig(projectRoot, config);
|
|
151
|
+
// 3. Update engine registrations (custom components only)
|
|
152
|
+
if (isCustom && config.custom[newName]?.register && (await pathExists(args.engineDir))) {
|
|
153
|
+
const ftlDir = resolveFtlDir(config.ftlBasePath);
|
|
154
|
+
await updateEngineRegistrations(args.engineDir, oldName, newName, newDir, ftlDir, journal);
|
|
155
|
+
}
|
|
156
|
+
// 4. Re-key furnace-state.json checksums from old name to new name
|
|
157
|
+
await rekeyStateChecksums(args.projectRoot, componentType, oldName, newName);
|
|
158
|
+
// 5. Remove old directory
|
|
159
|
+
await removeDir(oldDir);
|
|
160
|
+
// 6. Clean up stale Storybook story file for the old name (if it exists
|
|
161
|
+
// from a previous `furnace preview` session). The next preview will
|
|
162
|
+
// regenerate the story under the new name via `syncStories`.
|
|
163
|
+
const oldStoryPath = join(getStoriesDir(args.engineDir), 'furnace', `${oldName}.stories.mjs`);
|
|
164
|
+
if (await pathExists(oldStoryPath)) {
|
|
165
|
+
await snapshotFile(journal, oldStoryPath);
|
|
166
|
+
await removeFile(oldStoryPath);
|
|
167
|
+
info(`Deleted stale story file: ${oldName}.stories.mjs`);
|
|
168
|
+
}
|
|
169
|
+
// 7. Rename test files created by `furnace create --with-tests` (custom only).
|
|
170
|
+
if (isCustom && (await pathExists(args.engineDir))) {
|
|
171
|
+
await renameTestFiles(args.engineDir, projectRoot, oldName, newName, journal);
|
|
172
|
+
}
|
|
173
|
+
info(`Renamed ${componentType} component: ${oldName} → ${newName}`);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
try {
|
|
177
|
+
if (await pathExists(newDir)) {
|
|
178
|
+
await removeDir(newDir);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Best effort cleanup
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
await restoreRollbackJournalOrThrow(journal, `Failed to rename component "${oldName}" to "${newName}"`);
|
|
186
|
+
}
|
|
187
|
+
catch (rollbackError) {
|
|
188
|
+
await recordFurnaceRollbackFailure(projectRoot, 'rename-rollback', toError(rollbackError).message);
|
|
189
|
+
throw rollbackError;
|
|
190
|
+
}
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Re-keys checksum entries in furnace-state.json from the old component name
|
|
197
|
+
* to the new name so that `doctor` doesn't flag stale entries and the next
|
|
198
|
+
* `apply` can correctly detect whether the renamed component has changed.
|
|
199
|
+
*/
|
|
200
|
+
async function rekeyStateChecksums(projectRoot, componentType, oldName, newName) {
|
|
201
|
+
const oldPrefix = `${componentType}/${oldName}/`;
|
|
202
|
+
const newPrefix = `${componentType}/${newName}/`;
|
|
203
|
+
await updateFurnaceState(projectRoot, (state) => {
|
|
204
|
+
const result = { ...state };
|
|
205
|
+
for (const field of ['appliedChecksums', 'engineChecksums']) {
|
|
206
|
+
const checksums = state[field];
|
|
207
|
+
if (!checksums)
|
|
208
|
+
continue;
|
|
209
|
+
const updated = {};
|
|
210
|
+
for (const [key, value] of Object.entries(checksums)) {
|
|
211
|
+
if (key.startsWith(oldPrefix)) {
|
|
212
|
+
updated[newPrefix + key.slice(oldPrefix.length)] = value;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
updated[key] = value;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
result[field] = updated;
|
|
219
|
+
}
|
|
220
|
+
return result;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
async function updateEngineRegistrations(engineDir, oldName, newName, newDir, ftlDir, journal) {
|
|
224
|
+
const customElementsPath = join(engineDir, 'toolkit/content/customElements.js');
|
|
225
|
+
const jarMnPath = join(engineDir, 'toolkit/content/jar.mn');
|
|
226
|
+
if (await pathExists(customElementsPath)) {
|
|
227
|
+
await snapshotFile(journal, customElementsPath);
|
|
228
|
+
await removeCustomElementRegistration(engineDir, oldName);
|
|
229
|
+
await addCustomElementRegistration(engineDir, newName, `chrome://global/content/elements/${newName}.mjs`);
|
|
230
|
+
}
|
|
231
|
+
if (await pathExists(jarMnPath)) {
|
|
232
|
+
await snapshotFile(journal, jarMnPath);
|
|
233
|
+
await removeJarMnEntries(engineDir, oldName);
|
|
234
|
+
const files = (await readdir(newDir, { withFileTypes: true }))
|
|
235
|
+
.filter((e) => e.isFile() && (e.name.endsWith('.mjs') || e.name.endsWith('.css')))
|
|
236
|
+
.map((e) => e.name);
|
|
237
|
+
if (files.length > 0) {
|
|
238
|
+
await addJarMnEntries(engineDir, newName, files);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Rename FTL localization files in the engine locale directory
|
|
242
|
+
const ftlDirPath = join(engineDir, ftlDir);
|
|
243
|
+
const oldFtlPath = join(ftlDirPath, `${oldName}.ftl`);
|
|
244
|
+
const newFtlPath = join(ftlDirPath, `${newName}.ftl`);
|
|
245
|
+
if (await pathExists(oldFtlPath)) {
|
|
246
|
+
await snapshotFile(journal, oldFtlPath);
|
|
247
|
+
const ftlContent = await readText(oldFtlPath);
|
|
248
|
+
await writeText(newFtlPath, ftlContent);
|
|
249
|
+
await removeFile(oldFtlPath);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Renames a custom or override component atomically: updates directory name,
|
|
254
|
+
* file names, file contents, furnace.json, and engine registrations.
|
|
255
|
+
*/
|
|
256
|
+
export async function furnaceRenameCommand(projectRoot, oldName, newName) {
|
|
257
|
+
intro('Furnace Rename');
|
|
258
|
+
if (!CUSTOM_ELEMENT_TAG_PATTERN.test(oldName)) {
|
|
259
|
+
throw new InvalidArgumentError(`Invalid source name "${oldName}": ${CUSTOM_ELEMENT_TAG_RULES}`, 'old-name');
|
|
260
|
+
}
|
|
261
|
+
if (!CUSTOM_ELEMENT_TAG_PATTERN.test(newName)) {
|
|
262
|
+
throw new InvalidArgumentError(`Invalid target name "${newName}": ${CUSTOM_ELEMENT_TAG_RULES}`, 'new-name');
|
|
263
|
+
}
|
|
264
|
+
if (oldName === newName) {
|
|
265
|
+
throw new InvalidArgumentError('Source and target names are identical.', 'new-name');
|
|
266
|
+
}
|
|
267
|
+
const config = await loadFurnaceConfig(projectRoot);
|
|
268
|
+
const furnacePaths = getFurnacePaths(projectRoot);
|
|
269
|
+
const paths = getProjectPaths(projectRoot);
|
|
270
|
+
const isCustom = oldName in config.custom;
|
|
271
|
+
const isOverride = oldName in config.overrides;
|
|
272
|
+
if (!isCustom && !isOverride) {
|
|
273
|
+
throw new FurnaceError(`Component "${oldName}" not found in furnace.json. Only custom and override components can be renamed.`, oldName);
|
|
274
|
+
}
|
|
275
|
+
if (newName in config.custom || newName in config.overrides || config.stock.includes(newName)) {
|
|
276
|
+
throw new FurnaceError(`A component named "${newName}" already exists in furnace.json.`, newName);
|
|
277
|
+
}
|
|
278
|
+
const componentType = isCustom ? 'custom' : 'override';
|
|
279
|
+
const baseDir = isCustom ? furnacePaths.customDir : furnacePaths.overridesDir;
|
|
280
|
+
const oldDir = join(baseDir, oldName);
|
|
281
|
+
const newDir = join(baseDir, newName);
|
|
282
|
+
if (!(await pathExists(oldDir))) {
|
|
283
|
+
throw new FurnaceError(`Component directory not found: components/${componentType}s/${oldName}`, oldName);
|
|
284
|
+
}
|
|
285
|
+
if (await pathExists(newDir)) {
|
|
286
|
+
throw new FurnaceError(`Target directory already exists: components/${componentType}s/${newName}`, newName);
|
|
287
|
+
}
|
|
288
|
+
await performRenameMutations({
|
|
289
|
+
projectRoot,
|
|
290
|
+
oldName,
|
|
291
|
+
newName,
|
|
292
|
+
oldDir,
|
|
293
|
+
newDir,
|
|
294
|
+
isCustom,
|
|
295
|
+
componentType,
|
|
296
|
+
config,
|
|
297
|
+
furnaceConfigPath: furnacePaths.furnaceConfig,
|
|
298
|
+
engineDir: paths.engine,
|
|
299
|
+
});
|
|
300
|
+
note(`Component renamed: ${oldName} → ${newName}\n\n` +
|
|
301
|
+
`Directory: components/${componentType}s/${newName}/\n\n` +
|
|
302
|
+
'Next steps:\n' +
|
|
303
|
+
' 1. Review the renamed files for any remaining references\n' +
|
|
304
|
+
' 2. Run "fireforge furnace validate" to verify\n' +
|
|
305
|
+
' 3. Run "fireforge furnace apply" to update the engine', newName);
|
|
306
|
+
outro('Rename complete');
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=rename.js.map
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runs the furnace scan command to discover MozLitElement components.
|
|
3
3
|
* @param projectRoot - Root directory of the project
|
|
4
|
+
* @param options - Scan options
|
|
4
5
|
*/
|
|
5
|
-
export declare function furnaceScanCommand(projectRoot: string
|
|
6
|
+
export declare function furnaceScanCommand(projectRoot: string, options?: {
|
|
7
|
+
deep?: boolean;
|
|
8
|
+
}): Promise<void>;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
-
import { confirm, multiselect } from '@clack/prompts';
|
|
2
|
+
import { confirm, multiselect, select } from '@clack/prompts';
|
|
3
3
|
import { getProjectPaths } from '../../core/config.js';
|
|
4
|
-
import { ensureFurnaceConfig, furnaceConfigExists, loadFurnaceConfig, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
5
|
-
import {
|
|
4
|
+
import { ensureFurnaceConfig, furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
5
|
+
import { recordFurnaceRollbackFailure, runFurnaceMutation } from '../../core/furnace-operation.js';
|
|
6
|
+
import { createRollbackJournal, restoreRollbackJournalOrThrow, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
7
|
+
import { DEEP_SCAN_PATHS, scanWidgetsDirectory } from '../../core/furnace-scanner.js';
|
|
6
8
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
9
|
+
import { toError } from '../../utils/errors.js';
|
|
7
10
|
import { pathExists } from '../../utils/fs.js';
|
|
8
11
|
import { cancel, info, intro, isCancel, note, outro, spinner, success, } from '../../utils/logger.js';
|
|
12
|
+
import { furnaceOverrideCommand } from './override.js';
|
|
9
13
|
/**
|
|
10
14
|
* Prompts the user to add newly discovered stock components to furnace.json.
|
|
11
15
|
* @param components - Components discovered in the engine scan
|
|
@@ -41,24 +45,81 @@ async function promptAddComponents(components, tracked, projectRoot) {
|
|
|
41
45
|
outro('Scan complete');
|
|
42
46
|
return;
|
|
43
47
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
48
|
+
// Wrap the furnace.json mutation in the standard furnace lifecycle so the
|
|
49
|
+
// write goes through the furnace-wide lock and is visible to the global
|
|
50
|
+
// SIGINT/SIGTERM rollback pathway. The journal snapshots furnace.json
|
|
51
|
+
// *before* `ensureFurnaceConfig` runs, so a failed run after the file is
|
|
52
|
+
// auto-created cleans up after itself instead of leaving an unwanted
|
|
53
|
+
// default config behind.
|
|
54
|
+
await runFurnaceMutation(projectRoot, 'scan-rollback', async (ctx) => {
|
|
55
|
+
const journal = createRollbackJournal();
|
|
56
|
+
ctx.registerJournal(journal);
|
|
57
|
+
const furnacePaths = getFurnacePaths(projectRoot);
|
|
58
|
+
await snapshotFile(journal, furnacePaths.furnaceConfig);
|
|
59
|
+
try {
|
|
60
|
+
const config = await ensureFurnaceConfig(projectRoot);
|
|
61
|
+
const toAdd = selected.filter((s) => !config.stock.includes(s));
|
|
62
|
+
config.stock.push(...toAdd);
|
|
63
|
+
await writeFurnaceConfig(projectRoot, config);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
try {
|
|
67
|
+
await restoreRollbackJournalOrThrow(journal, 'Failed to update furnace.json during scan');
|
|
68
|
+
}
|
|
69
|
+
catch (rollbackError) {
|
|
70
|
+
await recordFurnaceRollbackFailure(projectRoot, 'scan-rollback', toError(rollbackError).message);
|
|
71
|
+
throw rollbackError;
|
|
72
|
+
}
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
const addedNames = selected;
|
|
77
|
+
success(`Added ${addedNames.length} component${addedNames.length === 1 ? '' : 's'} to furnace.json`);
|
|
78
|
+
// Offer to immediately override one of the just-added stock components.
|
|
79
|
+
const shouldOverride = await confirm({
|
|
80
|
+
message: 'Override any of the newly added components?',
|
|
81
|
+
});
|
|
82
|
+
if (isCancel(shouldOverride) || !shouldOverride) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const overrideTarget = await select({
|
|
86
|
+
message: 'Select a component to override',
|
|
87
|
+
options: addedNames.map((name) => ({ value: name, label: name })),
|
|
88
|
+
});
|
|
89
|
+
if (isCancel(overrideTarget)) {
|
|
90
|
+
cancel('Cancelled');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
await furnaceOverrideCommand(projectRoot, overrideTarget);
|
|
49
94
|
}
|
|
50
95
|
/**
|
|
51
96
|
* Runs the furnace scan command to discover MozLitElement components.
|
|
52
97
|
* @param projectRoot - Root directory of the project
|
|
98
|
+
* @param options - Scan options
|
|
53
99
|
*/
|
|
54
|
-
export async function furnaceScanCommand(projectRoot) {
|
|
55
|
-
intro('Furnace Scan');
|
|
100
|
+
export async function furnaceScanCommand(projectRoot, options = {}) {
|
|
101
|
+
intro(options.deep ? 'Furnace Scan (deep)' : 'Furnace Scan');
|
|
56
102
|
const paths = getProjectPaths(projectRoot);
|
|
57
103
|
if (!(await pathExists(paths.engine))) {
|
|
58
104
|
throw new FurnaceError('Engine directory not found. Run "fireforge download" first.');
|
|
59
105
|
}
|
|
106
|
+
// Load scan paths from config if available, merge with deep paths if requested
|
|
107
|
+
const extraScanPaths = [];
|
|
108
|
+
if (await furnaceConfigExists(projectRoot)) {
|
|
109
|
+
const preConfig = await loadFurnaceConfig(projectRoot);
|
|
110
|
+
if (preConfig.scanPaths) {
|
|
111
|
+
extraScanPaths.push(...preConfig.scanPaths);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (options.deep) {
|
|
115
|
+
for (const deepPath of DEEP_SCAN_PATHS) {
|
|
116
|
+
if (!extraScanPaths.includes(deepPath)) {
|
|
117
|
+
extraScanPaths.push(deepPath);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
60
121
|
const s = spinner('Scanning engine for components...');
|
|
61
|
-
const components = await scanWidgetsDirectory(paths.engine);
|
|
122
|
+
const components = await scanWidgetsDirectory(paths.engine, undefined, extraScanPaths.length > 0 ? extraScanPaths : undefined);
|
|
62
123
|
s.stop(`Found ${components.length} component${components.length === 1 ? '' : 's'}`);
|
|
63
124
|
// Build tracking info from furnace.json if it exists
|
|
64
125
|
const tracked = new Map();
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { getProjectPaths } from '../../core/config.js';
|
|
4
|
-
import { extractComponentChecksums, hasComponentChanged } from '../../core/furnace-apply.js';
|
|
3
|
+
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
4
|
+
import { extractComponentChecksums, hasComponentChanged, hasCustomEngineDrift, hasOverrideEngineDrift, } from '../../core/furnace-apply.js';
|
|
5
5
|
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from '../../core/furnace-config.js';
|
|
6
|
+
import { resolveFtlDir } from '../../core/furnace-constants.js';
|
|
6
7
|
import { checkRegistrationConsistency } from '../../core/furnace-validate-checks.js';
|
|
8
|
+
import { findOverrideBaseVersionDrift, formatOverrideBaseVersionDriftWarning, } from '../../core/furnace-version-drift.js';
|
|
7
9
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
8
10
|
import { pathExists } from '../../utils/fs.js';
|
|
9
11
|
import { info, intro, note, outro, warn } from '../../utils/logger.js';
|
|
@@ -13,7 +15,7 @@ import { info, intro, note, outro, warn } from '../../utils/logger.js';
|
|
|
13
15
|
* @param config - Loaded Furnace configuration
|
|
14
16
|
* @param projectRoot - Root directory of the project
|
|
15
17
|
*/
|
|
16
|
-
async function showDetailedComponentStatus(name, config, projectRoot) {
|
|
18
|
+
async function showDetailedComponentStatus(name, config, state, projectRoot, paths, furnacePaths, ftlDir) {
|
|
17
19
|
const customConfig = config.custom[name];
|
|
18
20
|
const overrideConfig = config.overrides[name];
|
|
19
21
|
if (!customConfig && !overrideConfig && !config.stock.includes(name)) {
|
|
@@ -23,20 +25,51 @@ async function showDetailedComponentStatus(name, config, projectRoot) {
|
|
|
23
25
|
info(`"${name}" is an override component (${overrideConfig.type}).`);
|
|
24
26
|
info(`Base path: ${overrideConfig.basePath}`);
|
|
25
27
|
info(`Base version: ${overrideConfig.baseVersion}`);
|
|
26
|
-
|
|
28
|
+
// baseVersion drift is advisory but reported here alongside the other
|
|
29
|
+
// override metadata so the operator sees the warning before drilling
|
|
30
|
+
// into registration drift or file diff.
|
|
31
|
+
const forgeConfig = await loadConfig(projectRoot);
|
|
32
|
+
const scopedDrift = findOverrideBaseVersionDrift(config, forgeConfig.firefox.version).filter((entry) => entry.name === name);
|
|
33
|
+
for (const entry of scopedDrift) {
|
|
34
|
+
warn(formatOverrideBaseVersionDriftWarning(entry));
|
|
35
|
+
}
|
|
36
|
+
const overrideDir = join(furnacePaths.overridesDir, name);
|
|
37
|
+
const sourceExists = await pathExists(overrideDir);
|
|
38
|
+
const lines = [`${sourceExists ? '\u2713' : '\u2717'} Override directory exists`];
|
|
39
|
+
if (!sourceExists) {
|
|
40
|
+
lines.push('\u2717 Workspace status unavailable (override directory missing)');
|
|
41
|
+
lines.push('\u2717 Engine comparison unavailable (override directory missing)');
|
|
42
|
+
note(lines.join('\n'), `${name} Override Status`);
|
|
43
|
+
outro('Status complete');
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const previous = extractComponentChecksums(state.appliedChecksums, 'override', name);
|
|
47
|
+
const workspaceChanged = await hasComponentChanged(overrideDir, previous);
|
|
48
|
+
lines.push(`${workspaceChanged ? '\u2717' : '\u2713'} Workspace unchanged since last apply`);
|
|
49
|
+
const engineExists = await pathExists(paths.engine);
|
|
50
|
+
if (!engineExists) {
|
|
51
|
+
lines.push('\u2717 Engine comparison unavailable (engine directory missing)');
|
|
52
|
+
note(lines.join('\n'), `${name} Override Status`);
|
|
53
|
+
outro('Status complete');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const engineDrifted = await hasOverrideEngineDrift(paths.engine, overrideDir, overrideConfig, ftlDir);
|
|
57
|
+
lines.push(`${engineDrifted ? '\u2717' : '\u2713'} Engine matches override workspace`);
|
|
58
|
+
note(lines.join('\n'), `${name} Override Status`);
|
|
59
|
+
outro('Status complete');
|
|
27
60
|
return;
|
|
28
61
|
}
|
|
29
62
|
if (config.stock.includes(name)) {
|
|
30
63
|
info(`"${name}" is a stock component. No local registration to check.`);
|
|
31
|
-
outro('');
|
|
64
|
+
outro('Status complete');
|
|
32
65
|
return;
|
|
33
66
|
}
|
|
34
67
|
if (!customConfig) {
|
|
35
|
-
outro('');
|
|
68
|
+
outro('Status complete');
|
|
36
69
|
return;
|
|
37
70
|
}
|
|
38
71
|
// Custom component — run registration consistency check
|
|
39
|
-
const status = await checkRegistrationConsistency(projectRoot, name, customConfig);
|
|
72
|
+
const status = await checkRegistrationConsistency(projectRoot, name, customConfig, ftlDir);
|
|
40
73
|
const lines = [];
|
|
41
74
|
const check = (ok, label) => {
|
|
42
75
|
lines.push(`${ok ? '\u2713' : '\u2717'} ${label}`);
|
|
@@ -55,7 +88,7 @@ async function showDetailedComponentStatus(name, config, projectRoot) {
|
|
|
55
88
|
lines.push(`Missing in engine: ${status.missingTargetFiles.join(', ')}`);
|
|
56
89
|
}
|
|
57
90
|
note(lines.join('\n'), `${name} Registration Status`);
|
|
58
|
-
outro('');
|
|
91
|
+
outro('Status complete');
|
|
59
92
|
}
|
|
60
93
|
/**
|
|
61
94
|
* Runs the furnace status command to show an overview of Furnace state.
|
|
@@ -74,10 +107,18 @@ export async function furnaceStatusCommand(projectRoot, name) {
|
|
|
74
107
|
const state = await loadFurnaceState(projectRoot);
|
|
75
108
|
const paths = getProjectPaths(projectRoot);
|
|
76
109
|
const furnacePaths = getFurnacePaths(projectRoot);
|
|
110
|
+
const ftlDir = resolveFtlDir(config.ftlBasePath);
|
|
77
111
|
if (name) {
|
|
78
|
-
await showDetailedComponentStatus(name, config, projectRoot);
|
|
112
|
+
await showDetailedComponentStatus(name, config, state, projectRoot, paths, furnacePaths, ftlDir);
|
|
79
113
|
return;
|
|
80
114
|
}
|
|
115
|
+
// Surface a pendingRepair marker before the normal summary so it cannot
|
|
116
|
+
// be missed. The marker means the last mutation could not roll back
|
|
117
|
+
// cleanly, so the engine and workspace may have drifted in ways apply
|
|
118
|
+
// cannot detect from checksums alone — doctor is the right next step.
|
|
119
|
+
if (state.pendingRepair) {
|
|
120
|
+
warn(`Furnace is in pending-repair state from ${state.pendingRepair.operation} (${state.pendingRepair.timestamp}): ${state.pendingRepair.reason}. Run \`fireforge doctor --repair-furnace\` to reconcile.`);
|
|
121
|
+
}
|
|
81
122
|
// --- Overview mode ---
|
|
82
123
|
const overrideCount = Object.keys(config.overrides).length;
|
|
83
124
|
const customCount = Object.keys(config.custom).length;
|
|
@@ -103,35 +144,59 @@ export async function furnaceStatusCommand(projectRoot, name) {
|
|
|
103
144
|
// Last apply
|
|
104
145
|
lines.push(`Last apply: ${state.lastApply ?? 'never'}`);
|
|
105
146
|
note(lines.join('\n'), 'Furnace Status');
|
|
106
|
-
//
|
|
147
|
+
// Surface override baseVersion drift from the project config. This check
|
|
148
|
+
// is cheap (no I/O besides the already-loaded fireforge.json) and catches
|
|
149
|
+
// the single most common silent-drift case: Firefox bumped, overrides
|
|
150
|
+
// still point at the old version. Advisory only — status never fails.
|
|
151
|
+
const forgeConfig = await loadConfig(projectRoot);
|
|
152
|
+
for (const entry of findOverrideBaseVersionDrift(config, forgeConfig.firefox.version)) {
|
|
153
|
+
warn(formatOverrideBaseVersionDriftWarning(entry));
|
|
154
|
+
}
|
|
155
|
+
// Check for both workspace changes (developer edits) and engine drift
|
|
156
|
+
// (reset/download/manual edits). The two have different remediation
|
|
157
|
+
// hints, so report them separately rather than collapsing into a single
|
|
158
|
+
// "something is off" message.
|
|
107
159
|
if (await pathExists(paths.engine)) {
|
|
108
|
-
let
|
|
109
|
-
|
|
160
|
+
let workspaceChanged = false;
|
|
161
|
+
let engineDrifted = false;
|
|
162
|
+
for (const [oName, overrideConfig] of Object.entries(config.overrides)) {
|
|
110
163
|
const componentDir = join(furnacePaths.overridesDir, oName);
|
|
111
164
|
if (!(await pathExists(componentDir)))
|
|
112
165
|
continue;
|
|
113
166
|
const previous = extractComponentChecksums(state.appliedChecksums, 'override', oName);
|
|
114
167
|
if (await hasComponentChanged(componentDir, previous)) {
|
|
115
|
-
|
|
116
|
-
|
|
168
|
+
workspaceChanged = true;
|
|
169
|
+
}
|
|
170
|
+
else if (await hasOverrideEngineDrift(paths.engine, componentDir, overrideConfig, ftlDir)) {
|
|
171
|
+
engineDrifted = true;
|
|
117
172
|
}
|
|
173
|
+
if (workspaceChanged && engineDrifted)
|
|
174
|
+
break;
|
|
118
175
|
}
|
|
119
|
-
if (!
|
|
120
|
-
for (const cName of Object.
|
|
176
|
+
if (!(workspaceChanged && engineDrifted)) {
|
|
177
|
+
for (const [cName, customConfig] of Object.entries(config.custom)) {
|
|
121
178
|
const componentDir = join(furnacePaths.customDir, cName);
|
|
122
179
|
if (!(await pathExists(componentDir)))
|
|
123
180
|
continue;
|
|
124
181
|
const previous = extractComponentChecksums(state.appliedChecksums, 'custom', cName);
|
|
125
182
|
if (await hasComponentChanged(componentDir, previous)) {
|
|
126
|
-
|
|
127
|
-
break;
|
|
183
|
+
workspaceChanged = true;
|
|
128
184
|
}
|
|
185
|
+
else if (await hasCustomEngineDrift(projectRoot, cName, componentDir, customConfig, ftlDir)) {
|
|
186
|
+
engineDrifted = true;
|
|
187
|
+
}
|
|
188
|
+
if (workspaceChanged && engineDrifted)
|
|
189
|
+
break;
|
|
129
190
|
}
|
|
130
191
|
}
|
|
131
|
-
if (
|
|
192
|
+
if (workspaceChanged) {
|
|
132
193
|
warn('Components have been modified since last apply. Run `fireforge build` or `fireforge furnace apply`.');
|
|
133
194
|
}
|
|
195
|
+
if (engineDrifted) {
|
|
196
|
+
warn('Engine drift detected since last apply (reset/download/manual edit). Run `fireforge furnace apply` to re-deploy.');
|
|
197
|
+
}
|
|
134
198
|
}
|
|
135
|
-
|
|
199
|
+
info('Tip: run `furnace status <name>` for detailed component info, or `furnace --help` for all subcommands.');
|
|
200
|
+
outro('Status complete');
|
|
136
201
|
}
|
|
137
202
|
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FurnaceSyncOptions } from '../../types/commands/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Runs the furnace sync command: detects overrides with baseVersion drift,
|
|
4
|
+
* refreshes them (three-way merge), and re-applies all components.
|
|
5
|
+
*
|
|
6
|
+
* This is the recommended single command to run after `fireforge download`
|
|
7
|
+
* updates the Firefox source.
|
|
8
|
+
*
|
|
9
|
+
* @param projectRoot - Root directory of the project
|
|
10
|
+
* @param options - Sync options
|
|
11
|
+
*/
|
|
12
|
+
export declare function furnaceSyncCommand(projectRoot: string, options?: FurnaceSyncOptions): Promise<void>;
|