@aspruyt/xfg 4.0.2 → 4.0.5

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 (151) hide show
  1. package/dist/cli/index.d.ts +1 -1
  2. package/dist/cli/index.js +0 -6
  3. package/dist/cli/program.js +3 -2
  4. package/dist/cli/settings-report-builder.js +4 -4
  5. package/dist/cli/sync-command.js +72 -36
  6. package/dist/cli/sync-report-builder.d.ts +2 -6
  7. package/dist/cli/types.d.ts +2 -14
  8. package/dist/cli/types.js +1 -9
  9. package/dist/config/file-reference-resolver.js +13 -23
  10. package/dist/config/formatter.d.ts +0 -6
  11. package/dist/config/formatter.js +0 -9
  12. package/dist/config/index.d.ts +1 -2
  13. package/dist/config/index.js +0 -2
  14. package/dist/config/loader.d.ts +1 -1
  15. package/dist/config/loader.js +3 -3
  16. package/dist/config/normalizer.d.ts +1 -1
  17. package/dist/config/normalizer.js +44 -57
  18. package/dist/config/validator.d.ts +1 -1
  19. package/dist/config/validator.js +120 -121
  20. package/dist/config/validators/file-validator.d.ts +2 -4
  21. package/dist/config/validators/file-validator.js +3 -7
  22. package/dist/config/validators/repo-settings-validator.js +1 -1
  23. package/dist/config/validators/ruleset-validator.js +28 -12
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.js +0 -1
  26. package/dist/lifecycle/ado-migration-source.d.ts +2 -1
  27. package/dist/lifecycle/ado-migration-source.js +7 -5
  28. package/dist/lifecycle/github-lifecycle-provider.d.ts +6 -3
  29. package/dist/lifecycle/github-lifecycle-provider.js +29 -19
  30. package/dist/lifecycle/lifecycle-formatter.js +2 -1
  31. package/dist/lifecycle/lifecycle-helpers.d.ts +5 -1
  32. package/dist/lifecycle/lifecycle-helpers.js +4 -4
  33. package/dist/lifecycle/repo-lifecycle-factory.d.ts +4 -4
  34. package/dist/lifecycle/repo-lifecycle-factory.js +12 -9
  35. package/dist/lifecycle/repo-lifecycle-manager.d.ts +4 -1
  36. package/dist/lifecycle/repo-lifecycle-manager.js +11 -7
  37. package/dist/lifecycle/types.d.ts +0 -15
  38. package/dist/output/github-summary.d.ts +6 -5
  39. package/dist/output/github-summary.js +36 -52
  40. package/dist/output/index.d.ts +2 -2
  41. package/dist/output/index.js +1 -1
  42. package/dist/output/lifecycle-report.d.ts +2 -12
  43. package/dist/output/lifecycle-report.js +18 -35
  44. package/dist/output/settings-report.d.ts +4 -4
  45. package/dist/output/settings-report.js +6 -6
  46. package/dist/output/sync-report.d.ts +4 -6
  47. package/dist/output/sync-report.js +2 -2
  48. package/dist/output/unified-summary.d.ts +1 -0
  49. package/dist/output/unified-summary.js +8 -8
  50. package/dist/settings/base-processor.d.ts +1 -1
  51. package/dist/settings/base-processor.js +1 -1
  52. package/dist/settings/index.d.ts +3 -3
  53. package/dist/settings/index.js +3 -3
  54. package/dist/settings/labels/diff.js +3 -2
  55. package/dist/settings/labels/formatter.js +3 -3
  56. package/dist/settings/labels/github-labels-strategy.d.ts +2 -23
  57. package/dist/settings/labels/github-labels-strategy.js +8 -28
  58. package/dist/settings/labels/index.d.ts +1 -0
  59. package/dist/settings/labels/index.js +2 -0
  60. package/dist/settings/labels/processor.d.ts +2 -2
  61. package/dist/settings/labels/processor.js +3 -4
  62. package/dist/settings/labels/types.d.ts +0 -3
  63. package/dist/settings/repo-settings/diff.d.ts +1 -1
  64. package/dist/settings/repo-settings/diff.js +2 -2
  65. package/dist/settings/repo-settings/formatter.d.ts +1 -1
  66. package/dist/settings/repo-settings/formatter.js +4 -4
  67. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -7
  68. package/dist/settings/repo-settings/github-repo-settings-strategy.js +9 -17
  69. package/dist/settings/repo-settings/index.d.ts +1 -0
  70. package/dist/settings/repo-settings/index.js +2 -0
  71. package/dist/settings/repo-settings/processor.d.ts +2 -2
  72. package/dist/settings/repo-settings/processor.js +5 -6
  73. package/dist/settings/repo-settings/types.d.ts +9 -13
  74. package/dist/settings/repo-settings/types.js +1 -14
  75. package/dist/settings/rulesets/diff-algorithm.d.ts +0 -1
  76. package/dist/settings/rulesets/diff-algorithm.js +6 -8
  77. package/dist/settings/rulesets/formatter.js +15 -51
  78. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +2 -20
  79. package/dist/settings/rulesets/github-ruleset-strategy.js +6 -30
  80. package/dist/settings/rulesets/index.d.ts +2 -1
  81. package/dist/settings/rulesets/index.js +3 -1
  82. package/dist/settings/rulesets/processor.d.ts +2 -2
  83. package/dist/settings/rulesets/processor.js +3 -4
  84. package/dist/{vcs → shared}/branch-utils.js +5 -4
  85. package/dist/shared/command-executor.d.ts +2 -1
  86. package/dist/shared/command-executor.js +9 -5
  87. package/dist/shared/env.d.ts +6 -6
  88. package/dist/shared/env.js +10 -17
  89. package/dist/shared/errors.d.ts +26 -0
  90. package/dist/shared/errors.js +34 -0
  91. package/dist/shared/gh-api-utils.d.ts +21 -14
  92. package/dist/shared/gh-api-utils.js +33 -22
  93. package/dist/shared/index.d.ts +9 -2
  94. package/dist/shared/index.js +16 -2
  95. package/dist/shared/logger.d.ts +24 -1
  96. package/dist/shared/logger.js +8 -3
  97. package/dist/shared/repo-detector.js +9 -11
  98. package/dist/shared/retry-utils.d.ts +5 -7
  99. package/dist/shared/retry-utils.js +3 -10
  100. package/dist/shared/shell-utils.d.ts +0 -3
  101. package/dist/shared/shell-utils.js +2 -4
  102. package/dist/shared/type-guards.d.ts +2 -9
  103. package/dist/shared/type-guards.js +0 -6
  104. package/dist/shared/xfg-template.d.ts +2 -2
  105. package/dist/shared/xfg-template.js +2 -1
  106. package/dist/sync/auth-options-builder.d.ts +3 -2
  107. package/dist/sync/auth-options-builder.js +14 -10
  108. package/dist/sync/branch-manager.d.ts +12 -7
  109. package/dist/sync/branch-manager.js +4 -7
  110. package/dist/sync/commit-message.d.ts +1 -1
  111. package/dist/sync/commit-push-manager.d.ts +8 -2
  112. package/dist/sync/commit-push-manager.js +6 -5
  113. package/dist/sync/file-sync-orchestrator.js +17 -21
  114. package/dist/sync/file-writer.js +3 -5
  115. package/dist/sync/index.d.ts +1 -1
  116. package/dist/sync/manifest-manager.d.ts +1 -0
  117. package/dist/sync/manifest.d.ts +4 -7
  118. package/dist/sync/manifest.js +42 -45
  119. package/dist/sync/repository-processor.d.ts +5 -2
  120. package/dist/sync/repository-processor.js +11 -17
  121. package/dist/sync/repository-session.js +2 -1
  122. package/dist/sync/sync-workflow.d.ts +2 -2
  123. package/dist/sync/sync-workflow.js +16 -23
  124. package/dist/sync/types.d.ts +20 -25
  125. package/dist/vcs/authenticated-git-ops.d.ts +3 -4
  126. package/dist/vcs/authenticated-git-ops.js +5 -1
  127. package/dist/vcs/azure-pr-strategy.d.ts +6 -1
  128. package/dist/vcs/azure-pr-strategy.js +38 -31
  129. package/dist/vcs/commit-strategy-selector.d.ts +10 -19
  130. package/dist/vcs/commit-strategy-selector.js +8 -24
  131. package/dist/vcs/git-commit-strategy.d.ts +1 -1
  132. package/dist/vcs/git-commit-strategy.js +1 -3
  133. package/dist/vcs/git-ops.d.ts +4 -8
  134. package/dist/vcs/git-ops.js +18 -22
  135. package/dist/vcs/github-app-token-manager.js +9 -8
  136. package/dist/vcs/github-pr-strategy.js +18 -11
  137. package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
  138. package/dist/vcs/gitlab-pr-strategy.js +14 -7
  139. package/dist/vcs/graphql-commit-strategy.d.ts +1 -7
  140. package/dist/vcs/graphql-commit-strategy.js +24 -32
  141. package/dist/vcs/index.d.ts +2 -1
  142. package/dist/vcs/pr-creator.d.ts +6 -9
  143. package/dist/vcs/pr-strategy-factory.d.ts +1 -1
  144. package/dist/vcs/pr-strategy-factory.js +2 -1
  145. package/dist/vcs/pr-strategy.d.ts +1 -1
  146. package/dist/vcs/pr-strategy.js +2 -3
  147. package/dist/vcs/types.d.ts +6 -10
  148. package/package.json +2 -2
  149. package/dist/config/errors.d.ts +0 -9
  150. package/dist/config/errors.js +0 -11
  151. /package/dist/{vcs → shared}/branch-utils.d.ts +0 -0
