@hominis/fireforge 0.39.0 → 0.41.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 +100 -0
- package/README.md +35 -5
- package/dist/bin/fireforge.js +25 -4
- package/dist/build-info.json +9 -0
- package/dist/src/cli.js +27 -3
- package/dist/src/commands/bootstrap.js +5 -1
- package/dist/src/commands/discard.js +2 -22
- package/dist/src/commands/doctor/post-rebase-audit.js +27 -4
- package/dist/src/commands/doctor-check-core.d.ts +20 -0
- package/dist/src/commands/doctor-check-core.js +28 -0
- package/dist/src/commands/doctor-external-toolchains.js +2 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +144 -46
- package/dist/src/commands/doctor-furnace.js +13 -11
- package/dist/src/commands/doctor-orphaned-harness.js +2 -1
- package/dist/src/commands/doctor.js +2 -2
- package/dist/src/commands/export-all.js +9 -1
- package/dist/src/commands/export-flow.js +27 -9
- package/dist/src/commands/export-placement-conflicts.d.ts +17 -0
- package/dist/src/commands/export-placement-conflicts.js +74 -0
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.d.ts +8 -1
- package/dist/src/commands/export-shared.js +30 -10
- package/dist/src/commands/export.js +2 -2
- package/dist/src/commands/furnace/apply.js +4 -3
- package/dist/src/commands/furnace/chrome-doc-remove.js +13 -2
- package/dist/src/commands/furnace/chrome-doc.js +13 -2
- package/dist/src/commands/furnace/create-browser-test.d.ts +5 -0
- package/dist/src/commands/furnace/create-browser-test.js +5 -0
- package/dist/src/commands/furnace/create.js +2 -2
- package/dist/src/commands/furnace/diff.js +2 -1
- package/dist/src/commands/furnace/init.js +4 -6
- package/dist/src/commands/furnace/override.js +2 -2
- package/dist/src/commands/furnace/remove-mutations.d.ts +40 -0
- package/dist/src/commands/furnace/remove-mutations.js +171 -0
- package/dist/src/commands/furnace/remove.js +21 -128
- package/dist/src/commands/furnace/rename-browser-test.js +54 -5
- package/dist/src/commands/furnace/scan.js +27 -36
- package/dist/src/commands/furnace/status.js +30 -28
- package/dist/src/commands/furnace/validate.js +25 -18
- package/dist/src/commands/import.js +6 -20
- package/dist/src/commands/lint-per-patch.js +73 -51
- package/dist/src/commands/lint-per-run-checkjs.d.ts +64 -0
- package/dist/src/commands/lint-per-run-checkjs.js +102 -0
- package/dist/src/commands/lint-report.d.ts +21 -0
- package/dist/src/commands/lint-report.js +62 -0
- package/dist/src/commands/lint.js +26 -22
- package/dist/src/commands/manifest.js +3 -1
- package/dist/src/commands/package.js +9 -16
- package/dist/src/commands/patch/compact.js +5 -5
- package/dist/src/commands/patch/delete.js +5 -5
- package/dist/src/commands/patch/lint-ignore.js +3 -2
- package/dist/src/commands/patch/move-files-create.js +22 -4
- package/dist/src/commands/patch/move-files-into.js +14 -10
- package/dist/src/commands/patch/move-files.js +6 -5
- package/dist/src/commands/patch/rename.d.ts +2 -2
- package/dist/src/commands/patch/rename.js +152 -51
- package/dist/src/commands/patch/reorder.d.ts +9 -0
- package/dist/src/commands/patch/reorder.js +14 -10
- package/dist/src/commands/patch/split-plan.d.ts +13 -6
- package/dist/src/commands/patch/split-plan.js +22 -12
- package/dist/src/commands/patch/split.d.ts +1 -1
- package/dist/src/commands/patch/split.js +23 -9
- package/dist/src/commands/patch/staged-dependency-validate.d.ts +15 -0
- package/dist/src/commands/patch/staged-dependency-validate.js +49 -0
- package/dist/src/commands/patch/staged-dependency.js +112 -14
- package/dist/src/commands/re-export-adjacent.d.ts +34 -0
- package/dist/src/commands/re-export-adjacent.js +105 -0
- package/dist/src/commands/re-export-bulk-scan.d.ts +13 -0
- package/dist/src/commands/re-export-bulk-scan.js +142 -2
- package/dist/src/commands/re-export-drift.d.ts +74 -0
- package/dist/src/commands/re-export-drift.js +234 -0
- package/dist/src/commands/re-export-files.js +5 -3
- package/dist/src/commands/re-export-lint.d.ts +97 -0
- package/dist/src/commands/re-export-lint.js +210 -0
- package/dist/src/commands/re-export-options.js +11 -0
- package/dist/src/commands/re-export-register.d.ts +18 -0
- package/dist/src/commands/re-export-register.js +61 -0
- package/dist/src/commands/re-export-scan.js +13 -7
- package/dist/src/commands/re-export-single.d.ts +20 -0
- package/dist/src/commands/re-export-single.js +261 -0
- package/dist/src/commands/re-export.d.ts +0 -4
- package/dist/src/commands/re-export.js +123 -289
- package/dist/src/commands/rebase/abort.js +13 -4
- package/dist/src/commands/rebase/continue.js +11 -4
- package/dist/src/commands/rebase/index.js +19 -29
- package/dist/src/commands/rebase/patch-loop.js +12 -0
- package/dist/src/commands/reset.js +4 -23
- package/dist/src/commands/resolve.js +2 -9
- package/dist/src/commands/run.js +29 -23
- package/dist/src/commands/source.js +3 -9
- package/dist/src/commands/status-check.d.ts +48 -0
- package/dist/src/commands/status-check.js +106 -0
- package/dist/src/commands/status-json.d.ts +31 -0
- package/dist/src/commands/status-json.js +76 -0
- package/dist/src/commands/status-output.d.ts +1 -0
- package/dist/src/commands/status-output.js +10 -1
- package/dist/src/commands/status-ownership.d.ts +52 -0
- package/dist/src/commands/status-ownership.js +57 -0
- package/dist/src/commands/status.js +190 -170
- package/dist/src/commands/test-build-phase.d.ts +19 -0
- package/dist/src/commands/test-build-phase.js +168 -0
- package/dist/src/commands/test-diagnose.js +30 -2
- package/dist/src/commands/test-modes.d.ts +9 -1
- package/dist/src/commands/test-modes.js +46 -13
- package/dist/src/commands/test-register.js +59 -27
- package/dist/src/commands/test-run.d.ts +29 -3
- package/dist/src/commands/test-run.js +59 -7
- package/dist/src/commands/test-verdict.d.ts +32 -0
- package/dist/src/commands/test-verdict.js +48 -0
- package/dist/src/commands/test.d.ts +9 -0
- package/dist/src/commands/test.js +100 -123
- package/dist/src/commands/token-coverage.js +4 -9
- package/dist/src/commands/token.js +4 -1
- package/dist/src/commands/tree.d.ts +19 -0
- package/dist/src/commands/tree.js +320 -0
- package/dist/src/commands/verify.js +2 -2
- package/dist/src/commands/watch.js +22 -21
- package/dist/src/commands/wire.js +1 -1
- package/dist/src/core/ast-utils.d.ts +11 -0
- package/dist/src/core/ast-utils.js +11 -0
- package/dist/src/core/build-audit-platform.js +6 -1
- package/dist/src/core/build-audit-registration.js +2 -0
- package/dist/src/core/build-audit-resolve.d.ts +13 -0
- package/dist/src/core/build-audit-resolve.js +34 -13
- package/dist/src/core/build-audit.js +4 -20
- package/dist/src/core/build-baseline-types.d.ts +29 -8
- package/dist/src/core/build-baseline-types.js +2 -1
- package/dist/src/core/build-baseline.d.ts +10 -2
- package/dist/src/core/build-baseline.js +39 -8
- package/dist/src/core/build-prepare.d.ts +4 -0
- package/dist/src/core/build-prepare.js +7 -1
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-test-toolchains.js +3 -2
- package/dist/src/core/config-validate.js +45 -30
- package/dist/src/core/config.js +16 -3
- package/dist/src/core/coverage-extend.d.ts +89 -0
- package/dist/src/core/coverage-extend.js +188 -0
- package/dist/src/core/destructive.js +3 -1
- package/dist/src/core/discard-baseline.js +18 -4
- package/dist/src/core/engine-precondition.d.ts +35 -0
- package/dist/src/core/engine-precondition.js +50 -0
- package/dist/src/core/engine-session-lock.d.ts +38 -1
- package/dist/src/core/engine-session-lock.js +78 -4
- package/dist/src/core/file-lock.d.ts +7 -0
- package/dist/src/core/file-lock.js +20 -30
- package/dist/src/core/firefox.js +5 -2
- package/dist/src/core/furnace-apply-helpers.d.ts +12 -1
- package/dist/src/core/furnace-apply-helpers.js +12 -3
- package/dist/src/core/furnace-apply-output.js +9 -0
- package/dist/src/core/furnace-apply-overwrite-warn.d.ts +63 -0
- package/dist/src/core/furnace-apply-overwrite-warn.js +123 -0
- package/dist/src/core/furnace-apply-undeploy-actions.d.ts +11 -0
- package/dist/src/core/furnace-apply-undeploy-actions.js +49 -0
- package/dist/src/core/furnace-apply.d.ts +16 -4
- package/dist/src/core/furnace-apply.js +39 -43
- package/dist/src/core/furnace-config.js +2 -0
- package/dist/src/core/furnace-css-fragments.js +2 -0
- package/dist/src/core/furnace-jsconfig.js +14 -7
- package/dist/src/core/furnace-registration-ast.js +8 -8
- package/dist/src/core/furnace-registration-remove.js +6 -3
- package/dist/src/core/furnace-registration.js +9 -12
- package/dist/src/core/furnace-rollback.js +1 -1
- package/dist/src/core/furnace-scanner.js +2 -1
- package/dist/src/core/furnace-stale-export.js +2 -1
- package/dist/src/core/furnace-state-persist.d.ts +4 -6
- package/dist/src/core/furnace-validate-compatibility.js +4 -7
- package/dist/src/core/furnace-validate-helpers.js +33 -1
- package/dist/src/core/furnace-validate-registration.js +16 -0
- package/dist/src/core/furnace-validate.js +4 -2
- package/dist/src/core/git-diff.js +37 -2
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +49 -14
- package/dist/src/core/mach-build-artifacts.d.ts +63 -1
- package/dist/src/core/mach-build-artifacts.js +108 -2
- package/dist/src/core/mach-resource-shim.js +1 -0
- package/dist/src/core/marionette-port.d.ts +16 -0
- package/dist/src/core/marionette-port.js +93 -8
- package/dist/src/core/marionette-preflight.js +6 -5
- package/dist/src/core/patch-apply.d.ts +20 -13
- package/dist/src/core/patch-apply.js +32 -33
- package/dist/src/core/patch-export.d.ts +2 -2
- package/dist/src/core/patch-export.js +4 -6
- package/dist/src/core/patch-lint-checkjs.d.ts +42 -2
- package/dist/src/core/patch-lint-checkjs.js +132 -8
- package/dist/src/core/patch-lint-chrome-jsdoc.js +9 -0
- package/dist/src/core/patch-lint-creators.d.ts +30 -0
- package/dist/src/core/patch-lint-creators.js +45 -0
- package/dist/src/core/patch-lint-cross.d.ts +1 -17
- package/dist/src/core/patch-lint-cross.js +16 -32
- package/dist/src/core/patch-lint-css.js +64 -16
- package/dist/src/core/patch-lint-diff.d.ts +8 -0
- package/dist/src/core/patch-lint-diff.js +31 -0
- package/dist/src/core/patch-lint-jsdoc.d.ts +3 -1
- package/dist/src/core/patch-lint-jsdoc.js +15 -5
- package/dist/src/core/patch-lint-module-registration.d.ts +15 -0
- package/dist/src/core/patch-lint-module-registration.js +88 -0
- package/dist/src/core/patch-lint-mozbuild.js +99 -11
- package/dist/src/core/patch-lint-ownership.d.ts +21 -0
- package/dist/src/core/patch-lint-ownership.js +30 -0
- package/dist/src/core/patch-lint-reexports.d.ts +2 -1
- package/dist/src/core/patch-lint-reexports.js +1 -0
- package/dist/src/core/patch-lint.d.ts +31 -0
- package/dist/src/core/patch-lint.js +114 -8
- package/dist/src/core/patch-lock.d.ts +12 -1
- package/dist/src/core/patch-lock.js +40 -4
- package/dist/src/core/patch-manifest-consistency.js +15 -3
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-parse.d.ts +8 -0
- package/dist/src/core/patch-parse.js +10 -0
- package/dist/src/core/patch-policy.d.ts +7 -0
- package/dist/src/core/patch-policy.js +4 -1
- package/dist/src/core/patch-transform.d.ts +10 -0
- package/dist/src/core/patch-transform.js +13 -2
- package/dist/src/core/rebase-session.d.ts +44 -2
- package/dist/src/core/rebase-session.js +114 -17
- package/dist/src/core/register-browser-content.js +2 -1
- package/dist/src/core/register-shared-css.js +2 -1
- package/dist/src/core/smoke-patterns.d.ts +24 -16
- package/dist/src/core/smoke-patterns.js +26 -14
- package/dist/src/core/status-binary.d.ts +55 -0
- package/dist/src/core/status-binary.js +124 -0
- package/dist/src/core/status-classify.d.ts +22 -1
- package/dist/src/core/status-classify.js +112 -70
- package/dist/src/core/test-harness-crash.d.ts +32 -1
- package/dist/src/core/test-harness-crash.js +59 -4
- package/dist/src/core/test-path-scope.js +2 -0
- package/dist/src/core/test-stale-check.js +1 -20
- package/dist/src/core/token-category.d.ts +40 -0
- package/dist/src/core/token-category.js +264 -0
- package/dist/src/core/token-manager.d.ts +7 -0
- package/dist/src/core/token-manager.js +48 -196
- package/dist/src/core/toolchain-preflight.js +24 -26
- package/dist/src/core/tree-cow.d.ts +18 -0
- package/dist/src/core/tree-cow.js +71 -0
- package/dist/src/core/tree-guard.d.ts +35 -0
- package/dist/src/core/tree-guard.js +179 -0
- package/dist/src/core/tree-store.d.ts +129 -0
- package/dist/src/core/tree-store.js +452 -0
- package/dist/src/core/typecheck-shim.d.ts +14 -0
- package/dist/src/core/typecheck-shim.js +41 -2
- package/dist/src/core/typecheck.js +2 -1
- package/dist/src/core/wire-destroy.js +14 -3
- package/dist/src/core/wire-init.js +37 -40
- package/dist/src/core/wire-subscript.js +11 -3
- package/dist/src/core/wire-utils.js +4 -4
- package/dist/src/core/xpcshell-appdir.js +3 -0
- package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
- package/dist/src/core/xpcshell-profile-dir.js +45 -0
- package/dist/src/errors/base.d.ts +13 -0
- package/dist/src/errors/base.js +13 -0
- package/dist/src/errors/git.d.ts +8 -0
- package/dist/src/errors/git.js +17 -0
- package/dist/src/errors/rebase.d.ts +13 -0
- package/dist/src/errors/rebase.js +17 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types/commands/options.d.ts +119 -3
- package/dist/src/types/commands/patches.d.ts +8 -0
- package/dist/src/types/config.d.ts +19 -0
- package/dist/src/types/furnace.d.ts +7 -0
- package/dist/src/utils/build-info.d.ts +31 -0
- package/dist/src/utils/build-info.js +98 -0
- package/dist/src/utils/concurrency.d.ts +8 -0
- package/dist/src/utils/concurrency.js +21 -0
- package/dist/src/utils/errors.d.ts +39 -0
- package/dist/src/utils/errors.js +55 -0
- package/dist/src/utils/fs.js +5 -12
- package/dist/src/utils/logger.d.ts +14 -2
- package/dist/src/utils/logger.js +43 -13
- package/dist/src/utils/options.d.ts +21 -4
- package/dist/src/utils/options.js +26 -6
- package/dist/src/utils/package-root.js +7 -4
- package/dist/src/utils/process-group.js +13 -2
- package/dist/src/utils/stdio-drain.d.ts +28 -0
- package/dist/src/utils/stdio-drain.js +54 -0
- package/dist/src/utils/validation.d.ts +33 -2
- package/dist/src/utils/validation.js +57 -2
- package/package.json +13 -13
|
@@ -1,257 +1,20 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
-
/* eslint-disable max-lines -- Re-export is at the guardrail; splitting register plumbing is unrelated to this fix. */
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
2
|
import { multiselect } from '@clack/prompts';
|
|
5
|
-
import { Command, Option } from 'commander';
|
|
6
3
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
7
|
-
import {
|
|
8
|
-
import { enforceFreshFurnaceSources } from '../core/furnace-stale-export.js';
|
|
4
|
+
import { collectFurnaceManagedPrefixes } from '../core/furnace-config.js';
|
|
9
5
|
import { isGitRepository } from '../core/git.js';
|
|
10
|
-
import {
|
|
11
|
-
import { getModifiedFilesInDir, getUntrackedFilesInDir } from '../core/git-status.js';
|
|
12
|
-
import { updatePatchAndMetadata } from '../core/patch-export.js';
|
|
13
|
-
import { buildPatchQueueContext } from '../core/patch-lint.js';
|
|
14
|
-
import { getClaimedFiles, loadPatchesManifest, resolvePatchIdentifier, stampPatchVersions, } from '../core/patch-manifest.js';
|
|
15
|
-
import { buildProjectedManifest, enforcePatchPolicy } from '../core/patch-policy.js';
|
|
6
|
+
import { loadPatchesManifest, resolvePatchIdentifier, stampPatchVersions, } from '../core/patch-manifest.js';
|
|
16
7
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
17
8
|
import { elapsedSince } from '../utils/elapsed.js';
|
|
18
9
|
import { toError } from '../utils/errors.js';
|
|
19
10
|
import { pathExists } from '../utils/fs.js';
|
|
20
11
|
import { cancel, info, intro, isCancel, outro, spinner, success, warn } from '../utils/logger.js';
|
|
21
|
-
import {
|
|
22
|
-
import { runPatchLint } from './export-shared.js';
|
|
23
|
-
import { loadScanFilesAssignments, withDryRunReExportLock } from './re-export-bulk-scan.js';
|
|
12
|
+
import { loadScanFilesAssignments, withDryRunPurityGuard, withDryRunReExportLock, } from './re-export-bulk-scan.js';
|
|
24
13
|
import { reExportFilesInPlace } from './re-export-files.js';
|
|
14
|
+
import { buildReExportLintContext, finishReExportLint } from './re-export-lint.js';
|
|
25
15
|
import { applyReExportFilesPositionalFolding, validateReExportOptionCombinations, } from './re-export-options.js';
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
const missingFiles = [];
|
|
29
|
-
for (const file of files) {
|
|
30
|
-
if (!(await pathExists(join(engineDir, file))))
|
|
31
|
-
missingFiles.push(file);
|
|
32
|
-
}
|
|
33
|
-
return missingFiles;
|
|
34
|
-
}
|
|
35
|
-
async function findLikelyNewSiblingFiles(args) {
|
|
36
|
-
const { currentFilesAffected, engineDir, manifest, patchFilename } = args;
|
|
37
|
-
const parentDirs = [...new Set(currentFilesAffected.map((file) => dirname(file)))];
|
|
38
|
-
const currentSet = new Set(currentFilesAffected);
|
|
39
|
-
const claimedByOthers = getClaimedFiles(manifest, patchFilename);
|
|
40
|
-
const candidates = new Set();
|
|
41
|
-
for (const dir of parentDirs) {
|
|
42
|
-
const [modifiedFiles, untrackedFiles] = await Promise.all([
|
|
43
|
-
getModifiedFilesInDir(engineDir, dir),
|
|
44
|
-
getUntrackedFilesInDir(engineDir, dir),
|
|
45
|
-
]);
|
|
46
|
-
for (const file of [...modifiedFiles, ...untrackedFiles]) {
|
|
47
|
-
if (currentSet.has(file) || claimedByOthers.has(file))
|
|
48
|
-
continue;
|
|
49
|
-
candidates.add(file);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return [...candidates].sort();
|
|
53
|
-
}
|
|
54
|
-
async function warnPlainReExportFileDrift(args) {
|
|
55
|
-
const { patch, paths, manifest, currentFilesAffected } = args;
|
|
56
|
-
const missingFiles = await findMissingFiles(paths.engine, currentFilesAffected);
|
|
57
|
-
if (missingFiles.length > 0) {
|
|
58
|
-
warn(`${patch.filename}: some files in patches.json no longer exist on disk ` +
|
|
59
|
-
`(${missingFiles.join(', ')}). Without --scan, re-export keeps the manifest's ` +
|
|
60
|
-
`filesAffected unchanged and the missing entries will be preserved — ` +
|
|
61
|
-
`\`fireforge verify\` may flag manifest inconsistency after this run.\n` +
|
|
62
|
-
` Re-run with --scan to reconcile filesAffected with the current worktree, ` +
|
|
63
|
-
`or pass --files <paths> to set the list explicitly.`);
|
|
64
|
-
}
|
|
65
|
-
const likelyNewFiles = await findLikelyNewSiblingFiles({
|
|
66
|
-
currentFilesAffected,
|
|
67
|
-
engineDir: paths.engine,
|
|
68
|
-
manifest,
|
|
69
|
-
patchFilename: patch.filename,
|
|
70
|
-
});
|
|
71
|
-
if (likelyNewFiles.length === 0)
|
|
72
|
-
return;
|
|
73
|
-
warn(`${patch.filename}: found ${likelyNewFiles.length} unowned changed sibling file${likelyNewFiles.length === 1 ? '' : 's'} near this patch. Plain re-export keeps filesAffected unchanged; add reviewed files explicitly with --scan-file.`);
|
|
74
|
-
for (const file of likelyNewFiles) {
|
|
75
|
-
info(` ${file} — fireforge re-export ${patch.filename} --scan --scan-file ${file}`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, config) {
|
|
79
|
-
let currentFilesAffected = [...patch.filesAffected];
|
|
80
|
-
// --- Scan for new/removed files ---
|
|
81
|
-
if (options.scan) {
|
|
82
|
-
const scanResult = await scanPatchFilesForReExport({
|
|
83
|
-
currentFilesAffected,
|
|
84
|
-
engineDir: paths.engine,
|
|
85
|
-
manifest,
|
|
86
|
-
patchFilename: patch.filename,
|
|
87
|
-
isDryRun,
|
|
88
|
-
...(options.scanFiles !== undefined ? { scanFiles: options.scanFiles } : {}),
|
|
89
|
-
});
|
|
90
|
-
// Forward-import gate at adoption time (runs in dry-run too): refuse
|
|
91
|
-
// to adopt unmanaged files that import modules created by LATER
|
|
92
|
-
// patches — the after-the-fact lint failure this leaves behind is the
|
|
93
|
-
// field-reported footgun. Applies to broad --scan, --scan-file, and
|
|
94
|
-
// the --scan-files bulk assignments alike.
|
|
95
|
-
await assertScanAdoptionsHaveNoForwardImports({
|
|
96
|
-
patchesDir: paths.patches,
|
|
97
|
-
engineDir: paths.engine,
|
|
98
|
-
patchFilename: patch.filename,
|
|
99
|
-
added: scanResult.added,
|
|
100
|
-
});
|
|
101
|
-
if (options.scanFiles === undefined) {
|
|
102
|
-
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
103
|
-
const proceed = await confirmBroadScanAdditions({
|
|
104
|
-
patchFilename: patch.filename,
|
|
105
|
-
added: scanResult.added,
|
|
106
|
-
isDryRun,
|
|
107
|
-
yes: options.yes === true,
|
|
108
|
-
isInteractive,
|
|
109
|
-
});
|
|
110
|
-
if (!proceed) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
currentFilesAffected = scanResult.updated;
|
|
115
|
-
}
|
|
116
|
-
else if (options.files === undefined) {
|
|
117
|
-
// Finding #16: when neither `--scan` nor `--files` is set and some
|
|
118
|
-
// of the manifest's claimed files no longer exist on disk, the
|
|
119
|
-
// re-export silently writes a refreshed body whose filesAffected
|
|
120
|
-
// still names the vanished paths. That is the documented contract,
|
|
121
|
-
// but it is also a footgun — a later `verify` then fails on
|
|
122
|
-
// manifest-consistency with no obvious trigger. Emit one advisory
|
|
123
|
-
// warning up-front when we can detect the drift cheaply, so the
|
|
124
|
-
// operator has a chance to re-run with `--scan` or `--files`
|
|
125
|
-
// before the stale filesAffected lands in patches.json.
|
|
126
|
-
await warnPlainReExportFileDrift({ patch, paths, manifest, currentFilesAffected });
|
|
127
|
-
}
|
|
128
|
-
// --- Explicit file-subset path ---
|
|
129
|
-
// When --files is given, the target filesAffected is authoritative — drop
|
|
130
|
-
// anything not in the list, add anything new. This is the surgical repair
|
|
131
|
-
// primitive that replaces hand-editing patches.json; the user has already
|
|
132
|
-
// acknowledged via confirmDestructive (done in the caller) that any drop
|
|
133
|
-
// is intentional.
|
|
134
|
-
if (options.files !== undefined) {
|
|
135
|
-
const requested = [...new Set(options.files)].sort();
|
|
136
|
-
currentFilesAffected = requested;
|
|
137
|
-
const removed = patch.filesAffected.filter((f) => !requested.includes(f));
|
|
138
|
-
const added = requested.filter((f) => !patch.filesAffected.includes(f));
|
|
139
|
-
for (const f of added)
|
|
140
|
-
info(` + ${f}`);
|
|
141
|
-
for (const f of removed)
|
|
142
|
-
info(` - ${f}`);
|
|
143
|
-
}
|
|
144
|
-
// Stale-furnace-source gate (0.37.0 item 4): re-export captures deployed
|
|
145
|
-
// engine copies, so a component source edited after the last furnace
|
|
146
|
-
// apply would land in the patch as its OLD deployed content. Refuse (or
|
|
147
|
-
// warn under --allow-stale-furnace) before diffing. Runs in dry-run too
|
|
148
|
-
// so the failure surfaces early.
|
|
149
|
-
await enforceFreshFurnaceSources(paths.root, currentFilesAffected, options.allowStaleFurnace === true, 're-export');
|
|
150
|
-
const missingFiles = await findMissingFiles(paths.engine, currentFilesAffected);
|
|
151
|
-
if (missingFiles.length === currentFilesAffected.length) {
|
|
152
|
-
warn(`Skipped ${patch.filename}: all affected files missing`);
|
|
153
|
-
warn(`Missing files: ${missingFiles.join(', ')}`);
|
|
154
|
-
return false;
|
|
155
|
-
}
|
|
156
|
-
if (missingFiles.length > 0) {
|
|
157
|
-
warn(`${patch.filename}: missing files will be skipped: ${missingFiles.join(', ')}`);
|
|
158
|
-
}
|
|
159
|
-
const missingSet = new Set(missingFiles);
|
|
160
|
-
const existingFiles = currentFilesAffected.filter((f) => !missingSet.has(f));
|
|
161
|
-
const diffContent = await getDiffForFilesAgainstHead(paths.engine, existingFiles);
|
|
162
|
-
assertScanFileAdditionsHaveDiffHunks({
|
|
163
|
-
diffContent,
|
|
164
|
-
patchFilename: patch.filename,
|
|
165
|
-
previousFilesAffected: patch.filesAffected,
|
|
166
|
-
scanFiles: options.scanFiles,
|
|
167
|
-
});
|
|
168
|
-
if (!diffContent.trim()) {
|
|
169
|
-
warn(`Skipped ${patch.filename}: no changes (files unchanged from HEAD)`);
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
// Thread the patch's own `lintIgnore` list through so the per-patch
|
|
173
|
-
// suppression honoured by export/export-all is also honoured here.
|
|
174
|
-
// Without this, `re-export` could not refresh an advisory-noisy but
|
|
175
|
-
// intentional patch (a cohesive branding bundle, a localised-resource
|
|
176
|
-
// pack) without either `--skip-lint` (too blunt) or falling through to
|
|
177
|
-
// the full `rebase` flow (which internally skips the lint pipeline).
|
|
178
|
-
// The paired `patch.tier` threads the explicit branding-threshold
|
|
179
|
-
// opt-in the same way, for the branding patch that also touches a
|
|
180
|
-
// non-allowlisted registration sibling.
|
|
181
|
-
//
|
|
182
|
-
// The CLI flags `--tier` and `--lint-ignore` participate too, with
|
|
183
|
-
// append/union semantics on the lint-ignore list (the operator's
|
|
184
|
-
// intuition for "I want this patch to also suppress X" — explicit
|
|
185
|
-
// removal lives on the `fireforge patch lint-ignore` subcommand).
|
|
186
|
-
// Computed before the lint pass so the new intent takes effect on
|
|
187
|
-
// this invocation, not the next one.
|
|
188
|
-
const existingIgnoreSet = new Set(patch.lintIgnore ?? []);
|
|
189
|
-
const flagIgnoreSet = new Set(options.lintIgnore ?? []);
|
|
190
|
-
const mergedIgnoreSet = new Set([...existingIgnoreSet, ...flagIgnoreSet]);
|
|
191
|
-
const effectiveLintIgnore = mergedIgnoreSet.size > 0 ? [...mergedIgnoreSet] : undefined;
|
|
192
|
-
const ignoreChecks = effectiveLintIgnore ? new Set(effectiveLintIgnore) : undefined;
|
|
193
|
-
const effectiveTier = options.tier ?? patch.tier;
|
|
194
|
-
const updates = {
|
|
195
|
-
filesAffected: currentFilesAffected,
|
|
196
|
-
};
|
|
197
|
-
if (options.tier !== undefined) {
|
|
198
|
-
updates.tier = options.tier;
|
|
199
|
-
}
|
|
200
|
-
if (effectiveLintIgnore !== undefined && flagIgnoreSet.size > 0) {
|
|
201
|
-
updates.lintIgnore = effectiveLintIgnore;
|
|
202
|
-
}
|
|
203
|
-
enforcePatchPolicy({
|
|
204
|
-
config,
|
|
205
|
-
manifest: buildProjectedManifest(manifest, manifest.patches.map((entry) => entry.filename === patch.filename ? { ...entry, ...updates } : entry)),
|
|
206
|
-
command: 're-export',
|
|
207
|
-
forceUnsafe: options.forceUnsafe === true,
|
|
208
|
-
});
|
|
209
|
-
// Pass the whole-queue context so checkJs resolves cross-patch
|
|
210
|
-
// `resource:///` imports against the real owning sources (report scope
|
|
211
|
-
// stays this patch — see runPatchLint).
|
|
212
|
-
const patchQueueCtx = (await pathExists(paths.patches))
|
|
213
|
-
? await buildPatchQueueContext(paths.patches)
|
|
214
|
-
: undefined;
|
|
215
|
-
await runPatchLint(paths.engine, existingFiles, diffContent, config, options.skipLint, patchQueueCtx, ignoreChecks, effectiveTier);
|
|
216
|
-
if (isDryRun) {
|
|
217
|
-
info(`[dry-run] ${patch.filename}: ${existingFiles.length} file(s)`);
|
|
218
|
-
if (effectiveTier !== undefined && effectiveTier !== patch.tier) {
|
|
219
|
-
info(`[dry-run] ${patch.filename}: tier would become ${effectiveTier}`);
|
|
220
|
-
}
|
|
221
|
-
const addedIgnores = [...flagIgnoreSet].filter((id) => !existingIgnoreSet.has(id));
|
|
222
|
-
if (addedIgnores.length > 0) {
|
|
223
|
-
info(`[dry-run] ${patch.filename}: lintIgnore would gain ${addedIgnores.join(', ')}`);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
else {
|
|
227
|
-
// Atomic body + manifest update under a single patch-directory lock.
|
|
228
|
-
// A split `updatePatch` (lock-free) + `updatePatchMetadata` (lock-guarded)
|
|
229
|
-
// sequence allows a concurrent `resolve` / `rebase --continue` / `patch
|
|
230
|
-
// compact` / `patch reorder` to rewrite the manifest between the two
|
|
231
|
-
// writes and leave patch body and `filesAffected` disagreeing.
|
|
232
|
-
await updatePatchAndMetadata(paths.patches, patch.filename, diffContent, updates, undefined, {
|
|
233
|
-
config,
|
|
234
|
-
command: 're-export',
|
|
235
|
-
forceUnsafe: options.forceUnsafe === true,
|
|
236
|
-
});
|
|
237
|
-
// Keep the in-memory manifest in sync so subsequent iterations (notably
|
|
238
|
-
// `--all --scan`, where `getClaimedFiles` reads from this manifest) see
|
|
239
|
-
// the just-written `filesAffected`. The on-disk write above is the
|
|
240
|
-
// authority; this is a cache update.
|
|
241
|
-
const patchIndex = manifest.patches.findIndex((pm) => pm.filename === patch.filename);
|
|
242
|
-
if (patchIndex !== -1) {
|
|
243
|
-
const existingEntry = manifest.patches[patchIndex];
|
|
244
|
-
if (existingEntry) {
|
|
245
|
-
manifest.patches[patchIndex] = {
|
|
246
|
-
...existingEntry,
|
|
247
|
-
...updates,
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
success(`Re-exported ${patch.filename}`);
|
|
252
|
-
}
|
|
253
|
-
return true;
|
|
254
|
-
}
|
|
16
|
+
import { normalizeEngineRelativeInput, normalizeScanFiles } from './re-export-scan.js';
|
|
17
|
+
import { reExportSinglePatchWithIndexLockRetry } from './re-export-single.js';
|
|
255
18
|
async function resolveSelectedPatches(patches, options, manifest) {
|
|
256
19
|
if (options.all) {
|
|
257
20
|
return [...manifest.patches];
|
|
@@ -355,32 +118,49 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
355
118
|
// we write anything.
|
|
356
119
|
if (options.files !== undefined) {
|
|
357
120
|
const filesConfig = await loadConfig(projectRoot);
|
|
358
|
-
await withDryRunReExportLock(paths.fireforgeDir, isDryRun, () => reExportFilesInPlace(paths, selectedPatches, options, filesConfig));
|
|
121
|
+
await withDryRunReExportLock(paths.fireforgeDir, isDryRun, () => withDryRunPurityGuard(paths.engine, paths.patches, isDryRun, () => reExportFilesInPlace(paths, selectedPatches, options, filesConfig)));
|
|
359
122
|
return;
|
|
360
123
|
}
|
|
361
124
|
const config = await loadConfig(projectRoot);
|
|
125
|
+
// Classification inputs for the scan-less adjacency advisory, computed
|
|
126
|
+
// once for the whole run (FORGE G2).
|
|
127
|
+
const adjacentCtx = {
|
|
128
|
+
binaryName: config.binaryName,
|
|
129
|
+
furnacePrefixes: await collectFurnaceManagedPrefixes(paths.root),
|
|
130
|
+
refuseAdjacentUnmanaged: options.refuseAdjacentUnmanaged === true,
|
|
131
|
+
refusals: [],
|
|
132
|
+
};
|
|
133
|
+
const driftCtx = buildForeignDriftContext(options);
|
|
134
|
+
// Hoisted lint context, one per run (FORGE J1): queue context + checkJs
|
|
135
|
+
// program + per-patch result cache, shared across every loop iteration.
|
|
136
|
+
const lintCtx = await buildReExportLintContext(projectRoot, paths, config, options.noCache === true);
|
|
362
137
|
let reExported = 0;
|
|
363
138
|
const reExportedFilenames = [];
|
|
139
|
+
const failedFilenames = [];
|
|
364
140
|
const progress = spinner('Preparing re-export...');
|
|
365
141
|
const startedAt = Date.now();
|
|
366
|
-
await withDryRunReExportLock(paths.fireforgeDir, isDryRun, async () => {
|
|
142
|
+
await withDryRunReExportLock(paths.fireforgeDir, isDryRun, async () => withDryRunPurityGuard(paths.engine, paths.patches, isDryRun, async () => {
|
|
367
143
|
for (const [index, patch] of selectedPatches.entries()) {
|
|
368
144
|
const assignedScanFiles = scanFilesByPatch?.get(patch.filename);
|
|
369
145
|
const patchOptions = assignedScanFiles !== undefined ? { ...options, scanFiles: assignedScanFiles } : options;
|
|
370
146
|
progress.message(`Re-exporting ${index + 1}/${selectedPatches.length}: ${patch.filename} (${patch.filesAffected.length} file(s), ${elapsedSince(startedAt)} elapsed)...`);
|
|
371
147
|
try {
|
|
372
|
-
const exported = await
|
|
148
|
+
const exported = await reExportSinglePatchWithIndexLockRetry(patch, paths, manifest, patchOptions, isDryRun, config, adjacentCtx, driftCtx, lintCtx);
|
|
373
149
|
if (exported) {
|
|
374
150
|
reExported++;
|
|
375
151
|
reExportedFilenames.push(patch.filename);
|
|
376
152
|
}
|
|
377
153
|
}
|
|
378
154
|
catch (error) {
|
|
155
|
+
failedFilenames.push(patch.filename);
|
|
379
156
|
warn(`Failed to re-export ${patch.filename}`);
|
|
380
157
|
warn(toError(error).message);
|
|
381
158
|
}
|
|
382
159
|
}
|
|
383
|
-
});
|
|
160
|
+
}));
|
|
161
|
+
await finishReExportLint(lintCtx);
|
|
162
|
+
warnUnseenExpectedDrift(driftCtx);
|
|
163
|
+
throwRunLevelRefusals(adjacentCtx, driftCtx, progress);
|
|
384
164
|
if (reExported === 0 && selectedPatches.length > 0) {
|
|
385
165
|
progress.error('Re-export failed');
|
|
386
166
|
throw new GeneralError('All selected patches failed to re-export. Check the errors above.');
|
|
@@ -396,59 +176,113 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
396
176
|
if (shouldStamp) {
|
|
397
177
|
await stampPatchVersions(paths.patches, reExportedFilenames, config.firefox.version, config.firefox.product);
|
|
398
178
|
}
|
|
179
|
+
reportReExportOutcome({
|
|
180
|
+
isDryRun,
|
|
181
|
+
reExported,
|
|
182
|
+
selectedCount: selectedPatches.length,
|
|
183
|
+
reExportedFilenames,
|
|
184
|
+
failedFilenames,
|
|
185
|
+
shouldStamp,
|
|
186
|
+
stampRequested: options.stamp === true,
|
|
187
|
+
config,
|
|
188
|
+
progress,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Foreign-drift guard context, one per run (FORGE J2). `--expect` paths
|
|
193
|
+
* (FORGE L6) are normalized once here so the per-patch comparison matches
|
|
194
|
+
* them against engine-relative diff-section paths.
|
|
195
|
+
*/
|
|
196
|
+
function buildForeignDriftContext(options) {
|
|
197
|
+
return {
|
|
198
|
+
refuseForeignDrift: options.refuseForeignDrift === true,
|
|
199
|
+
expectedDriftFiles: new Set((options.expect ?? []).map((file) => normalizeEngineRelativeInput(file, '--expect'))),
|
|
200
|
+
expectedSeen: new Set(),
|
|
201
|
+
evaluationRuns: 0,
|
|
202
|
+
refusals: [],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Names `--expect` paths that never drifted this run (FORGE L6). A typo'd
|
|
207
|
+
* `--expect` path silently degrades the flag back to refusing the slice it
|
|
208
|
+
* was meant to admit, so surface the mismatch — but only as a warning: an
|
|
209
|
+
* expected file legitimately shows no drift when the slice was already
|
|
210
|
+
* captured by an earlier export.
|
|
211
|
+
*/
|
|
212
|
+
function warnUnseenExpectedDrift(driftCtx) {
|
|
213
|
+
const unseen = [...driftCtx.expectedDriftFiles].filter((file) => !driftCtx.expectedSeen.has(file));
|
|
214
|
+
if (unseen.length === 0)
|
|
215
|
+
return;
|
|
216
|
+
if (driftCtx.evaluationRuns === 0) {
|
|
217
|
+
warn(`--expect path(s) were not evaluated because no selected patch reached the drift check (for example, an earlier refusal): ${unseen.join(', ')}`);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
warn(`--expect path(s) showed no drift this run (typo, or already captured?): ${unseen.join(', ')}`);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Turns the run's collected `--refuse-adjacent-unmanaged` (FORGE G2) and
|
|
224
|
+
* `--refuse-foreign-drift` (FORGE J2) refusals into the non-zero exit,
|
|
225
|
+
* naming every refused patch and the remedy.
|
|
226
|
+
*/
|
|
227
|
+
function throwRunLevelRefusals(adjacentCtx, driftCtx, progress) {
|
|
228
|
+
if (adjacentCtx.refusals.length > 0) {
|
|
229
|
+
progress.error('Re-export refused');
|
|
230
|
+
const names = adjacentCtx.refusals.map((r) => r.patchFilename).join(', ');
|
|
231
|
+
throw new GeneralError(`Refused ${String(adjacentCtx.refusals.length)} patch(es) with adjacent unmanaged files ` +
|
|
232
|
+
`(--refuse-adjacent-unmanaged): ${names}. Adopt reviewed files with --scan --scan-file, ` +
|
|
233
|
+
`or export them as a new patch.`);
|
|
234
|
+
}
|
|
235
|
+
if (driftCtx.refusals.length > 0) {
|
|
236
|
+
progress.error('Re-export refused');
|
|
237
|
+
const names = driftCtx.refusals.map((r) => r.patchFilename).join(', ');
|
|
238
|
+
const unreadable = driftCtx.refusals.filter((r) => r.reason === 'baseline-unreadable');
|
|
239
|
+
const unreadablePart = unreadable.length > 0
|
|
240
|
+
? ` ${String(unreadable.length)} of these refused because the old patch body was missing or unreadable, so the drift comparison could not run (fail-closed).`
|
|
241
|
+
: '';
|
|
242
|
+
throw new GeneralError(`Refused ${String(driftCtx.refusals.length)} patch(es) whose refreshed body would absorb ` +
|
|
243
|
+
`engine edits not present in the old patch body (--refuse-foreign-drift): ${names}.${unreadablePart} ` +
|
|
244
|
+
`Commit or stash the foreign edits (another session may own them), name the slice's ` +
|
|
245
|
+
`intended files with --expect <path>, or re-run without the flag to capture them intentionally.`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Prints the end-of-run summary and enforces the FORGE H8 exit contract:
|
|
250
|
+
* a partial run exits non-zero BY DEFAULT (adjudicated for 0.41.0, per the
|
|
251
|
+
* follow-up 0.40.0's G7 recorded). "Re-exported 2 of 3" used to print and
|
|
252
|
+
* exit 0, letting a partial refresh ride an `&&` chain as success. The
|
|
253
|
+
* summary still prints first so the honest "N of M" accounting stays
|
|
254
|
+
* visible; the throw then carries the failed/skipped breakdown. Dry-run
|
|
255
|
+
* included: a preview reporting that the real run would partially fail
|
|
256
|
+
* must not read as a passing preview in scripts.
|
|
257
|
+
*/
|
|
258
|
+
function reportReExportOutcome(args) {
|
|
259
|
+
const { isDryRun, reExported, selectedCount, config, progress } = args;
|
|
399
260
|
if (isDryRun) {
|
|
400
261
|
progress.stop('Dry run complete');
|
|
401
|
-
success(`[dry-run] Would re-export ${reExported} of ${
|
|
402
|
-
if (
|
|
262
|
+
success(`[dry-run] Would re-export ${reExported} of ${selectedCount} patch(es)`);
|
|
263
|
+
if (args.stampRequested) {
|
|
403
264
|
info(`[dry-run] Would stamp sourceVersion=${config.firefox.version} (${config.firefox.product}) on ${reExported} patch(es)`);
|
|
404
265
|
}
|
|
405
266
|
outro('Dry run complete');
|
|
406
267
|
}
|
|
407
268
|
else {
|
|
408
269
|
progress.stop('Re-export complete');
|
|
409
|
-
success(`Re-exported ${reExported} of ${
|
|
410
|
-
if (shouldStamp) {
|
|
411
|
-
success(`Stamped sourceVersion=${config.firefox.version} (${config.firefox.product}) on ${reExportedFilenames.length} patch(es)`);
|
|
270
|
+
success(`Re-exported ${reExported} of ${selectedCount} patch(es)`);
|
|
271
|
+
if (args.shouldStamp) {
|
|
272
|
+
success(`Stamped sourceVersion=${config.firefox.version} (${config.firefox.product}) on ${args.reExportedFilenames.length} patch(es)`);
|
|
412
273
|
}
|
|
413
|
-
else if (
|
|
274
|
+
else if (args.stampRequested && reExported !== selectedCount) {
|
|
414
275
|
warn('--stamp was requested but some patches failed or were skipped; refusing to stamp a partial set.');
|
|
415
276
|
}
|
|
416
277
|
outro('Re-export complete');
|
|
417
278
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
.option('-s, --scan', 'Scan directories for new/removed files and update filesAffected')
|
|
428
|
-
.option('--scan-file <path>', 'With --scan, add this explicit engine-relative file to one target patch without collecting adjacent files. Repeatable.', (value, prev) => [...prev, value], [])
|
|
429
|
-
.option('--scan-files <manifest>', 'With --scan, bulk-assign generated files from a JSON manifest: {"assignments":[{"patch":"002-name.patch","files":["path"]}]}. Selects patches from the manifest.')
|
|
430
|
-
.option('--files <paths>', 'Restrict the re-exported filesAffected to this comma-separated list (single target patch only)', (value) => value
|
|
431
|
-
.split(',')
|
|
432
|
-
.map((v) => v.trim())
|
|
433
|
-
.filter((v) => v.length > 0))
|
|
434
|
-
.option('--dry-run', 'Show what would change without writing')
|
|
435
|
-
.option('--skip-lint', 'Skip patch lint checks (downgrade errors to warnings)')
|
|
436
|
-
.option('--allow-stale-furnace', 'Export the deployed engine copy even when the components/ source changed since the last furnace apply')
|
|
437
|
-
.option('--allow-shrink', 'Allow --files to remove paths currently owned by the patch. Required before --yes can bypass the shrink confirmation.')
|
|
438
|
-
.option('-y, --yes', 'Skip confirmation prompts (required for non-TTY destructive writes)')
|
|
439
|
-
.option('--force-unsafe', 'Bypass cross-patch lint refusal when --files shrinks a patch')
|
|
440
|
-
.option('--stamp', "After every selected patch refreshes cleanly, stamp each re-exported patch's sourceVersion/sourceProduct in patches.json to firefox.version/firefox.product from fireforge.json. No effect on a partial run.")
|
|
441
|
-
.addOption(new Option('--tier <tier>', 'Force a tier override on the selected patch (only "branding" recognised). Mutually exclusive with --all.').choices(['branding']))
|
|
442
|
-
.option('--lint-ignore <check-id>', 'Append a lint check ID to the patch\'s PatchMetadata.lintIgnore (union, de-duped, repeatable). Mutually exclusive with --all. Use "fireforge patch lint-ignore" for --remove / --clear.', (value, prev) => [...prev, value], []);
|
|
443
|
-
addWaitLockOption(reExport).action(withErrorHandling(async (patches, options) => {
|
|
444
|
-
const { tier, lintIgnore, scanFile, scanFiles, ...rest } = options;
|
|
445
|
-
const projectRoot = getProjectRoot();
|
|
446
|
-
await withEngineSessionLock(projectRoot, 're-export', () => reExportCommand(projectRoot, patches, {
|
|
447
|
-
...pickDefined(rest),
|
|
448
|
-
...(scanFile !== undefined && scanFile.length > 0 ? { scanFiles: scanFile } : {}),
|
|
449
|
-
...(scanFiles !== undefined ? { scanFilesManifest: scanFiles } : {}),
|
|
450
|
-
...(tier !== undefined ? { tier: tier } : {}),
|
|
451
|
-
...(lintIgnore !== undefined && lintIgnore.length > 0 ? { lintIgnore } : {}),
|
|
452
|
-
}), { waitLockSeconds: resolveWaitLockSeconds(options.waitLock) });
|
|
453
|
-
}));
|
|
279
|
+
if (reExported < selectedCount) {
|
|
280
|
+
const skippedCount = selectedCount - reExported - args.failedFilenames.length;
|
|
281
|
+
const failedPart = args.failedFilenames.length > 0 ? ` Failed: ${args.failedFilenames.join(', ')}.` : '';
|
|
282
|
+
const skippedPart = skippedCount > 0 ? ` Skipped: ${String(skippedCount)} patch(es) (see warnings above).` : '';
|
|
283
|
+
throw new GeneralError(`${isDryRun ? '[dry-run] Would re-export' : 'Re-exported'} only ${String(reExported)} of ` +
|
|
284
|
+
`${String(selectedCount)} selected patch(es).${failedPart}${skippedPart} ` +
|
|
285
|
+
'Partial re-export exits non-zero so it cannot pass as success in scripts; ' +
|
|
286
|
+
'the per-patch errors are listed above.');
|
|
287
|
+
}
|
|
454
288
|
}
|
|
@@ -5,18 +5,27 @@
|
|
|
5
5
|
import { getProjectPaths, updateState } from '../../core/config.js';
|
|
6
6
|
import { clearAppliedFurnaceState } from '../../core/furnace-config.js';
|
|
7
7
|
import { resetChanges } from '../../core/git.js';
|
|
8
|
-
import { clearRebaseSession,
|
|
8
|
+
import { clearRebaseSession, getRebaseSessionPath, readRebaseSession, } from '../../core/rebase-session.js';
|
|
9
9
|
import { NoRebaseSessionError } from '../../errors/rebase.js';
|
|
10
|
-
import { intro, outro, spinner, success } from '../../utils/logger.js';
|
|
10
|
+
import { intro, outro, spinner, success, warn } from '../../utils/logger.js';
|
|
11
11
|
import { confirmDirtyEngineReset } from './confirm.js';
|
|
12
12
|
/**
|
|
13
13
|
* Handles `fireforge rebase --abort`.
|
|
14
14
|
*/
|
|
15
15
|
export async function handleAbort(projectRoot, yes) {
|
|
16
16
|
intro('FireForge Rebase — Abort');
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// Abort is the escape hatch, so it deliberately does NOT require a *valid*
|
|
18
|
+
// session — only that one is present. Nothing below reads the session
|
|
19
|
+
// object (the restore works off `paths.engine` and `resetChanges`), and the
|
|
20
|
+
// pre-0.41.0 `if (!session) throw` refused to run against a corrupt file,
|
|
21
|
+
// which was the only command that could have cleared it.
|
|
22
|
+
const read = await readRebaseSession(projectRoot);
|
|
23
|
+
if (!read.present)
|
|
19
24
|
throw new NoRebaseSessionError();
|
|
25
|
+
if (!read.valid) {
|
|
26
|
+
warn(`The rebase session at ${getRebaseSessionPath(projectRoot)} is unreadable ` +
|
|
27
|
+
`(${read.reason}). Aborting anyway: the engine will be restored and the session cleared.`);
|
|
28
|
+
}
|
|
20
29
|
const paths = getProjectPaths(projectRoot);
|
|
21
30
|
if (!(await confirmDirtyEngineReset({
|
|
22
31
|
engineDir: paths.engine,
|
|
@@ -8,10 +8,10 @@ import { getStagedDiffForFiles } from '../../core/git-diff.js';
|
|
|
8
8
|
import { stageFiles, unstageFiles } from '../../core/git-file-ops.js';
|
|
9
9
|
import { updatePatchAndMetadata } from '../../core/patch-export.js';
|
|
10
10
|
import { loadPatchesManifest } from '../../core/patch-manifest.js';
|
|
11
|
-
import {
|
|
11
|
+
import { getRebaseSessionPath, readRebaseSession, saveRebaseSession, } from '../../core/rebase-session.js';
|
|
12
12
|
import { runInSignalCriticalSection } from '../../core/signal-critical.js';
|
|
13
13
|
import { GeneralError } from '../../errors/base.js';
|
|
14
|
-
import { NoRebaseSessionError, RebaseError } from '../../errors/rebase.js';
|
|
14
|
+
import { CorruptRebaseSessionError, NoRebaseSessionError, RebaseError, } from '../../errors/rebase.js';
|
|
15
15
|
import { pathExists } from '../../utils/fs.js';
|
|
16
16
|
import { info, intro, success, warn } from '../../utils/logger.js';
|
|
17
17
|
import { runPatchLoop } from './patch-loop.js';
|
|
@@ -20,9 +20,16 @@ import { runPatchLoop } from './patch-loop.js';
|
|
|
20
20
|
*/
|
|
21
21
|
export async function handleContinue(projectRoot, maxFuzz) {
|
|
22
22
|
intro('FireForge Rebase — Continue');
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// A present-but-unreadable session is NOT "no session in progress" — that
|
|
24
|
+
// conflation is what made a corrupt file unrecoverable. Name the file and
|
|
25
|
+
// point at --abort, which can now clear it.
|
|
26
|
+
const read = await readRebaseSession(projectRoot);
|
|
27
|
+
if (!read.present)
|
|
25
28
|
throw new NoRebaseSessionError();
|
|
29
|
+
if (!read.valid) {
|
|
30
|
+
throw new CorruptRebaseSessionError(getRebaseSessionPath(projectRoot), read.reason);
|
|
31
|
+
}
|
|
32
|
+
const session = read.session;
|
|
26
33
|
const paths = getProjectPaths(projectRoot);
|
|
27
34
|
// Special case: every patch has already applied but a previous run failed
|
|
28
35
|
// somewhere in the post-apply work (re-export, version stamping). In that
|
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { Command } from 'commander';
|
|
14
14
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
15
|
+
import { assertEngineGitReady } from '../../core/engine-precondition.js';
|
|
15
16
|
import { clearAppliedFurnaceState } from '../../core/furnace-config.js';
|
|
16
|
-
import { getHead,
|
|
17
|
+
import { getHead, resetChanges } from '../../core/git.js';
|
|
17
18
|
import { discoverPatches } from '../../core/patch-files.js';
|
|
18
19
|
import { loadPatchesManifest } from '../../core/patch-manifest.js';
|
|
19
20
|
import { getPatchSourceProduct, getPatchSourceVersion } from '../../core/patch-source-metadata.js';
|
|
20
|
-
import {
|
|
21
|
+
import { getRebaseSessionPath, readRebaseSession, saveRebaseSession, } from '../../core/rebase-session.js';
|
|
21
22
|
import { GeneralError } from '../../errors/base.js';
|
|
22
|
-
import { RebaseSessionExistsError } from '../../errors/rebase.js';
|
|
23
|
-
import { pathExists } from '../../utils/fs.js';
|
|
23
|
+
import { CorruptRebaseSessionError, RebaseSessionExistsError } from '../../errors/rebase.js';
|
|
24
24
|
import { info, intro, outro, spinner } from '../../utils/logger.js';
|
|
25
25
|
import { commanderArgParser, pickDefined } from '../../utils/options.js';
|
|
26
26
|
import { parsePositiveIntegerFlag } from '../../utils/validation.js';
|
|
@@ -33,34 +33,24 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
33
33
|
const isDryRun = options.dryRun === true;
|
|
34
34
|
const maxFuzz = options.maxFuzz ?? 3;
|
|
35
35
|
intro(isDryRun ? 'FireForge Rebase (dry run)' : 'FireForge Rebase');
|
|
36
|
-
|
|
36
|
+
// One read decides liveness AND validity: a pathExists pre-probe here left
|
|
37
|
+
// a window where the file vanished between probe and read, reporting
|
|
38
|
+
// "already in progress" with no session on disk.
|
|
39
|
+
const existing = await readRebaseSession(projectRoot);
|
|
40
|
+
if (existing.present) {
|
|
41
|
+
// A corrupt session must not be reported as "already in progress": that
|
|
42
|
+
// message tells the operator to run --continue or --abort, and before
|
|
43
|
+
// 0.41.0 both of those then reported "no rebase session in progress".
|
|
44
|
+
if (!existing.valid) {
|
|
45
|
+
throw new CorruptRebaseSessionError(getRebaseSessionPath(projectRoot), existing.reason);
|
|
46
|
+
}
|
|
37
47
|
throw new RebaseSessionExistsError();
|
|
38
48
|
}
|
|
39
49
|
const paths = getProjectPaths(projectRoot);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
|
|
45
|
-
}
|
|
46
|
-
// 2026-04-24 eval Finding 11: `rebase --dry-run` used to print
|
|
47
|
-
// "Dry run complete" without validating that the engine had a valid
|
|
48
|
-
// HEAD. A previous `download --force` abort could leave `.git/`
|
|
49
|
-
// initialized but unborn (no baseline commit); the real rebase then
|
|
50
|
-
// failed immediately with `fatal: ambiguous argument 'HEAD'` on the
|
|
51
|
-
// first `git rev-parse HEAD` call. Replicate the same baseline check
|
|
52
|
-
// here so dry-run mirrors the real-run preconditions and operators
|
|
53
|
-
// cannot mistake a broken baseline for a ready-to-rebase tree.
|
|
54
|
-
try {
|
|
55
|
-
await getHead(paths.engine);
|
|
56
|
-
}
|
|
57
|
-
catch (err) {
|
|
58
|
-
if (isMissingHeadError(err)) {
|
|
59
|
-
throw new GeneralError('Engine repository has no baseline commit yet — a previous "fireforge download" was interrupted before git created the initial Firefox source commit. ' +
|
|
60
|
-
'Re-run "fireforge download --force" to recreate the baseline repository cleanly, then retry the rebase.');
|
|
61
|
-
}
|
|
62
|
-
throw err;
|
|
63
|
-
}
|
|
50
|
+
// Keeps the rebase-specific tail on the unborn-HEAD remediation (2026-04-24
|
|
51
|
+
// eval Finding 11: --dry-run used to skip this check entirely, so the real
|
|
52
|
+
// run then failed on `git rev-parse HEAD`).
|
|
53
|
+
await assertEngineGitReady(paths.engine, { unbornHeadSuffix: ', then retry the rebase.' });
|
|
64
54
|
const config = await loadConfig(projectRoot);
|
|
65
55
|
const currentVersion = config.firefox.version;
|
|
66
56
|
const manifest = await loadPatchesManifest(paths.patches);
|
|
@@ -19,6 +19,7 @@ import { elapsedSince } from '../../utils/elapsed.js';
|
|
|
19
19
|
import { toError } from '../../utils/errors.js';
|
|
20
20
|
import { pathExists } from '../../utils/fs.js';
|
|
21
21
|
import { error, info, outro, spinner, success, warn } from '../../utils/logger.js';
|
|
22
|
+
import { isValidFirefoxVersion } from '../../utils/validation.js';
|
|
22
23
|
import { buildRebaseConflictSummary } from './conflict-summary.js';
|
|
23
24
|
import { printSummary } from './summary.js';
|
|
24
25
|
/**
|
|
@@ -164,6 +165,17 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
|
|
|
164
165
|
// succeeded on the patch side, so the operator is committing to the
|
|
165
166
|
// new source baseline; per-component health checking stays with
|
|
166
167
|
// `fireforge furnace validate` / `doctor --repair-furnace`.
|
|
168
|
+
//
|
|
169
|
+
// "Unconditional" means unconditional on component health, not on the value:
|
|
170
|
+
// `stampFurnaceOverrideBaseVersions` assigns and persists whatever string it
|
|
171
|
+
// is handed, so a session whose `toVersion` is not a real Firefox version
|
|
172
|
+
// would rewrite every override's baseline to that value. `isValidSession`
|
|
173
|
+
// now rejects such a session at read time; this refuses to persist it even
|
|
174
|
+
// if a future caller reaches here another way.
|
|
175
|
+
if (!isValidFirefoxVersion(session.toVersion)) {
|
|
176
|
+
throw new RebaseError(`Refusing to stamp Furnace override baseVersion(s): the rebase session's target version ` +
|
|
177
|
+
`("${session.toVersion}") is not a valid Firefox version.`);
|
|
178
|
+
}
|
|
167
179
|
try {
|
|
168
180
|
const overridesStamped = await stampFurnaceOverrideBaseVersions(projectRoot, session.toVersion);
|
|
169
181
|
if (overridesStamped > 0) {
|