@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.
- 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 +6 -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
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { getProjectPaths } from '../../core/config.js';
|
|
3
|
+
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
4
4
|
import { applyAllComponents, applyCustomComponent, applyOverrideComponent, computeComponentChecksums, prefixChecksums, } from '../../core/furnace-apply.js';
|
|
5
|
+
import { logApplyResult } from '../../core/furnace-apply-output.js';
|
|
5
6
|
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, updateFurnaceState, } from '../../core/furnace-config.js';
|
|
7
|
+
import { resolveFtlDir } from '../../core/furnace-constants.js';
|
|
8
|
+
import { recordFurnaceRollbackFailure, runFurnaceMutation, } from '../../core/furnace-operation.js';
|
|
6
9
|
import { createRollbackJournal, restoreRollbackJournalOrThrow, } from '../../core/furnace-rollback.js';
|
|
7
|
-
import {
|
|
10
|
+
import { findOverrideBaseVersionDrift, formatOverrideBaseVersionDriftError, formatOverrideBaseVersionDriftWarning, } from '../../core/furnace-version-drift.js';
|
|
8
11
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
9
12
|
import { toError } from '../../utils/errors.js';
|
|
10
13
|
import { pathExists } from '../../utils/fs.js';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
14
|
+
import { info, intro, note, outro, spinner, warn } from '../../utils/logger.js';
|
|
15
|
+
import { runDeployValidation } from './validation-output.js';
|
|
13
16
|
/**
|
|
14
17
|
* Builds the final deploy failure summary from apply and validation error counts.
|
|
15
18
|
* @param applyErrors - Number of component application failures
|
|
@@ -39,6 +42,38 @@ function getFailedComponentNames(result) {
|
|
|
39
42
|
}
|
|
40
43
|
return failed;
|
|
41
44
|
}
|
|
45
|
+
function getPersistableAppliedEntry(name, appliedEntry) {
|
|
46
|
+
if (!appliedEntry) {
|
|
47
|
+
throw new FurnaceError(`Named deploy for "${name}" completed without an applied entry.`);
|
|
48
|
+
}
|
|
49
|
+
if (appliedEntry.type !== 'override' && appliedEntry.type !== 'custom') {
|
|
50
|
+
throw new FurnaceError(`Named deploy for "${name}" returned unsupported component type "${appliedEntry.type}".`);
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
name: appliedEntry.name,
|
|
54
|
+
type: appliedEntry.type,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Decides whether a single-component deploy completed cleanly enough to
|
|
59
|
+
* persist its checksums into furnace-state.json.
|
|
60
|
+
*
|
|
61
|
+
* Named deploy is atomic: if any apply step fails, the rollback journal
|
|
62
|
+
* restores the engine to its pre-deploy state and this helper returns
|
|
63
|
+
* `false` so state is not touched. The conditions must stay in lock-step
|
|
64
|
+
* with the rollback-trigger in `applyNamedComponent` — both now read from
|
|
65
|
+
* this helper so a future refactor cannot drift them apart and accidentally
|
|
66
|
+
* persist partial state.
|
|
67
|
+
*/
|
|
68
|
+
function shouldPersistNamedDeployState(result, isDryRun) {
|
|
69
|
+
if (isDryRun)
|
|
70
|
+
return false;
|
|
71
|
+
if (result.errors.length > 0)
|
|
72
|
+
return false;
|
|
73
|
+
if (getStepFailureCount(result) > 0)
|
|
74
|
+
return false;
|
|
75
|
+
return result.applied.length > 0;
|
|
76
|
+
}
|
|
42
77
|
/**
|
|
43
78
|
* Persists checksum state for a successfully applied named component.
|
|
44
79
|
* @param projectRoot - Root directory of the project
|
|
@@ -51,14 +86,60 @@ async function persistSingleComponentState(projectRoot, appliedEntry, furnacePat
|
|
|
51
86
|
: join(furnacePaths.customDir, appliedEntry.name);
|
|
52
87
|
const checksums = await computeComponentChecksums(componentDir);
|
|
53
88
|
const prefixed = prefixChecksums(checksums, appliedEntry.type, appliedEntry.name);
|
|
89
|
+
const componentPrefix = `${appliedEntry.type}/${appliedEntry.name}/`;
|
|
54
90
|
await updateFurnaceState(projectRoot, (current) => ({
|
|
55
91
|
...current,
|
|
56
|
-
appliedChecksums: {
|
|
92
|
+
appliedChecksums: {
|
|
93
|
+
...Object.fromEntries(Object.entries(current.appliedChecksums ?? {}).filter(([key]) => !key.startsWith(componentPrefix))),
|
|
94
|
+
...prefixed,
|
|
95
|
+
},
|
|
96
|
+
engineChecksums: {
|
|
97
|
+
...Object.fromEntries(Object.entries(current.engineChecksums ?? {}).filter(([key]) => !key.startsWith(componentPrefix))),
|
|
98
|
+
...prefixed,
|
|
99
|
+
},
|
|
57
100
|
lastApply: new Date().toISOString(),
|
|
58
101
|
}));
|
|
59
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* True when an applied-result carries any signal that the deploy did not
|
|
105
|
+
* complete cleanly. Any such signal must trigger the rollback journal and
|
|
106
|
+
* must suppress state persistence, so both call sites read from here.
|
|
107
|
+
*
|
|
108
|
+
* An apply failure on a single-component deploy means one of:
|
|
109
|
+
* - `result.errors` has an entry (the apply body threw)
|
|
110
|
+
* - an `applied[].stepErrors` entry is present (a registration step
|
|
111
|
+
* failed after the copy succeeded)
|
|
112
|
+
*
|
|
113
|
+
* Both are treated identically for atomicity purposes: rollback runs,
|
|
114
|
+
* state stays on the previous checkpoint, and the caller raises a deploy
|
|
115
|
+
* failure so the operator sees the error.
|
|
116
|
+
*/
|
|
117
|
+
function namedDeployHasFailures(result) {
|
|
118
|
+
return result.errors.length > 0 || getStepFailureCount(result) > 0;
|
|
119
|
+
}
|
|
120
|
+
async function restoreNamedDeployRollback(rollbackJournal, name, projectRoot) {
|
|
121
|
+
if (!rollbackJournal)
|
|
122
|
+
return;
|
|
123
|
+
try {
|
|
124
|
+
await restoreRollbackJournalOrThrow(rollbackJournal, `Furnace deploy failed for "${name}"`);
|
|
125
|
+
}
|
|
126
|
+
catch (rollbackError) {
|
|
127
|
+
if (projectRoot) {
|
|
128
|
+
await recordFurnaceRollbackFailure(projectRoot, 'deploy-rollback', toError(rollbackError).message);
|
|
129
|
+
}
|
|
130
|
+
throw rollbackError;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
60
133
|
/**
|
|
61
134
|
* Applies a single named override or custom component in targeted deploy mode.
|
|
135
|
+
*
|
|
136
|
+
* Atomicity contract: the helper owns a single rollback journal for the
|
|
137
|
+
* deploy. If any apply path fails (thrown error or step error), the journal
|
|
138
|
+
* restores the engine to its pre-deploy state and the returned `result` is
|
|
139
|
+
* still reported as failed to the caller. The caller must consult
|
|
140
|
+
* {@link shouldPersistNamedDeployState} before touching furnace-state.json —
|
|
141
|
+
* partial checksums must never be persisted on top of a rollback.
|
|
142
|
+
*
|
|
62
143
|
* @param name - Component name to apply
|
|
63
144
|
* @param engineDir - Firefox engine source directory
|
|
64
145
|
* @param furnacePaths - Resolved Furnace workspace paths
|
|
@@ -66,8 +147,11 @@ async function persistSingleComponentState(projectRoot, appliedEntry, furnacePat
|
|
|
66
147
|
* @param isDryRun - Whether file writes should be skipped
|
|
67
148
|
* @returns Apply result for the named component, or `stock` for stock-only entries
|
|
68
149
|
*/
|
|
69
|
-
async function applyNamedComponent(name, engineDir, furnacePaths, config, isDryRun) {
|
|
150
|
+
async function applyNamedComponent(name, engineDir, furnacePaths, config, ftlDir, isDryRun, operationContext, projectRoot) {
|
|
70
151
|
const rollbackJournal = isDryRun ? undefined : createRollbackJournal();
|
|
152
|
+
if (rollbackJournal && operationContext) {
|
|
153
|
+
operationContext.registerJournal(rollbackJournal);
|
|
154
|
+
}
|
|
71
155
|
const result = {
|
|
72
156
|
applied: [],
|
|
73
157
|
skipped: [],
|
|
@@ -82,7 +166,7 @@ async function applyNamedComponent(name, engineDir, furnacePaths, config, isDryR
|
|
|
82
166
|
throw new FurnaceError(`Component directory not found: components/overrides/${name}`, name);
|
|
83
167
|
}
|
|
84
168
|
try {
|
|
85
|
-
const { affectedPaths: filesAffected, actions } = await applyOverrideComponent(engineDir, name, componentDir, overrideConfig, isDryRun, rollbackJournal);
|
|
169
|
+
const { affectedPaths: filesAffected, actions } = await applyOverrideComponent(engineDir, name, componentDir, overrideConfig, ftlDir, isDryRun, rollbackJournal);
|
|
86
170
|
if (isDryRun && actions) {
|
|
87
171
|
result.actions = actions;
|
|
88
172
|
}
|
|
@@ -91,8 +175,8 @@ async function applyNamedComponent(name, engineDir, furnacePaths, config, isDryR
|
|
|
91
175
|
catch (error) {
|
|
92
176
|
result.errors.push({ name, error: toError(error).message });
|
|
93
177
|
}
|
|
94
|
-
if (!isDryRun && result
|
|
95
|
-
await
|
|
178
|
+
if (!isDryRun && namedDeployHasFailures(result)) {
|
|
179
|
+
await restoreNamedDeployRollback(rollbackJournal, name, projectRoot);
|
|
96
180
|
}
|
|
97
181
|
return result;
|
|
98
182
|
}
|
|
@@ -102,7 +186,7 @@ async function applyNamedComponent(name, engineDir, furnacePaths, config, isDryR
|
|
|
102
186
|
throw new FurnaceError(`Component directory not found: components/custom/${name}`, name);
|
|
103
187
|
}
|
|
104
188
|
try {
|
|
105
|
-
const { affectedPaths: filesAffected, stepErrors, actions, } = await applyCustomComponent(engineDir, name, componentDir, customConfig, isDryRun, rollbackJournal);
|
|
189
|
+
const { affectedPaths: filesAffected, stepErrors, actions, } = await applyCustomComponent(engineDir, name, componentDir, customConfig, ftlDir, isDryRun, rollbackJournal);
|
|
106
190
|
if (isDryRun && actions) {
|
|
107
191
|
result.actions = actions;
|
|
108
192
|
}
|
|
@@ -116,8 +200,8 @@ async function applyNamedComponent(name, engineDir, furnacePaths, config, isDryR
|
|
|
116
200
|
catch (error) {
|
|
117
201
|
result.errors.push({ name, error: toError(error).message });
|
|
118
202
|
}
|
|
119
|
-
if (!isDryRun &&
|
|
120
|
-
await
|
|
203
|
+
if (!isDryRun && namedDeployHasFailures(result)) {
|
|
204
|
+
await restoreNamedDeployRollback(rollbackJournal, name, projectRoot);
|
|
121
205
|
}
|
|
122
206
|
return result;
|
|
123
207
|
}
|
|
@@ -126,31 +210,6 @@ async function applyNamedComponent(name, engineDir, furnacePaths, config, isDryR
|
|
|
126
210
|
}
|
|
127
211
|
throw new FurnaceError(`Component "${name}" not found in furnace.json.`, name);
|
|
128
212
|
}
|
|
129
|
-
/**
|
|
130
|
-
* Resolves the validation target for a single named component.
|
|
131
|
-
* @param name - Component name to validate
|
|
132
|
-
* @param config - Loaded Furnace configuration
|
|
133
|
-
* @param furnacePaths - Resolved Furnace workspace paths
|
|
134
|
-
* @returns Validation target details, or `stock` for stock-only entries
|
|
135
|
-
*/
|
|
136
|
-
function resolveNamedValidationTarget(name, config, furnacePaths) {
|
|
137
|
-
if (name in config.overrides) {
|
|
138
|
-
return {
|
|
139
|
-
type: 'override',
|
|
140
|
-
componentDir: join(furnacePaths.overridesDir, name),
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
if (name in config.custom) {
|
|
144
|
-
return {
|
|
145
|
-
type: 'custom',
|
|
146
|
-
componentDir: join(furnacePaths.customDir, name),
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
if (config.stock.includes(name)) {
|
|
150
|
-
return 'stock';
|
|
151
|
-
}
|
|
152
|
-
throw new FurnaceError(`Component "${name}" not found in furnace.json.`, name);
|
|
153
|
-
}
|
|
154
213
|
/**
|
|
155
214
|
* Prints the deploy summary after apply and validation complete.
|
|
156
215
|
* @param result - Aggregate apply result
|
|
@@ -169,7 +228,8 @@ function printDeploymentSummary(result, totalErrors, totalWarnings, componentCou
|
|
|
169
228
|
note(`Would apply ${appliedCount} component(s)\n` +
|
|
170
229
|
`${result.actions?.length ?? 0} planned action(s)\n` +
|
|
171
230
|
`${applyErrors} apply error(s)\n` +
|
|
172
|
-
`${totalErrors} validation error(s), ${totalWarnings} validation warning(s) across ${componentCount} validated component(s)` +
|
|
231
|
+
`${totalErrors} validation error(s), ${totalWarnings} validation warning(s) across ${componentCount} validated component(s)\n` +
|
|
232
|
+
'(validation ran against current source files — no engine files were modified)' +
|
|
173
233
|
(skippedValidationCount > 0
|
|
174
234
|
? `\nSkipped validation for ${skippedValidationCount} component(s) with apply errors`
|
|
175
235
|
: ''), 'Dry Run Summary');
|
|
@@ -190,33 +250,12 @@ function printDeploymentSummary(result, totalErrors, totalWarnings, componentCou
|
|
|
190
250
|
}
|
|
191
251
|
outro(isDryRun ? 'Dry run complete (no files modified)' : 'Deploy complete');
|
|
192
252
|
}
|
|
193
|
-
function
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
for (const action of result.actions) {
|
|
197
|
-
info(` [${action.action}] ${action.component}: ${action.description}`);
|
|
198
|
-
}
|
|
253
|
+
function enforceScopedOverrideVersionDriftPreflight(scopedDrift, force) {
|
|
254
|
+
for (const entry of scopedDrift) {
|
|
255
|
+
warn(formatOverrideBaseVersionDriftWarning(entry));
|
|
199
256
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
else {
|
|
204
|
-
for (const applied of result.applied) {
|
|
205
|
-
success(`${applied.name} (${applied.type}) → ${applied.filesAffected.length} files`);
|
|
206
|
-
}
|
|
207
|
-
for (const skipped of result.skipped) {
|
|
208
|
-
info(`${skipped.name} — ${skipped.reason}`);
|
|
209
|
-
}
|
|
210
|
-
for (const applied of result.applied) {
|
|
211
|
-
if (applied.stepErrors && applied.stepErrors.length > 0) {
|
|
212
|
-
for (const stepErr of applied.stepErrors) {
|
|
213
|
-
warn(`${applied.name}: [${stepErr.step}] ${stepErr.error}`);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
for (const err of result.errors) {
|
|
219
|
-
error(`${err.name} — ${err.error}`);
|
|
257
|
+
if (!force && scopedDrift.length > 0) {
|
|
258
|
+
throw new FurnaceError(formatOverrideBaseVersionDriftError(scopedDrift));
|
|
220
259
|
}
|
|
221
260
|
}
|
|
222
261
|
/**
|
|
@@ -238,7 +277,7 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
|
|
|
238
277
|
throw new FurnaceError('No furnace.json found. Run "fireforge furnace create" or "fireforge furnace override" to get started.');
|
|
239
278
|
}
|
|
240
279
|
const config = await loadFurnaceConfig(projectRoot);
|
|
241
|
-
const furnacePaths = getFurnacePaths(projectRoot);
|
|
280
|
+
const [furnacePaths, ftlDir] = [getFurnacePaths(projectRoot), resolveFtlDir(config.ftlBasePath)];
|
|
242
281
|
const overrideCount = Object.keys(config.overrides).length;
|
|
243
282
|
const customCount = Object.keys(config.custom).length;
|
|
244
283
|
if (overrideCount === 0 && customCount === 0) {
|
|
@@ -246,92 +285,65 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
|
|
|
246
285
|
outro('Done');
|
|
247
286
|
return;
|
|
248
287
|
}
|
|
288
|
+
// Refuse real mutation when the targeted overrides were created against a
|
|
289
|
+
// different Firefox version. Dry-run still proceeds so operators can inspect
|
|
290
|
+
// the plan before deciding whether to refresh the override or acknowledge
|
|
291
|
+
// the new baseline in furnace.json.
|
|
292
|
+
const forgeConfig = await loadConfig(projectRoot);
|
|
293
|
+
const driftEntries = findOverrideBaseVersionDrift(config, forgeConfig.firefox.version);
|
|
294
|
+
const force = options.force ?? false;
|
|
295
|
+
const scopedDrift = name ? driftEntries.filter((entry) => entry.name === name) : driftEntries;
|
|
296
|
+
enforceScopedOverrideVersionDriftPreflight(scopedDrift, force);
|
|
249
297
|
// --- Step 1: Apply ---
|
|
250
298
|
const applySpinner = spinner(isDryRun ? 'Calculating planned actions...' : 'Applying components to engine...');
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
299
|
+
// The apply phase is lock-protected and registered with the global
|
|
300
|
+
// SIGINT/SIGTERM rollback pathway via runFurnaceMutation. The validation
|
|
301
|
+
// phase below is read-only and runs outside the lock so two concurrent
|
|
302
|
+
// `furnace deploy` runs only contend on the actual mutation.
|
|
303
|
+
const applyOutcome = await runFurnaceMutation(projectRoot, 'deploy-rollback', async (ctx) => {
|
|
304
|
+
if (name) {
|
|
305
|
+
const namedApplyResult = await applyNamedComponent(name, paths.engine, furnacePaths, config, ftlDir, isDryRun, ctx, projectRoot);
|
|
306
|
+
if (namedApplyResult === 'stock') {
|
|
307
|
+
return { kind: 'stock' };
|
|
308
|
+
}
|
|
309
|
+
// Named deploy is atomic: state is persisted only when every apply
|
|
310
|
+
// step succeeded. Any rollback triggered by applyNamedComponent has
|
|
311
|
+
// already restored the engine to its pre-deploy state, so persisting
|
|
312
|
+
// partial checksums here would mis-report the next status/apply run
|
|
313
|
+
// against a workspace that was never actually deployed.
|
|
314
|
+
if (shouldPersistNamedDeployState(namedApplyResult, isDryRun)) {
|
|
315
|
+
await persistSingleComponentState(projectRoot, getPersistableAppliedEntry(name, namedApplyResult.applied[0]), furnacePaths);
|
|
316
|
+
}
|
|
317
|
+
return { kind: 'result', result: namedApplyResult };
|
|
269
318
|
}
|
|
319
|
+
const allResult = await applyAllComponents(projectRoot, isDryRun, {
|
|
320
|
+
operationContext: ctx,
|
|
321
|
+
});
|
|
322
|
+
return { kind: 'result', result: allResult };
|
|
323
|
+
}, { dryRun: isDryRun });
|
|
324
|
+
if (applyOutcome.kind === 'stock') {
|
|
325
|
+
applySpinner.stop('Apply skipped');
|
|
326
|
+
warn(`"${name}" is a stock component. Stock components are not applied locally.`);
|
|
327
|
+
outro(isDryRun ? 'Dry run complete (no files modified)' : 'Deploy complete');
|
|
328
|
+
return;
|
|
270
329
|
}
|
|
271
|
-
|
|
272
|
-
result = await applyAllComponents(projectRoot, isDryRun);
|
|
273
|
-
}
|
|
330
|
+
const result = applyOutcome.result;
|
|
274
331
|
applySpinner.stop(isDryRun ? 'Planned actions calculated' : 'Components applied');
|
|
275
332
|
logApplyResult(result, isDryRun);
|
|
276
|
-
// --- Step 2: Validate (read-only, runs even in dry-run) ---
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if (name && failedComponents.has(name)) {
|
|
284
|
-
skippedValidationCount = 1;
|
|
285
|
-
validateSpinner.stop('Validation skipped');
|
|
286
|
-
warn(`Skipping validation for ${name} because apply failed.`);
|
|
287
|
-
}
|
|
288
|
-
else if (name) {
|
|
289
|
-
const target = resolveNamedValidationTarget(name, config, furnacePaths);
|
|
290
|
-
if (target === 'stock') {
|
|
291
|
-
validateSpinner.stop('Validation skipped');
|
|
292
|
-
info(`"${name}" is a stock component. Stock components are not validated locally.`);
|
|
293
|
-
outro(isDryRun ? 'Dry run complete' : 'Deploy complete');
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
|
-
if (!(await pathExists(target.componentDir))) {
|
|
297
|
-
validateSpinner.stop('Validation failed');
|
|
298
|
-
throw new FurnaceError(`Component directory not found for "${name}".`, name);
|
|
299
|
-
}
|
|
300
|
-
const issues = await validateComponent(target.componentDir, name, target.type, config, projectRoot);
|
|
301
|
-
componentCount = 1;
|
|
302
|
-
validateSpinner.stop('Validation complete');
|
|
303
|
-
if (issues.length === 0) {
|
|
304
|
-
success(`${name} — all checks passed`);
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
const [errors, warnings] = displayValidationIssues(issues);
|
|
308
|
-
totalErrors += errors;
|
|
309
|
-
totalWarnings += warnings;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
// Validate all components
|
|
314
|
-
const results = await validateAllComponents(projectRoot);
|
|
315
|
-
validateSpinner.stop('Validation complete');
|
|
316
|
-
for (const [componentName, issues] of results) {
|
|
317
|
-
if (failedComponents.has(componentName)) {
|
|
318
|
-
skippedValidationCount++;
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
componentCount++;
|
|
322
|
-
if (issues.length === 0) {
|
|
323
|
-
success(`${componentName} — all checks passed`);
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
const [errors, warnings] = displayValidationIssues(issues);
|
|
327
|
-
totalErrors += errors;
|
|
328
|
-
totalWarnings += warnings;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
if (skippedValidationCount > 0) {
|
|
332
|
-
warn(`Skipped validation for ${skippedValidationCount} component(s) because their apply step failed.`);
|
|
333
|
-
}
|
|
333
|
+
// --- Step 2: Validate (read-only, runs even in dry-run to show what would fail) ---
|
|
334
|
+
if (options.skipValidate) {
|
|
335
|
+
const applyErrors = result.errors.length + getStepFailureCount(result);
|
|
336
|
+
if (applyErrors > 0)
|
|
337
|
+
throw new FurnaceError(buildDeployFailureMessage(applyErrors, 0, isDryRun));
|
|
338
|
+
outro(isDryRun ? 'Dry run complete (validation skipped)' : 'Deploy complete (validation skipped)');
|
|
339
|
+
return;
|
|
334
340
|
}
|
|
341
|
+
const validateSpinner = spinner(isDryRun ? 'Validating (read-only)...' : 'Validating...');
|
|
342
|
+
const failedComponents = getFailedComponentNames(result);
|
|
343
|
+
const validation = await runDeployValidation(validateSpinner, name, config, furnacePaths, failedComponents, isDryRun, projectRoot);
|
|
344
|
+
if (validation.done)
|
|
345
|
+
return;
|
|
346
|
+
const { totalErrors, totalWarnings, componentCount, skippedValidationCount } = validation;
|
|
335
347
|
// --- Step 3: Summary ---
|
|
336
348
|
printDeploymentSummary(result, totalErrors, totalWarnings, componentCount, skippedValidationCount, isDryRun);
|
|
337
349
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runs the furnace diff command
|
|
3
|
-
*
|
|
2
|
+
* Runs the furnace diff command.
|
|
3
|
+
*
|
|
4
|
+
* For overrides: shows changes vs the Firefox original at baseCommit.
|
|
5
|
+
* For custom components: shows workspace vs engine-deployed copy.
|
|
6
|
+
* When no name is provided, diffs all override and custom components.
|
|
7
|
+
*
|
|
4
8
|
* @param projectRoot - Root directory of the project
|
|
5
|
-
* @param name -
|
|
9
|
+
* @param name - Optional component name to diff (diffs all when omitted)
|
|
6
10
|
*/
|
|
7
|
-
export declare function furnaceDiffCommand(projectRoot: string, name
|
|
11
|
+
export declare function furnaceDiffCommand(projectRoot: string, name?: string): Promise<void>;
|