@aspruyt/xfg 3.7.6 → 3.8.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 (164) hide show
  1. package/dist/cli/index.d.ts +6 -0
  2. package/dist/cli/index.js +9 -0
  3. package/dist/cli/program.d.ts +2 -0
  4. package/dist/cli/program.js +70 -0
  5. package/dist/cli/settings-command.d.ts +10 -0
  6. package/dist/cli/settings-command.js +237 -0
  7. package/dist/cli/settings-report-builder.d.ts +19 -0
  8. package/dist/cli/settings-report-builder.js +64 -0
  9. package/dist/cli/sync-command.d.ts +25 -0
  10. package/dist/cli/sync-command.js +180 -0
  11. package/dist/cli/sync-report-builder.d.ts +15 -0
  12. package/dist/cli/sync-report-builder.js +29 -0
  13. package/dist/cli/types.d.ts +45 -0
  14. package/dist/cli/types.js +15 -0
  15. package/dist/cli.js +2 -19
  16. package/dist/{file-reference-resolver.d.ts → config/file-reference-resolver.d.ts} +1 -1
  17. package/dist/config/index.d.ts +7 -0
  18. package/dist/config/index.js +12 -0
  19. package/dist/config/loader.d.ts +9 -0
  20. package/dist/{config.js → config/loader.js} +3 -24
  21. package/dist/{config-normalizer.d.ts → config/normalizer.d.ts} +1 -1
  22. package/dist/{config-normalizer.js → config/normalizer.js} +1 -1
  23. package/dist/{config.d.ts → config/types.d.ts} +5 -9
  24. package/dist/config/types.js +16 -0
  25. package/dist/{config-validator.d.ts → config/validator.d.ts} +5 -5
  26. package/dist/{config-validator.js → config/validator.js} +60 -372
  27. package/dist/config/validators/file-validator.d.ts +22 -0
  28. package/dist/config/validators/file-validator.js +46 -0
  29. package/dist/config/validators/index.d.ts +3 -0
  30. package/dist/config/validators/index.js +6 -0
  31. package/dist/config/validators/repo-settings-validator.d.ts +10 -0
  32. package/dist/config/validators/repo-settings-validator.js +71 -0
  33. package/dist/config/validators/ruleset-validator.d.ts +18 -0
  34. package/dist/config/validators/ruleset-validator.js +201 -0
  35. package/dist/index.d.ts +3 -66
  36. package/dist/index.js +3 -474
  37. package/dist/output/index.d.ts +4 -0
  38. package/dist/output/index.js +8 -0
  39. package/dist/output/settings-report.d.ts +37 -0
  40. package/dist/output/settings-report.js +300 -0
  41. package/dist/{summary-utils.d.ts → output/summary-utils.d.ts} +3 -3
  42. package/dist/output/sync-report.d.ts +24 -0
  43. package/dist/output/sync-report.js +99 -0
  44. package/dist/settings/index.d.ts +2 -0
  45. package/dist/settings/index.js +4 -0
  46. package/dist/{repo-settings-diff.d.ts → settings/repo-settings/diff.d.ts} +2 -2
  47. package/dist/{repo-settings-plan-formatter.d.ts → settings/repo-settings/formatter.d.ts} +3 -1
  48. package/dist/{repo-settings-plan-formatter.js → settings/repo-settings/formatter.js} +11 -2
  49. package/dist/{strategies → settings/repo-settings}/github-repo-settings-strategy.d.ts +4 -4
  50. package/dist/{strategies → settings/repo-settings}/github-repo-settings-strategy.js +3 -3
  51. package/dist/settings/repo-settings/index.d.ts +5 -0
  52. package/dist/settings/repo-settings/index.js +10 -0
  53. package/dist/{repo-settings-processor.d.ts → settings/repo-settings/processor.d.ts} +4 -4
  54. package/dist/{repo-settings-processor.js → settings/repo-settings/processor.js} +14 -8
  55. package/dist/{strategies/repo-settings-strategy.d.ts → settings/repo-settings/types.d.ts} +2 -2
  56. package/dist/settings/rulesets/diff-algorithm.d.ts +18 -0
  57. package/dist/settings/rulesets/diff-algorithm.js +166 -0
  58. package/dist/{ruleset-diff.d.ts → settings/rulesets/diff.d.ts} +2 -2
  59. package/dist/{ruleset-diff.js → settings/rulesets/diff.js} +1 -1
  60. package/dist/{ruleset-plan-formatter.d.ts → settings/rulesets/formatter.d.ts} +7 -12
  61. package/dist/{ruleset-plan-formatter.js → settings/rulesets/formatter.js} +10 -165
  62. package/dist/{strategies → settings/rulesets}/github-ruleset-strategy.d.ts +4 -4
  63. package/dist/{strategies → settings/rulesets}/github-ruleset-strategy.js +3 -3
  64. package/dist/settings/rulesets/index.d.ts +6 -0
  65. package/dist/settings/rulesets/index.js +10 -0
  66. package/dist/{ruleset-processor.d.ts → settings/rulesets/processor.d.ts} +4 -4
  67. package/dist/{ruleset-processor.js → settings/rulesets/processor.js} +6 -6
  68. package/dist/{strategies/ruleset-strategy.d.ts → settings/rulesets/types.d.ts} +2 -2
  69. package/dist/{command-executor.d.ts → shared/command-executor.d.ts} +10 -2
  70. package/dist/{command-executor.js → shared/command-executor.js} +2 -1
  71. package/dist/shared/index.d.ts +8 -0
  72. package/dist/shared/index.js +16 -0
  73. package/dist/{logger.d.ts → shared/logger.d.ts} +1 -1
  74. package/dist/{logger.js → shared/logger.js} +1 -1
  75. package/dist/sync/auth-options-builder.d.ts +12 -0
  76. package/dist/sync/auth-options-builder.js +54 -0
  77. package/dist/sync/branch-manager.d.ts +7 -0
  78. package/dist/sync/branch-manager.js +36 -0
  79. package/dist/sync/commit-message.d.ts +11 -0
  80. package/dist/sync/commit-message.js +27 -0
  81. package/dist/sync/commit-push-manager.d.ts +8 -0
  82. package/dist/sync/commit-push-manager.js +71 -0
  83. package/dist/sync/file-sync-orchestrator.d.ts +11 -0
  84. package/dist/sync/file-sync-orchestrator.js +58 -0
  85. package/dist/sync/file-writer.d.ts +18 -0
  86. package/dist/sync/file-writer.js +101 -0
  87. package/dist/sync/index.d.ts +14 -0
  88. package/dist/sync/index.js +17 -0
  89. package/dist/sync/manifest-manager.d.ts +10 -0
  90. package/dist/sync/manifest-manager.js +64 -0
  91. package/dist/sync/pr-merge-handler.d.ts +11 -0
  92. package/dist/sync/pr-merge-handler.js +63 -0
  93. package/dist/sync/repository-processor.d.ts +30 -0
  94. package/dist/sync/repository-processor.js +298 -0
  95. package/dist/sync/repository-session.d.ts +9 -0
  96. package/dist/sync/repository-session.js +35 -0
  97. package/dist/sync/types.d.ts +304 -0
  98. package/dist/{xfg-template.d.ts → sync/xfg-template.d.ts} +2 -2
  99. package/dist/{authenticated-git-ops.js → vcs/authenticated-git-ops.js} +3 -3
  100. package/dist/{strategies → vcs}/azure-pr-strategy.d.ts +2 -2
  101. package/dist/{strategies → vcs}/azure-pr-strategy.js +5 -5
  102. package/dist/{strategies → vcs}/commit-strategy-selector.d.ts +3 -3
  103. package/dist/{strategies → vcs}/commit-strategy-selector.js +1 -1
  104. package/dist/{strategies → vcs}/git-commit-strategy.d.ts +2 -2
  105. package/dist/{strategies → vcs}/git-commit-strategy.js +3 -3
  106. package/dist/{git-ops.d.ts → vcs/git-ops.d.ts} +1 -1
  107. package/dist/{git-ops.js → vcs/git-ops.js} +4 -4
  108. package/dist/{github-app-token-manager.d.ts → vcs/github-app-token-manager.d.ts} +1 -1
  109. package/dist/{github-app-token-manager.js → vcs/github-app-token-manager.js} +1 -1
  110. package/dist/{strategies → vcs}/github-pr-strategy.d.ts +2 -2
  111. package/dist/{strategies → vcs}/github-pr-strategy.js +30 -33
  112. package/dist/{strategies → vcs}/gitlab-pr-strategy.d.ts +2 -2
  113. package/dist/{strategies → vcs}/gitlab-pr-strategy.js +5 -5
  114. package/dist/{strategies → vcs}/graphql-commit-strategy.d.ts +2 -2
  115. package/dist/{strategies → vcs}/graphql-commit-strategy.js +3 -3
  116. package/dist/vcs/index.d.ts +16 -0
  117. package/dist/{strategies → vcs}/index.js +15 -10
  118. package/dist/{pr-creator.d.ts → vcs/pr-creator.d.ts} +4 -4
  119. package/dist/{pr-creator.js → vcs/pr-creator.js} +3 -3
  120. package/dist/vcs/pr-strategy.d.ts +41 -0
  121. package/dist/{strategies → vcs}/pr-strategy.js +1 -1
  122. package/dist/{strategies/pr-strategy.d.ts → vcs/types.d.ts} +32 -35
  123. package/dist/vcs/types.js +1 -0
  124. package/package.json +2 -2
  125. package/dist/plan-formatter.d.ts +0 -39
  126. package/dist/plan-formatter.js +0 -84
  127. package/dist/plan-summary.d.ts +0 -8
  128. package/dist/plan-summary.js +0 -110
  129. package/dist/repository-processor.d.ts +0 -79
  130. package/dist/repository-processor.js +0 -659
  131. package/dist/resource-converters.d.ts +0 -28
  132. package/dist/resource-converters.js +0 -107
  133. package/dist/strategies/commit-strategy.d.ts +0 -36
  134. package/dist/strategies/index.d.ts +0 -18
  135. /package/dist/{file-reference-resolver.js → config/file-reference-resolver.js} +0 -0
  136. /package/dist/{config-formatter.d.ts → config/formatter.d.ts} +0 -0
  137. /package/dist/{config-formatter.js → config/formatter.js} +0 -0
  138. /package/dist/{merge.d.ts → config/merge.d.ts} +0 -0
  139. /package/dist/{merge.js → config/merge.js} +0 -0
  140. /package/dist/{github-summary.d.ts → output/github-summary.d.ts} +0 -0
  141. /package/dist/{github-summary.js → output/github-summary.js} +0 -0
  142. /package/dist/{summary-utils.js → output/summary-utils.js} +0 -0
  143. /package/dist/{repo-settings-diff.js → settings/repo-settings/diff.js} +0 -0
  144. /package/dist/{strategies/repo-settings-strategy.js → settings/repo-settings/types.js} +0 -0
  145. /package/dist/{strategies/commit-strategy.js → settings/rulesets/types.js} +0 -0
  146. /package/dist/{env.d.ts → shared/env.d.ts} +0 -0
  147. /package/dist/{env.js → shared/env.js} +0 -0
  148. /package/dist/{repo-detector.d.ts → shared/repo-detector.d.ts} +0 -0
  149. /package/dist/{repo-detector.js → shared/repo-detector.js} +0 -0
  150. /package/dist/{retry-utils.d.ts → shared/retry-utils.d.ts} +0 -0
  151. /package/dist/{retry-utils.js → shared/retry-utils.js} +0 -0
  152. /package/dist/{sanitize-utils.d.ts → shared/sanitize-utils.d.ts} +0 -0
  153. /package/dist/{sanitize-utils.js → shared/sanitize-utils.js} +0 -0
  154. /package/dist/{shell-utils.d.ts → shared/shell-utils.d.ts} +0 -0
  155. /package/dist/{shell-utils.js → shared/shell-utils.js} +0 -0
  156. /package/dist/{workspace-utils.d.ts → shared/workspace-utils.d.ts} +0 -0
  157. /package/dist/{workspace-utils.js → shared/workspace-utils.js} +0 -0
  158. /package/dist/{diff-utils.d.ts → sync/diff-utils.d.ts} +0 -0
  159. /package/dist/{diff-utils.js → sync/diff-utils.js} +0 -0
  160. /package/dist/{manifest.d.ts → sync/manifest.d.ts} +0 -0
  161. /package/dist/{manifest.js → sync/manifest.js} +0 -0
  162. /package/dist/{strategies/ruleset-strategy.js → sync/types.js} +0 -0
  163. /package/dist/{xfg-template.js → sync/xfg-template.js} +0 -0
  164. /package/dist/{authenticated-git-ops.d.ts → vcs/authenticated-git-ops.d.ts} +0 -0
