@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,300 @@
1
+ import { appendFileSync } from "node:fs";
2
+ import chalk from "chalk";
3
+ import { formatPropertyTree, } from "../settings/rulesets/formatter.js";
4
+ // =============================================================================
5
+ // Helpers
6
+ // =============================================================================
7
+ function formatValue(val) {
8
+ if (val === null)
9
+ return "null";
10
+ if (val === undefined)
11
+ return "undefined";
12
+ if (typeof val === "string")
13
+ return `"${val}"`;
14
+ if (typeof val === "boolean")
15
+ return val ? "true" : "false";
16
+ return String(val);
17
+ }
18
+ function formatRulesetConfig(config, indent) {
19
+ const lines = [];
20
+ function renderObject(obj, currentIndent) {
21
+ for (const [k, v] of Object.entries(obj)) {
22
+ renderValue(k, v, currentIndent);
23
+ }
24
+ }
25
+ function renderValue(key, value, currentIndent) {
26
+ const pad = " ".repeat(currentIndent);
27
+ if (value === null || value === undefined)
28
+ return;
29
+ if (Array.isArray(value)) {
30
+ if (value.length === 0) {
31
+ lines.push(chalk.green(`${pad}+ ${key}: []`));
32
+ }
33
+ else if (value.every((v) => typeof v !== "object")) {
34
+ lines.push(chalk.green(`${pad}+ ${key}: [${value.map((v) => (typeof v === "string" ? `"${v}"` : String(v))).join(", ")}]`));
35
+ }
36
+ else {
37
+ lines.push(chalk.green(`${pad}+ ${key}:`));
38
+ for (let i = 0; i < value.length; i++) {
39
+ const item = value[i];
40
+ if (typeof item === "object" && item !== null) {
41
+ const obj = item;
42
+ const typeLabel = "type" in obj ? ` (${obj.type})` : "";
43
+ lines.push(chalk.green(`${pad} + [${i}]${typeLabel}:`));
44
+ renderObject(obj, currentIndent + 2);
45
+ }
46
+ else {
47
+ lines.push(chalk.green(`${pad} + ${formatValue(item)}`));
48
+ }
49
+ }
50
+ }
51
+ }
52
+ else if (typeof value === "object") {
53
+ lines.push(chalk.green(`${pad}+ ${key}:`));
54
+ renderObject(value, currentIndent + 1);
55
+ }
56
+ else {
57
+ lines.push(chalk.green(`${pad}+ ${key}: ${formatValue(value)}`));
58
+ }
59
+ }
60
+ for (const [key, value] of Object.entries(config)) {
61
+ if (key === "name")
62
+ continue; // Name is in the header
63
+ renderValue(key, value, indent);
64
+ }
65
+ return lines;
66
+ }
67
+ function formatSummary(totals) {
68
+ const parts = [];
69
+ const settingsTotal = totals.settings.add + totals.settings.change;
70
+ const rulesetsTotal = totals.rulesets.create + totals.rulesets.update + totals.rulesets.delete;
71
+ if (settingsTotal > 0) {
72
+ const settingWord = settingsTotal === 1 ? "setting" : "settings";
73
+ const actions = [];
74
+ if (totals.settings.add > 0)
75
+ actions.push(`${totals.settings.add} to add`);
76
+ if (totals.settings.change > 0)
77
+ actions.push(`${totals.settings.change} to change`);
78
+ parts.push(`${settingsTotal} ${settingWord} (${actions.join(", ")})`);
79
+ }
80
+ if (rulesetsTotal > 0) {
81
+ const rulesetWord = rulesetsTotal === 1 ? "ruleset" : "rulesets";
82
+ const actions = [];
83
+ if (totals.rulesets.create > 0)
84
+ actions.push(`${totals.rulesets.create} to create`);
85
+ if (totals.rulesets.update > 0)
86
+ actions.push(`${totals.rulesets.update} to update`);
87
+ if (totals.rulesets.delete > 0)
88
+ actions.push(`${totals.rulesets.delete} to delete`);
89
+ parts.push(`${rulesetsTotal} ${rulesetWord} (${actions.join(", ")})`);
90
+ }
91
+ if (parts.length === 0) {
92
+ return "No changes";
93
+ }
94
+ return `Plan: ${parts.join(", ")}`;
95
+ }
96
+ // =============================================================================
97
+ // CLI Formatter
98
+ // =============================================================================
99
+ export function formatSettingsReportCLI(report) {
100
+ const lines = [];
101
+ for (const repo of report.repos) {
102
+ if (repo.settings.length === 0 &&
103
+ repo.rulesets.length === 0 &&
104
+ !repo.error) {
105
+ continue;
106
+ }
107
+ // Repo header
108
+ lines.push(chalk.yellow(`~ ${repo.repoName}`));
109
+ // Settings
110
+ for (const setting of repo.settings) {
111
+ // Skip settings where both values are undefined
112
+ if (setting.oldValue === undefined && setting.newValue === undefined) {
113
+ continue;
114
+ }
115
+ if (setting.action === "add") {
116
+ lines.push(chalk.green(` + ${setting.name}: ${formatValue(setting.newValue)}`));
117
+ }
118
+ else {
119
+ lines.push(chalk.yellow(` ~ ${setting.name}: ${formatValue(setting.oldValue)} → ${formatValue(setting.newValue)}`));
120
+ }
121
+ }
122
+ // Rulesets
123
+ for (const ruleset of repo.rulesets) {
124
+ if (ruleset.action === "create") {
125
+ lines.push(chalk.green(` + ruleset "${ruleset.name}"`));
126
+ if (ruleset.config) {
127
+ lines.push(...formatRulesetConfig(ruleset.config, 2));
128
+ }
129
+ }
130
+ else if (ruleset.action === "update") {
131
+ lines.push(chalk.yellow(` ~ ruleset "${ruleset.name}"`));
132
+ if (ruleset.propertyDiffs && ruleset.propertyDiffs.length > 0) {
133
+ const treeLines = formatPropertyTree(ruleset.propertyDiffs);
134
+ for (const line of treeLines) {
135
+ lines.push(` ${line}`);
136
+ }
137
+ }
138
+ }
139
+ else if (ruleset.action === "delete") {
140
+ lines.push(chalk.red(` - ruleset "${ruleset.name}"`));
141
+ }
142
+ }
143
+ // Error
144
+ if (repo.error) {
145
+ lines.push(chalk.red(` Error: ${repo.error}`));
146
+ }
147
+ lines.push(""); // Blank line between repos
148
+ }
149
+ // Summary
150
+ lines.push(formatSummary(report.totals));
151
+ return lines;
152
+ }
153
+ // =============================================================================
154
+ // Markdown Formatter
155
+ // =============================================================================
156
+ function formatValuePlain(val) {
157
+ if (val === null)
158
+ return "null";
159
+ if (val === undefined)
160
+ return "undefined";
161
+ if (typeof val === "string")
162
+ return `"${val}"`;
163
+ if (typeof val === "boolean")
164
+ return val ? "true" : "false";
165
+ return String(val);
166
+ }
167
+ function formatRulesetConfigPlain(config, indent) {
168
+ const lines = [];
169
+ function renderObject(obj, currentIndent) {
170
+ for (const [k, v] of Object.entries(obj)) {
171
+ renderValue(k, v, currentIndent);
172
+ }
173
+ }
174
+ function renderValue(key, value, currentIndent) {
175
+ const pad = " ".repeat(currentIndent);
176
+ if (value === null || value === undefined)
177
+ return;
178
+ if (Array.isArray(value)) {
179
+ if (value.length === 0) {
180
+ lines.push(`${pad}+ ${key}: []`);
181
+ }
182
+ else if (value.every((v) => typeof v !== "object")) {
183
+ lines.push(`${pad}+ ${key}: [${value.map((v) => (typeof v === "string" ? `"${v}"` : String(v))).join(", ")}]`);
184
+ }
185
+ else {
186
+ lines.push(`${pad}+ ${key}:`);
187
+ for (let i = 0; i < value.length; i++) {
188
+ const item = value[i];
189
+ if (typeof item === "object" && item !== null) {
190
+ const obj = item;
191
+ const typeLabel = "type" in obj ? ` (${obj.type})` : "";
192
+ lines.push(`${pad} + [${i}]${typeLabel}:`);
193
+ renderObject(obj, currentIndent + 2);
194
+ }
195
+ else {
196
+ lines.push(`${pad} + ${formatValuePlain(item)}`);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ else if (typeof value === "object") {
202
+ lines.push(`${pad}+ ${key}:`);
203
+ renderObject(value, currentIndent + 1);
204
+ }
205
+ else {
206
+ lines.push(`${pad}+ ${key}: ${formatValuePlain(value)}`);
207
+ }
208
+ }
209
+ for (const [key, value] of Object.entries(config)) {
210
+ if (key === "name")
211
+ continue;
212
+ renderValue(key, value, indent);
213
+ }
214
+ return lines;
215
+ }
216
+ export function formatSettingsReportMarkdown(report, dryRun) {
217
+ const lines = [];
218
+ // Title
219
+ const titleSuffix = dryRun ? " (Dry Run)" : "";
220
+ lines.push(`## Repository Settings Summary${titleSuffix}`);
221
+ lines.push("");
222
+ // Dry-run warning
223
+ if (dryRun) {
224
+ lines.push("> [!WARNING]");
225
+ lines.push("> This was a dry run — no changes were applied");
226
+ lines.push("");
227
+ }
228
+ // Diff block
229
+ const diffLines = [];
230
+ for (const repo of report.repos) {
231
+ if (repo.settings.length === 0 &&
232
+ repo.rulesets.length === 0 &&
233
+ !repo.error) {
234
+ continue;
235
+ }
236
+ diffLines.push(`~ ${repo.repoName}`);
237
+ for (const setting of repo.settings) {
238
+ // Skip settings where both values are undefined
239
+ if (setting.oldValue === undefined && setting.newValue === undefined) {
240
+ continue;
241
+ }
242
+ if (setting.action === "add") {
243
+ diffLines.push(` + ${setting.name}: ${formatValuePlain(setting.newValue)}`);
244
+ }
245
+ else {
246
+ diffLines.push(` ~ ${setting.name}: ${formatValuePlain(setting.oldValue)} → ${formatValuePlain(setting.newValue)}`);
247
+ }
248
+ }
249
+ for (const ruleset of repo.rulesets) {
250
+ if (ruleset.action === "create") {
251
+ diffLines.push(` + ruleset "${ruleset.name}"`);
252
+ if (ruleset.config) {
253
+ diffLines.push(...formatRulesetConfigPlain(ruleset.config, 2));
254
+ }
255
+ }
256
+ else if (ruleset.action === "update") {
257
+ diffLines.push(` ~ ruleset "${ruleset.name}"`);
258
+ if (ruleset.propertyDiffs && ruleset.propertyDiffs.length > 0) {
259
+ for (const diff of ruleset.propertyDiffs) {
260
+ const path = diff.path.join(".");
261
+ if (diff.action === "add") {
262
+ diffLines.push(` + ${path}: ${formatValuePlain(diff.newValue)}`);
263
+ }
264
+ else if (diff.action === "change") {
265
+ diffLines.push(` ~ ${path}: ${formatValuePlain(diff.oldValue)} → ${formatValuePlain(diff.newValue)}`);
266
+ }
267
+ else if (diff.action === "remove") {
268
+ diffLines.push(` - ${path}`);
269
+ }
270
+ }
271
+ }
272
+ }
273
+ else if (ruleset.action === "delete") {
274
+ diffLines.push(` - ruleset "${ruleset.name}"`);
275
+ }
276
+ }
277
+ if (repo.error) {
278
+ diffLines.push(` ! Error: ${repo.error}`);
279
+ }
280
+ }
281
+ if (diffLines.length > 0) {
282
+ lines.push("```diff");
283
+ lines.push(...diffLines);
284
+ lines.push("```");
285
+ lines.push("");
286
+ }
287
+ // Summary
288
+ lines.push(`**${formatSummary(report.totals)}**`);
289
+ return lines.join("\n");
290
+ }
291
+ // =============================================================================
292
+ // File Writer
293
+ // =============================================================================
294
+ export function writeSettingsReportSummary(report, dryRun) {
295
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
296
+ if (!summaryPath)
297
+ return;
298
+ const markdown = formatSettingsReportMarkdown(report, dryRun);
299
+ appendFileSync(summaryPath, "\n" + markdown + "\n");
300
+ }
@@ -1,7 +1,7 @@
1
- import { ProcessorResult } from "./repository-processor.js";
2
- import { RepoConfig } from "./config.js";
1
+ import type { ProcessorResult } from "../sync/index.js";
2
+ import { RepoConfig } from "../config/index.js";
3
3
  import { MergeOutcome, FileChanges, RepoResult } from "./github-summary.js";
4
- import { DiffStats } from "./diff-utils.js";
4
+ import { DiffStats } from "../sync/diff-utils.js";
5
5
  /**
6
6
  * Determine merge outcome from repo config and processor result
7
7
  */
@@ -0,0 +1,24 @@
1
+ export interface SyncReport {
2
+ repos: RepoFileChanges[];
3
+ totals: {
4
+ files: {
5
+ create: number;
6
+ update: number;
7
+ delete: number;
8
+ };
9
+ };
10
+ }
11
+ export interface RepoFileChanges {
12
+ repoName: string;
13
+ files: FileChange[];
14
+ prUrl?: string;
15
+ mergeOutcome?: "manual" | "auto" | "force" | "direct";
16
+ error?: string;
17
+ }
18
+ export interface FileChange {
19
+ path: string;
20
+ action: "create" | "update" | "delete";
21
+ }
22
+ export declare function formatSyncReportCLI(report: SyncReport): string[];
23
+ export declare function formatSyncReportMarkdown(report: SyncReport, dryRun: boolean): string;
24
+ export declare function writeSyncReportSummary(report: SyncReport, dryRun: boolean): void;
@@ -0,0 +1,99 @@
1
+ // src/output/sync-report.ts
2
+ import { appendFileSync } from "node:fs";
3
+ import chalk from "chalk";
4
+ function formatSummary(totals) {
5
+ const total = totals.files.create + totals.files.update + totals.files.delete;
6
+ if (total === 0) {
7
+ return "No changes";
8
+ }
9
+ const parts = [];
10
+ if (totals.files.create > 0)
11
+ parts.push(`${totals.files.create} to create`);
12
+ if (totals.files.update > 0)
13
+ parts.push(`${totals.files.update} to update`);
14
+ if (totals.files.delete > 0)
15
+ parts.push(`${totals.files.delete} to delete`);
16
+ const fileWord = total === 1 ? "file" : "files";
17
+ return `Plan: ${total} ${fileWord} (${parts.join(", ")})`;
18
+ }
19
+ export function formatSyncReportCLI(report) {
20
+ const lines = [];
21
+ for (const repo of report.repos) {
22
+ if (repo.files.length === 0 && !repo.error) {
23
+ continue;
24
+ }
25
+ // Repo header
26
+ lines.push(chalk.yellow(`~ ${repo.repoName}`));
27
+ // Files
28
+ for (const file of repo.files) {
29
+ if (file.action === "create") {
30
+ lines.push(chalk.green(` + ${file.path}`));
31
+ }
32
+ else if (file.action === "update") {
33
+ lines.push(chalk.yellow(` ~ ${file.path}`));
34
+ }
35
+ else if (file.action === "delete") {
36
+ lines.push(chalk.red(` - ${file.path}`));
37
+ }
38
+ }
39
+ // Error
40
+ if (repo.error) {
41
+ lines.push(chalk.red(` Error: ${repo.error}`));
42
+ }
43
+ lines.push(""); // Blank line between repos
44
+ }
45
+ // Summary
46
+ lines.push(formatSummary(report.totals));
47
+ return lines;
48
+ }
49
+ export function formatSyncReportMarkdown(report, dryRun) {
50
+ const lines = [];
51
+ // Title
52
+ const titleSuffix = dryRun ? " (Dry Run)" : "";
53
+ lines.push(`## Config Sync Summary${titleSuffix}`);
54
+ lines.push("");
55
+ // Dry-run warning
56
+ if (dryRun) {
57
+ lines.push("> [!WARNING]");
58
+ lines.push("> This was a dry run — no changes were applied");
59
+ lines.push("");
60
+ }
61
+ // Diff block
62
+ const diffLines = [];
63
+ for (const repo of report.repos) {
64
+ if (repo.files.length === 0 && !repo.error) {
65
+ continue;
66
+ }
67
+ diffLines.push(`~ ${repo.repoName}`);
68
+ for (const file of repo.files) {
69
+ if (file.action === "create") {
70
+ diffLines.push(` + ${file.path}`);
71
+ }
72
+ else if (file.action === "update") {
73
+ diffLines.push(` ~ ${file.path}`);
74
+ }
75
+ else if (file.action === "delete") {
76
+ diffLines.push(` - ${file.path}`);
77
+ }
78
+ }
79
+ if (repo.error) {
80
+ diffLines.push(` ! Error: ${repo.error}`);
81
+ }
82
+ }
83
+ if (diffLines.length > 0) {
84
+ lines.push("```diff");
85
+ lines.push(...diffLines);
86
+ lines.push("```");
87
+ lines.push("");
88
+ }
89
+ // Summary
90
+ lines.push(`**${formatSummary(report.totals)}**`);
91
+ return lines.join("\n");
92
+ }
93
+ export function writeSyncReportSummary(report, dryRun) {
94
+ const summaryPath = process.env.GITHUB_STEP_SUMMARY;
95
+ if (!summaryPath)
96
+ return;
97
+ const markdown = formatSyncReportMarkdown(report, dryRun);
98
+ appendFileSync(summaryPath, "\n" + markdown + "\n");
99
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./rulesets/index.js";
2
+ export * from "./repo-settings/index.js";
@@ -0,0 +1,4 @@
1
+ // Rulesets
2
+ export * from "./rulesets/index.js";
3
+ // Repo settings
4
+ export * from "./repo-settings/index.js";
@@ -1,5 +1,5 @@
1
- import type { GitHubRepoSettings } from "./config.js";
2
- import type { CurrentRepoSettings } from "./strategies/repo-settings-strategy.js";
1
+ import type { GitHubRepoSettings } from "../../config/index.js";
2
+ import type { CurrentRepoSettings } from "./types.js";
3
3
  export type RepoSettingsAction = "add" | "change" | "unchanged";
4
4
  export interface RepoSettingsChange {
5
5
  property: keyof GitHubRepoSettings;
@@ -1,7 +1,9 @@
1
- import type { RepoSettingsChange } from "./repo-settings-diff.js";
1
+ import type { RepoSettingsChange } from "./diff.js";
2
2
  export interface RepoSettingsPlanEntry {
3
3
  property: string;
4
4
  action: "add" | "change";
5
+ oldValue?: unknown;
6
+ newValue?: unknown;
5
7
  }
6
8
  export interface RepoSettingsPlanResult {
7
9
  lines: string[];
@@ -54,12 +54,21 @@ export function formatRepoSettingsPlan(changes) {
54
54
  if (change.action === "add") {
55
55
  lines.push(chalk.green(` + ${change.property}: ${formatValue(change.newValue)}`));
56
56
  adds++;
57
- entries.push({ property: change.property, action: "add" });
57
+ entries.push({
58
+ property: change.property,
59
+ action: "add",
60
+ newValue: change.newValue,
61
+ });
58
62
  }
59
63
  else if (change.action === "change") {
60
64
  lines.push(chalk.yellow(` ~ ${change.property}: ${formatValue(change.oldValue)} → ${formatValue(change.newValue)}`));
61
65
  changesCount++;
62
- entries.push({ property: change.property, action: "change" });
66
+ entries.push({
67
+ property: change.property,
68
+ action: "change",
69
+ oldValue: change.oldValue,
70
+ newValue: change.newValue,
71
+ });
63
72
  }
64
73
  }
65
74
  return { lines, adds, changes: changesCount, warnings, entries };
@@ -1,7 +1,7 @@
1
- import { ICommandExecutor } from "../command-executor.js";
2
- import { RepoInfo } from "../repo-detector.js";
3
- import type { GitHubRepoSettings } from "../config.js";
4
- import type { IRepoSettingsStrategy, RepoSettingsStrategyOptions, CurrentRepoSettings } from "./repo-settings-strategy.js";
1
+ import { ICommandExecutor } from "../../shared/command-executor.js";
2
+ import { RepoInfo } from "../../shared/repo-detector.js";
3
+ import type { GitHubRepoSettings } from "../../config/index.js";
4
+ import type { IRepoSettingsStrategy, RepoSettingsStrategyOptions, CurrentRepoSettings } from "./types.js";
5
5
  /**
6
6
  * GitHub Repository Settings Strategy.
7
7
  * Manages repository settings via GitHub REST API using `gh api` CLI.
@@ -1,6 +1,6 @@
1
- import { defaultExecutor } from "../command-executor.js";
2
- import { isGitHubRepo } from "../repo-detector.js";
3
- import { escapeShellArg } from "../shell-utils.js";
1
+ import { defaultExecutor, } from "../../shared/command-executor.js";
2
+ import { isGitHubRepo, } from "../../shared/repo-detector.js";
3
+ import { escapeShellArg } from "../../shared/shell-utils.js";
4
4
  /**
5
5
  * Converts camelCase to snake_case.
6
6
  */
@@ -0,0 +1,5 @@
1
+ export { isRepoSettingsStrategy, type IRepoSettingsStrategy, type RepoSettingsStrategyOptions, type CurrentRepoSettings, } from "./types.js";
2
+ export { RepoSettingsProcessor, type IRepoSettingsProcessor, type RepoSettingsProcessorOptions, type RepoSettingsProcessorResult, } from "./processor.js";
3
+ export { diffRepoSettings, hasChanges, type RepoSettingsAction, type RepoSettingsChange, } from "./diff.js";
4
+ export { formatRepoSettingsPlan, type RepoSettingsPlanResult, type RepoSettingsPlanEntry, } from "./formatter.js";
5
+ export { GitHubRepoSettingsStrategy } from "./github-repo-settings-strategy.js";
@@ -0,0 +1,10 @@
1
+ // Types
2
+ export { isRepoSettingsStrategy, } from "./types.js";
3
+ // Repo settings processor
4
+ export { RepoSettingsProcessor, } from "./processor.js";
5
+ // Repo settings diff
6
+ export { diffRepoSettings, hasChanges, } from "./diff.js";
7
+ // Repo settings formatter
8
+ export { formatRepoSettingsPlan, } from "./formatter.js";
9
+ // Repo settings strategies
10
+ export { GitHubRepoSettingsStrategy } from "./github-repo-settings-strategy.js";
@@ -1,7 +1,7 @@
1
- import type { RepoConfig } from "./config.js";
2
- import type { RepoInfo } from "./repo-detector.js";
3
- import type { IRepoSettingsStrategy } from "./strategies/repo-settings-strategy.js";
4
- import { RepoSettingsPlanResult } from "./repo-settings-plan-formatter.js";
1
+ import type { RepoConfig } from "../../config/index.js";
2
+ import type { RepoInfo } from "../../shared/repo-detector.js";
3
+ import type { IRepoSettingsStrategy } from "./types.js";
4
+ import { RepoSettingsPlanResult } from "./formatter.js";
5
5
  export interface IRepoSettingsProcessor {
6
6
  process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: RepoSettingsProcessorOptions): Promise<RepoSettingsProcessorResult>;
7
7
  }
@@ -1,9 +1,9 @@
1
- import { isGitHubRepo, getRepoDisplayName } from "./repo-detector.js";
2
- import { GitHubRepoSettingsStrategy } from "./strategies/github-repo-settings-strategy.js";
3
- import { diffRepoSettings, hasChanges } from "./repo-settings-diff.js";
4
- import { formatRepoSettingsPlan, } from "./repo-settings-plan-formatter.js";
5
- import { hasGitHubAppCredentials } from "./strategies/index.js";
6
- import { GitHubAppTokenManager } from "./github-app-token-manager.js";
1
+ import { isGitHubRepo, getRepoDisplayName, } from "../../shared/repo-detector.js";
2
+ import { GitHubRepoSettingsStrategy } from "./github-repo-settings-strategy.js";
3
+ import { diffRepoSettings, hasChanges } from "./diff.js";
4
+ import { formatRepoSettingsPlan } from "./formatter.js";
5
+ import { hasGitHubAppCredentials } from "../../vcs/index.js";
6
+ import { GitHubAppTokenManager } from "../../vcs/github-app-token-manager.js";
7
7
  export class RepoSettingsProcessor {
8
8
  strategy;
9
9
  tokenManager;
@@ -69,8 +69,14 @@ export class RepoSettingsProcessor {
69
69
  planOutput,
70
70
  };
71
71
  }
72
- // Apply changes
73
- await this.applyChanges(githubRepo, desiredSettings, strategyOptions);
72
+ // Apply changes - only send settings that actually changed
73
+ const changedSettings = changes.reduce((acc, change) => {
74
+ if (change.action !== "unchanged") {
75
+ acc[change.property] = change.newValue;
76
+ }
77
+ return acc;
78
+ }, {});
79
+ await this.applyChanges(githubRepo, changedSettings, strategyOptions);
74
80
  return {
75
81
  success: true,
76
82
  repoName,
@@ -1,5 +1,5 @@
1
- import type { RepoInfo } from "../repo-detector.js";
2
- import type { GitHubRepoSettings } from "../config.js";
1
+ import type { RepoInfo } from "../../shared/repo-detector.js";
2
+ import type { GitHubRepoSettings } from "../../config/index.js";
3
3
  export interface RepoSettingsStrategyOptions {
4
4
  token?: string;
5
5
  host?: string;
@@ -0,0 +1,18 @@
1
+ export type DiffAction = "add" | "change" | "remove";
2
+ export interface PropertyDiff {
3
+ path: string[];
4
+ action: DiffAction;
5
+ oldValue?: unknown;
6
+ newValue?: unknown;
7
+ }
8
+ export declare function isObject(val: unknown): val is Record<string, unknown>;
9
+ export declare function deepEqual(a: unknown, b: unknown): boolean;
10
+ export declare function isArrayOfObjects(arr: unknown[]): boolean;
11
+ /**
12
+ * Recursively compute property-level diffs between two objects.
13
+ */
14
+ export declare function computePropertyDiffs(current: Record<string, unknown>, desired: Record<string, unknown>, parentPath?: string[]): PropertyDiff[];
15
+ /**
16
+ * Diff two arrays of objects by matching items on `type` field (or by index).
17
+ */
18
+ export declare function diffObjectArrays(currentArr: unknown[], desiredArr: unknown[], parentPath: string[]): PropertyDiff[];