@aspruyt/xfg 6.0.3 → 6.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.
Files changed (143) hide show
  1. package/dist/cli/lifecycle-report-builder.d.ts +2 -2
  2. package/dist/cli/lifecycle-report-builder.js +3 -11
  3. package/dist/cli/program.d.ts +2 -1
  4. package/dist/cli/program.js +2 -3
  5. package/dist/cli/repo-sync-runner.d.ts +24 -0
  6. package/dist/cli/repo-sync-runner.js +156 -0
  7. package/dist/cli/results-collector.d.ts +1 -1
  8. package/dist/cli/results-collector.js +2 -2
  9. package/dist/cli/settings-factories.d.ts +7 -0
  10. package/dist/cli/settings-factories.js +27 -0
  11. package/dist/cli/settings-report-builder.d.ts +1 -1
  12. package/dist/cli/settings-report-builder.js +12 -23
  13. package/dist/cli/settings-runner.d.ts +2 -0
  14. package/dist/cli/settings-runner.js +87 -0
  15. package/dist/cli/sync-command.d.ts +1 -1
  16. package/dist/cli/sync-command.js +31 -372
  17. package/dist/cli/sync-report-builder.d.ts +1 -1
  18. package/dist/cli/sync-utils.d.ts +8 -0
  19. package/dist/cli/sync-utils.js +36 -0
  20. package/dist/cli/types.d.ts +5 -7
  21. package/dist/cli/unified-summary.d.ts +1 -3
  22. package/dist/cli/unified-summary.js +7 -5
  23. package/dist/cli.js +2 -1
  24. package/dist/{shared → config}/env.js +2 -2
  25. package/dist/config/extends-resolver.js +4 -3
  26. package/dist/config/file-reference-resolver.js +4 -2
  27. package/dist/config/formatter.js +18 -1
  28. package/dist/config/index.d.ts +1 -1
  29. package/dist/config/loader.js +30 -6
  30. package/dist/config/merge.d.ts +11 -1
  31. package/dist/config/merge.js +78 -6
  32. package/dist/config/normalizer.js +53 -38
  33. package/dist/config/validator.d.ts +1 -4
  34. package/dist/config/validator.js +13 -599
  35. package/dist/config/validators/file-validator.d.ts +2 -1
  36. package/dist/config/validators/file-validator.js +9 -1
  37. package/dist/config/validators/group-validator.d.ts +3 -0
  38. package/dist/config/validators/group-validator.js +167 -0
  39. package/dist/config/validators/repo-entry-validator.d.ts +2 -0
  40. package/dist/config/validators/repo-entry-validator.js +165 -0
  41. package/dist/config/validators/repo-settings-validator.js +18 -7
  42. package/dist/config/validators/ruleset-validator.js +2 -5
  43. package/dist/config/validators/shared.d.ts +11 -0
  44. package/dist/config/validators/shared.js +242 -0
  45. package/dist/lifecycle/ado-migration-source.js +2 -4
  46. package/dist/lifecycle/github-lifecycle-provider.d.ts +7 -11
  47. package/dist/lifecycle/github-lifecycle-provider.js +125 -136
  48. package/dist/lifecycle/{lifecycle-helpers.d.ts → helpers.d.ts} +5 -1
  49. package/dist/lifecycle/{lifecycle-helpers.js → helpers.js} +9 -8
  50. package/dist/lifecycle/index.d.ts +2 -2
  51. package/dist/lifecycle/index.js +1 -1
  52. package/dist/lifecycle/repo-lifecycle-factory.d.ts +2 -2
  53. package/dist/output/github-summary.js +2 -3
  54. package/dist/output/index.d.ts +4 -0
  55. package/dist/output/index.js +4 -0
  56. package/dist/output/lifecycle-report.d.ts +1 -1
  57. package/dist/output/lifecycle-report.js +5 -0
  58. package/dist/output/sync-report.d.ts +25 -3
  59. package/dist/output/sync-report.js +11 -11
  60. package/dist/settings/base-processor.d.ts +18 -7
  61. package/dist/settings/base-processor.js +26 -5
  62. package/dist/settings/code-scanning/diff.js +2 -2
  63. package/dist/settings/code-scanning/formatter.d.ts +2 -6
  64. package/dist/settings/code-scanning/formatter.js +2 -25
  65. package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +3 -7
  66. package/dist/settings/code-scanning/github-code-scanning-strategy.js +2 -2
  67. package/dist/settings/code-scanning/processor.js +6 -4
  68. package/dist/settings/code-scanning/types.d.ts +10 -8
  69. package/dist/settings/labels/github-labels-strategy.d.ts +3 -11
  70. package/dist/settings/labels/types.d.ts +12 -10
  71. package/dist/settings/repo-settings/diff.d.ts +1 -1
  72. package/dist/settings/repo-settings/diff.js +1 -1
  73. package/dist/settings/repo-settings/formatter.d.ts +2 -6
  74. package/dist/settings/repo-settings/formatter.js +4 -23
  75. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -2
  76. package/dist/settings/repo-settings/github-repo-settings-strategy.js +8 -7
  77. package/dist/settings/repo-settings/processor.js +11 -11
  78. package/dist/settings/repo-settings/types.d.ts +2 -2
  79. package/dist/settings/rulesets/diff-algorithm.js +4 -2
  80. package/dist/settings/rulesets/diff.js +2 -51
  81. package/dist/settings/rulesets/formatter.js +4 -0
  82. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +3 -3
  83. package/dist/settings/rulesets/github-ruleset-strategy.js +4 -6
  84. package/dist/settings/rulesets/index.d.ts +1 -1
  85. package/dist/settings/rulesets/index.js +0 -2
  86. package/dist/settings/rulesets/processor.js +1 -1
  87. package/dist/settings/rulesets/types.d.ts +6 -2
  88. package/dist/shared/command-executor.d.ts +4 -4
  89. package/dist/shared/command-executor.js +9 -7
  90. package/dist/shared/diff-format.d.ts +1 -0
  91. package/dist/shared/diff-format.js +10 -0
  92. package/dist/shared/errors.d.ts +7 -4
  93. package/dist/shared/errors.js +8 -8
  94. package/dist/shared/gh-api-utils.d.ts +3 -34
  95. package/dist/shared/gh-api-utils.js +23 -53
  96. package/dist/shared/gh-token-utils.d.ts +26 -0
  97. package/dist/shared/gh-token-utils.js +32 -0
  98. package/dist/shared/json-utils.js +1 -1
  99. package/dist/shared/regex-utils.d.ts +1 -0
  100. package/dist/shared/regex-utils.js +3 -0
  101. package/dist/shared/retry-utils.d.ts +1 -0
  102. package/dist/shared/retry-utils.js +13 -7
  103. package/dist/sync/auth-options-builder.js +1 -1
  104. package/dist/sync/branch-manager.js +5 -3
  105. package/dist/sync/commit-push-manager.js +2 -3
  106. package/dist/sync/diff-utils.d.ts +0 -1
  107. package/dist/sync/diff-utils.js +5 -10
  108. package/dist/sync/file-sync-orchestrator.js +0 -2
  109. package/dist/sync/file-writer.d.ts +3 -0
  110. package/dist/sync/file-writer.js +84 -81
  111. package/dist/sync/index.d.ts +0 -1
  112. package/dist/sync/index.js +0 -1
  113. package/dist/sync/manifest.js +1 -1
  114. package/dist/sync/pr-merge-handler.js +6 -6
  115. package/dist/sync/sync-workflow.js +1 -1
  116. package/dist/sync/types.d.ts +2 -2
  117. package/dist/vcs/ado-pr-strategy.d.ts +3 -5
  118. package/dist/vcs/ado-pr-strategy.js +131 -33
  119. package/dist/vcs/authenticated-git-ops.js +45 -23
  120. package/dist/vcs/git-commit-strategy.js +10 -6
  121. package/dist/vcs/git-ops.js +30 -24
  122. package/dist/vcs/github-pr-strategy.d.ts +3 -2
  123. package/dist/vcs/github-pr-strategy.js +80 -30
  124. package/dist/vcs/gitlab-pr-strategy.d.ts +2 -5
  125. package/dist/vcs/gitlab-pr-strategy.js +88 -87
  126. package/dist/vcs/graphql-commit-strategy.d.ts +1 -5
  127. package/dist/vcs/graphql-commit-strategy.js +21 -37
  128. package/dist/vcs/pr-creator.js +9 -2
  129. package/dist/vcs/pr-strategy.d.ts +2 -3
  130. package/dist/vcs/pr-strategy.js +0 -1
  131. package/dist/vcs/types.d.ts +9 -5
  132. package/package.json +5 -5
  133. package/dist/config/validators/index.d.ts +0 -3
  134. package/dist/config/validators/index.js +0 -6
  135. package/dist/output/types.d.ts +0 -20
  136. package/dist/output/types.js +0 -1
  137. package/dist/shared/shell-utils.d.ts +0 -6
  138. package/dist/shared/shell-utils.js +0 -17
  139. /package/dist/{shared → config}/env.d.ts +0 -0
  140. /package/dist/lifecycle/{lifecycle-formatter.d.ts → formatter.d.ts} +0 -0
  141. /package/dist/lifecycle/{lifecycle-formatter.js → formatter.js} +0 -0
  142. /package/dist/{vcs → shared}/sanitize-utils.d.ts +0 -0
  143. /package/dist/{vcs → shared}/sanitize-utils.js +0 -0
