@aspruyt/xfg 5.1.3 → 5.1.4

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.
Files changed (81) hide show
  1. package/README.md +1 -0
  2. package/dist/cli/lifecycle-report-builder.d.ts +2 -0
  3. package/dist/cli/lifecycle-report-builder.js +15 -0
  4. package/dist/cli/program.js +16 -11
  5. package/dist/cli/settings-report-builder.d.ts +1 -1
  6. package/dist/cli/settings-report-builder.js +20 -32
  7. package/dist/cli/sync-command.js +59 -64
  8. package/dist/cli/types.d.ts +3 -6
  9. package/dist/config/formatter.js +1 -6
  10. package/dist/config/index.d.ts +2 -1
  11. package/dist/config/index.js +2 -0
  12. package/dist/config/merge.d.ts +0 -6
  13. package/dist/config/merge.js +0 -13
  14. package/dist/config/types.d.ts +0 -9
  15. package/dist/config/validator.d.ts +1 -1
  16. package/dist/config/validator.js +1 -1
  17. package/dist/lifecycle/github-lifecycle-provider.js +28 -45
  18. package/dist/lifecycle/lifecycle-helpers.d.ts +1 -1
  19. package/dist/lifecycle/repo-lifecycle-manager.d.ts +1 -1
  20. package/dist/lifecycle/types.d.ts +4 -4
  21. package/dist/output/github-summary.d.ts +0 -50
  22. package/dist/output/github-summary.js +0 -201
  23. package/dist/output/lifecycle-report.d.ts +0 -1
  24. package/dist/output/lifecycle-report.js +0 -15
  25. package/dist/output/settings-report.d.ts +1 -1
  26. package/dist/output/settings-report.js +15 -77
  27. package/dist/output/sync-report.d.ts +1 -0
  28. package/dist/output/sync-report.js +19 -1
  29. package/dist/output/unified-summary.d.ts +1 -2
  30. package/dist/output/unified-summary.js +1 -19
  31. package/dist/settings/base-processor.d.ts +8 -0
  32. package/dist/settings/base-processor.js +7 -0
  33. package/dist/settings/index.d.ts +2 -2
  34. package/dist/settings/index.js +3 -1
  35. package/dist/settings/labels/converter.d.ts +1 -1
  36. package/dist/settings/labels/diff.d.ts +1 -1
  37. package/dist/settings/labels/formatter.d.ts +1 -1
  38. package/dist/settings/repo-settings/processor.js +9 -5
  39. package/dist/settings/rulesets/formatter.js +97 -85
  40. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +2 -2
  41. package/dist/settings/rulesets/github-ruleset-strategy.js +7 -4
  42. package/dist/settings/rulesets/index.d.ts +1 -1
  43. package/dist/settings/rulesets/index.js +0 -2
  44. package/dist/settings/rulesets/processor.js +5 -1
  45. package/dist/settings/rulesets/types.d.ts +6 -1
  46. package/dist/shared/gh-api-utils.d.ts +1 -1
  47. package/dist/shared/logger.d.ts +8 -8
  48. package/dist/shared/logger.js +9 -8
  49. package/dist/shared/repo-detector.js +0 -1
  50. package/dist/sync/branch-manager.js +2 -2
  51. package/dist/sync/commit-push-manager.js +2 -2
  52. package/dist/sync/diff-utils.d.ts +0 -9
  53. package/dist/sync/diff-utils.js +0 -9
  54. package/dist/sync/file-sync-orchestrator.js +1 -1
  55. package/dist/sync/file-sync-strategy.d.ts +1 -1
  56. package/dist/sync/file-writer.d.ts +1 -1
  57. package/dist/sync/file-writer.js +3 -1
  58. package/dist/sync/index.d.ts +1 -2
  59. package/dist/sync/index.js +1 -1
  60. package/dist/sync/manifest-manager.d.ts +1 -1
  61. package/dist/sync/manifest-manager.js +1 -1
  62. package/dist/sync/manifest.js +9 -1
  63. package/dist/sync/pr-merge-handler.js +2 -2
  64. package/dist/sync/sync-workflow.d.ts +1 -1
  65. package/dist/sync/types.d.ts +2 -2
  66. package/dist/vcs/authenticated-git-ops.d.ts +2 -1
  67. package/dist/vcs/commit-strategy-selector.d.ts +1 -1
  68. package/dist/vcs/commit-strategy-selector.js +1 -1
  69. package/dist/vcs/git-ops.d.ts +0 -1
  70. package/dist/vcs/git-ops.js +15 -8
  71. package/dist/vcs/github-pr-strategy.d.ts +0 -3
  72. package/dist/vcs/github-pr-strategy.js +4 -7
  73. package/dist/vcs/index.d.ts +3 -3
  74. package/dist/vcs/index.js +2 -2
  75. package/dist/vcs/pr-creator.js +9 -9
  76. package/dist/vcs/pr-strategy-factory.d.ts +1 -1
  77. package/dist/vcs/pr-strategy-factory.js +1 -1
  78. package/dist/vcs/pr-strategy.js +6 -11
  79. package/package.json +1 -1
  80. package/dist/output/summary-utils.d.ts +0 -20
  81. package/dist/output/summary-utils.js +0 -79
