@hominis/fireforge 0.50.0 → 0.52.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 +67 -0
- package/dist/build-info.json +4 -4
- package/dist/src/commands/check-report.d.ts +17 -4
- package/dist/src/commands/check-report.js +19 -6
- package/dist/src/commands/check-run.d.ts +5 -0
- package/dist/src/commands/check-run.js +9 -1
- package/dist/src/commands/check-steps.d.ts +8 -0
- package/dist/src/commands/check-steps.js +31 -6
- package/dist/src/commands/check.js +1 -1
- package/dist/src/commands/declarations-report.d.ts +43 -0
- package/dist/src/commands/declarations-report.js +76 -0
- package/dist/src/commands/export-placement-gate.js +8 -0
- package/dist/src/commands/furnace/rename-registrations.js +7 -2
- package/dist/src/commands/furnace/validate.js +26 -1
- package/dist/src/commands/lint-per-patch.js +6 -2
- package/dist/src/commands/lint-per-run-checkjs.js +2 -2
- package/dist/src/commands/lint-report.js +7 -2
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/park.d.ts +22 -0
- package/dist/src/commands/park.js +232 -0
- package/dist/src/commands/patch/delete.js +11 -6
- package/dist/src/commands/patch/index.d.ts +3 -3
- package/dist/src/commands/patch/index.js +8 -4
- package/dist/src/commands/patch/lint-ignore-from.d.ts +44 -0
- package/dist/src/commands/patch/lint-ignore-from.js +142 -0
- package/dist/src/commands/patch/lint-ignore.js +39 -29
- package/dist/src/commands/patch/move-files-create.js +11 -4
- package/dist/src/commands/patch/move-files-into.js +10 -4
- package/dist/src/commands/patch/move-files.js +3 -1
- package/dist/src/commands/patch/removal-created-files.d.ts +48 -0
- package/dist/src/commands/patch/removal-created-files.js +83 -0
- package/dist/src/commands/patch/rename.js +15 -3
- package/dist/src/commands/patch/reserve.d.ts +33 -0
- package/dist/src/commands/patch/reserve.js +152 -0
- package/dist/src/commands/patch/retire.js +11 -6
- package/dist/src/commands/patch/split-plan.d.ts +10 -1
- package/dist/src/commands/patch/split-plan.js +16 -1
- package/dist/src/commands/patch/split.js +14 -5
- package/dist/src/commands/patch/stage.d.ts +34 -0
- package/dist/src/commands/patch/stage.js +153 -0
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +31 -0
- package/dist/src/commands/patch/staged-dependency-remove.js +80 -0
- package/dist/src/commands/patch/staged-dependency.js +40 -98
- package/dist/src/commands/re-export-adjacent.d.ts +5 -0
- package/dist/src/commands/re-export-adjacent.js +4 -3
- package/dist/src/commands/re-export-bulk-scan.d.ts +2 -1
- package/dist/src/commands/re-export-bulk-scan.js +22 -7
- package/dist/src/commands/re-export-hunks.d.ts +35 -0
- package/dist/src/commands/re-export-hunks.js +220 -0
- package/dist/src/commands/re-export-options.js +29 -2
- package/dist/src/commands/re-export-purity-scope.d.ts +18 -0
- package/dist/src/commands/re-export-purity-scope.js +131 -0
- package/dist/src/commands/re-export-register.js +15 -1
- package/dist/src/commands/re-export-removed.d.ts +1 -1
- package/dist/src/commands/re-export-removed.js +17 -2
- package/dist/src/commands/re-export-scan.d.ts +2 -0
- package/dist/src/commands/re-export-scan.js +4 -1
- package/dist/src/commands/re-export-single.js +102 -42
- package/dist/src/commands/re-export-stale-registrations.d.ts +18 -0
- package/dist/src/commands/re-export-stale-registrations.js +62 -0
- package/dist/src/commands/re-export.js +35 -34
- package/dist/src/commands/status-output.js +5 -1
- package/dist/src/commands/test-build-furnace.d.ts +3 -2
- package/dist/src/commands/test-build-furnace.js +8 -2
- package/dist/src/commands/test-build-phase.d.ts +2 -0
- package/dist/src/commands/test-build-phase.js +34 -17
- package/dist/src/commands/test-checkjs.js +3 -2
- package/dist/src/commands/test-dispatch.d.ts +39 -0
- package/dist/src/commands/test-dispatch.js +220 -0
- package/dist/src/commands/test-furnace-reach.d.ts +37 -0
- package/dist/src/commands/test-furnace-reach.js +132 -0
- package/dist/src/commands/test-orchestrate.d.ts +14 -0
- package/dist/src/commands/test-orchestrate.js +25 -4
- package/dist/src/commands/test-perf-scope.d.ts +21 -0
- package/dist/src/commands/test-perf-scope.js +55 -0
- package/dist/src/commands/test-prelock.js +100 -4
- package/dist/src/commands/test-register.js +18 -3
- package/dist/src/commands/test-results.d.ts +62 -0
- package/dist/src/commands/test-results.js +62 -0
- package/dist/src/commands/test-run.d.ts +13 -55
- package/dist/src/commands/test-run.js +73 -101
- package/dist/src/commands/test-shards.d.ts +110 -0
- package/dist/src/commands/test-shards.js +255 -0
- package/dist/src/commands/test-stale-gate.d.ts +5 -1
- package/dist/src/commands/test-stale-gate.js +11 -2
- package/dist/src/commands/test-verdict.d.ts +2 -1
- package/dist/src/commands/test.d.ts +1 -1
- package/dist/src/commands/test.js +68 -75
- package/dist/src/commands/token-lint.js +48 -8
- package/dist/src/commands/verify.js +9 -2
- package/dist/src/core/build-baseline-types.d.ts +13 -0
- package/dist/src/core/build-baseline-validate.js +9 -0
- package/dist/src/core/build-baseline.js +7 -0
- package/dist/src/core/build-prepare.d.ts +6 -0
- package/dist/src/core/build-prepare.js +29 -3
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-check.js +83 -7
- package/dist/src/core/config-validate-test-toolchains.js +14 -0
- package/dist/src/core/config-validate-token-scales.d.ts +3 -0
- package/dist/src/core/config-validate-token-scales.js +47 -5
- package/dist/src/core/config-validate.js +6 -0
- package/dist/src/core/coverage-extend.js +2 -2
- package/dist/src/core/coverage-records.d.ts +11 -0
- package/dist/src/core/coverage-records.js +34 -0
- package/dist/src/core/engine-session-lock.d.ts +6 -0
- package/dist/src/core/engine-session-lock.js +10 -2
- package/dist/src/core/file-lock-shared.d.ts +57 -0
- package/dist/src/core/file-lock-shared.js +117 -0
- package/dist/src/core/file-lock.d.ts +9 -0
- package/dist/src/core/file-lock.js +58 -2
- package/dist/src/core/furnace-apply.js +10 -10
- package/dist/src/core/furnace-config-validate-exceptions.d.ts +11 -3
- package/dist/src/core/furnace-config-validate-exceptions.js +59 -8
- package/dist/src/core/furnace-config-validate.js +7 -1
- package/dist/src/core/furnace-registration-jar-inplace.d.ts +12 -0
- package/dist/src/core/furnace-registration-jar-inplace.js +80 -0
- package/dist/src/core/furnace-validate-accessibility.d.ts +5 -3
- package/dist/src/core/furnace-validate-accessibility.js +75 -30
- package/dist/src/core/furnace-validate-exceptions.d.ts +13 -4
- package/dist/src/core/furnace-validate-exceptions.js +59 -4
- package/dist/src/core/furnace-validate-icon-button.js +30 -1
- package/dist/src/core/history-log-file.js +8 -1
- package/dist/src/core/line-prefixer.d.ts +18 -0
- package/dist/src/core/line-prefixer.js +34 -0
- package/dist/src/core/mach.d.ts +7 -0
- package/dist/src/core/mach.js +24 -9
- package/dist/src/core/module-resolution-preflight.d.ts +19 -3
- package/dist/src/core/module-resolution-preflight.js +24 -6
- package/dist/src/core/negative-control-compiled.d.ts +36 -0
- package/dist/src/core/negative-control-compiled.js +87 -0
- package/dist/src/core/negative-control-diff.d.ts +14 -2
- package/dist/src/core/negative-control-diff.js +49 -6
- package/dist/src/core/negative-control.d.ts +12 -0
- package/dist/src/core/negative-control.js +48 -5
- package/dist/src/core/park-store.d.ts +63 -0
- package/dist/src/core/park-store.js +216 -0
- package/dist/src/core/patch-export-update.d.ts +4 -0
- package/dist/src/core/patch-export-update.js +12 -3
- package/dist/src/core/patch-export.d.ts +3 -2
- package/dist/src/core/patch-export.js +21 -14
- package/dist/src/core/patch-files.js +26 -8
- package/dist/src/core/patch-lint-checkjs-any-import.d.ts +6 -4
- package/dist/src/core/patch-lint-checkjs-any-import.js +9 -7
- package/dist/src/core/patch-lint-checkjs-diagnostics.d.ts +17 -0
- package/dist/src/core/patch-lint-checkjs-diagnostics.js +32 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +10 -1
- package/dist/src/core/patch-lint-checkjs.js +13 -3
- package/dist/src/core/patch-lint-cross.d.ts +3 -1
- package/dist/src/core/patch-lint-cross.js +5 -0
- package/dist/src/core/patch-lint-css.js +14 -10
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +5 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +1 -1
- package/dist/src/core/patch-lint-ignore.d.ts +56 -12
- package/dist/src/core/patch-lint-ignore.js +173 -43
- package/dist/src/core/patch-lint-ownership.d.ts +10 -0
- package/dist/src/core/patch-lint-ownership.js +26 -0
- package/dist/src/core/patch-lint-size.js +9 -1
- package/dist/src/core/patch-lint-token-scale.d.ts +26 -8
- package/dist/src/core/patch-lint-token-scale.js +206 -29
- package/dist/src/core/patch-lint.js +2 -2
- package/dist/src/core/patch-manifest-io.js +8 -1
- package/dist/src/core/patch-manifest-key-order.d.ts +24 -0
- package/dist/src/core/patch-manifest-key-order.js +61 -0
- package/dist/src/core/patch-manifest-stage.d.ts +46 -0
- package/dist/src/core/patch-manifest-stage.js +96 -0
- package/dist/src/core/patch-manifest-validate.js +27 -1
- package/dist/src/core/patch-order-legal.d.ts +2 -1
- package/dist/src/core/patch-order-legal.js +7 -2
- package/dist/src/core/patch-policy.d.ts +6 -2
- package/dist/src/core/patch-policy.js +14 -7
- package/dist/src/core/patch-retired.d.ts +26 -4
- package/dist/src/core/patch-retired.js +40 -2
- package/dist/src/core/run-log.d.ts +6 -0
- package/dist/src/core/run-log.js +17 -1
- package/dist/src/core/test-harness-crash.d.ts +2 -1
- package/dist/src/core/test-harness-crash.js +5 -8
- package/dist/src/core/test-harness-signature.d.ts +16 -0
- package/dist/src/core/test-harness-signature.js +19 -1
- package/dist/src/core/test-raw-log.d.ts +66 -0
- package/dist/src/core/test-raw-log.js +122 -0
- package/dist/src/core/test-run-completeness.d.ts +47 -7
- package/dist/src/core/test-run-completeness.js +70 -23
- package/dist/src/core/test-stale-check.d.ts +10 -1
- package/dist/src/core/test-stale-check.js +15 -1
- package/dist/src/core/test-xpcshell-retry.d.ts +2 -0
- package/dist/src/core/test-xpcshell-retry.js +2 -1
- package/dist/src/core/token-lint.d.ts +5 -2
- package/dist/src/core/token-lint.js +8 -3
- package/dist/src/core/token-sibling-sheets.d.ts +46 -0
- package/dist/src/core/token-sibling-sheets.js +119 -0
- package/dist/src/core/tree-guard.js +2 -0
- package/dist/src/core/typecheck-shim.d.ts +6 -1
- package/dist/src/core/typecheck-shim.js +139 -2
- package/dist/src/core/typecheck.js +3 -1
- package/dist/src/index.d.ts +9 -3
- package/dist/src/index.js +4 -1
- package/dist/src/types/commands/index.d.ts +2 -2
- package/dist/src/types/commands/options.d.ts +68 -7
- package/dist/src/types/commands/patches.d.ts +43 -0
- package/dist/src/types/config.d.ts +74 -2
- package/dist/src/types/furnace.d.ts +29 -0
- package/dist/src/types/index.d.ts +1 -1
- package/dist/src/utils/options.d.ts +3 -2
- package/dist/src/utils/options.js +9 -4
- package/docs/configuration.md +110 -9
- package/docs/furnace.md +61 -1
- package/docs/lifecycle-invariants.md +13 -11
- package/docs/machine-output.md +68 -18
- package/docs/patch-workflow.md +174 -13
- package/docs/testing.md +89 -5
- package/package.json +3 -3
|
@@ -28,6 +28,7 @@ import { info, intro, outro } from '../../utils/logger.js';
|
|
|
28
28
|
import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../../utils/options.js';
|
|
29
29
|
import { proceedAfterDecision } from '../destructive-decision.js';
|
|
30
30
|
import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
31
|
+
import { deleteCreatedFiles, planClaimedFiles, withCreatedFileLock, } from './removal-created-files.js';
|
|
31
32
|
import { collectRemovalDependents, queueSnapshot, removalConflict, warnDanglingStagedOwners, } from './removal-dependents.js';
|
|
32
33
|
/**
|
|
33
34
|
* Runs the `patch retire` command.
|
|
@@ -49,16 +50,14 @@ export async function patchRetireCommand(projectRoot, identifier, options = {})
|
|
|
49
50
|
});
|
|
50
51
|
warnDanglingStagedOwners(baseCtx, target, 'retire');
|
|
51
52
|
const order = formatPatchOrder(target.order);
|
|
53
|
+
const claimed = await planClaimedFiles(baseCtx, target, paths.engine, options.deleteCreated === true);
|
|
52
54
|
const summary = [
|
|
53
55
|
`retire ${target.filename} (category: ${target.category}, order: ${order})`,
|
|
54
56
|
`description: ${target.description || '(none)'}`,
|
|
55
57
|
`order ${order} stays spent as a retired slot in patches.json; ` +
|
|
56
58
|
`"fireforge export --order ${order}" can fill it later`,
|
|
57
|
-
|
|
59
|
+
...claimed.summary,
|
|
58
60
|
];
|
|
59
|
-
for (const file of target.filesAffected) {
|
|
60
|
-
summary.push(` ${file} → will become unmanaged`);
|
|
61
|
-
}
|
|
62
61
|
const decision = await confirmDestructive({
|
|
63
62
|
operation: 'patch-retire',
|
|
64
63
|
title: `Retire ${target.filename}`,
|
|
@@ -80,25 +79,30 @@ export async function patchRetireCommand(projectRoot, identifier, options = {})
|
|
|
80
79
|
// Same locking and history contract as `patch delete`: the dependency
|
|
81
80
|
// scan approved before the prompt is re-checked under the lock, and the
|
|
82
81
|
// history record is appended inside it.
|
|
83
|
-
|
|
82
|
+
const waitLockSeconds = resolveWaitLockSeconds(options.waitLock);
|
|
83
|
+
await withCreatedFileLock(projectRoot, 'patch retire', claimed, waitLockSeconds, () => withPatchDirectoryLock(paths.patches, async () => {
|
|
84
84
|
const current = await buildPatchQueueContext(paths.patches);
|
|
85
85
|
if (queueSnapshot(current) !== queueSnapshot(baseCtx)) {
|
|
86
86
|
throw new GeneralError('Patch queue changed while waiting for confirmation or the lock. Re-run patch retire to review the current dependencies.');
|
|
87
87
|
}
|
|
88
88
|
await removePatchFileAndManifest(paths.patches, target.filename, slot);
|
|
89
|
+
await deleteCreatedFiles(paths.engine, claimed.deleteOnCommit);
|
|
89
90
|
await appendHistoryBestEffort(paths.patches, {
|
|
90
91
|
operation: 'patch-retire',
|
|
91
92
|
args: {
|
|
92
93
|
filename: target.filename,
|
|
93
94
|
order: target.order,
|
|
94
95
|
filesAffected: target.filesAffected,
|
|
96
|
+
...(claimed.deleteOnCommit.length > 0
|
|
97
|
+
? { deletedCreatedFiles: claimed.deleteOnCommit }
|
|
98
|
+
: {}),
|
|
95
99
|
...(options.reason !== undefined ? { reason: options.reason } : {}),
|
|
96
100
|
},
|
|
97
101
|
...(options.yes === true ? { yes: true } : {}),
|
|
98
102
|
...(options.forceUnsafe === true ? { unsafeOverride: true } : {}),
|
|
99
103
|
result: 'ok',
|
|
100
104
|
}, `patch retire committed (${target.filename})`);
|
|
101
|
-
}, { waitLockSeconds
|
|
105
|
+
}, { waitLockSeconds, command: 'patch retire' }));
|
|
102
106
|
info(`Retired ${target.filename}; order ${order} is kept as a retired slot.`);
|
|
103
107
|
if ((await loadConfig(projectRoot)).patchPolicy?.allowGaps !== false) {
|
|
104
108
|
info('patchPolicy.allowGaps is not false, so no check reads the retired slot; ' +
|
|
@@ -121,6 +125,7 @@ export function registerPatchRetire(parent, context) {
|
|
|
121
125
|
.option('--reason <text>', 'Rationale recorded on the retired slot')
|
|
122
126
|
.option('--dry-run', 'Show what would happen without writing')
|
|
123
127
|
.option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
|
|
128
|
+
.option('--delete-created', 'Also delete the engine files this patch creates that are still on disk, instead of leaving them unmanaged')
|
|
124
129
|
.option('--force-unsafe', 'Bypass the refusal when a later patch depends on this patch (last resort)');
|
|
125
130
|
addWaitLockOption(command).action(withErrorHandling(async (name, options) => {
|
|
126
131
|
await patchRetireCommand(getProjectRoot(), name, pickDefined(options));
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { type ConflictReport } from '../../core/destructive.js';
|
|
9
9
|
import type { PatchQueueContext } from '../../core/patch-lint-cross.js';
|
|
10
|
-
import type { PatchStagedForwardImport } from '../../types/commands/index.js';
|
|
10
|
+
import type { PatchLintIssue, PatchStagedForwardImport } from '../../types/commands/index.js';
|
|
11
11
|
import type { PatchCategory, PatchesManifest, PatchMetadata } from '../../types/commands/index.js';
|
|
12
12
|
import type { FireForgeConfig } from '../../types/config.js';
|
|
13
|
+
import type { DeclarationRenamer } from '../declarations-report.js';
|
|
13
14
|
import { type PlacementPlan } from '../export-flow.js';
|
|
14
15
|
import { type SplitStagedMaintenance } from './split-staged.js';
|
|
15
16
|
/** Everything the commit step needs, computed and confirmed up front. */
|
|
@@ -68,6 +69,7 @@ export declare function mergeStagedForwardImports(patch: PatchMetadata, decls: r
|
|
|
68
69
|
*/
|
|
69
70
|
export declare function runProjectedSplitLint(plan: SplitPlan, baseCtx: PatchQueueContext): {
|
|
70
71
|
conflicts: ConflictReport | null;
|
|
72
|
+
regressions: PatchLintIssue[];
|
|
71
73
|
stagedDependencyAdditions: Map<string, PatchStagedForwardImport[]>;
|
|
72
74
|
stagedMaintenance: SplitStagedMaintenance;
|
|
73
75
|
};
|
|
@@ -86,3 +88,10 @@ export declare function buildNewPatchMetadata(plan: SplitPlan, config: FireForge
|
|
|
86
88
|
* shown by `--dry-run` and by the destructive-operation confirmation prompt.
|
|
87
89
|
*/
|
|
88
90
|
export declare function buildSplitSummary(plan: SplitPlan): string[];
|
|
91
|
+
/**
|
|
92
|
+
* Maps a declaration the split projection proposes back onto the queue as
|
|
93
|
+
* it stands: renumbered patches take their current names, and an owner
|
|
94
|
+
* that is the patch the split would create is dropped (it does not exist
|
|
95
|
+
* until the split runs).
|
|
96
|
+
*/
|
|
97
|
+
export declare function splitDeclarationRenamer(plan: SplitPlan): DeclarationRenamer;
|
|
@@ -215,7 +215,7 @@ export function runProjectedSplitLint(plan, baseCtx) {
|
|
|
215
215
|
reason: `split would introduce ${regressions.length} cross-patch lint error(s)`,
|
|
216
216
|
details: regressions.map(formatPatchLintIssue),
|
|
217
217
|
};
|
|
218
|
-
return { conflicts, stagedDependencyAdditions, stagedMaintenance };
|
|
218
|
+
return { conflicts, regressions, stagedDependencyAdditions, stagedMaintenance };
|
|
219
219
|
}
|
|
220
220
|
/** Builds the projected manifest for policy enforcement. */
|
|
221
221
|
export function projectSplitManifest(manifest, plan, newMetadata) {
|
|
@@ -272,3 +272,18 @@ export function buildSplitSummary(plan) {
|
|
|
272
272
|
summary.push(...describeStagedMaintenance(plan.stagedMaintenance, plan.source.filename, plan.placement.newFilename));
|
|
273
273
|
return summary;
|
|
274
274
|
}
|
|
275
|
+
/**
|
|
276
|
+
* Maps a declaration the split projection proposes back onto the queue as
|
|
277
|
+
* it stands: renumbered patches take their current names, and an owner
|
|
278
|
+
* that is the patch the split would create is dropped (it does not exist
|
|
279
|
+
* until the split runs).
|
|
280
|
+
*/
|
|
281
|
+
export function splitDeclarationRenamer(plan) {
|
|
282
|
+
const oldNameByNew = new Map([...plan.placement.renameMap].map(([oldName, entry]) => [entry.newFilename, oldName]));
|
|
283
|
+
const current = (name) => oldNameByNew.get(name) ?? name;
|
|
284
|
+
return ({ owner, ...declaration }) => ({
|
|
285
|
+
...declaration,
|
|
286
|
+
patch: current(declaration.patch),
|
|
287
|
+
...(owner !== plan.placement.newFilename ? { owner: current(owner) } : {}),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
@@ -33,10 +33,11 @@ import { readText, removeFile, writeText } from '../../utils/fs.js';
|
|
|
33
33
|
import { info, intro, outro, success, warn } from '../../utils/logger.js';
|
|
34
34
|
import { addWaitLockOption, commanderArgParser, pickDefined, resolveWaitLockSeconds, } from '../../utils/options.js';
|
|
35
35
|
import { normalizePatchDisplayName, parsePositiveIntegerFlag } from '../../utils/validation.js';
|
|
36
|
+
import { withDeclarationsReport } from '../declarations-report.js';
|
|
36
37
|
import { proceedAfterDecision } from '../destructive-decision.js';
|
|
37
38
|
import { placementPlansEqual, resolvePlacementPlan } from '../export-flow.js';
|
|
38
39
|
import { runPatchLint } from '../export-shared.js';
|
|
39
|
-
import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, mergeStagedForwardImports, projectSplitManifest, runProjectedSplitLint, } from './split-plan.js';
|
|
40
|
+
import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, mergeStagedForwardImports, projectSplitManifest, runProjectedSplitLint, splitDeclarationRenamer, } from './split-plan.js';
|
|
40
41
|
import { describeStagedMaintenance, emptyStagedMaintenance, findOwnerRewriteHolders, rewriteSplitOwners, withoutDroppedDeclarations, withoutRelocatedDeclarations, } from './split-staged.js';
|
|
41
42
|
/**
|
|
42
43
|
* History payload for the declarations a split re-homed or dropped, omitted
|
|
@@ -285,9 +286,15 @@ export async function patchSplitCommand(projectRoot, sourceId, options) {
|
|
|
285
286
|
ignoreChecks,
|
|
286
287
|
patchTier: source.tier,
|
|
287
288
|
});
|
|
288
|
-
const
|
|
289
|
-
plan.stagedDependencyAdditions = stagedDependencyAdditions;
|
|
290
|
-
plan.stagedMaintenance = stagedMaintenance;
|
|
289
|
+
const projected = runProjectedSplitLint(plan, patchQueueCtx);
|
|
290
|
+
plan.stagedDependencyAdditions = projected.stagedDependencyAdditions;
|
|
291
|
+
plan.stagedMaintenance = projected.stagedMaintenance;
|
|
292
|
+
const conflicts = await withDeclarationsReport(projected.conflicts, {
|
|
293
|
+
projectRoot,
|
|
294
|
+
regressions: projected.regressions,
|
|
295
|
+
out: options.declarationsOut,
|
|
296
|
+
rename: splitDeclarationRenamer(plan),
|
|
297
|
+
});
|
|
291
298
|
const newMetadata = buildNewPatchMetadata(plan, config);
|
|
292
299
|
const decision = await confirmDestructive({
|
|
293
300
|
operation: 'patch-split',
|
|
@@ -323,7 +330,8 @@ export function registerPatchSplit(parent, context) {
|
|
|
323
330
|
.option('--dry-run', 'Show what would happen without writing')
|
|
324
331
|
.option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
|
|
325
332
|
.option('--force-unsafe', 'Bypass projected-lint refusals')
|
|
326
|
-
.option('--skip-lint', 'Skip per-patch lint of the projected bodies')
|
|
333
|
+
.option('--skip-lint', 'Skip per-patch lint of the projected bodies')
|
|
334
|
+
.option('--declarations-out <path>', 'Where a refusal writes its one --from-ready staged-declaration report (default: .fireforge/declarations/<timestamp>.json)');
|
|
327
335
|
addWaitLockOption(command).action(withErrorHandling(async (sourceId, options) => {
|
|
328
336
|
await patchSplitCommand(getProjectRoot(), sourceId, {
|
|
329
337
|
files: options.files,
|
|
@@ -338,6 +346,7 @@ export function registerPatchSplit(parent, context) {
|
|
|
338
346
|
yes: options.yes,
|
|
339
347
|
forceUnsafe: options.forceUnsafe,
|
|
340
348
|
skipLint: options.skipLint,
|
|
349
|
+
declarationsOut: options.declarationsOut,
|
|
341
350
|
waitLock: options.waitLock,
|
|
342
351
|
}),
|
|
343
352
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fireforge patch stage <name>...`: stages the named patches' `.patch`
|
|
3
|
+
* files and only their rows of `patches/patches.json` in the project
|
|
4
|
+
* repository's git index.
|
|
5
|
+
*
|
|
6
|
+
* The case it exists for: several sessions edit one checkout's queue, and
|
|
7
|
+
* each must commit only its own patches. Staging the whole manifest takes
|
|
8
|
+
* every session's rows; staging one owner's hunks of it with
|
|
9
|
+
* `git apply --cached --unidiff-zero` placed a kept insertion at its
|
|
10
|
+
* new-side line number once a dropped hunk above had shifted the file, and
|
|
11
|
+
* an invalid manifest was committed. This command builds the staged
|
|
12
|
+
* manifest from HEAD's rows plus the named working rows (see
|
|
13
|
+
* `core/patch-manifest-stage.ts`), validates it, and writes it straight to
|
|
14
|
+
* the index with `git hash-object` + `git update-index`. The working copy
|
|
15
|
+
* is never touched, and nothing is committed.
|
|
16
|
+
*/
|
|
17
|
+
import { Command } from 'commander';
|
|
18
|
+
import type { CommandContext } from '../../types/cli.js';
|
|
19
|
+
import type { PatchStageOptions } from '../../types/commands/index.js';
|
|
20
|
+
/**
|
|
21
|
+
* Runs the `patch stage` command.
|
|
22
|
+
*
|
|
23
|
+
* @param projectRoot - Project root directory (a git working tree)
|
|
24
|
+
* @param identifiers - Patch filenames, ordinals or names to stage
|
|
25
|
+
* @param options - Command options
|
|
26
|
+
*/
|
|
27
|
+
export declare function patchStageCommand(projectRoot: string, identifiers: readonly string[], options?: PatchStageOptions): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Registers the `patch stage` subcommand on the `patch` parent.
|
|
30
|
+
*
|
|
31
|
+
* @param parent - Parent Commander command
|
|
32
|
+
* @param context - Shared CLI registration context
|
|
33
|
+
*/
|
|
34
|
+
export declare function registerPatchStage(parent: Command, context: CommandContext): void;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* `fireforge patch stage <name>...`: stages the named patches' `.patch`
|
|
4
|
+
* files and only their rows of `patches/patches.json` in the project
|
|
5
|
+
* repository's git index.
|
|
6
|
+
*
|
|
7
|
+
* The case it exists for: several sessions edit one checkout's queue, and
|
|
8
|
+
* each must commit only its own patches. Staging the whole manifest takes
|
|
9
|
+
* every session's rows; staging one owner's hunks of it with
|
|
10
|
+
* `git apply --cached --unidiff-zero` placed a kept insertion at its
|
|
11
|
+
* new-side line number once a dropped hunk above had shifted the file, and
|
|
12
|
+
* an invalid manifest was committed. This command builds the staged
|
|
13
|
+
* manifest from HEAD's rows plus the named working rows (see
|
|
14
|
+
* `core/patch-manifest-stage.ts`), validates it, and writes it straight to
|
|
15
|
+
* the index with `git hash-object` + `git update-index`. The working copy
|
|
16
|
+
* is never touched, and nothing is committed.
|
|
17
|
+
*/
|
|
18
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
19
|
+
import { tmpdir } from 'node:os';
|
|
20
|
+
import { join, relative, sep } from 'node:path';
|
|
21
|
+
import { Command } from 'commander';
|
|
22
|
+
import { getProjectPaths } from '../../core/config.js';
|
|
23
|
+
import { git } from '../../core/git-base.js';
|
|
24
|
+
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
25
|
+
import { loadPatchesManifestForWrite, resolvePatchIdentifier, validatePatchesManifest, } from '../../core/patch-manifest.js';
|
|
26
|
+
import { mergeStagedManifest, serializeStagedManifest, } from '../../core/patch-manifest-stage.js';
|
|
27
|
+
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
28
|
+
import { toError } from '../../utils/errors.js';
|
|
29
|
+
import { pathExists } from '../../utils/fs.js';
|
|
30
|
+
import { info, intro, outro } from '../../utils/logger.js';
|
|
31
|
+
import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../../utils/options.js';
|
|
32
|
+
import { exec } from '../../utils/process.js';
|
|
33
|
+
/** A project-relative path in git's separator. */
|
|
34
|
+
function gitPath(projectRoot, path) {
|
|
35
|
+
return relative(projectRoot, path).split(sep).join('/');
|
|
36
|
+
}
|
|
37
|
+
/** HEAD's manifest, or null when HEAD has none (or there is no HEAD yet). */
|
|
38
|
+
async function readHeadManifest(projectRoot, manifestPath) {
|
|
39
|
+
const shown = await exec('git', ['show', `HEAD:./${manifestPath}`], { cwd: projectRoot });
|
|
40
|
+
if (shown.exitCode !== 0)
|
|
41
|
+
return null;
|
|
42
|
+
try {
|
|
43
|
+
return validatePatchesManifest(JSON.parse(shown.stdout));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
throw new GeneralError(`HEAD's ${manifestPath} is not a valid manifest (${toError(error).message}); stage it by hand.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Resolves each identifier to a filename, against the working rows then HEAD's. */
|
|
50
|
+
function resolveFilenames(identifiers, working, head) {
|
|
51
|
+
const filenames = new Set();
|
|
52
|
+
for (const identifier of identifiers) {
|
|
53
|
+
const live = resolvePatchIdentifier(identifier, working.patches);
|
|
54
|
+
// A patch deleted (or renamed away) in the working copy is named by its
|
|
55
|
+
// HEAD filename, so its row can be staged as removed.
|
|
56
|
+
const gone = (head?.patches ?? []).find((row) => row.filename === identifier || row.filename === `${identifier}.patch`);
|
|
57
|
+
const filename = live?.filename ?? gone?.filename;
|
|
58
|
+
if (filename === undefined) {
|
|
59
|
+
throw new InvalidArgumentError(`No patch "${identifier}" in the working manifest or at HEAD.`, identifier);
|
|
60
|
+
}
|
|
61
|
+
filenames.add(filename);
|
|
62
|
+
}
|
|
63
|
+
return filenames;
|
|
64
|
+
}
|
|
65
|
+
/** Stages one `.patch` file: its content, or its removal when it is gone. */
|
|
66
|
+
async function stagePatchFile(projectRoot, patchPath) {
|
|
67
|
+
if (await pathExists(join(projectRoot, patchPath))) {
|
|
68
|
+
await git(['add', '--', patchPath], projectRoot);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const tracked = await exec('git', ['ls-files', '--error-unmatch', '--', patchPath], {
|
|
72
|
+
cwd: projectRoot,
|
|
73
|
+
});
|
|
74
|
+
if (tracked.exitCode === 0) {
|
|
75
|
+
await git(['rm', '--cached', '--quiet', '--', patchPath], projectRoot);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Writes `content` as a blob and points the index entry at it. */
|
|
79
|
+
async function stageBlob(projectRoot, path, content) {
|
|
80
|
+
const scratch = await mkdtemp(join(tmpdir(), 'fireforge-stage-'));
|
|
81
|
+
try {
|
|
82
|
+
const file = join(scratch, 'patches.json');
|
|
83
|
+
await writeFile(file, content);
|
|
84
|
+
const sha = (await git(['hash-object', '-w', '--no-filters', file], projectRoot)).trim();
|
|
85
|
+
await git(['update-index', '--add', '--cacheinfo', `100644,${sha},${path}`], projectRoot);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
await rm(scratch, { recursive: true, force: true });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Runs the `patch stage` command.
|
|
93
|
+
*
|
|
94
|
+
* @param projectRoot - Project root directory (a git working tree)
|
|
95
|
+
* @param identifiers - Patch filenames, ordinals or names to stage
|
|
96
|
+
* @param options - Command options
|
|
97
|
+
*/
|
|
98
|
+
export async function patchStageCommand(projectRoot, identifiers, options = {}) {
|
|
99
|
+
intro(options.dryRun ? 'FireForge patch stage (dry run)' : 'FireForge patch stage');
|
|
100
|
+
const probe = await exec('git', ['rev-parse', '--is-inside-work-tree'], { cwd: projectRoot });
|
|
101
|
+
if (probe.exitCode !== 0 || probe.stdout.trim() !== 'true') {
|
|
102
|
+
throw new GeneralError('patch stage needs the project to be a git working tree.');
|
|
103
|
+
}
|
|
104
|
+
const { patches: patchesDir } = getProjectPaths(projectRoot);
|
|
105
|
+
const manifestPath = gitPath(projectRoot, join(patchesDir, 'patches.json'));
|
|
106
|
+
// Under the patch lock, so the rows and `.patch` files staged are one
|
|
107
|
+
// consistent snapshot rather than a peer's half-finished write.
|
|
108
|
+
await withPatchDirectoryLock(patchesDir, async () => {
|
|
109
|
+
const working = await loadPatchesManifestForWrite(patchesDir);
|
|
110
|
+
if (working === null) {
|
|
111
|
+
throw new GeneralError(`No ${manifestPath} to stage.`);
|
|
112
|
+
}
|
|
113
|
+
const head = await readHeadManifest(projectRoot, manifestPath);
|
|
114
|
+
const filenames = resolveFilenames(identifiers, working, head);
|
|
115
|
+
let staged;
|
|
116
|
+
try {
|
|
117
|
+
staged = mergeStagedManifest(head, working, filenames);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
throw new GeneralError(`Refusing to stage: ${toError(error).message}.`);
|
|
121
|
+
}
|
|
122
|
+
const prefix = options.dryRun === true ? '[dry-run] ' : '';
|
|
123
|
+
for (const { filename, change } of staged.changes) {
|
|
124
|
+
info(`${prefix}${filename}: row ${change}; .patch file staged`);
|
|
125
|
+
}
|
|
126
|
+
if (options.dryRun === true)
|
|
127
|
+
return;
|
|
128
|
+
await stageBlob(projectRoot, manifestPath, serializeStagedManifest(staged.manifest));
|
|
129
|
+
for (const filename of filenames) {
|
|
130
|
+
await stagePatchFile(projectRoot, gitPath(projectRoot, join(patchesDir, filename)));
|
|
131
|
+
}
|
|
132
|
+
info(`Staged ${filenames.size} patch(es) and their ${manifestPath} rows. Other rows stay as at HEAD; ` +
|
|
133
|
+
'nothing was committed.');
|
|
134
|
+
}, { waitLockSeconds: resolveWaitLockSeconds(options.waitLock), command: 'patch stage' });
|
|
135
|
+
outro(options.dryRun ? 'Dry run complete — nothing staged' : 'Patch stage complete');
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Registers the `patch stage` subcommand on the `patch` parent.
|
|
139
|
+
*
|
|
140
|
+
* @param parent - Parent Commander command
|
|
141
|
+
* @param context - Shared CLI registration context
|
|
142
|
+
*/
|
|
143
|
+
export function registerPatchStage(parent, context) {
|
|
144
|
+
const { getProjectRoot, withErrorHandling } = context;
|
|
145
|
+
const command = parent
|
|
146
|
+
.command('stage <names...>')
|
|
147
|
+
.description("Stage the named patches' .patch files and only their patches.json rows in the git index " +
|
|
148
|
+
"(HEAD's rows for every other patch); validates the staged manifest, commits nothing")
|
|
149
|
+
.option('--dry-run', 'Show which rows would be staged without touching the index');
|
|
150
|
+
addWaitLockOption(command).action(withErrorHandling(async (names, options) => {
|
|
151
|
+
await patchStageCommand(getProjectRoot(), names, pickDefined(options));
|
|
152
|
+
}));
|
|
153
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--remove` target selection for `patch staged-dependency`. Split out of
|
|
3
|
+
* `staged-dependency.ts` for the per-file line budget.
|
|
4
|
+
*
|
|
5
|
+
* A removal names its entries by any of `--file`, `--specifier`/`--line`,
|
|
6
|
+
* `--creates` and `--owner`, and must give at least `--creates` or the
|
|
7
|
+
* `--file` + matcher pair. Registration lines compare whitespace-
|
|
8
|
+
* insensitively (runs of whitespace collapse, ends trimmed), the way the
|
|
9
|
+
* lint compares them, so a line copied from a patch hunk matches the stored
|
|
10
|
+
* one whatever its padding. A selection that matches nothing is refused
|
|
11
|
+
* rather than reported as a no-op.
|
|
12
|
+
*/
|
|
13
|
+
import type { PatchStagedDependencies, PatchStagedDependencyOptions } from '../../types/commands/index.js';
|
|
14
|
+
import { type StagedDependencyKind, type StagedEntryView } from './staged-dependency-entries.js';
|
|
15
|
+
/**
|
|
16
|
+
* Resolves the staged entries a `--remove` invocation names. The returned
|
|
17
|
+
* views are the stored entries themselves, so the removal matches them
|
|
18
|
+
* exactly.
|
|
19
|
+
*
|
|
20
|
+
* - `--file` + matcher identifying one entry infers `--creates`.
|
|
21
|
+
* - `--file` + matcher identifying several entries without `--creates`
|
|
22
|
+
* refuses with the candidate list.
|
|
23
|
+
* - `--creates` alone selects every declaration for that created path.
|
|
24
|
+
*
|
|
25
|
+
* @param kind - Declaration kind (`--kind`)
|
|
26
|
+
* @param options - Command options
|
|
27
|
+
* @param staged - The target patch's current staged dependencies
|
|
28
|
+
* @returns The matching stored entries (never empty)
|
|
29
|
+
* @throws InvalidArgumentError when the selectors are insufficient or match nothing
|
|
30
|
+
*/
|
|
31
|
+
export declare function selectRemovalEntries(kind: StagedDependencyKind, options: PatchStagedDependencyOptions, staged: PatchStagedDependencies | undefined): StagedEntryView[];
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* `--remove` target selection for `patch staged-dependency`. Split out of
|
|
4
|
+
* `staged-dependency.ts` for the per-file line budget.
|
|
5
|
+
*
|
|
6
|
+
* A removal names its entries by any of `--file`, `--specifier`/`--line`,
|
|
7
|
+
* `--creates` and `--owner`, and must give at least `--creates` or the
|
|
8
|
+
* `--file` + matcher pair. Registration lines compare whitespace-
|
|
9
|
+
* insensitively (runs of whitespace collapse, ends trimmed), the way the
|
|
10
|
+
* lint compares them, so a line copied from a patch hunk matches the stored
|
|
11
|
+
* one whatever its padding. A selection that matches nothing is refused
|
|
12
|
+
* rather than reported as a no-op.
|
|
13
|
+
*/
|
|
14
|
+
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
15
|
+
import { info } from '../../utils/logger.js';
|
|
16
|
+
import { dependencyLabel, importView, registrationView, } from './staged-dependency-entries.js';
|
|
17
|
+
/** Collapses whitespace runs and trims, for registration-line comparison. */
|
|
18
|
+
function normalizeRegistrationLine(line) {
|
|
19
|
+
return line.trim().replace(/\s+/g, ' ');
|
|
20
|
+
}
|
|
21
|
+
function stagedViews(kind, staged) {
|
|
22
|
+
return kind === 'import'
|
|
23
|
+
? (staged?.forwardImports ?? []).map(importView)
|
|
24
|
+
: (staged?.registrations ?? []).map(registrationView);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolves the staged entries a `--remove` invocation names. The returned
|
|
28
|
+
* views are the stored entries themselves, so the removal matches them
|
|
29
|
+
* exactly.
|
|
30
|
+
*
|
|
31
|
+
* - `--file` + matcher identifying one entry infers `--creates`.
|
|
32
|
+
* - `--file` + matcher identifying several entries without `--creates`
|
|
33
|
+
* refuses with the candidate list.
|
|
34
|
+
* - `--creates` alone selects every declaration for that created path.
|
|
35
|
+
*
|
|
36
|
+
* @param kind - Declaration kind (`--kind`)
|
|
37
|
+
* @param options - Command options
|
|
38
|
+
* @param staged - The target patch's current staged dependencies
|
|
39
|
+
* @returns The matching stored entries (never empty)
|
|
40
|
+
* @throws InvalidArgumentError when the selectors are insufficient or match nothing
|
|
41
|
+
*/
|
|
42
|
+
export function selectRemovalEntries(kind, options, staged) {
|
|
43
|
+
const matcherFlag = kind === 'import' ? '--specifier' : '--line';
|
|
44
|
+
const matcher = kind === 'import' ? options.specifier : options.line;
|
|
45
|
+
const hasPair = Boolean(options.file) && Boolean(matcher);
|
|
46
|
+
if (!options.creates && !hasPair) {
|
|
47
|
+
throw new InvalidArgumentError(`--remove${kind === 'registration' ? ' --kind registration' : ''} requires --creates, ` +
|
|
48
|
+
`or --file with ${matcherFlag}; --creates alone removes every declaration for that path.`, matcher && !options.file
|
|
49
|
+
? '--file'
|
|
50
|
+
: options.file && !matcher
|
|
51
|
+
? matcherFlag
|
|
52
|
+
: `--creates or --file + ${matcherFlag}`);
|
|
53
|
+
}
|
|
54
|
+
const wanted = matcher === undefined ? undefined : comparableMatcher(kind, matcher);
|
|
55
|
+
const views = stagedViews(kind, staged);
|
|
56
|
+
const candidates = views.filter((view) => (!options.file || view.file === options.file) &&
|
|
57
|
+
(wanted === undefined || comparableMatcher(kind, view.matcher) === wanted) &&
|
|
58
|
+
(!options.creates || view.creates === options.creates) &&
|
|
59
|
+
(options.owner === undefined || view.owner === options.owner));
|
|
60
|
+
if (candidates.length === 0) {
|
|
61
|
+
const noun = kind === 'import' ? 'staged forward-import' : 'staged registration';
|
|
62
|
+
const existing = views.length === 0
|
|
63
|
+
? `This patch has no ${noun}s.`
|
|
64
|
+
: `This patch's ${noun}s:\n${views.map((view) => ` - ${dependencyLabel(kind, view)}`).join('\n')}`;
|
|
65
|
+
throw new InvalidArgumentError(`--remove matched no ${noun}. ${existing}`, '--remove');
|
|
66
|
+
}
|
|
67
|
+
if (candidates.length > 1 && !options.creates) {
|
|
68
|
+
const list = candidates.map((view) => ` - ${dependencyLabel(kind, view)}`).join('\n');
|
|
69
|
+
const noun = kind === 'import' ? 'staged forward-imports' : 'staged registrations';
|
|
70
|
+
throw new GeneralError(`--remove matches ${candidates.length} ${noun} on this patch; pass --creates to pick one:\n${list}`);
|
|
71
|
+
}
|
|
72
|
+
const single = candidates.length === 1 ? candidates[0] : undefined;
|
|
73
|
+
if (single !== undefined && !options.creates) {
|
|
74
|
+
info(`Inferred --creates ${single.creates} from the single matching staged entry.`);
|
|
75
|
+
}
|
|
76
|
+
return candidates;
|
|
77
|
+
}
|
|
78
|
+
function comparableMatcher(kind, matcher) {
|
|
79
|
+
return kind === 'registration' ? normalizeRegistrationLine(matcher) : matcher;
|
|
80
|
+
}
|