@@ -1,3 +1,3 @@
1
1
  export { runSync } from "./sync-command.js";
2
- export { type IRepositoryProcessor, type ProcessorFactory, type IRulesetProcessor, type RulesetProcessorFactory, type RepoSettingsProcessorFactory, type ILabelsProcessor, type LabelsProcessorFactory, defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, defaultLabelsProcessorFactory, } from "./types.js";
2
+ export { type ProcessorFactory, type RulesetProcessorFactory, type RepoSettingsProcessorFactory, type LabelsProcessorFactory, } from "./types.js";
3
3
  export type { SyncOptions, SharedOptions } from "./sync-command.js";
package/dist/cli/index.js CHANGED
@@ -1,7 +1 @@
1
- // CLI command implementations
2
1
  export { runSync } from "./sync-command.js";
3
- // Export types - using 'export type' for type aliases, but interfaces need special handling
4
- // For ESM compatibility, re-export everything from types.js
5
- export {
6
- // Runtime values
7
- defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, defaultLabelsProcessorFactory, } from "./types.js";
@@ -2,6 +2,7 @@ import { program, Command } from "commander";
2
2
  import { dirname, join } from "node:path";
3
3
  import { readFileSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { ValidationError } from "../shared/errors.js";
5
6
  import { runSync } from "./sync-command.js";
6
7
  const __filename = fileURLToPath(import.meta.url);
7
8
  const __dirname = dirname(__filename);
@@ -40,14 +41,14 @@ const syncCommand = new Command("sync")
40
41
  .option("-m, --merge <mode>", "PR merge mode: manual, auto (default, merge when checks pass), force (bypass requirements), direct (push to default branch, no PR)", (value) => {
41
42
  const valid = ["manual", "auto", "force", "direct"];
42
43
  if (!valid.includes(value)) {
43
- throw new Error(`Invalid merge mode: ${value}. Valid: ${valid.join(", ")}`);
44
+ throw new ValidationError(`Invalid merge mode: ${value}. Valid: ${valid.join(", ")}`);
44
45
  }
45
46
  return value;
46
47
  })
47
48
  .option("--merge-strategy <strategy>", "Merge strategy: merge, squash (default), rebase", (value) => {
48
49
  const valid = ["merge", "squash", "rebase"];
49
50
  if (!valid.includes(value)) {
50
- throw new Error(`Invalid merge strategy: ${value}. Valid: ${valid.join(", ")}`);
51
+ throw new ValidationError(`Invalid merge strategy: ${value}. Valid: ${valid.join(", ")}`);
51
52
  }
52
53
  return value;
53
54
  })
@@ -1,7 +1,7 @@
1
1
  export function buildSettingsReport(results) {
2
2
  const repos = [];
3
3
  const totals = {
4
- settings: { add: 0, change: 0 },
4
+ settings: { create: 0, update: 0 },
5
5
  rulesets: { create: 0, update: 0, delete: 0 },
6
6
  labels: { create: 0, update: 0, delete: 0 },
7
7
  };
@@ -26,11 +26,11 @@ export function buildSettingsReport(results) {
26
26
  newValue: entry.newValue,
27
27
  };
28
28
  repoChanges.settings.push(settingChange);
29
- if (entry.action === "add") {
30
- totals.settings.add++;
29
+ if (entry.action === "create") {
30
+ totals.settings.create++;
31
31
  }
32
32
  else {
33
- totals.settings.change++;
33
+ totals.settings.update++;
34
34
  }
35
35
  }
36
36
  }
@@ -1,13 +1,22 @@
1
1
  import { resolve, join } from "node:path";
2
2
  import { existsSync } from "node:fs";
3
3
  import { loadRawConfig, normalizeConfig } from "../config/index.js";
4
+ import { ValidationError, SyncError } from "../shared/errors.js";
4
5
  import { validateForSync } from "../config/validator.js";
5
6
  import { parseGitUrl, getRepoDisplayName, isGitHubRepo, } from "../shared/repo-detector.js";
6
- import { sanitizeBranchName, validateBranchName } from "../vcs/branch-utils.js";
7
+ import { sanitizeBranchName, validateBranchName, } from "../shared/branch-utils.js";
7
8
  import { createTokenManager } from "../vcs/index.js";
8
- import { logger } from "../shared/logger.js";
9
+ import { RepositoryProcessor } from "../sync/index.js";
10
+ import { RulesetProcessor, RepoSettingsProcessor, LabelsProcessor, GitHubRulesetStrategy, GitHubRepoSettingsStrategy, GitHubLabelsStrategy, } from "../settings/index.js";
11
+ import { ShellCommandExecutor } from "../shared/command-executor.js";
12
+ import { Logger } from "../shared/logger.js";
9
13
  import { generateWorkspaceName } from "../shared/workspace-utils.js";
10
- import { defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, defaultLabelsProcessorFactory, } from "./types.js";
14
+ const defaultExecutor = new ShellCommandExecutor(process.env);
15
+ const logger = new Logger(!!(process.env.DEBUG || process.env.XFG_DEBUG));
16
+ const cwd = process.cwd();
17
+ const defaultRulesetProcessorFactory = () => new RulesetProcessor(new GitHubRulesetStrategy(defaultExecutor, { cwd }));
18
+ const defaultRepoSettingsProcessorFactory = () => new RepoSettingsProcessor(new GitHubRepoSettingsStrategy(defaultExecutor, { cwd }));
19
+ const defaultLabelsProcessorFactory = () => new LabelsProcessor(new GitHubLabelsStrategy(defaultExecutor, { cwd }));
11
20
  import { ResultsCollector } from "./results-collector.js";
12
21
  import { buildSettingsReport } from "./settings-report-builder.js";
13
22
  import { formatSettingsReportCLI } from "../output/settings-report.js";
@@ -176,6 +185,7 @@ function displayReports(reportResults, lifecycleReportInputs, settingsCollector,
176
185
  sync: report,
177
186
  settings: settingsReport,
178
187
  dryRun,
188
+ summaryPath: process.env.GITHUB_STEP_SUMMARY,
179
189
  });
