@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
|
@@ -3,13 +3,57 @@ import { readdir, unlink } from 'node:fs/promises';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { confirm } from '@clack/prompts';
|
|
5
5
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
6
|
-
import {
|
|
6
|
+
import { extractComponentChecksums, getOverrideEngineTargetPath, isOverrideCopyCandidate, restoreOverrideFileToBaseline, } from '../../core/furnace-apply-helpers.js';
|
|
7
|
+
import { getFurnacePaths, loadFurnaceConfig, loadFurnaceState, updateFurnaceState, writeFurnaceConfig, } from '../../core/furnace-config.js';
|
|
8
|
+
import { resolveFtlDir } from '../../core/furnace-constants.js';
|
|
9
|
+
import { recordFurnaceRollbackFailure, runFurnaceMutation } from '../../core/furnace-operation.js';
|
|
7
10
|
import { removeCustomElementRegistration, removeJarMnEntries, } from '../../core/furnace-registration.js';
|
|
11
|
+
import { createRollbackJournal, restoreRollbackJournalOrThrow, snapshotDir, snapshotFile, } from '../../core/furnace-rollback.js';
|
|
12
|
+
import { isGitRepository } from '../../core/git.js';
|
|
8
13
|
import { deregisterTestManifest } from '../../core/manifest-register.js';
|
|
9
14
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
10
15
|
import { toError } from '../../utils/errors.js';
|
|
11
|
-
import { pathExists, readText, removeDir, writeText } from '../../utils/fs.js';
|
|
16
|
+
import { pathExists, readText, removeDir, removeFile, writeText } from '../../utils/fs.js';
|
|
12
17
|
import { cancel, info, intro, isCancel, outro, warn } from '../../utils/logger.js';
|
|
18
|
+
/**
|
|
19
|
+
* Removes an entire TOML section (header + body lines) for a given test file.
|
|
20
|
+
* Matches from `["filename"]` up to the next section header `[` or end-of-file,
|
|
21
|
+
* consuming the section's metadata keys and surrounding blank lines. This is
|
|
22
|
+
* more robust than a single-line regex that only removes the header.
|
|
23
|
+
*/
|
|
24
|
+
function removeTomlSection(toml, testFileName) {
|
|
25
|
+
const lines = toml.split('\n');
|
|
26
|
+
const header = `["${testFileName}"]`;
|
|
27
|
+
const result = [];
|
|
28
|
+
let i = 0;
|
|
29
|
+
while (i < lines.length) {
|
|
30
|
+
if (lines[i]?.trim() === header) {
|
|
31
|
+
// Skip the header line
|
|
32
|
+
i++;
|
|
33
|
+
// Skip all body lines until the next section header or EOF
|
|
34
|
+
while (i < lines.length && !/^\s*\[/.test(lines[i] ?? '')) {
|
|
35
|
+
i++;
|
|
36
|
+
}
|
|
37
|
+
// Collapse any double blank line left behind
|
|
38
|
+
while (result.length > 0 && result[result.length - 1]?.trim() === '') {
|
|
39
|
+
result.pop();
|
|
40
|
+
}
|
|
41
|
+
// Re-add a single blank separator if the next line is another section
|
|
42
|
+
if (i < lines.length && result.length > 0) {
|
|
43
|
+
result.push('');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
result.push(lines[i] ?? '');
|
|
48
|
+
i++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Trim trailing blank lines and ensure single trailing newline
|
|
52
|
+
while (result.length > 0 && result[result.length - 1]?.trim() === '') {
|
|
53
|
+
result.pop();
|
|
54
|
+
}
|
|
55
|
+
return result.join('\n') + '\n';
|
|
56
|
+
}
|
|
13
57
|
/**
|
|
14
58
|
* Finds which section a component belongs to in the furnace config.
|
|
15
59
|
* @returns The component type, or undefined if not found
|
|
@@ -23,53 +67,150 @@ function findComponentType(config, name) {
|
|
|
23
67
|
return 'custom';
|
|
24
68
|
return undefined;
|
|
25
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Restores every override-deployed file in `engine/` to its pristine HEAD
|
|
72
|
+
* state, inverting what `applyOverrideComponent` would have written. Files that
|
|
73
|
+
* existed in HEAD are restored via `git restore`; files the override
|
|
74
|
+
* introduced (not in HEAD) are deleted outright.
|
|
75
|
+
*
|
|
76
|
+
* The restore set is the **union** of (a) files currently in the override
|
|
77
|
+
* workspace directory and (b) filenames recorded in `previousChecksumKeys`
|
|
78
|
+
* — i.e. files we know we deployed last time, even if the developer has
|
|
79
|
+
* since deleted them from the workspace. Without (b), a workspace deletion
|
|
80
|
+
* leaves an orphaned engine copy that `furnace remove` would never see.
|
|
81
|
+
*
|
|
82
|
+
* Every touched engine file is snapshotted into the rollback journal before
|
|
83
|
+
* mutation so a mid-remove failure still rolls the engine back to its
|
|
84
|
+
* pre-command state.
|
|
85
|
+
*/
|
|
86
|
+
async function restoreOverrideEngineFiles(engineDir, overrideDir, overrideConfig, previousChecksumKeys, ftlDir, journal) {
|
|
87
|
+
// Engine-as-git is a hard precondition for restoration: git HEAD is the only
|
|
88
|
+
// honest oracle for "what was there before the override". If the engine is
|
|
89
|
+
// not a git repo we refuse rather than silently leaving files behind — the
|
|
90
|
+
// previous warn-and-continue behaviour is exactly what this fix removes.
|
|
91
|
+
if (!(await isGitRepository(engineDir))) {
|
|
92
|
+
throw new FurnaceError('Cannot restore override files: engine is not a git repository. Run "fireforge download" to initialise it.');
|
|
93
|
+
}
|
|
94
|
+
// Build the union of "files we still see on disk" and "files state.json
|
|
95
|
+
// claims we deployed". The state set is the only authority for files that
|
|
96
|
+
// were deployed and later deleted from the workspace; the workspace set is
|
|
97
|
+
// the only authority for files added since last apply that have not yet
|
|
98
|
+
// been recorded in state. We need both.
|
|
99
|
+
const fileSet = new Set();
|
|
100
|
+
if (await pathExists(overrideDir)) {
|
|
101
|
+
const entries = await readdir(overrideDir, { withFileTypes: true });
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (!entry.isFile())
|
|
104
|
+
continue;
|
|
105
|
+
if (!isOverrideCopyCandidate(entry.name, overrideConfig.type))
|
|
106
|
+
continue;
|
|
107
|
+
fileSet.add(entry.name);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const key of previousChecksumKeys) {
|
|
111
|
+
fileSet.add(key);
|
|
112
|
+
}
|
|
113
|
+
let restored = 0;
|
|
114
|
+
let removed = 0;
|
|
115
|
+
for (const fileName of fileSet) {
|
|
116
|
+
const enginePath = getOverrideEngineTargetPath(engineDir, overrideConfig, fileName, ftlDir);
|
|
117
|
+
const action = await restoreOverrideFileToBaseline(engineDir, enginePath, journal);
|
|
118
|
+
if (action === 'restored')
|
|
119
|
+
restored += 1;
|
|
120
|
+
else if (action === 'removed')
|
|
121
|
+
removed += 1;
|
|
122
|
+
}
|
|
123
|
+
return { restored, removed };
|
|
124
|
+
}
|
|
26
125
|
/**
|
|
27
126
|
* Removes generated browser mochitest files associated with a custom component.
|
|
28
127
|
* @param name - Custom component tag name
|
|
29
128
|
* @param projectRoot - Root directory of the project
|
|
129
|
+
* @param journal - Rollback journal that snapshots files before deletion
|
|
130
|
+
*
|
|
131
|
+
* The function preserves its original warn-and-continue contract: any failure
|
|
132
|
+
* during cleanup is reported via warn() rather than thrown. Snapshots taken
|
|
133
|
+
* before a failed step are still recorded so a later rollback (triggered by
|
|
134
|
+
* an error elsewhere in the command) can restore whatever was deleted before
|
|
135
|
+
* the failure.
|
|
30
136
|
*/
|
|
31
|
-
async function cleanupCustomTestFiles(name, projectRoot) {
|
|
137
|
+
async function cleanupCustomTestFiles(name, projectRoot, journal) {
|
|
138
|
+
let forgeConfig;
|
|
32
139
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
140
|
+
forgeConfig = await loadConfig(projectRoot);
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
warn(`Could not load config for test cleanup — ${toError(error).message}. Remove test files manually if needed.`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const paths = getProjectPaths(projectRoot);
|
|
147
|
+
const binaryName = forgeConfig.binaryName;
|
|
148
|
+
const strippedName = name.startsWith('moz-') ? name.slice(4) : name;
|
|
149
|
+
const withoutBinaryPrefix = strippedName.startsWith(binaryName + '-')
|
|
150
|
+
? strippedName.slice(binaryName.length + 1)
|
|
151
|
+
: strippedName;
|
|
152
|
+
const underscored = withoutBinaryPrefix.replace(/-/g, '_');
|
|
153
|
+
const testFileName = `browser_${binaryName}_${underscored}.js`;
|
|
154
|
+
const testDir = join(paths.engine, 'browser/base/content/test', binaryName);
|
|
155
|
+
if (!(await pathExists(testDir)))
|
|
156
|
+
return;
|
|
157
|
+
// Step 1: Delete the test file itself
|
|
158
|
+
try {
|
|
159
|
+
const testFilePath = join(testDir, testFileName);
|
|
160
|
+
if (await pathExists(testFilePath)) {
|
|
161
|
+
await snapshotFile(journal, testFilePath);
|
|
162
|
+
await unlink(testFilePath);
|
|
163
|
+
info(`Deleted test file: ${testFileName}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
warn(`Could not delete test file ${testFileName} — ${toError(error).message}. Remove it manually if needed.`);
|
|
168
|
+
}
|
|
169
|
+
// Step 2: Remove the test entry from browser.toml
|
|
170
|
+
try {
|
|
171
|
+
const tomlPath = join(testDir, 'browser.toml');
|
|
172
|
+
if (await pathExists(tomlPath)) {
|
|
173
|
+
const toml = await readText(tomlPath);
|
|
174
|
+
const entryPattern = `["${testFileName}"]`;
|
|
175
|
+
if (toml.includes(entryPattern)) {
|
|
176
|
+
await snapshotFile(journal, tomlPath);
|
|
177
|
+
const updated = removeTomlSection(toml, testFileName);
|
|
178
|
+
await writeText(tomlPath, updated);
|
|
57
179
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
warn(`Could not update browser.toml — ${toError(error).message}. Remove the test entry manually if needed.`);
|
|
184
|
+
}
|
|
185
|
+
// Step 3: Clean up empty test directory and deregister from moz.build
|
|
186
|
+
try {
|
|
187
|
+
const remaining = await readdir(testDir);
|
|
188
|
+
const hasTests = remaining.some((f) => f.startsWith('browser_') && f.endsWith('.js'));
|
|
189
|
+
if (!hasTests) {
|
|
190
|
+
await snapshotDir(journal, testDir);
|
|
191
|
+
await removeDir(testDir);
|
|
192
|
+
info(`Deleted empty test directory: browser/base/content/test/${binaryName}/`);
|
|
193
|
+
const mozBuildPath = join(paths.engine, 'browser/base/moz.build');
|
|
194
|
+
await snapshotFile(journal, mozBuildPath);
|
|
195
|
+
if (await deregisterTestManifest(paths.engine, binaryName)) {
|
|
196
|
+
info('Deregistered test manifest from browser/base/moz.build');
|
|
66
197
|
}
|
|
67
198
|
}
|
|
68
199
|
}
|
|
69
200
|
catch (error) {
|
|
70
|
-
warn(`Could not clean up test
|
|
201
|
+
warn(`Could not clean up test directory — ${toError(error).message}. Remove it manually if needed.`);
|
|
71
202
|
}
|
|
72
203
|
}
|
|
204
|
+
function dropChecksumsByPrefix(state, prefix) {
|
|
205
|
+
const result = { ...state };
|
|
206
|
+
if (state.appliedChecksums) {
|
|
207
|
+
result.appliedChecksums = Object.fromEntries(Object.entries(state.appliedChecksums).filter(([k]) => !k.startsWith(prefix)));
|
|
208
|
+
}
|
|
209
|
+
if (state.engineChecksums) {
|
|
210
|
+
result.engineChecksums = Object.fromEntries(Object.entries(state.engineChecksums).filter(([k]) => !k.startsWith(prefix)));
|
|
211
|
+
}
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
73
214
|
/**
|
|
74
215
|
* Runs the furnace remove command to remove a component from the workspace.
|
|
75
216
|
* @param projectRoot - Root directory of the project
|
|
@@ -80,18 +221,20 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
|
|
|
80
221
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
81
222
|
intro('Furnace Remove');
|
|
82
223
|
const config = await loadFurnaceConfig(projectRoot);
|
|
224
|
+
const state = await loadFurnaceState(projectRoot);
|
|
83
225
|
const furnacePaths = getFurnacePaths(projectRoot);
|
|
226
|
+
const ftlDir = resolveFtlDir(config.ftlBasePath);
|
|
84
227
|
// Find which section the component belongs to
|
|
85
228
|
const type = findComponentType(config, name);
|
|
86
229
|
if (!type) {
|
|
87
230
|
throw new FurnaceError(`Component "${name}" not found in furnace.json. Run "fireforge furnace list" to see registered components.`, name);
|
|
88
231
|
}
|
|
89
|
-
// Require --
|
|
90
|
-
if (!isInteractive && !options.
|
|
91
|
-
throw new FurnaceError(`Cannot remove "${name}" in non-interactive mode without --
|
|
232
|
+
// Require --yes in non-interactive mode to prevent silent removals
|
|
233
|
+
if (!isInteractive && !options.yes) {
|
|
234
|
+
throw new FurnaceError(`Cannot remove "${name}" in non-interactive mode without --yes flag.`, name);
|
|
92
235
|
}
|
|
93
|
-
// Confirm removal (skip if --
|
|
94
|
-
if (!options.
|
|
236
|
+
// Confirm removal (skip if --yes)
|
|
237
|
+
if (!options.yes && isInteractive) {
|
|
95
238
|
const confirmed = await confirm({
|
|
96
239
|
message: `Remove ${type} component "${name}"?`,
|
|
97
240
|
});
|
|
@@ -100,59 +243,112 @@ export async function furnaceRemoveCommand(projectRoot, name, options = {}) {
|
|
|
100
243
|
return;
|
|
101
244
|
}
|
|
102
245
|
}
|
|
103
|
-
//
|
|
246
|
+
// Begin transactional mutation: every file deleted or rewritten is first
|
|
247
|
+
// snapshotted in a rollback journal so any failure mid-removal restores the
|
|
248
|
+
// workspace and engine to their pre-command state. The mutation runs under
|
|
249
|
+
// the furnace-wide lock and is registered with the global SIGINT/SIGTERM
|
|
250
|
+
// rollback pathway.
|
|
104
251
|
const paths = getProjectPaths(projectRoot);
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
252
|
+
await runFurnaceMutation(projectRoot, 'remove-rollback', async (ctx) => {
|
|
253
|
+
const journal = createRollbackJournal();
|
|
254
|
+
ctx.registerJournal(journal);
|
|
255
|
+
try {
|
|
256
|
+
if (type === 'override') {
|
|
257
|
+
const overrideConfig = config.overrides[name];
|
|
258
|
+
const dir = join(furnacePaths.overridesDir, name);
|
|
259
|
+
// Restore deployed engine files BEFORE removing the workspace
|
|
260
|
+
// directory. The restore set is the union of (a) files currently in
|
|
261
|
+
// the workspace and (b) files state.json says we deployed last time
|
|
262
|
+
// — without (b), source-side deletions would orphan engine copies
|
|
263
|
+
// that this command can never see again.
|
|
264
|
+
if (overrideConfig?.basePath) {
|
|
265
|
+
const previousKeys = Object.keys(extractComponentChecksums(state.appliedChecksums, 'override', name));
|
|
266
|
+
const { restored, removed } = await restoreOverrideEngineFiles(paths.engine, dir, overrideConfig, previousKeys, ftlDir, journal);
|
|
267
|
+
if (restored > 0) {
|
|
268
|
+
info(`Restored ${restored} file${restored === 1 ? '' : 's'} in engine/${overrideConfig.basePath} to Firefox baseline`);
|
|
269
|
+
}
|
|
270
|
+
if (removed > 0) {
|
|
271
|
+
info(`Removed ${removed} override-introduced file${removed === 1 ? '' : 's'} from engine/${overrideConfig.basePath}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (await pathExists(dir)) {
|
|
275
|
+
await snapshotDir(journal, dir);
|
|
276
|
+
await removeDir(dir);
|
|
277
|
+
info(`Deleted components/overrides/${name}/`);
|
|
278
|
+
}
|
|
117
279
|
}
|
|
280
|
+
else if (type === 'custom') {
|
|
281
|
+
const customConfig = config.custom[name];
|
|
282
|
+
if (customConfig?.register) {
|
|
283
|
+
// customElements.js is the only file removeCustomElementRegistration touches.
|
|
284
|
+
await snapshotFile(journal, join(paths.engine, 'toolkit/content/customElements.js'));
|
|
285
|
+
await removeCustomElementRegistration(paths.engine, name);
|
|
286
|
+
info(`Deregistered ${name} from customElements.js`);
|
|
287
|
+
}
|
|
288
|
+
// jar.mn is the only file removeJarMnEntries touches.
|
|
289
|
+
await snapshotFile(journal, join(paths.engine, 'toolkit/content/jar.mn'));
|
|
290
|
+
await removeJarMnEntries(paths.engine, name);
|
|
291
|
+
info(`Removed ${name} entries from toolkit/content/jar.mn`);
|
|
292
|
+
const dir = join(furnacePaths.customDir, name);
|
|
293
|
+
if (await pathExists(dir)) {
|
|
294
|
+
await snapshotDir(journal, dir);
|
|
295
|
+
await removeDir(dir);
|
|
296
|
+
info(`Deleted components/custom/${name}/`);
|
|
297
|
+
}
|
|
298
|
+
// Clean up deployed files in engine
|
|
299
|
+
if (customConfig?.targetPath) {
|
|
300
|
+
const engineDir = join(paths.engine, customConfig.targetPath);
|
|
301
|
+
if (await pathExists(engineDir)) {
|
|
302
|
+
await snapshotDir(journal, engineDir);
|
|
303
|
+
await removeDir(engineDir);
|
|
304
|
+
info(`Deleted deployed files from engine/${customConfig.targetPath}/`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
// Localized components deploy a .ftl outside targetPath into the
|
|
308
|
+
// shared Fluent tree; apply writes it, so remove must delete it too
|
|
309
|
+
// or the locale payload is orphaned.
|
|
310
|
+
if (customConfig?.localized) {
|
|
311
|
+
const ftlRel = join(ftlDir, `${name}.ftl`);
|
|
312
|
+
const ftlPath = join(paths.engine, ftlRel);
|
|
313
|
+
if (await pathExists(ftlPath)) {
|
|
314
|
+
await snapshotFile(journal, ftlPath);
|
|
315
|
+
await removeFile(ftlPath);
|
|
316
|
+
info(`Deleted localized file engine/${ftlRel}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
if (type === 'custom') {
|
|
321
|
+
await cleanupCustomTestFiles(name, projectRoot, journal);
|
|
322
|
+
}
|
|
323
|
+
// Remove entry from furnace.json
|
|
324
|
+
if (type === 'stock') {
|
|
325
|
+
config.stock = config.stock.filter((s) => s !== name);
|
|
326
|
+
}
|
|
327
|
+
else if (type === 'override') {
|
|
328
|
+
config.overrides = Object.fromEntries(Object.entries(config.overrides).filter(([key]) => key !== name));
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
config.custom = Object.fromEntries(Object.entries(config.custom).filter(([key]) => key !== name));
|
|
332
|
+
}
|
|
333
|
+
await snapshotFile(journal, furnacePaths.furnaceConfig);
|
|
334
|
+
await writeFurnaceConfig(projectRoot, config);
|
|
335
|
+
// Drop stale per-file checksums inside the same transactional block.
|
|
336
|
+
// Snapshotting the state file into the rollback journal means the
|
|
337
|
+
// entire remove operation is a single atomic unit.
|
|
338
|
+
await snapshotFile(journal, furnacePaths.furnaceState);
|
|
339
|
+
await updateFurnaceState(projectRoot, (state) => dropChecksumsByPrefix(state, `${type}/${name}/`));
|
|
118
340
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
await removeJarMnEntries(paths.engine, name);
|
|
127
|
-
info(`Removed ${name} entries from toolkit/content/jar.mn`);
|
|
128
|
-
const dir = join(furnacePaths.customDir, name);
|
|
129
|
-
if (await pathExists(dir)) {
|
|
130
|
-
await removeDir(dir);
|
|
131
|
-
info(`Deleted components/custom/${name}/`);
|
|
132
|
-
}
|
|
133
|
-
// Clean up deployed files in engine
|
|
134
|
-
if (customConfig?.targetPath) {
|
|
135
|
-
const engineDir = join(paths.engine, customConfig.targetPath);
|
|
136
|
-
if (await pathExists(engineDir)) {
|
|
137
|
-
await removeDir(engineDir);
|
|
138
|
-
info(`Deleted deployed files from engine/${customConfig.targetPath}/`);
|
|
341
|
+
catch (error) {
|
|
342
|
+
try {
|
|
343
|
+
await restoreRollbackJournalOrThrow(journal, `Failed to remove component "${name}"`);
|
|
344
|
+
}
|
|
345
|
+
catch (rollbackError) {
|
|
346
|
+
await recordFurnaceRollbackFailure(projectRoot, 'remove-rollback', toError(rollbackError).message);
|
|
347
|
+
throw rollbackError;
|
|
139
348
|
}
|
|
349
|
+
throw error;
|
|
140
350
|
}
|
|
141
|
-
}
|
|
142
|
-
if (type === 'custom') {
|
|
143
|
-
await cleanupCustomTestFiles(name, projectRoot);
|
|
144
|
-
}
|
|
145
|
-
// Remove entry from furnace.json
|
|
146
|
-
if (type === 'stock') {
|
|
147
|
-
config.stock = config.stock.filter((s) => s !== name);
|
|
148
|
-
}
|
|
149
|
-
else if (type === 'override') {
|
|
150
|
-
config.overrides = Object.fromEntries(Object.entries(config.overrides).filter(([key]) => key !== name));
|
|
151
|
-
}
|
|
152
|
-
else {
|
|
153
|
-
config.custom = Object.fromEntries(Object.entries(config.custom).filter(([key]) => key !== name));
|
|
154
|
-
}
|
|
155
|
-
await writeFurnaceConfig(projectRoot, config);
|
|
351
|
+
});
|
|
156
352
|
info(`Removed "${name}" from furnace.json`);
|
|
157
353
|
outro('Component removed');
|
|
158
354
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Renames a custom or override component atomically: updates directory name,
|
|
3
|
+
* file names, file contents, furnace.json, and engine registrations.
|
|
4
|
+
*/
|
|
5
|
+
export declare function furnaceRenameCommand(projectRoot: string, oldName: string, newName: string): Promise<void>;
|