@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
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { isBrandingManagedPath } from '../core/branding.js';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
5
|
+
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
5
6
|
import { getStatusWithCodes, isGitRepository } from '../core/git.js';
|
|
6
7
|
import { getUntrackedFilesInDir } from '../core/git-status.js';
|
|
7
8
|
import { isFileRegistered, matchesRegistrablePattern } from '../core/manifest-rules.js';
|
|
9
|
+
import { buildOwnershipTable, renderOwnershipTable } from '../core/ownership-table.js';
|
|
8
10
|
import { computePatchedContent } from '../core/patch-apply.js';
|
|
11
|
+
import { buildPatchQueueContext, collectNewFileCreatorsByPath } from '../core/patch-lint.js';
|
|
9
12
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
10
13
|
import { GeneralError } from '../errors/base.js';
|
|
11
14
|
import { toError } from '../utils/errors.js';
|
|
@@ -116,7 +119,7 @@ async function expandDirectoryEntries(files, engineDir) {
|
|
|
116
119
|
/**
|
|
117
120
|
* Classifies files into patch-backed, unmanaged, or branding buckets.
|
|
118
121
|
*/
|
|
119
|
-
async function classifyFiles(files, engineDir, patchesDir, binaryName) {
|
|
122
|
+
async function classifyFiles(files, engineDir, patchesDir, binaryName, furnacePrefixes) {
|
|
120
123
|
const manifest = await loadPatchesManifest(patchesDir);
|
|
121
124
|
// Build set of all patch-claimed file paths
|
|
122
125
|
const patchClaimedFiles = new Set();
|
|
@@ -134,6 +137,20 @@ async function classifyFiles(files, engineDir, patchesDir, binaryName) {
|
|
|
134
137
|
results.push({ ...entry, classification: 'branding' });
|
|
135
138
|
continue;
|
|
136
139
|
}
|
|
140
|
+
// Furnace-managed component paths
|
|
141
|
+
if (furnacePrefixes.size > 0) {
|
|
142
|
+
let isFurnace = false;
|
|
143
|
+
for (const prefix of furnacePrefixes) {
|
|
144
|
+
if (entry.file.startsWith(prefix)) {
|
|
145
|
+
isFurnace = true;
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (isFurnace) {
|
|
150
|
+
results.push({ ...entry, classification: 'furnace' });
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
137
154
|
// Not in any patch → unmanaged
|
|
138
155
|
if (!patchClaimedFiles.has(entry.file)) {
|
|
139
156
|
results.push({ ...entry, classification: 'unmanaged' });
|
|
@@ -169,20 +186,74 @@ async function classifyFiles(files, engineDir, patchesDir, binaryName) {
|
|
|
169
186
|
}
|
|
170
187
|
return results;
|
|
171
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Renders classified file status as machine-readable JSON to stdout.
|
|
191
|
+
*/
|
|
192
|
+
async function renderJsonStatus(files, paths, projectRoot, binaryName) {
|
|
193
|
+
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
194
|
+
const classified = await classifyFiles(files, paths.engine, paths.patches, binaryName, furnacePrefixes);
|
|
195
|
+
const output = classified.map((f) => ({
|
|
196
|
+
file: f.file,
|
|
197
|
+
status: f.status.trim(),
|
|
198
|
+
classification: f.classification,
|
|
199
|
+
}));
|
|
200
|
+
process.stdout.write(JSON.stringify(output, null, 2) + '\n');
|
|
201
|
+
}
|
|
172
202
|
/**
|
|
173
203
|
* Runs the status command to show modified files.
|
|
174
204
|
* @param projectRoot - Root directory of the project
|
|
175
205
|
* @param options - Status display options
|
|
176
206
|
*/
|
|
177
207
|
export async function statusCommand(projectRoot, options = {}) {
|
|
178
|
-
|
|
179
|
-
|
|
208
|
+
const modeCount = [options.raw, options.unmanaged, options.ownership, options.json].filter((v) => v === true).length;
|
|
209
|
+
if (modeCount > 1) {
|
|
210
|
+
throw new GeneralError('Cannot use --raw, --unmanaged, --ownership, and --json together. Pick at most one.');
|
|
180
211
|
}
|
|
181
|
-
if (!options.raw) {
|
|
212
|
+
if (!options.raw && !options.json) {
|
|
182
213
|
intro('FireForge Status');
|
|
183
214
|
}
|
|
184
215
|
const paths = getProjectPaths(projectRoot);
|
|
185
216
|
const config = await loadConfig(projectRoot);
|
|
217
|
+
// Ownership mode is a flat file→patch table; sources are the manifest's
|
|
218
|
+
// filesAffected, any worktree drift, and the cross-patch
|
|
219
|
+
// duplicate-new-file-creation map produced by walking each patch
|
|
220
|
+
// body. The latter is the alignment fix between `status --ownership`
|
|
221
|
+
// and `fireforge verify` — see buildOwnershipTable's header comment.
|
|
222
|
+
// Runs before the default classify path so we can short-circuit
|
|
223
|
+
// without computing patch-backed state.
|
|
224
|
+
if (options.ownership) {
|
|
225
|
+
if (!(await pathExists(paths.engine))) {
|
|
226
|
+
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
227
|
+
}
|
|
228
|
+
const manifest = await loadPatchesManifest(paths.patches);
|
|
229
|
+
const rawFilesOwnership = (await isGitRepository(paths.engine))
|
|
230
|
+
? await expandDirectoryEntries(await getStatusWithCodes(paths.engine), paths.engine)
|
|
231
|
+
: [];
|
|
232
|
+
// Only walk the patch bodies when the directory actually exists.
|
|
233
|
+
// Fresh projects with no patch queue yet pass through with an empty
|
|
234
|
+
// creators map, which degrades to the old filesAffected-only
|
|
235
|
+
// behavior for the empty case.
|
|
236
|
+
const newFileCreatorsByPath = (await pathExists(paths.patches))
|
|
237
|
+
? collectNewFileCreatorsByPath(await buildPatchQueueContext(paths.patches))
|
|
238
|
+
: new Map();
|
|
239
|
+
const rows = buildOwnershipTable(manifest?.patches ?? [], rawFilesOwnership, newFileCreatorsByPath);
|
|
240
|
+
renderOwnershipTable(rows);
|
|
241
|
+
const conflictCount = rows.filter((r) => r.conflict).length;
|
|
242
|
+
const unmanagedCount = rows.filter((r) => r.unmanaged).length;
|
|
243
|
+
const managedCount = rows.filter((r) => !r.unmanaged).length;
|
|
244
|
+
const parts = [`${managedCount} managed`];
|
|
245
|
+
if (conflictCount > 0)
|
|
246
|
+
parts.push(`${conflictCount} conflict${conflictCount === 1 ? '' : 's'}`);
|
|
247
|
+
if (unmanagedCount > 0)
|
|
248
|
+
parts.push(`${unmanagedCount} unmanaged`);
|
|
249
|
+
outro(parts.join(', '));
|
|
250
|
+
if (conflictCount > 0) {
|
|
251
|
+
throw new GeneralError(`${conflictCount} path(s) are claimed by more than one patch. ` +
|
|
252
|
+
'Run "fireforge verify" for full details, then use "re-export --files" or ' +
|
|
253
|
+
'"patch delete" to resolve.');
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
186
257
|
// Check if engine exists
|
|
187
258
|
if (!(await pathExists(paths.engine))) {
|
|
188
259
|
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
@@ -203,11 +274,18 @@ export async function statusCommand(projectRoot, options = {}) {
|
|
|
203
274
|
renderRawStatus(files);
|
|
204
275
|
return;
|
|
205
276
|
}
|
|
277
|
+
// JSON mode and default mode both need classification
|
|
278
|
+
if (options.json) {
|
|
279
|
+
await renderJsonStatus(files, paths, projectRoot, config.binaryName);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
206
282
|
// Patch-aware classification
|
|
207
|
-
const
|
|
283
|
+
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
284
|
+
const classified = await classifyFiles(files, paths.engine, paths.patches, config.binaryName, furnacePrefixes);
|
|
208
285
|
const unmanagedFiles = classified.filter((f) => f.classification === 'unmanaged');
|
|
209
286
|
const patchBackedFiles = classified.filter((f) => f.classification === 'patch-backed');
|
|
210
287
|
const brandingFiles = classified.filter((f) => f.classification === 'branding');
|
|
288
|
+
const furnaceFiles = classified.filter((f) => f.classification === 'furnace');
|
|
211
289
|
// --unmanaged mode: only show unmanaged
|
|
212
290
|
if (options.unmanaged) {
|
|
213
291
|
info(`${unmanagedFiles.length} unmanaged file${unmanagedFiles.length === 1 ? '' : 's'} (${files.length} total modified):\n`);
|
|
@@ -242,7 +320,16 @@ export async function statusCommand(projectRoot, options = {}) {
|
|
|
242
320
|
warn('Tool-managed branding changes:');
|
|
243
321
|
printStatusGroups(brandingFiles);
|
|
244
322
|
}
|
|
245
|
-
if (
|
|
323
|
+
if (furnaceFiles.length > 0) {
|
|
324
|
+
if (unmanagedFiles.length > 0 || patchBackedFiles.length > 0 || brandingFiles.length > 0)
|
|
325
|
+
info('');
|
|
326
|
+
warn('Furnace-managed component changes:');
|
|
327
|
+
printStatusGroups(furnaceFiles);
|
|
328
|
+
}
|
|
329
|
+
if (unmanagedFiles.length === 0 &&
|
|
330
|
+
patchBackedFiles.length === 0 &&
|
|
331
|
+
brandingFiles.length === 0 &&
|
|
332
|
+
furnaceFiles.length === 0) {
|
|
246
333
|
info('No changes');
|
|
247
334
|
}
|
|
248
335
|
const parts = [];
|
|
@@ -252,6 +339,8 @@ export async function statusCommand(projectRoot, options = {}) {
|
|
|
252
339
|
parts.push(`${patchBackedFiles.length} patch-backed`);
|
|
253
340
|
if (brandingFiles.length > 0)
|
|
254
341
|
parts.push(`${brandingFiles.length} branding`);
|
|
342
|
+
if (furnaceFiles.length > 0)
|
|
343
|
+
parts.push(`${furnaceFiles.length} furnace`);
|
|
255
344
|
outro(parts.join(', '));
|
|
256
345
|
}
|
|
257
346
|
/** Registers the status command on the CLI program. */
|
|
@@ -261,6 +350,8 @@ export function registerStatus(program, { getProjectRoot, withErrorHandling }) {
|
|
|
261
350
|
.description('Show modified files in engine/')
|
|
262
351
|
.option('--raw', 'Show raw worktree status without patch classification')
|
|
263
352
|
.option('--unmanaged', 'Show only unmanaged changes (not covered by patches or tools)')
|
|
353
|
+
.option('--ownership', 'Show a flat path → owning patch table (flags files claimed by multiple patches)')
|
|
354
|
+
.option('--json', 'Output classified file status as JSON')
|
|
264
355
|
.action(withErrorHandling(async (options) => {
|
|
265
356
|
await statusCommand(getProjectRoot(), options);
|
|
266
357
|
}));
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { prepareBuildEnvironment } from '../core/build-prepare.js';
|
|
4
4
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
5
|
-
import {
|
|
6
|
-
import { buildArtifactMismatchMessage, buildUI, generateMozconfig, hasBuildArtifacts, testWithOutput, } from '../core/mach.js';
|
|
5
|
+
import { buildArtifactMismatchMessage, buildUI, hasBuildArtifacts, testWithOutput, } from '../core/mach.js';
|
|
7
6
|
import { GeneralError } from '../errors/base.js';
|
|
8
7
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
9
8
|
import { pathExists } from '../utils/fs.js';
|
|
@@ -53,38 +52,6 @@ function hasStaleBuildArtifactsSignal(output) {
|
|
|
53
52
|
/resource:\/\/\/modules\/distribution\.sys\.mjs/i.test(output) ||
|
|
54
53
|
/browser\/branding\/[^/\s]+\/moz\.build/i.test(output));
|
|
55
54
|
}
|
|
56
|
-
async function prepareIncrementalTestBuild(projectRoot) {
|
|
57
|
-
const config = await loadConfig(projectRoot);
|
|
58
|
-
const paths = getProjectPaths(projectRoot);
|
|
59
|
-
const brandingConfig = {
|
|
60
|
-
name: config.name,
|
|
61
|
-
vendor: config.vendor,
|
|
62
|
-
appId: config.appId,
|
|
63
|
-
binaryName: config.binaryName,
|
|
64
|
-
};
|
|
65
|
-
if (!(await isBrandingSetup(paths.engine, brandingConfig))) {
|
|
66
|
-
const brandingSpinner = spinner('Setting up branding...');
|
|
67
|
-
try {
|
|
68
|
-
await setupBranding(paths.engine, brandingConfig);
|
|
69
|
-
brandingSpinner.stop('Branding configured');
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
brandingSpinner.error('Failed to set up branding');
|
|
73
|
-
throw error;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
const mozconfigSpinner = spinner('Generating mozconfig...');
|
|
77
|
-
try {
|
|
78
|
-
await generateMozconfig(paths.configs, paths.engine, config);
|
|
79
|
-
mozconfigSpinner.stop('mozconfig generated');
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
mozconfigSpinner.error('Failed to generate mozconfig');
|
|
83
|
-
throw error;
|
|
84
|
-
}
|
|
85
|
-
await cleanStories(paths.engine);
|
|
86
|
-
return { engineDir: paths.engine };
|
|
87
|
-
}
|
|
88
55
|
function handleNonZeroTestExit(result, normalizedPaths) {
|
|
89
56
|
if (result.exitCode === 0 || result.exitCode === 130)
|
|
90
57
|
return;
|
|
@@ -133,9 +100,10 @@ export async function testCommand(projectRoot, testPaths, options = {}) {
|
|
|
133
100
|
}
|
|
134
101
|
// Run incremental build if requested
|
|
135
102
|
if (options.build) {
|
|
136
|
-
const
|
|
103
|
+
const config = await loadConfig(projectRoot);
|
|
104
|
+
await prepareBuildEnvironment(projectRoot, paths, config);
|
|
137
105
|
const s = spinner('Running incremental build...');
|
|
138
|
-
const buildExitCode = await buildUI(
|
|
106
|
+
const buildExitCode = await buildUI(paths.engine);
|
|
139
107
|
if (buildExitCode !== 0) {
|
|
140
108
|
s.error('Pre-test build failed');
|
|
141
109
|
throw new BuildError('Pre-test build failed', 'mach build faster');
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fireforge verify` — read-only fsck for the patch queue.
|
|
3
|
+
*
|
|
4
|
+
* Combines the manifest consistency check (orphan files, missing entries,
|
|
5
|
+
* files-affected mismatch, duplicate entries) with the cross-patch lint
|
|
6
|
+
* rules (duplicate /dev/null creation, forward imports). Does not run
|
|
7
|
+
* `planExport` per patch — that is intentionally out of scope because it
|
|
8
|
+
* would couple verify to engine state and make the command too slow to be
|
|
9
|
+
* useful as a pre-flight gate. Engine-level patch application issues are
|
|
10
|
+
* still covered by the existing `fireforge doctor` and `fireforge import`
|
|
11
|
+
* paths.
|
|
12
|
+
*
|
|
13
|
+
* Exits non-zero when any error-severity finding is reported so CI can
|
|
14
|
+
* treat the output as pass/fail.
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from 'commander';
|
|
17
|
+
import type { CommandContext } from '../types/cli.js';
|
|
18
|
+
/**
|
|
19
|
+
* Runs the `verify` command: manifest consistency + cross-patch lint.
|
|
20
|
+
* Read-only; exits non-zero on any error-severity finding.
|
|
21
|
+
*
|
|
22
|
+
* @param projectRoot - Project root directory
|
|
23
|
+
*/
|
|
24
|
+
export declare function verifyCommand(projectRoot: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Registers the `verify` command on the CLI program.
|
|
27
|
+
*
|
|
28
|
+
* @param program - Commander root program
|
|
29
|
+
* @param context - Shared CLI registration context
|
|
30
|
+
*/
|
|
31
|
+
export declare function registerVerify(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* `fireforge verify` — read-only fsck for the patch queue.
|
|
4
|
+
*
|
|
5
|
+
* Combines the manifest consistency check (orphan files, missing entries,
|
|
6
|
+
* files-affected mismatch, duplicate entries) with the cross-patch lint
|
|
7
|
+
* rules (duplicate /dev/null creation, forward imports). Does not run
|
|
8
|
+
* `planExport` per patch — that is intentionally out of scope because it
|
|
9
|
+
* would couple verify to engine state and make the command too slow to be
|
|
10
|
+
* useful as a pre-flight gate. Engine-level patch application issues are
|
|
11
|
+
* still covered by the existing `fireforge doctor` and `fireforge import`
|
|
12
|
+
* paths.
|
|
13
|
+
*
|
|
14
|
+
* Exits non-zero when any error-severity finding is reported so CI can
|
|
15
|
+
* treat the output as pass/fail.
|
|
16
|
+
*/
|
|
17
|
+
import { getProjectPaths } from '../core/config.js';
|
|
18
|
+
import { buildPatchQueueContext, lintPatchQueue } from '../core/patch-lint.js';
|
|
19
|
+
import { loadPatchesManifest, validatePatchesManifestConsistency } from '../core/patch-manifest.js';
|
|
20
|
+
import { GeneralError } from '../errors/base.js';
|
|
21
|
+
import { pathExists } from '../utils/fs.js';
|
|
22
|
+
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
23
|
+
/**
|
|
24
|
+
* Reports duplicate `filesAffected` entries across patches — the manifest
|
|
25
|
+
* consistency check only flags per-patch duplicates and orphan files, not
|
|
26
|
+
* the case where two different patches claim the same path. `verify`
|
|
27
|
+
* surfaces that here so it can be caught before `export`, `re-export`, or
|
|
28
|
+
* `rebase` hit it.
|
|
29
|
+
*/
|
|
30
|
+
function detectCrossPatchFileClaims(manifestPatches) {
|
|
31
|
+
const claims = new Map();
|
|
32
|
+
for (const patch of manifestPatches) {
|
|
33
|
+
for (const file of patch.filesAffected) {
|
|
34
|
+
const existing = claims.get(file) ?? [];
|
|
35
|
+
existing.push(patch.filename);
|
|
36
|
+
claims.set(file, existing);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const results = [];
|
|
40
|
+
for (const [path, filenames] of claims) {
|
|
41
|
+
if (filenames.length > 1) {
|
|
42
|
+
results.push({ path, filenames });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return results;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Runs the `verify` command: manifest consistency + cross-patch lint.
|
|
49
|
+
* Read-only; exits non-zero on any error-severity finding.
|
|
50
|
+
*
|
|
51
|
+
* @param projectRoot - Project root directory
|
|
52
|
+
*/
|
|
53
|
+
export async function verifyCommand(projectRoot) {
|
|
54
|
+
intro('FireForge Verify');
|
|
55
|
+
const paths = getProjectPaths(projectRoot);
|
|
56
|
+
if (!(await pathExists(paths.patches))) {
|
|
57
|
+
info('No patches directory. Nothing to verify.');
|
|
58
|
+
outro('Verify clean');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
let errorCount = 0;
|
|
62
|
+
let warningCount = 0;
|
|
63
|
+
// 1. Manifest consistency: orphan patch files, missing entries,
|
|
64
|
+
// files-affected mismatch, duplicate entries, unparseable manifest.
|
|
65
|
+
const consistencyIssues = await validatePatchesManifestConsistency(paths.patches);
|
|
66
|
+
if (consistencyIssues.length > 0) {
|
|
67
|
+
warn(`Manifest consistency issues (${consistencyIssues.length}):`);
|
|
68
|
+
for (const issue of consistencyIssues) {
|
|
69
|
+
warn(` [${issue.code}] ${issue.message}`);
|
|
70
|
+
errorCount += 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// 2. Cross-patch file claims: two or more manifest entries listing the
|
|
74
|
+
// same path in filesAffected. Not caught by per-patch consistency.
|
|
75
|
+
const manifest = await loadPatchesManifest(paths.patches);
|
|
76
|
+
if (manifest) {
|
|
77
|
+
const crossClaims = detectCrossPatchFileClaims(manifest.patches);
|
|
78
|
+
if (crossClaims.length > 0) {
|
|
79
|
+
warn(`Cross-patch filesAffected conflicts (${crossClaims.length}):`);
|
|
80
|
+
for (const claim of crossClaims) {
|
|
81
|
+
warn(` ${claim.path} claimed by: ${claim.filenames.join(', ')}`);
|
|
82
|
+
errorCount += 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// 3. Cross-patch lint: duplicate /dev/null creation + forward imports.
|
|
87
|
+
const ctx = await buildPatchQueueContext(paths.patches);
|
|
88
|
+
const lintIssues = lintPatchQueue(ctx);
|
|
89
|
+
if (lintIssues.length > 0) {
|
|
90
|
+
warn(`Cross-patch lint issues (${lintIssues.length}):`);
|
|
91
|
+
for (const issue of lintIssues) {
|
|
92
|
+
const label = issue.severity === 'error' ? 'ERROR' : 'WARN';
|
|
93
|
+
warn(` ${label} [${issue.check}] ${issue.file}: ${issue.message}`);
|
|
94
|
+
if (issue.severity === 'error')
|
|
95
|
+
errorCount += 1;
|
|
96
|
+
else
|
|
97
|
+
warningCount += 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (errorCount === 0 && warningCount === 0) {
|
|
101
|
+
success('Patch queue is consistent.');
|
|
102
|
+
outro('Verify clean');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
info(`\nVerify: ${errorCount} error(s), ${warningCount} warning(s)`);
|
|
106
|
+
if (errorCount > 0) {
|
|
107
|
+
outro('Verify failed');
|
|
108
|
+
throw new GeneralError(`fireforge verify found ${errorCount} error(s). Fix these before running export/import/rebase.`);
|
|
109
|
+
}
|
|
110
|
+
outro('Verify passed with warnings');
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Registers the `verify` command on the CLI program.
|
|
114
|
+
*
|
|
115
|
+
* @param program - Commander root program
|
|
116
|
+
* @param context - Shared CLI registration context
|
|
117
|
+
*/
|
|
118
|
+
export function registerVerify(program, { getProjectRoot, withErrorHandling }) {
|
|
119
|
+
program
|
|
120
|
+
.command('verify')
|
|
121
|
+
.description('Read-only fsck for the patch queue (manifest + cross-patch lint)')
|
|
122
|
+
.action(withErrorHandling(async () => {
|
|
123
|
+
await verifyCommand(getProjectRoot());
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
* Shared pre-flight logic for build and package commands:
|
|
4
4
|
* story cleanup, branding setup, Furnace component application, and mozconfig generation.
|
|
5
5
|
*/
|
|
6
|
+
import { FurnaceError } from '../errors/furnace.js';
|
|
7
|
+
import { pathExists } from '../utils/fs.js';
|
|
6
8
|
import { spinner, warn } from '../utils/logger.js';
|
|
7
9
|
import { isBrandingSetup, setupBranding } from './branding.js';
|
|
8
10
|
import { applyAllComponents } from './furnace-apply.js';
|
|
9
|
-
import { furnaceConfigExists, loadFurnaceConfig } from './furnace-config.js';
|
|
11
|
+
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from './furnace-config.js';
|
|
12
|
+
import { runFurnaceMutation } from './furnace-operation.js';
|
|
10
13
|
import { cleanStories } from './furnace-stories.js';
|
|
11
14
|
import { generateMozconfig } from './mach.js';
|
|
12
15
|
/**
|
|
@@ -22,6 +25,17 @@ import { generateMozconfig } from './mach.js';
|
|
|
22
25
|
* @returns Preparation results
|
|
23
26
|
*/
|
|
24
27
|
export async function prepareBuildEnvironment(projectRoot, paths, config) {
|
|
28
|
+
// Block the build if Furnace has an unresolved repair marker. This prevents
|
|
29
|
+
// building against an engine that may be in an inconsistent state after a
|
|
30
|
+
// failed rollback.
|
|
31
|
+
const furnaceStatePath = getFurnacePaths(projectRoot).furnaceState;
|
|
32
|
+
if (await pathExists(furnaceStatePath)) {
|
|
33
|
+
const furnaceState = await loadFurnaceState(projectRoot);
|
|
34
|
+
if (furnaceState.pendingRepair) {
|
|
35
|
+
throw new FurnaceError(`Furnace has an unresolved repair marker (from ${furnaceState.pendingRepair.operation}). ` +
|
|
36
|
+
'Run "fireforge doctor --repair-furnace" to reconcile engine state before building.');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
25
39
|
// Clean stories before build to ensure they don't leak into production binary
|
|
26
40
|
await cleanStories(paths.engine);
|
|
27
41
|
// Set up custom branding directory and patch moz.configure
|
|
@@ -50,19 +64,26 @@ export async function prepareBuildEnvironment(projectRoot, paths, config) {
|
|
|
50
64
|
Object.keys(furnaceConfig.custom).length > 0;
|
|
51
65
|
if (hasComponents) {
|
|
52
66
|
const furnaceSpinner = spinner('Applying Furnace components...');
|
|
67
|
+
let result;
|
|
53
68
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
result = await runFurnaceMutation(projectRoot, 'apply-rollback', (ctx) => applyAllComponents(projectRoot, false, { operationContext: ctx }));
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
furnaceSpinner.error('Failed to apply Furnace components');
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
furnaceApplied = result.applied.length;
|
|
76
|
+
// Count entries that were "applied" but recorded step-level errors
|
|
77
|
+
// mid-apply (e.g. a post-step failure after file writes succeeded).
|
|
78
|
+
// These are distinct from `result.errors`, which captures
|
|
79
|
+
// components that failed before reaching the applied list at all.
|
|
80
|
+
// The sum of the two is the total count of failed components.
|
|
81
|
+
const appliedWithStepErrorsCount = result.applied.filter((entry) => (entry.stepErrors?.length ?? 0) > 0).length;
|
|
82
|
+
const totalApplyFailures = result.errors.length + appliedWithStepErrorsCount;
|
|
83
|
+
if (totalApplyFailures > 0) {
|
|
84
|
+
furnaceSpinner.error('Failed to apply Furnace components');
|
|
85
|
+
for (const err of result.errors) {
|
|
86
|
+
warn(`Furnace: ${err.name} — ${err.error}`);
|
|
66
87
|
}
|
|
67
88
|
for (const applied of result.applied) {
|
|
68
89
|
if (applied.stepErrors && applied.stepErrors.length > 0) {
|
|
@@ -71,10 +92,13 @@ export async function prepareBuildEnvironment(projectRoot, paths, config) {
|
|
|
71
92
|
}
|
|
72
93
|
}
|
|
73
94
|
}
|
|
95
|
+
throw new FurnaceError(`${totalApplyFailures} component${totalApplyFailures === 1 ? '' : 's'} failed to apply cleanly`);
|
|
74
96
|
}
|
|
75
|
-
|
|
76
|
-
furnaceSpinner.
|
|
77
|
-
|
|
97
|
+
if (furnaceApplied > 0) {
|
|
98
|
+
furnaceSpinner.stop(`Applied ${furnaceApplied} component${furnaceApplied === 1 ? '' : 's'}`);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
furnaceSpinner.stop('Components up to date');
|
|
78
102
|
}
|
|
79
103
|
}
|
|
80
104
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared destructive-operation contract: interactive confirmation, non-TTY
|
|
3
|
+
* refusal, dry-run plumbing, hard-refusal on structural conflicts, and a
|
|
4
|
+
* JSONL audit log.
|
|
5
|
+
*
|
|
6
|
+
* This exists because the repair primitives (`patch delete`, `patch reorder`,
|
|
7
|
+
* `re-export --files`) and the new `export --dry-run`/`export --order` flags
|
|
8
|
+
* all share the same dance: build a change summary, gate it behind a prompt,
|
|
9
|
+
* accept a `--yes` bypass for CI, accept a `--dry-run` no-op, and refuse
|
|
10
|
+
* outright when the change would introduce a structural conflict (e.g. a
|
|
11
|
+
* forward-import that later-patch lint would then block). Without a single
|
|
12
|
+
* helper, every new destructive command would re-implement this and drift.
|
|
13
|
+
*/
|
|
14
|
+
/** Filename of the audit log, relative to the patches directory. */
|
|
15
|
+
export declare const HISTORY_LOG_FILENAME = ".fireforge-history.jsonl";
|
|
16
|
+
/**
|
|
17
|
+
* A structural conflict that must block the operation even under `--force`.
|
|
18
|
+
*
|
|
19
|
+
* Intended for cases like "reorder would introduce a forward-import" or
|
|
20
|
+
* "delete would orphan a later patch's import" — situations where the
|
|
21
|
+
* operator almost certainly wants a different fix (re-export --files, etc.)
|
|
22
|
+
* rather than a bypass. `--force-unsafe` is the escape hatch when the
|
|
23
|
+
* operator genuinely accepts the risk.
|
|
24
|
+
*/
|
|
25
|
+
export interface ConflictReport {
|
|
26
|
+
/** Short one-line reason the operation is refused. */
|
|
27
|
+
reason: string;
|
|
28
|
+
/** Specific conflicts (patch names, file paths, lint findings). */
|
|
29
|
+
details: string[];
|
|
30
|
+
}
|
|
31
|
+
/** Inputs to {@link confirmDestructive}. */
|
|
32
|
+
export interface DestructiveOpInput {
|
|
33
|
+
/** Operation identifier, used in history entries (e.g. `patch-delete`). */
|
|
34
|
+
operation: string;
|
|
35
|
+
/** Short one-line title shown in the prompt. */
|
|
36
|
+
title: string;
|
|
37
|
+
/**
|
|
38
|
+
* Detailed change summary — every affected patch, file, or renumber row.
|
|
39
|
+
* Generic "proceed? [y/N]" is insufficient per the destructive-op contract;
|
|
40
|
+
* callers must list every concrete change here.
|
|
41
|
+
*/
|
|
42
|
+
summary: string[];
|
|
43
|
+
/** Whether the caller passed `--yes`. */
|
|
44
|
+
yes: boolean;
|
|
45
|
+
/** Whether the caller passed `--dry-run`. */
|
|
46
|
+
dryRun: boolean;
|
|
47
|
+
/** Whether the caller passed `--force-unsafe`. Only this flag bypasses conflicts. */
|
|
48
|
+
unsafeOverride?: boolean;
|
|
49
|
+
/** Structural conflicts that should block the operation unless `unsafeOverride`. */
|
|
50
|
+
conflicts?: ConflictReport | null;
|
|
51
|
+
}
|
|
52
|
+
/** Outcome of {@link confirmDestructive}. */
|
|
53
|
+
export type DestructiveOpResult = 'proceed' | 'dry-run' | 'cancelled';
|
|
54
|
+
/**
|
|
55
|
+
* Inputs to {@link appendHistory}. Entries are appended as one JSON record
|
|
56
|
+
* per line. Callers should only append after a mutation succeeds so
|
|
57
|
+
* rolled-back failures never leave ghost entries.
|
|
58
|
+
*/
|
|
59
|
+
export interface HistoryEntry {
|
|
60
|
+
/** Operation identifier matching the DestructiveOpInput. */
|
|
61
|
+
operation: string;
|
|
62
|
+
/** Serializable argument payload (flags, targets, renumber map, etc.). */
|
|
63
|
+
args: Record<string, unknown>;
|
|
64
|
+
/** True if `--yes` was used. */
|
|
65
|
+
yes?: boolean;
|
|
66
|
+
/** True if `--force-unsafe` was used. */
|
|
67
|
+
unsafeOverride?: boolean;
|
|
68
|
+
/** Result: `'ok'` on success; other strings describe failure. */
|
|
69
|
+
result: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Executes the destructive-operation contract: summary → conflict refusal →
|
|
73
|
+
* dry-run / force / prompt / non-TTY refusal.
|
|
74
|
+
*
|
|
75
|
+
* Returns the decision for the caller to act on; callers must not execute the
|
|
76
|
+
* mutation when the result is `'dry-run'` or `'cancelled'`, and must call
|
|
77
|
+
* {@link appendHistory} only after the mutation succeeds (never on dry-run or
|
|
78
|
+
* cancellation).
|
|
79
|
+
*
|
|
80
|
+
* @param input - Operation description, flags, and optional conflict report
|
|
81
|
+
* @returns `'proceed'` to execute, `'dry-run'` to skip execution, or
|
|
82
|
+
* `'cancelled'` when the user declined the prompt
|
|
83
|
+
*/
|
|
84
|
+
export declare function confirmDestructive(input: DestructiveOpInput): Promise<DestructiveOpResult>;
|
|
85
|
+
/**
|
|
86
|
+
* Appends a single JSONL record to `patches/.fireforge-history.jsonl`.
|
|
87
|
+
*
|
|
88
|
+
* Call order matters: append only after the mutation succeeds, never
|
|
89
|
+
* pre-emptively, so rolled-back failures do not leave ghost entries. The log
|
|
90
|
+
* is append-only and advisory — no code path reads it back; it exists purely
|
|
91
|
+
* so operators have a post-hoc audit trail when something goes wrong.
|
|
92
|
+
*
|
|
93
|
+
* @param patchesDir - Path to the patches directory
|
|
94
|
+
* @param entry - Serializable history record
|
|
95
|
+
*/
|
|
96
|
+
export declare function appendHistory(patchesDir: string, entry: HistoryEntry): Promise<void>;
|