@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
|
@@ -26,12 +26,13 @@ import { Command, Option } from 'commander';
|
|
|
26
26
|
import { confirmDestructive } from '../../core/destructive.js';
|
|
27
27
|
import { appendHistoryBestEffort } from '../../core/history-log.js';
|
|
28
28
|
import { mutatePatchMetadata } from '../../core/patch-export.js';
|
|
29
|
-
import { addedLintIgnoreEntries, formatLintIgnoreEntry, formatLintIgnoreList,
|
|
29
|
+
import { addedLintIgnoreEntries, formatLintIgnoreEntry, formatLintIgnoreList, lintIgnoreRule, removeLintIgnoreRules, upsertLintIgnoreEntries, } from '../../core/patch-lint-ignore.js';
|
|
30
30
|
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
31
31
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
32
32
|
import { addWaitLockOption, commanderArgParser, resolveWaitLockSeconds, stringListOption, } from '../../utils/options.js';
|
|
33
33
|
import { parsePositiveIntegerFlag } from '../../utils/validation.js';
|
|
34
34
|
import { proceedAfterDecision } from '../destructive-decision.js';
|
|
35
|
+
import { parseShare, readLintIgnoreBatch, resolveAddEntries } from './lint-ignore-from.js';
|
|
35
36
|
import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
36
37
|
/**
|
|
37
38
|
* Printed whenever `--add` lands a new check id. The waiver this command
|
|
@@ -88,37 +89,28 @@ function describeChange(before, after, mode, values) {
|
|
|
88
89
|
}
|
|
89
90
|
/**
|
|
90
91
|
* Turns the mode's IDs into manifest entries, applying `--max`/`--reason`
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* measurement, so a waiver that could
|
|
94
|
-
* written.
|
|
92
|
+
* and `<rule>=<max>` to `--add` (see `lint-ignore-from.ts`). Refuses a
|
|
93
|
+
* ceiling flag without `--add`, a ceiling without a `--reason`, and a
|
|
94
|
+
* ceiling on a rule that carries no measurement, so a waiver that could
|
|
95
|
+
* never suppress anything is not written. A `--from` add resolves later,
|
|
96
|
+
* once the target patch is known.
|
|
95
97
|
*/
|
|
96
98
|
function resolveRequestedEntries(mode, options) {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
throw new InvalidArgumentError('--max and --reason only apply to --add.', hasMax ? '--max' : '--reason');
|
|
101
|
-
}
|
|
102
|
-
if (hasMax !== hasReason) {
|
|
103
|
-
throw new InvalidArgumentError('A lintIgnore ceiling needs both --max <n> and --reason <text>.', hasMax ? '--reason' : '--max');
|
|
99
|
+
const ceilingFlag = options.max !== undefined ? '--max' : options.share !== undefined ? '--share' : undefined;
|
|
100
|
+
if ((ceilingFlag !== undefined || options.reason !== undefined) && mode !== 'add') {
|
|
101
|
+
throw new InvalidArgumentError('--max, --reason and --share only apply to --add.', ceilingFlag ?? '--reason');
|
|
104
102
|
}
|
|
105
103
|
if (mode === 'remove')
|
|
106
104
|
return [...(options.remove ?? [])];
|
|
107
105
|
if (mode !== 'add')
|
|
108
106
|
return [];
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
const unmeasured = ids.filter((id) => !LINT_IGNORE_CEILING_RULES.includes(id));
|
|
116
|
-
if (unmeasured.length > 0) {
|
|
117
|
-
throw new InvalidArgumentError(`--max only applies to the measured size rules (${LINT_IGNORE_CEILING_RULES.join(', ')}); ` +
|
|
118
|
-
`got ${unmeasured.join(', ')}.`, '--max');
|
|
107
|
+
if (options.from !== undefined) {
|
|
108
|
+
if ((options.add?.length ?? 0) > 0 || ceilingFlag !== undefined) {
|
|
109
|
+
throw new InvalidArgumentError('--from takes its entries from the file; it combines only with --reason (the default reason).', (options.add?.length ?? 0) > 0 ? '--add' : ceilingFlag);
|
|
110
|
+
}
|
|
111
|
+
return [];
|
|
119
112
|
}
|
|
120
|
-
|
|
121
|
-
return ids.map((rule) => ({ rule, max, reason }));
|
|
113
|
+
return resolveAddEntries(options.add ?? [], options);
|
|
122
114
|
}
|
|
123
115
|
/**
|
|
124
116
|
* Runs the `patch lint-ignore` command: reads the patch's existing
|
|
@@ -135,7 +127,7 @@ export async function patchLintIgnoreCommand(projectRoot, identifier, options =
|
|
|
135
127
|
// `--add foo --remove bar` are rejected: an operator who needs both
|
|
136
128
|
// runs the command twice (clearer audit trail), and `--clear` plus a
|
|
137
129
|
// mode is contradictory.
|
|
138
|
-
const adding = (options.add?.length ?? 0) > 0;
|
|
130
|
+
const adding = (options.add?.length ?? 0) > 0 || options.from !== undefined;
|
|
139
131
|
const removing = (options.remove?.length ?? 0) > 0;
|
|
140
132
|
const clearing = options.clear === true;
|
|
141
133
|
const modeCount = [adding, removing, clearing].filter(Boolean).length;
|
|
@@ -143,12 +135,22 @@ export async function patchLintIgnoreCommand(projectRoot, identifier, options =
|
|
|
143
135
|
throw new InvalidArgumentError('--add, --remove, and --clear are mutually exclusive. Pick one mode per invocation.', 'patch lint-ignore');
|
|
144
136
|
}
|
|
145
137
|
if (modeCount === 0) {
|
|
146
|
-
throw new InvalidArgumentError('Specify --add <id>, --remove <id>, or --clear.', 'patch lint-ignore');
|
|
138
|
+
throw new InvalidArgumentError('Specify --add <id>, --from <json-file>, --remove <id>, or --clear.', 'patch lint-ignore');
|
|
147
139
|
}
|
|
148
140
|
const mode = adding ? 'add' : removing ? 'remove' : 'clear';
|
|
149
|
-
|
|
141
|
+
let values = resolveRequestedEntries(mode, options);
|
|
150
142
|
const { paths, manifest } = await requirePatchQueue(projectRoot);
|
|
151
143
|
const target = requirePatchTarget(identifier, manifest.patches);
|
|
144
|
+
if (options.from !== undefined) {
|
|
145
|
+
const batch = await readLintIgnoreBatch(options.from, target.filename, options.reason);
|
|
146
|
+
if (batch.skippedForOtherPatches > 0) {
|
|
147
|
+
info(`--from: ${batch.skippedForOtherPatches} entry/entries for other patches left out.`);
|
|
148
|
+
}
|
|
149
|
+
if (batch.entries.length === 0) {
|
|
150
|
+
throw new InvalidArgumentError(`--from ${options.from} holds no lintIgnore entry for ${target.filename}.`, '--from');
|
|
151
|
+
}
|
|
152
|
+
values = batch.entries;
|
|
153
|
+
}
|
|
152
154
|
// Destructive-operation contract (docs/lifecycle-invariants.md): manifest
|
|
153
155
|
// metadata mutations get summary + dry-run + confirmation/--yes + history
|
|
154
156
|
// uniformly. Accepting --yes without ever prompting makes the flag appear
|
|
@@ -216,12 +218,16 @@ export function registerPatchLintIgnore(parent, context) {
|
|
|
216
218
|
const command = parent
|
|
217
219
|
.command('lint-ignore <name>')
|
|
218
220
|
.description('Edit PatchMetadata.lintIgnore on a single patch (no .patch body rewrite). One mode per invocation.')
|
|
219
|
-
.option('--add <check-id>', 'Lint check ID to add to the patch lintIgnore list (repeatable)', ...stringListOption())
|
|
221
|
+
.option('--add <check-id>', 'Lint check ID to add to the patch lintIgnore list (repeatable); <rule>=<max> adds a ceiling at <max>', ...stringListOption())
|
|
220
222
|
.option('--remove <check-id>', 'Lint check ID to remove from the patch lintIgnore list (repeatable)', ...stringListOption())
|
|
221
223
|
.option('--clear', 'Drop the lintIgnore field entirely')
|
|
222
224
|
.addOption(new Option('--max <n>', 'With --add: write a size waiver with a ceiling, suppressing the rule only while its ' +
|
|
223
225
|
'measurement is at most <n> (file-too-large, large-patch-lines, large-patch-files)').argParser(commanderArgParser((v) => parsePositiveIntegerFlag('--max', v))))
|
|
224
|
-
.option('--reason <text>', '
|
|
226
|
+
.option('--reason <text>', 'Why the ceiling exists (required with --max or <rule>=<max>; with --from: the default for entries without one)')
|
|
227
|
+
.addOption(new Option('--share <share>', 'With a large-patch-lines ceiling: bound only the "product" or "test" share of the measurement').argParser(commanderArgParser(parseShare)))
|
|
228
|
+
.option('--from <json-file>', 'Add every entry in a JSON file under one lock hold: an array of check IDs or ' +
|
|
229
|
+
'{ rule, max, reason?, share?, meta?, patch? } objects, or a "lint --per-patch --report" ' +
|
|
230
|
+
'document, whose ceilings for this patch are rebaselined to their current measurement')
|
|
225
231
|
.option('--dry-run', 'Show what would change without writing')
|
|
226
232
|
.option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)');
|
|
227
233
|
addWaitLockOption(command).action(withErrorHandling(async (name, options) => {
|
|
@@ -240,6 +246,10 @@ export function registerPatchLintIgnore(parent, context) {
|
|
|
240
246
|
normalized.max = options.max;
|
|
241
247
|
if (options.reason !== undefined)
|
|
242
248
|
normalized.reason = options.reason;
|
|
249
|
+
if (options.share !== undefined)
|
|
250
|
+
normalized.share = options.share;
|
|
251
|
+
if (options.from !== undefined)
|
|
252
|
+
normalized.from = options.from;
|
|
243
253
|
if (options.dryRun === true)
|
|
244
254
|
normalized.dryRun = true;
|
|
245
255
|
if (options.yes === true)
|
|
@@ -21,11 +21,12 @@ import { enforcePatchPolicy } from '../../core/patch-policy.js';
|
|
|
21
21
|
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
22
22
|
import { info, intro, outro, success } from '../../utils/logger.js';
|
|
23
23
|
import { normalizePatchDisplayName } from '../../utils/validation.js';
|
|
24
|
+
import { withDeclarationsReport } from '../declarations-report.js';
|
|
24
25
|
import { proceedAfterDecision } from '../destructive-decision.js';
|
|
25
26
|
import { resolvePlacementPlan } from '../export-flow.js';
|
|
26
27
|
import { runPatchLint } from '../export-shared.js';
|
|
27
28
|
import { commitPatchSplit, reportSplitStagedChanges } from './split.js';
|
|
28
|
-
import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, projectSplitManifest, runProjectedSplitLint, } from './split-plan.js';
|
|
29
|
+
import { assertSourceOwnsFiles, buildNewPatchMetadata, buildSplitDiff, buildSplitSummary, projectSplitManifest, runProjectedSplitLint, splitDeclarationRenamer, } from './split-plan.js';
|
|
29
30
|
import { emptyStagedMaintenance, findOwnerRewriteHolders } from './split-staged.js';
|
|
30
31
|
/**
|
|
31
32
|
* Runs `patch move-files --create --order <n>`: plans the create+move as a
|
|
@@ -136,9 +137,15 @@ export async function patchMoveFilesCreateCommand(projectRoot, fromIdentifier, n
|
|
|
136
137
|
ignoreChecks,
|
|
137
138
|
patchTier: source.tier,
|
|
138
139
|
});
|
|
139
|
-
const
|
|
140
|
-
plan.stagedDependencyAdditions = stagedDependencyAdditions;
|
|
141
|
-
plan.stagedMaintenance = stagedMaintenance;
|
|
140
|
+
const projected = runProjectedSplitLint(plan, patchQueueCtx);
|
|
141
|
+
plan.stagedDependencyAdditions = projected.stagedDependencyAdditions;
|
|
142
|
+
plan.stagedMaintenance = projected.stagedMaintenance;
|
|
143
|
+
const conflicts = await withDeclarationsReport(projected.conflicts, {
|
|
144
|
+
projectRoot,
|
|
145
|
+
regressions: projected.regressions,
|
|
146
|
+
out: options.declarationsOut,
|
|
147
|
+
rename: splitDeclarationRenamer(plan),
|
|
148
|
+
});
|
|
142
149
|
const newMetadata = buildNewPatchMetadata(plan, config);
|
|
143
150
|
const decision = await confirmDestructive({
|
|
144
151
|
operation: 'patch-move-files-create',
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* reflect both patches' content, because both bodies are regenerated from
|
|
15
15
|
* the worktree.
|
|
16
16
|
*/
|
|
17
|
-
import { join } from 'node:path';
|
|
17
|
+
import { dirname, join, resolve } from 'node:path';
|
|
18
18
|
import { assertConfirmationAvailable, confirmDestructive, } from '../../core/destructive.js';
|
|
19
19
|
import { appendHistoryBestEffort } from '../../core/history-log.js';
|
|
20
20
|
import { buildPatchQueueContext, collectForwardImportEdges, formatPatchLintIssue, lintPatchQueue, } from '../../core/patch-lint.js';
|
|
@@ -27,6 +27,7 @@ import { toError } from '../../utils/errors.js';
|
|
|
27
27
|
import { readText, writeText } from '../../utils/fs.js';
|
|
28
28
|
import { info, outro, success, warn } from '../../utils/logger.js';
|
|
29
29
|
import { resolveWaitLockSeconds } from '../../utils/options.js';
|
|
30
|
+
import { withDeclarationsReport } from '../declarations-report.js';
|
|
30
31
|
import { proceedAfterDecision } from '../destructive-decision.js';
|
|
31
32
|
import { runPatchLint } from '../export-shared.js';
|
|
32
33
|
import { projectEntryBody } from './entry-projection.js';
|
|
@@ -91,7 +92,7 @@ function runProjectedMoveLint(plan, baseCtx) {
|
|
|
91
92
|
reason: `move would introduce ${regressions.length} cross-patch lint error(s)`,
|
|
92
93
|
details: regressions.map(formatPatchLintIssue),
|
|
93
94
|
};
|
|
94
|
-
return { conflicts, stagedDependencyAdditions };
|
|
95
|
+
return { conflicts, regressions, stagedDependencyAdditions };
|
|
95
96
|
}
|
|
96
97
|
/**
|
|
97
98
|
* Commits a confirmed move under the patch-directory lock: write both
|
|
@@ -225,8 +226,13 @@ export async function runMoveFilesInto(args) {
|
|
|
225
226
|
ownerRewrites: findOwnerRewriteHolders(manifest?.patches ?? [], source.filename, new Set(files)),
|
|
226
227
|
stagedDependencyAdditions: new Map(),
|
|
227
228
|
};
|
|
228
|
-
const
|
|
229
|
-
plan.stagedDependencyAdditions = stagedDependencyAdditions;
|
|
229
|
+
const projected = runProjectedMoveLint(plan, patchQueueCtx);
|
|
230
|
+
plan.stagedDependencyAdditions = projected.stagedDependencyAdditions;
|
|
231
|
+
const conflicts = await withDeclarationsReport(projected.conflicts, {
|
|
232
|
+
projectRoot: dirname(resolve(patchesDir)),
|
|
233
|
+
regressions: projected.regressions,
|
|
234
|
+
out: options.declarationsOut,
|
|
235
|
+
});
|
|
230
236
|
const decision = await confirmDestructive({
|
|
231
237
|
operation: 'patch-move-files',
|
|
232
238
|
title: `Move ${files.length} file(s) from ${source.filename} to ${target.filename}`,
|
|
@@ -172,7 +172,8 @@ export function registerPatchMoveFiles(parent, context) {
|
|
|
172
172
|
.option('--dry-run', 'Show what would happen without writing')
|
|
173
173
|
.option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
|
|
174
174
|
.option('--force-unsafe', 'Bypass projected-lint refusals')
|
|
175
|
-
.option('--skip-lint', 'Skip per-patch lint of the projected bodies')
|
|
175
|
+
.option('--skip-lint', 'Skip per-patch lint of the projected bodies')
|
|
176
|
+
.option('--declarations-out <path>', 'Where a refusal writes its one --from-ready staged-declaration report (default: .fireforge/declarations/<timestamp>.json)');
|
|
176
177
|
addWaitLockOption(command).action(withErrorHandling(async (from, to, options) => {
|
|
177
178
|
await patchMoveFilesCommand(getProjectRoot(), from, to, {
|
|
178
179
|
...pickDefined({
|
|
@@ -185,6 +186,7 @@ export function registerPatchMoveFiles(parent, context) {
|
|
|
185
186
|
yes: options.yes,
|
|
186
187
|
forceUnsafe: options.forceUnsafe,
|
|
187
188
|
skipLint: options.skipLint,
|
|
189
|
+
declarationsOut: options.declarationsOut,
|
|
188
190
|
waitLock: options.waitLock,
|
|
189
191
|
}),
|
|
190
192
|
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `patch delete` and `patch retire` do to the files the removed patch
|
|
3
|
+
* claims, shared so both commands say and do the same.
|
|
4
|
+
*
|
|
5
|
+
* A file the patch modifies stays in the engine with its edits, unmanaged.
|
|
6
|
+
* A file the patch creates is different: once the patch is gone nothing
|
|
7
|
+
* owns it, so a created file still on disk turns into an unmanaged file the
|
|
8
|
+
* engine-clean gate reports. The summary names each such file (in the dry
|
|
9
|
+
* run and at the prompt), and `--delete-created` removes them as part of
|
|
10
|
+
* the confirmed operation.
|
|
11
|
+
*/
|
|
12
|
+
import type { PatchQueueContext } from '../../core/patch-lint.js';
|
|
13
|
+
import type { PatchMetadata } from '../../types/commands/index.js';
|
|
14
|
+
/** The summary lines for a removal's claimed files, and what to delete. */
|
|
15
|
+
export interface ClaimedFilePlan {
|
|
16
|
+
/** One summary line per claimed file, plus a header. */
|
|
17
|
+
summary: string[];
|
|
18
|
+
/** Created files still on disk that the removal will delete. */
|
|
19
|
+
deleteOnCommit: string[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Plans the claimed-file half of a removal.
|
|
23
|
+
*
|
|
24
|
+
* @param ctx - Queue context built before the removal
|
|
25
|
+
* @param target - The patch about to leave the queue
|
|
26
|
+
* @param engineDir - Engine directory
|
|
27
|
+
* @param deleteCreated - Whether `--delete-created` was passed
|
|
28
|
+
*/
|
|
29
|
+
export declare function planClaimedFiles(ctx: PatchQueueContext, target: PatchMetadata, engineDir: string, deleteCreated: boolean): Promise<ClaimedFilePlan>;
|
|
30
|
+
/**
|
|
31
|
+
* Deletes the created files a confirmed removal planned to delete.
|
|
32
|
+
*
|
|
33
|
+
* @param engineDir - Engine directory
|
|
34
|
+
* @param files - Engine-relative paths from {@link planClaimedFiles}
|
|
35
|
+
*/
|
|
36
|
+
export declare function deleteCreatedFiles(engineDir: string, files: readonly string[]): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Runs a removal's locked commit, inside the engine-session lock as well
|
|
39
|
+
* when it will delete engine files, so no engine-mutating peer runs while
|
|
40
|
+
* created files disappear under it.
|
|
41
|
+
*
|
|
42
|
+
* @param projectRoot - Project root directory
|
|
43
|
+
* @param command - Command name for lock attribution
|
|
44
|
+
* @param plan - The claimed-file plan
|
|
45
|
+
* @param waitLockSeconds - Wait budget for the engine lock
|
|
46
|
+
* @param operation - The patch-directory-locked commit
|
|
47
|
+
*/
|
|
48
|
+
export declare function withCreatedFileLock(projectRoot: string, command: string, plan: ClaimedFilePlan, waitLockSeconds: number | undefined, operation: () => Promise<void>): Promise<void>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* What `patch delete` and `patch retire` do to the files the removed patch
|
|
4
|
+
* claims, shared so both commands say and do the same.
|
|
5
|
+
*
|
|
6
|
+
* A file the patch modifies stays in the engine with its edits, unmanaged.
|
|
7
|
+
* A file the patch creates is different: once the patch is gone nothing
|
|
8
|
+
* owns it, so a created file still on disk turns into an unmanaged file the
|
|
9
|
+
* engine-clean gate reports. The summary names each such file (in the dry
|
|
10
|
+
* run and at the prompt), and `--delete-created` removes them as part of
|
|
11
|
+
* the confirmed operation.
|
|
12
|
+
*/
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import { withEngineSessionLock } from '../../core/engine-session-lock.js';
|
|
15
|
+
import { pathExists, removeFile } from '../../utils/fs.js';
|
|
16
|
+
/**
|
|
17
|
+
* Plans the claimed-file half of a removal.
|
|
18
|
+
*
|
|
19
|
+
* @param ctx - Queue context built before the removal
|
|
20
|
+
* @param target - The patch about to leave the queue
|
|
21
|
+
* @param engineDir - Engine directory
|
|
22
|
+
* @param deleteCreated - Whether `--delete-created` was passed
|
|
23
|
+
*/
|
|
24
|
+
export async function planClaimedFiles(ctx, target, engineDir, deleteCreated) {
|
|
25
|
+
const created = ctx.entries.find((entry) => entry.filename === target.filename)?.createdFiles;
|
|
26
|
+
const summary = [`files currently claimed by this patch (${target.filesAffected.length}):`];
|
|
27
|
+
const deleteOnCommit = [];
|
|
28
|
+
let lingering = 0;
|
|
29
|
+
for (const file of target.filesAffected) {
|
|
30
|
+
if (created?.has(file) !== true) {
|
|
31
|
+
summary.push(` ${file} → will become unmanaged`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!(await pathExists(join(engineDir, file)))) {
|
|
35
|
+
summary.push(` ${file} → created by this patch; already gone`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (deleteCreated) {
|
|
39
|
+
deleteOnCommit.push(file);
|
|
40
|
+
summary.push(` ${file} → created by this patch; will be deleted (--delete-created)`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
lingering++;
|
|
44
|
+
summary.push(` ${file} → created by this patch and still on disk: will become unmanaged`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (lingering > 0) {
|
|
48
|
+
summary.push(`${lingering} created file(s) are still on disk. Delete them first, or pass ` +
|
|
49
|
+
'--delete-created to remove them with the patch; otherwise status reports them as unmanaged.');
|
|
50
|
+
}
|
|
51
|
+
return { summary, deleteOnCommit };
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Deletes the created files a confirmed removal planned to delete.
|
|
55
|
+
*
|
|
56
|
+
* @param engineDir - Engine directory
|
|
57
|
+
* @param files - Engine-relative paths from {@link planClaimedFiles}
|
|
58
|
+
*/
|
|
59
|
+
export async function deleteCreatedFiles(engineDir, files) {
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
await removeFile(join(engineDir, file));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Runs a removal's locked commit, inside the engine-session lock as well
|
|
66
|
+
* when it will delete engine files, so no engine-mutating peer runs while
|
|
67
|
+
* created files disappear under it.
|
|
68
|
+
*
|
|
69
|
+
* @param projectRoot - Project root directory
|
|
70
|
+
* @param command - Command name for lock attribution
|
|
71
|
+
* @param plan - The claimed-file plan
|
|
72
|
+
* @param waitLockSeconds - Wait budget for the engine lock
|
|
73
|
+
* @param operation - The patch-directory-locked commit
|
|
74
|
+
*/
|
|
75
|
+
export async function withCreatedFileLock(projectRoot, command, plan, waitLockSeconds, operation) {
|
|
76
|
+
if (plan.deleteOnCommit.length === 0) {
|
|
77
|
+
await operation();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await withEngineSessionLock(projectRoot, command, operation, {
|
|
81
|
+
...(waitLockSeconds !== undefined ? { waitLockSeconds } : {}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -26,7 +26,7 @@ import { buildPatchQueueContext, formatPatchLintIssue, lintPatchQueue, } from '.
|
|
|
26
26
|
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
27
27
|
import { loadPatchesManifest, rewriteStagedDependencyOwners, savePatchesManifest, } from '../../core/patch-manifest.js';
|
|
28
28
|
import { buildProjectedManifest, enforcePatchPolicy, getPatchPolicyCategories, isCategoryAllowedByConfig, } from '../../core/patch-policy.js';
|
|
29
|
-
import { carryRetiredSlots } from '../../core/patch-retired.js';
|
|
29
|
+
import { carryReservedSlots, carryRetiredSlots } from '../../core/patch-retired.js';
|
|
30
30
|
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
31
31
|
import { toError } from '../../utils/errors.js';
|
|
32
32
|
import { pathExists } from '../../utils/fs.js';
|
|
@@ -116,12 +116,17 @@ async function commitRenameUnderLock(input) {
|
|
|
116
116
|
...(descriptionChanging ? { description: newDescription ?? '' } : {}),
|
|
117
117
|
};
|
|
118
118
|
}
|
|
119
|
-
// A rename onto a retired order fills that slot.
|
|
119
|
+
// A rename onto a retired or reserved order fills that slot.
|
|
120
120
|
const retired = carryRetiredSlots(fresh, fresh.patches);
|
|
121
121
|
if (retired === undefined)
|
|
122
122
|
delete fresh.retired;
|
|
123
123
|
else
|
|
124
124
|
fresh.retired = retired;
|
|
125
|
+
const reserved = carryReservedSlots(fresh, fresh.patches);
|
|
126
|
+
if (reserved === undefined)
|
|
127
|
+
delete fresh.reserved;
|
|
128
|
+
else
|
|
129
|
+
fresh.reserved = reserved;
|
|
125
130
|
enforcePatchPolicy({
|
|
126
131
|
config: input.config,
|
|
127
132
|
manifest: buildProjectedManifest(fresh, fresh.patches),
|
|
@@ -306,11 +311,18 @@ export async function patchRenameCommand(projectRoot, identifier, options = {})
|
|
|
306
311
|
command: 'patch rename',
|
|
307
312
|
forceUnsafe: options.forceUnsafe === true,
|
|
308
313
|
});
|
|
314
|
+
// A description-only edit renames nothing and moves no order, so it needs
|
|
315
|
+
// no confirmation: scripts may change a description without --yes.
|
|
316
|
+
const descriptionOnly = descriptionChanging &&
|
|
317
|
+
!filenameChanging &&
|
|
318
|
+
!nameChanging &&
|
|
319
|
+
!orderChanging &&
|
|
320
|
+
!categoryChanging;
|
|
309
321
|
const decision = await confirmDestructive({
|
|
310
322
|
operation: 'patch-rename',
|
|
311
323
|
title: `Rename ${target.filename}`,
|
|
312
324
|
summary,
|
|
313
|
-
yes: options.yes === true,
|
|
325
|
+
yes: options.yes === true || descriptionOnly,
|
|
314
326
|
dryRun: isDryRun,
|
|
315
327
|
conflicts: null,
|
|
316
328
|
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fireforge patch reserve <order>`: holds an order number for a patch a
|
|
3
|
+
* session has agreed to export but has not exported yet.
|
|
4
|
+
*
|
|
5
|
+
* The case it exists for: under `patchPolicy.allowGaps: false`, a session
|
|
6
|
+
* cannot `export --order 571` while a peer has claimed 569 and 570 but not
|
|
7
|
+
* exported them, because the unfilled orders read as a gap. A reservation
|
|
8
|
+
* is a placeholder in `patches.json#reserved` that the gap check counts as
|
|
9
|
+
* occupied and the earliest-legal-order search counts as taken. The export
|
|
10
|
+
* that lands on the order fills it (see `core/patch-retired.ts`), and
|
|
11
|
+
* `--release` drops a reservation whose patch will not come.
|
|
12
|
+
*
|
|
13
|
+
* Metadata-only: no `.patch` file is written and no row is added, so
|
|
14
|
+
* apply, lint and rebase never see a reservation.
|
|
15
|
+
*/
|
|
16
|
+
import { Command } from 'commander';
|
|
17
|
+
import type { CommandContext } from '../../types/cli.js';
|
|
18
|
+
import type { PatchReserveOptions } from '../../types/commands/index.js';
|
|
19
|
+
/**
|
|
20
|
+
* Runs the `patch reserve` command.
|
|
21
|
+
*
|
|
22
|
+
* @param projectRoot - Project root directory
|
|
23
|
+
* @param order - Order number to reserve or release
|
|
24
|
+
* @param options - Command options
|
|
25
|
+
*/
|
|
26
|
+
export declare function patchReserveCommand(projectRoot: string, order: number, options?: PatchReserveOptions): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Registers the `patch reserve` subcommand on the `patch` parent.
|
|
29
|
+
*
|
|
30
|
+
* @param parent - Parent Commander command
|
|
31
|
+
* @param context - Shared CLI registration context
|
|
32
|
+
*/
|
|
33
|
+
export declare function registerPatchReserve(parent: Command, context: CommandContext): void;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* `fireforge patch reserve <order>`: holds an order number for a patch a
|
|
4
|
+
* session has agreed to export but has not exported yet.
|
|
5
|
+
*
|
|
6
|
+
* The case it exists for: under `patchPolicy.allowGaps: false`, a session
|
|
7
|
+
* cannot `export --order 571` while a peer has claimed 569 and 570 but not
|
|
8
|
+
* exported them, because the unfilled orders read as a gap. A reservation
|
|
9
|
+
* is a placeholder in `patches.json#reserved` that the gap check counts as
|
|
10
|
+
* occupied and the earliest-legal-order search counts as taken. The export
|
|
11
|
+
* that lands on the order fills it (see `core/patch-retired.ts`), and
|
|
12
|
+
* `--release` drops a reservation whose patch will not come.
|
|
13
|
+
*
|
|
14
|
+
* Metadata-only: no `.patch` file is written and no row is added, so
|
|
15
|
+
* apply, lint and rebase never see a reservation.
|
|
16
|
+
*/
|
|
17
|
+
import { Command } from 'commander';
|
|
18
|
+
import { getProjectPaths, loadConfig } from '../../core/config.js';
|
|
19
|
+
import { appendHistoryBestEffort } from '../../core/history-log.js';
|
|
20
|
+
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
21
|
+
import { loadPatchesManifestForWrite, savePatchesManifest } from '../../core/patch-manifest.js';
|
|
22
|
+
import { formatPatchOrder } from '../../core/patch-parse.js';
|
|
23
|
+
import { categoryRangeForOrder, categoryRangeLabel, isCategoryAllowedByConfig, } from '../../core/patch-policy.js';
|
|
24
|
+
import { InvalidArgumentError } from '../../errors/base.js';
|
|
25
|
+
import { info, intro, outro } from '../../utils/logger.js';
|
|
26
|
+
import { addWaitLockOption, pickDefined, resolveWaitLockSeconds } from '../../utils/options.js';
|
|
27
|
+
import { parsePositiveIntegerFlag } from '../../utils/validation.js';
|
|
28
|
+
/**
|
|
29
|
+
* Refuses a reservation the manifest or the policy cannot hold: an order a
|
|
30
|
+
* patch, a retired slot or another reservation already takes, or one
|
|
31
|
+
* outside the category's configured ranges.
|
|
32
|
+
*/
|
|
33
|
+
function assertReservable(manifest, config, order, category) {
|
|
34
|
+
const label = formatPatchOrder(order);
|
|
35
|
+
const patch = manifest.patches.find((entry) => entry.order === order);
|
|
36
|
+
if (patch !== undefined) {
|
|
37
|
+
throw new InvalidArgumentError(`Order ${label} is taken by ${patch.filename}.`, '<order>');
|
|
38
|
+
}
|
|
39
|
+
if ((manifest.retired ?? []).some((slot) => slot.order === order)) {
|
|
40
|
+
throw new InvalidArgumentError(`Order ${label} is a retired slot; "fireforge export --order ${label}" fills it without a reservation.`, '<order>');
|
|
41
|
+
}
|
|
42
|
+
const held = manifest.reserved?.find((slot) => slot.order === order);
|
|
43
|
+
if (held !== undefined) {
|
|
44
|
+
throw new InvalidArgumentError(`Order ${label} is already reserved by ${held.owner} (since ${held.reservedAt}).`, '<order>');
|
|
45
|
+
}
|
|
46
|
+
const policy = config.patchPolicy;
|
|
47
|
+
if (policy !== undefined && categoryRangeForOrder(policy, category, order) === null) {
|
|
48
|
+
throw new InvalidArgumentError(`Order ${label} is outside the ${category} range (${categoryRangeLabel(policy.ranges, category)}).`, '<order>');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Runs the `patch reserve` command.
|
|
53
|
+
*
|
|
54
|
+
* @param projectRoot - Project root directory
|
|
55
|
+
* @param order - Order number to reserve or release
|
|
56
|
+
* @param options - Command options
|
|
57
|
+
*/
|
|
58
|
+
export async function patchReserveCommand(projectRoot, order, options = {}) {
|
|
59
|
+
const releasing = options.release === true;
|
|
60
|
+
const verb = releasing ? 'release' : 'reserve';
|
|
61
|
+
intro(options.dryRun ? `FireForge patch ${verb} (dry run)` : `FireForge patch ${verb}`);
|
|
62
|
+
const config = await loadConfig(projectRoot);
|
|
63
|
+
const label = formatPatchOrder(order);
|
|
64
|
+
let category;
|
|
65
|
+
let owner;
|
|
66
|
+
if (!releasing) {
|
|
67
|
+
category = options.category;
|
|
68
|
+
owner = options.owner?.trim();
|
|
69
|
+
if (category === undefined || owner === undefined || owner === '') {
|
|
70
|
+
throw new InvalidArgumentError('patch reserve requires --category and --owner (or --release to drop a reservation).', [category === undefined ? '--category' : '', owner ? '' : '--owner']
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.join(', '));
|
|
73
|
+
}
|
|
74
|
+
if (!isCategoryAllowedByConfig(config, category)) {
|
|
75
|
+
throw new InvalidArgumentError(`Unknown patch category "${category}".`, '--category');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else if (options.category !== undefined || options.owner !== undefined) {
|
|
79
|
+
throw new InvalidArgumentError('--release takes only the order; --category and --owner apply when reserving.', '--release');
|
|
80
|
+
}
|
|
81
|
+
const { patches: patchesDir } = getProjectPaths(projectRoot);
|
|
82
|
+
await withPatchDirectoryLock(patchesDir, async () => {
|
|
83
|
+
const manifest = (await loadPatchesManifestForWrite(patchesDir)) ?? {
|
|
84
|
+
version: 1,
|
|
85
|
+
patches: [],
|
|
86
|
+
};
|
|
87
|
+
const reserved = manifest.reserved ?? [];
|
|
88
|
+
let slot;
|
|
89
|
+
let next;
|
|
90
|
+
if (releasing) {
|
|
91
|
+
const held = reserved.find((entry) => entry.order === order);
|
|
92
|
+
if (held === undefined) {
|
|
93
|
+
throw new InvalidArgumentError(`Order ${label} is not reserved.`, '<order>');
|
|
94
|
+
}
|
|
95
|
+
slot = held;
|
|
96
|
+
next = reserved.filter((entry) => entry.order !== order);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
assertReservable(manifest, config, order, category);
|
|
100
|
+
slot = {
|
|
101
|
+
order,
|
|
102
|
+
category: category,
|
|
103
|
+
owner: owner,
|
|
104
|
+
reservedAt: new Date().toISOString(),
|
|
105
|
+
...(options.reason !== undefined ? { reason: options.reason } : {}),
|
|
106
|
+
};
|
|
107
|
+
next = [...reserved, slot].sort((a, b) => a.order - b.order);
|
|
108
|
+
}
|
|
109
|
+
const summary = releasing
|
|
110
|
+
? `release order ${label} (reserved by ${slot.owner})`
|
|
111
|
+
: `reserve order ${label} for ${slot.owner} (category: ${slot.category})`;
|
|
112
|
+
if (options.dryRun === true) {
|
|
113
|
+
info(`[dry-run] ${summary}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (next.length > 0)
|
|
117
|
+
manifest.reserved = next;
|
|
118
|
+
else
|
|
119
|
+
delete manifest.reserved;
|
|
120
|
+
await savePatchesManifest(patchesDir, manifest);
|
|
121
|
+
await appendHistoryBestEffort(patchesDir, {
|
|
122
|
+
operation: releasing ? 'patch-reserve-release' : 'patch-reserve',
|
|
123
|
+
args: { ...slot },
|
|
124
|
+
result: 'ok',
|
|
125
|
+
}, `patch ${verb} committed (${label})`);
|
|
126
|
+
info(releasing
|
|
127
|
+
? `Released order ${label}.`
|
|
128
|
+
: `Reserved order ${label} for ${slot.owner}; "fireforge export --order ${label}" fills it.`);
|
|
129
|
+
}, { waitLockSeconds: resolveWaitLockSeconds(options.waitLock), command: `patch ${verb}` });
|
|
130
|
+
outro(options.dryRun ? 'Dry run complete — no changes made' : `Patch ${verb} complete`);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Registers the `patch reserve` subcommand on the `patch` parent.
|
|
134
|
+
*
|
|
135
|
+
* @param parent - Parent Commander command
|
|
136
|
+
* @param context - Shared CLI registration context
|
|
137
|
+
*/
|
|
138
|
+
export function registerPatchReserve(parent, context) {
|
|
139
|
+
const { getProjectRoot, withErrorHandling } = context;
|
|
140
|
+
const command = parent
|
|
141
|
+
.command('reserve <order>')
|
|
142
|
+
.description('Hold an order number for a patch not exported yet, so patchPolicy.allowGaps: false ' +
|
|
143
|
+
'sees no gap; the export that takes the order fills it (metadata only)')
|
|
144
|
+
.option('--category <category>', 'Category the reserved patch will carry')
|
|
145
|
+
.option('--owner <label>', 'Who holds the reservation (a session label or planned filename)')
|
|
146
|
+
.option('--reason <text>', 'Rationale recorded on the reserved slot')
|
|
147
|
+
.option('--release', 'Drop the reservation at <order> instead of creating one')
|
|
148
|
+
.option('--dry-run', 'Show what would change without writing');
|
|
149
|
+
addWaitLockOption(command).action(withErrorHandling(async (rawOrder, options) => {
|
|
150
|
+
await patchReserveCommand(getProjectRoot(), parsePositiveIntegerFlag('<order>', rawOrder), pickDefined(options));
|
|
151
|
+
}));
|
|
152
|
+
}
|