@aspruyt/xfg 3.7.5 → 3.7.7

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 (156) 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 +228 -0
  7. package/dist/cli/sync-command.d.ts +25 -0
  8. package/dist/cli/sync-command.js +155 -0
  9. package/dist/cli/types.d.ts +45 -0
  10. package/dist/cli/types.js +15 -0
  11. package/dist/cli.js +2 -19
  12. package/dist/{file-reference-resolver.d.ts → config/file-reference-resolver.d.ts} +1 -1
  13. package/dist/config/index.d.ts +7 -0
  14. package/dist/config/index.js +12 -0
  15. package/dist/config/loader.d.ts +9 -0
  16. package/dist/{config.js → config/loader.js} +3 -24
  17. package/dist/{config-normalizer.d.ts → config/normalizer.d.ts} +1 -1
  18. package/dist/{config-normalizer.js → config/normalizer.js} +1 -1
  19. package/dist/{config.d.ts → config/types.d.ts} +5 -9
  20. package/dist/config/types.js +16 -0
  21. package/dist/{config-validator.d.ts → config/validator.d.ts} +5 -5
  22. package/dist/{config-validator.js → config/validator.js} +60 -372
  23. package/dist/config/validators/file-validator.d.ts +22 -0
  24. package/dist/config/validators/file-validator.js +46 -0
  25. package/dist/config/validators/index.d.ts +3 -0
  26. package/dist/config/validators/index.js +6 -0
  27. package/dist/config/validators/repo-settings-validator.d.ts +10 -0
  28. package/dist/config/validators/repo-settings-validator.js +71 -0
  29. package/dist/config/validators/ruleset-validator.d.ts +18 -0
  30. package/dist/config/validators/ruleset-validator.js +201 -0
  31. package/dist/index.d.ts +3 -66
  32. package/dist/index.js +3 -474
  33. package/dist/output/index.d.ts +4 -0
  34. package/dist/output/index.js +8 -0
  35. package/dist/{summary-utils.d.ts → output/summary-utils.d.ts} +3 -3
  36. package/dist/settings/index.d.ts +3 -0
  37. package/dist/settings/index.js +6 -0
  38. package/dist/{repo-settings-diff.d.ts → settings/repo-settings/diff.d.ts} +2 -2
  39. package/dist/{repo-settings-plan-formatter.d.ts → settings/repo-settings/formatter.d.ts} +1 -1
  40. package/dist/{strategies → settings/repo-settings}/github-repo-settings-strategy.d.ts +4 -4
  41. package/dist/{strategies → settings/repo-settings}/github-repo-settings-strategy.js +3 -3
  42. package/dist/settings/repo-settings/index.d.ts +5 -0
  43. package/dist/settings/repo-settings/index.js +10 -0
  44. package/dist/{repo-settings-processor.d.ts → settings/repo-settings/processor.d.ts} +4 -4
  45. package/dist/{repo-settings-processor.js → settings/repo-settings/processor.js} +6 -6
  46. package/dist/{strategies/repo-settings-strategy.d.ts → settings/repo-settings/types.d.ts} +2 -2
  47. package/dist/{resource-converters.d.ts → settings/resource-converters.d.ts} +4 -4
  48. package/dist/settings/rulesets/diff-algorithm.d.ts +18 -0
  49. package/dist/settings/rulesets/diff-algorithm.js +166 -0
  50. package/dist/{ruleset-diff.d.ts → settings/rulesets/diff.d.ts} +2 -2
  51. package/dist/{ruleset-diff.js → settings/rulesets/diff.js} +1 -1
  52. package/dist/{ruleset-plan-formatter.d.ts → settings/rulesets/formatter.d.ts} +4 -12
  53. package/dist/{ruleset-plan-formatter.js → settings/rulesets/formatter.js} +5 -166
  54. package/dist/{strategies → settings/rulesets}/github-ruleset-strategy.d.ts +4 -4
  55. package/dist/{strategies → settings/rulesets}/github-ruleset-strategy.js +3 -3
  56. package/dist/settings/rulesets/index.d.ts +6 -0
  57. package/dist/settings/rulesets/index.js +10 -0
  58. package/dist/{ruleset-processor.d.ts → settings/rulesets/processor.d.ts} +4 -4
  59. package/dist/{ruleset-processor.js → settings/rulesets/processor.js} +6 -6
  60. package/dist/{strategies/ruleset-strategy.d.ts → settings/rulesets/types.d.ts} +2 -2
  61. package/dist/{command-executor.d.ts → shared/command-executor.d.ts} +10 -2
  62. package/dist/{command-executor.js → shared/command-executor.js} +2 -1
  63. package/dist/shared/index.d.ts +8 -0
  64. package/dist/shared/index.js +16 -0
  65. package/dist/{logger.d.ts → shared/logger.d.ts} +1 -1
  66. package/dist/{logger.js → shared/logger.js} +1 -1
  67. package/dist/sync/auth-options-builder.d.ts +12 -0
  68. package/dist/sync/auth-options-builder.js +54 -0
  69. package/dist/sync/branch-manager.d.ts +7 -0
  70. package/dist/sync/branch-manager.js +36 -0
  71. package/dist/sync/commit-message.d.ts +11 -0
  72. package/dist/sync/commit-message.js +27 -0
  73. package/dist/sync/commit-push-manager.d.ts +8 -0
  74. package/dist/sync/commit-push-manager.js +71 -0
  75. package/dist/sync/file-sync-orchestrator.d.ts +11 -0
  76. package/dist/sync/file-sync-orchestrator.js +58 -0
  77. package/dist/sync/file-writer.d.ts +18 -0
  78. package/dist/sync/file-writer.js +101 -0
  79. package/dist/sync/index.d.ts +14 -0
  80. package/dist/sync/index.js +17 -0
  81. package/dist/sync/manifest-manager.d.ts +10 -0
  82. package/dist/sync/manifest-manager.js +64 -0
  83. package/dist/sync/pr-merge-handler.d.ts +11 -0
  84. package/dist/sync/pr-merge-handler.js +62 -0
  85. package/dist/sync/repository-processor.d.ts +30 -0
  86. package/dist/sync/repository-processor.js +278 -0
  87. package/dist/sync/repository-session.d.ts +9 -0
  88. package/dist/sync/repository-session.js +35 -0
  89. package/dist/sync/types.d.ts +296 -0
  90. package/dist/{xfg-template.d.ts → sync/xfg-template.d.ts} +2 -2
  91. package/dist/{authenticated-git-ops.js → vcs/authenticated-git-ops.js} +3 -3
  92. package/dist/{strategies → vcs}/azure-pr-strategy.d.ts +2 -2
  93. package/dist/{strategies → vcs}/azure-pr-strategy.js +5 -5
  94. package/dist/{strategies → vcs}/commit-strategy-selector.d.ts +3 -3
  95. package/dist/{strategies → vcs}/commit-strategy-selector.js +1 -1
  96. package/dist/{strategies → vcs}/git-commit-strategy.d.ts +2 -2
  97. package/dist/{strategies → vcs}/git-commit-strategy.js +3 -3
  98. package/dist/{git-ops.d.ts → vcs/git-ops.d.ts} +1 -1
  99. package/dist/{git-ops.js → vcs/git-ops.js} +4 -4
  100. package/dist/{github-app-token-manager.d.ts → vcs/github-app-token-manager.d.ts} +1 -1
  101. package/dist/{github-app-token-manager.js → vcs/github-app-token-manager.js} +1 -1
  102. package/dist/{strategies → vcs}/github-pr-strategy.d.ts +2 -2
  103. package/dist/{strategies → vcs}/github-pr-strategy.js +30 -33
  104. package/dist/{strategies → vcs}/gitlab-pr-strategy.d.ts +2 -2
  105. package/dist/{strategies → vcs}/gitlab-pr-strategy.js +5 -5
  106. package/dist/{strategies → vcs}/graphql-commit-strategy.d.ts +2 -2
  107. package/dist/{strategies → vcs}/graphql-commit-strategy.js +3 -3
  108. package/dist/vcs/index.d.ts +16 -0
  109. package/dist/{strategies → vcs}/index.js +15 -10
  110. package/dist/{pr-creator.d.ts → vcs/pr-creator.d.ts} +4 -4
  111. package/dist/{pr-creator.js → vcs/pr-creator.js} +3 -3
  112. package/dist/vcs/pr-strategy.d.ts +41 -0
  113. package/dist/{strategies → vcs}/pr-strategy.js +1 -1
  114. package/dist/{strategies/pr-strategy.d.ts → vcs/types.d.ts} +32 -35
  115. package/dist/vcs/types.js +1 -0
  116. package/package.json +2 -2
  117. package/dist/repository-processor.d.ts +0 -79
  118. package/dist/repository-processor.js +0 -659
  119. package/dist/strategies/commit-strategy.d.ts +0 -36
  120. package/dist/strategies/index.d.ts +0 -18
  121. /package/dist/{file-reference-resolver.js → config/file-reference-resolver.js} +0 -0
  122. /package/dist/{config-formatter.d.ts → config/formatter.d.ts} +0 -0
  123. /package/dist/{config-formatter.js → config/formatter.js} +0 -0
  124. /package/dist/{merge.d.ts → config/merge.d.ts} +0 -0
  125. /package/dist/{merge.js → config/merge.js} +0 -0
  126. /package/dist/{github-summary.d.ts → output/github-summary.d.ts} +0 -0
  127. /package/dist/{github-summary.js → output/github-summary.js} +0 -0
  128. /package/dist/{plan-formatter.d.ts → output/plan-formatter.d.ts} +0 -0
  129. /package/dist/{plan-formatter.js → output/plan-formatter.js} +0 -0
  130. /package/dist/{plan-summary.d.ts → output/plan-summary.d.ts} +0 -0
  131. /package/dist/{plan-summary.js → output/plan-summary.js} +0 -0
  132. /package/dist/{summary-utils.js → output/summary-utils.js} +0 -0
  133. /package/dist/{repo-settings-diff.js → settings/repo-settings/diff.js} +0 -0
  134. /package/dist/{repo-settings-plan-formatter.js → settings/repo-settings/formatter.js} +0 -0
  135. /package/dist/{strategies/repo-settings-strategy.js → settings/repo-settings/types.js} +0 -0
  136. /package/dist/{resource-converters.js → settings/resource-converters.js} +0 -0
  137. /package/dist/{strategies/commit-strategy.js → settings/rulesets/types.js} +0 -0
  138. /package/dist/{env.d.ts → shared/env.d.ts} +0 -0
  139. /package/dist/{env.js → shared/env.js} +0 -0
  140. /package/dist/{repo-detector.d.ts → shared/repo-detector.d.ts} +0 -0
  141. /package/dist/{repo-detector.js → shared/repo-detector.js} +0 -0
  142. /package/dist/{retry-utils.d.ts → shared/retry-utils.d.ts} +0 -0
  143. /package/dist/{retry-utils.js → shared/retry-utils.js} +0 -0
  144. /package/dist/{sanitize-utils.d.ts → shared/sanitize-utils.d.ts} +0 -0
  145. /package/dist/{sanitize-utils.js → shared/sanitize-utils.js} +0 -0
  146. /package/dist/{shell-utils.d.ts → shared/shell-utils.d.ts} +0 -0
  147. /package/dist/{shell-utils.js → shared/shell-utils.js} +0 -0
  148. /package/dist/{workspace-utils.d.ts → shared/workspace-utils.d.ts} +0 -0
  149. /package/dist/{workspace-utils.js → shared/workspace-utils.js} +0 -0
  150. /package/dist/{diff-utils.d.ts → sync/diff-utils.d.ts} +0 -0
  151. /package/dist/{diff-utils.js → sync/diff-utils.js} +0 -0
  152. /package/dist/{manifest.d.ts → sync/manifest.d.ts} +0 -0
  153. /package/dist/{manifest.js → sync/manifest.js} +0 -0
  154. /package/dist/{strategies/ruleset-strategy.js → sync/types.js} +0 -0
  155. /package/dist/{xfg-template.js → sync/xfg-template.js} +0 -0
  156. /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,228 @@
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 { printPlan } from "../output/plan-formatter.js";
12
+ import { writePlanSummary } from "../output/plan-summary.js";
13
+ import { rulesetResultToResources, repoSettingsResultToResources, } from "../settings/resource-converters.js";
14
+ import { defaultProcessorFactory, defaultRulesetProcessorFactory, defaultRepoSettingsProcessorFactory, } from "./types.js";
15
+ /**
16
+ * Run the settings command - manages GitHub Rulesets and repo settings.
17
+ */
18
+ export async function runSettings(options, processorFactory = defaultRulesetProcessorFactory, repoProcessorFactory = defaultProcessorFactory, repoSettingsProcessorFactory = defaultRepoSettingsProcessorFactory) {
19
+ const configPath = resolve(options.config);
20
+ if (!existsSync(configPath)) {
21
+ console.error(`Config file not found: ${configPath}`);
22
+ process.exit(1);
23
+ }
24
+ console.log(`Loading config from: ${configPath}`);
25
+ if (options.dryRun) {
26
+ console.log("Running in DRY RUN mode - no changes will be made\n");
27
+ }
28
+ const rawConfig = loadRawConfig(configPath);
29
+ try {
30
+ validateForSettings(rawConfig);
31
+ }
32
+ catch (error) {
33
+ console.error(error instanceof Error ? error.message : String(error));
34
+ process.exit(1);
35
+ }
36
+ const config = normalizeConfig(rawConfig);
37
+ const reposWithRulesets = config.repos.filter((r) => r.settings?.rulesets && Object.keys(r.settings.rulesets).length > 0);
38
+ const reposWithRepoSettings = config.repos.filter((r) => r.settings?.repo && Object.keys(r.settings.repo).length > 0);
39
+ if (reposWithRulesets.length === 0 && reposWithRepoSettings.length === 0) {
40
+ console.log("No settings configured. Add settings.rulesets or settings.repo to your config.");
41
+ return;
42
+ }
43
+ if (reposWithRulesets.length > 0) {
44
+ console.log(`Found ${reposWithRulesets.length} repositories with rulesets`);
45
+ }
46
+ if (reposWithRepoSettings.length > 0) {
47
+ console.log(`Found ${reposWithRepoSettings.length} repositories with repo settings`);
48
+ }
49
+ console.log("");
50
+ logger.setTotal(reposWithRulesets.length + reposWithRepoSettings.length);
51
+ const processor = processorFactory();
52
+ const repoProcessor = repoProcessorFactory();
53
+ const results = [];
54
+ const plan = { resources: [], errors: [] };
55
+ for (let i = 0; i < reposWithRulesets.length; i++) {
56
+ const repoConfig = reposWithRulesets[i];
57
+ let repoInfo;
58
+ try {
59
+ repoInfo = parseGitUrl(repoConfig.git, {
60
+ githubHosts: config.githubHosts,
61
+ });
62
+ }
63
+ catch (error) {
64
+ logger.error(i + 1, repoConfig.git, String(error));
65
+ results.push(buildErrorResult(repoConfig.git, error));
66
+ plan.errors.push({
67
+ repo: repoConfig.git,
68
+ message: error instanceof Error ? error.message : String(error),
69
+ });
70
+ continue;
71
+ }
72
+ const repoName = getRepoDisplayName(repoInfo);
73
+ if (!isGitHubRepo(repoInfo)) {
74
+ logger.skip(i + 1, repoName, "GitHub Rulesets only supported for GitHub repos");
75
+ if (repoConfig.settings?.rulesets) {
76
+ for (const rulesetName of Object.keys(repoConfig.settings.rulesets)) {
77
+ plan.resources.push({
78
+ type: "ruleset",
79
+ repo: repoName,
80
+ name: rulesetName,
81
+ action: "skipped",
82
+ skipReason: "GitHub Rulesets only supported for GitHub repos",
83
+ });
84
+ }
85
+ }
86
+ continue;
87
+ }
88
+ const managedRulesets = getManagedRulesets(null, config.id);
89
+ try {
90
+ logger.progress(i + 1, repoName, "Processing rulesets...");
91
+ const result = await processor.process(repoConfig, repoInfo, {
92
+ configId: config.id,
93
+ dryRun: options.dryRun,
94
+ managedRulesets,
95
+ noDelete: options.noDelete,
96
+ });
97
+ if (result.planOutput && result.planOutput.lines.length > 0) {
98
+ logger.info("");
99
+ logger.info(chalk.bold(`${repoName} - Rulesets:`));
100
+ for (const line of result.planOutput.lines) {
101
+ logger.info(line);
102
+ }
103
+ }
104
+ if (result.skipped) {
105
+ logger.skip(i + 1, repoName, result.message);
106
+ }
107
+ else if (result.success) {
108
+ logger.success(i + 1, repoName, result.message);
109
+ if (result.manifestUpdate &&
110
+ result.manifestUpdate.rulesets.length > 0) {
111
+ const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(i)));
112
+ logger.progress(i + 1, repoName, "Updating manifest...");
113
+ const manifestResult = await repoProcessor.updateManifestOnly(repoInfo, repoConfig, {
114
+ branchName: "chore/sync-rulesets",
115
+ workDir,
116
+ configId: config.id,
117
+ dryRun: options.dryRun,
118
+ retries: options.retries,
119
+ }, result.manifestUpdate);
120
+ if (!manifestResult.success && !manifestResult.skipped) {
121
+ logger.info(`Warning: Failed to update manifest for ${repoName}: ${manifestResult.message}`);
122
+ }
123
+ }
124
+ }
125
+ else {
126
+ logger.error(i + 1, repoName, result.message);
127
+ }
128
+ results.push({
129
+ repoName,
130
+ status: result.skipped
131
+ ? "skipped"
132
+ : result.success
133
+ ? "succeeded"
134
+ : "failed",
135
+ message: result.message,
136
+ rulesetPlanDetails: result.planOutput?.entries,
137
+ });
138
+ plan.resources.push(...rulesetResultToResources(repoName, result));
139
+ }
140
+ catch (error) {
141
+ logger.error(i + 1, repoName, String(error));
142
+ results.push(buildErrorResult(repoName, error));
143
+ plan.errors.push({
144
+ repo: repoName,
145
+ message: error instanceof Error ? error.message : String(error),
146
+ });
147
+ }
148
+ }
149
+ if (reposWithRepoSettings.length > 0) {
150
+ const repoSettingsProcessor = repoSettingsProcessorFactory();
151
+ console.log(`\nProcessing repo settings for ${reposWithRepoSettings.length} repositories\n`);
152
+ for (let i = 0; i < reposWithRepoSettings.length; i++) {
153
+ const repoConfig = reposWithRepoSettings[i];
154
+ let repoInfo;
155
+ try {
156
+ repoInfo = parseGitUrl(repoConfig.git, {
157
+ githubHosts: config.githubHosts,
158
+ });
159
+ }
160
+ catch (error) {
161
+ console.error(`Failed to parse ${repoConfig.git}: ${error}`);
162
+ plan.errors.push({
163
+ repo: repoConfig.git,
164
+ message: error instanceof Error ? error.message : String(error),
165
+ });
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
+ plan.resources.push(...repoSettingsResultToResources(repoName, result));
209
+ }
210
+ catch (error) {
211
+ console.error(` ✗ ${repoName}: ${error}`);
212
+ plan.errors.push({
213
+ repo: repoName,
214
+ message: error instanceof Error ? error.message : String(error),
215
+ });
216
+ }
217
+ }
218
+ }
219
+ console.log("");
220
+ printPlan(plan);
221
+ writePlanSummary(plan, {
222
+ title: "Repository Settings Summary",
223
+ dryRun: options.dryRun ?? false,
224
+ });
225
+ if (plan.errors && plan.errors.length > 0) {
226
+ process.exit(1);
227
+ }
228
+ }
@@ -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,155 @@
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 { buildRepoResult, buildErrorResult } from "../output/summary-utils.js";
10
+ import { printPlan } from "../output/plan-formatter.js";
11
+ import { writePlanSummary } from "../output/plan-summary.js";
12
+ import { syncResultToResources } from "../settings/resource-converters.js";
13
+ import { defaultProcessorFactory } from "./types.js";
14
+ /**
15
+ * Get unique file names from all repos in the config
16
+ */
17
+ function getUniqueFileNames(config) {
18
+ const fileNames = new Set();
19
+ for (const repo of config.repos) {
20
+ for (const file of repo.files) {
21
+ fileNames.add(file.fileName);
22
+ }
23
+ }
24
+ return Array.from(fileNames);
25
+ }
26
+ /**
27
+ * Generate default branch name based on files being synced
28
+ */
29
+ function generateBranchName(fileNames) {
30
+ if (fileNames.length === 1) {
31
+ return `chore/sync-${sanitizeBranchName(fileNames[0])}`;
32
+ }
33
+ return "chore/sync-config";
34
+ }
35
+ /**
36
+ * Format file names for display
37
+ */
38
+ function formatFileNames(fileNames) {
39
+ if (fileNames.length === 1) {
40
+ return fileNames[0];
41
+ }
42
+ if (fileNames.length <= 3) {
43
+ return fileNames.join(", ");
44
+ }
45
+ return `${fileNames.length} files`;
46
+ }
47
+ /**
48
+ * Run the sync command - synchronizes files across repositories.
49
+ */
50
+ export async function runSync(options, processorFactory = defaultProcessorFactory) {
51
+ const configPath = resolve(options.config);
52
+ if (!existsSync(configPath)) {
53
+ console.error(`Config file not found: ${configPath}`);
54
+ process.exit(1);
55
+ }
56
+ console.log(`Loading config from: ${configPath}`);
57
+ if (options.dryRun) {
58
+ console.log("Running in DRY RUN mode - no changes will be made\n");
59
+ }
60
+ const rawConfig = loadRawConfig(configPath);
61
+ try {
62
+ validateForSync(rawConfig);
63
+ }
64
+ catch (error) {
65
+ console.error(error instanceof Error ? error.message : String(error));
66
+ process.exit(1);
67
+ }
68
+ const config = normalizeConfig(rawConfig);
69
+ const fileNames = getUniqueFileNames(config);
70
+ let branchName;
71
+ if (options.branch) {
72
+ validateBranchName(options.branch);
73
+ branchName = options.branch;
74
+ }
75
+ else {
76
+ branchName = generateBranchName(fileNames);
77
+ }
78
+ logger.setTotal(config.repos.length);
79
+ console.log(`Found ${config.repos.length} repositories to process`);
80
+ console.log(`Target files: ${formatFileNames(fileNames)}`);
81
+ console.log(`Branch: ${branchName}\n`);
82
+ const processor = processorFactory();
83
+ const results = [];
84
+ const plan = { resources: [], errors: [] };
85
+ for (let i = 0; i < config.repos.length; i++) {
86
+ const repoConfig = config.repos[i];
87
+ if (options.merge || options.mergeStrategy || options.deleteBranch) {
88
+ repoConfig.prOptions = {
89
+ ...repoConfig.prOptions,
90
+ merge: options.merge ?? repoConfig.prOptions?.merge,
91
+ mergeStrategy: options.mergeStrategy ?? repoConfig.prOptions?.mergeStrategy,
92
+ deleteBranch: options.deleteBranch ?? repoConfig.prOptions?.deleteBranch,
93
+ };
94
+ }
95
+ const current = i + 1;
96
+ let repoInfo;
97
+ try {
98
+ repoInfo = parseGitUrl(repoConfig.git, {
99
+ githubHosts: config.githubHosts,
100
+ });
101
+ }
102
+ catch (error) {
103
+ logger.error(current, repoConfig.git, String(error));
104
+ results.push(buildErrorResult(repoConfig.git, error));
105
+ plan.errors.push({
106
+ repo: repoConfig.git,
107
+ message: error instanceof Error ? error.message : String(error),
108
+ });
109
+ continue;
110
+ }
111
+ const repoName = getRepoDisplayName(repoInfo);
112
+ const workDir = resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(i)));
113
+ try {
114
+ logger.progress(current, repoName, "Processing...");
115
+ const result = await processor.process(repoConfig, repoInfo, {
116
+ branchName,
117
+ workDir,
118
+ configId: config.id,
119
+ dryRun: options.dryRun,
120
+ retries: options.retries,
121
+ prTemplate: config.prTemplate,
122
+ noDelete: options.noDelete,
123
+ });
124
+ const repoResult = buildRepoResult(repoName, repoConfig, result);
125
+ results.push(repoResult);
126
+ if (result.skipped) {
127
+ logger.skip(current, repoName, result.message);
128
+ }
129
+ else if (result.success) {
130
+ logger.success(current, repoName, repoResult.message);
131
+ }
132
+ else {
133
+ logger.error(current, repoName, result.message);
134
+ }
135
+ plan.resources.push(...syncResultToResources(repoName, repoConfig, result));
136
+ }
137
+ catch (error) {
138
+ logger.error(current, repoName, String(error));
139
+ results.push(buildErrorResult(repoName, error));
140
+ plan.errors.push({
141
+ repo: repoName,
142
+ message: error instanceof Error ? error.message : String(error),
143
+ });
144
+ }
145
+ }
146
+ console.log("");
147
+ printPlan(plan);
148
+ writePlanSummary(plan, {
149
+ title: "Config Sync Summary",
150
+ dryRun: options.dryRun ?? false,
151
+ });
152
+ if (plan.errors && plan.errors.length > 0) {
153
+ process.exit(1);
154
+ }
155
+ }
@@ -0,0 +1,45 @@
1
+ import { RepoConfig } from "../config/index.js";
2
+ import { RepoInfo } from "../shared/repo-detector.js";
3
+ import { type ProcessorResult, type ProcessorOptions } from "../sync/index.js";
4
+ import { RulesetProcessorOptions, RulesetProcessorResult } from "../settings/rulesets/processor.js";
5
+ import { type IRepoSettingsProcessor } from "../settings/repo-settings/processor.js";
6
+ /**
7
+ * Processor interface for dependency injection in tests.
8
+ */
9
+ export interface IRepositoryProcessor {
10
+ process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: ProcessorOptions): Promise<ProcessorResult>;
11
+ updateManifestOnly(repoInfo: RepoInfo, repoConfig: RepoConfig, options: ProcessorOptions, manifestUpdate: {
12
+ rulesets: string[];
13
+ }): Promise<ProcessorResult>;
14
+ }
15
+ /**
16
+ * Factory function type for creating processors.
17
+ */
18
+ export type ProcessorFactory = () => IRepositoryProcessor;
19
+ /**
20
+ * Default factory that creates a real RepositoryProcessor.
21
+ */
22
+ export declare const defaultProcessorFactory: ProcessorFactory;
23
+ /**
24
+ * Ruleset processor interface for dependency injection in tests.
25
+ */
26
+ export interface IRulesetProcessor {
27
+ process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: RulesetProcessorOptions): Promise<RulesetProcessorResult>;
28
+ }
29
+ /**
30
+ * Factory function type for creating ruleset processors.
31
+ */
32
+ export type RulesetProcessorFactory = () => IRulesetProcessor;
33
+ /**
34
+ * Default factory that creates a real RulesetProcessor.
35
+ */
36
+ export declare const defaultRulesetProcessorFactory: RulesetProcessorFactory;
37
+ /**
38
+ * Repo settings processor factory function type.
39
+ */
40
+ export type RepoSettingsProcessorFactory = () => IRepoSettingsProcessor;
41
+ /**
42
+ * Default factory that creates a real RepoSettingsProcessor.
43
+ */
44
+ export declare const defaultRepoSettingsProcessorFactory: RepoSettingsProcessorFactory;
45
+ export type { IRepoSettingsProcessor };
@@ -0,0 +1,15 @@
1
+ import { RepositoryProcessor, } from "../sync/index.js";
2
+ import { RulesetProcessor, } from "../settings/rulesets/processor.js";
3
+ import { RepoSettingsProcessor, } from "../settings/repo-settings/processor.js";
4
+ /**
5
+ * Default factory that creates a real RepositoryProcessor.
6
+ */
7
+ export const defaultProcessorFactory = () => new RepositoryProcessor();
8
+ /**
9
+ * Default factory that creates a real RulesetProcessor.
10
+ */
11
+ export const defaultRulesetProcessorFactory = () => new RulesetProcessor();
12
+ /**
13
+ * Default factory that creates a real RepoSettingsProcessor.
14
+ */
15
+ export const defaultRepoSettingsProcessorFactory = () => new RepoSettingsProcessor();
package/dist/cli.js CHANGED
@@ -1,20 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import { program } from "./index.js";
3
- // Handle backwards compatibility: if no subcommand is provided, default to sync
4
- // This maintains compatibility with existing usage like `xfg -c config.yaml`
5
- const args = process.argv.slice(2);
6
- const subcommands = ["sync", "settings", "help"];
7
- const versionFlags = ["-V", "--version"];
8
- // Check if the first argument is a subcommand or version flag
9
- const firstArg = args[0];
10
- const isSubcommand = firstArg && subcommands.includes(firstArg);
11
- const isVersionFlag = firstArg && versionFlags.includes(firstArg);
12
- if (isSubcommand || isVersionFlag) {
13
- // Explicit subcommand or version flag - parse normally
14
- program.parse();
15
- }
16
- else {
17
- // No subcommand - prepend 'sync' for backwards compatibility
18
- // This handles: `xfg -c config.yaml`, `xfg --help`, `xfg` (no args)
19
- program.parse(["node", "xfg", "sync", ...args]);
20
- }
2
+ import { program } from "./cli/program.js";
3
+ program.parse();
@@ -1,4 +1,4 @@
1
- import type { ContentValue, RawConfig } from "./config.js";
1
+ import type { ContentValue, RawConfig } from "./types.js";
2
2
  export interface FileReferenceOptions {
3
3
  configDir: string;
4
4
  }
