@aspruyt/json-config-sync 3.0.0 → 3.2.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 CHANGED
@@ -66,6 +66,8 @@ json-config-sync --config ./config.yaml
66
66
  - **Environment Variables** - Use `${VAR}` syntax for dynamic values
67
67
  - **Merge Strategies** - Control how arrays merge (replace, append, prepend)
68
68
  - **Override Mode** - Skip merging entirely for specific repos
69
+ - **Empty Files** - Create files with no content (e.g., `.prettierignore`)
70
+ - **YAML Comments** - Add header comments and schema directives to YAML files
69
71
  - **GitHub & Azure DevOps** - Works with both platforms
70
72
  - **Dry-Run Mode** - Preview changes without creating PRs
71
73
  - **Error Resilience** - Continues processing if individual repos fail
@@ -165,10 +167,13 @@ repos: # List of repositories
165
167
 
166
168
  ### Per-File Fields
167
169
 
168
- | Field | Description | Required |
169
- | --------------- | ---------------------------------------------------- | -------- |
170
- | `content` | Base config inherited by all repos | Yes |
171
- | `mergeStrategy` | Array merge strategy: `replace`, `append`, `prepend` | No |
170
+ | Field | Description | Required |
171
+ | --------------- | ---------------------------------------------------------- | -------- |
172
+ | `content` | Base config inherited by all repos (omit for empty file) | No |
173
+ | `mergeStrategy` | Array merge strategy: `replace`, `append`, `prepend` | No |
174
+ | `createOnly` | If `true`, only create file if it doesn't exist | No |
175
+ | `header` | Comment line(s) at top of YAML files (string or array) | No |
176
+ | `schemaUrl` | Adds `# yaml-language-server: $schema=<url>` to YAML files | No |
172
177
 
173
178
  ### Per-Repo Fields
174
179
 
@@ -179,10 +184,13 @@ repos: # List of repositories
179
184
 
180
185
  ### Per-Repo File Override Fields
181
186
 
182
- | Field | Description | Required |
183
- | ---------- | ------------------------------------------------------- | -------- |
184
- | `content` | Content overlay merged onto file's base content | No |
185
- | `override` | If `true`, ignore base content and use only this repo's | No |
187
+ | Field | Description | Required |
188
+ | ------------ | ------------------------------------------------------- | -------- |
189
+ | `content` | Content overlay merged onto file's base content | No |
190
+ | `override` | If `true`, ignore base content and use only this repo's | No |
191
+ | `createOnly` | Override root-level `createOnly` for this repo | No |
192
+ | `header` | Override root-level `header` for this repo | No |
193
+ | `schemaUrl` | Override root-level `schemaUrl` for this repo | No |
186
194
 
187
195
  **File Exclusion:** Set a file to `false` to exclude it from a specific repo:
188
196
 
@@ -363,6 +371,85 @@ repos:
363
371
  # Results in extends: ["@company/base", "plugin:react/recommended"]