@@ -0,0 +1,6 @@
1
+ export { runSync } from "./sync-command.js";
2
+ export { runSettings } from "./settings-command.js";
3
+ export { program } from "./program.js";
4
+ export { type IRepositoryProcessor, type ProcessorFactory, type IRulesetProcessor, type RulesetProcessorFactory, type RepoSettingsProcessorFactory, type IRepoSettingsProcessor, defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, } from "./types.js";
5
+ export type { SyncOptions, SharedOptions } from "./sync-command.js";
6
+ export type { SettingsOptions } from "./settings-command.js";
@@ -0,0 +1,9 @@
1
+ // CLI command implementations
2
+ export { runSync } from "./sync-command.js";
3
+ export { runSettings } from "./settings-command.js";
4
+ export { program } from "./program.js";
5
+ // Export types - using 'export type' for type aliases, but interfaces need special handling
6
+ // For ESM compatibility, re-export everything from types.js
7
+ export {
8
+ // Runtime values
9
+ defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, } from "./types.js";
@@ -0,0 +1,2 @@
1
+ import { program } from "commander";
2
+ export { program };
@@ -0,0 +1,70 @@
1
+ import { program, Command } from "commander";
2
+ import { dirname, join } from "node:path";
3
+ import { readFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import { runSync } from "./sync-command.js";
6
+ import { runSettings } from "./settings-command.js";
7
+ // Get version from package.json
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const packageJson = JSON.parse(readFileSync(join(__dirname, "../..", "package.json"), "utf-8"));
11
+ // =============================================================================
12
+ // Shared CLI Options
13
+ // =============================================================================
14
+ /**
15
+ * Adds shared options to a command.
16
+ */
17
+ function addSharedOptions(cmd) {
18
+ return cmd
19
+ .requiredOption("-c, --config <path>", "Path to YAML config file")
20
+ .option("-d, --dry-run", "Show what would be done without making changes")
21
+ .option("-w, --work-dir <path>", "Temporary directory for cloning", "./tmp")
22
+ .option("-r, --retries <number>", "Number of retries for network operations (0 to disable)", (v) => parseInt(v, 10), 3)
23
+ .option("--no-delete", "Skip deletion of orphaned resources even if deleteOrphaned is configured");
24
+ }
25
+ // =============================================================================
26
+ // CLI Program
27
+ // =============================================================================
28
+ program
29
+ .name("xfg")
30
+ .description("Sync files and manage settings across repositories")
31
+ .version(packageJson.version);
32
+ // Sync command (file synchronization)
33
+ const syncCommand = new Command("sync")
34
+ .description("Sync configuration files across repositories")
35
+ .option("-b, --branch <name>", "Override the branch name (default: chore/sync-{filename} or chore/sync-config)")
36
+ .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) => {
37
+ const valid = ["manual", "auto", "force", "direct"];
38
+ if (!valid.includes(value)) {
39
+ throw new Error(`Invalid merge mode: ${value}. Valid: ${valid.join(", ")}`);
40
+ }
41
+ return value;
42
+ })
43
+ .option("--merge-strategy <strategy>", "Merge strategy: merge, squash (default), rebase", (value) => {
44
+ const valid = ["merge", "squash", "rebase"];
45
+ if (!valid.includes(value)) {
46
+ throw new Error(`Invalid merge strategy: ${value}. Valid: ${valid.join(", ")}`);
47
+ }
48
+ return value;
49
+ })
50
+ .option("--delete-branch", "Delete source branch after merge")
51
+ .action((opts) => {
52
+ runSync(opts).catch((error) => {
53
+ console.error("Fatal error:", error);
54
+ process.exit(1);
55
+ });
56
+ });
57
+ addSharedOptions(syncCommand);
58
+ program.addCommand(syncCommand);
59
+ // Settings command (ruleset management)
60
+ const settingsCommand = new Command("settings")
61
+ .description("Manage GitHub Rulesets for repositories")
62
+ .action((opts) => {
63
+ runSettings(opts).catch((error) => {
64
+ console.error("Fatal error:", error);
65
+ process.exit(1);
66
+ });
67
+ });
68
+ addSharedOptions(settingsCommand);
69
+ program.addCommand(settingsCommand);
70
+ export { program };
@@ -0,0 +1,10 @@
1
+ import { SharedOptions } from "./sync-command.js";
2
+ import { ProcessorFactory, RulesetProcessorFactory, RepoSettingsProcessorFactory } from "./types.js";
3
+ /**
4
+ * Options for the settings command.
5
+ */
6
+ export type SettingsOptions = SharedOptions;
7
+ /**
8
+ * Run the settings command - manages GitHub Rulesets and repo settings.
9
+ */
10
+ export declare function runSettings(options: SettingsOptions, processorFactory?: RulesetProcessorFactory, repoProcessorFactory?: ProcessorFactory, repoSettingsProcessorFactory?: RepoSettingsProcessorFactory): Promise<void>;
@@ -0,0 +1,237 @@
1
+ import { resolve, join } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import chalk from "chalk";
4
+ import { loadRawConfig, normalizeConfig } from "../config/index.js";
5
+ import { validateForSettings } from "../config/validator.js";
6
+ import { parseGitUrl, getRepoDisplayName, isGitHubRepo, } from "../shared/repo-detector.js";
7
+ import { logger } from "../shared/logger.js";
8
+ import { generateWorkspaceName } from "../shared/workspace-utils.js";
9
+ import { buildErrorResult } from "../output/summary-utils.js";
10
+ import { getManagedRulesets } from "../sync/manifest.js";
11
+ import { formatSettingsReportCLI, writeSettingsReportSummary, } from "../output/settings-report.js";
12
+ import { buildSettingsReport } from "./settings-report-builder.js";
13
+ import { defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, } from "./types.js";
14
+ /**
15
+ * Run the settings command - manages GitHub Rulesets and repo settings.
16
+ */
17
+ export async function runSettings(options, processorFactory = defaultRulesetProcessorFactory, repoProcessorFactory = defaultProcessorFactory, repoSettingsProcessorFactory = defaultRepoSettingsProcessorFactory) {
18
+ const configPath = resolve(options.config);
19
+ if (!existsSync(configPath)) {
20
+ console.error(`Config file not found: ${configPath}`);
21
+ process.exit(1);
22
+ }
23
+ console.log(`Loading config from: ${configPath}`);
24
+ if (options.dryRun) {
25
+ console.log("Running in DRY RUN mode - no changes will be made\n");
26
+ }
27
+ const rawConfig = loadRawConfig(configPath);
28
+ try {
29
+ validateForSettings(rawConfig);
30
+ }
31
+ catch (error) {
32
+ console.error(error instanceof Error ? error.message : String(error));
33
+ process.exit(1);
34
+ }
35
+ const config = normalizeConfig(rawConfig);
36
+ const reposWithRulesets = config.repos.filter((r) => r.settings?.rulesets && Object.keys(r.settings.rulesets).length > 0);
37
+ const reposWithRepoSettings = config.repos.filter((r) => r.settings?.repo && Object.keys(r.settings.repo).length > 0);
38
+ if (reposWithRulesets.length === 0 && reposWithRepoSettings.length === 0) {
39
+ console.log("No settings configured. Add settings.rulesets or settings.repo to your config.");
40
+ return;
41
+ }
42
+ if (reposWithRulesets.length > 0) {
43
+ console.log(`Found ${reposWithRulesets.length} repositories with rulesets`);
44
+ }
45
+ if (reposWithRepoSettings.length > 0) {
46
+ console.log(`Found ${reposWithRepoSettings.length} repositories with repo settings`);
47
+ }
48
+ console.log("");
49
+ logger.setTotal(reposWithRulesets.length + reposWithRepoSettings.length);
50
+ const processor = processorFactory();
51
+ const repoProcessor = repoProcessorFactory();
52
+ const results = [];
53
+ const processingResults = [];
54
+ function getOrCreateResult(repoName) {
55
+ let result = processingResults.find((r) => r.repoName === repoName);
56
+ if (!result) {
57
+ result = { repoName };
58
+ processingResults.push(result);
59
+ }
60
+ return result;
61
+ }
62
+ for (let i = 0; i < reposWithRulesets.length; i++) {
63
+ const repoConfig = reposWithRulesets[i];
64
+ let repoInfo;
65
+ try {
66
+ repoInfo = parseGitUrl(repoConfig.git, {
67
+ githubHosts: config.githubHosts,
68
+ });
69
+ }
70
+ catch (error) {
71
+ logger.error(i + 1, repoConfig.git, String(error));
72
+ results.push(buildErrorResult(repoConfig.git, error));
73
+ getOrCreateResult(repoConfig.git).error =
74
+ error instanceof Error ? error.message : String(error);
75
+ continue;
76
+ }
77
+ const repoName = getRepoDisplayName(repoInfo);
78
+ if (!isGitHubRepo(repoInfo)) {
79
+ logger.skip(i + 1, repoName, "GitHub Rulesets only supported for GitHub repos");
80
+ // Skipped repos don't appear in the report
81
+ continue;
82
+ }
83
+ const managedRulesets = getManagedRulesets(null, config.id);
84
+ try {
85
+ logger.progress(i + 1, repoName, "Processing rulesets...");
86
+ const result = await processor.process(repoConfig, repoInfo, {
87
+ configId: config.id,
88
+ dryRun: options.dryRun,
89
+ managedRulesets,
90
+ noDelete: options.noDelete,
91
+ });
92
+ if (result.planOutput && result.planOutput.lines.length > 0) {
93
+ logger.info("");
94
+ logger.info(chalk.bold(`${repoName} - Rulesets:`));
95
+ for (const line of result.planOutput.lines) {
96
+ logger.info(line);
97
+ }
98
+ }
99
+ if (result.skipped) {
100
+ logger.skip(i + 1, repoName, result.message);
101
+ }
102
+ else if (result.success) {
103
+ logger.success(i + 1, repoName, result.message);
104
+ if (result.manifestUpdate &&
105
+ result.manifestUpdate.rulesets.length > 0) {
106
+ const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(i)));
107
+ logger.progress(i + 1, repoName, "Updating manifest...");
108
+ const manifestResult = await repoProcessor.updateManifestOnly(repoInfo, repoConfig, {
109
+ branchName: "chore/sync-rulesets",
110
+ workDir,
111
+ configId: config.id,
112
+ dryRun: options.dryRun,
113
+ retries: options.retries,
114
+ }, result.manifestUpdate);
115
+ if (!manifestResult.success && !manifestResult.skipped) {
116
+ logger.info(`Warning: Failed to update manifest for ${repoName}: ${manifestResult.message}`);
117
+ }
118
+ }
119
+ }
120
+ else {
121
+ logger.error(i + 1, repoName, result.message);
122
+ }
123
+ results.push({
124
+ repoName,
125
+ status: result.skipped
126
+ ? "skipped"
127
+ : result.success
128
+ ? "succeeded"
129
+ : "failed",
130
+ message: result.message,
131
+ rulesetPlanDetails: result.planOutput?.entries,
132
+ });
133
+ // Collect result for SettingsReport
134
+ if (!result.skipped) {
135
+ getOrCreateResult(repoName).rulesetResult = result;
136
+ }
137
+ }
138
+ catch (error) {
139
+ logger.error(i + 1, repoName, String(error));
140
+ results.push(buildErrorResult(repoName, error));
141
+ const existingResult = getOrCreateResult(repoName);
142
+ const errorMsg = error instanceof Error ? error.message : String(error);
143
+ if (existingResult.error) {
144
+ existingResult.error += `; ${errorMsg}`;
145
+ }
146
+ else {
147
+ existingResult.error = errorMsg;
148
+ }
149
+ }
150
+ }
151
+ if (reposWithRepoSettings.length > 0) {
152
+ const repoSettingsProcessor = repoSettingsProcessorFactory();
153
+ console.log(`\nProcessing repo settings for ${reposWithRepoSettings.length} repositories\n`);
154
+ for (let i = 0; i < reposWithRepoSettings.length; i++) {
155
+ const repoConfig = reposWithRepoSettings[i];
156
+ let repoInfo;
157
+ try {
158
+ repoInfo = parseGitUrl(repoConfig.git, {
159
+ githubHosts: config.githubHosts,
160
+ });
161
+ }
162
+ catch (error) {
163
+ console.error(`Failed to parse ${repoConfig.git}: ${error}`);
164
+ getOrCreateResult(repoConfig.git).error =
165
+ error instanceof Error ? error.message : String(error);
166
+ continue;
167
+ }
168
+ const repoName = getRepoDisplayName(repoInfo);
169
+ try {
170
+ const result = await repoSettingsProcessor.process(repoConfig, repoInfo, {
171
+ dryRun: options.dryRun,
172
+ });
173
+ if (result.planOutput && result.planOutput.lines.length > 0) {
174
+ console.log(`\n ${chalk.bold(repoName)}:`);
175
+ console.log(" Repo Settings:");
176
+ for (const line of result.planOutput.lines) {
177
+ console.log(line);
178
+ }
179
+ if (result.warnings && result.warnings.length > 0) {
180
+ for (const warning of result.warnings) {
181
+ console.log(chalk.yellow(` ⚠️ Warning: ${warning}`));
182
+ }
183
+ }
184
+ }
185
+ if (result.skipped) {
186
+ // Silent skip
187
+ }
188
+ else if (result.success) {
189
+ console.log(chalk.green(` ✓ ${repoName}: ${result.message}`));
190
+ }
191
+ else {
192
+ console.log(chalk.red(` ✗ ${repoName}: ${result.message}`));
193
+ }
194
+ if (!result.skipped) {
195
+ const existing = results.find((r) => r.repoName === repoName);
196
+ if (existing) {
197
+ existing.repoSettingsPlanDetails = result.planOutput?.entries;
198
+ }
199
+ else {
200
+ results.push({
201
+ repoName,
202
+ status: result.success ? "succeeded" : "failed",
203
+ message: result.message,
204
+ repoSettingsPlanDetails: result.planOutput?.entries,
205
+ });
206
+ }
207
+ }
208
+ // Collect result for SettingsReport
209
+ if (!result.skipped) {
210
+ getOrCreateResult(repoName).settingsResult = result;
211
+ }
212
+ }
213
+ catch (error) {
214
+ console.error(` ✗ ${repoName}: ${error}`);
215
+ const existingResult = getOrCreateResult(repoName);
216
+ const errorMsg = error instanceof Error ? error.message : String(error);
217
+ if (existingResult.error) {
218
+ existingResult.error += `; ${errorMsg}`;
219
+ }
220
+ else {
221
+ existingResult.error = errorMsg;
222
+ }
223
+ }
224
+ }
225
+ }
226
+ console.log("");
227
+ const report = buildSettingsReport(processingResults);
228
+ const lines = formatSettingsReportCLI(report);
229
+ for (const line of lines) {
230
+ console.log(line);
231
+ }
232
+ writeSettingsReportSummary(report, options.dryRun ?? false);
233
+ const hasErrors = report.repos.some((r) => r.error);
234
+ if (hasErrors) {
235
+ process.exit(1);
236
+ }
237
+ }
@@ -0,0 +1,19 @@
1
+ import type { SettingsReport } from "../output/settings-report.js";
2
+ import type { RepoSettingsPlanEntry } from "../settings/repo-settings/formatter.js";
3
+ import type { RulesetPlanEntry } from "../settings/rulesets/formatter.js";
4
+ interface ProcessorResults {
5
+ repoName: string;
6
+ settingsResult?: {
7
+ planOutput?: {
8
+ entries?: RepoSettingsPlanEntry[];
9
+ };
10
+ };
11
+ rulesetResult?: {
12
+ planOutput?: {
13
+ entries?: RulesetPlanEntry[];
14
+ };
15
+ };
16
+ error?: string;
17
+ }
18
+ export declare function buildSettingsReport(results: ProcessorResults[]): SettingsReport;
19
+ export {};
@@ -0,0 +1,64 @@
1
+ export function buildSettingsReport(results) {
2
+ const repos = [];
3
+ const totals = {
4
+ settings: { add: 0, change: 0 },
5
+ rulesets: { create: 0, update: 0, delete: 0 },
6
+ };
7
+ for (const result of results) {
8
+ const repoChanges = {
9
+ repoName: result.repoName,
10
+ settings: [],
11
+ rulesets: [],
12
+ };
13
+ // Convert settings processor output
14
+ if (result.settingsResult?.planOutput?.entries) {
15
+ for (const entry of result.settingsResult.planOutput.entries) {
16
+ // Skip settings where both values are undefined (no actual change)
17
+ if (entry.oldValue === undefined && entry.newValue === undefined) {
18
+ continue;
19
+ }
20
+ const settingChange = {
21
+ name: entry.property,
22
+ action: entry.action,
23
+ oldValue: entry.oldValue,
24
+ newValue: entry.newValue,
25
+ };
26
+ repoChanges.settings.push(settingChange);
27
+ if (entry.action === "add") {
28
+ totals.settings.add++;
29
+ }
30
+ else {
31
+ totals.settings.change++;
32
+ }
33
+ }
34
+ }
35
+ // Convert ruleset processor output
36
+ if (result.rulesetResult?.planOutput?.entries) {
37
+ for (const entry of result.rulesetResult.planOutput.entries) {
38
+ if (entry.action === "unchanged")
39
+ continue;
40
+ const rulesetChange = {
41
+ name: entry.name,
42
+ action: entry.action,
43
+ propertyDiffs: entry.propertyDiffs,
44
+ config: entry.config,
45
+ };
46
+ repoChanges.rulesets.push(rulesetChange);
47
+ if (entry.action === "create") {
48
+ totals.rulesets.create++;
49
+ }
50
+ else if (entry.action === "update") {
51
+ totals.rulesets.update++;
52
+ }
53
+ else if (entry.action === "delete") {
54
+ totals.rulesets.delete++;
55
+ }
56
+ }
57
+ }
58
+ if (result.error) {
59
+ repoChanges.error = result.error;
60
+ }
61
+ repos.push(repoChanges);
62
+ }
63
+ return { repos, totals };
64
+ }
@@ -0,0 +1,25 @@
1
+ import { MergeMode, MergeStrategy } from "../config/index.js";
2
+ import { ProcessorFactory } from "./types.js";
3
+ /**
4
+ * Shared options common to all commands.
5
+ */
6
+ export interface SharedOptions {
7
+ config: string;
8
+ dryRun?: boolean;
9
+ workDir?: string;
10
+ retries?: number;
11
+ noDelete?: boolean;
12
+ }
13
+ /**
14
+ * Options specific to the sync command.
15
+ */
16
+ export interface SyncOptions extends SharedOptions {
17
+ branch?: string;
18
+ merge?: MergeMode;
19
+ mergeStrategy?: MergeStrategy;
20
+ deleteBranch?: boolean;
21
+ }
22
+ /**
23
+ * Run the sync command - synchronizes files across repositories.
24
+ */
25
+ export declare function runSync(options: SyncOptions, processorFactory?: ProcessorFactory): Promise<void>;
@@ -0,0 +1,180 @@
1
+ import { resolve, join } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import { loadRawConfig, normalizeConfig, } from "../config/index.js";
4
+ import { validateForSync } from "../config/validator.js";
5
+ import { parseGitUrl, getRepoDisplayName } from "../shared/repo-detector.js";
6
+ import { sanitizeBranchName, validateBranchName } from "../vcs/git-ops.js";
7
+ import { logger } from "../shared/logger.js";
8
+ import { generateWorkspaceName } from "../shared/workspace-utils.js";
9
+ import { defaultProcessorFactory } from "./types.js";
10
+ import { buildSyncReport } from "./sync-report-builder.js";
11
+ import { formatSyncReportCLI, writeSyncReportSummary, } from "../output/sync-report.js";
12
+ /**
13
+ * Get unique file names from all repos in the config
14
+ */
15
+ function getUniqueFileNames(config) {
16
+ const fileNames = new Set();
17
+ for (const repo of config.repos) {
18
+ for (const file of repo.files) {
19
+ fileNames.add(file.fileName);
20
+ }
21
+ }
22
+ return Array.from(fileNames);
23
+ }
24
+ /**
25
+ * Generate default branch name based on files being synced
26
+ */
27
+ function generateBranchName(fileNames) {
28
+ if (fileNames.length === 1) {
29
+ return `chore/sync-${sanitizeBranchName(fileNames[0])}`;
30
+ }
31
+ return "chore/sync-config";
32
+ }
33
+ /**
34
+ * Format file names for display
35
+ */
36
+ function formatFileNames(fileNames) {
37
+ if (fileNames.length === 1) {
38
+ return fileNames[0];
39
+ }
40
+ if (fileNames.length <= 3) {
41
+ return fileNames.join(", ");
42
+ }
43
+ return `${fileNames.length} files`;
44
+ }
45
+ /**
46
+ * Determine merge outcome from processor result
47
+ */
48
+ function determineMergeOutcome(result) {
49
+ if (!result.success)
50
+ return undefined;
51
+ if (!result.prUrl)
52
+ return "direct";
53
+ if (result.mergeResult?.merged)
54
+ return "force";
55
+ if (result.mergeResult?.autoMergeEnabled)
56
+ return "auto";
57
+ return "manual";
58
+ }
59
+ /**
60
+ * Run the sync command - synchronizes files across repositories.
61
+ */
62
+ export async function runSync(options, processorFactory = defaultProcessorFactory) {
63
+ const configPath = resolve(options.config);
64
+ if (!existsSync(configPath)) {
65
+ console.error(`Config file not found: ${configPath}`);
66
+ process.exit(1);
67
+ }
68
+ console.log(`Loading config from: ${configPath}`);
69
+ if (options.dryRun) {
70
+ console.log("Running in DRY RUN mode - no changes will be made\n");
71
+ }
72
+ const rawConfig = loadRawConfig(configPath);
73
+ try {
74
+ validateForSync(rawConfig);
75
+ }
76
+ catch (error) {
77
+ console.error(error instanceof Error ? error.message : String(error));
78
+ process.exit(1);
79
+ }
80
+ const config = normalizeConfig(rawConfig);
81
+ const fileNames = getUniqueFileNames(config);
82
+ let branchName;
83
+ if (options.branch) {
84
+ validateBranchName(options.branch);
85
+ branchName = options.branch;
86
+ }
87
+ else {
88
+ branchName = generateBranchName(fileNames);
89
+ }
90
+ logger.setTotal(config.repos.length);
91
+ console.log(`Found ${config.repos.length} repositories to process`);
92
+ console.log(`Target files: ${formatFileNames(fileNames)}`);
93
+ console.log(`Branch: ${branchName}\n`);
94
+ const processor = processorFactory();
95
+ const reportResults = [];
96
+ for (let i = 0; i < config.repos.length; i++) {
97
+ const repoConfig = config.repos[i];
98
+ if (options.merge || options.mergeStrategy || options.deleteBranch) {
99
+ repoConfig.prOptions = {
100
+ ...repoConfig.prOptions,
101
+ merge: options.merge ?? repoConfig.prOptions?.merge,
102
+ mergeStrategy: options.mergeStrategy ?? repoConfig.prOptions?.mergeStrategy,
103
+ deleteBranch: options.deleteBranch ?? repoConfig.prOptions?.deleteBranch,
104
+ };
105
+ }
106
+ const current = i + 1;
107
+ let repoInfo;
108
+ try {
109
+ repoInfo = parseGitUrl(repoConfig.git, {
110
+ githubHosts: config.githubHosts,
111
+ });
112
+ }
113
+ catch (error) {
114
+ logger.error(current, repoConfig.git, String(error));
115
+ reportResults.push({
116
+ repoName: repoConfig.git,
117
+ success: false,
118
+ fileChanges: [],
119
+ error: error instanceof Error ? error.message : String(error),
120
+ });
121
+ continue;
122
+ }
123
+ const repoName = getRepoDisplayName(repoInfo);
124
+ const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(i)));
125
+ try {
126
+ logger.progress(current, repoName, "Processing...");
127
+ const result = await processor.process(repoConfig, repoInfo, {
128
+ branchName,
129
+ workDir,
130
+ configId: config.id,
131
+ dryRun: options.dryRun,
132
+ retries: options.retries,
133
+ prTemplate: config.prTemplate,
134
+ noDelete: options.noDelete,
135
+ });
136
+ const mergeOutcome = determineMergeOutcome(result);
137
+ reportResults.push({
138
+ repoName,
139
+ success: result.success,
140
+ fileChanges: (result.fileChanges ?? []).map((f) => ({
141
+ path: f.path,
142
+ action: f.action,
143
+ })),
144
+ prUrl: result.prUrl,
145
+ mergeOutcome,
146
+ error: result.success ? undefined : result.message,
147
+ });
148
+ if (result.skipped) {
149
+ logger.skip(current, repoName, result.message);
150
+ }
151
+ else if (result.success) {
152
+ logger.success(current, repoName, result.message);
153
+ }
154
+ else {
155
+ logger.error(current, repoName, result.message);
156
+ }
157
+ }
158
+ catch (error) {
159
+ logger.error(current, repoName, String(error));
160
+ reportResults.push({
161
+ repoName,
162
+ success: false,
163
+ fileChanges: [],
164
+ error: error instanceof Error ? error.message : String(error),
165
+ });
166
+ }
167
+ }
168
+ // Build and display report
169
+ const report = buildSyncReport(reportResults);
170
+ console.log("");
171
+ for (const line of formatSyncReportCLI(report)) {
172
+ console.log(line);
173
+ }
174
+ writeSyncReportSummary(report, options.dryRun ?? false);
175
+ // Exit with error if any failures
176
+ const hasErrors = reportResults.some((r) => r.error);
177
+ if (hasErrors) {
178
+ process.exit(1);
179
+ }
180
+ }
@@ -0,0 +1,15 @@
1
+ import type { SyncReport } from "../output/sync-report.js";
2
+ interface FileChangeInput {
3
+ path: string;
4
+ action: "create" | "update" | "delete";
5
+ }
6
+ interface SyncResultInput {
7
+ repoName: string;
8
+ success: boolean;
9
+ fileChanges: FileChangeInput[];
10
+ prUrl?: string;
11
+ mergeOutcome?: "manual" | "auto" | "force" | "direct";
12
+ error?: string;
13
+ }
14
+ export declare function buildSyncReport(results: SyncResultInput[]): SyncReport;
15
+ export {};
@@ -0,0 +1,29 @@
1
+ export function buildSyncReport(results) {
2
+ const repos = [];
3
+ const totals = {
4
+ files: { create: 0, update: 0, delete: 0 },
5
+ };
6
+ for (const result of results) {
7
+ const files = result.fileChanges.map((f) => ({
8
+ path: f.path,
9
+ action: f.action,
10
+ }));
11
+ // Count totals
12
+ for (const file of files) {
13
+ if (file.action === "create")
14
+ totals.files.create++;
15
+ else if (file.action === "update")
16
+ totals.files.update++;
17
+ else if (file.action === "delete")
18
+ totals.files.delete++;
19
+ }
20
+ repos.push({
21
+ repoName: result.repoName,
22
+ files,
23
+ prUrl: result.prUrl,
24
+ mergeOutcome: result.mergeOutcome,
25
+ error: result.error,
26
+ });
27
+ }
28
+ return { repos, totals };
29
+ }