@@ -0,0 +1,7 @@
1
+ export type { MergeMode, MergeStrategy, PRMergeOptions, RulesetTarget, RulesetEnforcement, BypassActorType, BypassMode, PatternOperator, MergeMethod, AlertsThreshold, SecurityAlertsThreshold, BypassActor, RefNameCondition, RulesetConditions, StatusCheckConfig, RequiredReviewer, CodeScanningTool, WorkflowConfig, PullRequestRuleParameters, RequiredStatusChecksParameters, UpdateRuleParameters, RequiredDeploymentsParameters, CodeScanningParameters, CodeQualityParameters, WorkflowsParameters, PatternRuleParameters, FilePathRestrictionParameters, FileExtensionRestrictionParameters, MaxFilePathLengthParameters, MaxFileSizeParameters, PullRequestRule, RequiredStatusChecksRule, RequiredSignaturesRule, RequiredLinearHistoryRule, NonFastForwardRule, CreationRule, UpdateRule, DeletionRule, RequiredDeploymentsRule, CodeScanningRule, CodeQualityRule, WorkflowsRule, CommitAuthorEmailPatternRule, CommitMessagePatternRule, CommitterEmailPatternRule, BranchNamePatternRule, TagNamePatternRule, FilePathRestrictionRule, FileExtensionRestrictionRule, MaxFilePathLengthRule, MaxFileSizeRule, RulesetRule, Ruleset, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage, RepoVisibility, GitHubRepoSettings, RepoSettings, ContentValue, RawFileConfig, RawRepoFileOverride, RawRepoSettings, RawRepoConfig, RawConfig, FileContent, RepoConfig, Config, } from "./types.js";
2
+ export { RULESET_FIELD_MAP, RULESET_COMPARABLE_FIELDS } from "./types.js";
3
+ export { loadRawConfig, loadConfig, normalizeConfig } from "./loader.js";
4
+ export { convertContentToString, detectOutputFormat, type OutputFormat, type ConvertOptions, } from "./formatter.js";
5
+ export { isFileReference, resolveFileReference, type FileReferenceOptions, } from "./file-reference-resolver.js";
6
+ export { arrayMergeStrategies, deepMerge, stripMergeDirectives, createMergeContext, isTextContent, mergeTextContent, type ArrayMergeStrategy, type ArrayMergeHandler, type MergeContext, } from "./merge.js";
7
+ export { validateRawConfig, validateSettings, validateForSync, validateForSettings, hasActionableSettings, } from "./validator.js";
@@ -0,0 +1,12 @@
1
+ // Re-export values (non-type exports)
2
+ export { RULESET_FIELD_MAP, RULESET_COMPARABLE_FIELDS } from "./types.js";
3
+ // Re-export loading functions
4
+ export { loadRawConfig, loadConfig, normalizeConfig } from "./loader.js";
5
+ // Config formatting
6
+ export { convertContentToString, detectOutputFormat, } from "./formatter.js";
7
+ // File reference resolution
8
+ export { isFileReference, resolveFileReference, } from "./file-reference-resolver.js";
9
+ // Deep merge utilities
10
+ export { arrayMergeStrategies, deepMerge, stripMergeDirectives, createMergeContext, isTextContent, mergeTextContent, } from "./merge.js";
11
+ // Validation
12
+ export { validateRawConfig, validateSettings, validateForSync, validateForSettings, hasActionableSettings, } from "./validator.js";
@@ -0,0 +1,9 @@
1
+ import { normalizeConfig as normalizeConfigInternal } from "./normalizer.js";
2
+ import type { RawConfig, Config } from "./types.js";
3
+ export { normalizeConfigInternal as normalizeConfig };
4
+ /**
5
+ * Load and validate raw config without normalization.
6
+ * Use this when you need to perform command-specific validation before normalizing.
7
+ */
8
+ export declare function loadRawConfig(filePath: string): RawConfig;
9
+ export declare function loadConfig(filePath: string): Config;