180
190
  }
181
191
  /**
@@ -219,11 +229,19 @@ async function processSingleRepo(repoConfig, index, ctx) {
219
229
  const repoToken = isGitHubRepo(repoInfo)
220
230
  ? (await resolveGitHubToken(repoInfo, ctx.tokenManager, repoName, logger, process.env.GH_TOKEN)).token
221
231
  : undefined;
222
- const skipFileSync = await runLifecyclePhase(repoConfig, repoInfo, repoName, index, workDir, repoToken, ctx);
232
+ const repo = {
233
+ repoConfig,
234
+ repoInfo,
235
+ repoName,
236
+ index,
237
+ workDir,
238
+ token: repoToken,
239
+ };
240
+ const skipFileSync = await runLifecyclePhase(repo, ctx);
223
241
  if (skipFileSync)
224
242
  return;
225
243
  // Sync files via processor
226
- await runFileSyncPhase(repoConfig, repoInfo, repoName, current, workDir, repoToken, ctx);
244
+ await runFileSyncPhase(repo, ctx);
227
245
  // Apply settings via API (GitHub-only — ADO and GitLab repos are skipped)
228
246
  await applyRepoSettings({
229
247
  repoConfig,
@@ -242,24 +260,27 @@ async function processSingleRepo(repoConfig, index, ctx) {
242
260
  * Run lifecycle check (repo existence, creation, forking).
243
261
  * Returns true if the main loop should skip file sync for this repo.
244
262
  */
