@hominis/fireforge 0.21.0 → 0.21.2
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 +2 -0
- package/README.md +41 -0
- package/dist/src/commands/config.js +5 -0
- package/dist/src/commands/export-all.js +10 -6
- package/dist/src/commands/export-flow.d.ts +10 -0
- package/dist/src/commands/export-flow.js +50 -2
- package/dist/src/commands/export-shared.d.ts +1 -1
- package/dist/src/commands/export-shared.js +12 -13
- package/dist/src/commands/export.js +40 -4
- package/dist/src/commands/furnace/create-templates.js +10 -3
- package/dist/src/commands/furnace/create.js +1 -0
- package/dist/src/commands/furnace/deploy.js +1 -1
- package/dist/src/commands/furnace/validation-output.d.ts +2 -2
- package/dist/src/commands/furnace/validation-output.js +20 -4
- package/dist/src/commands/lint.js +9 -0
- package/dist/src/commands/patch/rename.js +40 -9
- package/dist/src/commands/patch/reorder.js +17 -3
- package/dist/src/commands/re-export-files.js +16 -1
- package/dist/src/commands/re-export.js +21 -10
- package/dist/src/commands/verify.js +15 -1
- package/dist/src/core/config-paths.d.ts +2 -2
- package/dist/src/core/config-paths.js +2 -0
- package/dist/src/core/config-validate-patch-policy.d.ts +7 -0
- package/dist/src/core/config-validate-patch-policy.js +176 -0
- package/dist/src/core/config-validate.js +6 -0
- package/dist/src/core/furnace-config-order.d.ts +7 -0
- package/dist/src/core/furnace-config-order.js +86 -0
- package/dist/src/core/furnace-config.js +13 -1
- package/dist/src/core/furnace-validate.js +3 -0
- package/dist/src/core/patch-export-coverage.d.ts +58 -0
- package/dist/src/core/patch-export-coverage.js +103 -0
- package/dist/src/core/patch-export-metadata.d.ts +36 -0
- package/dist/src/core/patch-export-metadata.js +69 -0
- package/dist/src/core/patch-export-update.d.ts +20 -0
- package/dist/src/core/patch-export-update.js +67 -0
- package/dist/src/core/patch-export.d.ts +13 -153
- package/dist/src/core/patch-export.js +23 -262
- package/dist/src/core/patch-manifest-validate.js +2 -2
- package/dist/src/core/patch-policy.d.ts +47 -0
- package/dist/src/core/patch-policy.js +350 -0
- package/dist/src/types/commands/options.d.ts +2 -0
- package/dist/src/types/commands/patches.d.ts +1 -1
- package/dist/src/types/config.d.ts +51 -0
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@ import { isObject, isString } from '../utils/validation.js';
|
|
|
8
8
|
import { FIREFORGE_DIR } from './config.js';
|
|
9
9
|
import { parseStringArray } from './furnace-config-array-utils.js';
|
|
10
10
|
import { parseCustomConfig } from './furnace-config-custom.js';
|
|
11
|
+
import { orderFurnaceConfigForWrite } from './furnace-config-order.js';
|
|
11
12
|
import { validateRuntimeVariables, validateTokenHostDocuments } from './furnace-config-tokens.js';
|
|
12
13
|
import { resolveFtlDir } from './furnace-constants.js';
|
|
13
14
|
import { detectComposesCycles, validateComposesReferences } from './furnace-graph-utils.js';
|
|
@@ -410,7 +411,18 @@ export async function loadFurnaceConfig(root) {
|
|
|
410
411
|
*/
|
|
411
412
|
export async function writeFurnaceConfig(root, config) {
|
|
412
413
|
const paths = getFurnacePaths(root);
|
|
413
|
-
|
|
414
|
+
let existing;
|
|
415
|
+
if (await pathExists(paths.furnaceConfig)) {
|
|
416
|
+
try {
|
|
417
|
+
const raw = await readJson(paths.furnaceConfig);
|
|
418
|
+
if (isObject(raw))
|
|
419
|
+
existing = raw;
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
existing = undefined;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
await writeJson(paths.furnaceConfig, orderFurnaceConfigForWrite(existing, config));
|
|
414
426
|
}
|
|
415
427
|
/**
|
|
416
428
|
* Stamps every override's `baseVersion` to the supplied version. Used by
|
|
@@ -233,6 +233,9 @@ async function findOrphanXpcshellScaffolds(root, config) {
|
|
|
233
233
|
for (const entry of entries) {
|
|
234
234
|
if (known.has(entry))
|
|
235
235
|
continue;
|
|
236
|
+
const chromeDocPackagingTest = join(parentAbs, entry, `test_${entry}_packaging.js`);
|
|
237
|
+
if (await pathExists(chromeDocPackagingTest))
|
|
238
|
+
continue;
|
|
236
239
|
issues.push({
|
|
237
240
|
component: entry,
|
|
238
241
|
severity: 'error',
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patch coverage and supersession helpers used by export planning.
|
|
3
|
+
*/
|
|
4
|
+
import type { PatchInfo, PatchMetadata } from '../types/commands/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Finds patches that are completely superseded by newer patches.
|
|
7
|
+
* A patch is superseded if all its affected files are covered by newer patches.
|
|
8
|
+
* @param patchesDir - Path to the patches directory
|
|
9
|
+
* @param newPatchFiles - Files affected by the new patch
|
|
10
|
+
* @param excludeFilename - Filename to exclude from results (the new patch itself)
|
|
11
|
+
* @returns Superseded patches
|
|
12
|
+
*/
|
|
13
|
+
export declare function findSupersededPatches(patchesDir: string, newPatchFiles: string[], excludeFilename?: string): Promise<PatchInfo[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Report whether a patch is fully covered by a new export, and which of its
|
|
16
|
+
* files caused the coverage.
|
|
17
|
+
*/
|
|
18
|
+
export interface PatchCoverage {
|
|
19
|
+
covered: boolean;
|
|
20
|
+
byFiles: string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Checks whether a patch is fully covered by a new export.
|
|
24
|
+
* A patch is fully covered when every file it affects is present in the new export.
|
|
25
|
+
* @param patchFiles - Files affected by the existing patch
|
|
26
|
+
* @param targetFiles - Files affected by the new export
|
|
27
|
+
* @returns Coverage report with the triggering file list when `covered` is true
|
|
28
|
+
*/
|
|
29
|
+
export declare function isPatchFullyCovered(patchFiles: string[], targetFiles: string[]): PatchCoverage;
|
|
30
|
+
/**
|
|
31
|
+
* Finds patches whose filesAffected entries are fully covered by the specified files.
|
|
32
|
+
* Used for complete supersession when exporting full-file patches.
|
|
33
|
+
* @param patchesDir - Path to the patches directory
|
|
34
|
+
* @param targetFiles - Files affected by the new export
|
|
35
|
+
* @param excludeFilename - Filename to exclude from results (the new patch itself)
|
|
36
|
+
* @returns Patches that are fully covered by the new export
|
|
37
|
+
*/
|
|
38
|
+
export declare function findAllPatchesForFiles(patchesDir: string, targetFiles: string[], excludeFilename?: string): Promise<PatchInfo[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Describes which files in a covered patch triggered its supersession.
|
|
41
|
+
* Returned from `planExport` so dry-run previews can render a complete
|
|
42
|
+
* "moved / removed" picture rather than a bare patch count.
|
|
43
|
+
*/
|
|
44
|
+
export interface SupersedeCoverageDetail {
|
|
45
|
+
/** Existing patch filename. */
|
|
46
|
+
filename: string;
|
|
47
|
+
/** Files the existing patch claimed that the new export also claims. */
|
|
48
|
+
coveredByFiles: string[];
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolves coverage details for every existing patch that the new export
|
|
52
|
+
* would fully cover.
|
|
53
|
+
*/
|
|
54
|
+
export declare function findAllPatchesForFilesWithDetails(patchesDir: string, targetFiles: string[], excludeFilename?: string): Promise<{
|
|
55
|
+
patch: PatchInfo;
|
|
56
|
+
coverage: PatchCoverage;
|
|
57
|
+
metadata: PatchMetadata;
|
|
58
|
+
}[]>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Patch coverage and supersession helpers used by export planning.
|
|
4
|
+
*/
|
|
5
|
+
import { discoverPatches, isNewFilePatch } from './patch-apply.js';
|
|
6
|
+
import { loadPatchesManifest } from './patch-manifest.js';
|
|
7
|
+
/**
|
|
8
|
+
* Finds patches that are completely superseded by newer patches.
|
|
9
|
+
* A patch is superseded if all its affected files are covered by newer patches.
|
|
10
|
+
* @param patchesDir - Path to the patches directory
|
|
11
|
+
* @param newPatchFiles - Files affected by the new patch
|
|
12
|
+
* @param excludeFilename - Filename to exclude from results (the new patch itself)
|
|
13
|
+
* @returns Superseded patches
|
|
14
|
+
*/
|
|
15
|
+
export async function findSupersededPatches(patchesDir, newPatchFiles, excludeFilename) {
|
|
16
|
+
const manifest = await loadPatchesManifest(patchesDir);
|
|
17
|
+
if (!manifest)
|
|
18
|
+
return [];
|
|
19
|
+
const patches = await discoverPatches(patchesDir);
|
|
20
|
+
const superseded = [];
|
|
21
|
+
for (const metadata of manifest.patches) {
|
|
22
|
+
if (excludeFilename && metadata.filename === excludeFilename)
|
|
23
|
+
continue;
|
|
24
|
+
if (metadata.filesAffected.length === 1) {
|
|
25
|
+
const affectedFile = metadata.filesAffected[0];
|
|
26
|
+
if (affectedFile && newPatchFiles.includes(affectedFile)) {
|
|
27
|
+
const patch = patches.find((p) => p.filename === metadata.filename);
|
|
28
|
+
if (patch && (await isNewFilePatch(patch.path))) {
|
|
29
|
+
superseded.push(patch);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return superseded;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Checks whether a patch is fully covered by a new export.
|
|
38
|
+
* A patch is fully covered when every file it affects is present in the new export.
|
|
39
|
+
* @param patchFiles - Files affected by the existing patch
|
|
40
|
+
* @param targetFiles - Files affected by the new export
|
|
41
|
+
* @returns Coverage report with the triggering file list when `covered` is true
|
|
42
|
+
*/
|
|
43
|
+
export function isPatchFullyCovered(patchFiles, targetFiles) {
|
|
44
|
+
if (patchFiles.length === 0) {
|
|
45
|
+
return { covered: false, byFiles: [] };
|
|
46
|
+
}
|
|
47
|
+
const targetFileSet = new Set(targetFiles);
|
|
48
|
+
const covered = patchFiles.every((file) => targetFileSet.has(file));
|
|
49
|
+
return {
|
|
50
|
+
covered,
|
|
51
|
+
byFiles: covered ? [...patchFiles] : [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Finds patches whose filesAffected entries are fully covered by the specified files.
|
|
56
|
+
* Used for complete supersession when exporting full-file patches.
|
|
57
|
+
* @param patchesDir - Path to the patches directory
|
|
58
|
+
* @param targetFiles - Files affected by the new export
|
|
59
|
+
* @param excludeFilename - Filename to exclude from results (the new patch itself)
|
|
60
|
+
* @returns Patches that are fully covered by the new export
|
|
61
|
+
*/
|
|
62
|
+
export async function findAllPatchesForFiles(patchesDir, targetFiles, excludeFilename) {
|
|
63
|
+
const manifest = await loadPatchesManifest(patchesDir);
|
|
64
|
+
if (!manifest)
|
|
65
|
+
return [];
|
|
66
|
+
const patches = await discoverPatches(patchesDir);
|
|
67
|
+
const superseded = [];
|
|
68
|
+
for (const metadata of manifest.patches) {
|
|
69
|
+
if (excludeFilename && metadata.filename === excludeFilename)
|
|
70
|
+
continue;
|
|
71
|
+
if (isPatchFullyCovered(metadata.filesAffected, targetFiles).covered) {
|
|
72
|
+
const patch = patches.find((p) => p.filename === metadata.filename);
|
|
73
|
+
if (patch) {
|
|
74
|
+
superseded.push(patch);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return superseded;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolves coverage details for every existing patch that the new export
|
|
82
|
+
* would fully cover.
|
|
83
|
+
*/
|
|
84
|
+
export async function findAllPatchesForFilesWithDetails(patchesDir, targetFiles, excludeFilename) {
|
|
85
|
+
const manifest = await loadPatchesManifest(patchesDir);
|
|
86
|
+
if (!manifest)
|
|
87
|
+
return [];
|
|
88
|
+
const patches = await discoverPatches(patchesDir);
|
|
89
|
+
const results = [];
|
|
90
|
+
for (const metadata of manifest.patches) {
|
|
91
|
+
if (excludeFilename && metadata.filename === excludeFilename)
|
|
92
|
+
continue;
|
|
93
|
+
const coverage = isPatchFullyCovered(metadata.filesAffected, targetFiles);
|
|
94
|
+
if (!coverage.covered)
|
|
95
|
+
continue;
|
|
96
|
+
const patch = patches.find((p) => p.filename === metadata.filename);
|
|
97
|
+
if (!patch)
|
|
98
|
+
continue;
|
|
99
|
+
results.push({ patch, coverage, metadata });
|
|
100
|
+
}
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=patch-export-coverage.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest metadata mutation helpers for patch export commands.
|
|
3
|
+
*/
|
|
4
|
+
import type { PatchMetadata } from '../types/commands/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Optional `PatchMetadata` keys safe to clear via the helpers below.
|
|
7
|
+
*/
|
|
8
|
+
export type ClearablePatchMetadataField = 'tier' | 'lintIgnore';
|
|
9
|
+
/**
|
|
10
|
+
* Updates metadata for a patch in the manifest.
|
|
11
|
+
*
|
|
12
|
+
* @param patchesDir - Path to the patches directory
|
|
13
|
+
* @param filename - Patch filename
|
|
14
|
+
* @param updates - Field values to set
|
|
15
|
+
* @param unsetFields - Optional fields to remove from the entry
|
|
16
|
+
*/
|
|
17
|
+
export declare function updatePatchMetadata(patchesDir: string, filename: string, updates: Partial<PatchMetadata>, unsetFields?: ReadonlyArray<ClearablePatchMetadataField>): Promise<void>;
|
|
18
|
+
/** Return shape from a `mutatePatchMetadata` mutator. */
|
|
19
|
+
export interface PatchMetadataMutation {
|
|
20
|
+
/** Field values to set on the entry. */
|
|
21
|
+
set?: Partial<PatchMetadata>;
|
|
22
|
+
/** Optional fields to remove from the entry entirely. */
|
|
23
|
+
unset?: ReadonlyArray<ClearablePatchMetadataField>;
|
|
24
|
+
}
|
|
25
|
+
/** Result of a successful `mutatePatchMetadata` call. */
|
|
26
|
+
export interface PatchMetadataMutationResult {
|
|
27
|
+
/** Pre-mutation snapshot of the patch's metadata. */
|
|
28
|
+
before: PatchMetadata;
|
|
29
|
+
/** Post-mutation state of the patch's metadata. */
|
|
30
|
+
after: PatchMetadata;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Reads a patch's metadata under the directory lock, applies a mutator
|
|
34
|
+
* function to compute the update, and writes the result back.
|
|
35
|
+
*/
|
|
36
|
+
export declare function mutatePatchMetadata(patchesDir: string, filename: string, mutator: (existing: PatchMetadata) => PatchMetadataMutation): Promise<PatchMetadataMutationResult | null>;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Manifest metadata mutation helpers for patch export commands.
|
|
4
|
+
*/
|
|
5
|
+
import { withPatchDirectoryLock } from './patch-apply.js';
|
|
6
|
+
import { loadPatchesManifest, savePatchesManifest } from './patch-manifest.js';
|
|
7
|
+
/**
|
|
8
|
+
* Merges `updates` onto `existing` and removes the listed optional fields.
|
|
9
|
+
*/
|
|
10
|
+
function applyMetadataUpdate(existing, updates, unset) {
|
|
11
|
+
const next = { ...existing, ...updates };
|
|
12
|
+
for (const field of unset) {
|
|
13
|
+
switch (field) {
|
|
14
|
+
case 'tier':
|
|
15
|
+
delete next.tier;
|
|
16
|
+
break;
|
|
17
|
+
case 'lintIgnore':
|
|
18
|
+
delete next.lintIgnore;
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return next;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Updates metadata for a patch in the manifest.
|
|
26
|
+
*
|
|
27
|
+
* @param patchesDir - Path to the patches directory
|
|
28
|
+
* @param filename - Patch filename
|
|
29
|
+
* @param updates - Field values to set
|
|
30
|
+
* @param unsetFields - Optional fields to remove from the entry
|
|
31
|
+
*/
|
|
32
|
+
export async function updatePatchMetadata(patchesDir, filename, updates, unsetFields = []) {
|
|
33
|
+
await withPatchDirectoryLock(patchesDir, async () => {
|
|
34
|
+
const manifest = await loadPatchesManifest(patchesDir);
|
|
35
|
+
if (!manifest)
|
|
36
|
+
return;
|
|
37
|
+
const patchIndex = manifest.patches.findIndex((p) => p.filename === filename);
|
|
38
|
+
if (patchIndex === -1)
|
|
39
|
+
return;
|
|
40
|
+
const existingPatch = manifest.patches[patchIndex];
|
|
41
|
+
if (existingPatch) {
|
|
42
|
+
manifest.patches[patchIndex] = applyMetadataUpdate(existingPatch, updates, unsetFields);
|
|
43
|
+
await savePatchesManifest(patchesDir, manifest);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reads a patch's metadata under the directory lock, applies a mutator
|
|
49
|
+
* function to compute the update, and writes the result back.
|
|
50
|
+
*/
|
|
51
|
+
export async function mutatePatchMetadata(patchesDir, filename, mutator) {
|
|
52
|
+
return await withPatchDirectoryLock(patchesDir, async () => {
|
|
53
|
+
const manifest = await loadPatchesManifest(patchesDir);
|
|
54
|
+
if (!manifest)
|
|
55
|
+
return null;
|
|
56
|
+
const patchIndex = manifest.patches.findIndex((p) => p.filename === filename);
|
|
57
|
+
if (patchIndex === -1)
|
|
58
|
+
return null;
|
|
59
|
+
const existingPatch = manifest.patches[patchIndex];
|
|
60
|
+
if (!existingPatch)
|
|
61
|
+
return null;
|
|
62
|
+
const { set = {}, unset = [] } = mutator(existingPatch);
|
|
63
|
+
const updatedPatch = applyMetadataUpdate(existingPatch, set, unset);
|
|
64
|
+
manifest.patches[patchIndex] = updatedPatch;
|
|
65
|
+
await savePatchesManifest(patchesDir, manifest);
|
|
66
|
+
return { before: existingPatch, after: updatedPatch };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=patch-export-metadata.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { PatchMetadata } from '../types/commands/index.js';
|
|
2
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Optional post-commit hook for {@link updatePatchAndMetadata}. Runs inside
|
|
5
|
+
* the patch directory lock after the mutation has succeeded but before the
|
|
6
|
+
* lock is released.
|
|
7
|
+
*/
|
|
8
|
+
export type UpdatePatchCommittedHook = () => Promise<void>;
|
|
9
|
+
/** Optional policy gate run against the under-lock projected manifest. */
|
|
10
|
+
export interface UpdatePatchPolicyGate {
|
|
11
|
+
config: FireForgeConfig;
|
|
12
|
+
command: string;
|
|
13
|
+
forceUnsafe?: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Updates a patch file body and its manifest row under the same patch
|
|
17
|
+
* directory lock. Intended for commands like `re-export --files` where the
|
|
18
|
+
* file body and `filesAffected` metadata must move together.
|
|
19
|
+
*/
|
|
20
|
+
export declare function updatePatchAndMetadata(patchesDir: string, filename: string, newContent: string, updates: Partial<PatchMetadata>, onCommitted?: UpdatePatchCommittedHook, policyGate?: UpdatePatchPolicyGate): Promise<void>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { toError } from '../utils/errors.js';
|
|
4
|
+
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
5
|
+
import { warn } from '../utils/logger.js';
|
|
6
|
+
import { withPatchDirectoryLock } from './patch-apply.js';
|
|
7
|
+
import { loadPatchesManifest, savePatchesManifest } from './patch-manifest.js';
|
|
8
|
+
import { buildProjectedManifest, enforcePatchPolicy } from './patch-policy.js';
|
|
9
|
+
/**
|
|
10
|
+
* Updates a patch file body and its manifest row under the same patch
|
|
11
|
+
* directory lock. Intended for commands like `re-export --files` where the
|
|
12
|
+
* file body and `filesAffected` metadata must move together.
|
|
13
|
+
*/
|
|
14
|
+
export async function updatePatchAndMetadata(patchesDir, filename, newContent, updates, onCommitted, policyGate) {
|
|
15
|
+
await withPatchDirectoryLock(patchesDir, async () => {
|
|
16
|
+
const manifest = await loadPatchesManifest(patchesDir);
|
|
17
|
+
if (!manifest) {
|
|
18
|
+
throw new Error('Cannot update patch metadata: patches.json is missing.');
|
|
19
|
+
}
|
|
20
|
+
const patchIndex = manifest.patches.findIndex((p) => p.filename === filename);
|
|
21
|
+
if (patchIndex === -1) {
|
|
22
|
+
throw new Error(`Cannot update patch metadata: ${filename} not found in patches.json.`);
|
|
23
|
+
}
|
|
24
|
+
const patchPath = join(patchesDir, filename);
|
|
25
|
+
if (!(await pathExists(patchPath))) {
|
|
26
|
+
throw new Error(`Cannot update patch: patch file is missing on disk: ${filename}`);
|
|
27
|
+
}
|
|
28
|
+
const originalContent = await readText(patchPath);
|
|
29
|
+
const existingPatch = manifest.patches[patchIndex];
|
|
30
|
+
manifest.patches[patchIndex] = { ...existingPatch, ...updates };
|
|
31
|
+
if (policyGate !== undefined) {
|
|
32
|
+
enforcePatchPolicy({
|
|
33
|
+
config: policyGate.config,
|
|
34
|
+
manifest: buildProjectedManifest(manifest, manifest.patches),
|
|
35
|
+
command: policyGate.command,
|
|
36
|
+
forceUnsafe: policyGate.forceUnsafe === true,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
let patchWritten = false;
|
|
40
|
+
try {
|
|
41
|
+
await writeText(patchPath, newContent);
|
|
42
|
+
patchWritten = true;
|
|
43
|
+
await savePatchesManifest(patchesDir, manifest);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (patchWritten) {
|
|
47
|
+
try {
|
|
48
|
+
await writeText(patchPath, originalContent);
|
|
49
|
+
}
|
|
50
|
+
catch (rollbackError) {
|
|
51
|
+
warn(`Rollback warning: could not restore ${filename} after metadata write failed: ${toError(rollbackError).message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
if (onCommitted) {
|
|
57
|
+
try {
|
|
58
|
+
await onCommitted();
|
|
59
|
+
}
|
|
60
|
+
catch (hookError) {
|
|
61
|
+
warn(`History log append failed after updatePatchAndMetadata committed (${filename}): ` +
|
|
62
|
+
toError(hookError).message);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=patch-export-update.js.map
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { PatchCategory, PatchesManifest, PatchInfo, PatchMetadata } from '../types/commands/index.js';
|
|
2
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
3
|
+
import { type SupersedeCoverageDetail } from './patch-export-coverage.js';
|
|
4
|
+
export { findAllPatchesForFiles, findAllPatchesForFilesWithDetails, findSupersededPatches, isPatchFullyCovered, type PatchCoverage, type SupersedeCoverageDetail, } from './patch-export-coverage.js';
|
|
5
|
+
export { type ClearablePatchMetadataField, mutatePatchMetadata, type PatchMetadataMutation, type PatchMetadataMutationResult, updatePatchMetadata, } from './patch-export-metadata.js';
|
|
6
|
+
export { updatePatchAndMetadata } from './patch-export-update.js';
|
|
2
7
|
/**
|
|
3
8
|
* Gets the next patch number for a new patch.
|
|
4
9
|
* @param patchesDir - Path to the patches directory
|
|
@@ -35,6 +40,12 @@ export interface CommitExportedPatchInput {
|
|
|
35
40
|
tier?: 'branding';
|
|
36
41
|
/** Optional `PatchMetadata.lintIgnore` (empty array treated as absent). */
|
|
37
42
|
lintIgnore?: string[];
|
|
43
|
+
/** Project config, used only when opt-in patchPolicy is present. */
|
|
44
|
+
config?: FireForgeConfig;
|
|
45
|
+
/** Mutating command name for policy errors. */
|
|
46
|
+
policyCommand?: string;
|
|
47
|
+
/** Whether --force-unsafe was supplied by the mutating command. */
|
|
48
|
+
forceUnsafe?: boolean;
|
|
38
49
|
}
|
|
39
50
|
export interface CommitExportedPatchResult {
|
|
40
51
|
patchFilename: string;
|
|
@@ -76,165 +87,12 @@ export declare function findExistingPatchForFile(patchesDir: string, filePath: s
|
|
|
76
87
|
* @param newContent - New patch content
|
|
77
88
|
*/
|
|
78
89
|
export declare function updatePatch(patchPath: string, newContent: string): Promise<void>;
|
|
79
|
-
/**
|
|
80
|
-
* Optional post-commit hook for {@link updatePatchAndMetadata}. Runs inside
|
|
81
|
-
* the patch directory lock after the mutation has succeeded but before the
|
|
82
|
-
* lock is released. Intended for history-log appends so the audit record
|
|
83
|
-
* lands atomically with the mutation. Hook failures are warned but never
|
|
84
|
-
* re-thrown: by the time the hook runs the mutation is already committed,
|
|
85
|
-
* so there is nothing meaningful to roll back.
|
|
86
|
-
*/
|
|
87
|
-
export type UpdatePatchCommittedHook = () => Promise<void>;
|
|
88
|
-
/**
|
|
89
|
-
* Updates a patch file body and its manifest row under the same patch
|
|
90
|
-
* directory lock. Intended for commands like `re-export --files` where the
|
|
91
|
-
* file body and `filesAffected` metadata must move together.
|
|
92
|
-
*
|
|
93
|
-
* If the manifest write fails after the patch body has been rewritten, the
|
|
94
|
-
* original patch content is restored best-effort before the error is
|
|
95
|
-
* re-thrown.
|
|
96
|
-
*
|
|
97
|
-
* @param patchesDir - Path to the patches directory
|
|
98
|
-
* @param filename - Target patch filename
|
|
99
|
-
* @param newContent - New patch body
|
|
100
|
-
* @param updates - Metadata fields to merge into the existing row
|
|
101
|
-
* @param onCommitted - Optional hook that runs inside the same lock after
|
|
102
|
-
* the mutation succeeds. See {@link UpdatePatchCommittedHook}.
|
|
103
|
-
*/
|
|
104
|
-
export declare function updatePatchAndMetadata(patchesDir: string, filename: string, newContent: string, updates: Partial<PatchMetadata>, onCommitted?: UpdatePatchCommittedHook): Promise<void>;
|
|
105
|
-
/**
|
|
106
|
-
* Optional `PatchMetadata` keys safe to clear via the helpers below.
|
|
107
|
-
* Required keys (filename, order, etc.) are excluded by construction so
|
|
108
|
-
* an over-eager `unsetFields: ['filename']` cannot delete a field the
|
|
109
|
-
* manifest validator requires. Add new keys here only when they become
|
|
110
|
-
* optional on the type.
|
|
111
|
-
*/
|
|
112
|
-
export type ClearablePatchMetadataField = 'tier' | 'lintIgnore';
|
|
113
|
-
/**
|
|
114
|
-
* Updates metadata for a patch in the manifest.
|
|
115
|
-
*
|
|
116
|
-
* Required-field updates go through the `updates` partial. Clearing an
|
|
117
|
-
* optional field (e.g. removing the `tier` override) goes through
|
|
118
|
-
* `unsetFields` because TypeScript's `exactOptionalPropertyTypes` does
|
|
119
|
-
* not let `Partial<PatchMetadata>` carry an explicit `undefined` value
|
|
120
|
-
* for fields whose declared type does not include `undefined`. The
|
|
121
|
-
* implementation deletes the listed keys from the merged record before
|
|
122
|
-
* writing, so the on-disk JSON omits them and the validator's
|
|
123
|
-
* "preserve only when present" contract is preserved.
|
|
124
|
-
*
|
|
125
|
-
* @param patchesDir - Path to the patches directory
|
|
126
|
-
* @param filename - Patch filename
|
|
127
|
-
* @param updates - Field values to set. Pass an empty object when only
|
|
128
|
-
* clearing fields.
|
|
129
|
-
* @param unsetFields - Optional fields to remove from the entry (so
|
|
130
|
-
* serialization drops them).
|
|
131
|
-
*/
|
|
132
|
-
export declare function updatePatchMetadata(patchesDir: string, filename: string, updates: Partial<PatchMetadata>, unsetFields?: ReadonlyArray<ClearablePatchMetadataField>): Promise<void>;
|
|
133
|
-
/**
|
|
134
|
-
* Return shape from a {@link mutatePatchMetadata} mutator.
|
|
135
|
-
*/
|
|
136
|
-
export interface PatchMetadataMutation {
|
|
137
|
-
/** Field values to set on the entry. */
|
|
138
|
-
set?: Partial<PatchMetadata>;
|
|
139
|
-
/** Optional fields to remove from the entry entirely. */
|
|
140
|
-
unset?: ReadonlyArray<ClearablePatchMetadataField>;
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* Result of a successful {@link mutatePatchMetadata} call.
|
|
144
|
-
*/
|
|
145
|
-
export interface PatchMetadataMutationResult {
|
|
146
|
-
/** Pre-mutation snapshot of the patch's metadata. */
|
|
147
|
-
before: PatchMetadata;
|
|
148
|
-
/** Post-mutation state of the patch's metadata. */
|
|
149
|
-
after: PatchMetadata;
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Reads a patch's metadata under the directory lock, applies a mutator
|
|
153
|
-
* function to compute the update, and writes the result back — all
|
|
154
|
-
* under a single lock so a concurrent writer cannot interleave a
|
|
155
|
-
* read-modify-write cycle. Useful for operations that need to compute
|
|
156
|
-
* the new value from the old (e.g. unioning a `lintIgnore` list,
|
|
157
|
-
* removing a specific entry), which {@link updatePatchMetadata}'s flat
|
|
158
|
-
* merge cannot express on its own.
|
|
159
|
-
*
|
|
160
|
-
* The mutator returns `{ set, unset }` so it can both write fields
|
|
161
|
-
* and drop optional ones. `set` and `unset` are merged before write:
|
|
162
|
-
* `set` runs first via spread, then `unset` deletes the listed keys.
|
|
163
|
-
*
|
|
164
|
-
* @returns The pre/post metadata pair when the patch is found and the
|
|
165
|
-
* write succeeds; `null` when the manifest is missing or the named
|
|
166
|
-
* patch is not in it. Callers should treat `null` as "no-op, nothing
|
|
167
|
-
* to log".
|
|
168
|
-
*/
|
|
169
|
-
export declare function mutatePatchMetadata(patchesDir: string, filename: string, mutator: (existing: PatchMetadata) => PatchMetadataMutation): Promise<PatchMetadataMutationResult | null>;
|
|
170
|
-
/**
|
|
171
|
-
* Finds patches that are completely superseded by newer patches.
|
|
172
|
-
* A patch is superseded if all its affected files are covered by newer patches.
|
|
173
|
-
* @param patchesDir - Path to the patches directory
|
|
174
|
-
* @param newPatchFiles - Files affected by the new patch
|
|
175
|
-
* @param excludeFilename - Filename to exclude from results (the new patch itself)
|
|
176
|
-
* @returns Superseded patches
|
|
177
|
-
*/
|
|
178
|
-
export declare function findSupersededPatches(patchesDir: string, newPatchFiles: string[], excludeFilename?: string): Promise<PatchInfo[]>;
|
|
179
90
|
/**
|
|
180
91
|
* Deletes a patch file and removes it from the manifest.
|
|
181
92
|
* @param patchesDir - Path to the patches directory
|
|
182
93
|
* @param filename - Patch filename to delete
|
|
183
94
|
*/
|
|
184
95
|
export declare function deletePatch(patchesDir: string, filename: string): Promise<void>;
|
|
185
|
-
/**
|
|
186
|
-
* Report whether a patch is fully covered by a new export, and which of its
|
|
187
|
-
* files caused the coverage.
|
|
188
|
-
*
|
|
189
|
-
* Widened from a bare boolean to `{covered, byFiles}` so that `export
|
|
190
|
-
* --supersede --dry-run` can tell the operator which files in each existing
|
|
191
|
-
* patch triggered its supersession — the opaque "this export would
|
|
192
|
-
* supersede N patches" message was the primary reason `--supersede` was
|
|
193
|
-
* unsafe before this change.
|
|
194
|
-
*/
|
|
195
|
-
export interface PatchCoverage {
|
|
196
|
-
covered: boolean;
|
|
197
|
-
byFiles: string[];
|
|
198
|
-
}
|
|
199
|
-
/**
|
|
200
|
-
* Checks whether a patch is fully covered by a new export.
|
|
201
|
-
* A patch is fully covered when every file it affects is present in the new export.
|
|
202
|
-
* @param patchFiles - Files affected by the existing patch
|
|
203
|
-
* @param targetFiles - Files affected by the new export
|
|
204
|
-
* @returns Coverage report with the triggering file list when `covered` is true
|
|
205
|
-
*/
|
|
206
|
-
export declare function isPatchFullyCovered(patchFiles: string[], targetFiles: string[]): PatchCoverage;
|
|
207
|
-
/**
|
|
208
|
-
* Finds patches whose filesAffected entries are fully covered by the specified files.
|
|
209
|
-
* Used for complete supersession when exporting full-file patches.
|
|
210
|
-
* @param patchesDir - Path to the patches directory
|
|
211
|
-
* @param targetFiles - Files affected by the new export
|
|
212
|
-
* @param excludeFilename - Filename to exclude from results (the new patch itself)
|
|
213
|
-
* @returns Patches that are fully covered by the new export
|
|
214
|
-
*/
|
|
215
|
-
export declare function findAllPatchesForFiles(patchesDir: string, targetFiles: string[], excludeFilename?: string): Promise<PatchInfo[]>;
|
|
216
|
-
/**
|
|
217
|
-
* Describes which files in a covered patch triggered its supersession.
|
|
218
|
-
* Returned from {@link planExport} so dry-run previews can render a
|
|
219
|
-
* complete "moved / removed" picture rather than a bare patch count.
|
|
220
|
-
*/
|
|
221
|
-
export interface SupersedeCoverageDetail {
|
|
222
|
-
/** Existing patch filename. */
|
|
223
|
-
filename: string;
|
|
224
|
-
/** Files the existing patch claimed that the new export also claims. */
|
|
225
|
-
coveredByFiles: string[];
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Resolves coverage details for every existing patch that the new export
|
|
229
|
-
* would fully cover. Mirrors {@link findAllPatchesForFiles} but returns the
|
|
230
|
-
* widened {@link PatchCoverage.byFiles} list per match so callers can render
|
|
231
|
-
* a per-patch breakdown.
|
|
232
|
-
*/
|
|
233
|
-
export declare function findAllPatchesForFilesWithDetails(patchesDir: string, targetFiles: string[], excludeFilename?: string): Promise<{
|
|
234
|
-
patch: PatchInfo;
|
|
235
|
-
coverage: PatchCoverage;
|
|
236
|
-
metadata: PatchMetadata;
|
|
237
|
-
}[]>;
|
|
238
96
|
/**
|
|
239
97
|
* Fully computed plan for a pending export. Returned from
|
|
240
98
|
* {@link planExport} so that `--dry-run` previews can render the full
|
|
@@ -281,6 +139,8 @@ export interface PlanExportInput {
|
|
|
281
139
|
* preserves the field when it has at least one entry.
|
|
282
140
|
*/
|
|
283
141
|
lintIgnore?: string[];
|
|
142
|
+
/** Project config, used only when opt-in patchPolicy is present. */
|
|
143
|
+
config?: FireForgeConfig;
|
|
284
144
|
}
|
|
285
145
|
/**
|
|
286
146
|
* Read-only planning function — computes everything a real export would
|