@aspruyt/xfg 3.13.1 → 4.0.1

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 (190) hide show
  1. package/README.md +1 -4
  2. package/dist/cli/index.d.ts +1 -4
  3. package/dist/cli/index.js +0 -2
  4. package/dist/cli/program.js +7 -14
  5. package/dist/cli/{settings/results-collector.d.ts → results-collector.d.ts} +1 -1
  6. package/dist/cli/{settings/results-collector.js → results-collector.js} +2 -1
  7. package/dist/cli/settings-report-builder.d.ts +1 -3
  8. package/dist/cli/sync-command.d.ts +3 -26
  9. package/dist/cli/sync-command.js +312 -179
  10. package/dist/cli/types.d.ts +68 -41
  11. package/dist/cli/types.js +1 -12
  12. package/dist/config/errors.d.ts +9 -0
  13. package/dist/config/errors.js +11 -0
  14. package/dist/config/file-reference-resolver.d.ts +2 -1
  15. package/dist/config/file-reference-resolver.js +10 -8
  16. package/dist/config/formatter.d.ts +3 -2
  17. package/dist/config/index.d.ts +4 -6
  18. package/dist/config/index.js +4 -8
  19. package/dist/config/loader.js +4 -2
  20. package/dist/config/merge.d.ts +0 -9
  21. package/dist/config/merge.js +2 -7
  22. package/dist/config/normalizer.d.ts +4 -0
  23. package/dist/config/normalizer.js +61 -110
  24. package/dist/config/types.d.ts +15 -19
  25. package/dist/config/types.js +1 -1
  26. package/dist/config/validator.d.ts +0 -9
  27. package/dist/config/validator.js +297 -391
  28. package/dist/config/validators/file-validator.d.ts +2 -8
  29. package/dist/config/validators/file-validator.js +6 -17
  30. package/dist/config/validators/index.d.ts +3 -3
  31. package/dist/config/validators/index.js +3 -3
  32. package/dist/config/validators/repo-settings-validator.d.ts +0 -6
  33. package/dist/config/validators/repo-settings-validator.js +9 -9
  34. package/dist/config/validators/ruleset-validator.d.ts +0 -14
  35. package/dist/config/validators/ruleset-validator.js +28 -28
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.js +1 -1
  38. package/dist/lifecycle/ado-migration-source.js +2 -1
  39. package/dist/lifecycle/github-lifecycle-provider.d.ts +15 -5
  40. package/dist/lifecycle/github-lifecycle-provider.js +101 -81
  41. package/dist/lifecycle/index.d.ts +2 -6
  42. package/dist/lifecycle/index.js +0 -4
  43. package/dist/lifecycle/lifecycle-formatter.d.ts +2 -1
  44. package/dist/lifecycle/lifecycle-formatter.js +4 -0
  45. package/dist/lifecycle/lifecycle-helpers.d.ts +3 -2
  46. package/dist/lifecycle/repo-lifecycle-manager.js +4 -11
  47. package/dist/lifecycle/types.d.ts +0 -8
  48. package/dist/output/github-summary.d.ts +5 -0
  49. package/dist/output/github-summary.js +9 -2
  50. package/dist/output/index.d.ts +2 -2
  51. package/dist/output/index.js +1 -1
  52. package/dist/output/lifecycle-report.js +5 -23
  53. package/dist/output/settings-report.d.ts +14 -3
  54. package/dist/output/settings-report.js +137 -197
  55. package/dist/output/summary-utils.d.ts +1 -1
  56. package/dist/output/summary-utils.js +2 -1
  57. package/dist/output/sync-report.js +5 -8
  58. package/dist/output/unified-summary.d.ts +2 -1
  59. package/dist/output/unified-summary.js +71 -133
  60. package/dist/settings/base-processor.d.ts +67 -0
  61. package/dist/settings/base-processor.js +91 -0
  62. package/dist/settings/index.d.ts +4 -3
  63. package/dist/settings/index.js +3 -3
  64. package/dist/settings/labels/converter.d.ts +2 -1
  65. package/dist/settings/labels/diff.d.ts +2 -2
  66. package/dist/settings/labels/diff.js +15 -19
  67. package/dist/settings/labels/github-labels-strategy.d.ts +9 -18
  68. package/dist/settings/labels/github-labels-strategy.js +17 -73
  69. package/dist/settings/labels/index.d.ts +2 -6
  70. package/dist/settings/labels/index.js +1 -9
  71. package/dist/settings/labels/processor.d.ts +6 -40
  72. package/dist/settings/labels/processor.js +62 -165
  73. package/dist/settings/labels/types.d.ts +5 -8
  74. package/dist/settings/repo-settings/formatter.d.ts +2 -2
  75. package/dist/settings/repo-settings/formatter.js +6 -6
  76. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +11 -12
  77. package/dist/settings/repo-settings/github-repo-settings-strategy.js +32 -79
  78. package/dist/settings/repo-settings/index.d.ts +2 -5
  79. package/dist/settings/repo-settings/index.js +1 -9
  80. package/dist/settings/repo-settings/processor.d.ts +6 -27
  81. package/dist/settings/repo-settings/processor.js +51 -104
  82. package/dist/settings/repo-settings/types.d.ts +7 -9
  83. package/dist/settings/rulesets/diff-algorithm.d.ts +0 -4
  84. package/dist/settings/rulesets/diff-algorithm.js +1 -10
  85. package/dist/settings/rulesets/diff.d.ts +3 -3
  86. package/dist/settings/rulesets/diff.js +8 -29
  87. package/dist/settings/rulesets/formatter.d.ts +1 -3
  88. package/dist/settings/rulesets/formatter.js +1 -8
  89. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +11 -51
  90. package/dist/settings/rulesets/github-ruleset-strategy.js +24 -85
  91. package/dist/settings/rulesets/index.d.ts +3 -6
  92. package/dist/settings/rulesets/index.js +5 -9
  93. package/dist/settings/rulesets/processor.d.ts +8 -43
  94. package/dist/settings/rulesets/processor.js +58 -166
  95. package/dist/settings/rulesets/types.d.ts +35 -6
  96. package/dist/shared/command-executor.d.ts +2 -22
  97. package/dist/shared/command-executor.js +8 -7
  98. package/dist/shared/env.d.ts +0 -8
  99. package/dist/shared/env.js +14 -70
  100. package/dist/shared/file-status.d.ts +2 -0
  101. package/dist/shared/file-status.js +13 -0
  102. package/dist/shared/gh-api-utils.d.ts +46 -0
  103. package/dist/shared/gh-api-utils.js +107 -0
  104. package/dist/shared/index.d.ts +5 -5
  105. package/dist/shared/index.js +3 -3
  106. package/dist/shared/interpolation-engine.d.ts +31 -0
  107. package/dist/shared/interpolation-engine.js +50 -0
  108. package/dist/shared/logger.d.ts +3 -7
  109. package/dist/shared/logger.js +4 -1
  110. package/dist/shared/repo-detector.d.ts +17 -2
  111. package/dist/shared/repo-detector.js +27 -0
  112. package/dist/shared/retry-utils.d.ts +9 -17
  113. package/dist/shared/retry-utils.js +22 -28
  114. package/dist/shared/sanitize-utils.d.ts +0 -7
  115. package/dist/shared/sanitize-utils.js +0 -7
  116. package/dist/shared/shell-utils.d.ts +1 -0
  117. package/dist/shared/shell-utils.js +3 -0
  118. package/dist/shared/string-utils.d.ts +4 -0
  119. package/dist/shared/string-utils.js +6 -0
  120. package/dist/shared/type-guards.d.ts +17 -0
  121. package/dist/shared/type-guards.js +26 -0
  122. package/dist/shared/workspace-utils.d.ts +0 -4
  123. package/dist/shared/workspace-utils.js +0 -4
  124. package/dist/{sync → shared}/xfg-template.d.ts +3 -2
  125. package/dist/{sync → shared}/xfg-template.js +13 -54
  126. package/dist/sync/auth-options-builder.d.ts +4 -5
  127. package/dist/sync/auth-options-builder.js +15 -26
  128. package/dist/sync/branch-manager.d.ts +5 -0
  129. package/dist/sync/branch-manager.js +12 -10
  130. package/dist/sync/commit-push-manager.d.ts +1 -1
  131. package/dist/sync/commit-push-manager.js +22 -18
  132. package/dist/sync/diff-utils.d.ts +4 -9
  133. package/dist/sync/diff-utils.js +2 -19
  134. package/dist/sync/file-sync-orchestrator.js +9 -8
  135. package/dist/sync/file-writer.d.ts +2 -1
  136. package/dist/sync/file-writer.js +3 -6
  137. package/dist/sync/index.d.ts +2 -16
  138. package/dist/sync/index.js +0 -20
  139. package/dist/sync/manifest-manager.d.ts +4 -0
  140. package/dist/sync/manifest-manager.js +5 -1
  141. package/dist/sync/manifest.d.ts +11 -84
  142. package/dist/sync/manifest.js +50 -215
  143. package/dist/sync/pr-merge-handler.d.ts +2 -6
  144. package/dist/sync/pr-merge-handler.js +6 -3
  145. package/dist/sync/repository-processor.d.ts +2 -8
  146. package/dist/sync/repository-processor.js +21 -63
  147. package/dist/sync/repository-session.js +5 -14
  148. package/dist/sync/sync-workflow.js +31 -38
  149. package/dist/sync/types.d.ts +43 -182
  150. package/dist/vcs/authenticated-git-ops.d.ts +27 -70
  151. package/dist/vcs/authenticated-git-ops.js +70 -96
  152. package/dist/vcs/azure-pr-strategy.d.ts +6 -4
  153. package/dist/vcs/azure-pr-strategy.js +34 -82
  154. package/dist/vcs/branch-utils.d.ts +6 -0
  155. package/dist/vcs/branch-utils.js +29 -0
  156. package/dist/vcs/commit-strategy-selector.d.ts +5 -0
  157. package/dist/vcs/commit-strategy-selector.js +10 -0
  158. package/dist/vcs/git-commit-strategy.js +1 -2
  159. package/dist/vcs/git-ops.d.ts +15 -59
  160. package/dist/vcs/git-ops.js +46 -110
  161. package/dist/vcs/github-app-token-manager.d.ts +0 -6
  162. package/dist/vcs/github-app-token-manager.js +5 -12
  163. package/dist/vcs/github-pr-strategy.d.ts +5 -5
  164. package/dist/vcs/github-pr-strategy.js +44 -122
  165. package/dist/vcs/gitlab-pr-strategy.d.ts +6 -4
  166. package/dist/vcs/gitlab-pr-strategy.js +39 -87
  167. package/dist/vcs/graphql-commit-strategy.d.ts +3 -4
  168. package/dist/vcs/graphql-commit-strategy.js +45 -63
  169. package/dist/vcs/index.d.ts +3 -16
  170. package/dist/vcs/index.js +2 -33
  171. package/dist/vcs/pr-creator.d.ts +9 -9
  172. package/dist/vcs/pr-creator.js +11 -10
  173. package/dist/vcs/pr-strategy-factory.d.ts +5 -0
  174. package/dist/vcs/pr-strategy-factory.js +17 -0
  175. package/dist/vcs/pr-strategy.d.ts +13 -26
  176. package/dist/vcs/pr-strategy.js +20 -25
  177. package/dist/vcs/types.d.ts +87 -21
  178. package/package.json +2 -1
  179. package/dist/cli/settings/lifecycle-checks.d.ts +0 -11
  180. package/dist/cli/settings/lifecycle-checks.js +0 -64
  181. package/dist/cli/settings/process-labels.d.ts +0 -9
  182. package/dist/cli/settings/process-labels.js +0 -125
  183. package/dist/cli/settings/process-repo-settings.d.ts +0 -9
  184. package/dist/cli/settings/process-repo-settings.js +0 -80
  185. package/dist/cli/settings/process-rulesets.d.ts +0 -9
  186. package/dist/cli/settings/process-rulesets.js +0 -118
  187. package/dist/cli/settings-command.d.ts +0 -11
  188. package/dist/cli/settings-command.js +0 -90
  189. package/dist/sync/manifest-strategy.d.ts +0 -21
  190. package/dist/sync/manifest-strategy.js +0 -67
