@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
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getProjectPaths } from '../core/config.js';
|
|
2
|
+
import type { PatchMetadata, ReExportOptions } from '../types/commands/index.js';
|
|
3
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
4
|
+
/**
|
|
5
|
+
* Handles `re-export --files` end-to-end: computes the projected diff,
|
|
6
|
+
* runs the per-patch and cross-patch lint against a context in which the
|
|
7
|
+
* target patch has been replaced with the projected state, gates on
|
|
8
|
+
* confirmDestructive, and writes atomically.
|
|
9
|
+
*
|
|
10
|
+
* Lives outside reExportSinglePatch because the --files path has strictly
|
|
11
|
+
* different semantics (authoritative file list, destructive shrink
|
|
12
|
+
* confirmation, cross-patch projection lint) and shoehorning it through
|
|
13
|
+
* the generic single-patch helper is what led to the earlier bug where
|
|
14
|
+
* the projection lint ran against the current (unchanged) queue instead
|
|
15
|
+
* of the projected state.
|
|
16
|
+
*/
|
|
17
|
+
export declare function reExportFilesInPlace(paths: ReturnType<typeof getProjectPaths>, selectedPatches: PatchMetadata[], options: ReExportOptions, config: FireForgeConfig): Promise<void>;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { appendHistory, confirmDestructive } from '../core/destructive.js';
|
|
4
|
+
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
5
|
+
import { computeProjectedLintRegressions } from '../core/lint-projection.js';
|
|
6
|
+
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
7
|
+
import { updatePatchAndMetadata } from '../core/patch-export.js';
|
|
8
|
+
import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, detectNewFilesInDiff, lintPatchQueue, } from '../core/patch-lint.js';
|
|
9
|
+
import { extractNewFileContentFromDiff } from '../core/patch-transform.js';
|
|
10
|
+
import { InvalidArgumentError } from '../errors/base.js';
|
|
11
|
+
import { pathExists } from '../utils/fs.js';
|
|
12
|
+
import { info, outro, success, warn } from '../utils/logger.js';
|
|
13
|
+
import { runPatchLint } from './export-shared.js';
|
|
14
|
+
/**
|
|
15
|
+
* Handles `re-export --files` end-to-end: computes the projected diff,
|
|
16
|
+
* runs the per-patch and cross-patch lint against a context in which the
|
|
17
|
+
* target patch has been replaced with the projected state, gates on
|
|
18
|
+
* confirmDestructive, and writes atomically.
|
|
19
|
+
*
|
|
20
|
+
* Lives outside reExportSinglePatch because the --files path has strictly
|
|
21
|
+
* different semantics (authoritative file list, destructive shrink
|
|
22
|
+
* confirmation, cross-patch projection lint) and shoehorning it through
|
|
23
|
+
* the generic single-patch helper is what led to the earlier bug where
|
|
24
|
+
* the projection lint ran against the current (unchanged) queue instead
|
|
25
|
+
* of the projected state.
|
|
26
|
+
*/
|
|
27
|
+
export async function reExportFilesInPlace(paths, selectedPatches, options, config) {
|
|
28
|
+
const isDryRun = options.dryRun === true;
|
|
29
|
+
const target = selectedPatches[0];
|
|
30
|
+
if (!target) {
|
|
31
|
+
throw new InvalidArgumentError('--files requires a target patch.', '--files');
|
|
32
|
+
}
|
|
33
|
+
const filesOption = options.files;
|
|
34
|
+
if (filesOption === undefined) {
|
|
35
|
+
throw new InvalidArgumentError('reExportFilesInPlace called with no --files.', '--files');
|
|
36
|
+
}
|
|
37
|
+
const requested = [...new Set(filesOption)].sort();
|
|
38
|
+
const removed = target.filesAffected.filter((f) => !requested.includes(f));
|
|
39
|
+
const added = requested.filter((f) => !target.filesAffected.includes(f));
|
|
40
|
+
// Filter out paths that no longer exist on disk; we cannot include
|
|
41
|
+
// them in the new diff because getDiffForFilesAgainstHead would fail.
|
|
42
|
+
// Missing files are still dropped from the manifest so the resulting
|
|
43
|
+
// filesAffected reflects reality.
|
|
44
|
+
const missingFiles = [];
|
|
45
|
+
for (const file of requested) {
|
|
46
|
+
const filePath = join(paths.engine, file);
|
|
47
|
+
if (!(await pathExists(filePath))) {
|
|
48
|
+
missingFiles.push(file);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const missingSet = new Set(missingFiles);
|
|
52
|
+
const diffableFiles = requested.filter((f) => !missingSet.has(f));
|
|
53
|
+
for (const file of missingFiles) {
|
|
54
|
+
warn(`${target.filename}: requested file is missing on disk and will be dropped: ${file}`);
|
|
55
|
+
}
|
|
56
|
+
// Compute the projected diff up front. This is the same diff the real
|
|
57
|
+
// write would produce, so we get an exact preview through the lint
|
|
58
|
+
// gate and avoid computing it twice.
|
|
59
|
+
const projectedDiff = diffableFiles.length > 0 ? await getDiffForFilesAgainstHead(paths.engine, diffableFiles) : '';
|
|
60
|
+
if (!projectedDiff.trim()) {
|
|
61
|
+
throw new InvalidArgumentError(`Refusing to re-export ${target.filename} with --files because the projected scope ` +
|
|
62
|
+
'produces an empty patch. FireForge does not write zero-hunk patch files; ' +
|
|
63
|
+
`use "fireforge patch delete ${target.filename}" if this patch should be removed entirely.`, '--files');
|
|
64
|
+
}
|
|
65
|
+
const actualProjectedFiles = extractAffectedFiles(projectedDiff);
|
|
66
|
+
const actualProjectedSet = new Set(actualProjectedFiles);
|
|
67
|
+
const noDiffFiles = diffableFiles.filter((file) => !actualProjectedSet.has(file));
|
|
68
|
+
if (noDiffFiles.length > 0) {
|
|
69
|
+
throw new InvalidArgumentError(`Refusing to re-export ${target.filename} with --files because ${noDiffFiles.length} requested path${noDiffFiles.length === 1 ? '' : 's'} produced no diff hunks (${noDiffFiles.join(', ')}). ` +
|
|
70
|
+
'Keeping them in filesAffected would desync patches.json from the patch body. ' +
|
|
71
|
+
'Remove those paths from --files or modify them before retrying.', '--files');
|
|
72
|
+
}
|
|
73
|
+
// Run the per-patch lint against the projected diff. This mirrors what
|
|
74
|
+
// runPatchLint does in the standard re-export path.
|
|
75
|
+
await runPatchLint(paths.engine, actualProjectedFiles, projectedDiff, config, options.skipLint);
|
|
76
|
+
// Project the cross-patch context: replace the target entry with its
|
|
77
|
+
// would-be shrunken self (new diff + new newFiles + new
|
|
78
|
+
// modifiedFileAdditions). The projected entry must repopulate both
|
|
79
|
+
// source-site maps so the forward-import rule sees imports the
|
|
80
|
+
// shrunken diff would add — or stop adding — consistently with how a
|
|
81
|
+
// real rebuild would see them.
|
|
82
|
+
const baseCtx = await buildPatchQueueContext(paths.patches);
|
|
83
|
+
const projectedNewFiles = new Map();
|
|
84
|
+
for (const path of detectNewFilesInDiff(projectedDiff)) {
|
|
85
|
+
projectedNewFiles.set(path, extractNewFileContentFromDiff(projectedDiff, path));
|
|
86
|
+
}
|
|
87
|
+
const projectedModifiedFileAdditions = buildModifiedFileAdditionsFromDiff(projectedDiff);
|
|
88
|
+
const projectedEntries = baseCtx.entries.map((entry) => {
|
|
89
|
+
if (entry.filename !== target.filename)
|
|
90
|
+
return entry;
|
|
91
|
+
return {
|
|
92
|
+
...entry,
|
|
93
|
+
diff: projectedDiff,
|
|
94
|
+
newFiles: projectedNewFiles,
|
|
95
|
+
modifiedFileAdditions: projectedModifiedFileAdditions,
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
// Baseline-vs-projected diffing: only regressions introduced *by* this
|
|
99
|
+
// shrink should block. A pre-existing cross-patch error elsewhere in
|
|
100
|
+
// the queue must not prevent the user from shrinking an unrelated
|
|
101
|
+
// patch (which is often exactly the tool they reach for to repair
|
|
102
|
+
// such a queue).
|
|
103
|
+
const baselineIssues = lintPatchQueue(baseCtx).filter((i) => i.severity === 'error');
|
|
104
|
+
const projectedIssues = lintPatchQueue({ entries: projectedEntries }).filter((i) => i.severity === 'error');
|
|
105
|
+
const regressions = computeProjectedLintRegressions(baselineIssues, projectedIssues);
|
|
106
|
+
const conflicts = regressions.length > 0
|
|
107
|
+
? {
|
|
108
|
+
reason: `projected --files state introduces ${regressions.length} new cross-patch lint error(s)`,
|
|
109
|
+
details: regressions.map((i) => `[${i.check}] ${i.file}: ${i.message}`),
|
|
110
|
+
}
|
|
111
|
+
: null;
|
|
112
|
+
// Surface pre-existing errors as a non-blocking warning so the user
|
|
113
|
+
// doesn't walk away thinking the queue is clean.
|
|
114
|
+
if (baselineIssues.length > 0 && regressions.length === 0) {
|
|
115
|
+
warn(`Note: projected queue still has ${baselineIssues.length} pre-existing ` +
|
|
116
|
+
`cross-patch error(s) unrelated to this shrink. Run "fireforge verify" to list them.`);
|
|
117
|
+
}
|
|
118
|
+
// Shrinks are destructive (previously-owned files become unmanaged).
|
|
119
|
+
// Additive-only changes still deserve a prompt because --files asserts
|
|
120
|
+
// an authoritative file set.
|
|
121
|
+
const summary = [
|
|
122
|
+
`re-export ${target.filename} with --files scope`,
|
|
123
|
+
`current files (${target.filesAffected.length}): ${target.filesAffected.join(', ') || '(none)'}`,
|
|
124
|
+
`new files (${actualProjectedFiles.length}): ${actualProjectedFiles.join(', ') || '(none)'}`,
|
|
125
|
+
];
|
|
126
|
+
if (removed.length > 0) {
|
|
127
|
+
summary.push(`would drop (become unmanaged): ${removed.join(', ')}`);
|
|
128
|
+
}
|
|
129
|
+
if (added.length > 0) {
|
|
130
|
+
summary.push(`would add: ${added.join(', ')}`);
|
|
131
|
+
}
|
|
132
|
+
if (missingFiles.length > 0) {
|
|
133
|
+
summary.push(`missing on disk (will be dropped): ${missingFiles.join(', ')}`);
|
|
134
|
+
}
|
|
135
|
+
const decision = await confirmDestructive({
|
|
136
|
+
operation: 're-export-files',
|
|
137
|
+
title: `Re-export ${target.filename} with --files`,
|
|
138
|
+
summary,
|
|
139
|
+
yes: options.yes === true,
|
|
140
|
+
dryRun: isDryRun,
|
|
141
|
+
unsafeOverride: options.forceUnsafe === true,
|
|
142
|
+
conflicts,
|
|
143
|
+
});
|
|
144
|
+
if (decision === 'cancelled') {
|
|
145
|
+
outro('Re-export cancelled');
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (decision === 'dry-run') {
|
|
149
|
+
info(`[dry-run] ${target.filename}: ${actualProjectedFiles.length} file(s) in projected scope`);
|
|
150
|
+
outro('Dry run complete — no changes made');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Execute the write. At this point the projected diff is guaranteed to
|
|
154
|
+
// be non-empty and `actualProjectedFiles` is guaranteed to match the
|
|
155
|
+
// paths the body really touches, so the manifest cannot drift from the
|
|
156
|
+
// regenerated patch body. The history append runs inside the same patch
|
|
157
|
+
// directory lock as the mutation (via the onCommitted hook) so two
|
|
158
|
+
// concurrent re-exports cannot interleave records and a crash between
|
|
159
|
+
// mutation and append cannot orphan the audit trail.
|
|
160
|
+
await updatePatchAndMetadata(paths.patches, target.filename, projectedDiff, { filesAffected: actualProjectedFiles }, async () => {
|
|
161
|
+
await appendHistory(paths.patches, {
|
|
162
|
+
operation: 're-export-files',
|
|
163
|
+
args: {
|
|
164
|
+
filename: target.filename,
|
|
165
|
+
files: actualProjectedFiles,
|
|
166
|
+
previousFiles: target.filesAffected,
|
|
167
|
+
missingFilesDropped: missingFiles,
|
|
168
|
+
},
|
|
169
|
+
...(options.yes === true ? { yes: true } : {}),
|
|
170
|
+
...(options.forceUnsafe === true ? { unsafeOverride: true } : {}),
|
|
171
|
+
result: 'ok',
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
success(`Re-exported ${target.filename}`);
|
|
175
|
+
outro('Re-export complete');
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=re-export-files.js.map
|
|
@@ -13,6 +13,7 @@ import { pathExists } from '../utils/fs.js';
|
|
|
13
13
|
import { cancel, info, intro, isCancel, outro, spinner, success, warn } from '../utils/logger.js';
|
|
14
14
|
import { pickDefined } from '../utils/options.js';
|
|
15
15
|
import { runPatchLint } from './export-shared.js';
|
|
16
|
+
import { reExportFilesInPlace } from './re-export-files.js';
|
|
16
17
|
/**
|
|
17
18
|
* Resolves patch identifiers (numbers or filenames) to manifest entries.
|
|
18
19
|
* @param identifier - Patch number (e.g. "005") or filename (e.g. "005-ui-storage-modules.patch")
|
|
@@ -74,6 +75,22 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
74
75
|
if (options.scan) {
|
|
75
76
|
currentFilesAffected = await scanPatchFiles(currentFilesAffected, paths.engine, manifest, patch.filename, isDryRun);
|
|
76
77
|
}
|
|
78
|
+
// --- Explicit file-subset path ---
|
|
79
|
+
// When --files is given, the target filesAffected is authoritative — drop
|
|
80
|
+
// anything not in the list, add anything new. This is the surgical repair
|
|
81
|
+
// primitive that replaces hand-editing patches.json; the user has already
|
|
82
|
+
// acknowledged via confirmDestructive (done in the caller) that any drop
|
|
83
|
+
// is intentional.
|
|
84
|
+
if (options.files !== undefined) {
|
|
85
|
+
const requested = [...new Set(options.files)].sort();
|
|
86
|
+
currentFilesAffected = requested;
|
|
87
|
+
const removed = patch.filesAffected.filter((f) => !requested.includes(f));
|
|
88
|
+
const added = requested.filter((f) => !patch.filesAffected.includes(f));
|
|
89
|
+
for (const f of added)
|
|
90
|
+
info(` + ${f}`);
|
|
91
|
+
for (const f of removed)
|
|
92
|
+
info(` - ${f}`);
|
|
93
|
+
}
|
|
77
94
|
const missingFiles = [];
|
|
78
95
|
for (const file of currentFilesAffected) {
|
|
79
96
|
const filePath = join(paths.engine, file);
|
|
@@ -165,6 +182,16 @@ async function resolveSelectedPatches(patches, options, manifest) {
|
|
|
165
182
|
export async function reExportCommand(projectRoot, patches, options) {
|
|
166
183
|
const isDryRun = options.dryRun === true;
|
|
167
184
|
intro(isDryRun ? 'FireForge Re-export (dry run)' : 'FireForge Re-export');
|
|
185
|
+
// --files is mutually exclusive with --scan and --all: they select
|
|
186
|
+
// different scope contracts.
|
|
187
|
+
if (options.files !== undefined) {
|
|
188
|
+
if (options.all || options.scan) {
|
|
189
|
+
throw new InvalidArgumentError('--files cannot be combined with --scan or --all.', '--files');
|
|
190
|
+
}
|
|
191
|
+
if (patches.length !== 1) {
|
|
192
|
+
throw new InvalidArgumentError('--files operates on exactly one target patch. Pass a single patch identifier.', '--files');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
168
195
|
const paths = getProjectPaths(projectRoot);
|
|
169
196
|
// Check if engine exists
|
|
170
197
|
if (!(await pathExists(paths.engine))) {
|
|
@@ -188,6 +215,17 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
188
215
|
outro('Nothing to re-export');
|
|
189
216
|
return;
|
|
190
217
|
}
|
|
218
|
+
// --files path: handled end-to-end here so we can lint the *projected*
|
|
219
|
+
// shrunken state (not the current queue) and skip the generic re-export
|
|
220
|
+
// loop. The projection substitutes the target patch's diff and newFiles
|
|
221
|
+
// with the freshly computed content, then runs lintPatchQueue so any
|
|
222
|
+
// forward-import introduced or uncovered by the shrink is caught before
|
|
223
|
+
// we write anything.
|
|
224
|
+
if (options.files !== undefined) {
|
|
225
|
+
const filesConfig = await loadConfig(projectRoot);
|
|
226
|
+
await reExportFilesInPlace(paths, selectedPatches, options, filesConfig);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
191
229
|
const config = await loadConfig(projectRoot);
|
|
192
230
|
let reExported = 0;
|
|
193
231
|
const progress = spinner('Preparing re-export...');
|
|
@@ -225,8 +263,14 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
225
263
|
.description('Re-export existing patches from current engine state')
|
|
226
264
|
.option('-a, --all', 'Re-export all patches')
|
|
227
265
|
.option('-s, --scan', 'Scan directories for new/removed files and update filesAffected')
|
|
266
|
+
.option('--files <paths>', 'Restrict the re-exported filesAffected to this comma-separated list (single target patch only)', (value) => value
|
|
267
|
+
.split(',')
|
|
268
|
+
.map((v) => v.trim())
|
|
269
|
+
.filter((v) => v.length > 0))
|
|
228
270
|
.option('--dry-run', 'Show what would change without writing')
|
|
229
271
|
.option('--skip-lint', 'Skip patch lint checks (downgrade errors to warnings)')
|
|
272
|
+
.option('-y, --yes', 'Skip confirmation when --files shrinks a patch (required for non-TTY)')
|
|
273
|
+
.option('--force-unsafe', 'Bypass cross-patch lint refusal when --files shrinks a patch')
|
|
230
274
|
.action(withErrorHandling(async (patches, options) => {
|
|
231
275
|
await reExportCommand(getProjectRoot(), patches, pickDefined(options));
|
|
232
276
|
}));
|
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
* Rebase abort flow.
|
|
4
4
|
*/
|
|
5
5
|
import { getProjectPaths, loadState, saveState } from '../../core/config.js';
|
|
6
|
+
import { getFurnacePaths, updateFurnaceState } from '../../core/furnace-config.js';
|
|
6
7
|
import { resetChanges } from '../../core/git.js';
|
|
7
8
|
import { clearRebaseSession, loadRebaseSession } from '../../core/rebase-session.js';
|
|
8
9
|
import { NoRebaseSessionError } from '../../errors/rebase.js';
|
|
10
|
+
import { pathExists } from '../../utils/fs.js';
|
|
9
11
|
import { intro, outro, spinner, success } from '../../utils/logger.js';
|
|
10
12
|
import { confirmDirtyEngineReset } from './confirm.js';
|
|
11
13
|
/**
|
|
12
14
|
* Handles `fireforge rebase --abort`.
|
|
13
15
|
*/
|
|
14
|
-
export async function handleAbort(projectRoot,
|
|
16
|
+
export async function handleAbort(projectRoot, yes) {
|
|
15
17
|
intro('FireForge Rebase — Abort');
|
|
16
18
|
const session = await loadRebaseSession(projectRoot);
|
|
17
19
|
if (!session)
|
|
@@ -19,8 +21,8 @@ export async function handleAbort(projectRoot, force) {
|
|
|
19
21
|
const paths = getProjectPaths(projectRoot);
|
|
20
22
|
if (!(await confirmDirtyEngineReset({
|
|
21
23
|
engineDir: paths.engine,
|
|
22
|
-
|
|
23
|
-
nonInteractiveHint: 'Use: fireforge rebase --abort --
|
|
24
|
+
yes: yes ?? false,
|
|
25
|
+
nonInteractiveHint: 'Use: fireforge rebase --abort --yes',
|
|
24
26
|
warningMessage: 'The engine directory has uncommitted changes that will be lost.',
|
|
25
27
|
promptMessage: 'Discard uncommitted changes and abort rebase?',
|
|
26
28
|
cancelMessage: 'Abort cancelled',
|
|
@@ -31,6 +33,13 @@ export async function handleAbort(projectRoot, force) {
|
|
|
31
33
|
try {
|
|
32
34
|
await resetChanges(paths.engine);
|
|
33
35
|
s.stop('Engine restored');
|
|
36
|
+
// Clear Furnace state — the engine has been rolled back to pre-rebase state.
|
|
37
|
+
const furnacePaths = getFurnacePaths(projectRoot);
|
|
38
|
+
if (await pathExists(furnacePaths.furnaceState)) {
|
|
39
|
+
await updateFurnaceState(projectRoot, (current) => ({
|
|
40
|
+
...(current.pendingRepair ? { pendingRepair: current.pendingRepair } : {}),
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
34
43
|
}
|
|
35
44
|
catch (error) {
|
|
36
45
|
s.error('Failed to restore engine');
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
/** Options for the dirty-engine confirmation prompt. */
|
|
5
5
|
export interface DirtyEngineConfirmationOptions {
|
|
6
6
|
engineDir: string;
|
|
7
|
-
|
|
7
|
+
yes: boolean;
|
|
8
8
|
nonInteractiveHint: string;
|
|
9
9
|
warningMessage: string;
|
|
10
10
|
promptMessage: string;
|
|
@@ -13,6 +13,6 @@ export interface DirtyEngineConfirmationOptions {
|
|
|
13
13
|
/**
|
|
14
14
|
* Checks if the engine has uncommitted changes and prompts for confirmation.
|
|
15
15
|
* Returns true if safe to proceed, false if the user cancelled.
|
|
16
|
-
* Throws in non-interactive mode without --
|
|
16
|
+
* Throws in non-interactive mode without --yes.
|
|
17
17
|
*/
|
|
18
|
-
export declare function confirmDirtyEngineReset({ engineDir,
|
|
18
|
+
export declare function confirmDirtyEngineReset({ engineDir, yes, nonInteractiveHint, warningMessage, promptMessage, cancelMessage, }: DirtyEngineConfirmationOptions): Promise<boolean>;
|
|
@@ -9,15 +9,15 @@ import { cancel, isCancel, warn } from '../../utils/logger.js';
|
|
|
9
9
|
/**
|
|
10
10
|
* Checks if the engine has uncommitted changes and prompts for confirmation.
|
|
11
11
|
* Returns true if safe to proceed, false if the user cancelled.
|
|
12
|
-
* Throws in non-interactive mode without --
|
|
12
|
+
* Throws in non-interactive mode without --yes.
|
|
13
13
|
*/
|
|
14
|
-
export async function confirmDirtyEngineReset({ engineDir,
|
|
15
|
-
if (!(await hasChanges(engineDir)) ||
|
|
14
|
+
export async function confirmDirtyEngineReset({ engineDir, yes, nonInteractiveHint, warningMessage, promptMessage, cancelMessage, }) {
|
|
15
|
+
if (!(await hasChanges(engineDir)) || yes) {
|
|
16
16
|
return true;
|
|
17
17
|
}
|
|
18
18
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
19
19
|
if (!isInteractive) {
|
|
20
|
-
throw new InvalidArgumentError('Engine has uncommitted changes and interactive confirmation is not available. Use --
|
|
20
|
+
throw new InvalidArgumentError('Engine has uncommitted changes and interactive confirmation is not available. Use --yes to proceed.', nonInteractiveHint);
|
|
21
21
|
}
|
|
22
22
|
warn(warningMessage);
|
|
23
23
|
const confirmed = await confirm({
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* Supports `--continue` (resume after manual fix) and `--abort` (cancel).
|
|
12
12
|
*/
|
|
13
13
|
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
14
|
+
import { getFurnacePaths, updateFurnaceState } from '../../core/furnace-config.js';
|
|
14
15
|
import { getHead, isGitRepository, resetChanges } from '../../core/git.js';
|
|
15
16
|
import { discoverPatches } from '../../core/patch-files.js';
|
|
16
17
|
import { loadPatchesManifest } from '../../core/patch-manifest.js';
|
|
@@ -64,8 +65,8 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
64
65
|
}
|
|
65
66
|
if (!(await confirmDirtyEngineReset({
|
|
66
67
|
engineDir: paths.engine,
|
|
67
|
-
|
|
68
|
-
nonInteractiveHint: 'Use: fireforge rebase --
|
|
68
|
+
yes: options.yes ?? false,
|
|
69
|
+
nonInteractiveHint: 'Use: fireforge rebase --yes',
|
|
69
70
|
warningMessage: 'The engine directory has uncommitted changes that will be lost by the rebase.',
|
|
70
71
|
promptMessage: 'Discard uncommitted changes and start rebase?',
|
|
71
72
|
cancelMessage: 'Rebase cancelled',
|
|
@@ -78,6 +79,14 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
78
79
|
const resetSpinner = spinner('Resetting engine to baseline...');
|
|
79
80
|
await resetChanges(paths.engine);
|
|
80
81
|
resetSpinner.stop('Engine reset to baseline');
|
|
82
|
+
// Clear Furnace state — the engine no longer contains deployed components.
|
|
83
|
+
// Preserve pendingRepair since that tracks authoring-side rollback issues.
|
|
84
|
+
const furnacePaths = getFurnacePaths(projectRoot);
|
|
85
|
+
if (await pathExists(furnacePaths.furnaceState)) {
|
|
86
|
+
await updateFurnaceState(projectRoot, (current) => ({
|
|
87
|
+
...(current.pendingRepair ? { pendingRepair: current.pendingRepair } : {}),
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
81
90
|
// Create rebase session
|
|
82
91
|
const allPatches = await discoverPatches(paths.patches);
|
|
83
92
|
const session = {
|
|
@@ -103,7 +112,7 @@ async function handleFreshStart(projectRoot, options) {
|
|
|
103
112
|
*/
|
|
104
113
|
export async function rebaseCommand(projectRoot, options = {}) {
|
|
105
114
|
if (options.abort) {
|
|
106
|
-
return handleAbort(projectRoot, options.
|
|
115
|
+
return handleAbort(projectRoot, options.yes);
|
|
107
116
|
}
|
|
108
117
|
if (options.continue) {
|
|
109
118
|
return handleContinue(projectRoot, options.maxFuzz ?? 3);
|
|
@@ -119,7 +128,7 @@ export function registerRebase(program, { getProjectRoot, withErrorHandling }) {
|
|
|
119
128
|
.option('--abort', 'Cancel the rebase and restore engine to pre-rebase state')
|
|
120
129
|
.option('--dry-run', 'Show what would happen without modifying anything')
|
|
121
130
|
.option('--max-fuzz <n>', 'Maximum fuzz factor for git apply (default: 3)', parseInt)
|
|
122
|
-
.option('-
|
|
131
|
+
.option('-y, --yes', 'Skip dirty-tree confirmation prompt')
|
|
123
132
|
.action(withErrorHandling(async (options) => {
|
|
124
133
|
await rebaseCommand(getProjectRoot(), pickDefined(options));
|
|
125
134
|
}));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { confirm } from '@clack/prompts';
|
|
3
3
|
import { getProjectPaths } from '../core/config.js';
|
|
4
|
+
import { getFurnacePaths, updateFurnaceState } from '../core/furnace-config.js';
|
|
4
5
|
import { hasChanges, isGitRepository, resetChanges } from '../core/git.js';
|
|
5
6
|
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
6
7
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
@@ -41,12 +42,12 @@ export async function resetCommand(projectRoot, options) {
|
|
|
41
42
|
outro('Dry run complete — no changes made');
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
|
-
// Confirm reset unless --
|
|
45
|
-
if (!options.
|
|
45
|
+
// Confirm reset unless --yes is specified
|
|
46
|
+
if (!options.yes) {
|
|
46
47
|
// Check for non-interactive mode
|
|
47
48
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
48
49
|
if (!isInteractive) {
|
|
49
|
-
throw new InvalidArgumentError('Interactive confirmation not available. Use --
|
|
50
|
+
throw new InvalidArgumentError('Interactive confirmation not available. Use --yes flag to reset without confirmation.', 'Use: fireforge reset --yes');
|
|
50
51
|
}
|
|
51
52
|
warn('This will discard all uncommitted changes in the engine directory, including staged additions and untracked files.');
|
|
52
53
|
const confirmed = await confirm({
|
|
@@ -61,6 +62,21 @@ export async function resetCommand(projectRoot, options) {
|
|
|
61
62
|
const s = spinner('Resetting changes...');
|
|
62
63
|
try {
|
|
63
64
|
await resetChanges(paths.engine);
|
|
65
|
+
// Clearing furnace-state.json is the honest representation of what just
|
|
66
|
+
// happened: any previously deployed Furnace files have been discarded
|
|
67
|
+
// with the engine reset. Without this, a subsequent `furnace apply`
|
|
68
|
+
// would match on workspace checksums and report "up to date" against
|
|
69
|
+
// an engine that no longer contains the deployed copies. (The drift
|
|
70
|
+
// check in apply also catches this, but clearing here keeps state
|
|
71
|
+
// consistent regardless of the drift oracle.) Preserve pendingRepair:
|
|
72
|
+
// authoring-side rollback markers are about the workspace/component
|
|
73
|
+
// tree, not the engine checkout, so reset must not silently forget them.
|
|
74
|
+
const furnacePaths = getFurnacePaths(projectRoot);
|
|
75
|
+
if (await pathExists(furnacePaths.furnaceState)) {
|
|
76
|
+
await updateFurnaceState(projectRoot, (current) => ({
|
|
77
|
+
...(current.pendingRepair ? { pendingRepair: current.pendingRepair } : {}),
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
64
80
|
s.stop('Changes reset');
|
|
65
81
|
outro('Working tree restored to clean state');
|
|
66
82
|
}
|
|
@@ -74,7 +90,7 @@ export function registerReset(program, { getProjectRoot, withErrorHandling }) {
|
|
|
74
90
|
program
|
|
75
91
|
.command('reset')
|
|
76
92
|
.description('Reset engine/ to clean state')
|
|
77
|
-
.option('-
|
|
93
|
+
.option('-y, --yes', 'Skip confirmation prompt (required for scripts/CI)')
|
|
78
94
|
.option('--dry-run', 'Show what would be reset without doing it')
|
|
79
95
|
.action(withErrorHandling(async (options) => {
|
|
80
96
|
await resetCommand(getProjectRoot(), pickDefined(options));
|
package/dist/src/commands/run.js
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import { readdir } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { getProjectPaths } from '../core/config.js';
|
|
5
|
+
import { extractComponentChecksums, hasComponentChanged } from '../core/furnace-apply-helpers.js';
|
|
6
|
+
import { furnaceConfigExists, getFurnacePaths, loadFurnaceConfig, loadFurnaceState, } from '../core/furnace-config.js';
|
|
5
7
|
import { buildArtifactMismatchMessage, hasBuildArtifacts, run } from '../core/mach.js';
|
|
6
8
|
import { GeneralError } from '../errors/base.js';
|
|
7
9
|
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
8
10
|
import { toError } from '../utils/errors.js';
|
|
9
11
|
import { pathExists, removeDir, removeFile } from '../utils/fs.js';
|
|
10
|
-
import { info, intro, verbose } from '../utils/logger.js';
|
|
12
|
+
import { info, intro, verbose, warn } from '../utils/logger.js';
|
|
11
13
|
/**
|
|
12
14
|
* Cleans the dev profile to prevent stale-state startup failures.
|
|
13
15
|
*
|
|
@@ -45,6 +47,47 @@ async function cleanDevProfile(engineDir) {
|
|
|
45
47
|
verbose(`Non-fatal dev profile cleanup failure: ${toError(error).message}`);
|
|
46
48
|
}
|
|
47
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Checks whether any Furnace component has changed since the last apply
|
|
52
|
+
* and warns the user. The build command auto-applies, but run does not,
|
|
53
|
+
* so this advisory message prevents the common "forgot to apply" mistake.
|
|
54
|
+
*/
|
|
55
|
+
async function warnIfFurnaceStale(projectRoot) {
|
|
56
|
+
try {
|
|
57
|
+
if (!(await furnaceConfigExists(projectRoot)))
|
|
58
|
+
return;
|
|
59
|
+
const config = await loadFurnaceConfig(projectRoot);
|
|
60
|
+
const state = await loadFurnaceState(projectRoot);
|
|
61
|
+
const furnacePaths = getFurnacePaths(projectRoot);
|
|
62
|
+
if (!state.appliedChecksums)
|
|
63
|
+
return;
|
|
64
|
+
const stale = [];
|
|
65
|
+
for (const name of Object.keys(config.overrides)) {
|
|
66
|
+
const dir = `${furnacePaths.overridesDir}/${name}`;
|
|
67
|
+
if (!(await pathExists(dir)))
|
|
68
|
+
continue;
|
|
69
|
+
const prev = extractComponentChecksums(state.appliedChecksums, 'override', name);
|
|
70
|
+
if (await hasComponentChanged(dir, prev))
|
|
71
|
+
stale.push(name);
|
|
72
|
+
}
|
|
73
|
+
for (const name of Object.keys(config.custom)) {
|
|
74
|
+
const dir = `${furnacePaths.customDir}/${name}`;
|
|
75
|
+
if (!(await pathExists(dir)))
|
|
76
|
+
continue;
|
|
77
|
+
const prev = extractComponentChecksums(state.appliedChecksums, 'custom', name);
|
|
78
|
+
if (await hasComponentChanged(dir, prev))
|
|
79
|
+
stale.push(name);
|
|
80
|
+
}
|
|
81
|
+
if (stale.length > 0) {
|
|
82
|
+
warn(`Furnace component${stale.length === 1 ? '' : 's'} modified since last apply: ${stale.join(', ')}. ` +
|
|
83
|
+
'Run "fireforge furnace apply" (or "fireforge build" which auto-applies) to update the engine.');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// Non-fatal: a broken furnace config should not block run.
|
|
88
|
+
verbose('Furnace staleness check skipped due to an error.');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
48
91
|
/**
|
|
49
92
|
* Runs the run command to launch the built browser.
|
|
50
93
|
* @param projectRoot - Root directory of the project
|
|
@@ -71,6 +114,8 @@ export async function runCommand(projectRoot) {
|
|
|
71
114
|
throw new GeneralError(`Run requires a completed build. ${detail}\n\n` +
|
|
72
115
|
"Run 'fireforge build' first, then rerun 'fireforge run'.");
|
|
73
116
|
}
|
|
117
|
+
// Warn if Furnace components changed since the last apply
|
|
118
|
+
await warnIfFurnaceStale(projectRoot);
|
|
74
119
|
// Clean stale profile state to prevent silent startup failures
|
|
75
120
|
await cleanDevProfile(paths.engine);
|
|
76
121
|
info('Launching browser...\n');
|
|
@@ -63,7 +63,7 @@ export function validateSetupOptions(options) {
|
|
|
63
63
|
throw new InvalidArgumentError('Binary name must start with a letter and contain only lowercase letters, numbers, and hyphens', '--binary-name');
|
|
64
64
|
}
|
|
65
65
|
if (options.firefoxVersion !== undefined && !isValidFirefoxVersion(options.firefoxVersion)) {
|
|
66
|
-
throw new InvalidArgumentError('Invalid Firefox version format (e.g., 146.0,
|
|
66
|
+
throw new InvalidArgumentError('Invalid Firefox version format (e.g., 146.0, 146.0esr, or 147.0b1)', '--firefox-version');
|
|
67
67
|
}
|
|
68
68
|
if (options.product !== undefined) {
|
|
69
69
|
resolveFirefoxProduct(options.product, '--product');
|
|
@@ -127,10 +127,10 @@ async function promptSetupInputs(options) {
|
|
|
127
127
|
? Promise.resolve(options.firefoxVersion)
|
|
128
128
|
: text({
|
|
129
129
|
message: 'Firefox version to base on',
|
|
130
|
-
placeholder: '
|
|
130
|
+
placeholder: '146.0esr',
|
|
131
131
|
validate: (value) => {
|
|
132
132
|
if (value && !isValidFirefoxVersion(value)) {
|
|
133
|
-
return 'Invalid Firefox version format (e.g., 146.0,
|
|
133
|
+
return 'Invalid Firefox version format (e.g., 146.0, 146.0esr, or 147.0b1)';
|
|
134
134
|
}
|
|
135
135
|
return undefined;
|
|
136
136
|
},
|
|
@@ -141,7 +141,7 @@ async function promptSetupInputs(options) {
|
|
|
141
141
|
}
|
|
142
142
|
const effectiveVersion = (typeof results.firefoxVersion === 'string' && results.firefoxVersion.trim()) ||
|
|
143
143
|
options.firefoxVersion ||
|
|
144
|
-
'
|
|
144
|
+
'146.0esr';
|
|
145
145
|
const inferredProduct = inferProductFromVersion(effectiveVersion);
|
|
146
146
|
if (inferredProduct) {
|
|
147
147
|
return Promise.resolve(inferredProduct);
|
|
@@ -183,7 +183,7 @@ async function promptSetupInputs(options) {
|
|
|
183
183
|
const finalAppId = (typeof project.appId === 'string' ? project.appId.trim() : '') ||
|
|
184
184
|
`org.${sanitizedName}.browser`;
|
|
185
185
|
const finalBinaryName = (typeof project.binaryName === 'string' ? project.binaryName.trim() : '') || sanitizedName;
|
|
186
|
-
const finalFirefoxVersion = (typeof project.firefoxVersion === 'string' ? project.firefoxVersion.trim() : '') || '
|
|
186
|
+
const finalFirefoxVersion = (typeof project.firefoxVersion === 'string' ? project.firefoxVersion.trim() : '') || '146.0esr';
|
|
187
187
|
if (!isValidAppId(finalAppId)) {
|
|
188
188
|
throw new InvalidArgumentError(`Derived appId "${finalAppId}" is invalid.`, 'appId');
|
|
189
189
|
}
|