@hominis/fireforge 0.30.1 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +22 -0
- package/dist/src/commands/export-all.js +5 -15
- package/dist/src/commands/export-flow.d.ts +6 -0
- package/dist/src/commands/export-flow.js +6 -1
- package/dist/src/commands/export-placement-gate.d.ts +38 -0
- package/dist/src/commands/export-placement-gate.js +105 -0
- package/dist/src/commands/export-shared.d.ts +28 -0
- package/dist/src/commands/export-shared.js +36 -0
- package/dist/src/commands/export.js +47 -112
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
- package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
- package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
- package/dist/src/commands/furnace/create.d.ts +1 -2
- package/dist/src/commands/furnace/deploy.js +36 -114
- package/dist/src/commands/furnace/refresh.js +52 -32
- package/dist/src/commands/furnace/sync.js +2 -0
- package/dist/src/commands/import.js +108 -73
- package/dist/src/commands/lint-per-patch.d.ts +1 -1
- package/dist/src/commands/lint-per-patch.js +110 -81
- package/dist/src/commands/lint.d.ts +1 -58
- package/dist/src/commands/lint.js +96 -84
- package/dist/src/commands/patch/compact.d.ts +5 -2
- package/dist/src/commands/patch/compact.js +85 -25
- package/dist/src/commands/patch/delete.js +17 -17
- package/dist/src/commands/patch/index.js +2 -0
- package/dist/src/commands/patch/lint-ignore.js +3 -16
- package/dist/src/commands/patch/move-files.js +2 -0
- package/dist/src/commands/patch/patch-context.d.ts +41 -0
- package/dist/src/commands/patch/patch-context.js +53 -0
- package/dist/src/commands/patch/rename.js +10 -15
- package/dist/src/commands/patch/reorder.d.ts +0 -2
- package/dist/src/commands/patch/reorder.js +18 -19
- package/dist/src/commands/patch/split-plan.d.ts +66 -0
- package/dist/src/commands/patch/split-plan.js +178 -0
- package/dist/src/commands/patch/split.d.ts +30 -0
- package/dist/src/commands/patch/split.js +283 -0
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
- package/dist/src/commands/patch/staged-dependency.js +4 -17
- package/dist/src/commands/patch/tier.js +4 -17
- package/dist/src/commands/re-export-scan.js +8 -1
- package/dist/src/commands/rebase/summary.d.ts +1 -5
- package/dist/src/commands/rebase/summary.js +1 -1
- package/dist/src/commands/status-output.js +77 -68
- package/dist/src/commands/test-diagnose.d.ts +23 -0
- package/dist/src/commands/test-diagnose.js +210 -0
- package/dist/src/commands/test-run.d.ts +58 -0
- package/dist/src/commands/test-run.js +88 -0
- package/dist/src/commands/test.js +169 -257
- package/dist/src/commands/token.js +15 -1
- package/dist/src/commands/wire.js +109 -78
- package/dist/src/core/build-audit.d.ts +1 -1
- package/dist/src/core/build-audit.js +2 -46
- package/dist/src/core/build-baseline-types.d.ts +38 -0
- package/dist/src/core/build-baseline-types.js +10 -0
- package/dist/src/core/build-baseline.d.ts +1 -31
- package/dist/src/core/build-prepare.d.ts +1 -1
- package/dist/src/core/build-prepare.js +2 -45
- package/dist/src/core/config-paths.d.ts +0 -8
- package/dist/src/core/config-paths.js +4 -4
- package/dist/src/core/config-state.d.ts +0 -6
- package/dist/src/core/config-state.js +1 -1
- package/dist/src/core/config-validate-patch-policy.js +12 -13
- package/dist/src/core/config-validate.js +48 -28
- package/dist/src/core/engine-changes.d.ts +24 -0
- package/dist/src/core/engine-changes.js +64 -0
- package/dist/src/core/firefox-cache.d.ts +0 -5
- package/dist/src/core/firefox-cache.js +1 -1
- package/dist/src/core/firefox-download.d.ts +0 -6
- package/dist/src/core/firefox-download.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
- package/dist/src/core/furnace-apply-helpers.js +11 -20
- package/dist/src/core/furnace-apply.d.ts +1 -1
- package/dist/src/core/furnace-apply.js +1 -1
- package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
- package/dist/src/core/furnace-checksum-utils.js +15 -0
- package/dist/src/core/furnace-config-validate.d.ts +31 -0
- package/dist/src/core/furnace-config-validate.js +133 -0
- package/dist/src/core/furnace-config.d.ts +4 -32
- package/dist/src/core/furnace-config.js +15 -111
- package/dist/src/core/furnace-constants.d.ts +0 -10
- package/dist/src/core/furnace-constants.js +2 -2
- package/dist/src/core/furnace-css-fragments.d.ts +79 -0
- package/dist/src/core/furnace-css-fragments.js +243 -0
- package/dist/src/core/furnace-jsconfig.d.ts +63 -0
- package/dist/src/core/furnace-jsconfig.js +171 -0
- package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
- package/dist/src/core/furnace-validate-helpers.js +40 -1
- package/dist/src/core/furnace-validate-registration.js +16 -1
- package/dist/src/core/furnace-validate.js +54 -2
- package/dist/src/core/git-file-ops.d.ts +0 -12
- package/dist/src/core/git-file-ops.js +2 -2
- package/dist/src/core/lint-cache.d.ts +0 -13
- package/dist/src/core/lint-cache.js +5 -5
- package/dist/src/core/mach.d.ts +5 -1
- package/dist/src/core/mach.js +6 -2
- package/dist/src/core/manifest-register.d.ts +5 -16
- package/dist/src/core/manifest-register.js +3 -1
- package/dist/src/core/patch-lint-checkjs.js +53 -7
- package/dist/src/core/patch-lint-jsdoc.js +63 -4
- package/dist/src/core/patch-lint-observer.d.ts +37 -0
- package/dist/src/core/patch-lint-observer.js +168 -0
- package/dist/src/core/patch-lint.js +132 -125
- package/dist/src/core/patch-manifest-io.d.ts +16 -0
- package/dist/src/core/patch-manifest-io.js +44 -2
- package/dist/src/core/patch-manifest-validate.d.ts +1 -8
- package/dist/src/core/patch-manifest-validate.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-policy.d.ts +0 -4
- package/dist/src/core/patch-policy.js +10 -4
- package/dist/src/core/register-browser-content.d.ts +1 -1
- package/dist/src/core/register-module.d.ts +1 -1
- package/dist/src/core/register-result.d.ts +21 -0
- package/dist/src/core/register-result.js +9 -0
- package/dist/src/core/register-shared-css.d.ts +1 -1
- package/dist/src/core/register-test-manifest.d.ts +1 -1
- package/dist/src/core/test-harness-crash.d.ts +61 -0
- package/dist/src/core/test-harness-crash.js +140 -0
- package/dist/src/core/test-stale-check.d.ts +1 -1
- package/dist/src/core/test-stale-check.js +2 -46
- package/dist/src/core/test-xpcshell-retry.d.ts +1 -1
- package/dist/src/core/test-xpcshell-retry.js +4 -2
- package/dist/src/core/token-dark-mode.js +14 -26
- package/dist/src/core/token-manager.d.ts +4 -0
- package/dist/src/core/token-manager.js +70 -16
- package/dist/src/core/typecheck-shim.d.ts +0 -21
- package/dist/src/core/typecheck-shim.js +26 -4
- package/dist/src/core/wire-utils.js +37 -44
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +105 -0
- package/dist/src/types/furnace.d.ts +12 -1
- package/dist/src/utils/elapsed.d.ts +0 -2
- package/dist/src/utils/elapsed.js +1 -1
- package/dist/src/utils/fs.d.ts +0 -5
- package/dist/src/utils/fs.js +1 -1
- package/dist/src/utils/regex.d.ts +0 -6
- package/dist/src/utils/regex.js +3 -3
- package/dist/src/utils/validation.d.ts +0 -8
- package/dist/src/utils/validation.js +2 -2
- package/package.json +6 -4
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { FurnaceCreateOptions } from '../../types/commands/index.js';
|
|
2
|
-
|
|
3
|
-
export type ResolvedTestStyle = 'mochikit' | 'browser-chrome' | 'xpcshell' | 'none';
|
|
2
|
+
import type { ResolvedTestStyle } from '../../types/furnace.js';
|
|
4
3
|
/**
|
|
5
4
|
* Collapses `--with-tests`, `--xpcshell`, and `--test-style` into the single
|
|
6
5
|
* scaffold dispatch used inside the mutation phase.
|
|
@@ -1,16 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
4
|
-
import { applyAllComponents,
|
|
4
|
+
import { applyAllComponents, computeComponentChecksums, prefixChecksums, } from '../../core/furnace-apply.js';
|
|
5
5
|
import { logApplyResult } from '../../core/furnace-apply-output.js';
|
|
6
6
|
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, updateFurnaceState, } from '../../core/furnace-config.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { recordFurnaceRollbackFailure, runFurnaceMutation, } from '../../core/furnace-operation.js';
|
|
10
|
-
import { createRollbackJournal, restoreRollbackJournalOrThrow, } from '../../core/furnace-rollback.js';
|
|
7
|
+
import { reportJsconfigPathsSync } from '../../core/furnace-jsconfig.js';
|
|
8
|
+
import { runFurnaceMutation } from '../../core/furnace-operation.js';
|
|
11
9
|
import { findOverrideBaseVersionDrift, formatOverrideBaseVersionDriftError, formatOverrideBaseVersionDriftWarning, } from '../../core/furnace-version-drift.js';
|
|
12
10
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
13
|
-
import { toError } from '../../utils/errors.js';
|
|
14
11
|
import { pathExists } from '../../utils/fs.js';
|
|
15
12
|
import { info, intro, note, outro, spinner, warn } from '../../utils/logger.js';
|
|
16
13
|
import { runDeployValidation } from './validation-output.js';
|
|
@@ -116,115 +113,42 @@ async function persistSingleComponentState(projectRoot, appliedEntry, furnacePat
|
|
|
116
113
|
lastApply: new Date().toISOString(),
|
|
117
114
|
}));
|
|
118
115
|
}
|
|
119
|
-
/**
|
|
120
|
-
* True when an applied-result carries any signal that the deploy did not
|
|
121
|
-
* complete cleanly. Any such signal must trigger the rollback journal and
|
|
122
|
-
* must suppress state persistence, so both call sites read from here.
|
|
123
|
-
*
|
|
124
|
-
* An apply failure on a single-component deploy means one of:
|
|
125
|
-
* - `result.errors` has an entry (the apply body threw)
|
|
126
|
-
* - an `applied[].stepErrors` entry is present (a registration step
|
|
127
|
-
* failed after the copy succeeded)
|
|
128
|
-
*
|
|
129
|
-
* Both are treated identically for atomicity purposes: rollback runs,
|
|
130
|
-
* state stays on the previous checkpoint, and the caller raises a deploy
|
|
131
|
-
* failure so the operator sees the error.
|
|
132
|
-
*/
|
|
133
|
-
function namedDeployHasFailures(result) {
|
|
134
|
-
return result.errors.length > 0 || getStepFailureCount(result) > 0;
|
|
135
|
-
}
|
|
136
|
-
async function restoreNamedDeployRollback(rollbackJournal, name, projectRoot) {
|
|
137
|
-
if (!rollbackJournal)
|
|
138
|
-
return;
|
|
139
|
-
try {
|
|
140
|
-
await restoreRollbackJournalOrThrow(rollbackJournal, `Furnace deploy failed for "${name}"`);
|
|
141
|
-
}
|
|
142
|
-
catch (rollbackError) {
|
|
143
|
-
if (projectRoot) {
|
|
144
|
-
await recordFurnaceRollbackFailure(projectRoot, 'deploy-rollback', `component "${name}": ${toError(rollbackError).message}`);
|
|
145
|
-
}
|
|
146
|
-
throw rollbackError;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
116
|
/**
|
|
150
117
|
* Applies a single named override or custom component in targeted deploy mode.
|
|
151
118
|
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
119
|
+
* Delegates to {@link applyAllComponents} with a `componentName` filter so
|
|
120
|
+
* targeted deploys run the exact same pipeline as deploy-all — including
|
|
121
|
+
* workspace-deletion detection, engine orphan undeploy, and jar.mn /
|
|
122
|
+
* customElements.js re-sync. The previous implementation called the
|
|
123
|
+
* per-component apply helpers directly and never pruned: renaming a helper
|
|
124
|
+
* file in the workspace left the old deployed file and its stale jar.mn
|
|
125
|
+
* line in the engine (field report D1).
|
|
126
|
+
*
|
|
127
|
+
* `persistState: false` is load-bearing: the batch persist path *replaces*
|
|
128
|
+
* `appliedChecksums` wholesale with only this run's entries, which for a
|
|
129
|
+
* named deploy would wipe every other component's state. Named deploy keeps
|
|
130
|
+
* its per-component state merge ({@link persistSingleComponentState}) and
|
|
131
|
+
* its atomicity gate ({@link shouldPersistNamedDeployState}) at the call
|
|
132
|
+
* site. Rollback on failure happens inside `applyAllComponents`; the
|
|
133
|
+
* journal returned on success is ignored (the deploy keeps its files).
|
|
158
134
|
*
|
|
159
135
|
* @param name - Component name to apply
|
|
160
|
-
* @param engineDir - Firefox engine source directory
|
|
161
|
-
* @param furnacePaths - Resolved Furnace workspace paths
|
|
162
136
|
* @param config - Loaded Furnace configuration
|
|
163
137
|
* @param isDryRun - Whether file writes should be skipped
|
|
164
138
|
* @returns Apply result for the named component, or `stock` for stock-only entries
|
|
165
139
|
*/
|
|
166
|
-
async function applyNamedComponent(name,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
const result = {
|
|
172
|
-
applied: [],
|
|
173
|
-
skipped: [],
|
|
174
|
-
errors: [],
|
|
175
|
-
actions: [],
|
|
176
|
-
};
|
|
177
|
-
const overrideConfig = config.overrides[name];
|
|
178
|
-
const customConfig = config.custom[name];
|
|
179
|
-
if (overrideConfig) {
|
|
180
|
-
const componentDir = join(furnacePaths.overridesDir, name);
|
|
181
|
-
if (!(await pathExists(componentDir))) {
|
|
182
|
-
throw new FurnaceError(`Component directory not found: components/overrides/${name}`, name);
|
|
183
|
-
}
|
|
184
|
-
try {
|
|
185
|
-
const { affectedPaths: filesAffected, actions } = await applyOverrideComponent(engineDir, name, componentDir, overrideConfig, ftlDir, isDryRun, rollbackJournal);
|
|
186
|
-
if (isDryRun && actions) {
|
|
187
|
-
result.actions = actions;
|
|
188
|
-
}
|
|
189
|
-
result.applied.push({ name, type: 'override', filesAffected });
|
|
190
|
-
}
|
|
191
|
-
catch (error) {
|
|
192
|
-
result.errors.push({ name, error: toError(error).message });
|
|
140
|
+
async function applyNamedComponent(name, config, isDryRun, projectRoot, operationContext) {
|
|
141
|
+
if (!(name in config.overrides) && !(name in config.custom)) {
|
|
142
|
+
if (config.stock.includes(name)) {
|
|
143
|
+
return 'stock';
|
|
193
144
|
}
|
|
194
|
-
|
|
195
|
-
await restoreNamedDeployRollback(rollbackJournal, name, projectRoot);
|
|
196
|
-
}
|
|
197
|
-
return result;
|
|
145
|
+
throw new FurnaceError(`Component "${name}" not found in furnace.json.`, name);
|
|
198
146
|
}
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
const { affectedPaths: filesAffected, stepErrors, actions, } = await applyCustomComponent(engineDir, name, componentDir, customConfig, ftlDir, isDryRun, rollbackJournal, markerComment !== undefined ? { markerComment } : {});
|
|
206
|
-
if (isDryRun && actions) {
|
|
207
|
-
result.actions = actions;
|
|
208
|
-
}
|
|
209
|
-
result.applied.push({
|
|
210
|
-
name,
|
|
211
|
-
type: 'custom',
|
|
212
|
-
filesAffected,
|
|
213
|
-
...(stepErrors.length > 0 ? { stepErrors } : {}),
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
catch (error) {
|
|
217
|
-
result.errors.push({ name, error: toError(error).message });
|
|
218
|
-
}
|
|
219
|
-
if (!isDryRun && namedDeployHasFailures(result)) {
|
|
220
|
-
await restoreNamedDeployRollback(rollbackJournal, name, projectRoot);
|
|
221
|
-
}
|
|
222
|
-
return result;
|
|
223
|
-
}
|
|
224
|
-
if (config.stock.includes(name)) {
|
|
225
|
-
return 'stock';
|
|
226
|
-
}
|
|
227
|
-
throw new FurnaceError(`Component "${name}" not found in furnace.json.`, name);
|
|
147
|
+
return applyAllComponents(projectRoot, isDryRun, {
|
|
148
|
+
componentName: name,
|
|
149
|
+
persistState: false,
|
|
150
|
+
...(operationContext ? { operationContext } : {}),
|
|
151
|
+
});
|
|
228
152
|
}
|
|
229
153
|
/**
|
|
230
154
|
* Prints the deploy summary after apply and validation complete.
|
|
@@ -293,7 +217,7 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
|
|
|
293
217
|
throw new FurnaceError('No furnace.json found. Run "fireforge furnace create" or "fireforge furnace override" to get started.');
|
|
294
218
|
}
|
|
295
219
|
const config = await loadFurnaceConfig(projectRoot);
|
|
296
|
-
const
|
|
220
|
+
const furnacePaths = getFurnacePaths(projectRoot);
|
|
297
221
|
const overrideCount = Object.keys(config.overrides).length;
|
|
298
222
|
const customCount = Object.keys(config.custom).length;
|
|
299
223
|
if (overrideCount === 0 && customCount === 0) {
|
|
@@ -306,14 +230,6 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
|
|
|
306
230
|
// the plan before deciding whether to refresh the override or acknowledge
|
|
307
231
|
// the new baseline in furnace.json.
|
|
308
232
|
const forgeConfig = await loadConfig(projectRoot);
|
|
309
|
-
// 2026-04-26 eval Finding 6: when `markerComment` is unset in
|
|
310
|
-
// fireforge.json, default it to `binaryName.toUpperCase()` so the
|
|
311
|
-
// furnace-emitted edits to upstream files satisfy
|
|
312
|
-
// `lintModificationComments` on the next `lint`/`export` round-trip.
|
|
313
|
-
// The lint rule keys on the same uppercased binaryName, so the
|
|
314
|
-
// implicit default is identical to what the rule expects. Threaded
|
|
315
|
-
// through `applyNamedComponent` below.
|
|
316
|
-
const resolvedMarkerComment = resolveFurnaceMarkerComment(forgeConfig);
|
|
317
233
|
const driftEntries = findOverrideBaseVersionDrift(config, forgeConfig.firefox.version);
|
|
318
234
|
const force = options.force ?? false;
|
|
319
235
|
const scopedDrift = name ? driftEntries.filter((entry) => entry.name === name) : driftEntries;
|
|
@@ -326,7 +242,7 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
|
|
|
326
242
|
// `furnace deploy` runs only contend on the actual mutation.
|
|
327
243
|
const applyOutcome = await runFurnaceMutation(projectRoot, 'deploy-rollback', async (ctx) => {
|
|
328
244
|
if (name) {
|
|
329
|
-
const namedApplyResult = await applyNamedComponent(name,
|
|
245
|
+
const namedApplyResult = await applyNamedComponent(name, config, isDryRun, projectRoot, ctx);
|
|
330
246
|
if (namedApplyResult === 'stock') {
|
|
331
247
|
return { kind: 'stock' };
|
|
332
248
|
}
|
|
@@ -354,6 +270,12 @@ export async function furnaceDeployCommand(projectRoot, name, options = {}) {
|
|
|
354
270
|
const result = applyOutcome.result;
|
|
355
271
|
applySpinner.stop(isDryRun ? 'Planned actions calculated' : 'Components applied');
|
|
356
272
|
logApplyResult(result, isDryRun);
|
|
273
|
+
// Keep the consumer jsconfig's chrome-module `paths` in step with the
|
|
274
|
+
// deployed module set (field report D3). Only after a clean apply —
|
|
275
|
+
// a rolled-back deploy must not advance the typecheck mapping either.
|
|
276
|
+
if (result.errors.length === 0 && getStepFailureCount(result) === 0) {
|
|
277
|
+
await reportJsconfigPathsSync(projectRoot, config, isDryRun);
|
|
278
|
+
}
|
|
357
279
|
// --- Step 2: Validate (read-only, runs even in dry-run to show what would fail) ---
|
|
358
280
|
if (options.skipValidate) {
|
|
359
281
|
const applyErrors = result.errors.length + getStepFailureCount(result);
|
|
@@ -158,6 +158,54 @@ async function refreshSingleOverride(projectRoot, name, options = {}) {
|
|
|
158
158
|
}, { dryRun });
|
|
159
159
|
return { results, currentVersion };
|
|
160
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* Refreshes every override sequentially, tallying merged/conflict/
|
|
163
|
+
* unchanged file counts. Per-override errors are expected (warned and
|
|
164
|
+
* recorded as failures) and do not abort the batch; only an error that
|
|
165
|
+
* escapes this function entirely warrants the caller's journal rollback.
|
|
166
|
+
*/
|
|
167
|
+
async function runBatchRefresh(projectRoot, overrideNames, options) {
|
|
168
|
+
let totalMerged = 0;
|
|
169
|
+
let totalConflicts = 0;
|
|
170
|
+
let totalUnchanged = 0;
|
|
171
|
+
let totalSkipped = 0;
|
|
172
|
+
const conflictComponents = [];
|
|
173
|
+
const failedOverrides = [];
|
|
174
|
+
for (const overrideName of overrideNames) {
|
|
175
|
+
try {
|
|
176
|
+
const { results } = await refreshSingleOverride(projectRoot, overrideName, options);
|
|
177
|
+
if (results.length === 0) {
|
|
178
|
+
totalSkipped++;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
for (const r of results) {
|
|
182
|
+
if (r.status === 'merged')
|
|
183
|
+
totalMerged++;
|
|
184
|
+
else if (r.status === 'conflict') {
|
|
185
|
+
totalConflicts++;
|
|
186
|
+
if (!conflictComponents.includes(overrideName)) {
|
|
187
|
+
conflictComponents.push(overrideName);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
else if (r.status === 'unchanged')
|
|
191
|
+
totalUnchanged++;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
const message = toError(error).message;
|
|
196
|
+
warn(`${overrideName}: ${message}`);
|
|
197
|
+
failedOverrides.push({ name: overrideName, message });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
totalMerged,
|
|
202
|
+
totalConflicts,
|
|
203
|
+
totalUnchanged,
|
|
204
|
+
totalSkipped,
|
|
205
|
+
conflictComponents,
|
|
206
|
+
failedOverrides,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
161
209
|
/**
|
|
162
210
|
* Runs the furnace refresh command to merge upstream Firefox changes into
|
|
163
211
|
* an override component using three-way merge.
|
|
@@ -208,12 +256,6 @@ export async function furnaceRefreshCommand(projectRoot, name, options = {}) {
|
|
|
208
256
|
outro('Done');
|
|
209
257
|
return;
|
|
210
258
|
}
|
|
211
|
-
let totalMerged = 0;
|
|
212
|
-
let totalConflicts = 0;
|
|
213
|
-
let totalUnchanged = 0;
|
|
214
|
-
let totalSkipped = 0;
|
|
215
|
-
const conflictComponents = [];
|
|
216
|
-
const failedOverrides = [];
|
|
217
259
|
// Snapshot furnace.json before the batch loop so an unexpected failure
|
|
218
260
|
// (process crash, unhandled error) can be recovered from. Per-component
|
|
219
261
|
// errors caught below are expected and do not trigger a restore — only
|
|
@@ -223,33 +265,9 @@ export async function furnaceRefreshCommand(projectRoot, name, options = {}) {
|
|
|
223
265
|
const furnacePaths = getFurnacePaths(projectRoot);
|
|
224
266
|
await snapshotFile(batchJournal, furnacePaths.furnaceConfig);
|
|
225
267
|
}
|
|
268
|
+
let tally;
|
|
226
269
|
try {
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
const { results } = await refreshSingleOverride(projectRoot, overrideName, options);
|
|
230
|
-
if (results.length === 0) {
|
|
231
|
-
totalSkipped++;
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
for (const r of results) {
|
|
235
|
-
if (r.status === 'merged')
|
|
236
|
-
totalMerged++;
|
|
237
|
-
else if (r.status === 'conflict') {
|
|
238
|
-
totalConflicts++;
|
|
239
|
-
if (!conflictComponents.includes(overrideName)) {
|
|
240
|
-
conflictComponents.push(overrideName);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
else if (r.status === 'unchanged')
|
|
244
|
-
totalUnchanged++;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
catch (error) {
|
|
248
|
-
const message = toError(error).message;
|
|
249
|
-
warn(`${overrideName}: ${message}`);
|
|
250
|
-
failedOverrides.push({ name: overrideName, message });
|
|
251
|
-
}
|
|
252
|
-
}
|
|
270
|
+
tally = await runBatchRefresh(projectRoot, overrideNames, options);
|
|
253
271
|
}
|
|
254
272
|
catch (error) {
|
|
255
273
|
// Unexpected batch-level failure: restore furnace.json to its
|
|
@@ -259,6 +277,8 @@ export async function furnaceRefreshCommand(projectRoot, name, options = {}) {
|
|
|
259
277
|
}
|
|
260
278
|
throw error;
|
|
261
279
|
}
|
|
280
|
+
const { totalMerged, totalConflicts, totalUnchanged, totalSkipped } = tally;
|
|
281
|
+
const { conflictComponents, failedOverrides } = tally;
|
|
262
282
|
const summary = `${overrideNames.length} override(s) processed, ${totalSkipped} already up-to-date\n` +
|
|
263
283
|
`${totalMerged} file(s) merged, ${totalUnchanged} unchanged, ${totalConflicts} conflict(s), ` +
|
|
264
284
|
`${failedOverrides.length} failed`;
|
|
@@ -3,6 +3,7 @@ import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
|
3
3
|
import { applyAllComponents } from '../../core/furnace-apply.js';
|
|
4
4
|
import { logApplyResult } from '../../core/furnace-apply-output.js';
|
|
5
5
|
import { furnaceConfigExists, loadFurnaceConfig } from '../../core/furnace-config.js';
|
|
6
|
+
import { reportJsconfigPathsSync } from '../../core/furnace-jsconfig.js';
|
|
6
7
|
import { runFurnaceMutation } from '../../core/furnace-operation.js';
|
|
7
8
|
import { findOverrideBaseVersionDrift, formatOverrideBaseVersionDriftWarning, } from '../../core/furnace-version-drift.js';
|
|
8
9
|
import { FurnaceError } from '../../errors/furnace.js';
|
|
@@ -68,6 +69,7 @@ export async function furnaceSyncCommand(projectRoot, options = {}) {
|
|
|
68
69
|
if (totalFailures > 0) {
|
|
69
70
|
throw new FurnaceError(`${totalFailures} component${totalFailures === 1 ? '' : 's'} failed to apply cleanly`);
|
|
70
71
|
}
|
|
72
|
+
await reportJsconfigPathsSync(projectRoot, config, false);
|
|
71
73
|
outro(`Sync complete — ${result.applied.length} applied, ${result.skipped.length} skipped`);
|
|
72
74
|
}
|
|
73
75
|
else {
|
|
@@ -200,6 +200,108 @@ function buildUntilFilenameSet(patches, until) {
|
|
|
200
200
|
}
|
|
201
201
|
return set;
|
|
202
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* Runs the manifest consistency check, scoped to the `--until` subset:
|
|
205
|
+
* global (manifest-level) issues always block, per-patch issues only
|
|
206
|
+
* block when the patch is in scope. Throws GeneralError with the repair
|
|
207
|
+
* hint when anything in scope is broken.
|
|
208
|
+
*/
|
|
209
|
+
async function assertScopedManifestConsistency(patchesDir, untilFilenameSet, until) {
|
|
210
|
+
const manifestConsistencyIssues = await validatePatchesManifestConsistency(patchesDir);
|
|
211
|
+
const scopedManifestIssues = until !== undefined
|
|
212
|
+
? manifestConsistencyIssues.filter((issue) =>
|
|
213
|
+
// Global (manifest-level) issues have no specific filename to scope
|
|
214
|
+
// against — a missing or unparseable patches.json blocks any
|
|
215
|
+
// import. Per-patch issues only block when the patch is in scope.
|
|
216
|
+
issue.code === 'manifest-missing' ||
|
|
217
|
+
issue.code === 'manifest-invalid' ||
|
|
218
|
+
untilFilenameSet.has(issue.filename))
|
|
219
|
+
: manifestConsistencyIssues;
|
|
220
|
+
if (scopedManifestIssues.length > 0) {
|
|
221
|
+
const issueSummary = scopedManifestIssues.map((issue) => issue.message).join('\n ');
|
|
222
|
+
throw new GeneralError('Patch manifest consistency check failed. Repair patches/patches.json before importing.\n' +
|
|
223
|
+
` ${issueSummary}\n\n` +
|
|
224
|
+
'Run "fireforge doctor --repair-patches-manifest" to rebuild the manifest from on-disk patch files.');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Prints advisory version-compatibility warnings for every in-scope patch
|
|
229
|
+
* whose recorded source version differs meaningfully from the configured
|
|
230
|
+
* Firefox version. Advisory only — never blocks the import.
|
|
231
|
+
*/
|
|
232
|
+
async function warnVersionCompatibility(projectRoot, manifest, untilFilenameSet, until) {
|
|
233
|
+
if (!manifest)
|
|
234
|
+
return;
|
|
235
|
+
const config = await loadConfig(projectRoot);
|
|
236
|
+
const currentVersion = config.firefox.version;
|
|
237
|
+
for (const patch of manifest.patches) {
|
|
238
|
+
// Scope the advisory warnings too: an operator running with --until
|
|
239
|
+
// doesn't need to see version warnings for patches outside the range.
|
|
240
|
+
if (until !== undefined && !untilFilenameSet.has(patch.filename))
|
|
241
|
+
continue;
|
|
242
|
+
const warning = checkVersionCompatibility(getPatchSourceVersion(patch), currentVersion);
|
|
243
|
+
if (warning) {
|
|
244
|
+
warn(`${patch.filename}: ${warning}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Patch-integrity gate: surfaces orphaned-modification issues scoped to
|
|
250
|
+
* the `--until` range and decides whether the import may proceed —
|
|
251
|
+
* `--force` continues with a warning, non-TTY refuses loudly, and an
|
|
252
|
+
* interactive operator is prompted. Returns false when the import should
|
|
253
|
+
* stop (the cancel outro has been printed).
|
|
254
|
+
*/
|
|
255
|
+
async function gateImportIntegrity(paths, untilFilenameSet, until, forceImport) {
|
|
256
|
+
const allIntegrityIssues = await validatePatchIntegrity(paths.patches, paths.engine);
|
|
257
|
+
const integrityIssues = until !== undefined
|
|
258
|
+
? allIntegrityIssues.filter((issue) => untilFilenameSet.has(issue.filename))
|
|
259
|
+
: allIntegrityIssues;
|
|
260
|
+
if (integrityIssues.length > 0) {
|
|
261
|
+
warn('\nPatch integrity issues detected:');
|
|
262
|
+
for (const issue of integrityIssues) {
|
|
263
|
+
warn(` ${issue.filename}: ${issue.message}`);
|
|
264
|
+
}
|
|
265
|
+
info('Run "fireforge doctor" for more details.');
|
|
266
|
+
if (forceImport) {
|
|
267
|
+
warn('Continuing because --force was provided. Integrity issues were not resolved.\n');
|
|
268
|
+
}
|
|
269
|
+
else if (!process.stdin.isTTY) {
|
|
270
|
+
throw new GeneralError(`Refusing to import while ${integrityIssues.length} patch integrity issue(s) are unresolved. ` +
|
|
271
|
+
`Fix the issues reported above (see "fireforge doctor") or re-run with --force to continue anyway.`);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
const shouldContinue = await confirm({
|
|
275
|
+
message: 'Patch integrity issues detected. Continuing may fail with cascading errors during patch application. Continue anyway?',
|
|
276
|
+
initialValue: false,
|
|
277
|
+
});
|
|
278
|
+
if (isCancel(shouldContinue) || !shouldContinue) {
|
|
279
|
+
outro('Import cancelled — fix the integrity issues and re-run');
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Dry-run rendering: lists the in-scope patches (or the bare count when no
|
|
288
|
+
* manifest exists) and prints the dry-run outro.
|
|
289
|
+
*/
|
|
290
|
+
function renderImportDryRun(manifest, untilFilenameSet, until, patchCount) {
|
|
291
|
+
if (manifest) {
|
|
292
|
+
const patches = until !== undefined
|
|
293
|
+
? manifest.patches.filter((p) => untilFilenameSet.has(p.filename))
|
|
294
|
+
: manifest.patches;
|
|
295
|
+
info(`\n[dry-run] Would apply ${patches.length} patch(es) in order:`);
|
|
296
|
+
for (const patch of patches) {
|
|
297
|
+
info(` ${patch.filename} (${patch.filesAffected.length} file${patch.filesAffected.length === 1 ? '' : 's'})`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
info(`\n[dry-run] Would apply ${patchCount} patch(es)`);
|
|
302
|
+
}
|
|
303
|
+
outro('Dry run complete — no changes made');
|
|
304
|
+
}
|
|
203
305
|
/**
|
|
204
306
|
* Runs the import command to apply patches.
|
|
205
307
|
* @param projectRoot - Root directory of the project
|
|
@@ -245,37 +347,8 @@ export async function importCommand(projectRoot, options = {}) {
|
|
|
245
347
|
const untilFilenameSet = buildUntilFilenameSet(manifest?.patches ?? [], options.until);
|
|
246
348
|
const scopedPatchCount = options.until !== undefined ? untilFilenameSet.size : patchCount;
|
|
247
349
|
info(`Found ${scopedPatchCount} patch${scopedPatchCount === 1 ? '' : 'es'} to apply${options.until !== undefined ? ` (up to ${options.until})` : ''}`);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
? manifestConsistencyIssues.filter((issue) =>
|
|
251
|
-
// Global (manifest-level) issues have no specific filename to scope
|
|
252
|
-
// against — a missing or unparseable patches.json blocks any
|
|
253
|
-
// import. Per-patch issues only block when the patch is in scope.
|
|
254
|
-
issue.code === 'manifest-missing' ||
|
|
255
|
-
issue.code === 'manifest-invalid' ||
|
|
256
|
-
untilFilenameSet.has(issue.filename))
|
|
257
|
-
: manifestConsistencyIssues;
|
|
258
|
-
if (scopedManifestIssues.length > 0) {
|
|
259
|
-
const issueSummary = scopedManifestIssues.map((issue) => issue.message).join('\n ');
|
|
260
|
-
throw new GeneralError('Patch manifest consistency check failed. Repair patches/patches.json before importing.\n' +
|
|
261
|
-
` ${issueSummary}\n\n` +
|
|
262
|
-
'Run "fireforge doctor --repair-patches-manifest" to rebuild the manifest from on-disk patch files.');
|
|
263
|
-
}
|
|
264
|
-
// Version compatibility warnings (advisory only)
|
|
265
|
-
if (manifest) {
|
|
266
|
-
const config = await loadConfig(projectRoot);
|
|
267
|
-
const currentVersion = config.firefox.version;
|
|
268
|
-
for (const patch of manifest.patches) {
|
|
269
|
-
// Scope the advisory warnings too: an operator running with --until
|
|
270
|
-
// doesn't need to see version warnings for patches outside the range.
|
|
271
|
-
if (options.until !== undefined && !untilFilenameSet.has(patch.filename))
|
|
272
|
-
continue;
|
|
273
|
-
const warning = checkVersionCompatibility(getPatchSourceVersion(patch), currentVersion);
|
|
274
|
-
if (warning) {
|
|
275
|
-
warn(`${patch.filename}: ${warning}`);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
350
|
+
await assertScopedManifestConsistency(paths.patches, untilFilenameSet, options.until);
|
|
351
|
+
await warnVersionCompatibility(projectRoot, manifest, untilFilenameSet, options.until);
|
|
279
352
|
// Validate patch integrity (detect orphaned modification patches). Warn
|
|
280
353
|
// and prompt the operator to confirm before proceeding — the legacy
|
|
281
354
|
// warn-and-continue behaviour hid the real root cause because import
|
|
@@ -286,49 +359,11 @@ export async function importCommand(projectRoot, options = {}) {
|
|
|
286
359
|
// integrity problems should not block importing an earlier good subset,
|
|
287
360
|
// which is exactly what operators reach for when the tail of the queue
|
|
288
361
|
// is broken and they want to keep working against an earlier checkpoint.
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
: allIntegrityIssues;
|
|
293
|
-
if (integrityIssues.length > 0) {
|
|
294
|
-
warn('\nPatch integrity issues detected:');
|
|
295
|
-
for (const issue of integrityIssues) {
|
|
296
|
-
warn(` ${issue.filename}: ${issue.message}`);
|
|
297
|
-
}
|
|
298
|
-
info('Run "fireforge doctor" for more details.');
|
|
299
|
-
if (forceImport) {
|
|
300
|
-
warn('Continuing because --force was provided. Integrity issues were not resolved.\n');
|
|
301
|
-
}
|
|
302
|
-
else if (!process.stdin.isTTY) {
|
|
303
|
-
throw new GeneralError(`Refusing to import while ${integrityIssues.length} patch integrity issue(s) are unresolved. ` +
|
|
304
|
-
`Fix the issues reported above (see "fireforge doctor") or re-run with --force to continue anyway.`);
|
|
305
|
-
}
|
|
306
|
-
else {
|
|
307
|
-
const shouldContinue = await confirm({
|
|
308
|
-
message: 'Patch integrity issues detected. Continuing may fail with cascading errors during patch application. Continue anyway?',
|
|
309
|
-
initialValue: false,
|
|
310
|
-
});
|
|
311
|
-
if (isCancel(shouldContinue) || !shouldContinue) {
|
|
312
|
-
outro('Import cancelled — fix the integrity issues and re-run');
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
// Dry-run: list patches that would be applied and exit
|
|
362
|
+
const integrityOk = await gateImportIntegrity(paths, untilFilenameSet, options.until, forceImport);
|
|
363
|
+
if (!integrityOk)
|
|
364
|
+
return;
|
|
318
365
|
if (isDryRun) {
|
|
319
|
-
|
|
320
|
-
const patches = options.until !== undefined
|
|
321
|
-
? manifest.patches.filter((p) => untilFilenameSet.has(p.filename))
|
|
322
|
-
: manifest.patches;
|
|
323
|
-
info(`\n[dry-run] Would apply ${patches.length} patch(es) in order:`);
|
|
324
|
-
for (const patch of patches) {
|
|
325
|
-
info(` ${patch.filename} (${patch.filesAffected.length} file${patch.filesAffected.length === 1 ? '' : 's'})`);
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
else {
|
|
329
|
-
info(`\n[dry-run] Would apply ${patchCount} patch(es)`);
|
|
330
|
-
}
|
|
331
|
-
outro('Dry run complete — no changes made');
|
|
366
|
+
renderImportDryRun(manifest, untilFilenameSet, options.until, patchCount);
|
|
332
367
|
return;
|
|
333
368
|
}
|
|
334
369
|
await checkUncommittedPatchFiles(paths.engine, paths.patches, forceImport);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getProjectPaths } from '../core/config.js';
|
|
2
|
-
import type { LintCommandOptions } from '
|
|
2
|
+
import type { LintCommandOptions } from '../types/commands/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Lints each patch in the queue as its own isolated diff, honouring
|
|
5
5
|
* per-patch `lintIgnore` entries. Cross-patch rules still run once over
|