364
372
  ```
365
373
 
374
+ ### Create-Only Files (Defaults That Can Be Customized)
375
+
376
+ Some files should only be created once as defaults, allowing repos to maintain their own versions:
377
+
378
+ ```yaml
379
+ files:
380
+ .trivyignore.yaml:
381
+ createOnly: true # Only create if doesn't exist
382
+ content:
383
+ vulnerabilities: []
384
+
385
+ .prettierignore:
386
+ createOnly: true
387
+ content:
388
+ patterns:
389
+ - "dist/"
390
+ - "node_modules/"
391
+
392
+ eslint.config.json:
393
+ content: # Always synced (no createOnly)
394
+ extends: ["@company/base"]
395
+
396
+ repos:
397
+ - git: git@github.com:org/repo.git
398
+ # .trivyignore.yaml and .prettierignore only created if missing
399
+ # eslint.config.json always updated
400
+
401
+ - git: git@github.com:org/special-repo.git
402
+ files:
403
+ .trivyignore.yaml:
404
+ createOnly: false # Override: always sync this file
405
+ ```
406
+
407
+ ### YAML Comments and Empty Files
408
+
409
+ Add schema directives and comments to YAML files, or create empty files:
410
+
411
+ ```yaml
412
+ files:
413
+ # YAML file with schema directive and header comment
414
+ trivy.yaml:
415
+ schemaUrl: https://trivy.dev/latest/docs/references/configuration/config-file/
416
+ header: "Trivy security scanner configuration"
417
+ content:
418
+ exit-code: 1
419
+ scan:
420
+ scanners:
421
+ - vuln
422
+
423
+ # Empty file (content omitted)
424
+ .prettierignore:
425
+ createOnly: true
426
+ # No content = empty file
427
+
428
+ # YAML with multi-line header
429
+ config.yaml:
430
+ header:
431
+ - "Auto-generated configuration"
432
+ - "Do not edit manually"
433
+ content:
434
+ version: 1
435
+
436
+ repos:
437
+ - git: git@github.com:org/repo.git
438
+ ```
439
+
440
+ **Output for trivy.yaml:**
441
+
442
+ ```yaml
443
+ # yaml-language-server: $schema=https://trivy.dev/latest/docs/references/configuration/config-file/
444
+ # Trivy security scanner configuration
445
+ exit-code: 1
446
+ scan:
447
+ scanners:
448
+ - vuln
449
+ ```
450
+
451
+ **Note:** `header` and `schemaUrl` only apply to YAML output files (`.yaml`, `.yml`). They are ignored for JSON files.
452
+
366
453
  ## Supported Git URL Formats
367
454
 
368
455
  ### GitHub
@@ -1,9 +1,17 @@
1
1
  export type OutputFormat = "json" | "yaml";
2
+ /**
3
+ * Options for content conversion.
4
+ */
5
+ export interface ConvertOptions {
6
+ header?: string[];
7
+ schemaUrl?: string;
8
+ }
2
9
  /**
3
10
  * Detects output format from file extension.
4
11
  */
5
12
  export declare function detectOutputFormat(fileName: string): OutputFormat;
6
13
  /**
7
14
  * Converts content object to string in the appropriate format.
15
+ * Handles null content (empty files) and comments (YAML only).
8
16
  */
9
- export declare function convertContentToString(content: Record<string, unknown>, fileName: string): string;
17
+ export declare function convertContentToString(content: Record<string, unknown> | null, fileName: string, options?: ConvertOptions): string;
@@ -1,4 +1,4 @@
1
- import { stringify } from "yaml";
1
+ import { Document, stringify } from "yaml";
2
2
  /**
3
3
  * Detects output format from file extension.
4
4
  */
@@ -9,13 +9,71 @@ export function detectOutputFormat(fileName) {
9
9
  }
10
10
  return "json";
11
11
  }
12
+ /**
13
+ * Builds header comment string from header lines and schemaUrl.
14
+ * Returns undefined if no comments to add.
15
+ * Each line gets a space prefix since yaml library adds # directly.
16
+ */
17
+ function buildHeaderComment(header, schemaUrl) {
18
+ const lines = [];
19
+ // Add yaml-language-server schema directive first (if present)
20
+ if (schemaUrl) {
21
+ lines.push(` yaml-language-server: $schema=${schemaUrl}`);
22
+ }
23
+ // Add custom header lines (with space prefix for proper formatting)
24
+ if (header && header.length > 0) {
25
+ lines.push(...header.map((h) => ` ${h}`));
26
+ }
27
+ if (lines.length === 0)
28
+ return undefined;
29
+ // Join with newlines - the yaml library adds # prefix to each line
30
+ return lines.join("\n");
31
+ }
32
+ /**
33
+ * Builds comment-only output for empty YAML files with headers.
34
+ */
35
+ function buildCommentOnlyYaml(header, schemaUrl) {
36
+ const lines = [];
37
+ // Add yaml-language-server schema directive first (if present)
38
+ if (schemaUrl) {
39
+ lines.push(`# yaml-language-server: $schema=${schemaUrl}`);
40
+ }
41
+ // Add custom header lines
42
+ if (header && header.length > 0) {
43
+ lines.push(...header.map((h) => `# ${h}`));
44
+ }
45
+ if (lines.length === 0)
46
+ return undefined;
47
+ return lines.join("\n") + "\n";
48
+ }
12
49
  /**
13
50
  * Converts content object to string in the appropriate format.
51
+ * Handles null content (empty files) and comments (YAML only).
14
52
  */
