@aspruyt/xfg 4.0.0 → 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 (173) hide show
  1. package/dist/cli/index.d.ts +1 -2
  2. package/dist/cli/index.js +0 -1
  3. package/dist/cli/program.js +7 -2
  4. package/dist/cli/{settings/results-collector.d.ts → results-collector.d.ts} +1 -1
  5. package/dist/cli/{settings/results-collector.js → results-collector.js} +2 -1
  6. package/dist/cli/settings-report-builder.d.ts +1 -3
  7. package/dist/cli/sync-command.d.ts +2 -24
  8. package/dist/cli/sync-command.js +295 -301
  9. package/dist/cli/types.d.ts +60 -40
  10. package/dist/cli/types.js +1 -12
  11. package/dist/config/errors.d.ts +9 -0
  12. package/dist/config/errors.js +11 -0
  13. package/dist/config/file-reference-resolver.d.ts +2 -1
  14. package/dist/config/file-reference-resolver.js +10 -8
  15. package/dist/config/formatter.d.ts +3 -2
  16. package/dist/config/index.d.ts +4 -6
  17. package/dist/config/index.js +4 -8
  18. package/dist/config/loader.js +4 -2
  19. package/dist/config/merge.d.ts +0 -9
  20. package/dist/config/merge.js +2 -7
  21. package/dist/config/normalizer.d.ts +4 -0
  22. package/dist/config/normalizer.js +61 -110
  23. package/dist/config/types.d.ts +15 -19
  24. package/dist/config/types.js +1 -1
  25. package/dist/config/validator.d.ts +0 -4
  26. package/dist/config/validator.js +286 -363
  27. package/dist/config/validators/file-validator.d.ts +2 -8
  28. package/dist/config/validators/file-validator.js +6 -17
  29. package/dist/config/validators/index.d.ts +3 -3
  30. package/dist/config/validators/index.js +3 -3
  31. package/dist/config/validators/repo-settings-validator.d.ts +0 -6
  32. package/dist/config/validators/repo-settings-validator.js +9 -9
  33. package/dist/config/validators/ruleset-validator.d.ts +0 -14
  34. package/dist/config/validators/ruleset-validator.js +28 -28
  35. package/dist/lifecycle/ado-migration-source.js +2 -1
  36. package/dist/lifecycle/github-lifecycle-provider.d.ts +6 -5
  37. package/dist/lifecycle/github-lifecycle-provider.js +79 -90
  38. package/dist/lifecycle/index.d.ts +2 -6
  39. package/dist/lifecycle/index.js +0 -4
  40. package/dist/lifecycle/lifecycle-formatter.d.ts +2 -1
  41. package/dist/lifecycle/lifecycle-formatter.js +4 -0
  42. package/dist/lifecycle/lifecycle-helpers.d.ts +3 -2
  43. package/dist/lifecycle/repo-lifecycle-manager.js +4 -11
  44. package/dist/lifecycle/types.d.ts +0 -8
  45. package/dist/output/github-summary.d.ts +5 -0
  46. package/dist/output/github-summary.js +9 -2
  47. package/dist/output/index.d.ts +2 -2
  48. package/dist/output/index.js +1 -1
  49. package/dist/output/lifecycle-report.js +5 -23
  50. package/dist/output/settings-report.d.ts +14 -3
  51. package/dist/output/settings-report.js +137 -197
  52. package/dist/output/summary-utils.d.ts +1 -1
  53. package/dist/output/summary-utils.js +2 -1
  54. package/dist/output/sync-report.js +5 -8
  55. package/dist/output/unified-summary.d.ts +2 -1
  56. package/dist/output/unified-summary.js +71 -133
  57. package/dist/settings/base-processor.d.ts +67 -0
  58. package/dist/settings/base-processor.js +91 -0
  59. package/dist/settings/index.d.ts +4 -3
  60. package/dist/settings/index.js +3 -3
  61. package/dist/settings/labels/converter.d.ts +2 -1
  62. package/dist/settings/labels/github-labels-strategy.d.ts +9 -18
  63. package/dist/settings/labels/github-labels-strategy.js +17 -73
  64. package/dist/settings/labels/index.d.ts +2 -6
  65. package/dist/settings/labels/index.js +1 -9
  66. package/dist/settings/labels/processor.d.ts +6 -30
  67. package/dist/settings/labels/processor.js +62 -152
  68. package/dist/settings/labels/types.d.ts +5 -8
  69. package/dist/settings/repo-settings/formatter.d.ts +2 -2
  70. package/dist/settings/repo-settings/formatter.js +6 -6
  71. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +11 -12
  72. package/dist/settings/repo-settings/github-repo-settings-strategy.js +32 -79
  73. package/dist/settings/repo-settings/index.d.ts +2 -5
  74. package/dist/settings/repo-settings/index.js +1 -9
  75. package/dist/settings/repo-settings/processor.d.ts +6 -27
  76. package/dist/settings/repo-settings/processor.js +51 -104
  77. package/dist/settings/repo-settings/types.d.ts +7 -9
  78. package/dist/settings/rulesets/diff-algorithm.d.ts +0 -4
  79. package/dist/settings/rulesets/diff-algorithm.js +1 -10
  80. package/dist/settings/rulesets/diff.d.ts +1 -1
  81. package/dist/settings/rulesets/diff.js +2 -21
  82. package/dist/settings/rulesets/formatter.d.ts +1 -3
  83. package/dist/settings/rulesets/formatter.js +1 -8
  84. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +11 -51
  85. package/dist/settings/rulesets/github-ruleset-strategy.js +24 -85
  86. package/dist/settings/rulesets/index.d.ts +3 -6
  87. package/dist/settings/rulesets/index.js +5 -9
  88. package/dist/settings/rulesets/processor.d.ts +8 -33
  89. package/dist/settings/rulesets/processor.js +58 -151
  90. package/dist/settings/rulesets/types.d.ts +35 -6
  91. package/dist/shared/command-executor.d.ts +2 -22
  92. package/dist/shared/command-executor.js +8 -7
  93. package/dist/shared/env.d.ts +0 -8
  94. package/dist/shared/env.js +14 -70
  95. package/dist/shared/file-status.d.ts +2 -0
  96. package/dist/shared/file-status.js +13 -0
  97. package/dist/shared/gh-api-utils.d.ts +46 -0
  98. package/dist/shared/gh-api-utils.js +107 -0
  99. package/dist/shared/index.d.ts +5 -5
  100. package/dist/shared/index.js +3 -3
  101. package/dist/shared/interpolation-engine.d.ts +31 -0
  102. package/dist/shared/interpolation-engine.js +50 -0
  103. package/dist/shared/logger.d.ts +3 -7
  104. package/dist/shared/logger.js +4 -1
  105. package/dist/shared/repo-detector.d.ts +17 -2
  106. package/dist/shared/repo-detector.js +27 -0
  107. package/dist/shared/retry-utils.d.ts +9 -17
  108. package/dist/shared/retry-utils.js +22 -28
  109. package/dist/shared/sanitize-utils.d.ts +0 -7
  110. package/dist/shared/sanitize-utils.js +0 -7
  111. package/dist/shared/shell-utils.d.ts +1 -0
  112. package/dist/shared/shell-utils.js +3 -0
  113. package/dist/shared/string-utils.d.ts +4 -0
  114. package/dist/shared/string-utils.js +6 -0
  115. package/dist/shared/type-guards.d.ts +17 -0
  116. package/dist/shared/type-guards.js +26 -0
  117. package/dist/shared/workspace-utils.d.ts +0 -4
  118. package/dist/shared/workspace-utils.js +0 -4
  119. package/dist/{sync → shared}/xfg-template.d.ts +3 -2
  120. package/dist/{sync → shared}/xfg-template.js +13 -54
  121. package/dist/sync/auth-options-builder.d.ts +4 -5
  122. package/dist/sync/auth-options-builder.js +15 -26
  123. package/dist/sync/branch-manager.d.ts +5 -0
  124. package/dist/sync/branch-manager.js +12 -10
  125. package/dist/sync/commit-push-manager.d.ts +1 -1
  126. package/dist/sync/commit-push-manager.js +22 -18
  127. package/dist/sync/diff-utils.d.ts +4 -9
  128. package/dist/sync/diff-utils.js +2 -19
  129. package/dist/sync/file-sync-orchestrator.js +9 -8
  130. package/dist/sync/file-writer.d.ts +2 -1
  131. package/dist/sync/file-writer.js +3 -6
  132. package/dist/sync/index.d.ts +2 -15
  133. package/dist/sync/index.js +0 -19
  134. package/dist/sync/manifest-manager.d.ts +4 -0
  135. package/dist/sync/manifest-manager.js +5 -1
  136. package/dist/sync/manifest.d.ts +10 -41
  137. package/dist/sync/manifest.js +11 -56
  138. package/dist/sync/pr-merge-handler.d.ts +2 -6
  139. package/dist/sync/pr-merge-handler.js +6 -3
  140. package/dist/sync/repository-processor.d.ts +1 -2
  141. package/dist/sync/repository-processor.js +20 -12
  142. package/dist/sync/repository-session.js +5 -14
  143. package/dist/sync/sync-workflow.js +31 -38
  144. package/dist/sync/types.d.ts +43 -178
  145. package/dist/vcs/authenticated-git-ops.d.ts +27 -70
  146. package/dist/vcs/authenticated-git-ops.js +70 -96
  147. package/dist/vcs/azure-pr-strategy.d.ts +6 -4
  148. package/dist/vcs/azure-pr-strategy.js +34 -82
  149. package/dist/vcs/branch-utils.d.ts +6 -0
  150. package/dist/vcs/branch-utils.js +29 -0
  151. package/dist/vcs/commit-strategy-selector.d.ts +5 -0
  152. package/dist/vcs/commit-strategy-selector.js +10 -0
  153. package/dist/vcs/git-commit-strategy.js +1 -2
  154. package/dist/vcs/git-ops.d.ts +15 -59
  155. package/dist/vcs/git-ops.js +46 -110
  156. package/dist/vcs/github-app-token-manager.d.ts +0 -6
  157. package/dist/vcs/github-app-token-manager.js +5 -12
  158. package/dist/vcs/github-pr-strategy.d.ts +5 -5
  159. package/dist/vcs/github-pr-strategy.js +44 -122
  160. package/dist/vcs/gitlab-pr-strategy.d.ts +6 -4
  161. package/dist/vcs/gitlab-pr-strategy.js +39 -87
  162. package/dist/vcs/graphql-commit-strategy.d.ts +3 -4
  163. package/dist/vcs/graphql-commit-strategy.js +31 -63
  164. package/dist/vcs/index.d.ts +3 -16
  165. package/dist/vcs/index.js +2 -33
  166. package/dist/vcs/pr-creator.d.ts +9 -9
  167. package/dist/vcs/pr-creator.js +11 -10
  168. package/dist/vcs/pr-strategy-factory.d.ts +5 -0
  169. package/dist/vcs/pr-strategy-factory.js +17 -0
  170. package/dist/vcs/pr-strategy.d.ts +13 -26
  171. package/dist/vcs/pr-strategy.js +20 -25
  172. package/dist/vcs/types.d.ts +87 -21
  173. package/package.json +2 -1
