@hominis/fireforge 0.30.1 → 0.32.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 +36 -0
- package/README.md +22 -0
- package/dist/src/commands/export-all.js +9 -16
- package/dist/src/commands/export-flow.d.ts +6 -0
- package/dist/src/commands/export-flow.js +6 -1
- package/dist/src/commands/export-placement-gate.d.ts +38 -0
- package/dist/src/commands/export-placement-gate.js +105 -0
- package/dist/src/commands/export-shared.d.ts +28 -0
- package/dist/src/commands/export-shared.js +46 -1
- package/dist/src/commands/export.js +52 -113
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
- package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
- package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
- package/dist/src/commands/furnace/create.d.ts +1 -2
- package/dist/src/commands/furnace/deploy.js +36 -114
- package/dist/src/commands/furnace/refresh.js +52 -32
- package/dist/src/commands/furnace/sync.js +2 -0
- package/dist/src/commands/import.js +108 -73
- package/dist/src/commands/lint-per-patch.d.ts +3 -1
- package/dist/src/commands/lint-per-patch.js +265 -74
- package/dist/src/commands/lint.d.ts +1 -58
- package/dist/src/commands/lint.js +193 -88
- package/dist/src/commands/patch/compact.d.ts +5 -2
- package/dist/src/commands/patch/compact.js +85 -25
- package/dist/src/commands/patch/delete.js +17 -17
- package/dist/src/commands/patch/index.js +2 -0
- package/dist/src/commands/patch/lint-ignore.js +3 -16
- package/dist/src/commands/patch/move-files.js +2 -0
- package/dist/src/commands/patch/patch-context.d.ts +41 -0
- package/dist/src/commands/patch/patch-context.js +53 -0
- package/dist/src/commands/patch/rename.js +10 -15
- package/dist/src/commands/patch/reorder.d.ts +0 -2
- package/dist/src/commands/patch/reorder.js +18 -19
- package/dist/src/commands/patch/split-plan.d.ts +66 -0
- package/dist/src/commands/patch/split-plan.js +178 -0
- package/dist/src/commands/patch/split.d.ts +30 -0
- package/dist/src/commands/patch/split.js +283 -0
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
- package/dist/src/commands/patch/staged-dependency.js +4 -17
- package/dist/src/commands/patch/tier.js +4 -17
- package/dist/src/commands/re-export-files.js +4 -1
- package/dist/src/commands/re-export-scan.js +8 -1
- package/dist/src/commands/re-export.js +8 -1
- package/dist/src/commands/rebase/summary.d.ts +1 -5
- package/dist/src/commands/rebase/summary.js +1 -1
- package/dist/src/commands/status-output.js +77 -68
- package/dist/src/commands/test-diagnose.d.ts +23 -0
- package/dist/src/commands/test-diagnose.js +210 -0
- package/dist/src/commands/test-run.d.ts +68 -0
- package/dist/src/commands/test-run.js +97 -0
- package/dist/src/commands/test.js +214 -263
- package/dist/src/commands/token.js +15 -1
- package/dist/src/commands/wire.js +109 -78
- package/dist/src/core/build-audit.d.ts +1 -1
- package/dist/src/core/build-audit.js +2 -46
- package/dist/src/core/build-baseline-types.d.ts +38 -0
- package/dist/src/core/build-baseline-types.js +10 -0
- package/dist/src/core/build-baseline.d.ts +1 -31
- package/dist/src/core/build-prepare.d.ts +1 -1
- package/dist/src/core/build-prepare.js +2 -45
- package/dist/src/core/config-paths.d.ts +0 -8
- package/dist/src/core/config-paths.js +4 -4
- package/dist/src/core/config-state.d.ts +0 -6
- package/dist/src/core/config-state.js +1 -1
- package/dist/src/core/config-validate-patch-policy.js +12 -13
- package/dist/src/core/config-validate.js +74 -28
- package/dist/src/core/engine-changes.d.ts +24 -0
- package/dist/src/core/engine-changes.js +64 -0
- package/dist/src/core/firefox-cache.d.ts +0 -5
- package/dist/src/core/firefox-cache.js +1 -1
- package/dist/src/core/firefox-download.d.ts +0 -6
- package/dist/src/core/firefox-download.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
- package/dist/src/core/furnace-apply-helpers.js +11 -20
- package/dist/src/core/furnace-apply.d.ts +1 -1
- package/dist/src/core/furnace-apply.js +1 -1
- package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
- package/dist/src/core/furnace-checksum-utils.js +15 -0
- package/dist/src/core/furnace-config-validate.d.ts +31 -0
- package/dist/src/core/furnace-config-validate.js +133 -0
- package/dist/src/core/furnace-config.d.ts +4 -32
- package/dist/src/core/furnace-config.js +15 -111
- package/dist/src/core/furnace-constants.d.ts +0 -10
- package/dist/src/core/furnace-constants.js +2 -2
- package/dist/src/core/furnace-css-fragments.d.ts +79 -0
- package/dist/src/core/furnace-css-fragments.js +243 -0
- package/dist/src/core/furnace-jsconfig.d.ts +63 -0
- package/dist/src/core/furnace-jsconfig.js +191 -0
- package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
- package/dist/src/core/furnace-validate-helpers.js +40 -1
- package/dist/src/core/furnace-validate-registration.js +16 -1
- package/dist/src/core/furnace-validate.js +54 -2
- package/dist/src/core/git-base.d.ts +15 -0
- package/dist/src/core/git-base.js +32 -0
- package/dist/src/core/git-diff.d.ts +8 -0
- package/dist/src/core/git-diff.js +224 -59
- package/dist/src/core/git-file-ops.d.ts +39 -12
- package/dist/src/core/git-file-ops.js +84 -3
- package/dist/src/core/lint-cache.d.ts +0 -13
- package/dist/src/core/lint-cache.js +5 -5
- package/dist/src/core/mach.d.ts +22 -1
- package/dist/src/core/mach.js +27 -2
- package/dist/src/core/manifest-register.d.ts +5 -16
- package/dist/src/core/manifest-register.js +3 -1
- package/dist/src/core/patch-lint-checkjs.d.ts +75 -21
- package/dist/src/core/patch-lint-checkjs.js +263 -71
- package/dist/src/core/patch-lint-css.d.ts +23 -0
- package/dist/src/core/patch-lint-css.js +172 -0
- package/dist/src/core/patch-lint-jsdoc.js +63 -4
- package/dist/src/core/patch-lint-observer.d.ts +37 -0
- package/dist/src/core/patch-lint-observer.js +168 -0
- package/dist/src/core/patch-lint.d.ts +34 -11
- package/dist/src/core/patch-lint.js +24 -161
- package/dist/src/core/patch-manifest-io.d.ts +16 -0
- package/dist/src/core/patch-manifest-io.js +44 -2
- package/dist/src/core/patch-manifest-validate.d.ts +1 -8
- package/dist/src/core/patch-manifest-validate.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-policy.d.ts +0 -4
- package/dist/src/core/patch-policy.js +10 -4
- package/dist/src/core/register-browser-content.d.ts +1 -1
- package/dist/src/core/register-module.d.ts +1 -1
- package/dist/src/core/register-result.d.ts +21 -0
- package/dist/src/core/register-result.js +9 -0
- package/dist/src/core/register-shared-css.d.ts +1 -1
- package/dist/src/core/register-test-manifest.d.ts +1 -1
- package/dist/src/core/test-harness-crash.d.ts +61 -0
- package/dist/src/core/test-harness-crash.js +140 -0
- package/dist/src/core/test-stale-check.d.ts +1 -1
- package/dist/src/core/test-stale-check.js +2 -46
- package/dist/src/core/test-xpcshell-retry.d.ts +9 -2
- package/dist/src/core/test-xpcshell-retry.js +10 -3
- package/dist/src/core/token-dark-mode.js +14 -26
- package/dist/src/core/token-manager.d.ts +4 -0
- package/dist/src/core/token-manager.js +70 -16
- package/dist/src/core/typecheck-shim.d.ts +3 -22
- package/dist/src/core/typecheck-shim.js +69 -7
- package/dist/src/core/wire-utils.js +37 -44
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +122 -0
- package/dist/src/types/config.d.ts +11 -2
- package/dist/src/types/furnace.d.ts +12 -1
- package/dist/src/utils/elapsed.d.ts +0 -2
- package/dist/src/utils/elapsed.js +1 -1
- package/dist/src/utils/fs.d.ts +0 -5
- package/dist/src/utils/fs.js +1 -1
- package/dist/src/utils/regex.d.ts +0 -6
- package/dist/src/utils/regex.js +3 -3
- package/dist/src/utils/validation.d.ts +0 -8
- package/dist/src/utils/validation.js +2 -2
- package/package.json +6 -4
|
@@ -9,7 +9,7 @@ import { getAllDiff, getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
|
9
9
|
import { expandUntrackedDirectoryEntries, getModifiedFilesInDir, getUntrackedFiles, getUntrackedFilesInDir, getWorkingTreeStatus, } from '../core/git-status.js';
|
|
10
10
|
import { clearPerPatchLintCache } from '../core/lint-cache.js';
|
|
11
11
|
import { extractAffectedFiles } from '../core/patch-apply.js';
|
|
12
|
-
import { buildPatchQueueContext, lintExportedPatch, lintPatchQueue } from '../core/patch-lint.js';
|
|
12
|
+
import { buildPatchQueueContext, countNonBinaryDiffLines, lintExportedPatch, lintPatchQueue, lintPatchSize, } from '../core/patch-lint.js';
|
|
13
13
|
import { collectDiffFilePaths, tagLintIssues } from '../core/patch-lint-diff-tag.js';
|
|
14
14
|
import { GeneralError } from '../errors/base.js';
|
|
15
15
|
import { pathExists } from '../utils/fs.js';
|
|
@@ -215,13 +215,12 @@ export function applyAggregateLintIgnoreSuppression(issues, ctx) {
|
|
|
215
215
|
return { issues: filtered, dropped: issues.length - filtered.length };
|
|
216
216
|
}
|
|
217
217
|
/**
|
|
218
|
-
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
221
|
-
*
|
|
218
|
+
* Up-front flag validation for `lintCommand`: rejects `--only-introduced`
|
|
219
|
+
* without `--since`, non-integer `--max-warnings`, and `--per-patch`
|
|
220
|
+
* combined with explicit file paths — each a misconfiguration that should
|
|
221
|
+
* fail loud rather than silently narrow the result.
|
|
222
222
|
*/
|
|
223
|
-
|
|
224
|
-
intro('FireForge Lint');
|
|
223
|
+
function validateLintFlags(options, files) {
|
|
225
224
|
// `--only-introduced` scopes the exit code to `--since`-tagged issues, so
|
|
226
225
|
// without a revision to anchor the diff there is no "introduced" subset
|
|
227
226
|
// to scope to — reject the combination up-front so a misconfigured CI
|
|
@@ -235,85 +234,31 @@ export async function lintCommand(projectRoot, files, options = {}) {
|
|
|
235
234
|
throw new GeneralError('--max-warnings must be a non-negative integer.');
|
|
236
235
|
}
|
|
237
236
|
// `--per-patch` rescopes the diff from "aggregate engine state" to "each
|
|
238
|
-
// patch's own filesAffected". Mixing in explicit file paths would
|
|
239
|
-
// an ambiguous set — is the file list an additional filter, or
|
|
240
|
-
// replace the per-patch scope? Reject up-front
|
|
241
|
-
//
|
|
237
|
+
// patch's own filesAffected". Mixing in explicit engine file paths would
|
|
238
|
+
// produce an ambiguous set — is the file list an additional filter, or
|
|
239
|
+
// does it replace the per-patch scope? Reject up-front, but point at the
|
|
240
|
+
// first-class subset filter so an operator who wanted to target patches
|
|
241
|
+
// (not engine files) knows the supported syntax.
|
|
242
242
|
if (options.perPatch && files.length > 0) {
|
|
243
|
-
throw new GeneralError('--per-patch cannot be combined with explicit file paths.
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (!(await pathExists(paths.engine))) {
|
|
247
|
-
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
243
|
+
throw new GeneralError('--per-patch cannot be combined with explicit engine file paths. ' +
|
|
244
|
+
'To lint a subset of patches, use `--per-patch --patches <name…>`; ' +
|
|
245
|
+
'to lint specific engine files, drop --per-patch.');
|
|
248
246
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
await lintPerPatch(projectRoot, paths, options);
|
|
254
|
-
return;
|
|
247
|
+
// `--patches` only means something in per-patch mode (it filters the
|
|
248
|
+
// queue); in aggregate/file-list mode there is no patch loop to narrow.
|
|
249
|
+
if (options.patches !== undefined && !options.perPatch) {
|
|
250
|
+
throw new GeneralError('--patches requires --per-patch.');
|
|
255
251
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
// post-preview lint run.
|
|
267
|
-
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
268
|
-
const diff = await resolveLintDiff(paths.engine, files, config.binaryName, furnacePrefixes);
|
|
269
|
-
if (diff === null)
|
|
270
|
-
return;
|
|
271
|
-
const filesAffected = extractAffectedFiles(diff);
|
|
272
|
-
// Build patch queue context once so it can be shared between the
|
|
273
|
-
// per-patch ownership resolver and the cross-patch rules.
|
|
274
|
-
let ctx;
|
|
275
|
-
if (await pathExists(paths.patches)) {
|
|
276
|
-
ctx = await buildPatchQueueContext(paths.patches);
|
|
277
|
-
}
|
|
278
|
-
let issues = [
|
|
279
|
-
...(await lintExportedPatch(paths.engine, filesAffected, diff, config, ctx)),
|
|
280
|
-
];
|
|
281
|
-
// Cross-patch rules operate over the whole queue, so run them whenever a
|
|
282
|
-
// patches directory exists — they surface duplicate /dev/null creations
|
|
283
|
-
// and forward-import chains that the per-patch orchestrator cannot see.
|
|
284
|
-
if (ctx) {
|
|
285
|
-
issues.push(...lintPatchQueue(ctx));
|
|
286
|
-
}
|
|
287
|
-
// Honor per-patch `lintIgnore` in aggregate mode by attributing each
|
|
288
|
-
// issue's file to its owning patches via the manifest's
|
|
289
|
-
// `filesAffected`. Per-patch mode threads `lintIgnore` directly into
|
|
290
|
-
// `lintExportedPatch`; aggregate mode previously had no patch-level
|
|
291
|
-
// scope to consult, so a check an operator had explicitly waived in
|
|
292
|
-
// `patches.json` re-surfaced on every `--since` run (CI default).
|
|
293
|
-
if (ctx) {
|
|
294
|
-
const result = applyAggregateLintIgnoreSuppression(issues, ctx);
|
|
295
|
-
issues = result.issues;
|
|
296
|
-
if (result.dropped > 0) {
|
|
297
|
-
info(`Suppressed ${result.dropped} issue(s) via per-patch lintIgnore (aggregate mode).`);
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
// When a queue manifest exists AND files were NOT scoped explicitly, the
|
|
301
|
-
// "diff" we just linted is every applied patch summed together. Patch-
|
|
302
|
-
// size rules (`large-patch-lines`, `large-patch-files`) then fire against
|
|
303
|
-
// the aggregate rather than any individual patch, producing counts like
|
|
304
|
-
// "Patch is 37529 lines" that read as a task-specific regression but are
|
|
305
|
-
// really an artefact of aggregation. Surface a one-line note pointing at
|
|
306
|
-
// `--per-patch` so the operator knows the per-patch scope exists before
|
|
307
|
-
// they read the error message as "my queue is broken".
|
|
308
|
-
//
|
|
309
|
-
// In aggregate mode over a multi-patch queue we also downgrade the two
|
|
310
|
-
// size rules from `error` to `warning`. Before this downgrade, a
|
|
311
|
-
// fresh-imported patch stack of 20+ patches hard-failed `fireforge lint`
|
|
312
|
-
// on lines-per-aggregate counts that are mathematically impossible to
|
|
313
|
-
// satisfy without splitting patches that were already split — the
|
|
314
|
-
// actionable unit is the individual patch, and `--per-patch` is the
|
|
315
|
-
// mode that matches. Per-patch mode keeps errors as errors (see
|
|
316
|
-
// `lintPerPatch` below).
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Aggregate-mode patch-size softening: when the linted diff is every
|
|
255
|
+
* applied patch summed (no explicit file scope, multi-patch queue), the
|
|
256
|
+
* `large-patch-lines` / `large-patch-files` counts are an artefact of
|
|
257
|
+
* aggregation rather than a property of any one patch. Surface the
|
|
258
|
+
* `--per-patch` hint and downgrade those two rules to warnings; per-patch
|
|
259
|
+
* mode keeps them as errors.
|
|
260
|
+
*/
|
|
261
|
+
function downgradeAggregateSizeRules(issues, files, ctx) {
|
|
317
262
|
const aggregateHintApplicable = files.length === 0 && ctx !== undefined && ctx.entries.length > 1;
|
|
318
263
|
if (aggregateHintApplicable &&
|
|
319
264
|
issues.some((i) => i.check === 'large-patch-lines' || i.check === 'large-patch-files')) {
|
|
@@ -325,18 +270,94 @@ export async function lintCommand(projectRoot, files, options = {}) {
|
|
|
325
270
|
}
|
|
326
271
|
}
|
|
327
272
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Evaluates the patch-size rules (`large-patch-files` / `large-patch-lines`)
|
|
276
|
+
* for an ad-hoc explicit-file-list lint, scoped to each file's **owning
|
|
277
|
+
* patch** rather than the combined file list.
|
|
278
|
+
*
|
|
279
|
+
* The default file-list path used to feed every passed file to
|
|
280
|
+
* `lintExportedPatch` as one synthetic patch, so a cross-patch selection of
|
|
281
|
+
* eight files belonging to four patches reported `Patch affects 8 files`
|
|
282
|
+
* even though no single owning patch was oversized. This helper instead
|
|
283
|
+
* groups the affected files by their owning patch (via the manifest's
|
|
284
|
+
* `filesAffected`), then runs `lintPatchSize` against each owner's real file
|
|
285
|
+
* count + diff, honouring that owner's `tier` and `lintIgnore` — so
|
|
286
|
+
* `lint <files>`, `lint --per-patch`, and `re-export --dry-run` agree on the
|
|
287
|
+
* same size findings for the same files. Files no patch claims are evaluated
|
|
288
|
+
* together as one prospective new patch, preserving the pre-export
|
|
289
|
+
* oversized-change warning.
|
|
290
|
+
*
|
|
291
|
+
* @param engineDir - Absolute engine directory
|
|
292
|
+
* @param filesAffected - Engine-relative files touched by the ad-hoc diff
|
|
293
|
+
* @param ctx - Patch queue context used to attribute file → owning patch
|
|
294
|
+
* @returns Size issues, each attributed to its owning patch by message prefix
|
|
295
|
+
*/
|
|
296
|
+
async function lintOwningPatchSizes(engineDir, filesAffected, ctx) {
|
|
297
|
+
const listed = new Set(filesAffected);
|
|
298
|
+
const owners = new Map();
|
|
299
|
+
const ownedListed = new Set();
|
|
300
|
+
for (const entry of ctx.entries) {
|
|
301
|
+
const md = entry.metadata;
|
|
302
|
+
if (!md)
|
|
303
|
+
continue;
|
|
304
|
+
let ownsAny = false;
|
|
305
|
+
for (const f of md.filesAffected) {
|
|
306
|
+
if (listed.has(f)) {
|
|
307
|
+
ownedListed.add(f);
|
|
308
|
+
ownsAny = true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (ownsAny)
|
|
312
|
+
owners.set(entry.filename, entry);
|
|
313
|
+
}
|
|
314
|
+
const issues = [];
|
|
315
|
+
const lineCountForFiles = async (relPaths) => {
|
|
316
|
+
const existing = [];
|
|
317
|
+
for (const f of relPaths) {
|
|
318
|
+
if (await pathExists(join(engineDir, f)))
|
|
319
|
+
existing.push(f);
|
|
320
|
+
}
|
|
321
|
+
if (existing.length === 0)
|
|
322
|
+
return 0;
|
|
323
|
+
const diff = await getDiffForFilesAgainstHead(engineDir, existing);
|
|
324
|
+
return countNonBinaryDiffLines(diff).textLines;
|
|
325
|
+
};
|
|
326
|
+
for (const entry of owners.values()) {
|
|
327
|
+
const md = entry.metadata;
|
|
328
|
+
if (!md)
|
|
329
|
+
continue;
|
|
330
|
+
const lineCount = await lineCountForFiles(md.filesAffected);
|
|
331
|
+
const ignore = md.lintIgnore?.length ? new Set(md.lintIgnore) : undefined;
|
|
332
|
+
for (const issue of lintPatchSize(md.filesAffected, lineCount, md.tier)) {
|
|
333
|
+
if (ignore?.has(issue.check))
|
|
334
|
+
continue;
|
|
335
|
+
issues.push({ ...issue, message: `${entry.filename}: ${issue.message}` });
|
|
336
|
+
}
|
|
332
337
|
}
|
|
338
|
+
// Files no patch claims are a prospective new patch: evaluate them as one
|
|
339
|
+
// unit so a genuinely oversized fresh change still surfaces.
|
|
340
|
+
const unowned = filesAffected.filter((f) => !ownedListed.has(f));
|
|
341
|
+
if (unowned.length > 0) {
|
|
342
|
+
const lineCount = await lineCountForFiles(unowned);
|
|
343
|
+
issues.push(...lintPatchSize(unowned, lineCount));
|
|
344
|
+
}
|
|
345
|
+
return issues;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Reporting + exit phase of `lintCommand`: tags issues against `--since`,
|
|
349
|
+
* renders every notice/warning/error row, prints the summary, and applies
|
|
350
|
+
* the failure criteria (`--only-introduced` scoping, `--max-warnings`)
|
|
351
|
+
* by throwing GeneralError. Issues must be non-empty.
|
|
352
|
+
*/
|
|
353
|
+
async function reportLintOutcome(engineDir, issues, options) {
|
|
333
354
|
// Diff-scoping: tag each issue as introduced-in-current-task vs
|
|
334
355
|
// cumulative-pre-existing-drift. Never filters — full set still prints
|
|
335
356
|
// and exit code semantics are unchanged — but the per-line prefix and
|
|
336
357
|
// summary make triage trivial on a large patch series.
|
|
337
358
|
const sinceActive = Boolean(options.since);
|
|
338
359
|
if (options.since) {
|
|
339
|
-
const diffFiles = await collectDiffFilePaths(
|
|
360
|
+
const diffFiles = await collectDiffFilePaths(engineDir, options.since);
|
|
340
361
|
tagLintIssues(issues, diffFiles);
|
|
341
362
|
}
|
|
342
363
|
const errors = issues.filter((i) => i.severity === 'error');
|
|
@@ -399,6 +420,86 @@ export async function lintCommand(projectRoot, files, options = {}) {
|
|
|
399
420
|
outro('Lint passed');
|
|
400
421
|
}
|
|
401
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Runs the lint command to check engine changes against patch quality rules.
|
|
425
|
+
* @param projectRoot - Root directory of the project
|
|
426
|
+
* @param files - Optional file/directory paths to lint (relative to engine/)
|
|
427
|
+
* @param options - Additional lint options such as `--since` diff-scoping
|
|
428
|
+
*/
|
|
429
|
+
export async function lintCommand(projectRoot, files, options = {}) {
|
|
430
|
+
intro('FireForge Lint');
|
|
431
|
+
validateLintFlags(options, files);
|
|
432
|
+
const paths = getProjectPaths(projectRoot);
|
|
433
|
+
if (!(await pathExists(paths.engine))) {
|
|
434
|
+
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
435
|
+
}
|
|
436
|
+
if (!(await isGitRepository(paths.engine))) {
|
|
437
|
+
throw new GeneralError('Engine directory is not a git repository. Run "fireforge download" to initialize.');
|
|
438
|
+
}
|
|
439
|
+
if (options.perPatch) {
|
|
440
|
+
await lintPerPatch(projectRoot, paths, options);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
// Load the config before resolving the diff so we can pass
|
|
444
|
+
// `binaryName` into the aggregate-mode branding exclusion in
|
|
445
|
+
// `resolveLintDiff`. The config was previously loaded only after
|
|
446
|
+
// the diff was resolved; hoisting it is cheap and keeps the two
|
|
447
|
+
// call sites close together.
|
|
448
|
+
const config = await loadConfig(projectRoot);
|
|
449
|
+
// Pull the Furnace-managed prefix set up-front so aggregate lint can
|
|
450
|
+
// mirror the branding exclusion for Furnace material — without it,
|
|
451
|
+
// preview-generated stories under `browser/components/storybook/
|
|
452
|
+
// stories/furnace/` show up as license-header errors on every
|
|
453
|
+
// post-preview lint run.
|
|
454
|
+
const furnacePrefixes = await collectFurnaceManagedPrefixes(projectRoot);
|
|
455
|
+
const diff = await resolveLintDiff(paths.engine, files, config.binaryName, furnacePrefixes);
|
|
456
|
+
if (diff === null)
|
|
457
|
+
return;
|
|
458
|
+
const filesAffected = extractAffectedFiles(diff);
|
|
459
|
+
// Build patch queue context once so it can be shared between the
|
|
460
|
+
// per-patch ownership resolver and the cross-patch rules.
|
|
461
|
+
let ctx;
|
|
462
|
+
if (await pathExists(paths.patches)) {
|
|
463
|
+
ctx = await buildPatchQueueContext(paths.patches);
|
|
464
|
+
}
|
|
465
|
+
// Ad-hoc explicit-file-list mode evaluates the patch-size rules per
|
|
466
|
+
// owning patch (see `lintOwningPatchSizes`), so suppress the synthetic
|
|
467
|
+
// combined-list size check in the shared pass — otherwise a cross-patch
|
|
468
|
+
// selection synthesises a phantom oversized patch from the file count.
|
|
469
|
+
const fileListMode = files.length > 0 && ctx !== undefined;
|
|
470
|
+
let issues = [
|
|
471
|
+
...(await lintExportedPatch(paths.engine, filesAffected, diff, config, ctx, undefined, undefined, fileListMode ? { skipPatchSize: true } : undefined)),
|
|
472
|
+
];
|
|
473
|
+
if (files.length > 0 && ctx) {
|
|
474
|
+
issues.push(...(await lintOwningPatchSizes(paths.engine, filesAffected, ctx)));
|
|
475
|
+
}
|
|
476
|
+
// Cross-patch rules operate over the whole queue, so run them whenever a
|
|
477
|
+
// patches directory exists — they surface duplicate /dev/null creations
|
|
478
|
+
// and forward-import chains that the per-patch orchestrator cannot see.
|
|
479
|
+
if (ctx) {
|
|
480
|
+
issues.push(...lintPatchQueue(ctx));
|
|
481
|
+
}
|
|
482
|
+
// Honor per-patch `lintIgnore` in aggregate mode by attributing each
|
|
483
|
+
// issue's file to its owning patches via the manifest's
|
|
484
|
+
// `filesAffected`. Per-patch mode threads `lintIgnore` directly into
|
|
485
|
+
// `lintExportedPatch`; aggregate mode previously had no patch-level
|
|
486
|
+
// scope to consult, so a check an operator had explicitly waived in
|
|
487
|
+
// `patches.json` re-surfaced on every `--since` run (CI default).
|
|
488
|
+
if (ctx) {
|
|
489
|
+
const result = applyAggregateLintIgnoreSuppression(issues, ctx);
|
|
490
|
+
issues = result.issues;
|
|
491
|
+
if (result.dropped > 0) {
|
|
492
|
+
info(`Suppressed ${result.dropped} issue(s) via per-patch lintIgnore (aggregate mode).`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
downgradeAggregateSizeRules(issues, files, ctx);
|
|
496
|
+
if (issues.length === 0) {
|
|
497
|
+
success('No lint issues found.');
|
|
498
|
+
outro('Lint passed');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
await reportLintOutcome(paths.engine, issues, options);
|
|
502
|
+
}
|
|
402
503
|
/** Registers the lint command on the CLI program. */
|
|
403
504
|
export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
404
505
|
const lint = program
|
|
@@ -409,6 +510,7 @@ export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
|
409
510
|
.option('--since <git-rev>', 'Tag issues as [introduced] or [cumulative] based on whether the file changed since <git-rev> (e.g. HEAD, a branch, a SHA)')
|
|
410
511
|
.option('--only-introduced', 'Fail only on issues tagged [introduced] (requires --since). Cumulative errors still print but do not set a non-zero exit.')
|
|
411
512
|
.option('--per-patch', "Lint each patch in the queue as its own isolated diff. Rescopes patch-size rules so they fire against individual patches rather than the aggregate. Honours each patch's `lintIgnore` entries.")
|
|
513
|
+
.option('--patches <names...>', 'With --per-patch, lint only the named patches (by filename or manifest name) instead of the whole queue. Queue-level findings are scoped to files those patches touch.')
|
|
412
514
|
.option('--max-warnings <n>', 'Fail when lint reports more than <n> warning(s); use 0 for warning-clean release gates.')
|
|
413
515
|
.option('--no-cache', 'Bypass per-patch lint result cache reads and writes.')
|
|
414
516
|
.action(withErrorHandling(async (paths, options) => {
|
|
@@ -422,6 +524,9 @@ export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
|
422
524
|
if (options.perPatch !== undefined) {
|
|
423
525
|
lintOptions.perPatch = options.perPatch;
|
|
424
526
|
}
|
|
527
|
+
if (options.patches !== undefined) {
|
|
528
|
+
lintOptions.patches = options.patches;
|
|
529
|
+
}
|
|
425
530
|
if (options.maxWarnings !== undefined) {
|
|
426
531
|
const maxWarnings = Number(options.maxWarnings);
|
|
427
532
|
if (!Number.isInteger(maxWarnings) || maxWarnings < 0) {
|
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
* `fireforge patch compact` — closes ordinal gaps in the patch queue.
|
|
3
3
|
*
|
|
4
4
|
* After deletes or splits, patch ordinals may have gaps (e.g. 1, 3, 7).
|
|
5
|
-
* This command renumbers
|
|
6
|
-
*
|
|
5
|
+
* This command renumbers patches to close those gaps in a single atomic
|
|
6
|
+
* operation, preserving relative order. Without a patch policy the whole
|
|
7
|
+
* queue is renumbered from 1; with `patchPolicy.ranges` configured the
|
|
8
|
+
* compaction is range-aware (each category range compacts independently,
|
|
9
|
+
* reserved ranges and out-of-range strays are left untouched).
|
|
7
10
|
*/
|
|
8
11
|
import { Command } from 'commander';
|
|
9
12
|
import type { CommandContext } from '../../types/cli.js';
|
|
@@ -3,38 +3,96 @@
|
|
|
3
3
|
* `fireforge patch compact` — closes ordinal gaps in the patch queue.
|
|
4
4
|
*
|
|
5
5
|
* After deletes or splits, patch ordinals may have gaps (e.g. 1, 3, 7).
|
|
6
|
-
* This command renumbers
|
|
7
|
-
*
|
|
6
|
+
* This command renumbers patches to close those gaps in a single atomic
|
|
7
|
+
* operation, preserving relative order. Without a patch policy the whole
|
|
8
|
+
* queue is renumbered from 1; with `patchPolicy.ranges` configured the
|
|
9
|
+
* compaction is range-aware (each category range compacts independently,
|
|
10
|
+
* reserved ranges and out-of-range strays are left untouched).
|
|
8
11
|
*/
|
|
9
|
-
import {
|
|
12
|
+
import { loadConfig } from '../../core/config.js';
|
|
10
13
|
import { appendHistory, confirmDestructive } from '../../core/destructive.js';
|
|
11
14
|
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
12
15
|
import { loadPatchesManifest, renumberPatchesInManifest, } from '../../core/patch-manifest.js';
|
|
13
16
|
import { applyRenameMapToManifest, enforcePatchPolicy } from '../../core/patch-policy.js';
|
|
14
17
|
import { GeneralError } from '../../errors/base.js';
|
|
15
18
|
import { toError } from '../../utils/errors.js';
|
|
16
|
-
import { pathExists } from '../../utils/fs.js';
|
|
17
19
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
18
20
|
import { pickDefined } from '../../utils/options.js';
|
|
21
|
+
import { requirePatchQueue } from './patch-context.js';
|
|
19
22
|
import { rebuildFilenameForOrder } from './reorder.js';
|
|
23
|
+
/** True when `order` falls inside a configured reserved range. */
|
|
24
|
+
function isReservedOrder(policyCfg, order) {
|
|
25
|
+
return (policyCfg.reservedRanges ?? []).some((r) => order >= r.from && order <= r.to);
|
|
26
|
+
}
|
|
20
27
|
/**
|
|
21
|
-
* Computes a rename map that
|
|
22
|
-
*
|
|
28
|
+
* Computes a rename map that closes ordinal gaps.
|
|
29
|
+
*
|
|
30
|
+
* Without a patch policy, all patches are renumbered to 1, 2, 3, … in
|
|
31
|
+
* current sort order (historical behaviour). With `patchPolicy.ranges`
|
|
32
|
+
* configured, compaction happens *within* each category range instead:
|
|
33
|
+
* each range's members are renumbered consecutively starting at the
|
|
34
|
+
* range's first occupied ordinal, skipping reserved orders — mirroring
|
|
35
|
+
* what `evaluateGaps` treats as gapless under `allowGaps: false`.
|
|
36
|
+
* Reserved-range patches and patches outside their category's range are
|
|
37
|
+
* never moved (a global renumber would project them across range
|
|
38
|
+
* boundaries and trip `category-range` refusals).
|
|
23
39
|
*/
|
|
24
|
-
function computeCompactRenameMap(patches) {
|
|
25
|
-
|
|
40
|
+
function computeCompactRenameMap(patches, policyCfg) {
|
|
41
|
+
if (!policyCfg || policyCfg.ranges.length === 0) {
|
|
42
|
+
const sorted = [...patches].sort((a, b) => a.order - b.order);
|
|
43
|
+
const renames = new Map();
|
|
44
|
+
for (const [i, patch] of sorted.entries()) {
|
|
45
|
+
const newOrder = i + 1;
|
|
46
|
+
if (patch.order !== newOrder) {
|
|
47
|
+
renames.set(patch.filename, {
|
|
48
|
+
newOrder,
|
|
49
|
+
newFilename: rebuildFilenameForOrder(patch, newOrder),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return renames;
|
|
54
|
+
}
|
|
26
55
|
const renames = new Map();
|
|
27
|
-
for (const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
56
|
+
for (const range of policyCfg.ranges) {
|
|
57
|
+
const members = patches
|
|
58
|
+
.filter((p) => p.category === range.category &&
|
|
59
|
+
p.order >= range.from &&
|
|
60
|
+
p.order <= range.to &&
|
|
61
|
+
!isReservedOrder(policyCfg, p.order))
|
|
62
|
+
.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
63
|
+
if (members.length === 0)
|
|
64
|
+
continue;
|
|
65
|
+
// Anchor at the first occupied ordinal rather than range.from: gap
|
|
66
|
+
// evaluation only requires contiguity between first and last occupied,
|
|
67
|
+
// and anchoring minimizes renames.
|
|
68
|
+
let next = members[0].order;
|
|
69
|
+
for (const patch of members) {
|
|
70
|
+
while (isReservedOrder(policyCfg, next))
|
|
71
|
+
next++;
|
|
72
|
+
if (patch.order !== next) {
|
|
73
|
+
renames.set(patch.filename, {
|
|
74
|
+
newOrder: next,
|
|
75
|
+
newFilename: rebuildFilenameForOrder(patch, next),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
next++;
|
|
34
79
|
}
|
|
35
80
|
}
|
|
36
81
|
return renames;
|
|
37
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Patches a range-aware compact leaves in place because they sit outside
|
|
85
|
+
* their category's configured range (or outside all ranges) without a
|
|
86
|
+
* reserved-range exception. They already violate `category-range`; moving
|
|
87
|
+
* them is a policy decision compact must not make silently.
|
|
88
|
+
*/
|
|
89
|
+
function findCompactStrays(patches, policyCfg) {
|
|
90
|
+
return patches.filter((p) => {
|
|
91
|
+
if (isReservedOrder(policyCfg, p.order))
|
|
92
|
+
return false;
|
|
93
|
+
return !policyCfg.ranges.some((range) => range.category === p.category && p.order >= range.from && p.order <= range.to);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
38
96
|
/**
|
|
39
97
|
* Runs the `patch compact` command: renumbers all patches to close ordinal
|
|
40
98
|
* gaps in a single atomic operation.
|
|
@@ -44,16 +102,18 @@ function computeCompactRenameMap(patches) {
|
|
|
44
102
|
*/
|
|
45
103
|
export async function patchCompactCommand(projectRoot, options = {}) {
|
|
46
104
|
intro(options.dryRun ? 'FireForge patch compact (dry run)' : 'FireForge patch compact');
|
|
47
|
-
const paths = getProjectPaths(projectRoot);
|
|
48
105
|
const config = await loadConfig(projectRoot);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
106
|
+
const { paths, manifest } = await requirePatchQueue(projectRoot);
|
|
107
|
+
const policyCfg = config.patchPolicy;
|
|
108
|
+
if (policyCfg && policyCfg.ranges.length > 0) {
|
|
109
|
+
const strays = findCompactStrays(manifest.patches, policyCfg);
|
|
110
|
+
for (const stray of strays) {
|
|
111
|
+
warn(`${stray.filename} (order ${stray.order}, category ${stray.category}) sits outside its ` +
|
|
112
|
+
'configured category range; compact leaves it in place. Use "fireforge patch reorder" ' +
|
|
113
|
+
'to move it into range first.');
|
|
114
|
+
}
|
|
55
115
|
}
|
|
56
|
-
const renameMap = computeCompactRenameMap(manifest.patches);
|
|
116
|
+
const renameMap = computeCompactRenameMap(manifest.patches, policyCfg);
|
|
57
117
|
if (renameMap.size === 0) {
|
|
58
118
|
info('Patch queue is already compact. Nothing to do.');
|
|
59
119
|
outro('Compact complete (no-op)');
|
|
@@ -91,7 +151,7 @@ export async function patchCompactCommand(projectRoot, options = {}) {
|
|
|
91
151
|
if (!currentManifest) {
|
|
92
152
|
throw new GeneralError('Manifest disappeared while waiting for lock.');
|
|
93
153
|
}
|
|
94
|
-
const currentRenameMap = computeCompactRenameMap(currentManifest.patches);
|
|
154
|
+
const currentRenameMap = computeCompactRenameMap(currentManifest.patches, policyCfg);
|
|
95
155
|
if (currentRenameMap.size === 0) {
|
|
96
156
|
info('Patch queue was compacted by another process. Nothing to do.');
|
|
97
157
|
return;
|
|
@@ -138,7 +198,7 @@ export function registerPatchCompact(parent, context) {
|
|
|
138
198
|
const { getProjectRoot, withErrorHandling } = context;
|
|
139
199
|
parent
|
|
140
200
|
.command('compact')
|
|
141
|
-
.description('Close ordinal gaps in the patch queue (
|
|
201
|
+
.description('Close ordinal gaps in the patch queue (range-aware when patchPolicy.ranges is configured)')
|
|
142
202
|
.option('--dry-run', 'Show what would happen without writing')
|
|
143
203
|
.option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
|
|
144
204
|
.option('--force-unsafe', 'Bypass force-mode patchPolicy refusals')
|
|
@@ -8,17 +8,14 @@
|
|
|
8
8
|
* `--dry-run`, and appends to `patches/.fireforge-history.jsonl` on success.
|
|
9
9
|
*/
|
|
10
10
|
import { basename } from 'node:path';
|
|
11
|
-
import { getProjectPaths } from '../../core/config.js';
|
|
12
11
|
import { appendHistory, confirmDestructive } from '../../core/destructive.js';
|
|
13
|
-
import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
|
|
14
12
|
import { buildPatchQueueContext, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, isForwardImportableFile, } from '../../core/patch-lint.js';
|
|
15
13
|
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
16
|
-
import {
|
|
17
|
-
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
14
|
+
import { removePatchFileAndManifest } from '../../core/patch-manifest.js';
|
|
18
15
|
import { toError } from '../../utils/errors.js';
|
|
19
|
-
import { pathExists } from '../../utils/fs.js';
|
|
20
16
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
21
17
|
import { pickDefined } from '../../utils/options.js';
|
|
18
|
+
import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
22
19
|
/**
|
|
23
20
|
* Runs the `patch delete` command: removes a patch file and its manifest
|
|
24
21
|
* row atomically, refusing when a later patch imports a leaf owned by the
|
|
@@ -30,18 +27,10 @@ import { pickDefined } from '../../utils/options.js';
|
|
|
30
27
|
*/
|
|
31
28
|
export async function patchDeleteCommand(projectRoot, identifier, options = {}) {
|
|
32
29
|
intro(options.dryRun ? 'FireForge patch delete (dry run)' : 'FireForge patch delete');
|
|
33
|
-
const paths =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const manifest = await loadPatchesManifest(paths.patches);
|
|
38
|
-
if (!manifest || manifest.patches.length === 0) {
|
|
39
|
-
throw new GeneralError('No patches in manifest.');
|
|
40
|
-
}
|
|
41
|
-
const target = resolvePatchIdentifier(identifier, manifest.patches);
|
|
42
|
-
if (!target) {
|
|
43
|
-
throw new InvalidArgumentError(formatPatchNotFoundError(identifier, manifest.patches), identifier);
|
|
44
|
-
}
|
|
30
|
+
const { paths, manifest } = await requirePatchQueue(projectRoot, {
|
|
31
|
+
missingDirMessage: 'Patches directory not found. No patches to delete.',
|
|
32
|
+
});
|
|
33
|
+
const target = requirePatchTarget(identifier, manifest.patches);
|
|
45
34
|
// Build the full queue context once so we can scan each patch's newFiles
|
|
46
35
|
// without re-parsing for the dependency check below.
|
|
47
36
|
const baseCtx = await buildPatchQueueContext(paths.patches);
|
|
@@ -113,6 +102,17 @@ export async function patchDeleteCommand(projectRoot, identifier, options = {})
|
|
|
113
102
|
break;
|
|
114
103
|
}
|
|
115
104
|
}
|
|
105
|
+
// Staged-dependency declarations on other patches may name the deleted
|
|
106
|
+
// patch as their forward-import owner. The dangling reference also
|
|
107
|
+
// surfaces via cross-patch lint later, but warning here puts the exact
|
|
108
|
+
// cleanup command in front of the operator at decision time.
|
|
109
|
+
const danglingOwnerHolders = baseCtx.entries.filter((entry) => entry.filename !== target.filename &&
|
|
110
|
+
(entry.metadata?.stagedDependencies?.forwardImports ?? []).some((fi) => fi.owner === target.filename));
|
|
111
|
+
for (const holder of danglingOwnerHolders) {
|
|
112
|
+
warn(`${holder.filename} declares a staged dependency with owner ${target.filename}; ` +
|
|
113
|
+
`after the delete, update it via "fireforge patch staged-dependency ${holder.filename} --remove ..." ` +
|
|
114
|
+
'or re-point the owner at the patch that will create the file.');
|
|
115
|
+
}
|
|
116
116
|
const conflicts = dependents.length > 0
|
|
117
117
|
? {
|
|
118
118
|
// Wording deliberately clarifies the *runtime* impact: `git apply`
|
|
@@ -13,6 +13,7 @@ import { registerPatchLintIgnore } from './lint-ignore.js';
|
|
|
13
13
|
import { registerPatchMoveFiles } from './move-files.js';
|
|
14
14
|
import { registerPatchRename } from './rename.js';
|
|
15
15
|
import { registerPatchReorder } from './reorder.js';
|
|
16
|
+
import { registerPatchSplit } from './split.js';
|
|
16
17
|
import { registerPatchStagedDependency } from './staged-dependency.js';
|
|
17
18
|
import { registerPatchTier } from './tier.js';
|
|
18
19
|
/**
|
|
@@ -40,6 +41,7 @@ export function registerPatch(program, context) {
|
|
|
40
41
|
registerPatchMoveFiles(patch, context);
|
|
41
42
|
registerPatchRename(patch, context);
|
|
42
43
|
registerPatchReorder(patch, context);
|
|
44
|
+
registerPatchSplit(patch, context);
|
|
43
45
|
registerPatchStagedDependency(patch, context);
|
|
44
46
|
registerPatchTier(patch, context);
|
|
45
47
|
}
|
|
@@ -19,15 +19,12 @@
|
|
|
19
19
|
* write — important when an operator scripts repeated invocations or
|
|
20
20
|
* runs `--add` and `--remove` back-to-back.
|
|
21
21
|
*/
|
|
22
|
-
import { getProjectPaths } from '../../core/config.js';
|
|
23
22
|
import { appendHistory } from '../../core/destructive.js';
|
|
24
23
|
import { mutatePatchMetadata } from '../../core/patch-export.js';
|
|
25
|
-
import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
|
|
26
|
-
import { loadPatchesManifest, resolvePatchIdentifier } from '../../core/patch-manifest.js';
|
|
27
24
|
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
28
25
|
import { toError } from '../../utils/errors.js';
|
|
29
|
-
import { pathExists } from '../../utils/fs.js';
|
|
30
26
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
27
|
+
import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
31
28
|
/**
|
|
32
29
|
* Computes the post-mutation `lintIgnore` list for a given mode.
|
|
33
30
|
* Returns `undefined` when the result should drop the field from the
|
|
@@ -108,18 +105,8 @@ export async function patchLintIgnoreCommand(projectRoot, identifier, options =
|
|
|
108
105
|
}
|
|
109
106
|
const mode = adding ? 'add' : removing ? 'remove' : 'clear';
|
|
110
107
|
const values = mode === 'add' ? (options.add ?? []) : mode === 'remove' ? (options.remove ?? []) : [];
|
|
111
|
-
const paths =
|
|
112
|
-
|
|
113
|
-
throw new GeneralError('Patches directory not found.');
|
|
114
|
-
}
|
|
115
|
-
const manifest = await loadPatchesManifest(paths.patches);
|
|
116
|
-
if (!manifest || manifest.patches.length === 0) {
|
|
117
|
-
throw new GeneralError('No patches in manifest.');
|
|
118
|
-
}
|
|
119
|
-
const target = resolvePatchIdentifier(identifier, manifest.patches);
|
|
120
|
-
if (!target) {
|
|
121
|
-
throw new InvalidArgumentError(formatPatchNotFoundError(identifier, manifest.patches), identifier);
|
|
122
|
-
}
|
|
108
|
+
const { paths, manifest } = await requirePatchQueue(projectRoot);
|
|
109
|
+
const target = requirePatchTarget(identifier, manifest.patches);
|
|
123
110
|
if (isDryRun) {
|
|
124
111
|
const existing = target.lintIgnore ?? [];
|
|
125
112
|
const projected = applyMode(existing, mode, values) ?? [];
|
|
@@ -116,6 +116,8 @@ export async function patchMoveFilesCommand(projectRoot, fromIdentifier, toIdent
|
|
|
116
116
|
const applyTarget = formatReExportCommand(target.filename, targetAfter, []);
|
|
117
117
|
note(`${dryRunSource}\n${dryRunTarget}`, 'Preview commands');
|
|
118
118
|
note(`${applySource}\n${applyTarget}`, 'Apply commands');
|
|
119
|
+
info('Tip: to move files into a brand-new patch in one transaction (including ' +
|
|
120
|
+
'staged-dependency owner rewrites), use "fireforge patch split" instead.');
|
|
119
121
|
outro('Move plan complete - no changes made');
|
|
120
122
|
}
|
|
121
123
|
/**
|