15
- export function convertContentToString(content, fileName) {
53
+ export function convertContentToString(content, fileName, options) {
16
54
  const format = detectOutputFormat(fileName);
55
+ // Handle empty file case
56
+ if (content === null) {
57
+ if (format === "yaml" && options) {
58
+ const commentOnly = buildCommentOnlyYaml(options.header, options.schemaUrl);
59
+ if (commentOnly) {
60
+ return commentOnly;
61
+ }
62
+ }
63
+ return "";
64
+ }
17
65
  if (format === "yaml") {
18
- return stringify(content, { indent: 2 });
66
+ // Use Document API for YAML to support comments
67
+ const doc = new Document(content);
68
+ // Add header comment if present
69
+ if (options) {
70
+ const headerComment = buildHeaderComment(options.header, options.schemaUrl);
71
+ if (headerComment) {
72
+ doc.commentBefore = headerComment;
73
+ }
74
+ }
75
+ return stringify(doc, { indent: 2 });
19
76
  }
77
+ // JSON format - comments not supported, ignore header/schemaUrl
20
78
  return JSON.stringify(content, null, 2);
21
79
  }
@@ -1,5 +1,15 @@
1
1
  import { deepMerge, stripMergeDirectives, createMergeContext, } from "./merge.js";
2
2
  import { interpolateEnvVars } from "./env.js";
3
+ /**
4
+ * Normalizes header to array format.
5
+ */
6
+ function normalizeHeader(header) {
7
+ if (header === undefined)
8
+ return undefined;
9
+ if (typeof header === "string")
10
+ return [header];
11
+ return header;
12
+ }
3
13
  /**
4
14
  * Normalizes raw config into expanded, merged config.
5
15
  * Pipeline: expand git arrays -> merge content -> interpolate env vars
@@ -20,29 +30,51 @@ export function normalizeConfig(raw) {
20
30
  continue;
21
31
  }
22
32
  const fileConfig = raw.files[fileName];
23
- const baseContent = fileConfig.content ?? {};
24
33
  const fileStrategy = fileConfig.mergeStrategy ?? "replace";
25
34
  // Step 3: Compute merged content for this file
26
35
  let mergedContent;
27
36
  if (repoOverride?.override) {
28
- // Override mode: use only repo file content
29
- mergedContent = stripMergeDirectives(structuredClone(repoOverride.content));
37
+ // Override mode: use only repo file content (may be undefined for empty file)
38
+ if (repoOverride.content === undefined) {
39
+ mergedContent = null;
40
+ }
41
+ else {
42
+ mergedContent = stripMergeDirectives(structuredClone(repoOverride.content));
43
+ }
44
+ }
45
+ else if (fileConfig.content === undefined) {
46
+ // Root file has no content = empty file (unless repo provides content)
47
+ if (repoOverride?.content) {
48
+ mergedContent = stripMergeDirectives(structuredClone(repoOverride.content));
49
+ }
50
+ else {
51
+ mergedContent = null;
52
+ }
30
53
  }
31
54
  else if (!repoOverride?.content) {
32
55
  // No repo override: use file base content as-is
33
- mergedContent = structuredClone(baseContent);
56
+ mergedContent = structuredClone(fileConfig.content);
34
57
  }
35
58
  else {
36
59
  // Merge mode: deep merge file base + repo overlay
37
60
  const ctx = createMergeContext(fileStrategy);
38
- mergedContent = deepMerge(structuredClone(baseContent), repoOverride.content, ctx);
61
+ mergedContent = deepMerge(structuredClone(fileConfig.content), repoOverride.content, ctx);
39
62
  mergedContent = stripMergeDirectives(mergedContent);
40
63
  }
41
- // Step 4: Interpolate env vars
42
- mergedContent = interpolateEnvVars(mergedContent, { strict: true });
64
+ // Step 4: Interpolate env vars (only if content exists)
65
+ if (mergedContent !== null) {
66
+ mergedContent = interpolateEnvVars(mergedContent, { strict: true });
67
+ }
68
+ // Resolve fields: per-repo overrides root level
69
+ const createOnly = repoOverride?.createOnly ?? fileConfig.createOnly;
70
+ const header = normalizeHeader(repoOverride?.header ?? fileConfig.header);
71
+ const schemaUrl = repoOverride?.schemaUrl ?? fileConfig.schemaUrl;
43
72
  files.push({
44
73
  fileName,
45
74
  content: mergedContent,
75
+ createOnly,
76
+ header,
77
+ schemaUrl,
46
78
  });
47
79
  }
48
80
  expandedRepos.push({
@@ -29,6 +29,21 @@ export function validateRawConfig(config) {
29
29
  !VALID_STRATEGIES.includes(fileConfig.mergeStrategy)) {
30
30
  throw new Error(`File '${fileName}' has invalid mergeStrategy: ${fileConfig.mergeStrategy}. Must be one of: ${VALID_STRATEGIES.join(", ")}`);
31
31
  }
32
+ if (fileConfig.createOnly !== undefined &&
33
+ typeof fileConfig.createOnly !== "boolean") {
34
+ throw new Error(`File '${fileName}' createOnly must be a boolean`);
35
+ }
36
+ if (fileConfig.header !== undefined) {
37
+ if (typeof fileConfig.header !== "string" &&
38
+ (!Array.isArray(fileConfig.header) ||
39
+ !fileConfig.header.every((h) => typeof h === "string"))) {
40
+ throw new Error(`File '${fileName}' header must be a string or array of strings`);
41
+ }
42
+ }
43
+ if (fileConfig.schemaUrl !== undefined &&
44
+ typeof fileConfig.schemaUrl !== "string") {
45
+ throw new Error(`File '${fileName}' schemaUrl must be a string`);
46
+ }
32
47
  }
33
48
  if (!config.repos || !Array.isArray(config.repos)) {
34
49
  throw new Error("Config missing required field: repos (must be an array)");
@@ -66,6 +81,21 @@ export function validateRawConfig(config) {
66
81
  Array.isArray(fileOverride.content))) {
67
82
  throw new Error(`Repo at index ${i}: file '${fileName}' content must be an object`);
68
83
  }
84
+ if (fileOverride.createOnly !== undefined &&
85
+ typeof fileOverride.createOnly !== "boolean") {
86
+ throw new Error(`Repo ${getGitDisplayName(repo.git)}: file '${fileName}' createOnly must be a boolean`);
87
+ }
88
+ if (fileOverride.header !== undefined) {
89
+ if (typeof fileOverride.header !== "string" &&
90
+ (!Array.isArray(fileOverride.header) ||
91
+ !fileOverride.header.every((h) => typeof h === "string"))) {
92
+ throw new Error(`Repo ${getGitDisplayName(repo.git)}: file '${fileName}' header must be a string or array of strings`);
93
+ }
94
+ }
95
+ if (fileOverride.schemaUrl !== undefined &&
96
+ typeof fileOverride.schemaUrl !== "string") {
97
+ throw new Error(`Repo ${getGitDisplayName(repo.git)}: file '${fileName}' schemaUrl must be a string`);
98
+ }
69
99
  }
70
100
  }
71
101
  }
package/dist/config.d.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  import type { ArrayMergeStrategy } from "./merge.js";
2
2
  export { convertContentToString } from "./config-formatter.js";
3
3
  export interface RawFileConfig {
4
- content: Record<string, unknown>;
4
+ content?: Record<string, unknown>;
5
5
  mergeStrategy?: ArrayMergeStrategy;
6
+ createOnly?: boolean;
7
+ header?: string | string[];
8
+ schemaUrl?: string;
6
9
  }
7
10
  export interface RawRepoFileOverride {
8
11
  content?: Record<string, unknown>;
9
12
  override?: boolean;
13
+ createOnly?: boolean;
14
+ header?: string | string[];
15
+ schemaUrl?: string;
10
16
  }
11
17
  export interface RawRepoConfig {
12
18
  git: string | string[];
@@ -18,7 +24,10 @@ export interface RawConfig {
18
24
  }
19
25
  export interface FileContent {
20
26
  fileName: string;
21
- content: Record<string, unknown>;
27
+ content: Record<string, unknown> | null;
28
+ createOnly?: boolean;
29
+ header?: string[];
30
+ schemaUrl?: string;
22
31
  }
23
32
  export interface RepoConfig {
24
33
  git: string;
@@ -2,7 +2,7 @@ import { RepoInfo } from "./repo-detector.js";
2
2
  export { escapeShellArg } from "./shell-utils.js";
3
3
  export interface FileAction {
4
4
  fileName: string;
5
- action: "create" | "update";
5
+ action: "create" | "update" | "skip";
6
6
  }
7
7
  export interface PROptions {
8
8
  repoInfo: RepoInfo;
@@ -24,7 +24,7 @@ export interface PRResult {
24
24
  */
25
25
  export declare function formatPRBody(files: FileAction[]): string;
26
26
  /**
27
- * Generate PR title based on files changed
27
+ * Generate PR title based on files changed (excludes skipped files)
28
28
  */
29
29
  export declare function formatPRTitle(files: FileAction[]): string;
30
30
  export declare function createPR(options: PROptions): Promise<PRResult>;
@@ -25,36 +25,37 @@ Configuration synced using [json-config-sync](https://github.com/anthony-spruyt/
25
25
  ---
26
26
  _This PR was automatically generated by [json-config-sync](https://github.com/anthony-spruyt/json-config-sync)_`;
27
27
  }
28
+ /**
29
+ * Format file changes list, excluding skipped files
30
+ */
31
+ function formatFileChanges(files) {
32
+ const changedFiles = files.filter((f) => f.action !== "skip");
33
+ return changedFiles
34
+ .map((f) => {
35
+ const actionText = f.action === "create" ? "Created" : "Updated";
36
+ return `- ${actionText} \`${f.fileName}\``;
37
+ })
38
+ .join("\n");
39
+ }
28
40
  /**
29
41
  * Format PR body for multiple files
30
42
  */
31
43
  export function formatPRBody(files) {
32
44
  const template = loadPRTemplate();
45
+ const fileChanges = formatFileChanges(files);
33
46
  // Check if template supports multi-file format
34
47
  if (template.includes("{{FILE_CHANGES}}")) {
35
- // Multi-file template
36
- const fileChanges = files
37
- .map((f) => {
38
- const actionText = f.action === "create" ? "Created" : "Updated";
39
- return `- ${actionText} \`${f.fileName}\``;
40
- })
41
- .join("\n");
42
48
  return template.replace(/\{\{FILE_CHANGES\}\}/g, fileChanges);
43
49
  }
44
50
  // Legacy single-file template - adapt it for multiple files
45
- if (files.length === 1) {
46
- const actionText = files[0].action === "create" ? "Created" : "Updated";
51
+ const changedFiles = files.filter((f) => f.action !== "skip");
52
+ if (changedFiles.length === 1) {
53
+ const actionText = changedFiles[0].action === "create" ? "Created" : "Updated";
47
54
  return template
48
- .replace(/\{\{FILE_NAME\}\}/g, files[0].fileName)
55
+ .replace(/\{\{FILE_NAME\}\}/g, changedFiles[0].fileName)
49
56
  .replace(/\{\{ACTION\}\}/g, actionText);
50
57
  }
51
58
  // Multiple files with legacy template - generate custom body
52
- const fileChanges = files
53
- .map((f) => {
54
- const actionText = f.action === "create" ? "Created" : "Updated";
55
- return `- ${actionText} \`${f.fileName}\``;
56
- })
57
- .join("\n");
58
59
  return `## Summary
59
60
  Automated sync of configuration files.
60
61
 
@@ -68,17 +69,18 @@ Configuration synced using [json-config-sync](https://github.com/anthony-spruyt/
68
69
  _This PR was automatically generated by [json-config-sync](https://github.com/anthony-spruyt/json-config-sync)_`;
69
70
  }
70
71
  /**
71
- * Generate PR title based on files changed
72
+ * Generate PR title based on files changed (excludes skipped files)
72
73
  */
73
74
  export function formatPRTitle(files) {
74
- if (files.length === 1) {
75
- return `chore: sync ${files[0].fileName}`;
75
+ const changedFiles = files.filter((f) => f.action !== "skip");
76
+ if (changedFiles.length === 1) {
77
+ return `chore: sync ${changedFiles[0].fileName}`;
76
78
  }
77
- if (files.length <= 3) {
78
- const fileNames = files.map((f) => f.fileName).join(", ");
79
+ if (changedFiles.length <= 3) {
80
+ const fileNames = changedFiles.map((f) => f.fileName).join(", ");
79
81
  return `chore: sync ${fileNames}`;
80
82
  }
81
- return `chore: sync ${files.length} config files`;
83
+ return `chore: sync ${changedFiles.length} config files`;
82
84
  }
83
85
  export async function createPR(options) {
84
86
  const { repoInfo, branchName, baseBranch, files, workDir, dryRun, retries } = options;
@@ -33,7 +33,7 @@ export declare class RepositoryProcessor {
33
33
  constructor(gitOpsFactory?: GitOpsFactory, log?: ILogger);
34
34
  process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions): Promise<ProcessorResult>;
35
35
  /**
36
- * Format commit message based on files changed
36
+ * Format commit message based on files changed (excludes skipped files)
37
37
  */
38
38
  private formatCommitMessage;
39
39
  }
@@ -38,13 +38,21 @@ export class RepositoryProcessor {
38
38
  // Step 5: Write all config files and track changes
39
39
  const changedFiles = [];
40
40
  for (const file of repoConfig.files) {
41
- this.log.info(`Writing ${file.fileName}...`);
42
- const fileContent = convertContentToString(file.content, file.fileName);
43
41
  const filePath = join(workDir, file.fileName);
42
+ const fileExists = existsSync(filePath);
43
+ // Handle createOnly - skip if file already exists
44
+ if (file.createOnly && fileExists) {
45
+ this.log.info(`Skipping ${file.fileName} (createOnly: already exists)`);
46
+ changedFiles.push({ fileName: file.fileName, action: "skip" });
47
+ continue;
48
+ }
49
+ this.log.info(`Writing ${file.fileName}...`);
50
+ const fileContent = convertContentToString(file.content, file.fileName, {
51
+ header: file.header,
52
+ schemaUrl: file.schemaUrl,
53
+ });
44
54
  // Determine action type (create vs update)
45
- const action = existsSync(filePath)
46
- ? "update"
47
- : "create";
55
+ const action = fileExists ? "update" : "create";
48
56
  if (dryRun) {
49
57
  // In dry-run, check if file would change without writing
50
58
  if (this.gitOps.wouldChange(file.fileName, fileContent)) {
@@ -56,23 +64,25 @@ export class RepositoryProcessor {
56
64
  this.gitOps.writeFile(file.fileName, fileContent);
57
65
  }
58
66
  }
59
- // Step 6: Check for changes
67
+ // Step 6: Check for changes (exclude skipped files)
60
68
  let hasChanges;
61
69
  if (dryRun) {
62
- hasChanges = changedFiles.length > 0;
70
+ hasChanges = changedFiles.filter((f) => f.action !== "skip").length > 0;
63
71
  }
64
72
  else {
65
73
  hasChanges = await this.gitOps.hasChanges();
66
74
  // If there are changes, determine which files changed
67
75
  if (hasChanges) {
68
76
  // Rebuild the changed files list by checking git status
69
- // For simplicity, we include all files with their detected actions
77
+ // Skip files that were already marked as skipped (createOnly)
78
+ const skippedFiles = new Set(changedFiles
79
+ .filter((f) => f.action === "skip")
80
+ .map((f) => f.fileName));
70
81
  for (const file of repoConfig.files) {
82
+ if (skippedFiles.has(file.fileName)) {
83
+ continue; // Already tracked as skipped
84
+ }
71
85
  const filePath = join(workDir, file.fileName);
72
- // We check if file existed before writing (action was determined above)
73
- // Since we don't have pre-write state, we'll mark all files that are in the commit
74
- // A more accurate approach would track this before writing, but for now
75
- // we'll assume all files are being synced and include them all
76
86
  const action = existsSync(filePath)
77
87
  ? "update"
78
88
  : "create";
@@ -126,16 +136,17 @@ export class RepositoryProcessor {
126
136
  }
127
137
  }
128
138
  /**
129
- * Format commit message based on files changed
139
+ * Format commit message based on files changed (excludes skipped files)
130
140
  */
131
141
  formatCommitMessage(files) {
132
- if (files.length === 1) {
133
- return `chore: sync ${files[0].fileName}`;
142
+ const changedFiles = files.filter((f) => f.action !== "skip");
143
+ if (changedFiles.length === 1) {
144
+ return `chore: sync ${changedFiles[0].fileName}`;
134
145
  }
135
- if (files.length <= 3) {
136
- const fileNames = files.map((f) => f.fileName).join(", ");
146
+ if (changedFiles.length <= 3) {
147
+ const fileNames = changedFiles.map((f) => f.fileName).join(", ");
137
148
  return `chore: sync ${fileNames}`;
138
149
  }
139
- return `chore: sync ${files.length} config files`;
150
+ return `chore: sync ${changedFiles.length} config files`;
140
151
  }
141
152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aspruyt/json-config-sync",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "CLI tool to sync JSON or YAML configuration files across multiple GitHub and Azure DevOps repositories",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",