@hominis/fireforge 0.37.0 → 0.39.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 +33 -0
- package/README.md +1 -1
- package/dist/src/commands/build.js +13 -8
- package/dist/src/commands/config.js +1 -0
- package/dist/src/commands/discard.js +75 -50
- package/dist/src/commands/download.js +5 -2
- package/dist/src/commands/export-flow.d.ts +6 -2
- package/dist/src/commands/export-flow.js +17 -11
- package/dist/src/commands/export-placement-gate.js +4 -6
- package/dist/src/commands/export-placement-policy.d.ts +11 -3
- package/dist/src/commands/export-placement-policy.js +32 -29
- package/dist/src/commands/export-shared.js +17 -2
- package/dist/src/commands/export.js +5 -5
- package/dist/src/commands/furnace/index.js +23 -18
- package/dist/src/commands/import.js +5 -0
- package/dist/src/commands/lint-per-patch.js +15 -5
- package/dist/src/commands/lint.js +1 -1
- package/dist/src/commands/patch/lint-ignore.js +20 -1
- package/dist/src/commands/patch/move-files-create.d.ts +19 -0
- package/dist/src/commands/patch/move-files-create.js +116 -0
- package/dist/src/commands/patch/move-files-into.d.ts +47 -0
- package/dist/src/commands/patch/move-files-into.js +245 -0
- package/dist/src/commands/patch/move-files.d.ts +3 -1
- package/dist/src/commands/patch/move-files.js +74 -15
- package/dist/src/commands/patch/rename.js +4 -1
- package/dist/src/commands/patch/split.d.ts +11 -1
- package/dist/src/commands/patch/split.js +6 -5
- package/dist/src/commands/re-export-scan.d.ts +17 -0
- package/dist/src/commands/re-export-scan.js +51 -0
- package/dist/src/commands/re-export.js +17 -6
- package/dist/src/commands/reset.js +5 -0
- package/dist/src/commands/source.js +28 -3
- package/dist/src/commands/status-output.d.ts +9 -0
- package/dist/src/commands/status-output.js +33 -1
- package/dist/src/commands/status.js +17 -3
- package/dist/src/commands/test-diagnose.d.ts +1 -1
- package/dist/src/commands/test-diagnose.js +9 -3
- package/dist/src/commands/test-modes.d.ts +21 -0
- package/dist/src/commands/test-modes.js +42 -0
- package/dist/src/commands/test-register.js +6 -5
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +8 -2
- package/dist/src/commands/test-stale-gate.d.ts +44 -0
- package/dist/src/commands/test-stale-gate.js +81 -0
- package/dist/src/commands/test.js +52 -81
- package/dist/src/core/build-audit.d.ts +12 -0
- package/dist/src/core/build-audit.js +14 -0
- package/dist/src/core/build-baseline-types.d.ts +41 -0
- package/dist/src/core/build-baseline.d.ts +6 -1
- package/dist/src/core/build-baseline.js +48 -7
- package/dist/src/core/build-prepare.d.ts +9 -0
- package/dist/src/core/build-prepare.js +24 -0
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +1 -0
- package/dist/src/core/config-validate.js +14 -1
- package/dist/src/core/discard-baseline.d.ts +60 -0
- package/dist/src/core/discard-baseline.js +232 -0
- package/dist/src/core/engine-session-lock.d.ts +10 -1
- package/dist/src/core/engine-session-lock.js +28 -2
- package/dist/src/core/file-lock.d.ts +33 -0
- package/dist/src/core/file-lock.js +32 -6
- package/dist/src/core/firefox-archive.d.ts +2 -1
- package/dist/src/core/firefox-archive.js +32 -8
- package/dist/src/core/firefox.d.ts +8 -3
- package/dist/src/core/firefox.js +11 -6
- package/dist/src/core/furnace-validate-helpers.js +28 -1
- package/dist/src/core/license-headers.d.ts +24 -0
- package/dist/src/core/license-headers.js +103 -1
- package/dist/src/core/patch-export.d.ts +10 -1
- package/dist/src/core/patch-export.js +12 -2
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -1
- package/dist/src/core/patch-lint-checkjs.js +30 -14
- package/dist/src/core/patch-lint-cross.d.ts +9 -1
- package/dist/src/core/patch-lint-cross.js +69 -9
- package/dist/src/core/patch-lint-mozbuild.d.ts +18 -0
- package/dist/src/core/patch-lint-mozbuild.js +104 -0
- package/dist/src/core/patch-lint.d.ts +3 -0
- package/dist/src/core/patch-lint.js +29 -8
- package/dist/src/core/patch-policy.d.ts +14 -1
- package/dist/src/core/patch-policy.js +13 -2
- package/dist/src/core/status-classify.d.ts +11 -2
- package/dist/src/core/status-classify.js +49 -25
- package/dist/src/core/test-harness-crash.d.ts +10 -0
- package/dist/src/core/test-harness-crash.js +16 -0
- package/dist/src/core/test-stale-check.d.ts +56 -1
- package/dist/src/core/test-stale-check.js +114 -4
- package/dist/src/core/token-manager.js +39 -2
- package/dist/src/core/token-variant.d.ts +7 -0
- package/dist/src/core/token-variant.js +9 -0
- package/dist/src/core/typecheck-shim.d.ts +13 -0
- package/dist/src/core/typecheck-shim.js +17 -2
- package/dist/src/core/typecheck.js +14 -3
- package/dist/src/types/commands/options.d.ts +69 -3
- package/dist/src/types/config.d.ts +22 -0
- package/dist/src/utils/options.d.ts +20 -0
- package/dist/src/utils/options.js +39 -0
- package/dist/src/utils/validation.d.ts +5 -0
- package/dist/src/utils/validation.js +7 -0
- package/package.json +3 -3
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Transactional file-ownership move into an EXISTING patch (FORGE F4).
|
|
4
|
+
*
|
|
5
|
+
* Before 0.39.0, `patch move-files <from> <to>` with an existing target was
|
|
6
|
+
* preview-only: it printed two `re-export --files` commands whose real
|
|
7
|
+
* execution cost three further refusals (`--allow-shrink`, `--yes`, and a
|
|
8
|
+
* projected `duplicate-new-file-creation` unless the SHRINK landed first —
|
|
9
|
+
* while the printed plan listed the grow first). This module performs the
|
|
10
|
+
* same shrink→grow as one transaction under the patch-directory lock with
|
|
11
|
+
* rollback, mirroring `patch split` / `move-files --create`.
|
|
12
|
+
*
|
|
13
|
+
* Preconditions match `re-export`: the engine worktree must currently
|
|
14
|
+
* reflect both patches' content, because both bodies are regenerated from
|
|
15
|
+
* the worktree.
|
|
16
|
+
*/
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { appendHistory, confirmDestructive } from '../../core/destructive.js';
|
|
19
|
+
import { computeProjectedLintRegressions } from '../../core/lint-projection.js';
|
|
20
|
+
import { normalizePatchArtifact } from '../../core/patch-artifact-normalize.js';
|
|
21
|
+
import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, collectForwardImportEdges, detectNewFilesInDiff, lintPatchQueue, } from '../../core/patch-lint.js';
|
|
22
|
+
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
23
|
+
import { loadPatchesManifest, savePatchesManifest, validatePatchesManifest, } from '../../core/patch-manifest.js';
|
|
24
|
+
import { extractNewFileContentFromDiff } from '../../core/patch-transform.js';
|
|
25
|
+
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
26
|
+
import { toError } from '../../utils/errors.js';
|
|
27
|
+
import { readText, writeText } from '../../utils/fs.js';
|
|
28
|
+
import { info, outro, success, warn } from '../../utils/logger.js';
|
|
29
|
+
import { runPatchLint } from '../export-shared.js';
|
|
30
|
+
import { buildSplitDiff, findOwnerRewriteHolders, mergeStagedForwardImports, rewriteSplitOwners, } from './split-plan.js';
|
|
31
|
+
function projectEntryBody(diff) {
|
|
32
|
+
const newFiles = new Map();
|
|
33
|
+
for (const path of detectNewFilesInDiff(diff)) {
|
|
34
|
+
newFiles.set(path, extractNewFileContentFromDiff(diff, path));
|
|
35
|
+
}
|
|
36
|
+
return { diff, newFiles, modifiedFileAdditions: buildModifiedFileAdditionsFromDiff(diff) };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Projects the post-move queue through cross-patch lint, auto-declaring the
|
|
40
|
+
* forward edges the move introduces into the target (an importer that used
|
|
41
|
+
* to live in the same patch as the moved file gains a forward edge when the
|
|
42
|
+
* target is later-ordered). Reports only regressions the move itself adds.
|
|
43
|
+
*/
|
|
44
|
+
async function runProjectedMoveLint(patchesDir, plan) {
|
|
45
|
+
const movedSet = new Set(plan.movedFiles);
|
|
46
|
+
const baseCtx = await buildPatchQueueContext(patchesDir);
|
|
47
|
+
const entries = baseCtx.entries.map((entry) => {
|
|
48
|
+
let metadata = entry.metadata;
|
|
49
|
+
if (metadata) {
|
|
50
|
+
metadata = rewriteSplitOwners(metadata, plan.source.filename, movedSet, plan.target.filename);
|
|
51
|
+
}
|
|
52
|
+
if (entry.filename === plan.source.filename) {
|
|
53
|
+
return { ...entry, metadata, ...projectEntryBody(plan.sourceDiff) };
|
|
54
|
+
}
|
|
55
|
+
if (entry.filename === plan.target.filename) {
|
|
56
|
+
return { ...entry, metadata, ...projectEntryBody(plan.targetDiff) };
|
|
57
|
+
}
|
|
58
|
+
return { ...entry, metadata };
|
|
59
|
+
});
|
|
60
|
+
const stagedDependencyAdditions = new Map();
|
|
61
|
+
for (const edge of collectForwardImportEdges({ entries })) {
|
|
62
|
+
if (edge.owner !== plan.target.filename || !movedSet.has(edge.creates))
|
|
63
|
+
continue;
|
|
64
|
+
const decl = {
|
|
65
|
+
file: edge.sitePath,
|
|
66
|
+
specifier: edge.specifier,
|
|
67
|
+
creates: edge.creates,
|
|
68
|
+
owner: plan.target.filename,
|
|
69
|
+
};
|
|
70
|
+
const list = stagedDependencyAdditions.get(edge.entry) ?? [];
|
|
71
|
+
if (!list.some((d) => d.file === decl.file && d.specifier === decl.specifier && d.creates === decl.creates)) {
|
|
72
|
+
list.push(decl);
|
|
73
|
+
}
|
|
74
|
+
stagedDependencyAdditions.set(edge.entry, list);
|
|
75
|
+
}
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
const decls = stagedDependencyAdditions.get(entry.filename);
|
|
78
|
+
if (decls?.length && entry.metadata) {
|
|
79
|
+
entry.metadata = mergeStagedForwardImports(entry.metadata, decls);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const baselineIssues = lintPatchQueue(baseCtx).filter((i) => i.severity === 'error');
|
|
83
|
+
const projectedIssues = lintPatchQueue({
|
|
84
|
+
entries,
|
|
85
|
+
...(baseCtx.patchPolicy ? { patchPolicy: baseCtx.patchPolicy } : {}),
|
|
86
|
+
}).filter((i) => i.severity === 'error');
|
|
87
|
+
const regressions = computeProjectedLintRegressions(baselineIssues, projectedIssues);
|
|
88
|
+
if (baselineIssues.length > 0 && regressions.length === 0) {
|
|
89
|
+
warn(`Note: projected queue still has ${baselineIssues.length} pre-existing cross-patch ` +
|
|
90
|
+
'error(s) unrelated to this move. Run "fireforge verify" to list them.');
|
|
91
|
+
}
|
|
92
|
+
const conflicts = regressions.length === 0
|
|
93
|
+
? null
|
|
94
|
+
: {
|
|
95
|
+
reason: `move would introduce ${regressions.length} cross-patch lint error(s)`,
|
|
96
|
+
details: regressions.map((i) => `[${i.check}] ${i.file}: ${i.message}`),
|
|
97
|
+
};
|
|
98
|
+
return { conflicts, stagedDependencyAdditions };
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Commits a confirmed move under the patch-directory lock: write both
|
|
102
|
+
* rewritten bodies, then one manifest rewrite (filesAffected on both rows +
|
|
103
|
+
* owner rewrites + auto-declared forward imports). Rolled back in reverse
|
|
104
|
+
* order on any failure.
|
|
105
|
+
*/
|
|
106
|
+
async function commitMoveInto(patchesDir, plan) {
|
|
107
|
+
await withPatchDirectoryLock(patchesDir, async () => {
|
|
108
|
+
const manifest = await loadPatchesManifest(patchesDir);
|
|
109
|
+
if (!manifest)
|
|
110
|
+
throw new GeneralError('Manifest disappeared while waiting for lock.');
|
|
111
|
+
const currentSource = manifest.patches.find((p) => p.filename === plan.source.filename);
|
|
112
|
+
const currentTarget = manifest.patches.find((p) => p.filename === plan.target.filename);
|
|
113
|
+
if (!currentSource ||
|
|
114
|
+
!currentTarget ||
|
|
115
|
+
currentSource.filesAffected.join('\n') !== plan.source.filesAffected.join('\n') ||
|
|
116
|
+
currentTarget.filesAffected.join('\n') !== plan.target.filesAffected.join('\n')) {
|
|
117
|
+
throw new InvalidArgumentError('Patch queue changed while waiting for move confirmation. Re-run the command.', 'patch move-files');
|
|
118
|
+
}
|
|
119
|
+
const movedSet = new Set(plan.movedFiles);
|
|
120
|
+
const sourcePath = join(patchesDir, plan.source.filename);
|
|
121
|
+
const targetPath = join(patchesDir, plan.target.filename);
|
|
122
|
+
const originalSourceBody = await readText(sourcePath);
|
|
123
|
+
const originalTargetBody = await readText(targetPath);
|
|
124
|
+
let sourceWritten = false;
|
|
125
|
+
let targetWritten = false;
|
|
126
|
+
try {
|
|
127
|
+
await writeText(sourcePath, normalizePatchArtifact(plan.sourceDiff));
|
|
128
|
+
sourceWritten = true;
|
|
129
|
+
await writeText(targetPath, normalizePatchArtifact(plan.targetDiff));
|
|
130
|
+
targetWritten = true;
|
|
131
|
+
const updatedPatches = manifest.patches.map((patch) => {
|
|
132
|
+
let updated = rewriteSplitOwners(patch, plan.source.filename, movedSet, plan.target.filename);
|
|
133
|
+
const decls = plan.stagedDependencyAdditions.get(updated.filename);
|
|
134
|
+
if (decls?.length)
|
|
135
|
+
updated = mergeStagedForwardImports(updated, decls);
|
|
136
|
+
if (updated.filename === plan.source.filename) {
|
|
137
|
+
return { ...updated, filesAffected: plan.sourceAfter };
|
|
138
|
+
}
|
|
139
|
+
if (updated.filename === plan.target.filename) {
|
|
140
|
+
return { ...updated, filesAffected: plan.targetAfter };
|
|
141
|
+
}
|
|
142
|
+
return updated;
|
|
143
|
+
});
|
|
144
|
+
const updated = validatePatchesManifest({ ...manifest, patches: updatedPatches });
|
|
145
|
+
await savePatchesManifest(patchesDir, updated);
|
|
146
|
+
try {
|
|
147
|
+
await appendHistory(patchesDir, {
|
|
148
|
+
operation: 'patch-move-files',
|
|
149
|
+
args: {
|
|
150
|
+
source: plan.source.filename,
|
|
151
|
+
target: plan.target.filename,
|
|
152
|
+
files: plan.movedFiles,
|
|
153
|
+
ownerRewrites: plan.ownerRewrites,
|
|
154
|
+
},
|
|
155
|
+
result: 'ok',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (historyError) {
|
|
159
|
+
warn(`History log append failed after patch move-files committed: ${toError(historyError).message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (targetWritten) {
|
|
164
|
+
try {
|
|
165
|
+
await writeText(targetPath, originalTargetBody);
|
|
166
|
+
}
|
|
167
|
+
catch (rollbackError) {
|
|
168
|
+
warn(`Rollback warning: could not restore target body: ${toError(rollbackError).message}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (sourceWritten) {
|
|
172
|
+
try {
|
|
173
|
+
await writeText(sourcePath, originalSourceBody);
|
|
174
|
+
}
|
|
175
|
+
catch (rollbackError) {
|
|
176
|
+
warn(`Rollback warning: could not restore source body: ${toError(rollbackError).message}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
await savePatchesManifest(patchesDir, manifest);
|
|
181
|
+
}
|
|
182
|
+
catch (rollbackError) {
|
|
183
|
+
warn(`Rollback warning: could not restore manifest: ${toError(rollbackError).message}`);
|
|
184
|
+
}
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Plans, lints, confirms, and commits a file-ownership move into an
|
|
191
|
+
* existing patch. Called by `patchMoveFilesCommand` after its ownership
|
|
192
|
+
* pre-checks resolved and validated both patches.
|
|
193
|
+
*/
|
|
194
|
+
export async function runMoveFilesInto(args) {
|
|
195
|
+
const { enginePath, patchesDir, source, target, files, sourceAfter, targetAfter, options } = args;
|
|
196
|
+
const sourceDiff = await buildSplitDiff(enginePath, sourceAfter, 'remaining', source.filename);
|
|
197
|
+
const targetDiff = await buildSplitDiff(enginePath, targetAfter, 'moved', target.filename);
|
|
198
|
+
// Per-patch lint both projected bodies with each patch's own waivers.
|
|
199
|
+
await runPatchLint(enginePath, sourceAfter, sourceDiff, args.config, options.skipLint, undefined, source.lintIgnore ? new Set(source.lintIgnore) : undefined, source.tier);
|
|
200
|
+
await runPatchLint(enginePath, targetAfter, targetDiff, args.config, options.skipLint, undefined, target.lintIgnore ? new Set(target.lintIgnore) : undefined, target.tier);
|
|
201
|
+
const manifest = await loadPatchesManifest(patchesDir);
|
|
202
|
+
const plan = {
|
|
203
|
+
source,
|
|
204
|
+
target,
|
|
205
|
+
movedFiles: files,
|
|
206
|
+
sourceAfter,
|
|
207
|
+
targetAfter,
|
|
208
|
+
sourceDiff,
|
|
209
|
+
targetDiff,
|
|
210
|
+
ownerRewrites: findOwnerRewriteHolders(manifest?.patches ?? [], source.filename, new Set(files)),
|
|
211
|
+
stagedDependencyAdditions: new Map(),
|
|
212
|
+
};
|
|
213
|
+
const { conflicts, stagedDependencyAdditions } = await runProjectedMoveLint(patchesDir, plan);
|
|
214
|
+
plan.stagedDependencyAdditions = stagedDependencyAdditions;
|
|
215
|
+
const decision = await confirmDestructive({
|
|
216
|
+
operation: 'patch-move-files',
|
|
217
|
+
title: `Move ${files.length} file(s) from ${source.filename} to ${target.filename}`,
|
|
218
|
+
summary: [
|
|
219
|
+
`moved files (${files.length}): ${files.join(', ')}`,
|
|
220
|
+
`${source.filename} keeps (${sourceAfter.length}): ${sourceAfter.join(', ')}`,
|
|
221
|
+
`${target.filename} grows to (${targetAfter.length}): ${targetAfter.join(', ')}`,
|
|
222
|
+
...(plan.ownerRewrites.length > 0
|
|
223
|
+
? [`staged-dependency owners re-pointed in: ${plan.ownerRewrites.join(', ')}`]
|
|
224
|
+
: []),
|
|
225
|
+
],
|
|
226
|
+
yes: options.yes === true,
|
|
227
|
+
dryRun: options.dryRun === true,
|
|
228
|
+
unsafeOverride: options.forceUnsafe === true,
|
|
229
|
+
conflicts,
|
|
230
|
+
});
|
|
231
|
+
if (decision === 'dry-run') {
|
|
232
|
+
outro('Dry run complete — no changes made');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (decision === 'cancelled') {
|
|
236
|
+
outro('Move cancelled');
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
await commitMoveInto(patchesDir, plan);
|
|
240
|
+
success(`Moved ${files.length} file(s) from ${source.filename} to ${target.filename}`);
|
|
241
|
+
if (plan.ownerRewrites.length > 0) {
|
|
242
|
+
info(`Re-pointed staged-dependency owners in: ${plan.ownerRewrites.join(', ')}`);
|
|
243
|
+
}
|
|
244
|
+
outro('Move complete');
|
|
245
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `fireforge patch move-files <from> <to>` previews the explicit
|
|
3
|
-
* re-export choreography needed to move file ownership between two
|
|
3
|
+
* re-export choreography needed to move file ownership between two
|
|
4
|
+
* patches. With `--create --order <n>` the target patch is created and
|
|
5
|
+
* the files move into it as one transaction (see move-files-create.ts).
|
|
4
6
|
*/
|
|
5
7
|
import { Command } from 'commander';
|
|
6
8
|
import type { CommandContext } from '../../types/cli.js';
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
3
|
* `fireforge patch move-files <from> <to>` previews the explicit
|
|
4
|
-
* re-export choreography needed to move file ownership between two
|
|
4
|
+
* re-export choreography needed to move file ownership between two
|
|
5
|
+
* patches. With `--create --order <n>` the target patch is created and
|
|
6
|
+
* the files move into it as one transaction (see move-files-create.ts).
|
|
5
7
|
*/
|
|
6
8
|
import { relative } from 'node:path';
|
|
7
9
|
import { Command } from 'commander';
|
|
8
|
-
import { getProjectPaths } from '../../core/config.js';
|
|
10
|
+
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
9
11
|
import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
|
|
10
12
|
import { loadPatchesManifest, resolvePatchIdentifier } from '../../core/patch-manifest.js';
|
|
11
13
|
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
12
14
|
import { pathExists } from '../../utils/fs.js';
|
|
13
|
-
import { info, intro, note,
|
|
15
|
+
import { info, intro, note, warn } from '../../utils/logger.js';
|
|
16
|
+
import { commanderArgParser, pickDefined } from '../../utils/options.js';
|
|
17
|
+
import { patchMoveFilesCreateCommand } from './move-files-create.js';
|
|
18
|
+
import { runMoveFilesInto } from './move-files-into.js';
|
|
14
19
|
function collectOption(value, previous) {
|
|
15
20
|
previous.push(value);
|
|
16
21
|
return previous;
|
|
@@ -67,6 +72,19 @@ function computeFileMovePlan(source, target, files) {
|
|
|
67
72
|
* @param options - Files to move and display mode
|
|
68
73
|
*/
|
|
69
74
|
export async function patchMoveFilesCommand(projectRoot, fromIdentifier, toIdentifier, options = {}) {
|
|
75
|
+
if (options.create === true && options.order === undefined) {
|
|
76
|
+
throw new InvalidArgumentError('--create requires --order <n> to place the new patch.', '--create');
|
|
77
|
+
}
|
|
78
|
+
if (options.create !== true && options.order !== undefined) {
|
|
79
|
+
throw new InvalidArgumentError('--order is only valid together with --create.', '--order');
|
|
80
|
+
}
|
|
81
|
+
if (options.create === true && options.order !== undefined) {
|
|
82
|
+
await patchMoveFilesCreateCommand(projectRoot, fromIdentifier, toIdentifier, {
|
|
83
|
+
...options,
|
|
84
|
+
order: options.order,
|
|
85
|
+
});
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
70
88
|
intro('FireForge patch move-files');
|
|
71
89
|
if (fromIdentifier === toIdentifier) {
|
|
72
90
|
throw new InvalidArgumentError('Source and target patch identifiers must be different.', 'patch move-files');
|
|
@@ -106,20 +124,34 @@ export async function patchMoveFilesCommand(projectRoot, fromIdentifier, toIdent
|
|
|
106
124
|
}
|
|
107
125
|
}
|
|
108
126
|
const relativePatchesDir = relative(projectRoot, paths.patches) || 'patches';
|
|
109
|
-
info(`
|
|
127
|
+
info(`Moving ownership in ${relativePatchesDir}/patches.json.`);
|
|
110
128
|
info(`Move ${files.length} file(s) from ${source.filename} to ${target.filename}:`);
|
|
111
129
|
note(formatFiles(files), 'Files');
|
|
112
130
|
note(formatFiles(sourceAfter), `${source.filename} files after move`);
|
|
113
131
|
note(formatFiles(targetAfter), `${target.filename} files after move`);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
'
|
|
122
|
-
|
|
132
|
+
// Manual fallback (e.g. when the worktree cannot carry both patches'
|
|
133
|
+
// content right now): the equivalent re-export choreography, in
|
|
134
|
+
// EXECUTABLE order — the shrink must land before the grow, or the
|
|
135
|
+
// projected cross-patch lint refuses the pair with
|
|
136
|
+
// duplicate-new-file-creation (FORGE F4).
|
|
137
|
+
const applySource = formatReExportCommand(source.filename, sourceAfter, [
|
|
138
|
+
'--allow-shrink',
|
|
139
|
+
'--yes',
|
|
140
|
+
]);
|
|
141
|
+
const applyTarget = formatReExportCommand(target.filename, targetAfter, ['--yes']);
|
|
142
|
+
note(`1. ${applySource}\n2. ${applyTarget}\n(the shrink must land first)`, 'Equivalent manual commands');
|
|
143
|
+
const config = await loadConfig(projectRoot);
|
|
144
|
+
await runMoveFilesInto({
|
|
145
|
+
enginePath: paths.engine,
|
|
146
|
+
patchesDir: paths.patches,
|
|
147
|
+
source,
|
|
148
|
+
target,
|
|
149
|
+
files,
|
|
150
|
+
sourceAfter,
|
|
151
|
+
targetAfter,
|
|
152
|
+
options,
|
|
153
|
+
config,
|
|
154
|
+
});
|
|
123
155
|
}
|
|
124
156
|
/**
|
|
125
157
|
* Registers the preview-only `patch move-files` subcommand.
|
|
@@ -131,9 +163,36 @@ export function registerPatchMoveFiles(parent, context) {
|
|
|
131
163
|
const { getProjectRoot, withErrorHandling } = context;
|
|
132
164
|
parent
|
|
133
165
|
.command('move-files <from> <to>')
|
|
134
|
-
.description('Preview the re-export commands needed to move file ownership between patches
|
|
166
|
+
.description('Preview the re-export commands needed to move file ownership between patches, ' +
|
|
167
|
+
'or create the target patch and move the files in one transaction with --create --order <n>.')
|
|
135
168
|
.option('--file <path>', 'File path relative to engine/ to move (repeatable)', collectOption, [])
|
|
169
|
+
.option('--create', 'Create <to> as a new patch and move the files into it (requires --order)')
|
|
170
|
+
.option('--order <n>', 'Exact sparse order for the created patch (only valid with --create)', commanderArgParser((raw) => {
|
|
171
|
+
const n = Number.parseInt(raw, 10);
|
|
172
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
173
|
+
throw new InvalidArgumentError(`--order must be a positive integer, got "${raw}".`, '--order');
|
|
174
|
+
}
|
|
175
|
+
return n;
|
|
176
|
+
}))
|
|
177
|
+
.option('--category <category>', "Category for the created patch (default: the source patch's)")
|
|
178
|
+
.option('--description <desc>', 'Description for the created patch')
|
|
179
|
+
.option('--dry-run', 'Show what would happen without writing')
|
|
180
|
+
.option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
|
|
181
|
+
.option('--force-unsafe', 'Bypass projected-lint refusals')
|
|
182
|
+
.option('--skip-lint', 'Skip per-patch lint of the projected bodies')
|
|
136
183
|
.action(withErrorHandling(async (from, to, options) => {
|
|
137
|
-
await patchMoveFilesCommand(getProjectRoot(), from, to,
|
|
184
|
+
await patchMoveFilesCommand(getProjectRoot(), from, to, {
|
|
185
|
+
...pickDefined({
|
|
186
|
+
file: options.file,
|
|
187
|
+
create: options.create,
|
|
188
|
+
order: options.order,
|
|
189
|
+
category: options.category,
|
|
190
|
+
description: options.description,
|
|
191
|
+
dryRun: options.dryRun,
|
|
192
|
+
yes: options.yes,
|
|
193
|
+
forceUnsafe: options.forceUnsafe,
|
|
194
|
+
skipLint: options.skipLint,
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
138
197
|
}));
|
|
139
198
|
}
|
|
@@ -173,7 +173,10 @@ export async function patchRenameCommand(projectRoot, identifier, options = {})
|
|
|
173
173
|
const nameChanging = options.to !== target.name;
|
|
174
174
|
const descriptionChanging = options.description !== undefined && options.description !== target.description;
|
|
175
175
|
if (!filenameChanging && !nameChanging && !descriptionChanging) {
|
|
176
|
-
info(`${target.filename}:
|
|
176
|
+
info(`${target.filename}: nothing to change — filename already "${target.filename}" ` +
|
|
177
|
+
`(slug of "${options.to}" is "${newSlug}"), name already "${target.name}"` +
|
|
178
|
+
(options.description !== undefined ? ', description already matches' : '') +
|
|
179
|
+
'.');
|
|
177
180
|
outro(isDryRun ? 'Dry run complete — no changes made' : 'Patch rename (no-op)');
|
|
178
181
|
return;
|
|
179
182
|
}
|
|
@@ -18,7 +18,17 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { Command } from 'commander';
|
|
20
20
|
import type { CommandContext } from '../../types/cli.js';
|
|
21
|
-
import type { PatchSplitOptions } from '../../types/commands/index.js';
|
|
21
|
+
import type { PatchMetadata, PatchSplitOptions } from '../../types/commands/index.js';
|
|
22
|
+
import type { FireForgeConfig } from '../../types/config.js';
|
|
23
|
+
import { type SplitPlan } from './split-plan.js';
|
|
24
|
+
/**
|
|
25
|
+
* Commits a confirmed split under the patch directory lock: renumber →
|
|
26
|
+
* write new patch body → write shrunken source body → single manifest
|
|
27
|
+
* rewrite (new row + shrunken source row + owner rewrites). On any
|
|
28
|
+
* failure the steps are rolled back in reverse order. Exported for
|
|
29
|
+
* `patch move-files --create`, which commits the same transaction shape.
|
|
30
|
+
*/
|
|
31
|
+
export declare function commitPatchSplit(patchesDir: string, plan: SplitPlan, newMetadata: PatchMetadata, options: Pick<PatchSplitOptions, 'yes' | 'forceUnsafe'>, config: FireForgeConfig): Promise<void>;
|
|
22
32
|
/**
|
|
23
33
|
* Runs the `patch split` command: plans the split, lints the projection,
|
|
24
34
|
* confirms, and commits transactionally.
|
|
@@ -38,9 +38,10 @@ import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSpli
|
|
|
38
38
|
* Commits a confirmed split under the patch directory lock: renumber →
|
|
39
39
|
* write new patch body → write shrunken source body → single manifest
|
|
40
40
|
* rewrite (new row + shrunken source row + owner rewrites). On any
|
|
41
|
-
* failure the steps are rolled back in reverse order.
|
|
41
|
+
* failure the steps are rolled back in reverse order. Exported for
|
|
42
|
+
* `patch move-files --create`, which commits the same transaction shape.
|
|
42
43
|
*/
|
|
43
|
-
async function commitPatchSplit(patchesDir, plan, newMetadata, options) {
|
|
44
|
+
export async function commitPatchSplit(patchesDir, plan, newMetadata, options, config) {
|
|
44
45
|
await withPatchDirectoryLock(patchesDir, async () => {
|
|
45
46
|
const manifest = await loadPatchesManifest(patchesDir);
|
|
46
47
|
if (!manifest)
|
|
@@ -49,7 +50,7 @@ async function commitPatchSplit(patchesDir, plan, newMetadata, options) {
|
|
|
49
50
|
if (!current || current.filesAffected.join('\n') !== plan.source.filesAffected.join('\n')) {
|
|
50
51
|
throw new InvalidArgumentError('Patch queue changed while waiting for split confirmation. Re-run the command.', 'patch split');
|
|
51
52
|
}
|
|
52
|
-
const currentPlacement = await resolvePlacementPlan(patchesDir, plan.placementOptions, plan.category, plan.name);
|
|
53
|
+
const currentPlacement = await resolvePlacementPlan(patchesDir, plan.placementOptions, plan.category, plan.name, config);
|
|
53
54
|
if (!placementPlansEqual(currentPlacement, plan.placement)) {
|
|
54
55
|
throw new InvalidArgumentError('Patch queue changed while waiting for split confirmation. Re-run the command.', 'patch split');
|
|
55
56
|
}
|
|
@@ -193,7 +194,7 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
|
|
|
193
194
|
after: options.after ??
|
|
194
195
|
(options.order === undefined && options.before === undefined ? source.filename : undefined),
|
|
195
196
|
});
|
|
196
|
-
const placement = await resolvePlacementPlan(paths.patches, placementOptions, category, options.name);
|
|
197
|
+
const placement = await resolvePlacementPlan(paths.patches, placementOptions, category, options.name, config);
|
|
197
198
|
const plan = {
|
|
198
199
|
source,
|
|
199
200
|
movedFiles,
|
|
@@ -240,7 +241,7 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
|
|
|
240
241
|
outro('Split cancelled');
|
|
241
242
|
return;
|
|
242
243
|
}
|
|
243
|
-
await commitPatchSplit(paths.patches, plan, newMetadata, options);
|
|
244
|
+
await commitPatchSplit(paths.patches, plan, newMetadata, options, config);
|
|
244
245
|
success(`Split ${source.filename}: ${placement.newFilename} now owns ${plan.movedFiles.length} file(s)`);
|
|
245
246
|
if (plan.ownerRewrites.length > 0) {
|
|
246
247
|
info(`Re-pointed staged-dependency owners in: ${plan.ownerRewrites.join(', ')}`);
|
|
@@ -28,6 +28,23 @@ export declare function confirmBroadScanAdditions(args: {
|
|
|
28
28
|
yes: boolean;
|
|
29
29
|
isInteractive: boolean;
|
|
30
30
|
}): Promise<boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* Refuses scan adoptions whose candidate files import modules created by
|
|
33
|
+
* LATER patches. Modeled on the `re-export --files` cross-patch
|
|
34
|
+
* projection: the candidates' diffs are projected into the adopting
|
|
35
|
+
* patch's queue entry, the queue lint runs baseline vs projection, and
|
|
36
|
+
* only forward-import regressions attributable to the candidates block.
|
|
37
|
+
* Staged-dependency declarations and inline lint-ignore markers are
|
|
38
|
+
* honored automatically because the real lint rule evaluates them.
|
|
39
|
+
* Covers broad `--scan`, `--scan-file`, and the `--scan-files` bulk flow
|
|
40
|
+
* (all adopt through the same scan result), including dry-run.
|
|
41
|
+
*/
|
|
42
|
+
export declare function assertScanAdoptionsHaveNoForwardImports(args: {
|
|
43
|
+
patchesDir: string;
|
|
44
|
+
engineDir: string;
|
|
45
|
+
patchFilename: string;
|
|
46
|
+
added: readonly string[];
|
|
47
|
+
}): Promise<void>;
|
|
31
48
|
/** Refuses explicit `--scan-file` additions that did not produce patch hunks. */
|
|
32
49
|
export declare function assertScanFileAdditionsHaveDiffHunks(args: {
|
|
33
50
|
diffContent: string;
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { confirm } from '@clack/prompts';
|
|
4
|
+
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
4
5
|
import { getModifiedFilesInDir, getUntrackedFilesInDir } from '../core/git-status.js';
|
|
6
|
+
import { computeProjectedLintRegressions } from '../core/lint-projection.js';
|
|
5
7
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
8
|
+
import { buildModifiedFileAdditionsFromDiff, buildPatchQueueContext, detectNewFilesInDiff, lintPatchQueue, } from '../core/patch-lint.js';
|
|
6
9
|
import { getClaimedFiles } from '../core/patch-manifest.js';
|
|
10
|
+
import { extractNewFileContentFromDiff } from '../core/patch-transform.js';
|
|
7
11
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
8
12
|
import { pathExists } from '../utils/fs.js';
|
|
9
13
|
import { cancel, info, isCancel, warn } from '../utils/logger.js';
|
|
@@ -132,6 +136,53 @@ function scanAdditionsNeedConfirmation(added) {
|
|
|
132
136
|
return true;
|
|
133
137
|
return new Set(added.map((f) => dirname(f))).size >= SCAN_DIR_COUNT_THRESHOLD;
|
|
134
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Refuses scan adoptions whose candidate files import modules created by
|
|
141
|
+
* LATER patches. Modeled on the `re-export --files` cross-patch
|
|
142
|
+
* projection: the candidates' diffs are projected into the adopting
|
|
143
|
+
* patch's queue entry, the queue lint runs baseline vs projection, and
|
|
144
|
+
* only forward-import regressions attributable to the candidates block.
|
|
145
|
+
* Staged-dependency declarations and inline lint-ignore markers are
|
|
146
|
+
* honored automatically because the real lint rule evaluates them.
|
|
147
|
+
* Covers broad `--scan`, `--scan-file`, and the `--scan-files` bulk flow
|
|
148
|
+
* (all adopt through the same scan result), including dry-run.
|
|
149
|
+
*/
|
|
150
|
+
export async function assertScanAdoptionsHaveNoForwardImports(args) {
|
|
151
|
+
const { patchesDir, engineDir, patchFilename, added } = args;
|
|
152
|
+
if (added.length === 0)
|
|
153
|
+
return;
|
|
154
|
+
const candidateDiff = await getDiffForFilesAgainstHead(engineDir, [...added]);
|
|
155
|
+
if (!candidateDiff.trim())
|
|
156
|
+
return;
|
|
157
|
+
const candidateNewFiles = new Map();
|
|
158
|
+
for (const path of detectNewFilesInDiff(candidateDiff)) {
|
|
159
|
+
candidateNewFiles.set(path, extractNewFileContentFromDiff(candidateDiff, path));
|
|
160
|
+
}
|
|
161
|
+
const candidateAdditions = buildModifiedFileAdditionsFromDiff(candidateDiff);
|
|
162
|
+
const baseCtx = await buildPatchQueueContext(patchesDir);
|
|
163
|
+
const projectedEntries = baseCtx.entries.map((entry) => {
|
|
164
|
+
if (entry.filename !== patchFilename)
|
|
165
|
+
return entry;
|
|
166
|
+
return {
|
|
167
|
+
...entry,
|
|
168
|
+
diff: `${entry.diff}\n${candidateDiff}`,
|
|
169
|
+
newFiles: new Map([...entry.newFiles, ...candidateNewFiles]),
|
|
170
|
+
modifiedFileAdditions: new Map([...entry.modifiedFileAdditions, ...candidateAdditions]),
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
const baselineIssues = lintPatchQueue(baseCtx).filter((i) => i.severity === 'error');
|
|
174
|
+
const projectedIssues = lintPatchQueue({ entries: projectedEntries }).filter((i) => i.severity === 'error');
|
|
175
|
+
const addedSet = new Set(added);
|
|
176
|
+
const offending = computeProjectedLintRegressions(baselineIssues, projectedIssues).filter((issue) => issue.check === 'forward-import' && addedSet.has(issue.file));
|
|
177
|
+
if (offending.length === 0)
|
|
178
|
+
return;
|
|
179
|
+
const details = offending.map((issue) => ` - ${issue.file}: ${issue.message}`).join('\n');
|
|
180
|
+
throw new GeneralError(`Refusing to adopt ${offending.length} scanned file${offending.length === 1 ? '' : 's'} into ${patchFilename} ` +
|
|
181
|
+
`because they import modules created by later patches:\n${details}\n` +
|
|
182
|
+
'Export those files as their own later patch ("fireforge export --order <n>" / ' +
|
|
183
|
+
'"fireforge patch split --order <n>") or declare the intentional dependency with ' +
|
|
184
|
+
'"fireforge patch staged-dependency --add" before re-running the scan.');
|
|
185
|
+
}
|
|
135
186
|
/** Refuses explicit `--scan-file` additions that did not produce patch hunks. */
|
|
136
187
|
export function assertScanFileAdditionsHaveDiffHunks(args) {
|
|
137
188
|
const { diffContent, patchFilename, previousFilesAffected, scanFiles } = args;
|
|
@@ -18,12 +18,12 @@ import { elapsedSince } from '../utils/elapsed.js';
|
|
|
18
18
|
import { toError } from '../utils/errors.js';
|
|
19
19
|
import { pathExists } from '../utils/fs.js';
|
|
20
20
|
import { cancel, info, intro, isCancel, outro, spinner, success, warn } from '../utils/logger.js';
|
|
21
|
-
import { pickDefined } from '../utils/options.js';
|
|
21
|
+
import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../utils/options.js';
|
|
22
22
|
import { runPatchLint } from './export-shared.js';
|
|
23
23
|
import { loadScanFilesAssignments, withDryRunReExportLock } from './re-export-bulk-scan.js';
|
|
24
24
|
import { reExportFilesInPlace } from './re-export-files.js';
|
|
25
25
|
import { applyReExportFilesPositionalFolding, validateReExportOptionCombinations, } from './re-export-options.js';
|
|
26
|
-
import { assertScanFileAdditionsHaveDiffHunks, confirmBroadScanAdditions, normalizeScanFiles, scanPatchFilesForReExport, } from './re-export-scan.js';
|
|
26
|
+
import { assertScanAdoptionsHaveNoForwardImports, assertScanFileAdditionsHaveDiffHunks, confirmBroadScanAdditions, normalizeScanFiles, scanPatchFilesForReExport, } from './re-export-scan.js';
|
|
27
27
|
async function findMissingFiles(engineDir, files) {
|
|
28
28
|
const missingFiles = [];
|
|
29
29
|
for (const file of files) {
|
|
@@ -87,6 +87,17 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
87
87
|
isDryRun,
|
|
88
88
|
...(options.scanFiles !== undefined ? { scanFiles: options.scanFiles } : {}),
|
|
89
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
|
+
});
|
|
90
101
|
if (options.scanFiles === undefined) {
|
|
91
102
|
const isInteractive = process.stdin.isTTY && process.stdout.isTTY;
|
|
92
103
|
const proceed = await confirmBroadScanAdditions({
|
|
@@ -407,7 +418,7 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
407
418
|
}
|
|
408
419
|
/** Registers the re-export command on the CLI program. */
|
|
409
420
|
export function registerReExport(program, { getProjectRoot, withErrorHandling }) {
|
|
410
|
-
program
|
|
421
|
+
const reExport = program
|
|
411
422
|
.command('re-export [patches...]')
|
|
412
423
|
.description('Refresh existing patch bodies (and filesAffected with --scan) from the current engine ' +
|
|
413
424
|
'state. Does NOT change sourceVersion/sourceProduct by default — use --stamp or run ' +
|
|
@@ -428,8 +439,8 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
428
439
|
.option('--force-unsafe', 'Bypass cross-patch lint refusal when --files shrinks a patch')
|
|
429
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.")
|
|
430
441
|
.addOption(new Option('--tier <tier>', 'Force a tier override on the selected patch (only "branding" recognised). Mutually exclusive with --all.').choices(['branding']))
|
|
431
|
-
.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], [])
|
|
432
|
-
|
|
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) => {
|
|
433
444
|
const { tier, lintIgnore, scanFile, scanFiles, ...rest } = options;
|
|
434
445
|
const projectRoot = getProjectRoot();
|
|
435
446
|
await withEngineSessionLock(projectRoot, 're-export', () => reExportCommand(projectRoot, patches, {
|
|
@@ -438,6 +449,6 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
438
449
|
...(scanFiles !== undefined ? { scanFilesManifest: scanFiles } : {}),
|
|
439
450
|
...(tier !== undefined ? { tier: tier } : {}),
|
|
440
451
|
...(lintIgnore !== undefined && lintIgnore.length > 0 ? { lintIgnore } : {}),
|
|
441
|
-
}));
|
|
452
|
+
}), { waitLockSeconds: resolveWaitLockSeconds(options.waitLock) });
|
|
442
453
|
}));
|
|
443
454
|
}
|
|
@@ -5,6 +5,7 @@ import { getProjectPaths } from '../core/config.js';
|
|
|
5
5
|
import { clearAppliedFurnaceState } from '../core/furnace-config.js';
|
|
6
6
|
import { getHead, hasChanges, isGitRepository, isMissingHeadError, resetChanges, } from '../core/git.js';
|
|
7
7
|
import { expandUntrackedDirectoryEntries, getWorkingTreeStatus } from '../core/git-status.js';
|
|
8
|
+
import { warnIfStaticComponentsStale } from '../core/test-stale-check.js';
|
|
8
9
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
9
10
|
import { pathExists } from '../utils/fs.js';
|
|
10
11
|
import { cancel, info, intro, isCancel, outro, spinner, warn } from '../utils/logger.js';
|
|
@@ -86,6 +87,10 @@ export async function resetCommand(projectRoot, options) {
|
|
|
86
87
|
// tree, not the engine checkout, so reset must not silently forget them.
|
|
87
88
|
await clearAppliedFurnaceState(projectRoot);
|
|
88
89
|
s.stop('Changes reset');
|
|
90
|
+
// The reset may have moved components.conf away from what the last full
|
|
91
|
+
// build compiled in — surface that now instead of at the next test
|
|
92
|
+
// refusal (FORGE F13).
|
|
93
|
+
await warnIfStaticComponentsStale(projectRoot, paths.engine);
|
|
89
94
|
outro('Working tree restored to clean state');
|
|
90
95
|
}
|
|
91
96
|
catch (error) {
|