245
- async function runLifecyclePhase(repoConfig, repoInfo, repoName, index, workDir, lifecycleToken, ctx) {
246
- const current = index + 1;
263
+ async function runLifecyclePhase(repo, ctx) {
264
+ const current = repo.index + 1;
247
265
  try {
248
- const { outputLines, lifecycleResult } = await runLifecycleCheck(repoConfig, repoInfo, index, {
266
+ const { outputLines, lifecycleResult } = await runLifecycleCheck(repo.repoConfig, repo.repoInfo, {
249
267
  dryRun: ctx.options.dryRun ?? false,
250
- resolvedWorkDir: workDir,
268
+ resolvedWorkDir: repo.workDir,
251
269
  githubHosts: ctx.config.githubHosts,
252
- token: lifecycleToken,
253
- }, ctx.lifecycleManager, ctx.config.settings?.repo);
270
+ token: repo.token,
271
+ repoIndex: repo.index,
272
+ lifecycleManager: ctx.lifecycleManager,
273
+ repoSettings: ctx.config.settings?.repo,
274
+ });
254
275
  for (const line of outputLines) {
255
276
  logger.info(line);
256
277
  }
257
278
  const createSettings = toCreateRepoSettings(ctx.config.settings?.repo);
258
279
  ctx.lifecycleReportInputs.push({
259
- repoName,
280
+ repoName: repo.repoName,
260
281
  action: lifecycleResult.action,
261
- upstream: repoConfig.upstream,
262
- source: repoConfig.source,
282
+ upstream: repo.repoConfig.upstream,
283
+ source: repo.repoConfig.source,
263
284
  settings: createSettings
264
285
  ? {
265
286
  visibility: createSettings.visibility,
@@ -270,7 +291,7 @@ async function runLifecyclePhase(repoConfig, repoInfo, repoName, index, workDir,
270
291
  // In dry-run, skip processing repos that don't exist yet
271
292
  if (ctx.options.dryRun && lifecycleResult.action !== "existed") {
272
293
  ctx.reportResults.push({
273
- repoName,
294
+ repoName: repo.repoName,
274
295
  success: true,
275
296
  fileChanges: [],
276
297
  });
@@ -279,9 +300,9 @@ async function runLifecyclePhase(repoConfig, repoInfo, repoName, index, workDir,
279
300
  return false;
280
301
  }
281
302
  catch (error) {
282
- logger.error(current, repoName, `Lifecycle error: ${toErrorMessage(error)}`);
303
+ logger.error(current, repo.repoName, `Lifecycle error: ${toErrorMessage(error)}`);
283
304
  ctx.reportResults.push({
284
- repoName,
305
+ repoName: repo.repoName,
285
306
  success: false,
286
307
  fileChanges: [],
287
308
  error: toErrorMessage(error),
@@ -292,23 +313,25 @@ async function runLifecyclePhase(repoConfig, repoInfo, repoName, index, workDir,
292
313
  /**
293
314
  * Run the file sync processor for a single repo and collect results.
294
315
  */
295
- async function runFileSyncPhase(repoConfig, repoInfo, repoName, current, workDir, token, ctx) {
316
+ async function runFileSyncPhase(repo, ctx) {
317
+ const current = repo.index + 1;
296
318
  try {
297
- logger.progress(current, repoName, "Processing...");
298
- const result = await ctx.processor.process(repoConfig, repoInfo, {
319
+ logger.progress(current, repo.repoName, "Processing...");
320
+ const result = await ctx.processor.process(repo.repoConfig, repo.repoInfo, {
299
321
  branchName: ctx.branchName,
300
- workDir,
322
+ workDir: repo.workDir,
301
323
  configId: ctx.config.id,
302
324
  dryRun: ctx.options.dryRun,
303
325
  retries: ctx.options.retries,
326
+ executor: defaultExecutor,
304
327
  prTemplate: ctx.config.prTemplate,
305
328
  noDelete: ctx.options.noDelete,
306
- token,
307
- isGraphQLCommitMode: isGitHubRepo(repoInfo) && ctx.tokenManager !== null,
329
+ token: repo.token,
330
+ hasAppCredentials: isGitHubRepo(repo.repoInfo) && ctx.tokenManager !== null,
308
331
  });
309
332
  const mergeOutcome = determineMergeOutcome(result);
310
333
  ctx.reportResults.push({
311
- repoName,
334
+ repoName: repo.repoName,
312
335
  success: result.success,
313
336
  fileChanges: (result.fileChanges ?? []).map((f) => ({
314
337
  path: f.path,
@@ -319,19 +342,19 @@ async function runFileSyncPhase(repoConfig, repoInfo, repoName, current, workDir
319
342
  error: result.success ? undefined : result.message,
320
343
  });
321
344
  if (result.skipped) {
322
- logger.skip(current, repoName, result.message);
345
+ logger.skip(current, repo.repoName, result.message);
323
346
  }
324
347
  else if (result.success) {
325
- logger.success(current, repoName, result.message);
348
+ logger.success(current, repo.repoName, result.message);
326
349
  }
327
350
  else {
328
- logger.error(current, repoName, result.message);
351
+ logger.error(current, repo.repoName, result.message);
329
352
  }
330
353
  }
331
354
  catch (error) {
332
- logger.error(current, repoName, toErrorMessage(error));
355
+ logger.error(current, repo.repoName, toErrorMessage(error));
333
356
  ctx.reportResults.push({
334
- repoName,
357
+ repoName: repo.repoName,
335
358
  success: false,
336
359
  fileChanges: [],
337
360
  error: toErrorMessage(error),
@@ -339,10 +362,10 @@ async function runFileSyncPhase(repoConfig, repoInfo, repoName, current, workDir
339
362
  }
340
363
  }
341
364
  export async function runSync(options, deps = {}) {
342
- const { processorFactory = defaultProcessorFactory, lifecycleManager, rulesetProcessorFactory = defaultRulesetProcessorFactory, repoSettingsProcessorFactory = defaultRepoSettingsProcessorFactory, labelsProcessorFactory = defaultLabelsProcessorFactory, } = deps;
365
+ const { lifecycleManager, rulesetProcessorFactory = defaultRulesetProcessorFactory, repoSettingsProcessorFactory = defaultRepoSettingsProcessorFactory, labelsProcessorFactory = defaultLabelsProcessorFactory, } = deps;
343
366
  const configPath = resolve(options.config);
344
367
  if (!existsSync(configPath)) {
345
- throw new Error(`Config file not found: ${configPath}`);
368
+ throw new ValidationError(`Config file not found: ${configPath}`);
346
369
  }
347
370
  logger.log(`Loading config from: ${configPath}`);
348
371
  if (options.dryRun) {
@@ -350,7 +373,7 @@ export async function runSync(options, deps = {}) {
350
373
  }
351
374
  const rawConfig = loadRawConfig(configPath);
352
375
  validateForSync(rawConfig);
353
- const config = normalizeConfig(rawConfig);
376
+ const config = normalizeConfig(rawConfig, process.env);
354
377
  const fileNames = getUniqueFileNames(config);
355
378
  let branchName;
356
379
  if (options.branch) {
@@ -364,13 +387,26 @@ export async function runSync(options, deps = {}) {
364
387
  logger.log(`Found ${config.repos.length} repositories to process`);
365
388
  logger.log(`Target files: ${formatFileNames(fileNames)}`);
366
389
  logger.log(`Branch: ${branchName}\n`);
390
+ const tokenManager = createTokenManager(process.env.XFG_GITHUB_APP_ID && process.env.XFG_GITHUB_APP_PRIVATE_KEY
391
+ ? {
392
+ appId: process.env.XFG_GITHUB_APP_ID,
393
+ privateKey: process.env.XFG_GITHUB_APP_PRIVATE_KEY,
394
+ }
395
+ : undefined);
396
+ const processor = deps.processorFactory
397
+ ? deps.processorFactory()
398
+ : new RepositoryProcessor(undefined, logger, {
399
+ tokenManager,
400
+ envToken: process.env.GH_TOKEN,
401
+ });
367
402
  const ctx = {
368
403
  config,
369
404
  options,
370
405
  branchName,
371
- processor: processorFactory(),
372
- lifecycleManager: lifecycleManager ?? new RepoLifecycleManager(undefined, options.retries),
373
- tokenManager: createTokenManager(),
406
+ processor,
407
+ lifecycleManager: lifecycleManager ??
408
+ new RepoLifecycleManager(undefined, defaultExecutor, options.retries, cwd, logger),
409
+ tokenManager,
374
410
  reportResults: [],
375
411
  lifecycleReportInputs: [],
376
412
  settingsCollector: new ResultsCollector(),
@@ -387,6 +423,6 @@ export async function runSync(options, deps = {}) {
387
423
  const hasErrors = ctx.reportResults.some((r) => r.error);
388
424
  const hasSettingsErrors = settingsResults.some((r) => r.error);
389
425
  if (hasErrors || hasSettingsErrors) {
390
- throw new Error("One or more repositories had errors during sync");
426
+ throw new SyncError("One or more repositories had errors during sync");
391
427
  }
392
428
  }
@@ -1,12 +1,8 @@
1
- import type { SyncReport } from "../output/sync-report.js";
2
- interface FileChangeInput {
3
- path: string;
4
- action: "create" | "update" | "delete";
5
- }
1
+ import type { SyncReport, ReportFileChange } from "../output/sync-report.js";
6
2
  interface SyncResultInput {
7
3
  repoName: string;
8
4
  success: boolean;
9
- fileChanges: FileChangeInput[];
5
+ fileChanges: ReportFileChange[];
10
6
  prUrl?: string;
11
7
  mergeOutcome?: "manual" | "auto" | "force" | "direct";
12
8
  error?: string;
@@ -1,25 +1,14 @@
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, type IRulesetProcessor, type IRepoSettingsProcessor, type ILabelsProcessor } from "../settings/index.js";
3
+ import type { IRepositoryProcessor } from "../sync/index.js";
4
+ import type { ISettingsProcessor, IRulesetProcessor, IRepoSettingsProcessor, ILabelsProcessor } from "../settings/index.js";
5
5
  import type { RepoInfo } from "../shared/repo-detector.js";
6
6
  import type { ResultsCollector } from "./results-collector.js";
7
- export type { IRepositoryProcessor, IRulesetProcessor };
8
7
  export type ProcessorFactory = () => IRepositoryProcessor;
9
- /**
10
- * Default factory that creates a real RepositoryProcessor.
11
- */
12
- export declare const defaultProcessorFactory: ProcessorFactory;
13
- /**
14
- * Generic factory type for settings processors.
15
- */
16
8
  export type SettingsProcessorFactory<T extends ISettingsProcessor> = () => T;
17
9
  export type RulesetProcessorFactory = SettingsProcessorFactory<IRulesetProcessor>;
18
10
  export type RepoSettingsProcessorFactory = SettingsProcessorFactory<IRepoSettingsProcessor>;
19
11
  export type LabelsProcessorFactory = SettingsProcessorFactory<ILabelsProcessor>;
20
- export declare const defaultRulesetProcessorFactory: RulesetProcessorFactory;
21
- export declare const defaultRepoSettingsProcessorFactory: RepoSettingsProcessorFactory;
22
- export declare const defaultLabelsProcessorFactory: LabelsProcessorFactory;
23
12
  /**
24
13
  * Dependencies for the sync command (dependency injection).
25
14
  */
@@ -79,4 +68,3 @@ export interface ApplyRepoSettingsContext {
79
68
  repoSettingsProcessorFactory: NonNullable<SyncDependencies["repoSettingsProcessorFactory"]>;
80
69
  labelsProcessorFactory: NonNullable<SyncDependencies["labelsProcessorFactory"]>;
81
70
  }
82
- export type { IRepoSettingsProcessor, ILabelsProcessor };
package/dist/cli/types.js CHANGED
@@ -1,9 +1 @@
1
- import { RepositoryProcessor, } from "../sync/index.js";
2
- import { RulesetProcessor, RepoSettingsProcessor, LabelsProcessor, } from "../settings/index.js";
3
- /**
4
- * Default factory that creates a real RepositoryProcessor.
5
- */
6
- export const defaultProcessorFactory = () => new RepositoryProcessor();
7
- export const defaultRulesetProcessorFactory = () => new RulesetProcessor();
8
- export const defaultRepoSettingsProcessorFactory = () => new RepoSettingsProcessor();
9
- export const defaultLabelsProcessorFactory = () => new LabelsProcessor();
1
+ export {};
@@ -3,7 +3,7 @@ import { resolve, isAbsolute, normalize, extname, relative } from "node:path";
3
3
  import JSON5 from "json5";
4
4
  import { parse as parseYaml } from "yaml";
5
5
  import { toErrorMessage } from "../shared/type-guards.js";
6
- import { ValidationError } from "./errors.js";
6
+ import { ValidationError } from "../shared/errors.js";
7
7
  /**
8
8
  * Check if a value is a file reference (string starting with @)
9
9
  */
@@ -44,40 +44,30 @@ export function resolveFileReference(reference, configDir) {
44
44
  }
45
45
  catch (error) {
46
46
  const msg = toErrorMessage(error);
47
- throw new Error(`Failed to load file reference "${reference}": ${msg}`);
47
+ throw new ValidationError(`Failed to load file reference "${reference}": ${msg}`);
48
48
  }
49
49
  // Parse based on extension
50
50
  const ext = extname(relativePath).toLowerCase();
51
51
  if (ext === ".json") {
52
- try {
53
- return JSON.parse(content);
54
- }
55
- catch (error) {
56
- const msg = toErrorMessage(error);
57
- throw new Error(`Invalid JSON in "${reference}": ${msg}`);
58
- }
52
+ return parseWithContext(() => JSON.parse(content), `Invalid JSON in "${reference}"`);
59
53
  }
60
54
  if (ext === ".json5") {
61
- try {
62
- return JSON5.parse(content);
63
- }
64
- catch (error) {
65
- const msg = toErrorMessage(error);
66
- throw new Error(`Invalid JSON5 in "${reference}": ${msg}`);
67
- }
55
+ return parseWithContext(() => JSON5.parse(content), `Invalid JSON5 in "${reference}"`);
68
56
  }
69
57
  if (ext === ".yaml" || ext === ".yml") {
70
- try {
71
- return parseYaml(content);
72
- }
73
- catch (error) {
74
- const msg = toErrorMessage(error);
75
- throw new Error(`Invalid YAML in "${reference}": ${msg}`);
76
- }
58
+ return parseWithContext(() => parseYaml(content), `Invalid YAML in "${reference}"`);
77
59
  }
78
60
  // Text file - return as string
79
61
  return content;
80
62
  }
63
+ function parseWithContext(fn, errorPrefix) {
64
+ try {
65
+ return fn();
66
+ }
67
+ catch (error) {
68
+ throw new ValidationError(`${errorPrefix}: ${toErrorMessage(error)}`);
69
+ }
70
+ }
81
71
  /**
82
72
  * Recursively resolve file references in a content value.
83
73
  * Only string values starting with @ are resolved.
@@ -1,14 +1,8 @@
1
1
  type OutputFormat = "json" | "json5" | "yaml";
2
- /**
3
- * Options for content conversion.
4
- */
5
2
  interface ConvertOptions {
6
3
  header?: string[];
7
4
  schemaUrl?: string;
8
5
  }
9
- /**
10
- * Detects output format from file extension.
11
- */
12
6
  export declare function detectOutputFormat(fileName: string): OutputFormat;
13
7
  /**
14
8
  * Converts content to string in the appropriate format.
@@ -1,7 +1,4 @@
1
1
  import { Document, stringify } from "yaml";
2
- /**
3
- * Detects output format from file extension.
4
- */
5
2
  export function detectOutputFormat(fileName) {
6
3
  const ext = fileName.toLowerCase().split(".").pop();
7
4
  if (ext === "yaml" || ext === "yml") {
@@ -54,7 +51,6 @@ function buildCommentOnlyYaml(header, schemaUrl) {
54
51
  * Handles null content (empty files), text content (string/string[]), and object content (JSON/YAML).
55
52
  */
56
53
  export function convertContentToString(content, fileName, options) {
57
- // Handle empty file case
58
54
  if (content === null) {
59
55
  const format = detectOutputFormat(fileName);
60
56
  if (format === "yaml" && options) {
@@ -65,18 +61,13 @@ export function convertContentToString(content, fileName, options) {
65
61
  }
66
62
  return "";
67
63
  }
68
- // Handle string content (text file)
69
64
  if (typeof content === "string") {
70
- // Ensure trailing newline for text files
71
65
  return content.endsWith("\n") ? content : content + "\n";
72
66
  }
73
- // Handle string[] content (text file with lines)
74
67
  if (Array.isArray(content)) {
75
- // Join lines with newlines and ensure trailing newline
76
68
  const text = content.join("\n");
77
69
  return text.length > 0 ? text + "\n" : "";
78
70
  }
79
- // Handle object content (JSON/YAML)
80
71
  const format = detectOutputFormat(fileName);
81
72
  if (format === "yaml") {
82
73
  // Use Document API for YAML to support comments
@@ -1,5 +1,4 @@
1
- export type { MergeMode, MergeStrategy, BypassActor, StatusCheckConfig, CodeScanningTool, PullRequestRuleParameters, RulesetRule, Ruleset, GitHubRepoSettings, 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, ContentValue, RawFileConfig, RawRepoFileOverride, RawRepoSettings, RawRepoConfig, RawConfig, RepoConfig, Config, } 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 { ValidationError } from "./errors.js";
@@ -4,5 +4,3 @@ 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
- // Errors
8
- export { ValidationError } from "./errors.js";
@@ -6,4 +6,4 @@ export { normalizeConfigInternal as normalizeConfig };
6
6
  * Use this when you need to perform command-specific validation before normalizing.
7
7
  */
8
8
  export declare function loadRawConfig(filePath: string): RawConfig;
9
- export declare function loadConfig(filePath: string): Config;
9
+ export declare function loadConfig(filePath: string, env: Record<string, string | undefined>): Config;
@@ -5,7 +5,7 @@ import { validateRawConfig } from "./validator.js";
5
5
  import { normalizeConfig as normalizeConfigInternal } from "./normalizer.js";
6
6
  import { resolveFileReferencesInConfig } from "./file-reference-resolver.js";
7
7
  import { toErrorMessage } from "../shared/type-guards.js";
8
- import { ValidationError } from "./errors.js";
8
+ import { ValidationError } from "../shared/errors.js";
9
9
  export { normalizeConfigInternal as normalizeConfig };
10
10
  /**
11
11
  * Load and validate raw config without normalization.
@@ -27,7 +27,7 @@ export function loadRawConfig(filePath) {
27
27
  validateRawConfig(rawConfig);
28
28
  return rawConfig;
29
29
  }
30
- export function loadConfig(filePath) {
30
+ export function loadConfig(filePath, env) {
31
31
  const rawConfig = loadRawConfig(filePath);
32
- return normalizeConfigInternal(rawConfig);
32
+ return normalizeConfigInternal(rawConfig, env);
33
33
  }
@@ -8,4 +8,4 @@ export declare function mergeSettings(root: RawRootSettings | undefined, perRepo
8
8
  * Normalizes raw config into expanded, merged config.
9
9
  * Pipeline: expand git arrays -> merge content -> interpolate env vars
10
10
  */
11
- export declare function normalizeConfig(raw: RawConfig): Config;
11
+ export declare function normalizeConfig(raw: RawConfig, env: Record<string, string | undefined>): Config;
@@ -104,17 +104,12 @@ function mergeRuleset(root, perRepo) {
104
104
  return structuredClone(perRepo ?? {});
105
105
  if (!perRepo)
106
106
  return structuredClone(root);
107
- // Deep merge using the existing merge utility with replace strategy
107
+ // Deep merge using the existing merge utility with replace strategy.
108
+ // deepMerge operates on Record<string, unknown> — the cast is safe because
109
+ // merging two Ruleset-shaped objects preserves the Ruleset structure.
108
110
  const ctx = createMergeContext("replace");
109
- const merged = deepMerge(structuredClone(root), perRepo, ctx);
110
- return merged;
111
+ return deepMerge(structuredClone(root), perRepo, ctx);
111
112
  }
112
- /**
113
- * Merges root and per-repo label configs.
114
- * Per-repo labels override root labels by name.
115
- * inherit: false skips all root labels.
116
- * label: false opts out of a specific root label.
117
- */
118
113
  function mergeLabels(rootLabels, repoLabels) {
119
114
  if (!rootLabels && !repoLabels)
120
115
  return undefined;
@@ -135,12 +130,12 @@ function mergeLabels(rootLabels, repoLabels) {
135
130
  continue;
136
131
  if (!inheritLabels && !repoLabel && rootLabel)
137
132
  continue;
138
- const merged = {
139
- ...(rootLabel && rootLabel !== false ? rootLabel : {}),
140
- ...(repoLabel && repoLabel !== false ? repoLabel : {}),
141
- };
142
- // Strip # from color and lowercase
143
- merged.color = merged.color.replace(/^#/, "").toLowerCase();
133
+ const base = rootLabel && typeof rootLabel === "object" ? rootLabel : {};
134
+ const overlay = repoLabel && typeof repoLabel === "object" ? repoLabel : {};
135
+ const color = (overlay.color ?? base.color ?? "")
136
+ .replace(/^#/, "")
137
+ .toLowerCase();
138
+ const merged = { ...base, ...overlay, color };
144
139
  result[name] = merged;
145
140
  }
146
141
  return Object.keys(result).length > 0 ? result : undefined;
@@ -363,11 +358,41 @@ function mergeGroupSettings(rootSettings, groupNames, groupDefs) {
363
358
  }
364
359
  return accumulated;
365
360
  }
361
+ /**
362
+ * Resolves a single file entry by merging root config with repo overrides.
363
+ * Returns null if the file should be skipped.
364
+ */
365
+ function resolveFileEntry(fileName, fileConfig, repoOverride, inheritFiles, globalDeleteOrphaned, env) {
366
+ if (repoOverride === false)
367
+ return null;
368
+ if (!inheritFiles && !repoOverride)
369
+ return null;
370
+ const fileStrategy = fileConfig.mergeStrategy ?? "replace";
371
+ let mergedContent = resolveFileContent(fileConfig.content, repoOverride, fileStrategy);
372
+ if (mergedContent !== null) {
373
+ mergedContent = interpolateContent(mergedContent, { strict: true, env });
374
+ }
375
+ return {
376
+ fileName,
377
+ content: mergedContent,
378
+ createOnly: repoOverride?.createOnly ?? fileConfig.createOnly,
379
+ executable: repoOverride?.executable ?? fileConfig.executable,
380
+ header: normalizeHeader(repoOverride?.header ?? fileConfig.header),
381
+ schemaUrl: repoOverride?.schemaUrl ?? fileConfig.schemaUrl,
382
+ template: repoOverride?.template ?? fileConfig.template,
383
+ vars: fileConfig.vars || repoOverride?.vars
384
+ ? { ...fileConfig.vars, ...repoOverride?.vars }
385
+ : undefined,
386
+ deleteOrphaned: repoOverride?.deleteOrphaned ??
387
+ fileConfig.deleteOrphaned ??
388
+ globalDeleteOrphaned,
389
+ };
390
+ }
366
391
  /**
367
392
  * Normalizes raw config into expanded, merged config.
368
393
  * Pipeline: expand git arrays -> merge content -> interpolate env vars
369
394
  */
370
- export function normalizeConfig(raw) {
395
+ export function normalizeConfig(raw, env) {
371
396
  const expandedRepos = [];
372
397
  for (const rawRepo of raw.repos) {
373
398
  const gitUrls = Array.isArray(rawRepo.git) ? rawRepo.git : [rawRepo.git];
@@ -389,47 +414,9 @@ export function normalizeConfig(raw) {
389
414
  // Skip reserved key
390
415
  if (fileName === "inherit")
391
416
  continue;
392
- const repoOverride = rawRepo.files?.[fileName];
393
- // Skip excluded files (set to false)
394
- if (repoOverride === false) {
395
- continue;
396
- }
397
- // Skip if inherit: false and no repo-specific override
398
- if (!inheritFiles && !repoOverride) {
399
- continue;
400
- }
401
- const fileConfig = effectiveRootFiles[fileName];
402
- const fileStrategy = fileConfig.mergeStrategy ?? "replace";
403
- let mergedContent = resolveFileContent(fileConfig.content, repoOverride, fileStrategy);
404
- if (mergedContent !== null) {
405
- mergedContent = interpolateContent(mergedContent, { strict: true });
406
- }
407
- // Resolve fields: per-repo overrides root level
408
- const createOnly = repoOverride?.createOnly ?? fileConfig.createOnly;
409
- const executable = repoOverride?.executable ?? fileConfig.executable;
410
- const header = normalizeHeader(repoOverride?.header ?? fileConfig.header);
411
- const schemaUrl = repoOverride?.schemaUrl ?? fileConfig.schemaUrl;
412
- // Template: per-repo overrides root level
413
- const template = repoOverride?.template ?? fileConfig.template;
414
- // Vars: merge root + per-repo (per-repo takes precedence)
415
- const vars = fileConfig.vars || repoOverride?.vars
416
- ? { ...fileConfig.vars, ...repoOverride?.vars }
417
- : undefined;
418
- // deleteOrphaned: per-repo overrides per-file overrides global
419
- const deleteOrphaned = repoOverride?.deleteOrphaned ??
420
- fileConfig.deleteOrphaned ??
421
- raw.deleteOrphaned;
422
- files.push({
423
- fileName,
424
- content: mergedContent,
425
- createOnly,
426
- executable,
427
- header,
428
- schemaUrl,
429
- template,
430
- vars,
431
- deleteOrphaned,
432
- });
417
+ const entry = resolveFileEntry(fileName, effectiveRootFiles[fileName], rawRepo.files?.[fileName], inheritFiles, raw.deleteOrphaned, env);
418
+ if (entry)
419
+ files.push(entry);
433
420
  }
434
421
  // Merge PR options: per-repo overrides effective (root + groups)
435
422
  const prOptions = mergePROptions(effectivePROptions, rawRepo.prOptions);