@@ -1,209 +1,47 @@
1
- import { resolve, join } from "node:path";
1
+ import { resolve } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
3
  import { loadRawConfig, normalizeConfig, validateForSync, } from "../config/index.js";
4
4
  import { ValidationError, SyncError } from "../shared/errors.js";
5
- import { parseGitUrl, getRepoDisplayName, isGitHubRepo, } from "../repo/index.js";
6
- import { sanitizeBranchName, validateBranchName } from "./branch-utils.js";
5
+ import { validateBranchName } from "./branch-utils.js";
7
6
  import { createTokenManager } from "../vcs/index.js";
8
7
  import { RepositoryProcessor } from "../sync/index.js";
9
- import { RulesetProcessor, RepoSettingsProcessor, LabelsProcessor, CodeScanningProcessor, GitHubRulesetStrategy, GitHubRepoSettingsStrategy, GitHubLabelsStrategy, GitHubCodeScanningStrategy, } from "../settings/index.js";
10
- import { GitHubRepoMetadataProvider } from "../repo/index.js";
11
- import { ShellCommandExecutor } from "../shared/command-executor.js";
8
+ import { ProcessExecutor } from "../shared/command-executor.js";
12
9
  import { Logger } from "../shared/logger.js";
13
- import { generateWorkspaceName } from "../shared/workspace-utils.js";
14
- let _defaultExecutor;
15
- let _logger;
16
- function getDefaultExecutor() {
17
- return (_defaultExecutor ??= new ShellCommandExecutor(process.env));
18
- }
19
- function getLogger() {
20
- return (_logger ??= new Logger(!!(process.env.DEBUG || process.env.XFG_DEBUG)));
21
- }
22
- function createDefaultRulesetProcessorFactory() {
23
- const cwd = process.cwd();
24
- return () => new RulesetProcessor(new GitHubRulesetStrategy(getDefaultExecutor(), { cwd }));
25
- }
26
- function createDefaultRepoSettingsProcessorFactory() {
27
- const cwd = process.cwd();
28
- const executor = getDefaultExecutor();
29
- return () => new RepoSettingsProcessor(new GitHubRepoSettingsStrategy(executor, { cwd }), new GitHubRepoMetadataProvider(executor, { cwd }));
30
- }
31
- function createDefaultLabelsProcessorFactory() {
32
- const cwd = process.cwd();
33
- return () => new LabelsProcessor(new GitHubLabelsStrategy(getDefaultExecutor(), { cwd }));
34
- }
35
- function createDefaultCodeScanningProcessorFactory() {
36
- const cwd = process.cwd();
37
- const executor = getDefaultExecutor();
38
- return () => new CodeScanningProcessor(new GitHubCodeScanningStrategy(executor, { cwd }), new GitHubRepoMetadataProvider(executor, { cwd }));
39
- }
40
10
  import { ResultsCollector } from "./results-collector.js";