@@ -1,83 +1,87 @@
1
- // src/output/unified-summary.ts
2
- import { appendFileSync } from "node:fs";
3
1
  import { hasLifecycleChanges } from "./lifecycle-report.js";
4
- import { formatValuePlain, formatRulesetConfigPlain, } from "./settings-report.js";
2
+ import { writeGitHubStepSummary } from "./github-summary.js";
3
+ import { renderRepoSettingsDiffLines, formatCountEntry, } from "./settings-report.js";
5
4
  // =============================================================================
6
5
  // Helpers
7
6
  // =============================================================================
7
+ function selectLabel(dry, pastLabel, futureLabel) {
8
+ return dry ? futureLabel : pastLabel;
9
+ }
8
10
  function formatCombinedSummary(input) {
9
11
  const parts = [];
10
12
  const dry = input.dryRun;
11
- // Lifecycle totals
12
13
  if (input.lifecycle) {
13
14
  const t = input.lifecycle.totals;
14
- const repoTotal = t.created + t.forked + t.migrated;
15
- if (repoTotal > 0) {
16
- const repoParts = [];
17
- if (t.created > 0)
18
- repoParts.push(`${t.created} ${dry ? "to create" : "created"}`);
19
- if (t.forked > 0)
20
- repoParts.push(`${t.forked} ${dry ? "to fork" : "forked"}`);
21
- if (t.migrated > 0)
22
- repoParts.push(`${t.migrated} ${dry ? "to migrate" : "migrated"}`);
23
- const repoWord = repoTotal === 1 ? "repo" : "repos";
24
- parts.push(`${repoTotal} ${repoWord} (${repoParts.join(", ")})`);
25
- }
15
+ const entry = formatCountEntry("repo", "repos", [
16
+ { label: selectLabel(dry, "created", "to create"), value: t.created },
17
+ { label: selectLabel(dry, "forked", "to fork"), value: t.forked },
18
+ { label: selectLabel(dry, "migrated", "to migrate"), value: t.migrated },
19
+ ]);
20
+ if (entry)
21
+ parts.push(entry);
26
22
  }
27
- // Sync totals
28
23
  if (input.sync) {
29
24
  const t = input.sync.totals;
30
- const fileTotal = t.files.create + t.files.update + t.files.delete;
31
- if (fileTotal > 0) {
32
- const fileParts = [];
33
- if (t.files.create > 0)
34
- fileParts.push(`${t.files.create} ${dry ? "to create" : "created"}`);
35
- if (t.files.update > 0)
36
- fileParts.push(`${t.files.update} ${dry ? "to update" : "updated"}`);
37
- if (t.files.delete > 0)
38
- fileParts.push(`${t.files.delete} ${dry ? "to delete" : "deleted"}`);
39
- const fileWord = fileTotal === 1 ? "file" : "files";
40
- parts.push(`${fileTotal} ${fileWord} (${fileParts.join(", ")})`);
41
- }
25
+ const entry = formatCountEntry("file", "files", [
26
+ {
27
+ label: selectLabel(dry, "created", "to create"),
28
+ value: t.files.create,
29
+ },
30
+ {
31
+ label: selectLabel(dry, "updated", "to update"),
32
+ value: t.files.update,
33
+ },
34
+ {
35
+ label: selectLabel(dry, "deleted", "to delete"),
36
+ value: t.files.delete,
37
+ },
38
+ ]);
39
+ if (entry)
40
+ parts.push(entry);
42
41
  }
43
- // Settings totals
44
42
  if (input.settings) {
45
43
  const t = input.settings.totals;
46
- const settingsTotal = t.settings.add + t.settings.change;
47
- if (settingsTotal > 0) {
48
- const settingWord = settingsTotal === 1 ? "setting" : "settings";
49
- const actions = [];
50
- if (t.settings.add > 0)
51
- actions.push(`${t.settings.add} ${dry ? "to add" : "added"}`);
52
- if (t.settings.change > 0)
53
- actions.push(`${t.settings.change} ${dry ? "to change" : "changed"}`);
54
- parts.push(`${settingsTotal} ${settingWord} (${actions.join(", ")})`);
55
- }
56
- const rulesetsTotal = t.rulesets.create + t.rulesets.update + t.rulesets.delete;
57
- if (rulesetsTotal > 0) {
58
- const rulesetWord = rulesetsTotal === 1 ? "ruleset" : "rulesets";
59
- const actions = [];
60
- if (t.rulesets.create > 0)
61
- actions.push(`${t.rulesets.create} ${dry ? "to create" : "created"}`);
62
- if (t.rulesets.update > 0)
63
- actions.push(`${t.rulesets.update} ${dry ? "to update" : "updated"}`);
64
- if (t.rulesets.delete > 0)
65
- actions.push(`${t.rulesets.delete} ${dry ? "to delete" : "deleted"}`);
66
- parts.push(`${rulesetsTotal} ${rulesetWord} (${actions.join(", ")})`);
67
- }
68
- const lt = t.labels;
69
- const labelsTotal = lt.create + lt.update + lt.delete;
70
- if (labelsTotal > 0) {
71
- const labelWord = labelsTotal === 1 ? "label" : "labels";
72
- const actions = [];
73
- if (lt.create > 0)
74
- actions.push(`${lt.create} ${dry ? "to create" : "created"}`);
75
- if (lt.update > 0)
76
- actions.push(`${lt.update} ${dry ? "to update" : "updated"}`);
77
- if (lt.delete > 0)
78
- actions.push(`${lt.delete} ${dry ? "to delete" : "deleted"}`);
79
- parts.push(`${labelsTotal} ${labelWord} (${actions.join(", ")})`);
80
- }
44
+ const settingsEntry = formatCountEntry("setting", "settings", [
45
+ { label: selectLabel(dry, "added", "to add"), value: t.settings.add },
46
+ {
47
+ label: selectLabel(dry, "changed", "to change"),
48
+ value: t.settings.change,
49
+ },
50
+ ]);
51
+ if (settingsEntry)
52
+ parts.push(settingsEntry);
53
+ const rulesetsEntry = formatCountEntry("ruleset", "rulesets", [
54
+ {
55
+ label: selectLabel(dry, "created", "to create"),
56
+ value: t.rulesets.create,
57
+ },
58
+ {
59
+ label: selectLabel(dry, "updated", "to update"),
60
+ value: t.rulesets.update,
61
+ },
62
+ {
63
+ label: selectLabel(dry, "deleted", "to delete"),
64
+ value: t.rulesets.delete,
65
+ },
66
+ ]);
67
+ if (rulesetsEntry)
68
+ parts.push(rulesetsEntry);
69
+ const labelsEntry = formatCountEntry("label", "labels", [
70
+ {
71
+ label: selectLabel(dry, "created", "to create"),
72
+ value: t.labels.create,
73
+ },
74
+ {
75
+ label: selectLabel(dry, "updated", "to update"),
76
+ value: t.labels.update,
77
+ },
78
+ {
79
+ label: selectLabel(dry, "deleted", "to delete"),
80
+ value: t.labels.delete,
81
+ },
82
+ ]);
83
+ if (labelsEntry)
84
+ parts.push(labelsEntry);
81
85
  }
82
86
  if (parts.length === 0) {
83
87
  return "No changes";
@@ -140,70 +144,7 @@ function renderSyncLines(syncRepo, diffLines) {
140
144
  }
141
145
  }
142
146
  function renderSettingsLines(settingsRepo, diffLines) {
143
- for (const setting of settingsRepo.settings) {
144
- if (setting.oldValue === undefined && setting.newValue === undefined) {
145
- continue;
146
- }
147
- if (setting.action === "add") {
148
- diffLines.push(`+ ${setting.name}: ${formatValuePlain(setting.newValue)}`);
149
- }
150
- else {
151
- diffLines.push(`! ${setting.name}: ${formatValuePlain(setting.oldValue)} → ${formatValuePlain(setting.newValue)}`);
152
- }
153
- }
154
- for (const ruleset of settingsRepo.rulesets) {
155
- if (ruleset.action === "create") {
156
- diffLines.push(`+ ruleset "${ruleset.name}"`);
157
- if (ruleset.config) {
158
- diffLines.push(...formatRulesetConfigPlain(ruleset.config));
159
- }
160
- }
161
- else if (ruleset.action === "update") {
162
- diffLines.push(`! ruleset "${ruleset.name}"`);
163
- if (ruleset.propertyDiffs && ruleset.propertyDiffs.length > 0) {
164
- for (const diff of ruleset.propertyDiffs) {
165
- const path = diff.path.join(".");
166
- if (diff.action === "add") {
167
- diffLines.push(`+ ${path}: ${formatValuePlain(diff.newValue)}`);
168
- }
169
- else if (diff.action === "change") {
170
- diffLines.push(`! ${path}: ${formatValuePlain(diff.oldValue)} → ${formatValuePlain(diff.newValue)}`);
171
- }
172
- else if (diff.action === "remove") {
173
- diffLines.push(`- ${path}`);
174
- }
175
- }
176
- }
177
- }
178
- else if (ruleset.action === "delete") {
179
- diffLines.push(`- ruleset "${ruleset.name}"`);
180
- }
181
- }
182
- for (const label of settingsRepo.labels) {
183
- if (label.action === "create") {
184
- diffLines.push(`+ label "${label.name}"`);
185
- if (label.config) {
186
- diffLines.push(`+ color: "${label.config.color}"`);
187
- if (label.config.description !== undefined) {
188
- diffLines.push(`+ description: "${label.config.description}"`);
189
- }
190
- }
191
- }
192
- else if (label.action === "update") {
193
- if (label.newName) {
194
- diffLines.push(`! label "${label.name}" \u2192 "${label.newName}"`);
195
- }
196
- else {
197
- diffLines.push(`! label "${label.name}"`);
198
- }
199
- }
200
- else if (label.action === "delete") {
201
- diffLines.push(`- label "${label.name}"`);
202
- }
203
- }
204
- if (settingsRepo.error) {
205
- diffLines.push(`- Error: ${settingsRepo.error}`);
206
- }
147
+ renderRepoSettingsDiffLines(settingsRepo, diffLines);
207
148
  }
208
149
  // =============================================================================
209
150
  // Markdown Formatter
@@ -276,11 +217,8 @@ export function formatUnifiedSummaryMarkdown(input) {
276
217
  // File Writer
277
218
  // =============================================================================
278
219
  export function writeUnifiedSummary(input) {
279
- const summaryPath = process.env.GITHUB_STEP_SUMMARY;
280
- if (!summaryPath)
281
- return;
282
220
  const markdown = formatUnifiedSummaryMarkdown(input);
283
221
  if (!markdown)
284
222
  return;
285
- appendFileSync(summaryPath, "\n" + markdown + "\n");
223
+ writeGitHubStepSummary(markdown);
286
224
  }
@@ -0,0 +1,67 @@
1
+ import type { RepoConfig } from "../config/index.js";
2
+ import type { RepoInfo, GitHubRepoInfo } from "../shared/repo-detector.js";
3
+ export interface BaseProcessorOptions {
4
+ dryRun?: boolean;
5
+ /** Pre-resolved auth token. Callers (e.g. sync-command) must resolve via resolveGitHubToken before passing. */
6
+ token?: string;
7
+ }
8
+ export interface BaseProcessorResult {
9
+ success: boolean;
10
+ repoName: string;
11
+ message: string;
12
+ skipped?: boolean;
13
+ dryRun?: boolean;
14
+ }
15
+ /**
16
+ * Generic settings processor interface for dependency injection.
17
+ * All three settings processors (rulesets, labels, repo-settings)
18
+ * share this contract — specific interfaces extend it for type safety.
19
+ */
20
+ export interface ISettingsProcessor<TOptions extends BaseProcessorOptions = BaseProcessorOptions, TResult extends BaseProcessorResult = BaseProcessorResult> {
21
+ process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: TOptions): Promise<TResult>;
22
+ }
23
+ /**
24
+ * Guards for GitHub settings processing — passed to withGitHubGuards.
25
+ */
26
+ interface SettingsGuards<TOptions extends BaseProcessorOptions, TResult extends BaseProcessorResult> {
27
+ hasDesiredSettings(repoConfig: RepoConfig): boolean;
28
+ emptySettingsMessage: string;
29
+ processSettings(githubRepo: GitHubRepoInfo, repoConfig: RepoConfig, options: TOptions, effectiveToken: string | undefined, repoName: string): Promise<TResult>;
30
+ }
31
+ /**
32
+ * Common boilerplate for GitHub settings processors: GitHub-only gating,
33
+ * empty settings check, token resolution, and error wrapping.
34
+ */
35
+ export declare function withGitHubGuards<TOptions extends BaseProcessorOptions, TResult extends BaseProcessorResult>(repoConfig: RepoConfig, repoInfo: RepoInfo, options: TOptions, guards: SettingsGuards<TOptions, TResult>): Promise<TResult>;
36
+ export interface ChangeCounts {
37
+ create: number;
38
+ update: number;
39
+ delete: number;
40
+ unchanged: number;
41
+ }
42
+ /**
43
+ * Count actions from a diff result array.
44
+ * Works with any change type that has an `action` field.
45
+ */
46
+ export declare function countActions(changes: ReadonlyArray<{
47
+ action: string;
48
+ }>): ChangeCounts;
49
+ export declare function formatChangeSummary(counts: ChangeCounts): string;
50
+ /**
51
+ * Build a standardized dry-run result for settings processors.
52
+ * Returns an intersection of BaseProcessorResult with the extra fields,
53
+ * which is assignable to any result subtype whose extra fields are provided.
54
+ */
55
+ export declare function buildDryRunResult<E extends Record<string, unknown> = Record<string, never>>(repoName: string, changeCounts: ChangeCounts, extra?: E): BaseProcessorResult & {
56
+ changes: ChangeCounts;
57
+ dryRun: true;
58
+ } & E;
59
+ /**
60
+ * Build a standardized apply result for settings processors.
61
+ * Returns an intersection of BaseProcessorResult with the extra fields,
62
+ * which is assignable to any result subtype whose extra fields are provided.
63
+ */
64
+ export declare function buildApplyResult<E extends Record<string, unknown> = Record<string, never>>(repoName: string, changeCounts: ChangeCounts, appliedCount: number, extra?: E): BaseProcessorResult & {
65
+ changes: ChangeCounts;
66
+ } & E;
67
+ export {};
@@ -0,0 +1,91 @@
1
+ import { isGitHubRepo, getRepoDisplayName } from "../shared/repo-detector.js";
2
+ import { toErrorMessage } from "../shared/type-guards.js";
3
+ /**
4
+ * Common boilerplate for GitHub settings processors: GitHub-only gating,
5
+ * empty settings check, token resolution, and error wrapping.
6
+ */
7
+ export async function withGitHubGuards(repoConfig, repoInfo, options, guards) {
8
+ const repoName = getRepoDisplayName(repoInfo);
9
+ if (!isGitHubRepo(repoInfo)) {
10
+ return {
11
+ success: true,
12
+ repoName,
13
+ message: `Skipped: ${repoName} is not a GitHub repository`,
14
+ skipped: true,
15
+ };
16
+ }
17
+ if (!guards.hasDesiredSettings(repoConfig)) {
18
+ return {
19
+ success: true,
20
+ repoName,
21
+ message: guards.emptySettingsMessage,
22
+ skipped: true,
23
+ };
24
+ }
25
+ try {
26
+ return await guards.processSettings(repoInfo, repoConfig, options, options.token, repoName);
27
+ }
28
+ catch (error) {
29
+ const message = toErrorMessage(error);
30
+ return {
31
+ success: false,
32
+ repoName,
33
+ message: `Failed: ${message}`,
34
+ };
35
+ }
36
+ }
37
+ /**
38
+ * Count actions from a diff result array.
39
+ * Works with any change type that has an `action` field.
40
+ */
41
+ export function countActions(changes) {
42
+ return {
43
+ create: changes.filter((c) => c.action === "create").length,
44
+ update: changes.filter((c) => c.action === "update").length,
45
+ delete: changes.filter((c) => c.action === "delete").length,
46
+ unchanged: changes.filter((c) => c.action === "unchanged").length,
47
+ };
48
+ }
49
+ export function formatChangeSummary(counts) {
50
+ const parts = [];
51
+ if (counts.create > 0)
52
+ parts.push(`${counts.create} created`);
53
+ if (counts.update > 0)
54
+ parts.push(`${counts.update} updated`);
55
+ if (counts.delete > 0)
56
+ parts.push(`${counts.delete} deleted`);
57
+ if (counts.unchanged > 0)
58
+ parts.push(`${counts.unchanged} unchanged`);
59
+ return parts.length > 0 ? parts.join(", ") : "no changes";
60
+ }
61
+ /**
62
+ * Build a standardized dry-run result for settings processors.
63
+ * Returns an intersection of BaseProcessorResult with the extra fields,
64
+ * which is assignable to any result subtype whose extra fields are provided.
65
+ */
66
+ export function buildDryRunResult(repoName, changeCounts, extra) {
67
+ const summary = formatChangeSummary(changeCounts);
68
+ const base = {
69
+ success: true,
70
+ repoName,
71
+ message: `[DRY RUN] ${summary}`,
72
+ dryRun: true,
73
+ changes: changeCounts,
74
+ };
75
+ return Object.assign(base, extra);
76
+ }
77
+ /**
78
+ * Build a standardized apply result for settings processors.
79
+ * Returns an intersection of BaseProcessorResult with the extra fields,
80
+ * which is assignable to any result subtype whose extra fields are provided.
81
+ */
82
+ export function buildApplyResult(repoName, changeCounts, appliedCount, extra) {
83
+ const summary = formatChangeSummary(changeCounts);
84
+ const base = {
85
+ success: true,
86
+ repoName,
87
+ message: appliedCount > 0 ? `Applied: ${summary}` : "No changes needed",
88
+ changes: changeCounts,
89
+ };
90
+ return Object.assign(base, extra);
91
+ }
@@ -1,3 +1,4 @@
1
- export * from "./rulesets/index.js";
2
- export * from "./repo-settings/index.js";
3
- export * from "./labels/index.js";
1
+ export { type ISettingsProcessor } from "./base-processor.js";
2
+ export { type PropertyDiff, formatPropertyTree, type RulesetPlanEntry, RulesetProcessor, type IRulesetProcessor, } from "./rulesets/index.js";
3
+ export { RepoSettingsProcessor, type IRepoSettingsProcessor, type RepoSettingsPlanEntry, } from "./repo-settings/index.js";
4
+ export { type LabelsPlanEntry, LabelsProcessor, type ILabelsProcessor, } from "./labels/index.js";
@@ -1,6 +1,6 @@
1
1
  // Rulesets
2
- export * from "./rulesets/index.js";
2
+ export { formatPropertyTree, RulesetProcessor, } from "./rulesets/index.js";
3
3
  // Repo settings
4
- export * from "./repo-settings/index.js";
4
+ export { RepoSettingsProcessor, } from "./repo-settings/index.js";
5
5
  // Labels
6
- export * from "./labels/index.js";
6
+ export { LabelsProcessor, } from "./labels/index.js";
@@ -1,5 +1,5 @@
1
1
  import type { Label } from "../../config/types.js";
2
- export interface GitHubLabelPayload {
2
+ interface GitHubLabelPayload {
3
3
  name: string;
4
4
  new_name?: string;
5
5
  color: string;
@@ -13,3 +13,4 @@ export declare function normalizeColor(color: string): string;
13
13
  * Converts a label config entry to a GitHub API payload.
14
14
  */
15
15
  export declare function labelConfigToPayload(name: string, label: Label): GitHubLabelPayload;
16
+ export {};
@@ -25,9 +25,9 @@ export interface LabelChange {
25
25
  *
26
26
  * @param current - Current labels from GitHub API
27
27
  * @param desired - Desired labels from config (name -> label)
28
- * @param managedLabels - Names of labels managed by xfg (from manifest)
28
+ * @param deleteOrphaned - If true, delete current labels not in desired config
29
29
  * @param noDelete - If true, skip delete operations
30
30
  * @returns Array of changes to apply
31
31
  * @throws Error if rename collisions are detected
32
32
  */
33
- export declare function diffLabels(current: GitHubLabel[], desired: Record<string, Label>, managedLabels: string[], noDelete: boolean): LabelChange[];
33
+ export declare function diffLabels(current: GitHubLabel[], desired: Record<string, Label>, deleteOrphaned: boolean, noDelete: boolean): LabelChange[];
@@ -11,19 +11,18 @@ import { normalizeColor } from "./converter.js";
11
11
  *
12
12
  * @param current - Current labels from GitHub API
13
13
  * @param desired - Desired labels from config (name -> label)
14
- * @param managedLabels - Names of labels managed by xfg (from manifest)
14
+ * @param deleteOrphaned - If true, delete current labels not in desired config
15
15
  * @param noDelete - If true, skip delete operations
16
16
  * @returns Array of changes to apply
17
17
  * @throws Error if rename collisions are detected
18
18
  */
19
- export function diffLabels(current, desired, managedLabels, noDelete) {
19
+ export function diffLabels(current, desired, deleteOrphaned, noDelete) {
20
20
  const changes = [];
21
21
  // Build case-insensitive lookup of current labels
22
22
  const currentByName = new Map();
23
23
  for (const label of current) {
24
24
  currentByName.set(label.name.toLowerCase(), label);
25
25
  }
26
- const managedSet = new Set(managedLabels.map((n) => n.toLowerCase()));
27
26
  // Collect rename targets for collision detection
28
27
  const renameTargets = new Map(); // lowercase target -> source name
29
28
  for (const [name, label] of Object.entries(desired)) {
@@ -38,10 +37,10 @@ export function diffLabels(current, desired, managedLabels, noDelete) {
38
37
  // Determine which labels will be deleted (for collision checking)
39
38
  const desiredLower = new Set(Object.keys(desired).map((n) => n.toLowerCase()));
40
39
  const deletedNames = new Set();
41
- if (!noDelete) {
42
- for (const name of managedSet) {
43
- if (!desiredLower.has(name) && currentByName.has(name)) {
44
- deletedNames.add(name);
40
+ if (deleteOrphaned && !noDelete) {
41
+ for (const nameLower of currentByName.keys()) {
42
+ if (!desiredLower.has(nameLower)) {
43
+ deletedNames.add(nameLower);
45
44
  }
46
45
  }
47
46
  }
@@ -130,18 +129,15 @@ export function diffLabels(current, desired, managedLabels, noDelete) {
130
129
  }
131
130
  }
132
131
  }
133
- // Check for orphaned labels (in manifest but not in desired config)
134
- if (!noDelete) {
135
- for (const name of managedSet) {
136
- if (!desiredLower.has(name)) {
137
- const currentLabel = currentByName.get(name);
138
- if (currentLabel) {
139
- changes.push({
140
- action: "delete",
141
- name: currentLabel.name,
142
- current: currentLabel,
143
- });
144
- }
132
+ // Desired-state orphan detection: delete ALL current not in desired
133
+ if (deleteOrphaned && !noDelete) {
134
+ for (const [nameLower, currentLabel] of currentByName) {
135
+ if (!desiredLower.has(nameLower)) {
136
+ changes.push({
137
+ action: "delete",
138
+ name: currentLabel.name,
139
+ current: currentLabel,
140
+ });
145
141
  }
146
142
  }
147
143
  }
@@ -1,7 +1,8 @@
1
1
  import { ICommandExecutor } from "../../shared/command-executor.js";
2
2
  import { RepoInfo } from "../../shared/repo-detector.js";
3
- import type { ILabelsStrategy, GitHubLabel, LabelsStrategyOptions } from "./types.js";
4
- export interface GitHubLabelsStrategyOptions {
3
+ import { type GhApiOptions } from "../../shared/gh-api-utils.js";
4
+ import type { ILabelsStrategy, GitHubLabel } from "./types.js";
5
+ interface GitHubLabelsStrategyOptions {
5
6
  retries?: number;
6
7
  }
7
8
  /**
@@ -12,14 +13,13 @@ export interface GitHubLabelsStrategyOptions {
12
13
  * escapeShellArg for input sanitization, matching the rulesets strategy pattern.
13
14
  */
14
15
  export declare class GitHubLabelsStrategy implements ILabelsStrategy {
15
- private executor;
16
- private retries;
16
+ private api;
17
17
  constructor(executor?: ICommandExecutor, options?: GitHubLabelsStrategyOptions);
18
18
  /**
19
19
  * Lists all labels for a repository.
20
20
  * Uses --paginate to retrieve all labels.
21
21
  */
22
- list(repoInfo: RepoInfo, options?: LabelsStrategyOptions): Promise<GitHubLabel[]>;
22
+ list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubLabel[]>;
23
23
  /**
24
24
  * Creates a new label.
25
25
  */
@@ -27,7 +27,7 @@ export declare class GitHubLabelsStrategy implements ILabelsStrategy {
27
27
  name: string;
28
28
  color: string;
29
29
  description?: string;
30
- }, options?: LabelsStrategyOptions): Promise<void>;
30
+ }, options?: GhApiOptions): Promise<void>;
31
31
  /**
32
32
  * Updates an existing label.
33
33
  * Uses encodeURIComponent for label name in URL path.
@@ -36,20 +36,11 @@ export declare class GitHubLabelsStrategy implements ILabelsStrategy {
36
36
  new_name?: string;
37
37
  color?: string;
38
38
  description?: string;
39
- }, options?: LabelsStrategyOptions): Promise<void>;
39
+ }, options?: GhApiOptions): Promise<void>;
40
40
  /**
41
41
  * Deletes a label.
42
42
  * Uses encodeURIComponent for label name in URL path.
43
43
  */
44
- delete(repoInfo: RepoInfo, name: string, options?: LabelsStrategyOptions): Promise<void>;
45
- /**
46
- * Validates that the repo is a GitHub repository.
47
- */
48
- private validateGitHub;
49
- /**
50
- * Executes a GitHub API call using the gh CLI.
51
- * Uses the project's ICommandExecutor + escapeShellArg pattern
52
- * (matching github-ruleset-strategy.ts).
53
- */
54
- private ghApi;
44
+ delete(repoInfo: RepoInfo, name: string, options?: GhApiOptions): Promise<void>;
55
45
  }
46
+ export {};