@hominis/fireforge 0.27.0 → 0.27.2
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 +11 -1
- package/README.md +6 -6
- package/dist/src/cli.js +5 -1
- package/dist/src/commands/build.js +61 -1
- package/dist/src/commands/doctor/post-rebase-audit.d.ts +2 -0
- package/dist/src/commands/doctor/post-rebase-audit.js +86 -0
- package/dist/src/commands/doctor-working-tree.js +5 -1
- package/dist/src/commands/doctor.js +3 -0
- package/dist/src/commands/download.js +41 -42
- package/dist/src/commands/export-all.js +3 -2
- package/dist/src/commands/export-flow.d.ts +2 -0
- package/dist/src/commands/export-flow.js +2 -0
- package/dist/src/commands/export.js +5 -4
- package/dist/src/commands/import.js +2 -1
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/re-export.js +10 -8
- package/dist/src/commands/rebase/conflict-summary.d.ts +12 -0
- package/dist/src/commands/rebase/conflict-summary.js +38 -0
- package/dist/src/commands/rebase/continue.js +2 -0
- package/dist/src/commands/rebase/index.d.ts +2 -2
- package/dist/src/commands/rebase/index.js +9 -4
- package/dist/src/commands/rebase/patch-loop.js +29 -11
- package/dist/src/commands/rebase/summary.js +7 -2
- package/dist/src/commands/resolve.js +2 -1
- package/dist/src/commands/setup-support.js +6 -2
- package/dist/src/commands/setup.js +1 -0
- package/dist/src/commands/source.d.ts +9 -0
- package/dist/src/commands/source.js +92 -0
- package/dist/src/commands/status-output.d.ts +13 -0
- package/dist/src/commands/status-output.js +186 -0
- package/dist/src/commands/status.js +4 -247
- package/dist/src/commands/verify.js +32 -16
- package/dist/src/core/build-prepare.js +12 -4
- package/dist/src/core/config-validate.js +1 -1
- package/dist/src/core/firefox-cache.d.ts +1 -1
- package/dist/src/core/firefox-cache.js +10 -3
- package/dist/src/core/firefox-extract.d.ts +1 -1
- package/dist/src/core/firefox-extract.js +13 -1
- package/dist/src/core/firefox.d.ts +2 -1
- package/dist/src/core/firefox.js +3 -3
- package/dist/src/core/furnace-registration-validate.d.ts +7 -0
- package/dist/src/core/furnace-registration-validate.js +29 -12
- package/dist/src/core/furnace-validate-registration.js +5 -37
- package/dist/src/core/git.js +25 -5
- package/dist/src/core/ownership-table.d.ts +3 -1
- package/dist/src/core/ownership-table.js +31 -7
- package/dist/src/core/patch-artifact-normalize.d.ts +9 -0
- package/dist/src/core/patch-artifact-normalize.js +13 -0
- package/dist/src/core/patch-export-update.js +2 -1
- package/dist/src/core/patch-export.d.ts +4 -0
- package/dist/src/core/patch-export.js +7 -2
- package/dist/src/core/patch-manifest-consistency.d.ts +1 -1
- package/dist/src/core/patch-manifest-consistency.js +4 -2
- package/dist/src/core/patch-manifest-query.d.ts +4 -3
- package/dist/src/core/patch-manifest-query.js +12 -4
- package/dist/src/core/patch-manifest-validate.js +22 -4
- package/dist/src/core/patch-source-metadata.d.ts +8 -0
- package/dist/src/core/patch-source-metadata.js +17 -0
- package/dist/src/core/rebase-session.d.ts +8 -3
- package/dist/src/core/rebase-session.js +1 -1
- package/dist/src/core/status-classify.d.ts +4 -1
- package/dist/src/core/status-classify.js +4 -5
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +16 -1
- package/dist/src/types/commands/patches.d.ts +9 -1
- package/dist/src/types/config.d.ts +1 -1
- package/dist/src/utils/elapsed.d.ts +4 -0
- package/dist/src/utils/elapsed.js +15 -0
- package/dist/src/utils/validation.d.ts +2 -2
- package/dist/src/utils/validation.js +5 -5
- package/package.json +1 -1
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { extractConflictingFiles } from '../../core/patch-parse.js';
|
|
3
|
+
function normalizeRejectFile(file) {
|
|
4
|
+
return file.replace(/\.rej$/, '');
|
|
5
|
+
}
|
|
6
|
+
function classifyConflict(files) {
|
|
7
|
+
if (files.some((file) => file.endsWith('toolkit/content/customElements.js'))) {
|
|
8
|
+
return 'registration context drift';
|
|
9
|
+
}
|
|
10
|
+
if (files.some((file) => file.endsWith('jar.mn') ||
|
|
11
|
+
file.endsWith('moz.build') ||
|
|
12
|
+
file.endsWith('browser.toml') ||
|
|
13
|
+
file.endsWith('browser/moz.configure'))) {
|
|
14
|
+
return 'manifest context drift';
|
|
15
|
+
}
|
|
16
|
+
return 'patch context drift';
|
|
17
|
+
}
|
|
18
|
+
/** Builds a concise operator-facing summary for a failed rebase patch. */
|
|
19
|
+
export function buildRebaseConflictSummary(args) {
|
|
20
|
+
const failedFiles = [
|
|
21
|
+
...new Set([
|
|
22
|
+
...extractConflictingFiles(args.error),
|
|
23
|
+
...(args.rejectFiles ?? []).map(normalizeRejectFile),
|
|
24
|
+
]),
|
|
25
|
+
].sort();
|
|
26
|
+
return {
|
|
27
|
+
patchFilename: args.patchFilename,
|
|
28
|
+
failedFiles,
|
|
29
|
+
category: classifyConflict(failedFiles),
|
|
30
|
+
nextCommands: [
|
|
31
|
+
"find engine -name '*.rej'",
|
|
32
|
+
'edit the affected engine/ files',
|
|
33
|
+
'fireforge rebase --continue',
|
|
34
|
+
'fireforge rebase --abort',
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=conflict-summary.js.map
|
|
@@ -73,6 +73,8 @@ export async function handleContinue(projectRoot, maxFuzz) {
|
|
|
73
73
|
// v0.14.0 resolve.ts fix.
|
|
74
74
|
await updatePatchAndMetadata(paths.patches, currentPatch.filename, diffContent, {
|
|
75
75
|
sourceEsrVersion: session.toVersion,
|
|
76
|
+
sourceVersion: session.toVersion,
|
|
77
|
+
...(session.toProduct !== undefined ? { sourceProduct: session.toProduct } : {}),
|
|
76
78
|
});
|
|
77
79
|
}
|
|
78
80
|
finally {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `fireforge rebase` — semi-automated
|
|
2
|
+
* `fireforge rebase` — semi-automated Firefox source version upgrade.
|
|
3
3
|
*
|
|
4
4
|
* Orchestrates the full patch-rebase workflow:
|
|
5
5
|
* 1. Reset engine to baseline
|
|
@@ -13,7 +13,7 @@ import { Command } from 'commander';
|
|
|
13
13
|
import type { CommandContext } from '../../types/cli.js';
|
|
14
14
|
import type { RebaseOptions } from '../../types/commands/index.js';
|
|
15
15
|
/**
|
|
16
|
-
* Runs the rebase command to orchestrate
|
|
16
|
+
* Runs the rebase command to orchestrate a Firefox source version upgrade.
|
|
17
17
|
* @param projectRoot - Root directory of the project
|
|
18
18
|
* @param options - Rebase options
|
|
19
19
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
* `fireforge rebase` — semi-automated
|
|
3
|
+
* `fireforge rebase` — semi-automated Firefox source version upgrade.
|
|
4
4
|
*
|
|
5
5
|
* Orchestrates the full patch-rebase workflow:
|
|
6
6
|
* 1. Reset engine to baseline
|
|
@@ -15,6 +15,7 @@ import { getFurnacePaths, updateFurnaceState } from '../../core/furnace-config.j
|
|
|
15
15
|
import { getHead, isGitRepository, isMissingHeadError, resetChanges } from '../../core/git.js';
|
|
16
16
|
import { discoverPatches } from '../../core/patch-files.js';
|
|
17
17
|
import { loadPatchesManifest } from '../../core/patch-manifest.js';
|
|
18
|
+
import { getPatchSourceProduct, getPatchSourceVersion } from '../../core/patch-source-metadata.js';
|
|
18
19
|
import { hasActiveRebaseSession, saveRebaseSession } from '../../core/rebase-session.js';
|
|
19
20
|
import { GeneralError } from '../../errors/base.js';
|
|
20
21
|
import { RebaseSessionExistsError } from '../../errors/rebase.js';
|
|
@@ -65,9 +66,11 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
65
66
|
throw new GeneralError('No patches found in manifest. Nothing to rebase.');
|
|
66
67
|
}
|
|
67
68
|
// Determine the "from" version from the patches
|
|
68
|
-
const patchVersions = new Set(manifest.patches.map((p) => p
|
|
69
|
+
const patchVersions = new Set(manifest.patches.map((p) => getPatchSourceVersion(p)));
|
|
70
|
+
const patchProducts = new Set(manifest.patches.map((p) => getPatchSourceProduct(p)).filter(Boolean));
|
|
69
71
|
const sortedVersions = [...patchVersions].sort();
|
|
70
72
|
const fromVersion = sortedVersions[0] ?? currentVersion;
|
|
73
|
+
const fromProduct = [...patchProducts].sort()[0] ?? config.firefox.product;
|
|
71
74
|
if (patchVersions.size === 1 && fromVersion === currentVersion) {
|
|
72
75
|
info('All patches already match the current Firefox version. Nothing to rebase.');
|
|
73
76
|
outro('Rebase not needed');
|
|
@@ -110,6 +113,8 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
110
113
|
const allPatches = await discoverPatches(paths.patches);
|
|
111
114
|
const session = {
|
|
112
115
|
startedAt: new Date().toISOString(),
|
|
116
|
+
fromProduct,
|
|
117
|
+
toProduct: config.firefox.product,
|
|
113
118
|
fromVersion,
|
|
114
119
|
toVersion: currentVersion,
|
|
115
120
|
preRebaseCommit,
|
|
@@ -125,7 +130,7 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
125
130
|
}
|
|
126
131
|
// ── Public API ──
|
|
127
132
|
/**
|
|
128
|
-
* Runs the rebase command to orchestrate
|
|
133
|
+
* Runs the rebase command to orchestrate a Firefox source version upgrade.
|
|
129
134
|
* @param projectRoot - Root directory of the project
|
|
130
135
|
* @param options - Rebase options
|
|
131
136
|
*/
|
|
@@ -142,7 +147,7 @@ export async function rebaseCommand(projectRoot, options = {}) {
|
|
|
142
147
|
export function registerRebase(program, { getProjectRoot, withErrorHandling }) {
|
|
143
148
|
program
|
|
144
149
|
.command('rebase')
|
|
145
|
-
.description('Semi-automated
|
|
150
|
+
.description('Semi-automated Firefox source version upgrade — apply patches with fuzz and re-export')
|
|
146
151
|
.option('--continue', 'Resume after manually resolving a failed patch')
|
|
147
152
|
.option('--abort', 'Cancel the rebase and restore engine to pre-rebase state')
|
|
148
153
|
.option('--dry-run', 'Show what would happen without modifying anything')
|
|
@@ -15,9 +15,11 @@ import { extractConflictingFiles } from '../../core/patch-parse.js';
|
|
|
15
15
|
import { clearRebaseSession, saveRebaseSession } from '../../core/rebase-session.js';
|
|
16
16
|
import { runInSignalCriticalSection } from '../../core/signal-critical.js';
|
|
17
17
|
import { RebaseError } from '../../errors/rebase.js';
|
|
18
|
+
import { elapsedSince } from '../../utils/elapsed.js';
|
|
18
19
|
import { toError } from '../../utils/errors.js';
|
|
19
20
|
import { pathExists } from '../../utils/fs.js';
|
|
20
21
|
import { error, info, outro, spinner, success, warn } from '../../utils/logger.js';
|
|
22
|
+
import { buildRebaseConflictSummary } from './conflict-summary.js';
|
|
21
23
|
import { printSummary } from './summary.js';
|
|
22
24
|
/**
|
|
23
25
|
* Runs the patch application loop, re-exports applied patches, and stamps versions.
|
|
@@ -37,7 +39,7 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
|
|
|
37
39
|
await saveRebaseSession(projectRoot, session);
|
|
38
40
|
continue;
|
|
39
41
|
}
|
|
40
|
-
s.message(`Applying ${entry.filename}...`);
|
|
42
|
+
s.message(`Applying ${i + 1}/${session.patches.length}: ${entry.filename}...`);
|
|
41
43
|
// Apply + session persist is wrapped in a signal-deferred critical
|
|
42
44
|
// section so a SIGINT / SIGTERM between the filesystem mutation and
|
|
43
45
|
// the session-file update is held until the bookkeeping write lands.
|
|
@@ -90,8 +92,24 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
|
|
|
90
92
|
},
|
|
91
93
|
}));
|
|
92
94
|
s.error(`${entry.filename} failed to apply`);
|
|
95
|
+
const summary = buildRebaseConflictSummary({
|
|
96
|
+
patchFilename: entry.filename,
|
|
97
|
+
...(result.error !== undefined ? { error: result.error } : {}),
|
|
98
|
+
...(result.rejectFiles !== undefined ? { rejectFiles: result.rejectFiles } : {}),
|
|
99
|
+
});
|
|
100
|
+
warn(`Conflict summary for ${summary.patchFilename}: ${summary.category}`);
|
|
101
|
+
if (summary.failedFiles.length > 0) {
|
|
102
|
+
warn(` Failed files: ${summary.failedFiles.join(', ')}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
warn(' Failed files: not detected from git output');
|
|
106
|
+
}
|
|
107
|
+
info(' Suggested next commands:');
|
|
108
|
+
for (const command of summary.nextCommands) {
|
|
109
|
+
info(` ${command}`);
|
|
110
|
+
}
|
|
93
111
|
if (result.error) {
|
|
94
|
-
error(`
|
|
112
|
+
error(` Raw apply detail: ${result.error}`);
|
|
95
113
|
}
|
|
96
114
|
if (result.rejectFiles && result.rejectFiles.length > 0) {
|
|
97
115
|
info(` .rej files created for manual resolution`);
|
|
@@ -125,16 +143,16 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
|
|
|
125
143
|
.filter((p) => p.status === 'applied-clean' || p.status === 'applied-fuzz' || p.status === 'resolved')
|
|
126
144
|
.map((p) => p.filename);
|
|
127
145
|
if (appliedFilenames.length > 0) {
|
|
128
|
-
await stampPatchVersions(paths.patches, appliedFilenames, session.toVersion);
|
|
146
|
+
await stampPatchVersions(paths.patches, appliedFilenames, session.toVersion, session.toProduct);
|
|
129
147
|
}
|
|
130
148
|
// Stamp every Furnace override's `baseVersion` to match the rebased
|
|
131
|
-
// Firefox version. Before this stamp, a successful
|
|
149
|
+
// Firefox source version. Before this stamp, a successful source bump left
|
|
132
150
|
// overrides in a doctor-failing drift state (each override still
|
|
133
|
-
// claimed the pre-rebase
|
|
151
|
+
// claimed the pre-rebase source as its baseline) and every subsequent
|
|
134
152
|
// `fireforge doctor` failed `Furnace component validation`. The
|
|
135
153
|
// stamp is unconditional per the helper's contract: rebase already
|
|
136
154
|
// succeeded on the patch side, so the operator is committing to the
|
|
137
|
-
// new
|
|
155
|
+
// new source baseline; per-component health checking stays with
|
|
138
156
|
// `fireforge furnace validate` / `doctor --repair-furnace`.
|
|
139
157
|
try {
|
|
140
158
|
const overridesStamped = await stampFurnaceOverrideBaseVersions(projectRoot, session.toVersion);
|
|
@@ -158,7 +176,7 @@ export async function runPatchLoop(projectRoot, session, paths, maxFuzz) {
|
|
|
158
176
|
return next;
|
|
159
177
|
});
|
|
160
178
|
info('');
|
|
161
|
-
success(`All patches re-exported with
|
|
179
|
+
success(`All patches re-exported with sourceVersion=${session.toVersion}`);
|
|
162
180
|
outro('Rebase complete!');
|
|
163
181
|
}
|
|
164
182
|
async function reExportAppliedPatches(session, paths) {
|
|
@@ -167,19 +185,19 @@ async function reExportAppliedPatches(session, paths) {
|
|
|
167
185
|
if (!manifest)
|
|
168
186
|
return failures;
|
|
169
187
|
const s = spinner('Re-exporting patches...');
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
188
|
+
const reExportable = session.patches.filter((entry) => entry.status === 'applied-clean' || entry.status === 'applied-fuzz');
|
|
189
|
+
const startedAt = Date.now();
|
|
190
|
+
for (const [index, entry] of reExportable.entries()) {
|
|
173
191
|
const meta = manifest.patches.find((p) => p.filename === entry.filename);
|
|
174
192
|
if (!meta)
|
|
175
193
|
continue;
|
|
176
|
-
s.message(`Re-exporting ${entry.filename}...`);
|
|
177
194
|
const existingFiles = [];
|
|
178
195
|
for (const f of meta.filesAffected) {
|
|
179
196
|
if (await pathExists(join(paths.engine, f))) {
|
|
180
197
|
existingFiles.push(f);
|
|
181
198
|
}
|
|
182
199
|
}
|
|
200
|
+
s.message(`Re-exporting ${index + 1}/${reExportable.length}: ${entry.filename} (${existingFiles.length}/${meta.filesAffected.length} file(s), ${elapsedSince(startedAt)} elapsed)...`);
|
|
183
201
|
try {
|
|
184
202
|
const diffContent = await getDiffForFilesAgainstHead(paths.engine, existingFiles);
|
|
185
203
|
if (diffContent.trim()) {
|
|
@@ -27,7 +27,11 @@ export function statusLabel(status, fuzzFactor) {
|
|
|
27
27
|
*/
|
|
28
28
|
export function printSummary(session) {
|
|
29
29
|
info('');
|
|
30
|
-
|
|
30
|
+
const from = session.fromProduct
|
|
31
|
+
? `${session.fromProduct} ${session.fromVersion}`
|
|
32
|
+
: session.fromVersion;
|
|
33
|
+
const to = session.toProduct ? `${session.toProduct} ${session.toVersion}` : session.toVersion;
|
|
34
|
+
info(`Source Rebase Summary: ${from} → ${to}`);
|
|
31
35
|
info('='.repeat(55));
|
|
32
36
|
for (const patch of session.patches) {
|
|
33
37
|
const label = statusLabel(patch.status, patch.fuzzFactor);
|
|
@@ -37,7 +41,8 @@ export function printSummary(session) {
|
|
|
37
41
|
const fuzz = session.patches.filter((p) => p.status === 'applied-fuzz').length;
|
|
38
42
|
const resolved = session.patches.filter((p) => p.status === 'resolved').length;
|
|
39
43
|
const failed = session.patches.filter((p) => p.status === 'failed').length;
|
|
44
|
+
const total = session.patches.length;
|
|
40
45
|
info('');
|
|
41
|
-
info(`Results: ${clean} clean, ${fuzz} fuzz-applied, ${resolved} manually resolved, ${failed} failed`);
|
|
46
|
+
info(`Results: ${total} total: ${clean} clean, ${fuzz} fuzz-applied, ${resolved} manually resolved, ${failed} failed`);
|
|
42
47
|
}
|
|
43
48
|
//# sourceMappingURL=summary.js.map
|
|
@@ -8,6 +8,7 @@ import { stageFiles, unstageFiles } from '../core/git-file-ops.js';
|
|
|
8
8
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
9
9
|
import { updatePatchAndMetadata } from '../core/patch-export.js';
|
|
10
10
|
import { loadPatchesManifest } from '../core/patch-manifest.js';
|
|
11
|
+
import { buildPatchSourceMetadata } from '../core/patch-source-metadata.js';
|
|
11
12
|
import { GeneralError, ResolutionError } from '../errors/base.js';
|
|
12
13
|
import { toError } from '../utils/errors.js';
|
|
13
14
|
import { pathExists } from '../utils/fs.js';
|
|
@@ -132,7 +133,7 @@ export async function resolveCommand(projectRoot, options = {}) {
|
|
|
132
133
|
const config = await loadConfig(projectRoot);
|
|
133
134
|
await updatePatchAndMetadata(paths.patches, patchFilename, diffContent, {
|
|
134
135
|
filesAffected: diffFilesAffected,
|
|
135
|
-
|
|
136
|
+
...buildPatchSourceMetadata(config.firefox),
|
|
136
137
|
});
|
|
137
138
|
// Cleanup: Clear pendingResolution from state.json transactionally so
|
|
138
139
|
// we don't clobber concurrent updates to unrelated keys (e.g. another
|
|
@@ -19,10 +19,13 @@ function renderLicenseTemplate(license, template, vendor, now = new Date()) {
|
|
|
19
19
|
return template.replace(/\[year\]/g, String(now.getFullYear())).replace(/\[fullname\]/g, vendor);
|
|
20
20
|
}
|
|
21
21
|
function resolveFirefoxProduct(value, field) {
|
|
22
|
-
if (value === 'firefox' ||
|
|
22
|
+
if (value === 'firefox' ||
|
|
23
|
+
value === 'firefox-esr' ||
|
|
24
|
+
value === 'firefox-beta' ||
|
|
25
|
+
value === 'firefox-devedition') {
|
|
23
26
|
return value;
|
|
24
27
|
}
|
|
25
|
-
throw new InvalidArgumentError('Invalid product (use: firefox, firefox-esr, firefox-beta)', field);
|
|
28
|
+
throw new InvalidArgumentError('Invalid product (use: firefox, firefox-esr, firefox-beta, firefox-devedition)', field);
|
|
26
29
|
}
|
|
27
30
|
function resolveProjectLicense(value, field) {
|
|
28
31
|
if (typeof value === 'string' && isValidProjectLicense(value)) {
|
|
@@ -153,6 +156,7 @@ async function promptSetupInputs(options) {
|
|
|
153
156
|
{ value: 'firefox', label: 'Firefox (stable releases)' },
|
|
154
157
|
{ value: 'firefox-esr', label: 'Firefox ESR (extended support)' },
|
|
155
158
|
{ value: 'firefox-beta', label: 'Firefox Beta (pre-release)' },
|
|
159
|
+
{ value: 'firefox-devedition', label: 'Firefox Developer Edition' },
|
|
156
160
|
],
|
|
157
161
|
});
|
|
158
162
|
},
|
|
@@ -71,6 +71,7 @@ export function registerSetup(program, { withErrorHandling }) {
|
|
|
71
71
|
'firefox',
|
|
72
72
|
'firefox-esr',
|
|
73
73
|
'firefox-beta',
|
|
74
|
+
'firefox-devedition',
|
|
74
75
|
]))
|
|
75
76
|
.addOption(new Option('--license <license>', 'Project license').choices([...PROJECT_LICENSES]))
|
|
76
77
|
.option('-f, --force', 'Overwrite existing configuration without prompting')
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { CommandContext } from '../types/cli.js';
|
|
3
|
+
import type { SourceSetOptions } from '../types/commands/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* Atomically updates the Firefox source tuple in fireforge.json.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sourceSetCommand(projectRoot: string, options: SourceSetOptions): Promise<void>;
|
|
8
|
+
/** Registers the source command on the CLI program. */
|
|
9
|
+
export declare function registerSource(program: Command, { getProjectRoot, withErrorHandling }: CommandContext): void;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { Option } from 'commander';
|
|
3
|
+
import { configExists, loadRawConfigDocument, validateConfig, withConfigFileLock, writeConfigDocument, } from '../core/config.js';
|
|
4
|
+
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
5
|
+
import { info, intro, outro, success } from '../utils/logger.js';
|
|
6
|
+
import { isValidFirefoxProduct } from '../utils/validation.js';
|
|
7
|
+
const SOURCE_PRODUCTS = [
|
|
8
|
+
'firefox',
|
|
9
|
+
'firefox-esr',
|
|
10
|
+
'firefox-beta',
|
|
11
|
+
'firefox-devedition',
|
|
12
|
+
];
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
function cloneRawConfig(raw) {
|
|
17
|
+
const cloned = structuredClone(raw);
|
|
18
|
+
if (!isRecord(cloned)) {
|
|
19
|
+
throw new GeneralError('Cannot update fireforge.json: config clone was not an object.');
|
|
20
|
+
}
|
|
21
|
+
return cloned;
|
|
22
|
+
}
|
|
23
|
+
function parseSourceProduct(product) {
|
|
24
|
+
if (isValidFirefoxProduct(product)) {
|
|
25
|
+
return product;
|
|
26
|
+
}
|
|
27
|
+
throw new InvalidArgumentError(`--product must be one of: ${SOURCE_PRODUCTS.join(', ')}`, '--product');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Atomically updates the Firefox source tuple in fireforge.json.
|
|
31
|
+
*/
|
|
32
|
+
export async function sourceSetCommand(projectRoot, options) {
|
|
33
|
+
intro('FireForge Source');
|
|
34
|
+
if (!(await configExists(projectRoot))) {
|
|
35
|
+
throw new GeneralError('No fireforge.json found. Run "fireforge setup" to create a project.');
|
|
36
|
+
}
|
|
37
|
+
if (options.sha256 !== undefined && options.clearSha256 === true) {
|
|
38
|
+
throw new InvalidArgumentError('--sha256 cannot be combined with --clear-sha256', '--sha256');
|
|
39
|
+
}
|
|
40
|
+
const written = await withConfigFileLock(projectRoot, async () => {
|
|
41
|
+
const raw = await loadRawConfigDocument(projectRoot);
|
|
42
|
+
const updated = cloneRawConfig(raw);
|
|
43
|
+
const firefox = isRecord(updated['firefox']) ? { ...updated['firefox'] } : {};
|
|
44
|
+
firefox['version'] = options.version;
|
|
45
|
+
firefox['product'] = options.product;
|
|
46
|
+
if (options.clearSha256 === true) {
|
|
47
|
+
delete firefox['sha256'];
|
|
48
|
+
}
|
|
49
|
+
else if (options.sha256 !== undefined) {
|
|
50
|
+
firefox['sha256'] = options.sha256;
|
|
51
|
+
}
|
|
52
|
+
updated['firefox'] = firefox;
|
|
53
|
+
const validated = validateConfig(updated);
|
|
54
|
+
if (validated.firefox.sha256 !== undefined) {
|
|
55
|
+
firefox['sha256'] = validated.firefox.sha256;
|
|
56
|
+
}
|
|
57
|
+
await writeConfigDocument(projectRoot, updated);
|
|
58
|
+
return validated.firefox;
|
|
59
|
+
});
|
|
60
|
+
success(`Set firefox.version = ${written.version}`);
|
|
61
|
+
success(`Set firefox.product = ${written.product}`);
|
|
62
|
+
if (written.sha256 !== undefined) {
|
|
63
|
+
success(`Set firefox.sha256 = ${written.sha256}`);
|
|
64
|
+
}
|
|
65
|
+
else if (options.clearSha256 === true) {
|
|
66
|
+
info('Cleared firefox.sha256');
|
|
67
|
+
}
|
|
68
|
+
outro('');
|
|
69
|
+
}
|
|
70
|
+
/** Registers the source command on the CLI program. */
|
|
71
|
+
export function registerSource(program, { getProjectRoot, withErrorHandling }) {
|
|
72
|
+
const source = program.command('source').description('Manage Firefox source configuration');
|
|
73
|
+
source
|
|
74
|
+
.command('set')
|
|
75
|
+
.description('Atomically set Firefox source version, product, and optional checksum')
|
|
76
|
+
.requiredOption('--version <version>', 'Firefox version to base on')
|
|
77
|
+
.addOption(new Option('--product <product>', 'Firefox product')
|
|
78
|
+
.choices([...SOURCE_PRODUCTS])
|
|
79
|
+
.makeOptionMandatory())
|
|
80
|
+
.option('--sha256 <hash>', 'Pinned SHA-256 for the resolved source archive')
|
|
81
|
+
.option('--clear-sha256', 'Clear any existing pinned SHA-256')
|
|
82
|
+
.action(withErrorHandling(async (options) => {
|
|
83
|
+
const { product, version, sha256, clearSha256 } = options;
|
|
84
|
+
await sourceSetCommand(getProjectRoot(), {
|
|
85
|
+
version,
|
|
86
|
+
product: parseSourceProduct(product),
|
|
87
|
+
...(sha256 !== undefined ? { sha256 } : {}),
|
|
88
|
+
...(clearSha256 !== undefined ? { clearSha256 } : {}),
|
|
89
|
+
});
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=source.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ClassifiedFile } from '../core/status-classify.js';
|
|
2
|
+
export interface ClassifiedBuckets {
|
|
3
|
+
conflict: ClassifiedFile[];
|
|
4
|
+
unmanaged: ClassifiedFile[];
|
|
5
|
+
patchBacked: ClassifiedFile[];
|
|
6
|
+
patchOwnedDrift: ClassifiedFile[];
|
|
7
|
+
branding: ClassifiedFile[];
|
|
8
|
+
furnace: ClassifiedFile[];
|
|
9
|
+
}
|
|
10
|
+
/** Renders the unmanaged-only status view and registration hints. */
|
|
11
|
+
export declare function renderUnmanagedOnly(unmanagedFiles: ClassifiedFile[], totalModified: number, projectRoot: string, binaryName: string): Promise<void>;
|
|
12
|
+
/** Renders the default classified status buckets. */
|
|
13
|
+
export declare function renderDefaultStatus(totalModified: number, buckets: ClassifiedBuckets, projectRoot: string, binaryName: string): Promise<void>;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { isFileRegistered, matchesRegistrablePattern } from '../core/manifest-rules.js';
|
|
3
|
+
import { GeneralError } from '../errors/base.js';
|
|
4
|
+
import { info, outro, warn } from '../utils/logger.js';
|
|
5
|
+
const STATUS_DESCRIPTIONS = {
|
|
6
|
+
M: 'modified',
|
|
7
|
+
A: 'added',
|
|
8
|
+
D: 'deleted',
|
|
9
|
+
R: 'renamed',
|
|
10
|
+
C: 'copied',
|
|
11
|
+
U: 'unmerged',
|
|
12
|
+
'?': 'untracked',
|
|
13
|
+
'!': 'ignored',
|
|
14
|
+
};
|
|
15
|
+
function getStatusDescription(code) {
|
|
16
|
+
return STATUS_DESCRIPTIONS[code] ?? 'changed';
|
|
17
|
+
}
|
|
18
|
+
function getPrimaryStatusCode(status) {
|
|
19
|
+
if (status.includes('?'))
|
|
20
|
+
return '?';
|
|
21
|
+
if (status.includes('!'))
|
|
22
|
+
return '!';
|
|
23
|
+
for (const code of status) {
|
|
24
|
+
if (code !== ' ')
|
|
25
|
+
return code;
|
|
26
|
+
}
|
|
27
|
+
return status;
|
|
28
|
+
}
|
|
29
|
+
function isNewFileStatus(status) {
|
|
30
|
+
const code = getPrimaryStatusCode(status);
|
|
31
|
+
return code === '?' || code === 'A';
|
|
32
|
+
}
|
|
33
|
+
function groupFilesByStatus(files) {
|
|
34
|
+
const grouped = new Map();
|
|
35
|
+
for (const { status, file } of files) {
|
|
36
|
+
const code = getPrimaryStatusCode(status);
|
|
37
|
+
const existing = grouped.get(code) ?? [];
|
|
38
|
+
existing.push(file);
|
|
39
|
+
grouped.set(code, existing);
|
|
40
|
+
}
|
|
41
|
+
return grouped;
|
|
42
|
+
}
|
|
43
|
+
function printStatusGroups(files) {
|
|
44
|
+
const grouped = groupFilesByStatus(files);
|
|
45
|
+
for (const [status, fileList] of grouped) {
|
|
46
|
+
warn(`${getStatusDescription(status)}:`);
|
|
47
|
+
for (const file of fileList)
|
|
48
|
+
info(` ${file}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function printUnregisteredWarnings(files, projectRoot, binaryName) {
|
|
52
|
+
const newFiles = files.filter((f) => isNewFileStatus(f.status));
|
|
53
|
+
if (newFiles.length === 0)
|
|
54
|
+
return;
|
|
55
|
+
const registrableFiles = newFiles.filter((f) => matchesRegistrablePattern(f.file, binaryName));
|
|
56
|
+
const registrationChecks = await Promise.all(registrableFiles.map(async (f) => {
|
|
57
|
+
try {
|
|
58
|
+
return {
|
|
59
|
+
file: f.file,
|
|
60
|
+
registered: await isFileRegistered(projectRoot, f.file),
|
|
61
|
+
manifestMissing: false,
|
|
62
|
+
manifestMissingMessage: undefined,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
if (err instanceof GeneralError && /^Manifest not found:/i.test(err.message)) {
|
|
67
|
+
return {
|
|
68
|
+
file: f.file,
|
|
69
|
+
registered: false,
|
|
70
|
+
manifestMissing: true,
|
|
71
|
+
manifestMissingMessage: err.message,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
const unregistered = registrationChecks.filter((f) => !f.registered && !f.manifestMissing);
|
|
78
|
+
const manifestMissing = registrationChecks.filter((f) => f.manifestMissing);
|
|
79
|
+
if (unregistered.length > 0) {
|
|
80
|
+
info('');
|
|
81
|
+
warn('Potentially unregistered files:');
|
|
82
|
+
for (const f of unregistered)
|
|
83
|
+
info(` ${f.file} — run 'fireforge register ${f.file}'`);
|
|
84
|
+
}
|
|
85
|
+
if (manifestMissing.length > 0) {
|
|
86
|
+
info('');
|
|
87
|
+
warn('Files whose registration manifest does not exist yet:');
|
|
88
|
+
for (const f of manifestMissing) {
|
|
89
|
+
info(` ${f.file} — ${f.manifestMissingMessage}`);
|
|
90
|
+
info(` Create the parent manifest, then run 'fireforge register ${f.file}'.`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Renders the unmanaged-only status view and registration hints. */
|
|
95
|
+
export async function renderUnmanagedOnly(unmanagedFiles, totalModified, projectRoot, binaryName) {
|
|
96
|
+
info(`${unmanagedFiles.length} unmanaged file${unmanagedFiles.length === 1 ? '' : 's'} (${totalModified} total modified):\n`);
|
|
97
|
+
if (unmanagedFiles.length > 0) {
|
|
98
|
+
printStatusGroups(unmanagedFiles);
|
|
99
|
+
await printUnregisteredWarnings(unmanagedFiles, projectRoot, binaryName);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
info('No unmanaged changes');
|
|
103
|
+
}
|
|
104
|
+
outro(unmanagedFiles.length === 0
|
|
105
|
+
? 'No unmanaged changes'
|
|
106
|
+
: `${unmanagedFiles.length} unmanaged change${unmanagedFiles.length === 1 ? '' : 's'}`);
|
|
107
|
+
}
|
|
108
|
+
/** Renders the default classified status buckets. */
|
|
109
|
+
export async function renderDefaultStatus(totalModified, buckets, projectRoot, binaryName) {
|
|
110
|
+
const { conflict, unmanaged, patchBacked, patchOwnedDrift, branding, furnace } = buckets;
|
|
111
|
+
info(`${totalModified} modified file${totalModified === 1 ? '' : 's'}:\n`);
|
|
112
|
+
if (conflict.length > 0) {
|
|
113
|
+
warn('Cross-patch ownership conflicts (same file claimed by multiple patches):');
|
|
114
|
+
printStatusGroups(conflict);
|
|
115
|
+
for (const entry of conflict) {
|
|
116
|
+
if (entry.claimedBy && entry.claimedBy.length > 0) {
|
|
117
|
+
info(` ${entry.file} — claimed by ${entry.claimedBy.join(', ')}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
info('Run "fireforge status --ownership" for the full conflict table, then repartition with "fireforge re-export --files <paths> <patch>".');
|
|
121
|
+
}
|
|
122
|
+
if (unmanaged.length > 0) {
|
|
123
|
+
if (conflict.length > 0)
|
|
124
|
+
info('');
|
|
125
|
+
warn('Unmanaged changes:');
|
|
126
|
+
printStatusGroups(unmanaged);
|
|
127
|
+
await printUnregisteredWarnings(unmanaged, projectRoot, binaryName);
|
|
128
|
+
}
|
|
129
|
+
if (patchBacked.length > 0) {
|
|
130
|
+
if (conflict.length > 0 || unmanaged.length > 0)
|
|
131
|
+
info('');
|
|
132
|
+
warn('Patch-backed materialized changes:');
|
|
133
|
+
printStatusGroups(patchBacked);
|
|
134
|
+
}
|
|
135
|
+
if (patchOwnedDrift.length > 0) {
|
|
136
|
+
if (conflict.length > 0 || unmanaged.length > 0 || patchBacked.length > 0)
|
|
137
|
+
info('');
|
|
138
|
+
warn('Patch-owned drift:');
|
|
139
|
+
printStatusGroups(patchOwnedDrift);
|
|
140
|
+
info('These files are claimed by exactly one patch, but engine/ no longer matches that patch output. Re-export the owning patch after reviewing the manual resolution.');
|
|
141
|
+
}
|
|
142
|
+
if (branding.length > 0) {
|
|
143
|
+
if (conflict.length > 0 ||
|
|
144
|
+
unmanaged.length > 0 ||
|
|
145
|
+
patchBacked.length > 0 ||
|
|
146
|
+
patchOwnedDrift.length > 0) {
|
|
147
|
+
info('');
|
|
148
|
+
}
|
|
149
|
+
warn('Tool-managed branding changes:');
|
|
150
|
+
printStatusGroups(branding);
|
|
151
|
+
}
|
|
152
|
+
if (furnace.length > 0) {
|
|
153
|
+
if (conflict.length > 0 ||
|
|
154
|
+
unmanaged.length > 0 ||
|
|
155
|
+
patchBacked.length > 0 ||
|
|
156
|
+
patchOwnedDrift.length > 0 ||
|
|
157
|
+
branding.length > 0) {
|
|
158
|
+
info('');
|
|
159
|
+
}
|
|
160
|
+
warn('Furnace-managed component changes:');
|
|
161
|
+
printStatusGroups(furnace);
|
|
162
|
+
}
|
|
163
|
+
if (conflict.length === 0 &&
|
|
164
|
+
unmanaged.length === 0 &&
|
|
165
|
+
patchBacked.length === 0 &&
|
|
166
|
+
patchOwnedDrift.length === 0 &&
|
|
167
|
+
branding.length === 0 &&
|
|
168
|
+
furnace.length === 0) {
|
|
169
|
+
info('No changes');
|
|
170
|
+
}
|
|
171
|
+
const parts = [];
|
|
172
|
+
if (conflict.length > 0)
|
|
173
|
+
parts.push(`${conflict.length} conflict`);
|
|
174
|
+
if (unmanaged.length > 0)
|
|
175
|
+
parts.push(`${unmanaged.length} unmanaged`);
|
|
176
|
+
if (patchBacked.length > 0)
|
|
177
|
+
parts.push(`${patchBacked.length} patch-backed`);
|
|
178
|
+
if (patchOwnedDrift.length > 0)
|
|
179
|
+
parts.push(`${patchOwnedDrift.length} patch-owned drift`);
|
|
180
|
+
if (branding.length > 0)
|
|
181
|
+
parts.push(`${branding.length} branding`);
|
|
182
|
+
if (furnace.length > 0)
|
|
183
|
+
parts.push(`${furnace.length} furnace`);
|
|
184
|
+
outro(parts.join(', '));
|
|
185
|
+
}
|
|
186
|
+
//# sourceMappingURL=status-output.js.map
|