@hominis/fireforge 0.30.0 → 0.31.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 +26 -1
- package/README.md +22 -5
- package/dist/src/commands/export-all.js +5 -15
- package/dist/src/commands/export-flow.d.ts +6 -0
- package/dist/src/commands/export-flow.js +6 -1
- package/dist/src/commands/export-placement-gate.d.ts +38 -0
- package/dist/src/commands/export-placement-gate.js +105 -0
- package/dist/src/commands/export-shared.d.ts +28 -0
- package/dist/src/commands/export-shared.js +36 -0
- package/dist/src/commands/export.js +47 -112
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +0 -13
- package/dist/src/commands/furnace/chrome-doc-templates.js +1 -1
- package/dist/src/commands/furnace/create-dry-run.d.ts +1 -1
- package/dist/src/commands/furnace/create.d.ts +1 -2
- package/dist/src/commands/furnace/deploy.js +36 -114
- package/dist/src/commands/furnace/refresh.js +52 -32
- package/dist/src/commands/furnace/sync.js +2 -0
- package/dist/src/commands/import.js +108 -73
- package/dist/src/commands/lint-per-patch.d.ts +1 -1
- package/dist/src/commands/lint-per-patch.js +119 -78
- package/dist/src/commands/lint.d.ts +1 -58
- package/dist/src/commands/lint.js +96 -84
- package/dist/src/commands/patch/compact.d.ts +5 -2
- package/dist/src/commands/patch/compact.js +85 -25
- package/dist/src/commands/patch/delete.js +17 -17
- package/dist/src/commands/patch/index.js +2 -0
- package/dist/src/commands/patch/lint-ignore.js +3 -16
- package/dist/src/commands/patch/move-files.js +2 -0
- package/dist/src/commands/patch/patch-context.d.ts +41 -0
- package/dist/src/commands/patch/patch-context.js +53 -0
- package/dist/src/commands/patch/rename.js +10 -15
- package/dist/src/commands/patch/reorder.d.ts +0 -2
- package/dist/src/commands/patch/reorder.js +18 -19
- package/dist/src/commands/patch/split-plan.d.ts +66 -0
- package/dist/src/commands/patch/split-plan.js +178 -0
- package/dist/src/commands/patch/split.d.ts +30 -0
- package/dist/src/commands/patch/split.js +283 -0
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -7
- package/dist/src/commands/patch/staged-dependency.js +4 -17
- package/dist/src/commands/patch/tier.js +4 -17
- package/dist/src/commands/re-export-scan.js +8 -1
- package/dist/src/commands/rebase/summary.d.ts +1 -5
- package/dist/src/commands/rebase/summary.js +1 -1
- package/dist/src/commands/status-output.js +77 -68
- package/dist/src/commands/test-diagnose.d.ts +23 -0
- package/dist/src/commands/test-diagnose.js +210 -0
- package/dist/src/commands/test-run.d.ts +58 -0
- package/dist/src/commands/test-run.js +88 -0
- package/dist/src/commands/test.js +169 -257
- package/dist/src/commands/token.js +15 -1
- package/dist/src/commands/wire.js +109 -78
- package/dist/src/core/build-audit.d.ts +1 -1
- package/dist/src/core/build-audit.js +2 -46
- package/dist/src/core/build-baseline-types.d.ts +38 -0
- package/dist/src/core/build-baseline-types.js +10 -0
- package/dist/src/core/build-baseline.d.ts +1 -31
- package/dist/src/core/build-prepare.d.ts +1 -1
- package/dist/src/core/build-prepare.js +2 -45
- package/dist/src/core/config-paths.d.ts +0 -8
- package/dist/src/core/config-paths.js +4 -4
- package/dist/src/core/config-state.d.ts +0 -6
- package/dist/src/core/config-state.js +1 -1
- package/dist/src/core/config-validate-patch-policy.js +12 -13
- package/dist/src/core/config-validate.js +48 -28
- package/dist/src/core/engine-changes.d.ts +24 -0
- package/dist/src/core/engine-changes.js +64 -0
- package/dist/src/core/firefox-cache.d.ts +0 -5
- package/dist/src/core/firefox-cache.js +1 -1
- package/dist/src/core/firefox-download.d.ts +0 -6
- package/dist/src/core/firefox-download.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +1 -8
- package/dist/src/core/furnace-apply-helpers.js +11 -20
- package/dist/src/core/furnace-apply.d.ts +1 -1
- package/dist/src/core/furnace-apply.js +1 -1
- package/dist/src/core/furnace-checksum-utils.d.ts +7 -0
- package/dist/src/core/furnace-checksum-utils.js +15 -0
- package/dist/src/core/furnace-config-validate.d.ts +31 -0
- package/dist/src/core/furnace-config-validate.js +133 -0
- package/dist/src/core/furnace-config.d.ts +4 -32
- package/dist/src/core/furnace-config.js +15 -111
- package/dist/src/core/furnace-constants.d.ts +0 -10
- package/dist/src/core/furnace-constants.js +2 -2
- package/dist/src/core/furnace-css-fragments.d.ts +79 -0
- package/dist/src/core/furnace-css-fragments.js +243 -0
- package/dist/src/core/furnace-jsconfig.d.ts +63 -0
- package/dist/src/core/furnace-jsconfig.js +171 -0
- package/dist/src/core/furnace-validate-helpers.d.ts +16 -14
- package/dist/src/core/furnace-validate-helpers.js +40 -1
- package/dist/src/core/furnace-validate-registration.js +16 -1
- package/dist/src/core/furnace-validate.js +54 -2
- package/dist/src/core/git-file-ops.d.ts +0 -12
- package/dist/src/core/git-file-ops.js +2 -2
- package/dist/src/core/lint-cache.d.ts +3 -13
- package/dist/src/core/lint-cache.js +11 -5
- package/dist/src/core/mach.d.ts +5 -1
- package/dist/src/core/mach.js +6 -2
- package/dist/src/core/manifest-register.d.ts +5 -16
- package/dist/src/core/manifest-register.js +3 -1
- package/dist/src/core/patch-lint-checkjs.js +53 -7
- package/dist/src/core/patch-lint-jsdoc.js +63 -4
- package/dist/src/core/patch-lint-observer.d.ts +37 -0
- package/dist/src/core/patch-lint-observer.js +168 -0
- package/dist/src/core/patch-lint.js +132 -125
- package/dist/src/core/patch-manifest-io.d.ts +16 -0
- package/dist/src/core/patch-manifest-io.js +44 -2
- package/dist/src/core/patch-manifest-validate.d.ts +1 -8
- package/dist/src/core/patch-manifest-validate.js +1 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -1
- package/dist/src/core/patch-policy.d.ts +0 -4
- package/dist/src/core/patch-policy.js +10 -4
- package/dist/src/core/register-browser-content.d.ts +1 -1
- package/dist/src/core/register-module.d.ts +1 -1
- package/dist/src/core/register-result.d.ts +21 -0
- package/dist/src/core/register-result.js +9 -0
- package/dist/src/core/register-shared-css.d.ts +1 -1
- package/dist/src/core/register-test-manifest.d.ts +1 -1
- package/dist/src/core/test-harness-crash.d.ts +61 -0
- package/dist/src/core/test-harness-crash.js +140 -0
- package/dist/src/core/test-stale-check.d.ts +1 -1
- package/dist/src/core/test-stale-check.js +2 -46
- package/dist/src/core/test-xpcshell-retry.d.ts +1 -1
- package/dist/src/core/test-xpcshell-retry.js +4 -2
- package/dist/src/core/token-dark-mode.js +14 -26
- package/dist/src/core/token-manager.d.ts +4 -0
- package/dist/src/core/token-manager.js +70 -16
- package/dist/src/core/typecheck-shim.d.ts +0 -21
- package/dist/src/core/typecheck-shim.js +26 -4
- package/dist/src/core/wire-utils.js +37 -44
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +105 -0
- package/dist/src/types/furnace.d.ts +12 -1
- package/dist/src/utils/elapsed.d.ts +0 -2
- package/dist/src/utils/elapsed.js +1 -1
- package/dist/src/utils/fs.d.ts +0 -5
- package/dist/src/utils/fs.js +1 -1
- package/dist/src/utils/regex.d.ts +0 -6
- package/dist/src/utils/regex.js +3 -3
- package/dist/src/utils/validation.d.ts +0 -8
- package/dist/src/utils/validation.js +2 -2
- package/package.json +6 -4
|
@@ -3,38 +3,96 @@
|
|
|
3
3
|
* `fireforge patch compact` — closes ordinal gaps in the patch queue.
|
|
4
4
|
*
|
|
5
5
|
* After deletes or splits, patch ordinals may have gaps (e.g. 1, 3, 7).
|
|
6
|
-
* This command renumbers
|
|
7
|
-
*
|
|
6
|
+
* This command renumbers patches to close those gaps in a single atomic
|
|
7
|
+
* operation, preserving relative order. Without a patch policy the whole
|
|
8
|
+
* queue is renumbered from 1; with `patchPolicy.ranges` configured the
|
|
9
|
+
* compaction is range-aware (each category range compacts independently,
|
|
10
|
+
* reserved ranges and out-of-range strays are left untouched).
|
|
8
11
|
*/
|
|
9
|
-
import {
|
|
12
|
+
import { loadConfig } from '../../core/config.js';
|
|
10
13
|
import { appendHistory, confirmDestructive } from '../../core/destructive.js';
|
|
11
14
|
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
12
15
|
import { loadPatchesManifest, renumberPatchesInManifest, } from '../../core/patch-manifest.js';
|
|
13
16
|
import { applyRenameMapToManifest, enforcePatchPolicy } from '../../core/patch-policy.js';
|
|
14
17
|
import { GeneralError } from '../../errors/base.js';
|
|
15
18
|
import { toError } from '../../utils/errors.js';
|
|
16
|
-
import { pathExists } from '../../utils/fs.js';
|
|
17
19
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
18
20
|
import { pickDefined } from '../../utils/options.js';
|
|
21
|
+
import { requirePatchQueue } from './patch-context.js';
|
|
19
22
|
import { rebuildFilenameForOrder } from './reorder.js';
|
|
23
|
+
/** True when `order` falls inside a configured reserved range. */
|
|
24
|
+
function isReservedOrder(policyCfg, order) {
|
|
25
|
+
return (policyCfg.reservedRanges ?? []).some((r) => order >= r.from && order <= r.to);
|
|
26
|
+
}
|
|
20
27
|
/**
|
|
21
|
-
* Computes a rename map that
|
|
22
|
-
*
|
|
28
|
+
* Computes a rename map that closes ordinal gaps.
|
|
29
|
+
*
|
|
30
|
+
* Without a patch policy, all patches are renumbered to 1, 2, 3, … in
|
|
31
|
+
* current sort order (historical behaviour). With `patchPolicy.ranges`
|
|
32
|
+
* configured, compaction happens *within* each category range instead:
|
|
33
|
+
* each range's members are renumbered consecutively starting at the
|
|
34
|
+
* range's first occupied ordinal, skipping reserved orders — mirroring
|
|
35
|
+
* what `evaluateGaps` treats as gapless under `allowGaps: false`.
|
|
36
|
+
* Reserved-range patches and patches outside their category's range are
|
|
37
|
+
* never moved (a global renumber would project them across range
|
|
38
|
+
* boundaries and trip `category-range` refusals).
|
|
23
39
|
*/
|
|
24
|
-
function computeCompactRenameMap(patches) {
|
|
25
|
-
|
|
40
|
+
function computeCompactRenameMap(patches, policyCfg) {
|
|
41
|
+
if (!policyCfg || policyCfg.ranges.length === 0) {
|
|
42
|
+
const sorted = [...patches].sort((a, b) => a.order - b.order);
|
|
43
|
+
const renames = new Map();
|
|
44
|
+
for (const [i, patch] of sorted.entries()) {
|
|
45
|
+
const newOrder = i + 1;
|
|
46
|
+
if (patch.order !== newOrder) {
|
|
47
|
+
renames.set(patch.filename, {
|
|
48
|
+
newOrder,
|
|
49
|
+
newFilename: rebuildFilenameForOrder(patch, newOrder),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return renames;
|
|
54
|
+
}
|
|
26
55
|
const renames = new Map();
|
|
27
|
-
for (const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
56
|
+
for (const range of policyCfg.ranges) {
|
|
57
|
+
const members = patches
|
|
58
|
+
.filter((p) => p.category === range.category &&
|
|
59
|
+
p.order >= range.from &&
|
|
60
|
+
p.order <= range.to &&
|
|
61
|
+
!isReservedOrder(policyCfg, p.order))
|
|
62
|
+
.sort((a, b) => a.order - b.order || a.filename.localeCompare(b.filename));
|
|
63
|
+
if (members.length === 0)
|
|
64
|
+
continue;
|
|
65
|
+
// Anchor at the first occupied ordinal rather than range.from: gap
|
|
66
|
+
// evaluation only requires contiguity between first and last occupied,
|
|
67
|
+
// and anchoring minimizes renames.
|
|
68
|
+
let next = members[0].order;
|
|
69
|
+
for (const patch of members) {
|
|
70
|
+
while (isReservedOrder(policyCfg, next))
|
|
71
|
+
next++;
|
|
72
|
+
if (patch.order !== next) {
|
|
73
|
+
renames.set(patch.filename, {
|
|
74
|
+
newOrder: next,
|
|
75
|
+
newFilename: rebuildFilenameForOrder(patch, next),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
next++;
|
|
34
79
|
}
|
|
35
80
|
}
|
|
36
81
|
return renames;
|
|
37
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Patches a range-aware compact leaves in place because they sit outside
|
|
85
|
+
* their category's configured range (or outside all ranges) without a
|
|
86
|
+
* reserved-range exception. They already violate `category-range`; moving
|
|
87
|
+
* them is a policy decision compact must not make silently.
|
|
88
|
+
*/
|
|
89
|
+
function findCompactStrays(patches, policyCfg) {
|
|
90
|
+
return patches.filter((p) => {
|
|
91
|
+
if (isReservedOrder(policyCfg, p.order))
|
|
92
|
+
return false;
|
|
93
|
+
return !policyCfg.ranges.some((range) => range.category === p.category && p.order >= range.from && p.order <= range.to);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
38
96
|
/**
|
|
39
97
|
* Runs the `patch compact` command: renumbers all patches to close ordinal
|
|
40
98
|
* gaps in a single atomic operation.
|
|
@@ -44,16 +102,18 @@ function computeCompactRenameMap(patches) {
|
|
|
44
102
|
*/
|
|
45
103
|
export async function patchCompactCommand(projectRoot, options = {}) {
|
|
46
104
|
intro(options.dryRun ? 'FireForge patch compact (dry run)' : 'FireForge patch compact');
|
|
47
|
-
const paths = getProjectPaths(projectRoot);
|
|
48
105
|
const config = await loadConfig(projectRoot);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
106
|
+
const { paths, manifest } = await requirePatchQueue(projectRoot);
|
|
107
|
+
const policyCfg = config.patchPolicy;
|
|
108
|
+
if (policyCfg && policyCfg.ranges.length > 0) {
|
|
109
|
+
const strays = findCompactStrays(manifest.patches, policyCfg);
|
|
110
|
+
for (const stray of strays) {
|
|
111
|
+
warn(`${stray.filename} (order ${stray.order}, category ${stray.category}) sits outside its ` +
|
|
112
|
+
'configured category range; compact leaves it in place. Use "fireforge patch reorder" ' +
|
|
113
|
+
'to move it into range first.');
|
|
114
|
+
}
|
|
55
115
|
}
|
|
56
|
-
const renameMap = computeCompactRenameMap(manifest.patches);
|
|
116
|
+
const renameMap = computeCompactRenameMap(manifest.patches, policyCfg);
|
|
57
117
|
if (renameMap.size === 0) {
|
|
58
118
|
info('Patch queue is already compact. Nothing to do.');
|
|
59
119
|
outro('Compact complete (no-op)');
|
|
@@ -91,7 +151,7 @@ export async function patchCompactCommand(projectRoot, options = {}) {
|
|
|
91
151
|
if (!currentManifest) {
|
|
92
152
|
throw new GeneralError('Manifest disappeared while waiting for lock.');
|
|
93
153
|
}
|
|
94
|
-
const currentRenameMap = computeCompactRenameMap(currentManifest.patches);
|
|
154
|
+
const currentRenameMap = computeCompactRenameMap(currentManifest.patches, policyCfg);
|
|
95
155
|
if (currentRenameMap.size === 0) {
|
|
96
156
|
info('Patch queue was compacted by another process. Nothing to do.');
|
|
97
157
|
return;
|
|
@@ -138,7 +198,7 @@ export function registerPatchCompact(parent, context) {
|
|
|
138
198
|
const { getProjectRoot, withErrorHandling } = context;
|
|
139
199
|
parent
|
|
140
200
|
.command('compact')
|
|
141
|
-
.description('Close ordinal gaps in the patch queue (
|
|
201
|
+
.description('Close ordinal gaps in the patch queue (range-aware when patchPolicy.ranges is configured)')
|
|
142
202
|
.option('--dry-run', 'Show what would happen without writing')
|
|
143
203
|
.option('-y, --yes', 'Skip confirmation prompt (required for non-TTY)')
|
|
144
204
|
.option('--force-unsafe', 'Bypass force-mode patchPolicy refusals')
|
|
@@ -8,17 +8,14 @@
|
|
|
8
8
|
* `--dry-run`, and appends to `patches/.fireforge-history.jsonl` on success.
|
|
9
9
|
*/
|
|
10
10
|
import { basename } from 'node:path';
|
|
11
|
-
import { getProjectPaths } from '../../core/config.js';
|
|
12
11
|
import { appendHistory, confirmDestructive } from '../../core/destructive.js';
|
|
13
|
-
import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
|
|
14
12
|
import { buildPatchQueueContext, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, isForwardImportableFile, } from '../../core/patch-lint.js';
|
|
15
13
|
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
16
|
-
import {
|
|
17
|
-
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
14
|
+
import { removePatchFileAndManifest } from '../../core/patch-manifest.js';
|
|
18
15
|
import { toError } from '../../utils/errors.js';
|
|
19
|
-
import { pathExists } from '../../utils/fs.js';
|
|
20
16
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
21
17
|
import { pickDefined } from '../../utils/options.js';
|
|
18
|
+
import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
22
19
|
/**
|
|
23
20
|
* Runs the `patch delete` command: removes a patch file and its manifest
|
|
24
21
|
* row atomically, refusing when a later patch imports a leaf owned by the
|
|
@@ -30,18 +27,10 @@ import { pickDefined } from '../../utils/options.js';
|
|
|
30
27
|
*/
|
|
31
28
|
export async function patchDeleteCommand(projectRoot, identifier, options = {}) {
|
|
32
29
|
intro(options.dryRun ? 'FireForge patch delete (dry run)' : 'FireForge patch delete');
|
|
33
|
-
const paths =
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const manifest = await loadPatchesManifest(paths.patches);
|
|
38
|
-
if (!manifest || manifest.patches.length === 0) {
|
|
39
|
-
throw new GeneralError('No patches in manifest.');
|
|
40
|
-
}
|
|
41
|
-
const target = resolvePatchIdentifier(identifier, manifest.patches);
|
|
42
|
-
if (!target) {
|
|
43
|
-
throw new InvalidArgumentError(formatPatchNotFoundError(identifier, manifest.patches), identifier);
|
|
44
|
-
}
|
|
30
|
+
const { paths, manifest } = await requirePatchQueue(projectRoot, {
|
|
31
|
+
missingDirMessage: 'Patches directory not found. No patches to delete.',
|
|
32
|
+
});
|
|
33
|
+
const target = requirePatchTarget(identifier, manifest.patches);
|
|
45
34
|
// Build the full queue context once so we can scan each patch's newFiles
|
|
46
35
|
// without re-parsing for the dependency check below.
|
|
47
36
|
const baseCtx = await buildPatchQueueContext(paths.patches);
|
|
@@ -113,6 +102,17 @@ export async function patchDeleteCommand(projectRoot, identifier, options = {})
|
|
|
113
102
|
break;
|
|
114
103
|
}
|
|
115
104
|
}
|
|
105
|
+
// Staged-dependency declarations on other patches may name the deleted
|
|
106
|
+
// patch as their forward-import owner. The dangling reference also
|
|
107
|
+
// surfaces via cross-patch lint later, but warning here puts the exact
|
|
108
|
+
// cleanup command in front of the operator at decision time.
|
|
109
|
+
const danglingOwnerHolders = baseCtx.entries.filter((entry) => entry.filename !== target.filename &&
|
|
110
|
+
(entry.metadata?.stagedDependencies?.forwardImports ?? []).some((fi) => fi.owner === target.filename));
|
|
111
|
+
for (const holder of danglingOwnerHolders) {
|
|
112
|
+
warn(`${holder.filename} declares a staged dependency with owner ${target.filename}; ` +
|
|
113
|
+
`after the delete, update it via "fireforge patch staged-dependency ${holder.filename} --remove ..." ` +
|
|
114
|
+
'or re-point the owner at the patch that will create the file.');
|
|
115
|
+
}
|
|
116
116
|
const conflicts = dependents.length > 0
|
|
117
117
|
? {
|
|
118
118
|
// Wording deliberately clarifies the *runtime* impact: `git apply`
|
|
@@ -13,6 +13,7 @@ import { registerPatchLintIgnore } from './lint-ignore.js';
|
|
|
13
13
|
import { registerPatchMoveFiles } from './move-files.js';
|
|
14
14
|
import { registerPatchRename } from './rename.js';
|
|
15
15
|
import { registerPatchReorder } from './reorder.js';
|
|
16
|
+
import { registerPatchSplit } from './split.js';
|
|
16
17
|
import { registerPatchStagedDependency } from './staged-dependency.js';
|
|
17
18
|
import { registerPatchTier } from './tier.js';
|
|
18
19
|
/**
|
|
@@ -40,6 +41,7 @@ export function registerPatch(program, context) {
|
|
|
40
41
|
registerPatchMoveFiles(patch, context);
|
|
41
42
|
registerPatchRename(patch, context);
|
|
42
43
|
registerPatchReorder(patch, context);
|
|
44
|
+
registerPatchSplit(patch, context);
|
|
43
45
|
registerPatchStagedDependency(patch, context);
|
|
44
46
|
registerPatchTier(patch, context);
|
|
45
47
|
}
|
|
@@ -19,15 +19,12 @@
|
|
|
19
19
|
* write — important when an operator scripts repeated invocations or
|
|
20
20
|
* runs `--add` and `--remove` back-to-back.
|
|
21
21
|
*/
|
|
22
|
-
import { getProjectPaths } from '../../core/config.js';
|
|
23
22
|
import { appendHistory } from '../../core/destructive.js';
|
|
24
23
|
import { mutatePatchMetadata } from '../../core/patch-export.js';
|
|
25
|
-
import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
|
|
26
|
-
import { loadPatchesManifest, resolvePatchIdentifier } from '../../core/patch-manifest.js';
|
|
27
24
|
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
28
25
|
import { toError } from '../../utils/errors.js';
|
|
29
|
-
import { pathExists } from '../../utils/fs.js';
|
|
30
26
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
27
|
+
import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
31
28
|
/**
|
|
32
29
|
* Computes the post-mutation `lintIgnore` list for a given mode.
|
|
33
30
|
* Returns `undefined` when the result should drop the field from the
|
|
@@ -108,18 +105,8 @@ export async function patchLintIgnoreCommand(projectRoot, identifier, options =
|
|
|
108
105
|
}
|
|
109
106
|
const mode = adding ? 'add' : removing ? 'remove' : 'clear';
|
|
110
107
|
const values = mode === 'add' ? (options.add ?? []) : mode === 'remove' ? (options.remove ?? []) : [];
|
|
111
|
-
const paths =
|
|
112
|
-
|
|
113
|
-
throw new GeneralError('Patches directory not found.');
|
|
114
|
-
}
|
|
115
|
-
const manifest = await loadPatchesManifest(paths.patches);
|
|
116
|
-
if (!manifest || manifest.patches.length === 0) {
|
|
117
|
-
throw new GeneralError('No patches in manifest.');
|
|
118
|
-
}
|
|
119
|
-
const target = resolvePatchIdentifier(identifier, manifest.patches);
|
|
120
|
-
if (!target) {
|
|
121
|
-
throw new InvalidArgumentError(formatPatchNotFoundError(identifier, manifest.patches), identifier);
|
|
122
|
-
}
|
|
108
|
+
const { paths, manifest } = await requirePatchQueue(projectRoot);
|
|
109
|
+
const target = requirePatchTarget(identifier, manifest.patches);
|
|
123
110
|
if (isDryRun) {
|
|
124
111
|
const existing = target.lintIgnore ?? [];
|
|
125
112
|
const projected = applyMode(existing, mode, values) ?? [];
|
|
@@ -116,6 +116,8 @@ export async function patchMoveFilesCommand(projectRoot, fromIdentifier, toIdent
|
|
|
116
116
|
const applyTarget = formatReExportCommand(target.filename, targetAfter, []);
|
|
117
117
|
note(`${dryRunSource}\n${dryRunTarget}`, 'Preview commands');
|
|
118
118
|
note(`${applySource}\n${applyTarget}`, 'Apply commands');
|
|
119
|
+
info('Tip: to move files into a brand-new patch in one transaction (including ' +
|
|
120
|
+
'staged-dependency owner rewrites), use "fireforge patch split" instead.');
|
|
119
121
|
outro('Move plan complete - no changes made');
|
|
120
122
|
}
|
|
121
123
|
/**
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared preamble for the patch subcommands: every mutation command starts
|
|
3
|
+
* by loading the project paths and the patches manifest and (for the
|
|
4
|
+
* single-patch commands) resolving the operator-supplied identifier. The
|
|
5
|
+
* sequence and its error wording were previously copied into each command;
|
|
6
|
+
* this module is the single source for both.
|
|
7
|
+
*/
|
|
8
|
+
import type { PatchesManifest, PatchMetadata } from '../../types/commands/index.js';
|
|
9
|
+
import type { ProjectPaths } from '../../types/config.js';
|
|
10
|
+
/** Resolved project paths plus the non-empty patches manifest. */
|
|
11
|
+
export interface PatchQueueContext {
|
|
12
|
+
/** Project paths resolved from the project root. */
|
|
13
|
+
paths: ProjectPaths;
|
|
14
|
+
/** The loaded manifest; guaranteed to contain at least one patch. */
|
|
15
|
+
manifest: PatchesManifest;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Loads the project paths and the patches manifest, throwing the shared
|
|
19
|
+
* command-preamble errors when the patches directory is missing or the
|
|
20
|
+
* manifest has no patches.
|
|
21
|
+
*
|
|
22
|
+
* @param projectRoot - Root directory of the project
|
|
23
|
+
* @param options - Optional overrides for the preamble error wording
|
|
24
|
+
* @param options.missingDirMessage - Replacement for the default
|
|
25
|
+
* "Patches directory not found." error (e.g. `patch delete` appends
|
|
26
|
+
* "No patches to delete.")
|
|
27
|
+
* @returns The resolved paths and the non-empty manifest
|
|
28
|
+
*/
|
|
29
|
+
export declare function requirePatchQueue(projectRoot: string, options?: {
|
|
30
|
+
missingDirMessage?: string;
|
|
31
|
+
}): Promise<PatchQueueContext>;
|
|
32
|
+
/**
|
|
33
|
+
* Resolves an operator-supplied patch identifier (order number, filename,
|
|
34
|
+
* or unique name fragment) against the manifest, throwing the shared
|
|
35
|
+
* not-found error with suggestions when no patch matches.
|
|
36
|
+
*
|
|
37
|
+
* @param identifier - Identifier as passed on the command line
|
|
38
|
+
* @param patches - Manifest rows to resolve against
|
|
39
|
+
* @returns The matching manifest row
|
|
40
|
+
*/
|
|
41
|
+
export declare function requirePatchTarget(identifier: string, patches: PatchMetadata[]): PatchMetadata;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Shared preamble for the patch subcommands: every mutation command starts
|
|
4
|
+
* by loading the project paths and the patches manifest and (for the
|
|
5
|
+
* single-patch commands) resolving the operator-supplied identifier. The
|
|
6
|
+
* sequence and its error wording were previously copied into each command;
|
|
7
|
+
* this module is the single source for both.
|
|
8
|
+
*/
|
|
9
|
+
import { getProjectPaths } from '../../core/config.js';
|
|
10
|
+
import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
|
|
11
|
+
import { loadPatchesManifest, resolvePatchIdentifier } from '../../core/patch-manifest.js';
|
|
12
|
+
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
13
|
+
import { pathExists } from '../../utils/fs.js';
|
|
14
|
+
/**
|
|
15
|
+
* Loads the project paths and the patches manifest, throwing the shared
|
|
16
|
+
* command-preamble errors when the patches directory is missing or the
|
|
17
|
+
* manifest has no patches.
|
|
18
|
+
*
|
|
19
|
+
* @param projectRoot - Root directory of the project
|
|
20
|
+
* @param options - Optional overrides for the preamble error wording
|
|
21
|
+
* @param options.missingDirMessage - Replacement for the default
|
|
22
|
+
* "Patches directory not found." error (e.g. `patch delete` appends
|
|
23
|
+
* "No patches to delete.")
|
|
24
|
+
* @returns The resolved paths and the non-empty manifest
|
|
25
|
+
*/
|
|
26
|
+
export async function requirePatchQueue(projectRoot, options = {}) {
|
|
27
|
+
const paths = getProjectPaths(projectRoot);
|
|
28
|
+
if (!(await pathExists(paths.patches))) {
|
|
29
|
+
throw new GeneralError(options.missingDirMessage ?? 'Patches directory not found.');
|
|
30
|
+
}
|
|
31
|
+
const manifest = await loadPatchesManifest(paths.patches);
|
|
32
|
+
if (!manifest || manifest.patches.length === 0) {
|
|
33
|
+
throw new GeneralError('No patches in manifest.');
|
|
34
|
+
}
|
|
35
|
+
return { paths, manifest };
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolves an operator-supplied patch identifier (order number, filename,
|
|
39
|
+
* or unique name fragment) against the manifest, throwing the shared
|
|
40
|
+
* not-found error with suggestions when no patch matches.
|
|
41
|
+
*
|
|
42
|
+
* @param identifier - Identifier as passed on the command line
|
|
43
|
+
* @param patches - Manifest rows to resolve against
|
|
44
|
+
* @returns The matching manifest row
|
|
45
|
+
*/
|
|
46
|
+
export function requirePatchTarget(identifier, patches) {
|
|
47
|
+
const target = resolvePatchIdentifier(identifier, patches);
|
|
48
|
+
if (!target) {
|
|
49
|
+
throw new InvalidArgumentError(formatPatchNotFoundError(identifier, patches), identifier);
|
|
50
|
+
}
|
|
51
|
+
return target;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=patch-context.js.map
|
|
@@ -18,18 +18,18 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { rename as fsRename } from 'node:fs/promises';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
|
-
import {
|
|
21
|
+
import { loadConfig } from '../../core/config.js';
|
|
22
22
|
import { appendHistory, confirmDestructive } from '../../core/destructive.js';
|
|
23
23
|
import { sanitizeName } from '../../core/patch-export.js';
|
|
24
|
-
import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
|
|
25
24
|
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
26
|
-
import { loadPatchesManifest,
|
|
25
|
+
import { loadPatchesManifest, rewriteStagedDependencyOwners, savePatchesManifest, } from '../../core/patch-manifest.js';
|
|
27
26
|
import { buildProjectedManifest, enforcePatchPolicy } from '../../core/patch-policy.js';
|
|
28
27
|
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
29
28
|
import { toError } from '../../utils/errors.js';
|
|
30
29
|
import { pathExists } from '../../utils/fs.js';
|
|
31
30
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
32
31
|
import { pickDefined } from '../../utils/options.js';
|
|
32
|
+
import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
33
33
|
/**
|
|
34
34
|
* Pulls the ordinal-string + category prefix out of a patch filename so
|
|
35
35
|
* the rename keeps the existing ordinal padding verbatim. Returning the
|
|
@@ -85,6 +85,11 @@ async function commitRenameUnderLock(input) {
|
|
|
85
85
|
name: newName,
|
|
86
86
|
...(descriptionChanging ? { description: newDescription ?? '' } : {}),
|
|
87
87
|
};
|
|
88
|
+
// Staged-dependency owners on other patches reference the old
|
|
89
|
+
// filename; remap them so forward-import declarations survive the
|
|
90
|
+
// rename instead of dangling.
|
|
91
|
+
const ownerLookup = (old) => old === target.filename ? newFilename : undefined;
|
|
92
|
+
fresh.patches = fresh.patches.map((p) => rewriteStagedDependencyOwners(p, ownerLookup));
|
|
88
93
|
}
|
|
89
94
|
else {
|
|
90
95
|
fresh.patches[idx] = {
|
|
@@ -151,19 +156,9 @@ export async function patchRenameCommand(projectRoot, identifier, options = {})
|
|
|
151
156
|
if (options.to === undefined || options.to.trim() === '') {
|
|
152
157
|
throw new InvalidArgumentError('Specify --to <new-name>. The new name is sanitised into the filename slug the same way `export --name` is.', 'patch rename');
|
|
153
158
|
}
|
|
154
|
-
const paths = getProjectPaths(projectRoot);
|
|
155
159
|
const config = await loadConfig(projectRoot);
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
const manifest = await loadPatchesManifest(paths.patches);
|
|
160
|
-
if (!manifest || manifest.patches.length === 0) {
|
|
161
|
-
throw new GeneralError('No patches in manifest.');
|
|
162
|
-
}
|
|
163
|
-
const target = resolvePatchIdentifier(identifier, manifest.patches);
|
|
164
|
-
if (!target) {
|
|
165
|
-
throw new InvalidArgumentError(formatPatchNotFoundError(identifier, manifest.patches), identifier);
|
|
166
|
-
}
|
|
160
|
+
const { paths, manifest } = await requirePatchQueue(projectRoot);
|
|
161
|
+
const target = requirePatchTarget(identifier, manifest.patches);
|
|
167
162
|
const split = splitPatchFilename(target.filename);
|
|
168
163
|
if (!split) {
|
|
169
164
|
throw new GeneralError(`Cannot rename ${target.filename}: filename does not match the expected {ordinal}-{category}-{slug}.patch convention. Re-export the patch instead.`);
|
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
import { Command } from 'commander';
|
|
11
11
|
import type { CommandContext } from '../../types/cli.js';
|
|
12
12
|
import type { PatchMetadata, PatchReorderOptions } from '../../types/commands/index.js';
|
|
13
|
-
/** Zero-pads an ordinal number to the given width. */
|
|
14
|
-
export declare function padOrder(value: number, width: number): string;
|
|
15
13
|
/** Builds a new patch filename by replacing the numeric prefix with `newOrder`. */
|
|
16
14
|
export declare function rebuildFilenameForOrder(existing: PatchMetadata, newOrder: number): string;
|
|
17
15
|
/**
|
|
@@ -9,21 +9,20 @@
|
|
|
9
9
|
* before any bytes move.
|
|
10
10
|
*/
|
|
11
11
|
import { Option } from 'commander';
|
|
12
|
-
import {
|
|
12
|
+
import { loadConfig } from '../../core/config.js';
|
|
13
13
|
import { appendHistory, confirmDestructive, } from '../../core/destructive.js';
|
|
14
|
-
import { formatPatchNotFoundError } from '../../core/patch-identifier-suggest.js';
|
|
15
14
|
import { buildPatchQueueContext, lintPatchQueue, } from '../../core/patch-lint.js';
|
|
16
15
|
import { withPatchDirectoryLock } from '../../core/patch-lock.js';
|
|
17
|
-
import { loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, } from '../../core/patch-manifest.js';
|
|
16
|
+
import { loadPatchesManifest, renumberPatchesInManifest, resolvePatchIdentifier, rewriteStagedDependencyOwners, } from '../../core/patch-manifest.js';
|
|
18
17
|
import { applyRenameMapToManifest, enforcePatchPolicy } from '../../core/patch-policy.js';
|
|
19
18
|
import { GeneralError, InvalidArgumentError } from '../../errors/base.js';
|
|
20
19
|
import { toError } from '../../utils/errors.js';
|
|
21
|
-
import { pathExists } from '../../utils/fs.js';
|
|
22
20
|
import { info, intro, outro, warn } from '../../utils/logger.js';
|
|
23
21
|
import { pickDefined } from '../../utils/options.js';
|
|
24
22
|
import { parsePositiveIntegerFlag } from '../../utils/validation.js';
|
|
23
|
+
import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
25
24
|
/** Zero-pads an ordinal number to the given width. */
|
|
26
|
-
|
|
25
|
+
function padOrder(value, width) {
|
|
27
26
|
return String(value).padStart(width, '0');
|
|
28
27
|
}
|
|
29
28
|
/** Builds a new patch filename by replacing the numeric prefix with `newOrder`. */
|
|
@@ -123,12 +122,22 @@ function renameMapsEqual(left, right) {
|
|
|
123
122
|
* can run against the projected state without touching disk.
|
|
124
123
|
*/
|
|
125
124
|
function projectReorder(base, renameMap) {
|
|
125
|
+
const ownerLookup = (oldFilename) => renameMap.get(oldFilename)?.newFilename;
|
|
126
126
|
const projectedEntries = base.entries.map((entry) => {
|
|
127
|
+
// Project staged-dependency owner references through the rename map on
|
|
128
|
+
// every entry — owners point at *other* patches' filenames, so a
|
|
129
|
+
// projection that skips non-renamed entries would lint against stale
|
|
130
|
+
// owners and report false forward-import regressions.
|
|
131
|
+
const metadata = entry.metadata
|
|
132
|
+
? rewriteStagedDependencyOwners(entry.metadata, ownerLookup)
|
|
133
|
+
: entry.metadata;
|
|
127
134
|
const rename = renameMap.get(entry.filename);
|
|
128
|
-
if (!rename)
|
|
129
|
-
return entry;
|
|
135
|
+
if (!rename) {
|
|
136
|
+
return metadata === entry.metadata ? entry : { ...entry, metadata };
|
|
137
|
+
}
|
|
130
138
|
return {
|
|
131
139
|
...entry,
|
|
140
|
+
metadata,
|
|
132
141
|
filename: rename.newFilename,
|
|
133
142
|
order: rename.newOrder,
|
|
134
143
|
};
|
|
@@ -268,19 +277,9 @@ export async function patchReorderCommand(projectRoot, identifier, options = {})
|
|
|
268
277
|
if (specifiedTargets > 1) {
|
|
269
278
|
throw new InvalidArgumentError('--to, --before, and --after are mutually exclusive.', 'patch reorder');
|
|
270
279
|
}
|
|
271
|
-
const paths = getProjectPaths(projectRoot);
|
|
272
280
|
const config = await loadConfig(projectRoot);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
276
|
-
const manifest = await loadPatchesManifest(paths.patches);
|
|
277
|
-
if (!manifest || manifest.patches.length === 0) {
|
|
278
|
-
throw new GeneralError('No patches in manifest.');
|
|
279
|
-
}
|
|
280
|
-
const target = resolvePatchIdentifier(identifier, manifest.patches);
|
|
281
|
-
if (!target) {
|
|
282
|
-
throw new InvalidArgumentError(formatPatchNotFoundError(identifier, manifest.patches), identifier);
|
|
283
|
-
}
|
|
281
|
+
const { paths, manifest } = await requirePatchQueue(projectRoot);
|
|
282
|
+
const target = requirePatchTarget(identifier, manifest.patches);
|
|
284
283
|
const { destinationOrder, anchorFilename } = resolveDestination(target, manifest.patches, options);
|
|
285
284
|
const renameMap = computeRenameMap(manifest.patches, target, destinationOrder);
|
|
286
285
|
if (renameMap.size === 0) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Planning helpers for `fireforge patch split`: ownership guards, diff
|
|
3
|
+
* construction from the worktree, staged-dependency owner-rewrite
|
|
4
|
+
* discovery, cross-patch lint projection, and policy-manifest projection.
|
|
5
|
+
* Split out of `split.ts` to keep both files within the per-file line
|
|
6
|
+
* budget; consumed only by the split command.
|
|
7
|
+
*/
|
|
8
|
+
import { type ConflictReport } from '../../core/destructive.js';
|
|
9
|
+
import { buildProjectedManifest } from '../../core/patch-policy.js';
|
|
10
|
+
import type { PatchCategory, PatchMetadata } from '../../types/commands/index.js';
|
|
11
|
+
import type { FireForgeConfig } from '../../types/config.js';
|
|
12
|
+
import { type PlacementPlan } from '../export-flow.js';
|
|
13
|
+
/** Everything the commit step needs, computed and confirmed up front. */
|
|
14
|
+
export interface SplitPlan {
|
|
15
|
+
source: PatchMetadata;
|
|
16
|
+
movedFiles: string[];
|
|
17
|
+
remainingFiles: string[];
|
|
18
|
+
movedDiff: string;
|
|
19
|
+
remainingDiff: string;
|
|
20
|
+
placement: PlacementPlan;
|
|
21
|
+
/** Effective placement flags (with the after-source default applied). */
|
|
22
|
+
placementOptions: {
|
|
23
|
+
order?: number;
|
|
24
|
+
before?: string;
|
|
25
|
+
after?: string;
|
|
26
|
+
};
|
|
27
|
+
category: PatchCategory;
|
|
28
|
+
name: string;
|
|
29
|
+
description: string;
|
|
30
|
+
/** Patches (by current filename) whose staged-dependency owners re-point to the new patch. */
|
|
31
|
+
ownerRewrites: string[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
*
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertSourceOwnsFiles(source: PatchMetadata, files: readonly string[]): void;
|
|
37
|
+
/**
|
|
38
|
+
*
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildSplitDiff(engineDir: string, files: readonly string[], label: string, sourceFilename: string): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Finds patches declaring a staged-dependency forward-import whose `owner`
|
|
43
|
+
* is the source patch and whose `creates` path moves to the new patch.
|
|
44
|
+
*/
|
|
45
|
+
export declare function findOwnerRewriteHolders(patches: readonly PatchMetadata[], sourceFilename: string, movedSet: ReadonlySet<string>): string[];
|
|
46
|
+
/** Rewrites split-affected owners on one manifest row. */
|
|
47
|
+
export declare function rewriteSplitOwners(patch: PatchMetadata, sourceFilename: string, movedSet: ReadonlySet<string>, newFilename: string): PatchMetadata;
|
|
48
|
+
/**
|
|
49
|
+
* Projects the full split (renumber + shrunken source + synthetic new
|
|
50
|
+
* patch + owner rewrites) through cross-patch lint, reporting only the
|
|
51
|
+
* regressions the split itself would introduce.
|
|
52
|
+
*/
|
|
53
|
+
export declare function runProjectedSplitLint(patchesDir: string, plan: SplitPlan): Promise<ConflictReport | null>;
|
|
54
|
+
/** Builds the projected manifest for policy enforcement. */
|
|
55
|
+
export declare function projectSplitManifest(manifest: {
|
|
56
|
+
version: 1;
|
|
57
|
+
patches: PatchMetadata[];
|
|
58
|
+
}, plan: SplitPlan, newMetadata: PatchMetadata): ReturnType<typeof buildProjectedManifest>;
|
|
59
|
+
/**
|
|
60
|
+
*
|
|
61
|
+
*/
|
|
62
|
+
export declare function buildNewPatchMetadata(plan: SplitPlan, config: FireForgeConfig): PatchMetadata;
|
|
63
|
+
/**
|
|
64
|
+
*
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildSplitSummary(plan: SplitPlan): string[];
|