@aspruyt/xfg 1.10.9 → 2.0.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/README.md +1 -0
- package/dist/config-normalizer.js +1 -0
- package/dist/config-validator.js +13 -0
- package/dist/config.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/manifest.d.ts +19 -11
- package/dist/manifest.js +64 -24
- package/dist/repository-processor.d.ts +2 -0
- package/dist/repository-processor.js +8 -8
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/config-validator.js
CHANGED
|
@@ -21,11 +21,24 @@ function isStructuredFileExtension(fileName) {
|
|
|
21
21
|
const ext = extname(fileName).toLowerCase();
|
|
22
22
|
return (ext === ".json" || ext === ".json5" || ext === ".yaml" || ext === ".yml");
|
|
23
23
|
}
|
|
24
|
+
// Pattern for valid config ID: alphanumeric, hyphens, underscores
|
|
25
|
+
const CONFIG_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
26
|
+
const CONFIG_ID_MAX_LENGTH = 64;
|
|
24
27
|
/**
|
|
25
28
|
* Validates raw config structure before normalization.
|
|
26
29
|
* @throws Error if validation fails
|
|
27
30
|
*/
|
|
28
31
|
export function validateRawConfig(config) {
|
|
32
|
+
// Validate required id field
|
|
33
|
+
if (!config.id || typeof config.id !== "string") {
|
|
34
|
+
throw new Error("Config requires an 'id' field. This unique identifier is used to namespace managed files in .xfg.json");
|
|
35
|
+
}
|
|
36
|
+
if (!CONFIG_ID_PATTERN.test(config.id)) {
|
|
37
|
+
throw new Error(`Config 'id' contains invalid characters: '${config.id}'. Use only alphanumeric characters, hyphens, and underscores.`);
|
|
38
|
+
}
|
|
39
|
+
if (config.id.length > CONFIG_ID_MAX_LENGTH) {
|
|
40
|
+
throw new Error(`Config 'id' exceeds maximum length of ${CONFIG_ID_MAX_LENGTH} characters`);
|
|
41
|
+
}
|
|
29
42
|
if (!config.files || typeof config.files !== "object") {
|
|
30
43
|
throw new Error("Config missing required field: files (must be an object)");
|
|
31
44
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export interface RawRepoConfig {
|
|
|
37
37
|
prOptions?: PRMergeOptions;
|
|
38
38
|
}
|
|
39
39
|
export interface RawConfig {
|
|
40
|
+
id: string;
|
|
40
41
|
files: Record<string, RawFileConfig>;
|
|
41
42
|
repos: RawRepoConfig[];
|
|
42
43
|
prOptions?: PRMergeOptions;
|
|
@@ -61,6 +62,7 @@ export interface RepoConfig {
|
|
|
61
62
|
prOptions?: PRMergeOptions;
|
|
62
63
|
}
|
|
63
64
|
export interface Config {
|
|
65
|
+
id: string;
|
|
64
66
|
repos: RepoConfig[];
|
|
65
67
|
prTemplate?: string;
|
|
66
68
|
githubHosts?: string[];
|
package/dist/index.js
CHANGED
package/dist/manifest.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export declare const MANIFEST_FILENAME = ".xfg.json";
|
|
2
2
|
export interface XfgManifest {
|
|
3
|
-
version:
|
|
4
|
-
|
|
3
|
+
version: 2;
|
|
4
|
+
configs: Record<string, string[]>;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Creates an empty manifest with the current version.
|
|
@@ -9,10 +9,14 @@ export interface XfgManifest {
|
|
|
9
9
|
export declare function createEmptyManifest(): XfgManifest;
|
|
10
10
|
/**
|
|
11
11
|
* Loads the xfg manifest from a repository's working directory.
|
|
12
|
-
* Returns null if the manifest file doesn't exist.
|
|
12
|
+
* Returns null if the manifest file doesn't exist or is v1 format.
|
|
13
|
+
*
|
|
14
|
+
* V1 manifests are treated as non-existent because they lack the config ID
|
|
15
|
+
* namespace required for multi-config support. The next run will create
|
|
16
|
+
* a fresh v2 manifest.
|
|
13
17
|
*
|
|
14
18
|
* @param workDir - The repository working directory
|
|
15
|
-
* @returns The manifest or null if not found
|
|
19
|
+
* @returns The manifest or null if not found or incompatible
|
|
16
20
|
*/
|
|
17
21
|
export declare function loadManifest(workDir: string): XfgManifest | null;
|
|
18
22
|
/**
|
|
@@ -23,24 +27,28 @@ export declare function loadManifest(workDir: string): XfgManifest | null;
|
|
|
23
27
|
*/
|
|
24
28
|
export declare function saveManifest(workDir: string, manifest: XfgManifest): void;
|
|
25
29
|
/**
|
|
26
|
-
* Gets the list of managed files from a manifest.
|
|
27
|
-
* Returns an empty array if the manifest is null.
|
|
30
|
+
* Gets the list of managed files for a specific config from a manifest.
|
|
31
|
+
* Returns an empty array if the manifest is null or the config isn't found.
|
|
28
32
|
*
|
|
29
33
|
* @param manifest - The manifest or null
|
|
30
|
-
* @
|
|
34
|
+
* @param configId - The config ID to get files for
|
|
35
|
+
* @returns Array of managed file names for the given config
|
|
31
36
|
*/
|
|
32
|
-
export declare function getManagedFiles(manifest: XfgManifest | null): string[];
|
|
37
|
+
export declare function getManagedFiles(manifest: XfgManifest | null, configId: string): string[];
|
|
33
38
|
/**
|
|
34
|
-
* Updates the manifest with the current set of files that have deleteOrphaned enabled
|
|
39
|
+
* Updates the manifest with the current set of files that have deleteOrphaned enabled
|
|
40
|
+
* for a specific config. Only modifies that config's namespace - other configs are untouched.
|
|
41
|
+
*
|
|
35
42
|
* Files with deleteOrphaned: true are added to managedFiles.
|
|
36
43
|
* Files with deleteOrphaned: false (explicit) are removed from managedFiles.
|
|
37
|
-
* Files not in the config but in managedFiles are candidates for deletion.
|
|
44
|
+
* Files not in the config but in managedFiles for this configId are candidates for deletion.
|
|
38
45
|
*
|
|
39
46
|
* @param manifest - The existing manifest (or null for new repos)
|
|
47
|
+
* @param configId - The config ID to update
|
|
40
48
|
* @param filesWithDeleteOrphaned - Map of fileName to deleteOrphaned value (true/false/undefined)
|
|
41
49
|
* @returns Updated manifest and list of files to delete
|
|
42
50
|
*/
|
|
43
|
-
export declare function updateManifest(manifest: XfgManifest | null, filesWithDeleteOrphaned: Map<string, boolean | undefined>): {
|
|
51
|
+
export declare function updateManifest(manifest: XfgManifest | null, configId: string, filesWithDeleteOrphaned: Map<string, boolean | undefined>): {
|
|
44
52
|
manifest: XfgManifest;
|
|
45
53
|
filesToDelete: string[];
|
|
46
54
|
};
|
package/dist/manifest.js
CHANGED
|
@@ -1,21 +1,44 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
export const MANIFEST_FILENAME = ".xfg.json";
|
|
4
|
+
/**
|
|
5
|
+
* Type guard to check if a manifest is v1 format.
|
|
6
|
+
*/
|
|
7
|
+
function isV1Manifest(manifest) {
|
|
8
|
+
return (typeof manifest === "object" &&
|
|
9
|
+
manifest !== null &&
|
|
10
|
+
manifest.version === 1 &&
|
|
11
|
+
Array.isArray(manifest.managedFiles));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Type guard to check if a manifest is v2 format.
|
|
15
|
+
*/
|
|
16
|
+
function isV2Manifest(manifest) {
|
|
17
|
+
return (typeof manifest === "object" &&
|
|
18
|
+
manifest !== null &&
|
|
19
|
+
manifest.version === 2 &&
|
|
20
|
+
typeof manifest.configs === "object" &&
|
|
21
|
+
manifest.configs !== null);
|
|
22
|
+
}
|
|
4
23
|
/**
|
|
5
24
|
* Creates an empty manifest with the current version.
|
|
6
25
|
*/
|
|
7
26
|
export function createEmptyManifest() {
|
|
8
27
|
return {
|
|
9
|
-
version:
|
|
10
|
-
|
|
28
|
+
version: 2,
|
|
29
|
+
configs: {},
|
|
11
30
|
};
|
|
12
31
|
}
|
|
13
32
|
/**
|
|
14
33
|
* Loads the xfg manifest from a repository's working directory.
|
|
15
|
-
* Returns null if the manifest file doesn't exist.
|
|
34
|
+
* Returns null if the manifest file doesn't exist or is v1 format.
|
|
35
|
+
*
|
|
36
|
+
* V1 manifests are treated as non-existent because they lack the config ID
|
|
37
|
+
* namespace required for multi-config support. The next run will create
|
|
38
|
+
* a fresh v2 manifest.
|
|
16
39
|
*
|
|
17
40
|
* @param workDir - The repository working directory
|
|
18
|
-
* @returns The manifest or null if not found
|
|
41
|
+
* @returns The manifest or null if not found or incompatible
|
|
19
42
|
*/
|
|
20
43
|
export function loadManifest(workDir) {
|
|
21
44
|
const manifestPath = join(workDir, MANIFEST_FILENAME);
|
|
@@ -25,17 +48,16 @@ export function loadManifest(workDir) {
|
|
|
25
48
|
try {
|
|
26
49
|
const content = readFileSync(manifestPath, "utf-8");
|
|
27
50
|
const parsed = JSON.parse(content);
|
|
28
|
-
//
|
|
29
|
-
if (
|
|
30
|
-
return
|
|
51
|
+
// V2 manifest - return as-is
|
|
52
|
+
if (isV2Manifest(parsed)) {
|
|
53
|
+
return parsed;
|
|
31
54
|
}
|
|
32
|
-
|
|
55
|
+
// V1 manifest - treat as no manifest (will be overwritten with v2)
|
|
56
|
+
if (isV1Manifest(parsed)) {
|
|
33
57
|
return null;
|
|
34
58
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
return parsed;
|
|
59
|
+
// Unknown format - treat as no manifest
|
|
60
|
+
return null;
|
|
39
61
|
}
|
|
40
62
|
catch {
|
|
41
63
|
return null;
|
|
@@ -53,30 +75,35 @@ export function saveManifest(workDir, manifest) {
|
|
|
53
75
|
writeFileSync(manifestPath, content, "utf-8");
|
|
54
76
|
}
|
|
55
77
|
/**
|
|
56
|
-
* Gets the list of managed files from a manifest.
|
|
57
|
-
* Returns an empty array if the manifest is null.
|
|
78
|
+
* Gets the list of managed files for a specific config from a manifest.
|
|
79
|
+
* Returns an empty array if the manifest is null or the config isn't found.
|
|
58
80
|
*
|
|
59
81
|
* @param manifest - The manifest or null
|
|
60
|
-
* @
|
|
82
|
+
* @param configId - The config ID to get files for
|
|
83
|
+
* @returns Array of managed file names for the given config
|
|
61
84
|
*/
|
|
62
|
-
export function getManagedFiles(manifest) {
|
|
85
|
+
export function getManagedFiles(manifest, configId) {
|
|
63
86
|
if (!manifest) {
|
|
64
87
|
return [];
|
|
65
88
|
}
|
|
66
|
-
return [...manifest.
|
|
89
|
+
return [...(manifest.configs[configId] ?? [])];
|
|
67
90
|
}
|
|
68
91
|
/**
|
|
69
|
-
* Updates the manifest with the current set of files that have deleteOrphaned enabled
|
|
92
|
+
* Updates the manifest with the current set of files that have deleteOrphaned enabled
|
|
93
|
+
* for a specific config. Only modifies that config's namespace - other configs are untouched.
|
|
94
|
+
*
|
|
70
95
|
* Files with deleteOrphaned: true are added to managedFiles.
|
|
71
96
|
* Files with deleteOrphaned: false (explicit) are removed from managedFiles.
|
|
72
|
-
* Files not in the config but in managedFiles are candidates for deletion.
|
|
97
|
+
* Files not in the config but in managedFiles for this configId are candidates for deletion.
|
|
73
98
|
*
|
|
74
99
|
* @param manifest - The existing manifest (or null for new repos)
|
|
100
|
+
* @param configId - The config ID to update
|
|
75
101
|
* @param filesWithDeleteOrphaned - Map of fileName to deleteOrphaned value (true/false/undefined)
|
|
76
102
|
* @returns Updated manifest and list of files to delete
|
|
77
103
|
*/
|
|
78
|
-
export function updateManifest(manifest, filesWithDeleteOrphaned) {
|
|
79
|
-
|
|
104
|
+
export function updateManifest(manifest, configId, filesWithDeleteOrphaned) {
|
|
105
|
+
// Get existing managed files for this config only
|
|
106
|
+
const existingManaged = new Set(getManagedFiles(manifest, configId));
|
|
80
107
|
const newManaged = new Set();
|
|
81
108
|
const filesToDelete = [];
|
|
82
109
|
// Process current config files
|
|
@@ -88,17 +115,30 @@ export function updateManifest(manifest, filesWithDeleteOrphaned) {
|
|
|
88
115
|
// If deleteOrphaned is false or undefined, don't add to managed set
|
|
89
116
|
// (explicitly setting false removes from tracking)
|
|
90
117
|
}
|
|
91
|
-
// Find orphaned files: in old manifest but not in current config
|
|
118
|
+
// Find orphaned files: in old manifest for this config but not in current config
|
|
92
119
|
for (const fileName of existingManaged) {
|
|
93
120
|
if (!filesWithDeleteOrphaned.has(fileName)) {
|
|
94
121
|
// File was managed before but is no longer in config - delete it
|
|
95
122
|
filesToDelete.push(fileName);
|
|
96
123
|
}
|
|
97
124
|
}
|
|
125
|
+
// Build updated manifest, preserving other configs
|
|
126
|
+
const updatedConfigs = {
|
|
127
|
+
...(manifest?.configs ?? {}),
|
|
128
|
+
};
|
|
129
|
+
// Update this config's managed files
|
|
130
|
+
const sortedManaged = Array.from(newManaged).sort();
|
|
131
|
+
if (sortedManaged.length > 0) {
|
|
132
|
+
updatedConfigs[configId] = sortedManaged;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
// Remove config entry if no managed files
|
|
136
|
+
delete updatedConfigs[configId];
|
|
137
|
+
}
|
|
98
138
|
return {
|
|
99
139
|
manifest: {
|
|
100
|
-
version:
|
|
101
|
-
|
|
140
|
+
version: 2,
|
|
141
|
+
configs: updatedConfigs,
|
|
102
142
|
},
|
|
103
143
|
filesToDelete,
|
|
104
144
|
};
|
|
@@ -6,6 +6,8 @@ import { CommandExecutor } from "./command-executor.js";
|
|
|
6
6
|
export interface ProcessorOptions {
|
|
7
7
|
branchName: string;
|
|
8
8
|
workDir: string;
|
|
9
|
+
/** Config ID for manifest namespacing */
|
|
10
|
+
configId: string;
|
|
9
11
|
dryRun?: boolean;
|
|
10
12
|
/** Number of retries for network operations (default: 3) */
|
|
11
13
|
retries?: number;
|
|
@@ -174,7 +174,7 @@ export class RepositoryProcessor {
|
|
|
174
174
|
filesWithDeleteOrphaned.set(file.fileName, file.deleteOrphaned);
|
|
175
175
|
}
|
|
176
176
|
// Update manifest and get list of files to delete
|
|
177
|
-
const { manifest: newManifest, filesToDelete } = updateManifest(existingManifest, filesWithDeleteOrphaned);
|
|
177
|
+
const { manifest: newManifest, filesToDelete } = updateManifest(existingManifest, options.configId, filesWithDeleteOrphaned);
|
|
178
178
|
// Delete orphaned files (unless --no-delete flag is set)
|
|
179
179
|
if (filesToDelete.length > 0 && !options.noDelete) {
|
|
180
180
|
for (const fileName of filesToDelete) {
|
|
@@ -197,18 +197,18 @@ export class RepositoryProcessor {
|
|
|
197
197
|
this.log.info(`Skipping deletion of ${filesToDelete.length} orphaned file(s) (--no-delete flag)`);
|
|
198
198
|
}
|
|
199
199
|
// Save updated manifest (tracks files with deleteOrphaned: true)
|
|
200
|
-
// Only save if there are managed files or if we had a previous manifest
|
|
201
|
-
|
|
200
|
+
// Only save if there are managed files for any config, or if we had a previous manifest
|
|
201
|
+
const hasAnyManagedFiles = Object.keys(newManifest.configs).length > 0;
|
|
202
|
+
if (hasAnyManagedFiles || existingManifest !== null) {
|
|
202
203
|
if (!dryRun) {
|
|
203
204
|
saveManifest(workDir, newManifest);
|
|
204
205
|
}
|
|
205
206
|
// Track manifest file as changed if it would be different
|
|
206
|
-
const
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
JSON.stringify(newManifestFiles);
|
|
207
|
+
const existingConfigs = existingManifest?.configs ?? {};
|
|
208
|
+
const manifestChanged = JSON.stringify(existingConfigs) !==
|
|
209
|
+
JSON.stringify(newManifest.configs);
|
|
210
210
|
if (manifestChanged) {
|
|
211
|
-
const manifestExisted =
|
|
211
|
+
const manifestExisted = existsSync(join(workDir, MANIFEST_FILENAME));
|
|
212
212
|
changedFiles.push({
|
|
213
213
|
fileName: MANIFEST_FILENAME,
|
|
214
214
|
action: manifestExisted ? "update" : "create",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aspruyt/xfg",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "CLI tool to sync JSON, JSON5, YAML, or text configuration files across multiple Git repositories via pull requests or direct push",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|