@hominis/fireforge 0.47.1 → 0.49.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 +83 -0
- package/dist/bin/fireforge.js +5 -0
- package/dist/build-info.json +4 -4
- package/dist/src/cli.js +16 -28
- package/dist/src/commands/build.js +30 -4
- package/dist/src/commands/doctor-engine-eslint.d.ts +3 -0
- package/dist/src/commands/doctor-engine-eslint.js +12 -0
- package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +158 -0
- package/dist/src/commands/doctor-source-pin.js +9 -2
- package/dist/src/commands/doctor.js +4 -0
- package/dist/src/commands/download.js +64 -3
- package/dist/src/commands/furnace/apply.js +10 -4
- package/dist/src/commands/furnace/create-templates.js +0 -3
- package/dist/src/commands/import.js +5 -2
- package/dist/src/commands/lint-per-patch.js +44 -6
- package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
- package/dist/src/commands/lint-per-run-checkjs.js +1 -0
- package/dist/src/commands/lint.js +14 -0
- package/dist/src/commands/manifest.d.ts +1 -2
- package/dist/src/commands/patch/delete.js +18 -4
- package/dist/src/commands/re-export-lint.d.ts +17 -2
- package/dist/src/commands/re-export-lint.js +33 -4
- package/dist/src/commands/re-export-options.js +7 -0
- package/dist/src/commands/re-export-register.js +1 -0
- package/dist/src/commands/re-export-removed.d.ts +4 -0
- package/dist/src/commands/re-export-removed.js +49 -0
- package/dist/src/commands/re-export-single.js +4 -1
- package/dist/src/commands/re-export.js +8 -2
- package/dist/src/commands/rebase/confirm.d.ts +12 -1
- package/dist/src/commands/rebase/confirm.js +6 -3
- package/dist/src/commands/rebase/index.js +77 -13
- package/dist/src/commands/run.js +43 -7
- package/dist/src/commands/status.js +5 -3
- package/dist/src/commands/test-browser-preflight.d.ts +28 -0
- package/dist/src/commands/test-browser-preflight.js +181 -0
- package/dist/src/commands/test-build-phase.js +30 -7
- package/dist/src/commands/test-checkjs.d.ts +3 -0
- package/dist/src/commands/test-checkjs.js +27 -0
- package/dist/src/commands/test-diagnose.js +3 -0
- package/dist/src/commands/test-harness-env.d.ts +5 -6
- package/dist/src/commands/test-harness-env.js +27 -7
- package/dist/src/commands/test-modes.d.ts +2 -4
- package/dist/src/commands/test-register.js +11 -5
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +21 -1
- package/dist/src/commands/test-verdict.d.ts +2 -2
- package/dist/src/commands/test.js +83 -211
- package/dist/src/commands/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- package/dist/src/core/build-baseline-types.d.ts +12 -14
- package/dist/src/core/build-baseline-validate.d.ts +3 -0
- package/dist/src/core/build-baseline-validate.js +53 -0
- package/dist/src/core/build-baseline.d.ts +4 -3
- package/dist/src/core/build-baseline.js +23 -11
- package/dist/src/core/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- package/dist/src/core/component-source-guard.d.ts +20 -0
- package/dist/src/core/component-source-guard.js +68 -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 +7 -0
- package/dist/src/core/coverage-extend.d.ts +5 -34
- package/dist/src/core/coverage-extend.js +26 -37
- package/dist/src/core/discard-baseline.js +2 -1
- package/dist/src/core/engine-write-boundary.d.ts +5 -0
- package/dist/src/core/engine-write-boundary.js +41 -0
- package/dist/src/core/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -0
- package/dist/src/core/furnace-apply-helpers.js +8 -2
- package/dist/src/core/furnace-apply.js +7 -0
- package/dist/src/core/furnace-validate-accessibility.js +9 -12
- package/dist/src/core/furnace-validate.js +0 -43
- package/dist/src/core/gecko-dom-types.d.ts +2 -0
- package/dist/src/core/gecko-dom-types.js +17 -0
- package/dist/src/core/git-file-ops.d.ts +21 -0
- package/dist/src/core/git-file-ops.js +75 -0
- package/dist/src/core/git-status.d.ts +14 -0
- package/dist/src/core/git-status.js +41 -2
- package/dist/src/core/lit-template-regions.d.ts +4 -0
- package/dist/src/core/lit-template-regions.js +50 -0
- package/dist/src/core/mach-build-artifacts.d.ts +6 -6
- package/dist/src/core/mach-build-artifacts.js +13 -7
- package/dist/src/core/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -0
- package/dist/src/core/mach-resource-shim.d.ts +1 -1
- package/dist/src/core/mach-resource-shim.js +2 -0
- package/dist/src/core/marionette-port.d.ts +2 -0
- package/dist/src/core/marionette-port.js +38 -53
- package/dist/src/core/mochitest-server-port.d.ts +1 -1
- package/dist/src/core/mochitest-server-port.js +20 -17
- package/dist/src/core/objdir-maintenance.d.ts +10 -0
- package/dist/src/core/objdir-maintenance.js +151 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
- package/dist/src/core/patch-apply-fuzz.js +27 -6
- package/dist/src/core/patch-apply.d.ts +13 -0
- package/dist/src/core/patch-apply.js +27 -2
- package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
- package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
- package/dist/src/core/patch-lint-checkjs.js +10 -2
- package/dist/src/core/patch-lint-cross.d.ts +7 -1
- package/dist/src/core/patch-lint-cross.js +13 -12
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
- package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
- package/dist/src/core/patch-lint-forward-registration.js +83 -37
- package/dist/src/core/patch-lint.d.ts +11 -0
- package/dist/src/core/patch-lint.js +20 -2
- package/dist/src/core/patch-transform.d.ts +14 -13
- package/dist/src/core/patch-transform.js +29 -17
- package/dist/src/core/preflight-wait.d.ts +2 -0
- package/dist/src/core/preflight-wait.js +24 -0
- package/dist/src/core/process-owner.d.ts +9 -0
- package/dist/src/core/process-owner.js +43 -0
- package/dist/src/core/rebase-dry-run.d.ts +48 -0
- package/dist/src/core/rebase-dry-run.js +105 -0
- package/dist/src/core/run-profile.d.ts +50 -0
- package/dist/src/core/run-profile.js +131 -0
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +3 -1
- package/dist/src/core/status-classify.js +29 -2
- package/dist/src/core/test-harness-crash.d.ts +1 -1
- package/dist/src/core/test-harness-crash.js +5 -5
- package/dist/src/core/test-host-state.d.ts +14 -0
- package/dist/src/core/test-host-state.js +80 -0
- package/dist/src/core/test-profile-files.d.ts +17 -0
- package/dist/src/core/test-profile-files.js +75 -0
- package/dist/src/core/test-stale-check.d.ts +6 -7
- package/dist/src/core/test-stale-check.js +9 -32
- package/dist/src/core/token-category.d.ts +7 -26
- package/dist/src/core/token-category.js +68 -121
- package/dist/src/core/token-inventory.js +12 -7
- package/dist/src/core/toolchain-preflight.d.ts +6 -0
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/typecheck.d.ts +11 -1
- package/dist/src/core/typecheck.js +86 -12
- package/dist/src/errors/rebase.d.ts +9 -0
- package/dist/src/errors/rebase.js +20 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/types/commands/options.d.ts +29 -3
- package/dist/src/types/config.d.ts +10 -0
- package/dist/src/utils/assert.js +2 -7
- package/dist/src/utils/child-env.d.ts +0 -3
- package/dist/src/utils/child-env.js +0 -3
- package/dist/src/utils/concurrency.d.ts +2 -2
- package/dist/src/utils/concurrency.js +15 -4
- package/dist/src/utils/fs.js +11 -1
- package/dist/src/utils/options.d.ts +16 -1
- package/dist/src/utils/options.js +5 -3
- package/dist/src/utils/paths.js +3 -2
- package/dist/src/utils/process-group.d.ts +1 -2
- package/dist/src/utils/process-group.js +1 -2
- package/dist/src/utils/process-input.d.ts +27 -0
- package/dist/src/utils/process-input.js +46 -0
- package/dist/src/utils/process.d.ts +5 -2
- package/dist/src/utils/process.js +21 -8
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +82 -0
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +13 -8
- package/docs/machine-output.md +36 -19
- package/docs/patch-workflow.md +50 -1
- package/docs/testing.md +77 -12
- package/package.json +10 -5
|
@@ -94,6 +94,7 @@ async function runWatchLoop(projectRoot) {
|
|
|
94
94
|
// because the post-apply checksum snapshot already reflects the edit so
|
|
95
95
|
// the 30s poll also sees no diff.
|
|
96
96
|
let pendingChange = false;
|
|
97
|
+
let retryNeeded = false;
|
|
97
98
|
let lastChecksums = new Map();
|
|
98
99
|
let pollTimer = null;
|
|
99
100
|
const runApplyCycle = async () => {
|
|
@@ -102,18 +103,21 @@ async function runWatchLoop(projectRoot) {
|
|
|
102
103
|
info('\nChange detected — re-applying...');
|
|
103
104
|
const result = await runFurnaceMutation(projectRoot, 'apply-rollback', (ctx) => applyAllComponents(projectRoot, false, { operationContext: ctx }));
|
|
104
105
|
logApplyResult(result, false);
|
|
106
|
+
if (result.errors.length > 0 || countEntriesWithBlockingStepErrors(result.applied) > 0) {
|
|
107
|
+
throw new FurnaceError('Watch apply did not complete; retrying on the next poll.');
|
|
108
|
+
}
|
|
105
109
|
const applied = result.applied.length;
|
|
106
110
|
const skipped = result.skipped.length;
|
|
107
111
|
info(`Re-applied: ${applied} applied, ${skipped} skipped`);
|
|
112
|
+
retryNeeded = false;
|
|
113
|
+
lastChecksums = await snapshotWatchedChecksums(watchDirs);
|
|
108
114
|
}
|
|
109
115
|
catch (err) {
|
|
116
|
+
retryNeeded = true;
|
|
110
117
|
warn(classifyWatchApplyError(err));
|
|
111
118
|
}
|
|
112
119
|
finally {
|
|
113
120
|
applyInFlight = false;
|
|
114
|
-
// Update checksums after apply so the next poll does not re-trigger
|
|
115
|
-
// for changes that are already reflected in the engine.
|
|
116
|
-
lastChecksums = await snapshotWatchedChecksums(watchDirs);
|
|
117
121
|
}
|
|
118
122
|
// Another change arrived while we were applying, so run again and the
|
|
119
123
|
// edit is not silently absorbed into the post-apply checksum bump.
|
|
@@ -224,7 +228,9 @@ async function runWatchLoop(projectRoot) {
|
|
|
224
228
|
return;
|
|
225
229
|
}
|
|
226
230
|
const current = await snapshotWatchedChecksums(watchDirs);
|
|
227
|
-
if
|
|
231
|
+
// A failed apply remains dirty even if source checksums did not
|
|
232
|
+
// change. The polling interval bounds retries without a busy loop.
|
|
233
|
+
if (retryNeeded || !checksumMapsEqual(current, lastChecksums)) {
|
|
228
234
|
triggerApply();
|
|
229
235
|
}
|
|
230
236
|
}
|
|
@@ -30,13 +30,16 @@ function isSafeIoFallback(error) {
|
|
|
30
30
|
const code = getNodeErrorCode(error);
|
|
31
31
|
return code !== undefined && SAFE_IO_FALLBACK_CODES.has(code);
|
|
32
32
|
}
|
|
33
|
-
/** Concurrency bound for per-file classification (
|
|
33
|
+
/** Concurrency bound for per-file classification (reads and patch replays). */
|
|
34
34
|
const UNMANAGED_CLASSIFY_CONCURRENCY = 8;
|
|
35
35
|
async function getUnmanagedDirtyFiles(engineDir, patchesDir, dirtyFiles) {
|
|
36
36
|
// One manifest+patch-discovery load for the whole batch (the per-call
|
|
37
37
|
// computePatchedContent re-read everything for every file), and bounded
|
|
38
38
|
// concurrency instead of an unbounded Promise.all over git spawns.
|
|
39
|
-
const { computePatched: computeExpected } = await createPatchedContentContext(patchesDir, engineDir);
|
|
39
|
+
const { computePatched: computeExpected, prefetchBase } = await createPatchedContentContext(patchesDir, engineDir);
|
|
40
|
+
// One `git cat-file --batch` for every HEAD blob instead of a `git show`
|
|
41
|
+
// per dirty file.
|
|
42
|
+
await prefetchBase(dirtyFiles);
|
|
40
43
|
const classifications = await mapWithConcurrency(dirtyFiles, UNMANAGED_CLASSIFY_CONCURRENCY, async (file) => {
|
|
41
44
|
try {
|
|
42
45
|
const [expected, exists] = await Promise.all([
|
|
@@ -138,7 +138,7 @@ const SUPPRESSED_SIZE_CHECKS = new Set([
|
|
|
138
138
|
* filename-prefixed issue rows, and the cache writes, all in the same
|
|
139
139
|
* sequence a serial run produced. Returns the run tallies.
|
|
140
140
|
*/
|
|
141
|
-
async function applyPerPatchResults(subset, results, issues, checkJs, cache) {
|
|
141
|
+
async function applyPerPatchResults(subset, results, issues, checkJs, cache, heldNotices) {
|
|
142
142
|
const totals = {
|
|
143
143
|
linted: 0,
|
|
144
144
|
skipped: 0,
|
|
@@ -190,6 +190,10 @@ async function applyPerPatchResults(subset, results, issues, checkJs, cache) {
|
|
|
190
190
|
for (const suppressedIssue of result.suppressedIssues) {
|
|
191
191
|
if (!SUPPRESSED_SIZE_CHECKS.has(suppressedIssue.check))
|
|
192
192
|
continue;
|
|
193
|
+
if (heldNotices !== undefined) {
|
|
194
|
+
heldNotices.push({ check: suppressedIssue.check, patch: patch.filename, suppressed: true });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
193
197
|
info(`NOTICE [${suppressedIssue.check}] ${patch.filename}: suppressed by lintIgnore — ${suppressedIssue.message}`);
|
|
194
198
|
}
|
|
195
199
|
totals.suppressed += result.suppressedIssues.length;
|
|
@@ -232,7 +236,10 @@ async function lintSubsetConcurrently(subset, lintCtx) {
|
|
|
232
236
|
* prints the per-patch summary, and applies the failure criteria
|
|
233
237
|
* (errors, `--max-warnings`) by throwing GeneralError.
|
|
234
238
|
*/
|
|
235
|
-
function reportPerPatchOutcome(issues, linted, skipped, options) {
|
|
239
|
+
function reportPerPatchOutcome(issues, linted, skipped, options, heldNotices) {
|
|
240
|
+
if (heldNotices !== undefined) {
|
|
241
|
+
reportNoticeSummary(heldNotices, issues);
|
|
242
|
+
}
|
|
236
243
|
if (issues.length === 0) {
|
|
237
244
|
if (linted === 0 && skipped > 0) {
|
|
238
245
|
info(`No patches in the queue have been applied to engine/. Run "fireforge import" first if you want lint findings against the staged hunks; otherwise this is expected.`);
|
|
@@ -247,8 +254,10 @@ function reportPerPatchOutcome(issues, linted, skipped, options) {
|
|
|
247
254
|
const errors = issues.filter((i) => i.severity === 'error');
|
|
248
255
|
const warnings = issues.filter((i) => i.severity === 'warning');
|
|
249
256
|
const notices = issues.filter((i) => i.severity === 'notice');
|
|
250
|
-
|
|
251
|
-
|
|
257
|
+
if (heldNotices === undefined) {
|
|
258
|
+
for (const issue of notices)
|
|
259
|
+
info(`NOTICE ${formatPatchLintIssue(issue)}`);
|
|
260
|
+
}
|
|
252
261
|
for (const issue of warnings)
|
|
253
262
|
warn(formatPatchLintIssue(issue));
|
|
254
263
|
for (const issue of errors)
|
|
@@ -272,6 +281,34 @@ function reportPerPatchOutcome(issues, linted, skipped, options) {
|
|
|
272
281
|
outro('Lint passed');
|
|
273
282
|
}
|
|
274
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* `--notices summary`: one line per check instead of one per finding, for
|
|
286
|
+
* a green queue whose hundreds of NOTICE lines are reviewed waivers and
|
|
287
|
+
* under-threshold measurements. The full list stays in `--report`.
|
|
288
|
+
* Errors and warnings are never summarised.
|
|
289
|
+
*/
|
|
290
|
+
function reportNoticeSummary(heldNotices, issues) {
|
|
291
|
+
const byCheck = new Map();
|
|
292
|
+
const tally = (check, patch, suppressed) => {
|
|
293
|
+
const entry = byCheck.get(check) ?? { count: 0, suppressed: 0, patches: new Set() };
|
|
294
|
+
entry.count += 1;
|
|
295
|
+
if (suppressed)
|
|
296
|
+
entry.suppressed += 1;
|
|
297
|
+
entry.patches.add(patch);
|
|
298
|
+
byCheck.set(check, entry);
|
|
299
|
+
};
|
|
300
|
+
for (const held of heldNotices)
|
|
301
|
+
tally(held.check, held.patch, held.suppressed);
|
|
302
|
+
for (const issue of issues) {
|
|
303
|
+
if (issue.severity !== 'notice')
|
|
304
|
+
continue;
|
|
305
|
+
tally(issue.check, issue.patches?.[0] ?? issue.file.split(' :: ')[0] ?? issue.file, false);
|
|
306
|
+
}
|
|
307
|
+
for (const [check, entry] of [...byCheck].sort(([a], [b]) => a.localeCompare(b))) {
|
|
308
|
+
const suppressed = entry.suppressed > 0 ? `, ${entry.suppressed} of them suppressed by lintIgnore` : '';
|
|
309
|
+
info(`NOTICE [${check}]: ${entry.count} across ${entry.patches.size} patch(es)${suppressed} (see --report)`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
275
312
|
/**
|
|
276
313
|
* Resolves the `--patches <name…>` subset filter against the manifest,
|
|
277
314
|
* matching each requested name tolerantly (exact filename, filename ±
|
|
@@ -394,7 +431,8 @@ async function lintPerPatchInner(projectRoot, paths, options = {}) {
|
|
|
394
431
|
engineHeadSha,
|
|
395
432
|
checkJs,
|
|
396
433
|
});
|
|
397
|
-
const
|
|
434
|
+
const heldNotices = options.notices === 'summary' ? [] : undefined;
|
|
435
|
+
const { linted, skipped, cacheDirty, reusedCacheEntries, suppressed } = await applyPerPatchResults(subset, results, issues, checkJs, cache, heldNotices);
|
|
398
436
|
for (const issue of lintPatchQueue(ctx)) {
|
|
399
437
|
if (isSubset && !subsetTouchedFiles.has(issue.file))
|
|
400
438
|
continue;
|
|
@@ -411,5 +449,5 @@ async function lintPerPatchInner(projectRoot, paths, options = {}) {
|
|
|
411
449
|
if (options.report !== undefined) {
|
|
412
450
|
await writePerPatchLintReport(options.report, subset, results);
|
|
413
451
|
}
|
|
414
|
-
reportPerPatchOutcome(issues, linted, skipped, options);
|
|
452
|
+
reportPerPatchOutcome(issues, linted, skipped, options, heldNotices);
|
|
415
453
|
}
|
|
@@ -52,6 +52,9 @@ export interface PerRunCheckJs {
|
|
|
52
52
|
/** True when this file kind participates in the run's checkJs surface
|
|
53
53
|
* (`.sys.mjs`, plus test scripts when `checkJsTestFiles` is on). */
|
|
54
54
|
isRelevant: (file: string) => boolean;
|
|
55
|
+
/** The program's roots under `rootScopePatches`, or undefined for the
|
|
56
|
+
* whole queue. A file outside it is resolvable but never checked. */
|
|
57
|
+
rootScope: ReadonlySet<string> | undefined;
|
|
55
58
|
}
|
|
56
59
|
/**
|
|
57
60
|
* Builds the per-run checkJs program controller when `patchLint.checkJs` is
|
|
@@ -16,6 +16,7 @@ import { collectDiffFilePaths, tagLintIssues } from '../core/patch-lint-diff-tag
|
|
|
16
16
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
17
17
|
import { pathExists } from '../utils/fs.js';
|
|
18
18
|
import { info, intro, outro, success, warn } from '../utils/logger.js';
|
|
19
|
+
import { addAcceptedWaitLockOption } from '../utils/options.js';
|
|
19
20
|
import { stripEnginePrefix } from '../utils/paths.js';
|
|
20
21
|
import { lintPerPatch } from './lint-per-patch.js';
|
|
21
22
|
/**
|
|
@@ -236,6 +237,9 @@ function validateLintFlags(options) {
|
|
|
236
237
|
if (options.report !== undefined && !options.perPatch) {
|
|
237
238
|
throw new InvalidArgumentError('--report requires --per-patch.', '--report');
|
|
238
239
|
}
|
|
240
|
+
if (options.notices !== undefined && !options.perPatch) {
|
|
241
|
+
throw new InvalidArgumentError('--notices requires --per-patch.', '--notices');
|
|
242
|
+
}
|
|
239
243
|
}
|
|
240
244
|
/**
|
|
241
245
|
* Aggregate-mode patch-size softening: when the linted diff is every
|
|
@@ -499,6 +503,7 @@ export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
|
499
503
|
.option('--max-warnings <n>', 'Fail when lint reports more than <n> warning(s); use 0 for warning-clean release gates.')
|
|
500
504
|
.option('--no-cache', 'Bypass per-patch lint result cache reads and writes.')
|
|
501
505
|
.option('--report <path>', 'With --per-patch, write a machine-readable JSON report (per-patch size metrics, tier, thresholds, issues, and lintIgnore-suppressed issues) to <path>.')
|
|
506
|
+
.option('--notices <mode>', 'With --per-patch: "full" (default) prints every NOTICE line; "summary" prints one line per check with its count and patch count. The full list stays in --report. Errors and warnings always print in full.')
|
|
502
507
|
.action(withErrorHandling(async (paths, options) => {
|
|
503
508
|
const lintOptions = {};
|
|
504
509
|
if (options.since !== undefined) {
|
|
@@ -526,8 +531,16 @@ export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
|
526
531
|
if (options.report !== undefined) {
|
|
527
532
|
lintOptions.report = options.report;
|
|
528
533
|
}
|
|
534
|
+
if (options.notices !== undefined) {
|
|
535
|
+
if (options.notices !== 'summary' && options.notices !== 'full') {
|
|
536
|
+
throw new InvalidArgumentError(`--notices must be "summary" or "full" (got "${options.notices}").`, '--notices');
|
|
537
|
+
}
|
|
538
|
+
lintOptions.notices = options.notices;
|
|
539
|
+
}
|
|
529
540
|
await lintCommand(getProjectRoot(), paths, lintOptions);
|
|
530
541
|
}));
|
|
542
|
+
// This action also has subcommands; all its wait flags are accept-only.
|
|
543
|
+
addAcceptedWaitLockOption(lint);
|
|
531
544
|
const lintCache = lint
|
|
532
545
|
.command('cache')
|
|
533
546
|
.description('Manage the per-patch lint result cache')
|
|
@@ -540,6 +553,7 @@ export function registerLint(program, { getProjectRoot, withErrorHandling }) {
|
|
|
540
553
|
'To lint a directory named cache/, disambiguate with a trailing slash: "fireforge lint cache/".');
|
|
541
554
|
return Promise.resolve();
|
|
542
555
|
}));
|
|
556
|
+
addAcceptedWaitLockOption(lintCache);
|
|
543
557
|
lintCache
|
|
544
558
|
.command('clear')
|
|
545
559
|
.description('Clear cached per-patch lint results')
|
|
@@ -25,8 +25,7 @@ export interface CommandManifestEntry {
|
|
|
25
25
|
*/
|
|
26
26
|
name: string;
|
|
27
27
|
/**
|
|
28
|
-
*
|
|
29
|
-
* documentation. Not surfaced in the CLI itself.
|
|
28
|
+
* Group label used by the root CLI help and generated documentation.
|
|
30
29
|
*/
|
|
31
30
|
group: 'project' | 'workflow' | 'engine' | 'diagnostics' | 'components';
|
|
32
31
|
/** Registers the command (and any subcommands) on the Commander program. */
|
|
@@ -15,6 +15,7 @@ import { appendHistoryBestEffort } from '../../core/history-log.js';
|
|
|
15
15
|
import { buildPatchQueueContext, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, isForwardImportableFile, } from '../../core/patch-lint.js';
|
|
16
16
|
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
17
17
|
import { removePatchFileAndManifest } from '../../core/patch-manifest.js';
|
|
18
|
+
import { GeneralError } from '../../errors/base.js';
|
|
18
19
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
19
20
|
import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../../utils/options.js';
|
|
20
21
|
import { proceedAfterDecision } from '../destructive-decision.js';
|
|
@@ -43,11 +44,12 @@ export async function patchDeleteCommand(projectRoot, identifier, options = {})
|
|
|
43
44
|
// check is built directly from `baseCtx` rather than by diffing lint runs
|
|
44
45
|
// over a projected state.
|
|
45
46
|
const targetEntry = baseCtx.entries.find((e) => e.filename === target.filename);
|
|
47
|
+
if (!targetEntry || JSON.stringify(targetEntry.metadata) !== JSON.stringify(target)) {
|
|
48
|
+
throw new GeneralError('Patch queue changed while preparing deletion. Re-run patch delete to review the current target.');
|
|
49
|
+
}
|
|
46
50
|
const targetNewFileLeaves = new Set();
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
targetNewFileLeaves.add(basename(fullPath));
|
|
50
|
-
}
|
|
51
|
+
for (const fullPath of targetEntry.newFiles.keys()) {
|
|
52
|
+
targetNewFileLeaves.add(basename(fullPath));
|
|
51
53
|
}
|
|
52
54
|
// Scan every later patch's new files and its added lines on pre-existing
|
|
53
55
|
// files for import specifiers that resolve to a leaf owned by the target.
|
|
@@ -161,6 +163,18 @@ export async function patchDeleteCommand(projectRoot, identifier, options = {})
|
|
|
161
163
|
// re-thrown: by that point the mutation has committed, and reporting
|
|
162
164
|
// failure to the caller would mislead.
|
|
163
165
|
await withPatchDirectoryLock(paths.patches, async () => {
|
|
166
|
+
const current = await buildPatchQueueContext(paths.patches);
|
|
167
|
+
const snapshot = (context) => JSON.stringify(context.entries.map(({ filename, order, diff, metadata }) => ({
|
|
168
|
+
filename,
|
|
169
|
+
order,
|
|
170
|
+
diff,
|
|
171
|
+
metadata,
|
|
172
|
+
})));
|
|
173
|
+
// A changed body or manifest invalidates the dependency scan approved
|
|
174
|
+
// before the prompt. Recheck all of its inputs while holding the lock.
|
|
175
|
+
if (snapshot(current) !== snapshot(baseCtx)) {
|
|
176
|
+
throw new GeneralError('Patch queue changed while waiting for confirmation or the lock. Re-run patch delete to review the current dependencies.');
|
|
177
|
+
}
|
|
164
178
|
await removePatchFileAndManifest(paths.patches, target.filename);
|
|
165
179
|
await appendHistoryBestEffort(paths.patches, {
|
|
166
180
|
operation: 'patch-delete',
|
|
@@ -44,8 +44,23 @@ export interface ReExportLintContext {
|
|
|
44
44
|
/** The pre-existing-queue-errors note is printed once per invocation. */
|
|
45
45
|
queueBaselineWarned: boolean;
|
|
46
46
|
}
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Builds the once-per-invocation lint context.
|
|
49
|
+
*
|
|
50
|
+
* The checkJs program is rooted at the patches being re-exported, exactly as
|
|
51
|
+
* `lint --per-patch --patches` roots it at its subset: the rest of the
|
|
52
|
+
* queue stays resolvable, so cross-patch imports type-check identically,
|
|
53
|
+
* but nothing else is parsed or checked. Unscoped, a one-patch re-export
|
|
54
|
+
* that missed the lint cache type-checked the whole queue (76 s on a
|
|
55
|
+
* 401-patch queue) to report on one patch.
|
|
56
|
+
*
|
|
57
|
+
* @param projectRoot - FireForge project root
|
|
58
|
+
* @param paths - Resolved project paths
|
|
59
|
+
* @param config - Loaded configuration
|
|
60
|
+
* @param noCache - Skip the per-patch lint result cache (`--no-cache`)
|
|
61
|
+
* @param scopePatches - Filenames of the patches this run re-exports
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildReExportLintContext(projectRoot: string, paths: ProjectPaths, config: FireForgeConfig, noCache: boolean, scopePatches: ReadonlySet<string>): Promise<ReExportLintContext>;
|
|
49
64
|
/** Fresh lint payload carried to the post-write cache store. */
|
|
50
65
|
export interface ReExportLintResult {
|
|
51
66
|
issues: PatchLintIssue[];
|
|
@@ -32,15 +32,39 @@ import { reportPatchLintOutcome } from './export-shared.js';
|
|
|
32
32
|
import { buildPerRunCheckJs } from './lint-per-run-checkjs.js';
|
|
33
33
|
import { projectEntryBody } from './patch/entry-projection.js';
|
|
34
34
|
import { formatForwardImportRemedy } from './re-export-scan.js';
|
|
35
|
-
/**
|
|
36
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Builds the once-per-invocation lint context.
|
|
37
|
+
*
|
|
38
|
+
* The checkJs program is rooted at the patches being re-exported, exactly as
|
|
39
|
+
* `lint --per-patch --patches` roots it at its subset: the rest of the
|
|
40
|
+
* queue stays resolvable, so cross-patch imports type-check identically,
|
|
41
|
+
* but nothing else is parsed or checked. Unscoped, a one-patch re-export
|
|
42
|
+
* that missed the lint cache type-checked the whole queue (76 s on a
|
|
43
|
+
* 401-patch queue) to report on one patch.
|
|
44
|
+
*
|
|
45
|
+
* @param projectRoot - FireForge project root
|
|
46
|
+
* @param paths - Resolved project paths
|
|
47
|
+
* @param config - Loaded configuration
|
|
48
|
+
* @param noCache - Skip the per-patch lint result cache (`--no-cache`)
|
|
49
|
+
* @param scopePatches - Filenames of the patches this run re-exports
|
|
50
|
+
*/
|
|
51
|
+
export async function buildReExportLintContext(projectRoot, paths, config, noCache, scopePatches) {
|
|
37
52
|
const patchQueueCtx = (await pathExists(paths.patches))
|
|
38
53
|
? await buildPatchQueueContext(paths.patches, config)
|
|
39
54
|
: undefined;
|
|
40
55
|
const checkJs = patchQueueCtx
|
|
41
|
-
? buildPerRunCheckJs(projectRoot, paths, config, patchQueueCtx)
|
|
56
|
+
? buildPerRunCheckJs(projectRoot, paths, config, patchQueueCtx, scopePatches)
|
|
42
57
|
: undefined;
|
|
43
58
|
const cache = noCache ? undefined : await loadPerPatchLintCache(projectRoot);
|
|
59
|
+
if (patchQueueCtx) {
|
|
60
|
+
// The queue rules and the projection below read every patch, so the
|
|
61
|
+
// wait is named rather than left behind one spinner line.
|
|
62
|
+
info(`Lint scope: queue rules over all ${patchQueueCtx.entries.length} patch(es); ` +
|
|
63
|
+
`checkJs rooted at the ${scopePatches.size} re-exported patch(es); ` +
|
|
64
|
+
(cache === undefined
|
|
65
|
+
? 'lint cache off (--no-cache).'
|
|
66
|
+
: `lint cache ${Object.keys(cache.entries).length === 0 ? 'empty' : 'loaded'}.`));
|
|
67
|
+
}
|
|
44
68
|
const engineHeadSha = cache ? await getPerPatchLintCacheHeadSha(paths.engine) : undefined;
|
|
45
69
|
return {
|
|
46
70
|
projectRoot,
|
|
@@ -123,7 +147,12 @@ export async function lintReExportedPatch(args) {
|
|
|
123
147
|
lintOptions.onSuppressed = (suppressed) => suppressedIssues.push(...suppressed);
|
|
124
148
|
if (checkJs) {
|
|
125
149
|
const created = checkJsFilesInDiff(diffContent, checkJs);
|
|
126
|
-
|
|
150
|
+
if (created.size > 0) {
|
|
151
|
+
info(`checkJs: type-checking ${created.size} file(s) of ${patch.filename} (lint cache ${cache ? 'miss' : 'off'}).`);
|
|
152
|
+
}
|
|
153
|
+
// A file the scoped program does not root at would be resolvable but
|
|
154
|
+
// never checked, so it takes the per-patch build too.
|
|
155
|
+
const allKnown = [...created].every((f) => checkJs.resolutionSet.has(f) && (checkJs.rootScope?.has(f) ?? true));
|
|
127
156
|
if (allKnown) {
|
|
128
157
|
// Slice this patch's findings out of the one queue-wide program.
|
|
129
158
|
lintOptions.precomputedCheckJs = await checkJs.sliceFor(created);
|
|
@@ -63,8 +63,15 @@ function validateWhitelistFlagDependencies(options) {
|
|
|
63
63
|
throw new InvalidArgumentError('--expect-unmanaged names approved exceptions to --refuse-adjacent-unmanaged and has no effect without it. Pass --refuse-adjacent-unmanaged, or drop --expect-unmanaged.', '--expect-unmanaged');
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
function validateExpectedRemovalUsage(patches, options) {
|
|
67
|
+
if (options.expectRemoved?.length &&
|
|
68
|
+
(options.scan || options.all || options.files !== undefined || patches.length === 0)) {
|
|
69
|
+
throw new InvalidArgumentError('--expect-removed requires explicit selected patches and the scan-less path (no --scan, --files, or --all).', '--expect-removed');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
66
72
|
/** Validates mutually exclusive `re-export` targeting and metadata options. */
|
|
67
73
|
export function validateReExportOptionCombinations(patches, options) {
|
|
74
|
+
validateExpectedRemovalUsage(patches, options);
|
|
68
75
|
if (options.files !== undefined) {
|
|
69
76
|
if (options.all || options.scan) {
|
|
70
77
|
throw new InvalidArgumentError('--files cannot be combined with --scan or --all.', '--files');
|
|
@@ -30,6 +30,7 @@ export function registerReExport(program, { getProjectRoot, withErrorHandling })
|
|
|
30
30
|
.option('--refuse-foreign-drift', "Refuse a scan-less re-export (non-zero exit, patch not written) when the refreshed body would absorb engine lines not present in the old patch body — protects multi-session checkouts from silently capturing another session's uncommitted edits. Mutually exclusive with --scan and --files.")
|
|
31
31
|
.option('--expect <path>', "With --refuse-foreign-drift, name an engine-relative file whose drift is this session's intended edit: drift confined to --expect files proceeds, drift anywhere else still refuses. Repeatable, and the whitelist applies across EVERY patch in this invocation, not per patch — so pass all the patches and all their --expect paths in one call rather than looping one patch at a time.", ...stringListOption())
|
|
32
32
|
.option('--expect-unmanaged <path>', 'With --refuse-adjacent-unmanaged, name an engine-relative file that is a REVIEWED, recorded exception to the adjacency refusal: an approved unmanaged path is still listed, but no longer refuses. Repeatable, and the carve-out applies across EVERY patch in this invocation, not per patch. Use it to keep the belt armed for everything else instead of dropping --refuse-adjacent-unmanaged entirely.', ...stringListOption())
|
|
33
|
+
.option('--expect-removed <path>', 'Retire an absent file created by the selected patch while keeping scan-less refusals armed. Repeatable.', ...stringListOption())
|
|
33
34
|
.option('--dry-run', 'Show what would change without writing')
|
|
34
35
|
.option('--skip-lint', 'Skip patch lint checks (downgrade errors to warnings)')
|
|
35
36
|
.option('--no-cache', 'Bypass per-patch lint result cache reads and writes for this re-export')
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PatchMetadata, ReExportOptions } from '../types/commands/index.js';
|
|
2
|
+
import type { ProjectPaths } from '../types/config.js';
|
|
3
|
+
/** Validates every retirement before any patch is written; typos never silently grant a carve-out. */
|
|
4
|
+
export declare function validateExpectedRemovals(paths: ProjectPaths, patches: PatchMetadata[], options: ReExportOptions): Promise<string[]>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { lstat } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { listTrackedInHead } from '../core/git-file-ops.js';
|
|
5
|
+
import { parseDiffSections } from '../core/patch-parse.js';
|
|
6
|
+
import { InvalidArgumentError } from '../errors/base.js';
|
|
7
|
+
import { getNodeErrorCode } from '../utils/errors.js';
|
|
8
|
+
import { readText } from '../utils/fs.js';
|
|
9
|
+
import { normalizeEngineRelativeInput } from './re-export-scan.js';
|
|
10
|
+
/** Validates every retirement before any patch is written; typos never silently grant a carve-out. */
|
|
11
|
+
export async function validateExpectedRemovals(paths, patches, options) {
|
|
12
|
+
const removed = [
|
|
13
|
+
...new Set((options.expectRemoved ?? []).map((file) => normalizeEngineRelativeInput(file, '--expect-removed'))),
|
|
14
|
+
];
|
|
15
|
+
if (removed.length === 0)
|
|
16
|
+
return removed;
|
|
17
|
+
const tracked = await listTrackedInHead(paths.engine, removed);
|
|
18
|
+
for (const file of removed) {
|
|
19
|
+
const owners = patches.filter((patch) => patch.filesAffected.includes(file));
|
|
20
|
+
if (owners.length !== 1 ||
|
|
21
|
+
tracked.has(file) ||
|
|
22
|
+
(await removalPathPresent(join(paths.engine, file)))) {
|
|
23
|
+
throw new InvalidArgumentError(`--expect-removed ${file} must be absent, untracked in engine HEAD, and owned by exactly one selected patch.`, '--expect-removed');
|
|
24
|
+
}
|
|
25
|
+
const owner = owners[0];
|
|
26
|
+
if (!owner)
|
|
27
|
+
throw new InvalidArgumentError(`No selected owner for ${file}`, '--expect-removed');
|
|
28
|
+
const body = await readText(join(paths.patches, owner.filename));
|
|
29
|
+
if (!parseDiffSections(body).some((section) => section.targetPath === file && section.isNewFile)) {
|
|
30
|
+
throw new InvalidArgumentError(`--expect-removed ${file} was not created by its selected patch.`, '--expect-removed');
|
|
31
|
+
}
|
|
32
|
+
if (owner.filesAffected.every((path) => removed.includes(path))) {
|
|
33
|
+
throw new InvalidArgumentError('Retiring every file would empty the patch; use patch delete instead.', '--expect-removed');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return removed;
|
|
37
|
+
}
|
|
38
|
+
/** A dangling symlink is still an existing owned path, not an approved absence. */
|
|
39
|
+
async function removalPathPresent(path) {
|
|
40
|
+
try {
|
|
41
|
+
await lstat(path);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (getNodeErrorCode(error) === 'ENOENT')
|
|
46
|
+
return false;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -41,7 +41,10 @@ export async function reExportSinglePatchWithIndexLockRetry(args) {
|
|
|
41
41
|
}
|
|
42
42
|
async function reExportSinglePatch(args) {
|
|
43
43
|
const { patch, paths, manifest, options, isDryRun, config, adjacentCtx, driftCtx, lintCtx } = args;
|
|
44
|
-
|
|
44
|
+
const removed = new Set(options.expectRemoved ?? []);
|
|
45
|
+
let currentFilesAffected = patch.filesAffected.filter((file) => !removed.has(file));
|
|
46
|
+
for (const file of patch.filesAffected.filter((path) => removed.has(path)))
|
|
47
|
+
info(` - ${file} (--expect-removed)`);
|
|
45
48
|
// --- Scan for new/removed files ---
|
|
46
49
|
if (options.scan) {
|
|
47
50
|
const scanResult = await scanPatchFilesForReExport({
|
|
@@ -15,6 +15,7 @@ import { warnUnseenExpectedDrift } from './re-export-drift.js';
|
|
|
15
15
|
import { reExportFilesInPlace } from './re-export-files.js';
|
|
16
16
|
import { buildReExportLintContext, finishReExportLint } from './re-export-lint.js';
|
|
17
17
|
import { applyReExportFilesPositionalFolding, validateReExportOptionCombinations, } from './re-export-options.js';
|
|
18
|
+
import { validateExpectedRemovals } from './re-export-removed.js';
|
|
18
19
|
import { normalizeEngineRelativeInput, normalizeScanFiles } from './re-export-scan.js';
|
|
19
20
|
import { reExportSinglePatchWithIndexLockRetry } from './re-export-single.js';
|
|
20
21
|
async function resolveSelectedPatches(patches, options, manifest) {
|
|
@@ -98,6 +99,9 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
98
99
|
outro('Nothing to re-export');
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
102
|
+
const expectedRemovals = await validateExpectedRemovals(paths, selectedPatches, options);
|
|
103
|
+
if (expectedRemovals.length > 0)
|
|
104
|
+
options = { ...options, expectRemoved: expectedRemovals };
|
|
101
105
|
if (scanFilesByPatch !== undefined) {
|
|
102
106
|
info(`Bulk scan assignments from ${options.scanFilesManifest}`);
|
|
103
107
|
for (const patch of selectedPatches) {
|
|
@@ -132,12 +136,14 @@ export async function reExportCommand(projectRoot, patches, options) {
|
|
|
132
136
|
const driftCtx = buildForeignDriftContext(options);
|
|
133
137
|
// Hoisted lint context, one per run: queue context + checkJs
|
|
134
138
|
// program + per-patch result cache, shared across every loop iteration.
|
|
135
|
-
|
|
139
|
+
// Taken before the context build, so the elapsed time the progress line
|
|
140
|
+
// reports includes it.
|
|
141
|
+
const startedAt = Date.now();
|
|
142
|
+
const lintCtx = await buildReExportLintContext(projectRoot, paths, config, options.noCache === true, new Set(selectedPatches.map((patch) => patch.filename)));
|
|
136
143
|
let reExported = 0;
|
|
137
144
|
const reExportedFilenames = [];
|
|
138
145
|
const failedFilenames = [];
|
|
139
146
|
const progress = spinner('Preparing re-export...');
|
|
140
|
-
const startedAt = Date.now();
|
|
141
147
|
await withDryRunReExportLock(paths.fireforgeDir, isDryRun, async () => withDryRunPurityGuard(paths.engine, paths.patches, isDryRun, async () => {
|
|
142
148
|
for (const [index, patch] of selectedPatches.entries()) {
|
|
143
149
|
const assignedScanFiles = scanFilesByPatch?.get(patch.filename);
|
|
@@ -22,10 +22,21 @@ export interface DirtyEngineConfirmationOptions {
|
|
|
22
22
|
warningMessage: string;
|
|
23
23
|
promptMessage: string;
|
|
24
24
|
cancelMessage: string;
|
|
25
|
+
/**
|
|
26
|
+
* Precomputed dirtiness. A caller that already measured what would be
|
|
27
|
+
* lost (and may count more than the working tree, such as commits made
|
|
28
|
+
* on top of the base) passes it here instead of the `hasChanges` probe.
|
|
29
|
+
*/
|
|
30
|
+
dirty?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Replaces the generic "Engine has uncommitted changes" opening of the
|
|
33
|
+
* non-interactive refusal, so the refusal names what would be lost.
|
|
34
|
+
*/
|
|
35
|
+
refusalDetail?: string;
|
|
25
36
|
}
|
|
26
37
|
/**
|
|
27
38
|
* Checks if the engine has uncommitted changes and prompts for confirmation.
|
|
28
39
|
* Returns true if safe to proceed, false if the user cancelled.
|
|
29
40
|
* Throws in non-interactive mode without --yes.
|
|
30
41
|
*/
|
|
31
|
-
export declare function confirmDirtyEngineReset({ engineDir, yes, nonInteractiveCommand, argumentName, warningMessage, promptMessage, cancelMessage, }: DirtyEngineConfirmationOptions): Promise<boolean>;
|
|
42
|
+
export declare function confirmDirtyEngineReset({ engineDir, yes, nonInteractiveCommand, argumentName, warningMessage, promptMessage, cancelMessage, dirty, refusalDetail, }: DirtyEngineConfirmationOptions): Promise<boolean>;
|
|
@@ -12,13 +12,16 @@ import { cancel, isCancel, warn } from '../../utils/logger.js';
|
|
|
12
12
|
* Returns true if safe to proceed, false if the user cancelled.
|
|
13
13
|
* Throws in non-interactive mode without --yes.
|
|
14
14
|
*/
|
|
15
|
-
export async function confirmDirtyEngineReset({ engineDir, yes, nonInteractiveCommand, argumentName, warningMessage, promptMessage, cancelMessage, }) {
|
|
16
|
-
if (!(await hasChanges(engineDir))
|
|
15
|
+
export async function confirmDirtyEngineReset({ engineDir, yes, nonInteractiveCommand, argumentName, warningMessage, promptMessage, cancelMessage, dirty, refusalDetail, }) {
|
|
16
|
+
if (yes || !(dirty ?? (await hasChanges(engineDir)))) {
|
|
17
17
|
return true;
|
|
18
18
|
}
|
|
19
19
|
const isInteractive = stdioIsInteractive();
|
|
20
20
|
if (!isInteractive) {
|
|
21
|
-
|
|
21
|
+
const opening = refusalDetail !== undefined
|
|
22
|
+
? `${refusalDetail} Interactive confirmation is not available.`
|
|
23
|
+
: 'Engine has uncommitted changes and interactive confirmation is not available.';
|
|
24
|
+
throw new InvalidArgumentError(`${opening} Run: ${nonInteractiveCommand}`, argumentName);
|
|
22
25
|
}
|
|
23
26
|
warn(warningMessage);
|
|
24
27
|
const confirmed = await confirm({
|