@@ -2,26 +2,13 @@ import type { RepoConfig } from "../../config/index.js";
2
2
  import type { RepoInfo } from "../../shared/repo-detector.js";
3
3
  import { type LabelsPlanResult } from "./formatter.js";
4
4
  import type { ILabelsStrategy } from "./types.js";
5
- export interface ILabelsProcessor {
6
- process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: LabelsProcessorOptions): Promise<LabelsProcessorResult>;
7
- }
8
- export interface LabelsProcessorOptions {
9
- dryRun?: boolean;
5
+ import { type BaseProcessorOptions, type BaseProcessorResult, type ISettingsProcessor, type ChangeCounts } from "../base-processor.js";
6
+ export type ILabelsProcessor = ISettingsProcessor<LabelsProcessorOptions, LabelsProcessorResult>;
7
+ export interface LabelsProcessorOptions extends BaseProcessorOptions {
10
8
  noDelete?: boolean;
11
- token?: string;
12
9
  }
13
- export interface LabelsProcessorResult {
14
- success: boolean;
15
- repoName: string;
16
- message: string;
17
- skipped?: boolean;
18
- dryRun?: boolean;
19
- changes?: {
20
- create: number;
21
- update: number;
22
- delete: number;
23
- unchanged: number;
24
- };
10
+ export interface LabelsProcessorResult extends BaseProcessorResult {
11
+ changes?: ChangeCounts;
25
12
  planOutput?: LabelsPlanResult;
26
13
  }
27
14
  /**
@@ -30,18 +17,7 @@ export interface LabelsProcessorResult {
30
17
  */
31
18
  export declare class LabelsProcessor implements ILabelsProcessor {
32
19
  private readonly strategy;
33
- private readonly tokenManager;
34
20
  constructor(strategy?: ILabelsStrategy);
35
- /**
36
- * Process labels for a single repository.
37
- */
38
21
  process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: LabelsProcessorOptions): Promise<LabelsProcessorResult>;
39
- /**
40
- * Format change counts into a summary string.
41
- */
42
- private formatChangeSummary;
43
- /**
44
- * Resolves a GitHub App installation token for the given repo.
45
- */
46
- private getInstallationToken;
22
+ private processSettings;
47
23
  }
@@ -1,176 +1,86 @@
1
- import { isGitHubRepo, getRepoDisplayName, } from "../../shared/repo-detector.js";
2
1
  import { GitHubLabelsStrategy } from "./github-labels-strategy.js";
3
2
  import { diffLabels } from "./diff.js";
4
3
  import { formatLabelsPlan } from "./formatter.js";
5
4
  import { labelConfigToPayload } from "./converter.js";
6
- import { hasGitHubAppCredentials } from "../../vcs/index.js";
7
- import { GitHubAppTokenManager } from "../../vcs/github-app-token-manager.js";
8
- // =============================================================================
9
- // Processor Implementation
10
- // =============================================================================
5
+ import { withGitHubGuards, countActions, buildDryRunResult, buildApplyResult, } from "../base-processor.js";
11
6
  /**
12
7
  * Processes label configuration for a repository.
13
8
  * Handles create/update/delete operations via GitHub Labels API.
14
9
  */
15
10
  export class LabelsProcessor {
16
11
  strategy;
17
- tokenManager;
18
12
  constructor(strategy) {
19
13
  this.strategy = strategy ?? new GitHubLabelsStrategy();
20
- if (hasGitHubAppCredentials()) {
21
- this.tokenManager = new GitHubAppTokenManager(process.env.XFG_GITHUB_APP_ID, process.env.XFG_GITHUB_APP_PRIVATE_KEY);
22
- }
23
- else {
24
- this.tokenManager = null;
25
- }
26
14
  }
27
- /**
28
- * Process labels for a single repository.
29
- */
30
15
  async process(repoConfig, repoInfo, options) {
31
- const repoName = getRepoDisplayName(repoInfo);
32
- const { dryRun, noDelete, token } = options;
33
- // Check if this is a GitHub repo
34
- if (!isGitHubRepo(repoInfo)) {
35
- return {
36
- success: true,
37
- repoName,
38
- message: `Skipped: ${repoName} is not a GitHub repository`,
39
- skipped: true,
40
- };
41
- }
42
- const githubRepo = repoInfo;
16
+ return withGitHubGuards(repoConfig, repoInfo, options, {
17
+ hasDesiredSettings: (rc) => Object.keys(rc.settings?.labels ?? {}).length > 0,
18
+ emptySettingsMessage: "No labels configured",
19
+ processSettings: (githubRepo, rc, opts, token, repoName) => this.processSettings(githubRepo, rc, opts, token, repoName),
20
+ });
21
+ }
22
+ async processSettings(githubRepo, repoConfig, options, effectiveToken, repoName) {
23
+ const { dryRun, noDelete } = options;
43
24
  const settings = repoConfig.settings;
44
25
  const desiredLabels = settings?.labels ?? {};
45
26
  const deleteOrphaned = settings?.deleteOrphaned ?? false;
46
- // If no labels configured, skip
47
- if (Object.keys(desiredLabels).length === 0) {
48
- return {
49
- success: true,
50
- repoName,
51
- message: "No labels configured",
52
- skipped: true,
53
- };
27
+ const strategyOptions = { token: effectiveToken, host: githubRepo.host };
28
+ const currentLabels = await this.strategy.list(githubRepo, strategyOptions);
29
+ // Compute diff
30
+ const changes = diffLabels(currentLabels, desiredLabels, deleteOrphaned, noDelete ?? false);
31
+ const changeCounts = countActions(changes);
32
+ const planOutput = formatLabelsPlan(changes);
33
+ if (dryRun) {
34
+ return buildDryRunResult(repoName, changeCounts, { planOutput });
54
35
  }
55
- try {
56
- // Resolve App token if available, fall back to provided token
57
- const effectiveToken = token ?? (await this.getInstallationToken(githubRepo));
58
- const strategyOptions = { token: effectiveToken, host: githubRepo.host };
59
- const currentLabels = await this.strategy.list(githubRepo, strategyOptions);
60
- // Compute diff
61
- const changes = diffLabels(currentLabels, desiredLabels, deleteOrphaned, noDelete ?? false);
62
- // Count changes by type
63
- const changeCounts = {
64
- create: changes.filter((c) => c.action === "create").length,
65
- update: changes.filter((c) => c.action === "update").length,
66
- delete: changes.filter((c) => c.action === "delete").length,
67
- unchanged: changes.filter((c) => c.action === "unchanged").length,
68
- };
69
- const planOutput = formatLabelsPlan(changes);
70
- // Dry run mode - report planned changes without applying
71
- if (dryRun) {
72
- const summary = this.formatChangeSummary(changeCounts);
73
- return {
74
- success: true,
75
- repoName,
76
- message: `[DRY RUN] ${summary}`,
77
- dryRun: true,
78
- changes: changeCounts,
79
- planOutput,
80
- };
81
- }
82
- // Apply changes (diff is already sorted: delete, update, create, unchanged)
83
- let appliedCount = 0;
84
- for (const change of changes) {
85
- switch (change.action) {
86
- case "create":
87
- if (change.desired) {
88
- const payload = labelConfigToPayload(change.name, change.desired);
89
- await this.strategy.create(githubRepo, {
90
- name: payload.name,
91
- color: payload.color,
92
- ...(payload.description !== undefined
93
- ? { description: payload.description }
94
- : {}),
95
- }, strategyOptions);
96
- appliedCount++;
97
- }
98
- break;
99
- case "update":
100
- if (change.desired) {
101
- const updatePayload = {};
102
- for (const prop of change.propertyChanges ?? []) {
103
- if (prop.property === "color") {
104
- updatePayload.color = prop.newValue;
105
- }
106
- else if (prop.property === "description") {
107
- updatePayload.description = prop.newValue;
108
- }
109
- else if (prop.property === "new_name") {
110
- updatePayload.new_name = prop.newValue;
111
- }
36
+ // Apply changes (diff is already sorted: delete, update, create, unchanged)
37
+ let appliedCount = 0;
38
+ for (const change of changes) {
39
+ switch (change.action) {
40
+ case "create":
41
+ if (change.desired) {
42
+ const payload = labelConfigToPayload(change.name, change.desired);
43
+ await this.strategy.create(githubRepo, {
44
+ name: payload.name,
45
+ color: payload.color,
46
+ ...(payload.description !== undefined
47
+ ? { description: payload.description }
48
+ : {}),
49
+ }, strategyOptions);
50
+ appliedCount++;
51
+ }
52
+ break;
53
+ case "update":
54
+ if (change.desired) {
55
+ const updatePayload = {};
56
+ for (const prop of change.propertyChanges ?? []) {
57
+ if (prop.property === "color") {
58
+ updatePayload.color = prop.newValue;
59
+ }
60
+ else if (prop.property === "description") {
61
+ updatePayload.description = prop.newValue;
62
+ }
63
+ else if (prop.property === "new_name") {
64
+ updatePayload.new_name = prop.newValue;
112
65
  }
113
- await this.strategy.update(githubRepo, change.name, updatePayload, strategyOptions);
114
- appliedCount++;
115
- }
116
- break;
117
- case "delete":
118
- if (!noDelete && deleteOrphaned) {
119
- await this.strategy.delete(githubRepo, change.name, strategyOptions);
120
- appliedCount++;
121
66
  }
122
- break;
123
- case "unchanged":
124
- // No action needed
125
- break;
126
- }
67
+ await this.strategy.update(githubRepo, change.name, updatePayload, strategyOptions);
68
+ appliedCount++;
69
+ }
70
+ break;
71
+ case "delete":
72
+ if (!noDelete && deleteOrphaned) {
73
+ await this.strategy.delete(githubRepo, change.name, strategyOptions);
74
+ appliedCount++;
75
+ }
76
+ break;
77
+ case "unchanged":
78
+ // No action needed
79
+ break;
127
80
  }
128
- const summary = this.formatChangeSummary(changeCounts);
129
- return {
130
- success: true,
131
- repoName,
132
- message: appliedCount > 0 ? `Applied: ${summary}` : "No changes needed",
133
- changes: changeCounts,
134
- planOutput,
135
- };
136
- }
137
- catch (error) {
138
- const message = error instanceof Error ? error.message : String(error);
139
- return {
140
- success: false,
141
- repoName,
142
- message: `Failed: ${message}`,
143
- };
144
- }
145
- }
146
- /**
147
- * Format change counts into a summary string.
148
- */
149
- formatChangeSummary(counts) {
150
- const parts = [];
151
- if (counts.create > 0)
152
- parts.push(`${counts.create} created`);
153
- if (counts.update > 0)
154
- parts.push(`${counts.update} updated`);
155
- if (counts.delete > 0)
156
- parts.push(`${counts.delete} deleted`);
157
- if (counts.unchanged > 0)
158
- parts.push(`${counts.unchanged} unchanged`);
159
- return parts.length > 0 ? parts.join(", ") : "no changes";
160
- }
161
- /**
162
- * Resolves a GitHub App installation token for the given repo.
163
- */
164
- async getInstallationToken(repoInfo) {
165
- if (!this.tokenManager) {
166
- return undefined;
167
- }
168
- try {
169
- const token = await this.tokenManager.getTokenForRepo(repoInfo);
170
- return token ?? undefined;
171
- }
172
- catch {
173
- return undefined;
174
81
  }
82
+ return buildApplyResult(repoName, changeCounts, appliedCount, {
83
+ planOutput,
84
+ });
175
85
  }
176
86
  }
@@ -1,8 +1,5 @@
1
1
  import type { RepoInfo } from "../../shared/repo-detector.js";
2
- export interface LabelsStrategyOptions {
3
- token?: string;
4
- host?: string;
5
- }
2
+ import type { GhApiOptions } from "../../shared/gh-api-utils.js";
6
3
  /**
7
4
  * GitHub label as returned by the API.
8
5
  */
@@ -18,16 +15,16 @@ export interface GitHubLabel {
18
15
  * Abstracts platform-specific API calls.
19
16
  */
20
17
  export interface ILabelsStrategy {
21
- list(repoInfo: RepoInfo, options?: LabelsStrategyOptions): Promise<GitHubLabel[]>;
18
+ list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubLabel[]>;
22
19
  create(repoInfo: RepoInfo, label: {
23
20
  name: string;
24
21
  color: string;
25
22
  description?: string;
26
- }, options?: LabelsStrategyOptions): Promise<void>;
23
+ }, options?: GhApiOptions): Promise<void>;
27
24
  update(repoInfo: RepoInfo, currentName: string, label: {
28
25
  new_name?: string;
29
26
  color?: string;
30
27
  description?: string;
31
- }, options?: LabelsStrategyOptions): Promise<void>;
32
- delete(repoInfo: RepoInfo, name: string, options?: LabelsStrategyOptions): Promise<void>;
28
+ }, options?: GhApiOptions): Promise<void>;
29
+ delete(repoInfo: RepoInfo, name: string, options?: GhApiOptions): Promise<void>;
33
30
  }
@@ -7,8 +7,8 @@ export interface RepoSettingsPlanEntry {
7
7
  }
8
8
  export interface RepoSettingsPlanResult {
9
9
  lines: string[];
10
- adds: number;
11
- changes: number;
10
+ creates: number;
11
+ updates: number;
12
12
  warnings: string[];
13
13
  entries: RepoSettingsPlanEntry[];
14
14
  }
@@ -40,11 +40,11 @@ function getWarning(change) {
40
40
  export function formatRepoSettingsPlan(changes) {
41
41
  const lines = [];
42
42
  const warnings = [];
43
- let adds = 0;
44
- let changesCount = 0;
43
+ let creates = 0;
44
+ let updates = 0;
45
45
  const entries = [];
46
46
  if (changes.length === 0) {
47
- return { lines, adds, changes: 0, warnings, entries };
47
+ return { lines, creates, updates, warnings, entries };
48
48
  }
49
49
  for (const change of changes) {
50
50
  const warning = getWarning(change);
@@ -53,7 +53,7 @@ export function formatRepoSettingsPlan(changes) {
53
53
  }
54
54
  if (change.action === "add") {
55
55
  lines.push(chalk.green(` + ${change.property}: ${formatValue(change.newValue)}`));
56
- adds++;
56
+ creates++;
57
57
  entries.push({
58
58
  property: change.property,
59
59
  action: "add",
@@ -62,7 +62,7 @@ export function formatRepoSettingsPlan(changes) {
62
62
  }
63
63
  else if (change.action === "change") {
64
64
  lines.push(chalk.yellow(` ~ ${change.property}: ${formatValue(change.oldValue)} → ${formatValue(change.newValue)}`));
65
- changesCount++;
65
+ updates++;
66
66
  entries.push({
67
67
  property: change.property,
68
68
  action: "change",
@@ -71,7 +71,7 @@ export function formatRepoSettingsPlan(changes) {
71
71
  });
72
72
  }
73
73
  }
74
- return { lines, adds, changes: changesCount, warnings, entries };
74
+ return { lines, creates, updates, warnings, entries };
75
75
  }
76
76
  /**
77
77
  * Formats warnings for display.
@@ -1,8 +1,9 @@
1
1
  import { ICommandExecutor } from "../../shared/command-executor.js";
2
- import { RepoInfo } from "../../shared/repo-detector.js";
2
+ import { type RepoInfo } from "../../shared/repo-detector.js";
3
+ import { type GhApiOptions } from "../../shared/gh-api-utils.js";
3
4
  import type { GitHubRepoSettings } from "../../config/index.js";
4
- import type { IRepoSettingsStrategy, RepoSettingsStrategyOptions, CurrentRepoSettings } from "./types.js";
5
- export interface GitHubRepoSettingsStrategyOptions {
5
+ import type { IRepoSettingsStrategy, CurrentRepoSettings } from "./types.js";
6
+ interface GitHubRepoSettingsStrategyOptions {
6
7
  retries?: number;
7
8
  }
8
9
  /**
@@ -12,17 +13,15 @@ export interface GitHubRepoSettingsStrategyOptions {
12
13
  * with other strategies in this codebase. Inputs are escaped via escapeShellArg.
13
14
  */
14
15
  export declare class GitHubRepoSettingsStrategy implements IRepoSettingsStrategy {
15
- private executor;
16
- private retries;
16
+ private api;
17
17
  constructor(executor?: ICommandExecutor, options?: GitHubRepoSettingsStrategyOptions);
18
- getSettings(repoInfo: RepoInfo, options?: RepoSettingsStrategyOptions): Promise<CurrentRepoSettings>;
19
- updateSettings(repoInfo: RepoInfo, settings: GitHubRepoSettings, options?: RepoSettingsStrategyOptions): Promise<void>;
20
- setVulnerabilityAlerts(repoInfo: RepoInfo, enable: boolean, options?: RepoSettingsStrategyOptions): Promise<void>;
21
- setAutomatedSecurityFixes(repoInfo: RepoInfo, enable: boolean, options?: RepoSettingsStrategyOptions): Promise<void>;
22
- setPrivateVulnerabilityReporting(repoInfo: RepoInfo, enable: boolean, options?: RepoSettingsStrategyOptions): Promise<void>;
18
+ getSettings(repoInfo: RepoInfo, options?: GhApiOptions): Promise<CurrentRepoSettings>;
19
+ updateSettings(repoInfo: RepoInfo, settings: GitHubRepoSettings, options?: GhApiOptions): Promise<void>;
20
+ setVulnerabilityAlerts(repoInfo: RepoInfo, enable: boolean, options?: GhApiOptions): Promise<void>;
21
+ setAutomatedSecurityFixes(repoInfo: RepoInfo, enable: boolean, options?: GhApiOptions): Promise<void>;
22
+ setPrivateVulnerabilityReporting(repoInfo: RepoInfo, enable: boolean, options?: GhApiOptions): Promise<void>;
23
23
  private getVulnerabilityAlerts;
24
24
  private getAutomatedSecurityFixes;
25
25
  private getPrivateVulnerabilityReporting;
26
- private validateGitHub;
27
- private ghApi;
28
26
  }
27
+ export {};
@@ -1,13 +1,7 @@
1
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
- import { withRetry } from "../../shared/retry-utils.js";
5
- /**
6
- * Converts camelCase to snake_case.
7
- */
8
- function camelToSnake(str) {
9
- return str.replace(/([A-Z])/g, "_$1").toLowerCase();
10
- }
2
+ import { assertGitHubRepo, } from "../../shared/repo-detector.js";
3
+ import { GhApiClient, parseApiJson, isHttp404Error, } from "../../shared/gh-api-utils.js";
4
+ import { camelToSnake } from "../../shared/string-utils.js";
11
5
  /**
12
6
  * Converts GitHubRepoSettings (camelCase) to GitHub API format (snake_case).
13
7
  */
@@ -69,70 +63,62 @@ function configToGitHubPayload(settings) {
69
63
  * with other strategies in this codebase. Inputs are escaped via escapeShellArg.
70
64
  */
71
65
  export class GitHubRepoSettingsStrategy {
72
- executor;
73
- retries;
66
+ api;
74
67
  constructor(executor, options) {
75
- this.executor = executor ?? defaultExecutor;
76
- this.retries = options?.retries ?? 3;
68
+ this.api = new GhApiClient(executor ?? defaultExecutor, options?.retries ?? 3);
77
69
  }
78
70
  async getSettings(repoInfo, options) {
79
- this.validateGitHub(repoInfo);
80
- const github = repoInfo;
81
- const endpoint = `/repos/${github.owner}/${github.repo}`;
82
- const result = await this.ghApi("GET", endpoint, undefined, options);
83
- const parsed = JSON.parse(result);
71
+ assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
72
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}`;
73
+ const result = await this.api.call("GET", endpoint, undefined, options);
74
+ const parsed = parseApiJson(result, "repo settings response");
84
75
  const settings = parsed;
85
76
  // Extract owner type from nested API response
86
77
  settings.owner_type = parsed.owner?.type;
87
78
  // Fetch security settings from separate endpoints
88
- settings.vulnerability_alerts = await this.getVulnerabilityAlerts(github, options);
79
+ settings.vulnerability_alerts = await this.getVulnerabilityAlerts(repoInfo, options);
89
80
  // Pass vulnerability_alerts state - automated security fixes requires it enabled
90
- settings.automated_security_fixes = await this.getAutomatedSecurityFixes(github, options, settings.vulnerability_alerts);
81
+ settings.automated_security_fixes = await this.getAutomatedSecurityFixes(repoInfo, options, settings.vulnerability_alerts);
91
82
  settings.private_vulnerability_reporting =
92
- await this.getPrivateVulnerabilityReporting(github, options);
83
+ await this.getPrivateVulnerabilityReporting(repoInfo, options);
93
84
  return settings;
94
85
  }
95
86
  async updateSettings(repoInfo, settings, options) {
96
- this.validateGitHub(repoInfo);
97
- const github = repoInfo;
87
+ assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
98
88
  const payload = configToGitHubPayload(settings);
99
89
  // Skip if no settings to update
100
90
  if (Object.keys(payload).length === 0) {
101
91
  return;
102
92
  }
103
- const endpoint = `/repos/${github.owner}/${github.repo}`;
104
- await this.ghApi("PATCH", endpoint, payload, options);
93
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}`;
94
+ await this.api.call("PATCH", endpoint, payload, options);
105
95
  }
106
96
  async setVulnerabilityAlerts(repoInfo, enable, options) {
107
- this.validateGitHub(repoInfo);
108
- const github = repoInfo;
109
- const endpoint = `/repos/${github.owner}/${github.repo}/vulnerability-alerts`;
97
+ assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
98
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/vulnerability-alerts`;
110
99
  const method = enable ? "PUT" : "DELETE";
111
- await this.ghApi(method, endpoint, undefined, options);
100
+ await this.api.call(method, endpoint, undefined, options);
112
101
  }
113
102
  async setAutomatedSecurityFixes(repoInfo, enable, options) {
114
- this.validateGitHub(repoInfo);
115
- const github = repoInfo;
116
- const endpoint = `/repos/${github.owner}/${github.repo}/automated-security-fixes`;
103
+ assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
104
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/automated-security-fixes`;
117
105
  const method = enable ? "PUT" : "DELETE";
118
- await this.ghApi(method, endpoint, undefined, options);
106
+ await this.api.call(method, endpoint, undefined, options);
119
107
  }
120
108
  async setPrivateVulnerabilityReporting(repoInfo, enable, options) {
121
- this.validateGitHub(repoInfo);
122
- const github = repoInfo;
123
- const endpoint = `/repos/${github.owner}/${github.repo}/private-vulnerability-reporting`;
109
+ assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
110
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/private-vulnerability-reporting`;
124
111
  const method = enable ? "PUT" : "DELETE";
125
- await this.ghApi(method, endpoint, undefined, options);
112
+ await this.api.call(method, endpoint, undefined, options);
126
113
  }
127
114
  async getVulnerabilityAlerts(github, options) {
128
115
  const endpoint = `/repos/${github.owner}/${github.repo}/vulnerability-alerts`;
129
116
  try {
130
- await this.ghApi("GET", endpoint, undefined, options);
117
+ await this.api.call("GET", endpoint, undefined, options);
131
118
  return true; // 204 = enabled
132
119
  }
133
120
  catch (error) {
134
- const message = error instanceof Error ? error.message : String(error);
135
- if (message.includes("HTTP 404")) {
121
+ if (isHttp404Error(error)) {
136
122
  return false; // 404 = disabled
137
123
  }
138
124
  throw error; // Re-throw other errors
@@ -142,18 +128,17 @@ export class GitHubRepoSettingsStrategy {
142
128
  // Note: GitHub returns JSON with {enabled: boolean} for this endpoint
143
129
  const endpoint = `/repos/${github.owner}/${github.repo}/automated-security-fixes`;
144
130
  try {
145
- const result = await this.ghApi("GET", endpoint, undefined, options);
131
+ const result = await this.api.call("GET", endpoint, undefined, options);
146
132
  // Parse JSON response - GitHub returns {"enabled": true/false}
147
133
  if (result) {
148
- const data = JSON.parse(result);
134
+ const data = parseApiJson(result, "automated security fixes response");
149
135
  return data.enabled === true;
150
136
  }
151
137
  // Empty response (204) means enabled
152
138
  return true;
153
139
  }
154
140
  catch (error) {
155
- const message = error instanceof Error ? error.message : String(error);
156
- if (message.includes("HTTP 404")) {
141
+ if (isHttp404Error(error)) {
157
142
  return false;
158
143
  }
159
144
  throw error;
@@ -162,47 +147,15 @@ export class GitHubRepoSettingsStrategy {
162
147
  async getPrivateVulnerabilityReporting(github, options) {
163
148
  const endpoint = `/repos/${github.owner}/${github.repo}/private-vulnerability-reporting`;
164
149
  try {
165
- const result = await this.ghApi("GET", endpoint, undefined, options);
166
- const data = JSON.parse(result);
150
+ const result = await this.api.call("GET", endpoint, undefined, options);
151
+ const data = parseApiJson(result, "private vulnerability reporting response");
167
152
  return data.enabled === true;
168
153
  }
169
154
  catch (error) {
170
- const message = error instanceof Error ? error.message : String(error);
171
- if (message.includes("HTTP 404")) {
155
+ if (isHttp404Error(error)) {
172
156
  return false; // 404 = not available (e.g. private repos)
173
157
  }
174
158
  throw error; // Re-throw other errors
175
159
  }
176
160
  }
177
- validateGitHub(repoInfo) {
178
- if (!isGitHubRepo(repoInfo)) {
179
- throw new Error(`GitHub Repo Settings strategy requires GitHub repositories. Got: ${repoInfo.type}`);
180
- }
181
- }
182
- async ghApi(method, endpoint, payload, options) {
183
- const args = ["gh", "api"];
184
- if (method !== "GET") {
185
- args.push("-X", method);
186
- }
187
- if (options?.host && options.host !== "github.com") {
188
- args.push("--hostname", escapeShellArg(options.host));
189
- }
190
- args.push(escapeShellArg(endpoint));
191
- const baseCommand = args.join(" ");
192
- const tokenPrefix = options?.token
193
- ? `GH_TOKEN=${escapeShellArg(options.token)} `
194
- : "";
195
- if (payload &&
196
- (method === "POST" || method === "PUT" || method === "PATCH")) {
197
- const payloadJson = JSON.stringify(payload);
198
- const command = `echo ${escapeShellArg(payloadJson)} | ${tokenPrefix}${baseCommand} --input -`;
199
- return await withRetry(() => this.executor.exec(command, process.cwd()), {
200
- retries: this.retries,
201
- });
202
- }
203
- const command = `${tokenPrefix}${baseCommand}`;
204
- return await withRetry(() => this.executor.exec(command, process.cwd()), {
205
- retries: this.retries,
206
- });
207
- }
208
161
  }
@@ -1,5 +1,2 @@
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";
1
+ export { RepoSettingsProcessor, type IRepoSettingsProcessor, } from "./processor.js";
2
+ export { type RepoSettingsPlanEntry } from "./formatter.js";
@@ -1,10 +1,2 @@
1
- // Types
2
- export { isRepoSettingsStrategy, } from "./types.js";
3
- // Repo settings processor
1
+ // Processor
4
2
  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";