package/README.md CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/anthony-spruyt/xfg/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/anthony-spruyt/xfg/actions/workflows/ci.yaml)
4
4
  [![codecov](https://codecov.io/gh/anthony-spruyt/xfg/graph/badge.svg)](https://codecov.io/gh/anthony-spruyt/xfg)
5
+ [![Socket Badge](https://badge.socket.dev/npm/package/@aspruyt/xfg)](https://socket.dev/npm/package/@aspruyt/xfg)
5
6
  [![npm version](https://img.shields.io/npm/v/@aspruyt/xfg.svg)](https://www.npmjs.com/package/@aspruyt/xfg)
6
7
  [![npm downloads](https://img.shields.io/npm/dw/@aspruyt/xfg.svg)](https://www.npmjs.com/package/@aspruyt/xfg)
7
8
  [![GitHub Marketplace](https://img.shields.io/badge/Marketplace-xfg-blue?logo=github)](https://github.com/marketplace/actions/xfg-repo-as-code)
@@ -0,0 +1,2 @@
1
+ import type { LifecycleReport, LifecycleAction } from "../output/lifecycle-report.js";
2
+ export declare function buildLifecycleReport(results: LifecycleAction[]): LifecycleReport;
@@ -0,0 +1,15 @@
1
+ export function buildLifecycleReport(results) {
2
+ const actions = [];
3
+ const totals = { created: 0, forked: 0, migrated: 0, existed: 0 };
4
+ for (const result of results) {
5
+ actions.push({
6
+ repoName: result.repoName,
7
+ action: result.action,
8
+ upstream: result.upstream,
9
+ source: result.source,
10
+ settings: result.settings,
11
+ });
12
+ totals[result.action]++;
13
+ }
14
+ return { actions, totals };
15
+ }
@@ -6,12 +6,14 @@ import { ValidationError } from "../shared/errors.js";
6
6
  import { runSync } from "./sync-command.js";
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = dirname(__filename);
9
- let packageJson;
10
- try {
11
- packageJson = JSON.parse(readFileSync(join(__dirname, "../..", "package.json"), "utf-8"));
12
- }
13
- catch {
14
- packageJson = { version: "0.0.0" };
9
+ function getVersion() {
10
+ try {
11
+ const pkg = JSON.parse(readFileSync(join(__dirname, "../..", "package.json"), "utf-8"));
12
+ return pkg.version;
13
+ }
14
+ catch {
15
+ return "0.0.0";
16
+ }
15
17
  }
16
18
  // =============================================================================
17
19
  // Shared CLI Options
@@ -50,7 +52,7 @@ export function parseMergeStrategy(value) {
50
52
  program
51
53
  .name("xfg")
52
54
  .description("Manage files, settings, and repositories across GitHub, Azure DevOps, and GitLab")
53
- .version(packageJson.version);
55
+ .version(getVersion());
54
56
  // Sync command (file synchronization)
55
57
  const syncCommand = new Command("sync")
56
58
  .description("Sync configuration files across repositories")
@@ -58,11 +60,14 @@ const syncCommand = new Command("sync")
58
60
  .option("-m, --merge <mode>", "PR merge mode: manual, auto (default, merge when checks pass), force (bypass requirements), direct (push to default branch, no PR)", parseMergeMode)
59
61
  .option("--merge-strategy <strategy>", "Merge strategy: merge, squash (default), rebase", parseMergeStrategy)
60
62
  .option("--delete-branch", "Delete source branch after merge")
61
- .action((opts) => {
62
- runSync(opts).catch((error) => {
63
+ .action(async (opts) => {
64
+ try {
65
+ await runSync(opts);
66
+ }
67
+ catch (error) {
63
68
  console.error("Fatal error:", error);
64
- process.exit(1);
65
- });
69
+ return process.exit(1);
70
+ }
66
71
  });
67
72
  addSharedOptions(syncCommand);
68
73
  program.addCommand(syncCommand);
@@ -1,5 +1,5 @@
1
1
  import type { SettingsReport } from "../output/settings-report.js";
2
- import type { RepoSettingsPlanEntry, RulesetPlanEntry, LabelsPlanEntry } from "../settings/index.js";
2
+ import { type RepoSettingsPlanEntry, type RulesetPlanEntry, type LabelsPlanEntry } from "../settings/index.js";
3
3
  /**
4
4
  * Result from processing a repository's settings and rulesets.
5
5
  * Used to collect results during settings command execution.
@@ -1,3 +1,4 @@
1
+ import { countActions, isActiveAction, } from "../settings/index.js";
1
2
  export function buildSettingsReport(results) {
2
3
  const repos = [];
3
4
  const totals = {
@@ -19,64 +20,51 @@ export function buildSettingsReport(results) {
19
20
  if (entry.oldValue === undefined && entry.newValue === undefined) {
20
21
  continue;
21
22
  }
22
- const settingChange = {
23
+ repoChanges.settings.push({
23
24
  name: entry.property,
24
25
  action: entry.action,
25
26
  oldValue: entry.oldValue,
26
27
  newValue: entry.newValue,
27
- };
28
- repoChanges.settings.push(settingChange);
29
- if (entry.action === "create") {
30
- totals.settings.create++;
31
- }
32
- else {
33
- totals.settings.update++;
34
- }
28
+ });
35
29
  }
30
+ const counts = countActions(repoChanges.settings);
31
+ totals.settings.create += counts.create;
32
+ totals.settings.update += counts.update;
36
33
  }
37
34
  // Convert ruleset processor output
38
35
  if (result.rulesetResult?.planOutput?.entries) {
39
36
  for (const entry of result.rulesetResult.planOutput.entries) {
40
- if (entry.action === "unchanged")
37
+ if (!isActiveAction(entry))
41
38
  continue;
42
- const rulesetChange = {
39
+ repoChanges.rulesets.push({
43
40
  name: entry.name,
44
41
  action: entry.action,
45
42
  propertyDiffs: entry.propertyDiffs,
46
43
  config: entry.config,
47
- };
48
- repoChanges.rulesets.push(rulesetChange);
49
- if (entry.action === "create") {
50
- totals.rulesets.create++;
51
- }
52
- else if (entry.action === "update") {
53
- totals.rulesets.update++;
54
- }
55
- else if (entry.action === "delete") {
56
- totals.rulesets.delete++;
57
- }
44
+ });
58
45
  }
46
+ const counts = countActions(repoChanges.rulesets);
47
+ totals.rulesets.create += counts.create;
48
+ totals.rulesets.update += counts.update;
49
+ totals.rulesets.delete += counts.delete;
59
50
  }
60
51
  // Convert labels processor output
61
52
  if (result.labelsResult?.planOutput?.entries) {
62
53
  for (const entry of result.labelsResult.planOutput.entries) {
63
- if (entry.action === "unchanged")
54
+ if (!isActiveAction(entry))
64
55
  continue;
65
- const labelChange = {
56
+ repoChanges.labels.push({
66
57
  name: entry.name,
67
58
  action: entry.action,
68
59
  newName: entry.newName,
69
60
  propertyChanges: entry.propertyChanges,
70
61
  config: entry.config,
71
- };
72
- repoChanges.labels.push(labelChange);
73
- if (entry.action === "create")
74
- totals.labels.create++;
75
- else if (entry.action === "update")
76
- totals.labels.update++;
77
- else if (entry.action === "delete")
78
- totals.labels.delete++;
62
+ });
79
63
  }
64
+ const counts = countActions(repoChanges.labels);
65
+ totals.labels.create += counts.create;
66
+ totals.labels.update += counts.update;
67
+ totals.labels.delete += counts.delete;
80
68
  }
81
69
  if (result.error) {
82
70
  repoChanges.error = result.error;
@@ -1,8 +1,7 @@
1
1
  import { resolve, join } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
- import { loadRawConfig, normalizeConfig } from "../config/index.js";
3
+ import { loadRawConfig, normalizeConfig, validateForSync, } from "../config/index.js";
4
4
  import { ValidationError, SyncError } from "../shared/errors.js";
5
- import { validateForSync } from "../config/validator.js";
6
5
  import { parseGitUrl, getRepoDisplayName, isGitHubRepo, } from "../shared/repo-detector.js";
7
6
  import { sanitizeBranchName, validateBranchName, } from "../shared/branch-utils.js";
8
7
  import { createTokenManager } from "../vcs/index.js";
@@ -32,18 +31,16 @@ function createDefaultLabelsProcessorFactory() {
32
31
  return () => new LabelsProcessor(new GitHubLabelsStrategy(getDefaultExecutor(), { cwd }));
33
32
  }
34
33
  import { ResultsCollector } from "./results-collector.js";
35
- import { buildSettingsReport } from "./settings-report-builder.js";
34
+ import { buildSettingsReport, } from "./settings-report-builder.js";
36
35
  import { formatSettingsReportCLI } from "../output/settings-report.js";
37
36
  import { buildSyncReport } from "./sync-report-builder.js";
38
37
  import { formatSyncReportCLI } from "../output/sync-report.js";
39
- import { buildLifecycleReport, formatLifecycleReportCLI, hasLifecycleChanges, } from "../output/lifecycle-report.js";
38
+ import { buildLifecycleReport } from "./lifecycle-report-builder.js";
39
+ import { formatLifecycleReportCLI, hasLifecycleChanges, } from "../output/lifecycle-report.js";
40
40
  import { writeUnifiedSummary } from "../output/unified-summary.js";
41
41
  import { toErrorMessage } from "../shared/type-guards.js";
42
42
  import { resolveGitHubToken } from "../shared/gh-api-utils.js";
43
43
  import { RepoLifecycleManager, runLifecycleCheck, toCreateRepoSettings, } from "../lifecycle/index.js";
44
- /**
45
- * Get unique file names from all repos in the config
46
- */
47
44
  function getUniqueFileNames(config) {
48
45
  const fileNames = new Set();
49
46
  for (const repo of config.repos) {
@@ -53,18 +50,12 @@ function getUniqueFileNames(config) {
53
50
  }
54
51
  return Array.from(fileNames);
55
52
  }
56
- /**
57
- * Generate default branch name based on files being synced
58
- */
59
53
  function generateBranchName(fileNames) {
60
54
  if (fileNames.length === 1) {
61
55
  return `chore/sync-${sanitizeBranchName(fileNames[0])}`;
62
56
  }
63
57
  return "chore/sync-config";
64
58
  }
65
- /**
66
- * Format file names for display
67
- */
68
59
  function formatFileNames(fileNames) {
69
60
  if (fileNames.length === 1) {
70
61
  return fileNames[0];
@@ -85,7 +76,7 @@ function determineMergeOutcome(result) {
85
76
  return "auto";
86
77
  return "manual";
87
78
  }
88
- function logSettingsResult(result, label, current, repoName, settingsCollector) {
79
+ function logSettingsResult(result, label, repoNumber, repoName, settingsCollector) {
89
80
  if (result.planOutput?.lines?.length) {
90
81
  getLogger().info("");
91
82
  getLogger().info(`${repoName} - ${label}:`);
@@ -99,70 +90,74 @@ function logSettingsResult(result, label, current, repoName, settingsCollector)
99
90
  }
100
91
  }
101
92
  else if (!result.skipped && result.success) {
102
- getLogger().success(current, repoName, `${label}: ${result.message}`);
93
+ getLogger().success(repoNumber, repoName, `${label}: ${result.message}`);
103
94
  }
104
95
  if (!result.success && !result.skipped) {
105
- getLogger().error(current, repoName, `${label}: ${result.message}`);
96
+ getLogger().error(repoNumber, repoName, `${label}: ${result.message}`);
106
97
  settingsCollector.appendError(repoName, result.message);
107
98
  }
108
99
  }
109
- async function applyRepoSettings(ctx) {
110
- const { repoConfig, repoInfo, repoName, current, options, token, settingsCollector, rulesetProcessorFactory, repoSettingsProcessorFactory, labelsProcessorFactory, } = ctx;
111
- if (!repoConfig.settings || !isGitHubRepo(repoInfo))
112
- return;
113
- const settingsDescriptors = [
100
+ function buildSettingsDescriptors(ctx) {
101
+ const { repoConfig, repoInfo, options, token, repoName, settingsCollector, rulesetProcessorFactory, repoSettingsProcessorFactory, labelsProcessorFactory, } = ctx;
102
+ const sharedOpts = {
103
+ dryRun: options.dryRun,
104
+ noDelete: options.noDelete,
105
+ token,
106
+ };
107
+ // Each processor returns a subtype of BaseProcessorResult whose planOutput
108
+ // contains both `lines` (for CLI display) and `entries` (for report building).
109
+ // ProcessorResults fields capture only the `entries` slice; the runtime object
110
+ // satisfies both views, so we assign with an explicit per-field cast.
111
+ const runAndStore = async (factory, opts, assign) => {
112
+ const result = await runSettingsProcessor(factory, repoConfig, repoInfo, opts);
113
+ if (!result.skipped) {
114
+ assign(settingsCollector.getOrCreate(repoName), result);
115
+ }
116
+ return result;
117
+ };
118
+ return [
114
119
  {
115
120
  key: "rulesets",
116
121
  label: "Rulesets",
117
- run: async () => {
118
- const result = await rulesetProcessorFactory().process(repoConfig, repoInfo, {
119
- dryRun: options.dryRun,
120
- noDelete: options.noDelete,
121
- token,
122
- });
123
- if (!result.skipped) {
124
- settingsCollector.getOrCreate(repoName).rulesetResult = result;
125
- }
126
- return result;
127
- },
122
+ run: () => runAndStore(rulesetProcessorFactory, sharedOpts, (e, r) => {
123
+ e.rulesetResult = r;
124
+ }),
128
125
  },
129
126
  {
130
127
  key: "labels",
131
128
  label: "Labels",
132
- run: async () => {
133
- const result = await labelsProcessorFactory().process(repoConfig, repoInfo, {
134
- dryRun: options.dryRun,
135
- noDelete: options.noDelete,
136
- token,
137
- });
138
- if (!result.skipped) {
139
- settingsCollector.getOrCreate(repoName).labelsResult = result;
140
- }
141
- return result;
142
- },
129
+ run: () => runAndStore(labelsProcessorFactory, sharedOpts, (e, r) => {
130
+ e.labelsResult = r;
131
+ }),
143
132
  },
144
133
  {
145
134
  key: "repo",
146
135
  label: "Repo Settings",
147
- run: async () => {
148
- const result = await repoSettingsProcessorFactory().process(repoConfig, repoInfo, { dryRun: options.dryRun, token });
149
- if (!result.skipped) {
150
- settingsCollector.getOrCreate(repoName).settingsResult = result;
151
- }
152
- return result;
153
- },
136
+ run: () => runAndStore(repoSettingsProcessorFactory, { dryRun: options.dryRun, token }, (e, r) => {
137
+ e.settingsResult = r;
138
+ }),
154
139
  },
155
140
  ];
156
- for (const desc of settingsDescriptors) {
141
+ }
142
+ function runSettingsProcessor(factory, repoConfig, repoInfo, processOptions) {
143
+ return factory()
144
+ .process(repoConfig, repoInfo, processOptions)
145
+ .then((result) => result);
146
+ }
147
+ async function applyRepoSettings(ctx) {
148
+ const { repoConfig, repoInfo, repoName, repoNumber, settingsCollector } = ctx;
149
+ if (!repoConfig.settings || !isGitHubRepo(repoInfo))
150
+ return;
151
+ for (const desc of buildSettingsDescriptors(ctx)) {
157
152
  const settingsValue = repoConfig.settings[desc.key];
158
153
  if (!settingsValue || Object.keys(settingsValue).length === 0)
159
154
  continue;
160
155
  try {
161
156
  const result = await desc.run();
162
- logSettingsResult(result, desc.label, current, repoName, settingsCollector);
157
+ logSettingsResult(result, desc.label, repoNumber, repoName, settingsCollector);
163
158
  }
164
159
  catch (error) {
165
- getLogger().error(current, repoName, `${desc.label}: ${toErrorMessage(error)}`);
160
+ getLogger().error(repoNumber, repoName, `${desc.label}: ${toErrorMessage(error)}`);
166
161
  settingsCollector.appendError(repoName, error);
167
162
  }
168
163
  }
@@ -208,7 +203,7 @@ function displayReports(reportResults, lifecycleReportInputs, settingsCollector,
208
203
  */
209
204
  async function processSingleRepo(repoConfig, index, ctx) {
210
205
  const { config, options } = ctx;
211
- const current = index + 1;
206
+ const repoNumber = index + 1;
212
207
  // Apply CLI-level PR option overrides
213
208
  if (options.merge || options.mergeStrategy || options.deleteBranch) {
214
209
  repoConfig.prOptions = {
@@ -229,7 +224,7 @@ async function processSingleRepo(repoConfig, index, ctx) {
229
224
  });
230
225
  }
231
226
  catch (error) {
232
- getLogger().error(current, repoConfig.git, toErrorMessage(error));
227
+ getLogger().error(repoNumber, repoConfig.git, toErrorMessage(error));
233
228
  ctx.reportResults.push({
234
229
  repoName: repoConfig.git,
235
230
  success: false,
@@ -267,7 +262,7 @@ async function processSingleRepo(repoConfig, index, ctx) {
267
262
  repoConfig,
268
263
  repoInfo,
269
264
  repoName,
270
- current,
265
+ repoNumber,
271
266
  options,
272
267
  token: repoToken,
273
268
  settingsCollector: ctx.settingsCollector,
@@ -281,7 +276,7 @@ async function processSingleRepo(repoConfig, index, ctx) {
281
276
  * Returns true if the main loop should skip file sync for this repo.
282
277
  */
283
278
  async function runLifecyclePhase(repo, ctx) {
284
- const current = repo.index + 1;
279
+ const repoNumber = repo.index + 1;
285
280
  try {
286
281
  const { outputLines, lifecycleResult } = await runLifecycleCheck(repo.repoConfig, repo.repoInfo, {
287
282
  dryRun: ctx.options.dryRun ?? false,
@@ -320,7 +315,7 @@ async function runLifecyclePhase(repo, ctx) {
320
315
  return false;
321
316
  }
322
317
  catch (error) {
323
- getLogger().error(current, repo.repoName, `Lifecycle error: ${toErrorMessage(error)}`);
318
+ getLogger().error(repoNumber, repo.repoName, `Lifecycle error: ${toErrorMessage(error)}`);
324
319
  ctx.reportResults.push({
325
320
  repoName: repo.repoName,
326
321
  success: false,
@@ -334,9 +329,9 @@ async function runLifecyclePhase(repo, ctx) {
334
329
  * Run the file sync processor for a single repo and collect results.
335
330
  */
336
331
  async function runFileSyncPhase(repo, ctx) {
337
- const current = repo.index + 1;
332
+ const repoNumber = repo.index + 1;
338
333
  try {
339
- getLogger().progress(current, repo.repoName, "Processing...");
334
+ getLogger().progress(repoNumber, repo.repoName, "Processing...");
340
335
  const result = await ctx.processor.process(repo.repoConfig, repo.repoInfo, {
341
336
  branchName: ctx.branchName,
342
337
  workDir: repo.workDir,
@@ -363,17 +358,17 @@ async function runFileSyncPhase(repo, ctx) {
363
358
  error: result.success ? undefined : result.message,
364
359
  });
365
360
  if (result.skipped) {
366
- getLogger().skip(current, repo.repoName, result.message);
361
+ getLogger().skip(repoNumber, repo.repoName, result.message);
367
362
  }
368
363
  else if (result.success) {
369
- getLogger().success(current, repo.repoName, result.message);
364
+ getLogger().success(repoNumber, repo.repoName, result.message);
370
365
  }
371
366
  else {
372
- getLogger().error(current, repo.repoName, result.message);
367
+ getLogger().error(repoNumber, repo.repoName, result.message);
373
368
  }
374
369
  }
375
370
  catch (error) {
376
- getLogger().error(current, repo.repoName, toErrorMessage(error));
371
+ getLogger().error(repoNumber, repo.repoName, toErrorMessage(error));
377
372
  ctx.reportResults.push({
378
373
  repoName: repo.repoName,
379
374
  success: false,
@@ -1,7 +1,7 @@
1
1
  import type { MergeMode, MergeStrategy, RepoConfig } from "../config/index.js";
2
2
  import type { IRepoLifecycleManager } from "../lifecycle/index.js";
3
3
  import type { IRepositoryProcessor } from "../sync/index.js";
4
- import type { ISettingsProcessor, IRulesetProcessor, IRepoSettingsProcessor, ILabelsProcessor } from "../settings/index.js";
4
+ import type { ISettingsProcessor, IRulesetProcessor, IRepoSettingsProcessor, ILabelsProcessor, BaseProcessorResult } from "../settings/index.js";
5
5
  import type { RepoInfo } from "../shared/repo-detector.js";
6
6
  import type { ResultsCollector } from "./results-collector.js";
7
7
  export type ProcessorFactory = () => IRepositoryProcessor;
@@ -44,10 +44,7 @@ export interface SyncResultEntry {
44
44
  mergeOutcome?: "manual" | "auto" | "force" | "direct";
45
45
  error?: string;
46
46
  }
47
- export interface SettingsResult {
48
- success: boolean;
49
- message: string;
50
- skipped?: boolean;
47
+ export interface SettingsResult extends Pick<BaseProcessorResult, "success" | "message" | "skipped"> {
51
48
  planOutput?: {
52
49
  lines?: string[];
53
50
  };
@@ -61,7 +58,7 @@ export interface ApplyRepoSettingsContext {
61
58
  repoConfig: RepoConfig;
62
59
  repoInfo: RepoInfo;
63
60
  repoName: string;
64
- current: number;
61
+ repoNumber: number;
65
62
  options: SyncOptions;
66
63
  token: string | undefined;
67
64
  settingsCollector: ResultsCollector;
@@ -94,11 +94,6 @@ export function convertContentToString(content, fileName, options) {
94
94
  defaultKeyType: "PLAIN",
95
95
  });
96
96
  }
97
- if (format === "json5") {
98
- // JSON5 format - output standard JSON (which is valid JSON5)
99
- // Using JSON.stringify for standard JSON output that's compatible everywhere
100
- return JSON.stringify(content, null, 2) + "\n";
101
- }
102
- // JSON format - comments not supported, ignore header/schemaUrl
97
+ // JSON and JSON5 — both use standard JSON.stringify (valid JSON5 superset)
103
98
  return JSON.stringify(content, null, 2) + "\n";
104
99
  }
@@ -1,4 +1,5 @@
1
- export type { MergeMode, MergeStrategy, BypassActor, StatusCheckConfig, CodeScanningTool, PullRequestRuleParameters, RulesetRule, Ruleset, GitHubRepoSettings, RepoVisibility, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage, Label, RepoSettings, ContentValue, RawFileConfig, RawRepoFileOverride, RawRepoSettings, RawRepoConfig, RawConfig, RepoConfig, Config, } from "./types.js";
1
+ export type { MergeMode, MergeStrategy, BypassActor, StatusCheckConfig, CodeScanningTool, PullRequestRuleParameters, RulesetRule, Ruleset, GitHubRepoSettings, RepoVisibility, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage, Label, RepoSettings, RawFileConfig, RawRepoFileOverride, RawRepoSettings, RawRepoConfig, RawConfig, RepoConfig, Config, FileContent, ContentValue, } from "./types.js";
2
2
  export { RULESET_COMPARABLE_FIELDS } from "./types.js";
3
3
  export { loadRawConfig, loadConfig, normalizeConfig } from "./loader.js";
4
4
  export { convertContentToString } from "./formatter.js";
5
+ export { validateForSync } from "./validator.js";
@@ -4,3 +4,5 @@ export { RULESET_COMPARABLE_FIELDS } from "./types.js";
4
4
  export { loadRawConfig, loadConfig, normalizeConfig } from "./loader.js";
5
5
  // Config formatting
6
6
  export { convertContentToString } from "./formatter.js";
7
+ // Config validation
8
+ export { validateForSync } from "./validator.js";
@@ -20,13 +20,7 @@ export declare function deepMerge(base: Record<string, unknown>, overlay: Record
20
20
  * Standard $-prefixed keys ($schema, $id, $ref, etc.) are preserved.
21
21
  */
22
22
  export declare function stripMergeDirectives(obj: Record<string, unknown>): Record<string, unknown>;
23
- /**
24
- * Create a default merge context.
25
- */
26
23
  export declare function createMergeContext(defaultStrategy?: ArrayMergeStrategy): MergeContext;
27
- /**
28
- * Check if content is text type (string or string[]).
29
- */
30
24
  export declare function isTextContent(content: unknown): content is string | string[];
31
25
  /**
32
26
  * Merge two text content values.
@@ -9,18 +9,11 @@ import { isPlainObject } from "../shared/type-guards.js";
9
9
  * like $schema, $id, $ref, $generated are preserved.
10
10
  */
11
11
  const XFG_DIRECTIVES = new Set(["$arrayMerge", "$values"]);
12
- /**
13
- * Strategy map for array merge operations.
14
- * Extensible: add new strategies by adding to this map.
15
- */
16
12
  const arrayMergeStrategies = new Map([
17
13
  ["replace", (_base, overlay) => overlay],
18
14
  ["append", (base, overlay) => [...base, ...overlay]],
19
15
  ["prepend", (base, overlay) => [...overlay, ...base]],
20
16
  ]);
21
- /**
22
- * Merge two arrays based on the specified strategy.
23
- */
24
17
  function mergeArrays(base, overlay, strategy) {
25
18
  const handler = arrayMergeStrategies.get(strategy);
26
19
  if (handler) {
@@ -94,9 +87,6 @@ export function stripMergeDirectives(obj) {
94
87
  }
95
88
  return result;
96
89
  }
97
- /**
98
- * Create a default merge context.
99
- */
100
90
  export function createMergeContext(defaultStrategy = "replace") {
101
91
  return {
102
92
  defaultArrayStrategy: defaultStrategy,
@@ -105,9 +95,6 @@ export function createMergeContext(defaultStrategy = "replace") {
105
95
  // =============================================================================
106
96
  // Text Content Utilities
107
97
  // =============================================================================
108
- /**
109
- * Check if content is text type (string or string[]).
110
- */
111
98
  export function isTextContent(content) {
112
99
  return (typeof content === "string" ||
113
100
  (Array.isArray(content) &&
@@ -8,21 +8,13 @@ export interface PRMergeOptions {
8
8
  bypassReason?: string;
9
9
  labels?: string[];
10
10
  }
11
- /** Ruleset target type */
12
11
  export type RulesetTarget = "branch" | "tag";
13
- /** Ruleset enforcement level */
14
12
  export type RulesetEnforcement = "active" | "disabled" | "evaluate";
15
- /** Bypass actor type */
16
13
  export type BypassActorType = "Team" | "User" | "Integration";
17
- /** Bypass mode - always bypass or only for PRs */
18
14
  export type BypassMode = "always" | "pull_request";
19
- /** Pattern operator for pattern-based rules */
20
15
  type PatternOperator = "starts_with" | "ends_with" | "contains" | "regex";
21
- /** Allowed merge methods */
22
16
  export type MergeMethod = "merge" | "squash" | "rebase";
23
- /** Code scanning alerts threshold */
24
17
  export type AlertsThreshold = "none" | "errors" | "errors_and_warnings" | "all";
25
- /** Security alerts threshold */
26
18
  export type SecurityAlertsThreshold = "none" | "critical" | "high_or_higher" | "medium_or_higher" | "all";
27
19
  export interface BypassActor {
28
20
  actorId: number;
@@ -190,7 +182,6 @@ export interface MaxFileSizeRule {
190
182
  type: "max_file_size";
191
183
  parameters?: MaxFileSizeParameters;
192
184
  }
193
- /** Union of all rule types */
194
185
  export type RulesetRule = PullRequestRule | RequiredStatusChecksRule | RequiredSignaturesRule | RequiredLinearHistoryRule | NonFastForwardRule | CreationRule | UpdateRule | DeletionRule | RequiredDeploymentsRule | CodeScanningRule | CodeQualityRule | WorkflowsRule | CommitAuthorEmailPatternRule | CommitMessagePatternRule | CommitterEmailPatternRule | BranchNamePatternRule | TagNamePatternRule | FilePathRestrictionRule | FileExtensionRestrictionRule | MaxFilePathLengthRule | MaxFileSizeRule;
195
186
  /**
196
187
  * GitHub Ruleset configuration.
@@ -1,7 +1,7 @@
1
1
  import type { RawConfig, RawRepoSettings, RawRootSettings } from "./types.js";
2
2
  /**
3
3
  * Validates raw config structure before normalization.
4
- * @throws Error if validation fails
4
+ * @throws ValidationError if validation fails
5
5
  */
6
6
  export declare function validateRawConfig(config: RawConfig): void;
7
7
  /**
@@ -418,7 +418,7 @@ function hasGroupFiles(config) {
418
418
  }
419
419
  /**
420
420
  * Validates raw config structure before normalization.
421
- * @throws Error if validation fails
421
+ * @throws ValidationError if validation fails
422
422
  */
423
423
  export function validateRawConfig(config) {
424
424
  validateConfigId(config);