41
- import { buildSettingsReport, } from "./settings-report-builder.js";
42
- import { formatSettingsReportCLI } from "../output/settings-report.js";
11
+ import { buildSettingsReport } from "./settings-report-builder.js";
12
+ import { formatSettingsReportCLI, formatSyncReportCLI, formatLifecycleReportCLI, hasLifecycleChanges, } from "../output/index.js";
43
13
  import { buildSyncReport } from "./sync-report-builder.js";
44
- import { formatSyncReportCLI } from "../output/sync-report.js";
45
14
  import { buildLifecycleReport } from "./lifecycle-report-builder.js";
46
- import { formatLifecycleReportCLI, hasLifecycleChanges, } from "../output/lifecycle-report.js";
47
15
  import { writeUnifiedSummary } from "./unified-summary.js";
48
- import { toErrorMessage } from "../shared/type-guards.js";
49
- import { resolveGitHubToken } from "../shared/gh-api-utils.js";
50
- import { RepoLifecycleManager, runLifecycleCheck, } from "../lifecycle/index.js";
51
- function getUniqueFileNames(config) {
52
- const fileNames = new Set();
53
- for (const repo of config.repos) {
54
- for (const file of repo.files) {
55
- fileNames.add(file.fileName);
56
- }
57
- }
58
- return Array.from(fileNames);
59
- }
60
- function generateBranchName(fileNames) {
61
- if (fileNames.length === 1) {
62
- return `chore/sync-${sanitizeBranchName(fileNames[0])}`;
63
- }
64
- return "chore/sync-config";
65
- }
66
- function formatFileNames(fileNames) {
67
- if (fileNames.length === 1) {
68
- return fileNames[0];
69
- }
70
- if (fileNames.length <= 3) {
71
- return fileNames.join(", ");
72
- }
73
- return `${fileNames.length} files`;
74
- }
75
- function determineMergeOutcome(result) {
76
- if (!result.success)
77
- return undefined;
78
- if (!result.prUrl)
79
- return "direct";
80
- if (result.mergeResult?.merged)
81
- return "force";
82
- if (result.mergeResult?.autoMergeEnabled)
83
- return "auto";
84
- return "manual";
85
- }
86
- function logSettingsResult(result, label, repoNumber, repoName, settingsCollector) {
87
- if (result.planOutput?.lines?.length) {
88
- getLogger().info("");
89
- getLogger().info(`${repoName} - ${label}:`);
90
- for (const line of result.planOutput.lines) {
91
- getLogger().info(line);
92
- }
93
- if (result.warnings?.length) {
94
- for (const warning of result.warnings) {
95
- getLogger().warn(warning);
96
- }
97
- }
98
- }
99
- else if (!result.skipped && result.success) {
100
- getLogger().success(repoNumber, repoName, `${label}: ${result.message}`);
101
- }
102
- if (!result.success && !result.skipped) {
103
- getLogger().error(repoNumber, repoName, `${label}: ${result.message}`);
104
- settingsCollector.appendError(repoName, result.message);
105
- }
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
- async function runAndStoreResult(factory, repoConfig, repoInfo, opts, repoName, settingsCollector, 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
- function buildSettingsDescriptors(ctx) {
119
- const { repoConfig, repoInfo, options, token, repoName, settingsCollector } = ctx;
120
- const { factories } = ctx;
121
- const sharedOpts = {
122
- dryRun: options.dryRun,
123
- noDelete: options.noDelete,
124
- token,
125
- };
126
- return [
127
- {
128
- key: "rulesets",
129
- label: "Rulesets",
130
- run: () => runAndStoreResult(factories.rulesets, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
131
- e.rulesetResult = r;
132
- }),
133
- },
134
- {
135
- key: "labels",
136
- label: "Labels",
137
- run: () => runAndStoreResult(factories.labels, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
138
- e.labelsResult = r;
139
- }),
140
- },
141
- {
142
- key: "repo",
143
- label: "Repo Settings",
144
- run: () => runAndStoreResult(factories.repo, repoConfig, repoInfo, { dryRun: options.dryRun, token }, repoName, settingsCollector, (e, r) => {
145
- e.settingsResult = r;
146
- }),
147
- },
148
- {
149
- key: "codeScanning",
150
- label: "Code Scanning",
151
- run: () => runAndStoreResult(factories.codeScanning, repoConfig, repoInfo, sharedOpts, repoName, settingsCollector, (e, r) => {
152
- e.codeScanningResult = r;
153
- }),
154
- },
155
- ];
156
- }
157
- function runSettingsProcessor(factory, repoConfig, repoInfo, processOptions) {
158
- return factory()
159
- .process(repoConfig, repoInfo, processOptions)
160
- .then((result) => result);
161
- }
162
- async function applyRepoSettings(ctx) {
163
- const { repoConfig, repoInfo, repoName, repoNumber, settingsCollector } = ctx;
164
- if (!repoConfig.settings || !isGitHubRepo(repoInfo))
165
- return;
166
- for (const desc of buildSettingsDescriptors(ctx)) {
167
- const settingsValue = repoConfig.settings[desc.key];
168
- if (!settingsValue || Object.keys(settingsValue).length === 0)
169
- continue;
170
- try {
171
- const result = await desc.run();
172
- logSettingsResult(result, desc.label, repoNumber, repoName, settingsCollector);
173
- }
174
- catch (error) {
175
- getLogger().error(repoNumber, repoName, `${desc.label}: ${toErrorMessage(error)}`);
176
- settingsCollector.appendError(repoName, error);
177
- }
178
- }
179
- }
180
- function displayReports(reportResults, lifecycleReportInputs, settingsCollector, dryRun) {
16
+ import { RepoLifecycleManager } from "../lifecycle/index.js";
17
+ import { createDefaultFactories } from "./settings-factories.js";
18
+ import { getUniqueFileNames, generateBranchName, formatFileNames, } from "./sync-utils.js";
19
+ import { runSingleRepo, } from "./repo-sync-runner.js";
20
+ function displayReports(logger, reportResults, lifecycleReportInputs, settingsCollector, dryRun) {
181
21
  const lifecycleReport = buildLifecycleReport(lifecycleReportInputs);
182
22
  if (hasLifecycleChanges(lifecycleReport)) {
183
- getLogger().log("");
23
+ logger.log("");
184
24
  for (const line of formatLifecycleReportCLI(lifecycleReport)) {
185
- getLogger().log(line);
25
+ logger.log(line);
186
26
  }
187
27
  }
188
28
  const report = buildSyncReport(reportResults);
189
- getLogger().log("");
29
+ logger.log("");
190
30
  for (const line of formatSyncReportCLI(report)) {
191
- getLogger().log(line);
31
+ logger.log(line);
192
32
  }
193
- // Build and display settings report (if any settings were processed)
194
33
  const settingsResults = settingsCollector.getAll();
195
34
  let settingsReport;
196
35
  if (settingsResults.length > 0) {
197
36
  settingsReport = buildSettingsReport(settingsResults);
198
37
  const settingsLines = formatSettingsReportCLI(settingsReport);
199
38
  if (settingsLines.length > 0) {
200
- getLogger().log("");
39
+ logger.log("");
201
40
  for (const line of settingsLines) {
202
- getLogger().log(line);
41
+ logger.log(line);
203
42
  }
204
43
  }
205
44
  }
206
- // Write unified summary to GITHUB_STEP_SUMMARY
207
45
  writeUnifiedSummary({
208
46
  lifecycle: lifecycleReport,
209
47
  sync: report,
@@ -212,198 +50,18 @@ function displayReports(reportResults, lifecycleReportInputs, settingsCollector,
212
50
  summaryPath: process.env.GITHUB_STEP_SUMMARY,
213
51
  });
214
52
  }
215
- function pushFailure(results, repoName, error) {
216
- results.push({
217
- repoName,
218
- success: false,
219
- fileChanges: [],
220
- error: toErrorMessage(error),
221
- });
222
- }
223
- /**
224
- * Process a single repository: resolve URL, run lifecycle check, sync files, apply settings.
225
- * Pushes results into ctx.reportResults, ctx.lifecycleReportInputs, and ctx.settingsCollector.
226
- */
227
- async function processSingleRepo(repoConfig, index, ctx) {
228
- const { config, options } = ctx;
229
- const repoNumber = index + 1;
230
- // Apply CLI-level PR option overrides
231
- if (options.merge || options.mergeStrategy || options.deleteBranch) {
232
- repoConfig.prOptions = {
233
- ...repoConfig.prOptions,
234
- merge: options.merge ?? repoConfig.prOptions?.merge,
235
- mergeStrategy: options.mergeStrategy ?? repoConfig.prOptions?.mergeStrategy,
236
- deleteBranch: options.deleteBranch ?? repoConfig.prOptions?.deleteBranch,
237
- };
238
- }
239
- const mergeMode = repoConfig.prOptions?.merge ?? "auto";
240
- if (mergeMode === "direct" && repoConfig.prOptions?.mergeStrategy) {
241
- getLogger().warn(`mergeStrategy '${repoConfig.prOptions.mergeStrategy}' is ignored in direct mode for ${repoConfig.git}`);
242
- }
243
- let repoInfo;
244
- try {
245
- repoInfo = parseGitUrl(repoConfig.git, {
246
- githubHosts: config.githubHosts,
247
- });
248
- }
249
- catch (error) {
250
- getLogger().error(repoNumber, repoConfig.git, toErrorMessage(error));
251
- pushFailure(ctx.reportResults, repoConfig.git, error);
252
- return;
253
- }
254
- const repoName = getRepoDisplayName(repoInfo);
255
- const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(index)));
256
- const repoToken = isGitHubRepo(repoInfo)
257
- ? (await resolveGitHubToken({
258
- repoInfo: repoInfo,
259
- tokenManager: ctx.tokenManager,
260
- context: repoName,
261
- log: getLogger(),
262
- envToken: process.env.GH_TOKEN,
263
- })).token
264
- : undefined;
265
- const repo = {
266
- repoConfig,
267
- repoInfo,
268
- repoName,
269
- index,
270
- workDir,
271
- token: repoToken,
272
- };
273
- const skipFileSync = await runLifecyclePhase(repo, ctx);
274
- if (skipFileSync)
275
- return;
276
- // Sync files via processor
277
- await runFileSyncPhase(repo, ctx);
278
- // Apply settings via API (GitHub-only — ADO and GitLab repos are skipped)
279
- await applyRepoSettings({
280
- repoConfig,
281
- repoInfo,
282
- repoName,
283
- repoNumber,
284
- options,
285
- token: repoToken,
286
- settingsCollector: ctx.settingsCollector,
287
- factories: ctx.factories,
288
- });
289
- }
290
- /**
291
- * Run lifecycle check (repo existence, creation, forking).
292
- * Returns true if the main loop should skip file sync for this repo.
293
- */
294
- async function runLifecyclePhase(repo, ctx) {
295
- const repoNumber = repo.index + 1;
296
- try {
297
- const { outputLines, lifecycleResult, createSettings } = await runLifecycleCheck(repo.repoConfig, repo.repoInfo, {
298
- dryRun: ctx.options.dryRun ?? false,
299
- resolvedWorkDir: repo.workDir,
300
- githubHosts: ctx.config.githubHosts,
301
- token: repo.token,
302
- repoIndex: repo.index,
303
- lifecycleManager: ctx.lifecycleManager,
304
- repoSettings: ctx.config.settings?.repo,
305
- });
306
- for (const line of outputLines) {
307
- getLogger().info(line);
308
- }
309
- ctx.lifecycleReportInputs.push({
310
- repoName: repo.repoName,
311
- action: lifecycleResult.action,
312
- upstream: repo.repoConfig.upstream,
313
- source: repo.repoConfig.source,
314
- settings: createSettings
315
- ? {
316
- visibility: createSettings.visibility,
317
- description: createSettings.description,
318
- }
319
- : undefined,
320
- });
321
- // In dry-run, skip processing repos that don't exist yet
322
- if (ctx.options.dryRun && lifecycleResult.action !== "existed") {
323
- ctx.reportResults.push({
324
- repoName: repo.repoName,
325
- success: true,
326
- fileChanges: [],
327
- });
328
- return true;
329
- }
330
- return false;
331
- }
332
- catch (error) {
333
- getLogger().error(repoNumber, repo.repoName, `Lifecycle error: ${toErrorMessage(error)}`);
334
- pushFailure(ctx.reportResults, repo.repoName, error);
335
- return true;
336
- }
337
- }
338
- /**
339
- * Run the file sync processor for a single repo and collect results.
340
- */
341
- async function runFileSyncPhase(repo, ctx) {
342
- const repoNumber = repo.index + 1;
343
- try {
344
- getLogger().progress(repoNumber, repo.repoName, "Processing...");
345
- const result = await ctx.processor.process(repo.repoConfig, repo.repoInfo, {
346
- branchName: ctx.branchName,
347
- workDir: repo.workDir,
348
- configId: ctx.config.id,
349
- dryRun: ctx.options.dryRun,
350
- retries: ctx.options.retries,
351
- executor: getDefaultExecutor(),
352
- prTemplate: ctx.config.prTemplate,
353
- noDelete: ctx.options.noDelete,
354
- token: repo.token,
355
- hasAppCredentials: isGitHubRepo(repo.repoInfo) && ctx.tokenManager !== null,
356
- });
357
- const mergeOutcome = determineMergeOutcome(result);
358
- ctx.reportResults.push({
359
- repoName: repo.repoName,
360
- success: result.success,
361
- fileChanges: (result.fileChanges ?? []).map((f) => ({
362
- path: f.path,
363
- action: f.action,
364
- ...(f.diffLines ? { diffLines: f.diffLines } : {}),
365
- })),
366
- prUrl: result.prUrl,
367
- mergeOutcome,
368
- error: result.success ? undefined : result.message,
369
- });
370
- if (result.skipped) {
371
- getLogger().skip(repoNumber, repo.repoName, result.message);
372
- }
373
- else if (result.success) {
374
- getLogger().success(repoNumber, repo.repoName, result.message);
375
- }
376
- else {
377
- getLogger().error(repoNumber, repo.repoName, result.message);
378
- }
379
- }
380
- catch (error) {
381
- getLogger().error(repoNumber, repo.repoName, toErrorMessage(error));
382
- pushFailure(ctx.reportResults, repo.repoName, error);
383
- }
384
- }
385
53
  export async function runSync(options, deps = {}) {
386
- // Reset module-level singletons to ensure fresh state per invocation
387
- _defaultExecutor = undefined;
388
- _logger = undefined;
54
+ const executor = new ProcessExecutor(process.env);
55
+ const logger = new Logger(!!(process.env.DEBUG || process.env.XFG_DEBUG));
389
56
  const { lifecycleManager, settingsProcessorFactories } = deps;
390
- const factories = {
391
- rulesets: settingsProcessorFactories?.rulesets ??
392
- createDefaultRulesetProcessorFactory(),
393
- labels: settingsProcessorFactories?.labels ??
394
- createDefaultLabelsProcessorFactory(),
395
- repo: settingsProcessorFactories?.repo ??
396
- createDefaultRepoSettingsProcessorFactory(),
397
- codeScanning: settingsProcessorFactories?.codeScanning ??
398
- createDefaultCodeScanningProcessorFactory(),
399
- };
57
+ const factories = createDefaultFactories(executor, settingsProcessorFactories);
400
58
  const configPath = resolve(options.config);
401
59
  if (!existsSync(configPath)) {
402
60
  throw new ValidationError(`Config path not found: ${configPath}`);
403
61
  }
404
- getLogger().log(`Loading config from: ${configPath}`);
62
+ logger.log(`Loading config from: ${configPath}`);
405
63
  if (options.dryRun) {
406
- getLogger().log("Running in DRY RUN mode - no changes will be made\n");
64
+ logger.log("Running in DRY RUN mode - no changes will be made\n");
407
65
  }
408
66
  const rawConfig = loadRawConfig(configPath);
409
67
  validateForSync(rawConfig);
@@ -417,10 +75,10 @@ export async function runSync(options, deps = {}) {
417
75
  else {
418
76
  branchName = generateBranchName(fileNames);
419
77
  }
420
- getLogger().setTotal(config.repos.length);
421
- getLogger().log(`Found ${config.repos.length} repositories to process`);
422
- getLogger().log(`Target files: ${formatFileNames(fileNames)}`);
423
- getLogger().log(`Branch: ${branchName}\n`);
78
+ logger.setTotal(config.repos.length);
79
+ logger.log(`Found ${config.repos.length} repositories to process`);
80
+ logger.log(`Target files: ${formatFileNames(fileNames)}`);
81
+ logger.log(`Branch: ${branchName}\n`);
424
82
  const tokenManager = createTokenManager(process.env.XFG_GITHUB_CLIENT_ID && process.env.XFG_GITHUB_APP_PRIVATE_KEY
425
83
  ? {
426
84
  clientId: process.env.XFG_GITHUB_CLIENT_ID,
@@ -429,7 +87,7 @@ export async function runSync(options, deps = {}) {
429
87
  : undefined);
430
88
  const processor = deps.processorFactory
431
89
  ? deps.processorFactory()
432
- : new RepositoryProcessor(undefined, getLogger(), {
90
+ : new RepositoryProcessor(undefined, logger, {
433
91
  tokenManager,
434
92
  envToken: process.env.GH_TOKEN,
435
93
  });
@@ -439,18 +97,19 @@ export async function runSync(options, deps = {}) {
439
97
  branchName,
440
98
  processor,
441
99
  lifecycleManager: lifecycleManager ??
442
- new RepoLifecycleManager(undefined, getDefaultExecutor(), options.retries, process.cwd(), getLogger()),
100
+ new RepoLifecycleManager(undefined, executor, options.retries, process.cwd(), logger),
443
101
  tokenManager,
444
102
  reportResults: [],
445
103
  lifecycleReportInputs: [],
446
104
  settingsCollector: new ResultsCollector(),
447
105
  factories,
106
+ logger,
107
+ executor,
448
108
  };
449
109
  for (let i = 0; i < config.repos.length; i++) {
450
- await processSingleRepo(config.repos[i], i, ctx);
110
+ await runSingleRepo(config.repos[i], i, ctx);
451
111
  }
452
- displayReports(ctx.reportResults, ctx.lifecycleReportInputs, ctx.settingsCollector, options.dryRun ?? false);
453
- // Propagate failures to caller (CLI entry handles process.exit)
112
+ displayReports(logger, ctx.reportResults, ctx.lifecycleReportInputs, ctx.settingsCollector, options.dryRun ?? false);
454
113
  const settingsResults = ctx.settingsCollector.getAll();
455
114
  const hasErrors = ctx.reportResults.some((r) => r.error);
456
115
  const hasSettingsErrors = settingsResults.some((r) => r.error);
@@ -1,5 +1,5 @@
1
1
  import type { MergeMode } from "../config/index.js";
2
- import type { SyncReport, ReportFileChange } from "../output/sync-report.js";
2
+ import type { SyncReport, ReportFileChange } from "../output/index.js";
3
3
  interface SyncResultInput {
4
4
  repoName: string;
5
5
  success: boolean;
@@ -0,0 +1,8 @@
1
+ import type { RepoConfig, MergeMode } from "../config/index.js";
2
+ import type { ProcessorResult } from "../sync/index.js";
3
+ export declare function getUniqueFileNames(config: {
4
+ repos: RepoConfig[];
5
+ }): string[];
6
+ export declare function generateBranchName(fileNames: string[]): string;
7
+ export declare function formatFileNames(fileNames: string[]): string;
8
+ export declare function determineMergeOutcome(result: ProcessorResult): MergeMode | undefined;
@@ -0,0 +1,36 @@
1
+ import { sanitizeBranchName } from "./branch-utils.js";
2
+ export function getUniqueFileNames(config) {
3
+ const fileNames = new Set();
4
+ for (const repo of config.repos) {
5
+ for (const file of repo.files) {
6
+ fileNames.add(file.fileName);
7
+ }
8
+ }
9
+ return Array.from(fileNames);
10
+ }
11
+ export function generateBranchName(fileNames) {
12
+ if (fileNames.length === 1) {
13
+ return `chore/sync-${sanitizeBranchName(fileNames[0])}`;
14
+ }
15
+ return "chore/sync-config";
16
+ }
17
+ export function formatFileNames(fileNames) {
18
+ if (fileNames.length === 1) {
19
+ return fileNames[0];
20
+ }
21
+ if (fileNames.length <= 3) {
22
+ return fileNames.join(", ");
23
+ }
24
+ return `${fileNames.length} files`;
25
+ }
26
+ export function determineMergeOutcome(result) {
27
+ if (!result.success)
28
+ return undefined;
29
+ if (!result.prUrl)
30
+ return "direct";
31
+ if (result.mergeResult?.merged)
32
+ return "force";
33
+ if (result.mergeResult?.autoMergeEnabled)
34
+ return "auto";
35
+ return "manual";
36
+ }
@@ -1,9 +1,10 @@
1
1
  import type { MergeMode, MergeStrategy, RepoConfig } from "../config/index.js";
2
2
  import type { IRepoLifecycleManager } from "../lifecycle/index.js";
3
- import type { IRepositoryProcessor } from "../sync/index.js";
4
- import type { ISettingsProcessor, IRulesetProcessor, IRepoSettingsProcessor, ILabelsProcessor, ICodeScanningProcessor, BaseProcessorResult, ActiveAction } from "../settings/index.js";
3
+ import type { IRepositoryProcessor, FileChangeDetail } from "../sync/index.js";
4
+ import type { ISettingsProcessor, IRulesetProcessor, IRepoSettingsProcessor, ILabelsProcessor, ICodeScanningProcessor, BaseProcessorResult } from "../settings/index.js";
5
5
  import type { RepoInfo } from "../repo/index.js";
6
6
  import type { ResultsCollector } from "./results-collector.js";
7
+ import type { Logger } from "../shared/logger.js";
7
8
  export type ProcessorFactory = () => IRepositoryProcessor;
8
9
  export type SettingsProcessorFactory<T extends ISettingsProcessor> = () => T;
9
10
  export type RulesetProcessorFactory = SettingsProcessorFactory<IRulesetProcessor>;
@@ -41,11 +42,7 @@ export interface SyncOptions extends SharedOptions {
41
42
  export interface SyncResultEntry {
42
43
  repoName: string;
43
44
  success: boolean;
44
- fileChanges: Array<{
45
- path: string;
46
- action: ActiveAction;
47
- diffLines?: string[];
48
- }>;
45
+ fileChanges: FileChangeDetail[];
49
46
  prUrl?: string;
50
47
  mergeOutcome?: MergeMode;
51
48
  error?: string;
@@ -69,4 +66,5 @@ export interface ApplyRepoSettingsContext {
69
66
  token: string | undefined;
70
67
  settingsCollector: ResultsCollector;
71
68
  factories: SettingsProcessorFactories;
69
+ logger: Logger;
72
70
  }
@@ -1,6 +1,4 @@
1
- import type { LifecycleReport } from "../output/lifecycle-report.js";
2
- import type { SyncReport } from "../output/types.js";
3
- import type { SettingsReport } from "../output/settings-report.js";
1
+ import { type LifecycleReport, type SyncReport, type SettingsReport } from "../output/index.js";
4
2
  interface UnifiedSummaryInput {
5
3
  lifecycle?: LifecycleReport;
6
4
  sync?: SyncReport;
@@ -1,7 +1,4 @@
1
- import { hasLifecycleChanges } from "../output/lifecycle-report.js";
2
- import { writeGitHubStepSummary } from "../output/github-summary.js";
3
- import { renderSyncLines } from "../output/sync-report.js";
4
- import { renderRepoSettingsDiffLines, formatCountEntry, } from "../output/settings-report.js";
1
+ import { hasLifecycleChanges, writeGitHubStepSummary, renderSyncLines, renderRepoSettingsDiffLines, formatCountEntry, } from "../output/index.js";
5
2
  // =============================================================================
6
3
  // Helpers
7
4
  // =============================================================================
@@ -121,6 +118,11 @@ function renderLifecycleLines(lcAction, diffLines) {
121
118
  case "migrated":
122
119
  diffLines.push(`+ MIGRATE ${lcAction.source ?? "source"} -> ${lcAction.repoName}`);
123
120
  break;
121
+ /* c8 ignore next 4 */
122
+ default: {
123
+ const _exhaustive = lcAction.action;
124
+ throw new Error(`Unexpected lifecycle action: ${_exhaustive}`);
125
+ }
124
126
  }
125
127
  if (lcAction.settings) {
126
128
  if (lcAction.settings.visibility) {
@@ -188,7 +190,7 @@ export function formatUnifiedSummaryMarkdown(input) {
188
190
  if (hasLcChange && hasSyncChanges)
189
191
  diffLines.push("");
190
192
  if (syncRepo)
191
- renderSyncLines(syncRepo, diffLines);
193
+ diffLines.push(...renderSyncLines(syncRepo));
192
194
  // Blank line between files and settings sections
193
195
  if (hasSyncChanges && hasSettingsChanges)
194
196
  diffLines.push("");
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { program } from "./cli/program.js";
2
+ import { program, getVersion } from "./cli/program.js";
3
+ program.version(getVersion());
3
4
  program.parse();
@@ -3,8 +3,8 @@
3
3
  * Supports ${VAR}, ${VAR:-default}, and ${VAR:?message} syntax.
4
4
  * Use $${VAR} to escape and output literal ${VAR}.
5
5
  */
6
- import { interpolateString, interpolateValue, } from "./interpolation-engine.js";
7
- import { ValidationError } from "./errors.js";
6
+ import { interpolateString, interpolateValue, } from "../shared/interpolation-engine.js";
7
+ import { ValidationError } from "../shared/errors.js";
8
8
  /**
9
9
  * Regex to match environment variable placeholders.
10
10
  * Captures:
@@ -1,3 +1,4 @@
1
+ import { ValidationError } from "../shared/errors.js";
1
2
  const MAX_EXTENDS_DEPTH = 100;
2
3
  /**
3
4
  * Resolves a single group's extends chain into an ordered list of group names.
@@ -7,16 +8,16 @@ const MAX_EXTENDS_DEPTH = 100;
7
8
  export function resolveExtendsChain(groupName, groupDefs) {
8
9
  function walk(name, visited, depth) {
9
10
  if (depth > MAX_EXTENDS_DEPTH) {
10
- throw new Error(`Extends chain exceeds maximum depth of ${MAX_EXTENDS_DEPTH} — likely misconfigured`);
11
+ throw new ValidationError(`Extends chain exceeds maximum depth of ${MAX_EXTENDS_DEPTH} — likely misconfigured`);
11
12
  }
12
13
  if (visited.has(name)) {
13
14
  const cycle = [...visited, name].join(" -> ");
14
- throw new Error(`Circular extends detected: ${cycle}`);
15
+ throw new ValidationError(`Circular extends detected: ${cycle}`);
15
16
  }
16
17
  visited.add(name);
17
18
  const group = groupDefs[name];
18
19
  if (!group) {
19
- throw new Error(`Group '${name}' referenced in extends chain does not exist`);
20
+ throw new ValidationError(`Group '${name}' referenced in extends chain does not exist`);
20
21
  }
21
22
  if (!group.extends) {
22
23
  return [name];
@@ -44,7 +44,7 @@ export function resolveFileReference(reference, configDir) {
44
44
  }
45
45
  catch (error) {
46
46
  const msg = toErrorMessage(error);
47
- throw new ValidationError(`Failed to load file reference "${reference}": ${msg}`);
47
+ throw new ValidationError(`Failed to load file reference "${reference}": ${msg}`, { cause: error });
48
48
  }
49
49
  // Parse based on extension
50
50
  const ext = extname(relativePath).toLowerCase();
@@ -65,7 +65,9 @@ function parseWithContext(fn, errorPrefix) {
65
65
  return fn();
66
66
  }
67
67
  catch (error) {
68
- throw new ValidationError(`${errorPrefix}: ${toErrorMessage(error)}`);
68
+ throw new ValidationError(`${errorPrefix}: ${toErrorMessage(error)}`, {
69
+ cause: error,
70
+ });
69
71
  }
70
72
  }
71
73
  /**