@hominis/fireforge 0.38.0 → 0.40.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 +41 -0
- package/README.md +25 -1
- package/dist/src/cli.js +7 -0
- package/dist/src/commands/build.js +8 -3
- package/dist/src/commands/discard.js +75 -50
- package/dist/src/commands/export-flow.d.ts +1 -1
- package/dist/src/commands/export-flow.js +21 -10
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.js +31 -5
- package/dist/src/commands/import.js +5 -0
- package/dist/src/commands/lint-per-patch.js +97 -18
- package/dist/src/commands/lint-report.d.ts +21 -0
- package/dist/src/commands/lint-report.js +62 -0
- package/dist/src/commands/lint.js +23 -19
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/patch/lint-ignore.js +20 -1
- 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.js +27 -13
- package/dist/src/commands/patch/rename.js +5 -2
- package/dist/src/commands/patch/staged-dependency.js +102 -14
- package/dist/src/commands/re-export-adjacent.d.ts +34 -0
- package/dist/src/commands/re-export-adjacent.js +106 -0
- package/dist/src/commands/re-export-options.js +3 -0
- package/dist/src/commands/re-export.js +42 -53
- package/dist/src/commands/reset.js +5 -0
- package/dist/src/commands/run.js +18 -2
- package/dist/src/commands/status-check.d.ts +35 -0
- package/dist/src/commands/status-check.js +88 -0
- 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 +33 -8
- 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-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +26 -5
- package/dist/src/commands/test.js +38 -32
- package/dist/src/commands/token.js +4 -1
- package/dist/src/commands/tree.d.ts +16 -0
- package/dist/src/commands/tree.js +174 -0
- package/dist/src/core/build-baseline-types.d.ts +7 -0
- package/dist/src/core/build-baseline.d.ts +1 -1
- package/dist/src/core/build-baseline.js +2 -1
- package/dist/src/core/build-prepare.d.ts +9 -0
- package/dist/src/core/build-prepare.js +24 -0
- package/dist/src/core/config-validate.js +25 -0
- package/dist/src/core/discard-baseline.d.ts +60 -0
- package/dist/src/core/discard-baseline.js +232 -0
- package/dist/src/core/license-headers.d.ts +12 -0
- package/dist/src/core/license-headers.js +27 -0
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +17 -5
- package/dist/src/core/patch-export.d.ts +12 -3
- package/dist/src/core/patch-export.js +13 -3
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -2
- package/dist/src/core/patch-lint-checkjs.js +85 -16
- 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-css.js +64 -16
- package/dist/src/core/patch-lint-diff.d.ts +8 -0
- package/dist/src/core/patch-lint-diff.js +31 -0
- package/dist/src/core/patch-lint-mozbuild.d.ts +18 -0
- package/dist/src/core/patch-lint-mozbuild.js +104 -0
- package/dist/src/core/patch-lint-ownership.d.ts +21 -0
- package/dist/src/core/patch-lint-ownership.js +30 -0
- package/dist/src/core/patch-lint-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint.d.ts +23 -0
- package/dist/src/core/patch-lint.js +34 -3
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-policy.d.ts +14 -1
- package/dist/src/core/patch-policy.js +13 -2
- package/dist/src/core/smoke-patterns.d.ts +24 -8
- package/dist/src/core/smoke-patterns.js +23 -12
- package/dist/src/core/status-classify.d.ts +16 -0
- package/dist/src/core/status-classify.js +20 -6
- 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 +15 -0
- package/dist/src/core/test-stale-check.js +37 -1
- package/dist/src/core/token-category.d.ts +40 -0
- package/dist/src/core/token-category.js +264 -0
- package/dist/src/core/token-manager.d.ts +7 -0
- package/dist/src/core/token-manager.js +82 -193
- package/dist/src/core/token-variant.d.ts +7 -0
- package/dist/src/core/token-variant.js +9 -0
- package/dist/src/core/tree-cow.d.ts +18 -0
- package/dist/src/core/tree-cow.js +71 -0
- package/dist/src/core/tree-guard.d.ts +35 -0
- package/dist/src/core/tree-guard.js +124 -0
- package/dist/src/core/tree-store.d.ts +53 -0
- package/dist/src/core/tree-store.js +214 -0
- package/dist/src/core/typecheck-shim.d.ts +27 -0
- package/dist/src/core/typecheck-shim.js +58 -4
- package/dist/src/core/typecheck.js +14 -3
- package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
- package/dist/src/core/xpcshell-profile-dir.js +45 -0
- package/dist/src/errors/git.d.ts +8 -0
- package/dist/src/errors/git.js +17 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types/commands/options.d.ts +45 -2
- package/dist/src/types/config.d.ts +34 -0
- package/dist/src/utils/validation.d.ts +10 -0
- package/dist/src/utils/validation.js +29 -0
- package/package.json +6 -6
|
@@ -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
|
+
}
|
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { relative } from 'node:path';
|
|
9
9
|
import { Command } from 'commander';
|
|
10
|
-
import { getProjectPaths } from '../../core/config.js';
|
|
10
|
+
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
11
11
|
import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
|
|
12
12
|
import { loadPatchesManifest, resolvePatchIdentifier } from '../../core/patch-manifest.js';
|
|
13
13
|
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
14
14
|
import { pathExists } from '../../utils/fs.js';
|
|
15
|
-
import { info, intro, note,
|
|
15
|
+
import { info, intro, note, warn } from '../../utils/logger.js';
|
|
16
16
|
import { commanderArgParser, pickDefined } from '../../utils/options.js';
|
|
17
17
|
import { patchMoveFilesCreateCommand } from './move-files-create.js';
|
|
18
|
+
import { runMoveFilesInto } from './move-files-into.js';
|
|
18
19
|
function collectOption(value, previous) {
|
|
19
20
|
previous.push(value);
|
|
20
21
|
return previous;
|
|
@@ -123,21 +124,34 @@ export async function patchMoveFilesCommand(projectRoot, fromIdentifier, toIdent
|
|
|
123
124
|
}
|
|
124
125
|
}
|
|
125
126
|
const relativePatchesDir = relative(projectRoot, paths.patches) || 'patches';
|
|
126
|
-
info(`
|
|
127
|
+
info(`Moving ownership in ${relativePatchesDir}/patches.json.`);
|
|
127
128
|
info(`Move ${files.length} file(s) from ${source.filename} to ${target.filename}:`);
|
|
128
129
|
note(formatFiles(files), 'Files');
|
|
129
130
|
note(formatFiles(sourceAfter), `${source.filename} files after move`);
|
|
130
131
|
note(formatFiles(targetAfter), `${target.filename} files after move`);
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
'
|
|
139
|
-
|
|
140
|
-
|
|
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
|
+
});
|
|
141
155
|
}
|
|
142
156
|
/**
|
|
143
157
|
* Registers the preview-only `patch move-files` subcommand.
|
|
@@ -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
|
}
|
|
@@ -260,7 +263,7 @@ export function registerPatchRename(parent, context) {
|
|
|
260
263
|
.command('rename <name>')
|
|
261
264
|
.description('Rename a patch: filename + manifest name (and optional description) update without rewriting the .patch body.')
|
|
262
265
|
.requiredOption('--to <new-name>', 'New human-readable name, category-prefixed slug, or full filename stem (normalised into the filename slug)')
|
|
263
|
-
.option('--description <text>', 'Replacement description (omit to leave description unchanged)')
|
|
266
|
+
.option('-d, --description <text>', 'Replacement description (omit to leave description unchanged)')
|
|
264
267
|
.option('--dry-run', 'Show what would change without writing')
|
|
265
268
|
.option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
|
|
266
269
|
.option('--force-unsafe', 'Bypass force-mode patchPolicy refusals')
|
|
@@ -10,35 +10,46 @@ import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
|
10
10
|
import { toError } from '../../utils/errors.js';
|
|
11
11
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
12
12
|
import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
13
|
+
/** Joins the names of the missing flags for error attribution (FORGE G12). */
|
|
14
|
+
function missingFlagList(pairs) {
|
|
15
|
+
return pairs
|
|
16
|
+
.filter(([, value]) => value === undefined || value === '')
|
|
17
|
+
.map(([flag]) => flag)
|
|
18
|
+
.join(', ');
|
|
19
|
+
}
|
|
13
20
|
function modeFromOptions(options) {
|
|
14
21
|
const adding = options.add === true;
|
|
15
22
|
const removing = options.remove === true;
|
|
16
23
|
const clearing = options.clear === true;
|
|
17
24
|
const modeCount = [adding, removing, clearing].filter(Boolean).length;
|
|
18
25
|
if (modeCount > 1) {
|
|
19
|
-
throw new InvalidArgumentError('--add, --remove, and --clear are mutually exclusive. Pick one mode per invocation.', '
|
|
26
|
+
throw new InvalidArgumentError('--add, --remove, and --clear are mutually exclusive. Pick one mode per invocation.', '--add/--remove/--clear');
|
|
20
27
|
}
|
|
21
28
|
if (modeCount === 0) {
|
|
22
|
-
throw new InvalidArgumentError('Specify --add, --remove, or --clear.', '
|
|
29
|
+
throw new InvalidArgumentError('Specify --add, --remove, or --clear.', '--add/--remove/--clear');
|
|
23
30
|
}
|
|
24
31
|
return adding ? 'add' : removing ? 'remove' : 'clear';
|
|
25
32
|
}
|
|
26
33
|
function kindFromOptions(options) {
|
|
27
34
|
const kind = options.kind ?? 'import';
|
|
28
35
|
if (kind !== 'import' && kind !== 'registration') {
|
|
29
|
-
throw new InvalidArgumentError(`--kind must be "import" or "registration" (got "${kind}").`, '
|
|
36
|
+
throw new InvalidArgumentError(`--kind must be "import" or "registration" (got "${kind}").`, '--kind');
|
|
30
37
|
}
|
|
31
38
|
if (kind === 'import' && options.line !== undefined) {
|
|
32
|
-
throw new InvalidArgumentError('--line only applies to --kind registration; import declarations use --specifier.', '
|
|
39
|
+
throw new InvalidArgumentError('--line only applies to --kind registration; import declarations use --specifier.', '--line');
|
|
33
40
|
}
|
|
34
41
|
if (kind === 'registration' && options.specifier !== undefined) {
|
|
35
|
-
throw new InvalidArgumentError('--specifier only applies to --kind import; registration declarations use --line.', '
|
|
42
|
+
throw new InvalidArgumentError('--specifier only applies to --kind import; registration declarations use --line.', '--specifier');
|
|
36
43
|
}
|
|
37
44
|
return kind;
|
|
38
45
|
}
|
|
39
|
-
function requireForwardImportOptions(options
|
|
46
|
+
function requireForwardImportOptions(options) {
|
|
40
47
|
if (!options.file || !options.specifier || !options.creates) {
|
|
41
|
-
throw new InvalidArgumentError(
|
|
48
|
+
throw new InvalidArgumentError('--add requires --file, --specifier, and --creates.', missingFlagList([
|
|
49
|
+
['--file', options.file],
|
|
50
|
+
['--specifier', options.specifier],
|
|
51
|
+
['--creates', options.creates],
|
|
52
|
+
]));
|
|
42
53
|
}
|
|
43
54
|
const dependency = {
|
|
44
55
|
file: options.file,
|
|
@@ -51,9 +62,13 @@ function requireForwardImportOptions(options, mode) {
|
|
|
51
62
|
dependency.reason = options.reason;
|
|
52
63
|
return dependency;
|
|
53
64
|
}
|
|
54
|
-
function requireRegistrationOptions(options
|
|
65
|
+
function requireRegistrationOptions(options) {
|
|
55
66
|
if (!options.file || !options.line || !options.creates) {
|
|
56
|
-
throw new InvalidArgumentError(
|
|
67
|
+
throw new InvalidArgumentError('--add --kind registration requires --file, --line, and --creates.', missingFlagList([
|
|
68
|
+
['--file', options.file],
|
|
69
|
+
['--line', options.line],
|
|
70
|
+
['--creates', options.creates],
|
|
71
|
+
]));
|
|
57
72
|
}
|
|
58
73
|
const dependency = {
|
|
59
74
|
file: options.file,
|
|
@@ -66,6 +81,70 @@ function requireRegistrationOptions(options, mode) {
|
|
|
66
81
|
dependency.reason = options.reason;
|
|
67
82
|
return dependency;
|
|
68
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Resolves the `--remove` target for forward-imports (FORGE G12):
|
|
86
|
+
* `--file` + `--specifier` suffice when they identify at most one staged
|
|
87
|
+
* entry — `--creates` is inferred from a unique match, an ambiguous match
|
|
88
|
+
* refuses with the candidate list, and no match falls through to the
|
|
89
|
+
* honest "no staged forward-import matched" summary.
|
|
90
|
+
*/
|
|
91
|
+
function resolveImportRemovalTarget(options, staged) {
|
|
92
|
+
if (!options.file || !options.specifier) {
|
|
93
|
+
throw new InvalidArgumentError('--remove requires --file and --specifier; --creates is only needed to disambiguate when several staged entries share them.', missingFlagList([
|
|
94
|
+
['--file', options.file],
|
|
95
|
+
['--specifier', options.specifier],
|
|
96
|
+
]));
|
|
97
|
+
}
|
|
98
|
+
if (options.creates) {
|
|
99
|
+
return requireForwardImportOptions(options);
|
|
100
|
+
}
|
|
101
|
+
const { file, specifier } = options;
|
|
102
|
+
const candidates = (staged?.forwardImports ?? []).filter((entry) => entry.file === file &&
|
|
103
|
+
entry.specifier === specifier &&
|
|
104
|
+
(options.owner === undefined || entry.owner === options.owner));
|
|
105
|
+
const single = candidates.length === 1 ? candidates[0] : undefined;
|
|
106
|
+
if (single !== undefined) {
|
|
107
|
+
info(`Inferred --creates ${single.creates} from the single matching staged entry.`);
|
|
108
|
+
return { ...single };
|
|
109
|
+
}
|
|
110
|
+
if (candidates.length > 1) {
|
|
111
|
+
const list = candidates
|
|
112
|
+
.map((entry) => ` - ${dependencyLabel('import', importView(entry))}`)
|
|
113
|
+
.join('\n');
|
|
114
|
+
throw new GeneralError(`--remove matches ${String(candidates.length)} staged forward-imports on this patch; ` +
|
|
115
|
+
`pass --creates to pick one:\n${list}`);
|
|
116
|
+
}
|
|
117
|
+
return { file, specifier, creates: '(no match)' };
|
|
118
|
+
}
|
|
119
|
+
/** Registration twin of {@link resolveImportRemovalTarget}, keyed on `--line`. */
|
|
120
|
+
function resolveRegistrationRemovalTarget(options, staged) {
|
|
121
|
+
if (!options.file || !options.line) {
|
|
122
|
+
throw new InvalidArgumentError('--remove --kind registration requires --file and --line; --creates is only needed to disambiguate when several staged entries share them.', missingFlagList([
|
|
123
|
+
['--file', options.file],
|
|
124
|
+
['--line', options.line],
|
|
125
|
+
]));
|
|
126
|
+
}
|
|
127
|
+
if (options.creates) {
|
|
128
|
+
return requireRegistrationOptions(options);
|
|
129
|
+
}
|
|
130
|
+
const { file, line } = options;
|
|
131
|
+
const candidates = (staged?.registrations ?? []).filter((entry) => entry.file === file &&
|
|
132
|
+
entry.line === line &&
|
|
133
|
+
(options.owner === undefined || entry.owner === options.owner));
|
|
134
|
+
const single = candidates.length === 1 ? candidates[0] : undefined;
|
|
135
|
+
if (single !== undefined) {
|
|
136
|
+
info(`Inferred --creates ${single.creates} from the single matching staged entry.`);
|
|
137
|
+
return { ...single };
|
|
138
|
+
}
|
|
139
|
+
if (candidates.length > 1) {
|
|
140
|
+
const list = candidates
|
|
141
|
+
.map((entry) => ` - ${dependencyLabel('registration', registrationView(entry))}`)
|
|
142
|
+
.join('\n');
|
|
143
|
+
throw new GeneralError(`--remove matches ${String(candidates.length)} staged registrations on this patch; ` +
|
|
144
|
+
`pass --creates to pick one:\n${list}`);
|
|
145
|
+
}
|
|
146
|
+
return { file, line, creates: '(no match)' };
|
|
147
|
+
}
|
|
69
148
|
function importView(dependency) {
|
|
70
149
|
return { ...dependency, matcher: dependency.specifier };
|
|
71
150
|
}
|
|
@@ -137,17 +216,26 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
|
|
|
137
216
|
intro(isDryRun ? 'FireForge patch staged-dependency (dry run)' : 'FireForge patch staged-dependency');
|
|
138
217
|
const mode = modeFromOptions(options);
|
|
139
218
|
const kind = kindFromOptions(options);
|
|
140
|
-
|
|
219
|
+
// The patch queue loads BEFORE the declaration fields resolve so
|
|
220
|
+
// `--remove` can infer `--creates` from the target patch's staged
|
|
221
|
+
// entries (FORGE G12).
|
|
222
|
+
const { paths, manifest } = await requirePatchQueue(projectRoot);
|
|
223
|
+
const targetPatch = requirePatchTarget(identifier, manifest.patches);
|
|
224
|
+
const importDependency = mode === 'clear' || kind !== 'import'
|
|
225
|
+
? undefined
|
|
226
|
+
: mode === 'add'
|
|
227
|
+
? requireForwardImportOptions(options)
|
|
228
|
+
: resolveImportRemovalTarget(options, targetPatch.stagedDependencies);
|
|
141
229
|
const registrationDependency = mode === 'clear' || kind !== 'registration'
|
|
142
230
|
? undefined
|
|
143
|
-
:
|
|
231
|
+
: mode === 'add'
|
|
232
|
+
? requireRegistrationOptions(options)
|
|
233
|
+
: resolveRegistrationRemovalTarget(options, targetPatch.stagedDependencies);
|
|
144
234
|
const target = importDependency !== undefined
|
|
145
235
|
? importView(importDependency)
|
|
146
236
|
: registrationDependency !== undefined
|
|
147
237
|
? registrationView(registrationDependency)
|
|
148
238
|
: undefined;
|
|
149
|
-
const { paths, manifest } = await requirePatchQueue(projectRoot);
|
|
150
|
-
const targetPatch = requirePatchTarget(identifier, manifest.patches);
|
|
151
239
|
const applyToStaged = (staged) => {
|
|
152
240
|
if (mode === 'clear')
|
|
153
241
|
return {};
|
|
@@ -219,7 +307,7 @@ export function registerPatchStagedDependency(parent, context) {
|
|
|
219
307
|
.option('--owner <patch>', 'Exact later patch filename expected to create --creates')
|
|
220
308
|
.option('--reason <text>', 'Human-readable rationale stored with the declaration')
|
|
221
309
|
.option('--dry-run', 'Show what would change without writing')
|
|
222
|
-
.option('-y, --yes', '
|
|
310
|
+
.option('-y, --yes', 'Record scripted consent in the destructive-operation history log. This command never prompts; the flag exists for workflow uniformity with commands that do.')
|
|
223
311
|
.action(withErrorHandling(async (name, options) => {
|
|
224
312
|
await patchStagedDependencyCommand(getProjectRoot(), name, options);
|
|
225
313
|
}));
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { getProjectPaths } from '../core/config.js';
|
|
2
|
+
import type { PatchesManifest, PatchMetadata } from '../types/commands/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Shared per-run context for the adjacency advisory: the classification
|
|
5
|
+
* inputs computed once by `reExportCommand`, plus the collection of
|
|
6
|
+
* patches refused under `--refuse-adjacent-unmanaged` (collected across
|
|
7
|
+
* the loop so a multi-patch run reports every offender in one refusal).
|
|
8
|
+
*/
|
|
9
|
+
export interface AdjacentUnmanagedContext {
|
|
10
|
+
binaryName: string;
|
|
11
|
+
furnacePrefixes: ReadonlySet<string>;
|
|
12
|
+
refuseAdjacentUnmanaged: boolean;
|
|
13
|
+
refusals: {
|
|
14
|
+
patchFilename: string;
|
|
15
|
+
files: string[];
|
|
16
|
+
}[];
|
|
17
|
+
}
|
|
18
|
+
/** Returns the subset of `files` that no longer exist under `engineDir`. */
|
|
19
|
+
export declare function findMissingFiles(engineDir: string, files: readonly string[]): Promise<string[]>;
|
|
20
|
+
/**
|
|
21
|
+
* Emits the scan-less advisory for one patch: missing manifest files and
|
|
22
|
+
* unmanaged files adjacent to the patch's ownership. When
|
|
23
|
+
* `--refuse-adjacent-unmanaged` is set and any unmanaged adjacent file
|
|
24
|
+
* exists, records the patch into `ctx.refusals` and returns true — the
|
|
25
|
+
* caller then skips the write for this patch and converts the collection
|
|
26
|
+
* into one run-level non-zero refusal after the loop.
|
|
27
|
+
*/
|
|
28
|
+
export declare function reportAdjacentUnmanagedFiles(args: {
|
|
29
|
+
patch: PatchMetadata;
|
|
30
|
+
paths: ReturnType<typeof getProjectPaths>;
|
|
31
|
+
manifest: PatchesManifest;
|
|
32
|
+
currentFilesAffected: readonly string[];
|
|
33
|
+
ctx: AdjacentUnmanagedContext;
|
|
34
|
+
}): Promise<boolean>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Scan-less re-export adjacency advisory (FORGE G2). A plain `re-export`
|
|
4
|
+
* keeps the manifest's filesAffected unchanged, so a brand-new file an
|
|
5
|
+
* author just created beside a patch's owned files is silently left out
|
|
6
|
+
* of the refreshed body. This module detects such adjacent files, filters
|
|
7
|
+
* out tool-managed paths (branding, furnace) so the notice names only
|
|
8
|
+
* genuinely unmanaged candidates, and — under
|
|
9
|
+
* `--refuse-adjacent-unmanaged` — lets the caller turn the notice into a
|
|
10
|
+
* non-zero refusal.
|
|
11
|
+
*
|
|
12
|
+
* Split out of re-export.ts to keep that file inside the max-lines
|
|
13
|
+
* budget; exports helpers consumed by re-export.ts, no registrar.
|
|
14
|
+
*/
|
|
15
|
+
import { dirname, join } from 'node:path';
|
|
16
|
+
import { isBrandingManagedPath } from '../core/branding.js';
|
|
17
|
+
import { getModifiedFilesInDir, getUntrackedFilesInDir } from '../core/git-status.js';
|
|
18
|
+
import { getClaimedFiles } from '../core/patch-manifest.js';
|
|
19
|
+
import { isGeneratedBrandingPath } from '../core/status-classify.js';
|
|
20
|
+
import { pathExists } from '../utils/fs.js';
|
|
21
|
+
import { info, warn } from '../utils/logger.js';
|
|
22
|
+
/** Returns the subset of `files` that no longer exist under `engineDir`. */
|
|
23
|
+
export async function findMissingFiles(engineDir, files) {
|
|
24
|
+
const missingFiles = [];
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
if (!(await pathExists(join(engineDir, file))))
|
|
27
|
+
missingFiles.push(file);
|
|
28
|
+
}
|
|
29
|
+
return missingFiles;
|
|
30
|
+
}
|
|
31
|
+
function isToolManagedPath(file, isNewFile, ctx) {
|
|
32
|
+
// Mirrors status-classify: a generated branding path is tool-managed
|
|
33
|
+
// outright; a brand-new unowned file under the branding root is still a
|
|
34
|
+
// patch candidate (the Assets.car precedent) and must stay listed.
|
|
35
|
+
if (isBrandingManagedPath(file, ctx.binaryName) &&
|
|
36
|
+
(!isNewFile || isGeneratedBrandingPath(file, ctx.binaryName))) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
for (const prefix of ctx.furnacePrefixes) {
|
|
40
|
+
if (file.startsWith(prefix))
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
async function findAdjacentUnmanagedFiles(args) {
|
|
46
|
+
const { currentFilesAffected, engineDir, manifest, patchFilename, ctx } = args;
|
|
47
|
+
const parentDirs = [...new Set(currentFilesAffected.map((file) => dirname(file)))];
|
|
48
|
+
const currentSet = new Set(currentFilesAffected);
|
|
49
|
+
const claimedByOthers = getClaimedFiles(manifest, patchFilename);
|
|
50
|
+
const candidates = new Set();
|
|
51
|
+
for (const dir of parentDirs) {
|
|
52
|
+
const [modifiedFiles, untrackedFiles] = await Promise.all([
|
|
53
|
+
getModifiedFilesInDir(engineDir, dir),
|
|
54
|
+
getUntrackedFilesInDir(engineDir, dir),
|
|
55
|
+
]);
|
|
56
|
+
const untrackedSet = new Set(untrackedFiles);
|
|
57
|
+
for (const file of [...modifiedFiles, ...untrackedFiles]) {
|
|
58
|
+
if (currentSet.has(file) || claimedByOthers.has(file))
|
|
59
|
+
continue;
|
|
60
|
+
if (isToolManagedPath(file, untrackedSet.has(file), ctx))
|
|
61
|
+
continue;
|
|
62
|
+
candidates.add(file);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return [...candidates].sort();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Emits the scan-less advisory for one patch: missing manifest files and
|
|
69
|
+
* unmanaged files adjacent to the patch's ownership. When
|
|
70
|
+
* `--refuse-adjacent-unmanaged` is set and any unmanaged adjacent file
|
|
71
|
+
* exists, records the patch into `ctx.refusals` and returns true — the
|
|
72
|
+
* caller then skips the write for this patch and converts the collection
|
|
73
|
+
* into one run-level non-zero refusal after the loop.
|
|
74
|
+
*/
|
|
75
|
+
export async function reportAdjacentUnmanagedFiles(args) {
|
|
76
|
+
const { patch, paths, manifest, currentFilesAffected, ctx } = args;
|
|
77
|
+
const missingFiles = await findMissingFiles(paths.engine, currentFilesAffected);
|
|
78
|
+
if (missingFiles.length > 0) {
|
|
79
|
+
warn(`${patch.filename}: some files in patches.json no longer exist on disk ` +
|
|
80
|
+
`(${missingFiles.join(', ')}). Without --scan, re-export keeps the manifest's ` +
|
|
81
|
+
`filesAffected unchanged and the missing entries will be preserved — ` +
|
|
82
|
+
`\`fireforge verify\` may flag manifest inconsistency after this run.\n` +
|
|
83
|
+
` Re-run with --scan to reconcile filesAffected with the current worktree, ` +
|
|
84
|
+
`or pass --files <paths> to set the list explicitly.`);
|
|
85
|
+
}
|
|
86
|
+
const unmanagedFiles = await findAdjacentUnmanagedFiles({
|
|
87
|
+
currentFilesAffected,
|
|
88
|
+
engineDir: paths.engine,
|
|
89
|
+
manifest,
|
|
90
|
+
patchFilename: patch.filename,
|
|
91
|
+
ctx,
|
|
92
|
+
});
|
|
93
|
+
if (unmanagedFiles.length === 0)
|
|
94
|
+
return false;
|
|
95
|
+
const firstFew = unmanagedFiles.slice(0, 3).join(', ');
|
|
96
|
+
const more = unmanagedFiles.length > 3 ? `, +${String(unmanagedFiles.length - 3)} more` : '';
|
|
97
|
+
warn(`${patch.filename}: found ${String(unmanagedFiles.length)} unmanaged file(s) adjacent to this patch's ownership (${firstFew}${more}); plain re-export keeps filesAffected unchanged — use --scan-file to include reviewed files.`);
|
|
98
|
+
for (const file of unmanagedFiles) {
|
|
99
|
+
info(` ${file} — fireforge re-export ${patch.filename} --scan --scan-file ${file}`);
|
|
100
|
+
}
|
|
101
|
+
if (ctx.refuseAdjacentUnmanaged) {
|
|
102
|
+
ctx.refusals.push({ patchFilename: patch.filename, files: unmanagedFiles });
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|