@aspruyt/xfg 4.0.2 → 4.0.4

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 (151) hide show
  1. package/dist/cli/index.d.ts +1 -1
  2. package/dist/cli/index.js +0 -6
  3. package/dist/cli/program.js +3 -2
  4. package/dist/cli/settings-report-builder.js +4 -4
  5. package/dist/cli/sync-command.js +72 -36
  6. package/dist/cli/sync-report-builder.d.ts +2 -6
  7. package/dist/cli/types.d.ts +2 -14
  8. package/dist/cli/types.js +1 -9
  9. package/dist/config/file-reference-resolver.js +13 -23
  10. package/dist/config/formatter.d.ts +0 -6
  11. package/dist/config/formatter.js +0 -9
  12. package/dist/config/index.d.ts +1 -2
  13. package/dist/config/index.js +0 -2
  14. package/dist/config/loader.d.ts +1 -1
  15. package/dist/config/loader.js +3 -3
  16. package/dist/config/normalizer.d.ts +1 -1
  17. package/dist/config/normalizer.js +44 -57
  18. package/dist/config/validator.d.ts +1 -1
  19. package/dist/config/validator.js +120 -121
  20. package/dist/config/validators/file-validator.d.ts +2 -4
  21. package/dist/config/validators/file-validator.js +3 -7
  22. package/dist/config/validators/repo-settings-validator.js +1 -1
  23. package/dist/config/validators/ruleset-validator.js +28 -12
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.js +0 -1
  26. package/dist/lifecycle/ado-migration-source.d.ts +2 -1
  27. package/dist/lifecycle/ado-migration-source.js +7 -5
  28. package/dist/lifecycle/github-lifecycle-provider.d.ts +6 -3
  29. package/dist/lifecycle/github-lifecycle-provider.js +29 -19
  30. package/dist/lifecycle/lifecycle-formatter.js +2 -1
  31. package/dist/lifecycle/lifecycle-helpers.d.ts +5 -1
  32. package/dist/lifecycle/lifecycle-helpers.js +4 -4
  33. package/dist/lifecycle/repo-lifecycle-factory.d.ts +4 -4
  34. package/dist/lifecycle/repo-lifecycle-factory.js +12 -9
  35. package/dist/lifecycle/repo-lifecycle-manager.d.ts +4 -1
  36. package/dist/lifecycle/repo-lifecycle-manager.js +11 -7
  37. package/dist/lifecycle/types.d.ts +0 -15
  38. package/dist/output/github-summary.d.ts +6 -5
  39. package/dist/output/github-summary.js +36 -52
  40. package/dist/output/index.d.ts +2 -2
  41. package/dist/output/index.js +1 -1
  42. package/dist/output/lifecycle-report.d.ts +2 -12
  43. package/dist/output/lifecycle-report.js +18 -35
  44. package/dist/output/settings-report.d.ts +4 -4
  45. package/dist/output/settings-report.js +6 -6
  46. package/dist/output/sync-report.d.ts +4 -6
  47. package/dist/output/sync-report.js +2 -2
  48. package/dist/output/unified-summary.d.ts +1 -0
  49. package/dist/output/unified-summary.js +8 -8
  50. package/dist/settings/base-processor.d.ts +1 -1
  51. package/dist/settings/base-processor.js +1 -1
  52. package/dist/settings/index.d.ts +3 -3
  53. package/dist/settings/index.js +3 -3
  54. package/dist/settings/labels/diff.js +3 -2
  55. package/dist/settings/labels/formatter.js +3 -3
  56. package/dist/settings/labels/github-labels-strategy.d.ts +2 -23
  57. package/dist/settings/labels/github-labels-strategy.js +8 -28
  58. package/dist/settings/labels/index.d.ts +1 -0
  59. package/dist/settings/labels/index.js +2 -0
  60. package/dist/settings/labels/processor.d.ts +2 -2
  61. package/dist/settings/labels/processor.js +3 -4
  62. package/dist/settings/labels/types.d.ts +0 -3
  63. package/dist/settings/repo-settings/diff.d.ts +1 -1
  64. package/dist/settings/repo-settings/diff.js +2 -2
  65. package/dist/settings/repo-settings/formatter.d.ts +1 -1
  66. package/dist/settings/repo-settings/formatter.js +4 -4
  67. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -7
  68. package/dist/settings/repo-settings/github-repo-settings-strategy.js +9 -17
  69. package/dist/settings/repo-settings/index.d.ts +1 -0
  70. package/dist/settings/repo-settings/index.js +2 -0
  71. package/dist/settings/repo-settings/processor.d.ts +2 -2
  72. package/dist/settings/repo-settings/processor.js +5 -6
  73. package/dist/settings/repo-settings/types.d.ts +9 -13
  74. package/dist/settings/repo-settings/types.js +1 -14
  75. package/dist/settings/rulesets/diff-algorithm.d.ts +0 -1
  76. package/dist/settings/rulesets/diff-algorithm.js +6 -8
  77. package/dist/settings/rulesets/formatter.js +15 -51
  78. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +2 -20
  79. package/dist/settings/rulesets/github-ruleset-strategy.js +6 -30
  80. package/dist/settings/rulesets/index.d.ts +2 -1
  81. package/dist/settings/rulesets/index.js +3 -1
  82. package/dist/settings/rulesets/processor.d.ts +2 -2
  83. package/dist/settings/rulesets/processor.js +3 -4
  84. package/dist/{vcs → shared}/branch-utils.js +5 -4
  85. package/dist/shared/command-executor.d.ts +2 -1
  86. package/dist/shared/command-executor.js +9 -5
  87. package/dist/shared/env.d.ts +6 -6
  88. package/dist/shared/env.js +10 -17
  89. package/dist/shared/errors.d.ts +26 -0
  90. package/dist/shared/errors.js +34 -0
  91. package/dist/shared/gh-api-utils.d.ts +21 -14
  92. package/dist/shared/gh-api-utils.js +33 -22
  93. package/dist/shared/index.d.ts +9 -2
  94. package/dist/shared/index.js +16 -2
  95. package/dist/shared/logger.d.ts +24 -1
  96. package/dist/shared/logger.js +8 -3
  97. package/dist/shared/repo-detector.js +9 -11
  98. package/dist/shared/retry-utils.d.ts +5 -7
  99. package/dist/shared/retry-utils.js +3 -10
  100. package/dist/shared/shell-utils.d.ts +0 -3
  101. package/dist/shared/shell-utils.js +2 -4
  102. package/dist/shared/type-guards.d.ts +2 -9
  103. package/dist/shared/type-guards.js +0 -6
  104. package/dist/shared/xfg-template.d.ts +2 -2
  105. package/dist/shared/xfg-template.js +2 -1
  106. package/dist/sync/auth-options-builder.d.ts +3 -2
  107. package/dist/sync/auth-options-builder.js +14 -10
  108. package/dist/sync/branch-manager.d.ts +12 -7
  109. package/dist/sync/branch-manager.js +4 -7
  110. package/dist/sync/commit-message.d.ts +1 -1
  111. package/dist/sync/commit-push-manager.d.ts +8 -2
  112. package/dist/sync/commit-push-manager.js +6 -5
  113. package/dist/sync/file-sync-orchestrator.js +17 -21
  114. package/dist/sync/file-writer.js +3 -5
  115. package/dist/sync/index.d.ts +1 -1
  116. package/dist/sync/manifest-manager.d.ts +1 -0
  117. package/dist/sync/manifest.d.ts +4 -7
  118. package/dist/sync/manifest.js +42 -45
  119. package/dist/sync/repository-processor.d.ts +5 -2
  120. package/dist/sync/repository-processor.js +11 -17
  121. package/dist/sync/repository-session.js +2 -1
  122. package/dist/sync/sync-workflow.d.ts +2 -2
  123. package/dist/sync/sync-workflow.js +16 -23
  124. package/dist/sync/types.d.ts +20 -25
  125. package/dist/vcs/authenticated-git-ops.d.ts +3 -4
  126. package/dist/vcs/authenticated-git-ops.js +5 -1
  127. package/dist/vcs/azure-pr-strategy.d.ts +6 -1
  128. package/dist/vcs/azure-pr-strategy.js +38 -31
  129. package/dist/vcs/commit-strategy-selector.d.ts +10 -19
  130. package/dist/vcs/commit-strategy-selector.js +8 -24
  131. package/dist/vcs/git-commit-strategy.d.ts +1 -1
  132. package/dist/vcs/git-commit-strategy.js +1 -3
  133. package/dist/vcs/git-ops.d.ts +4 -8
  134. package/dist/vcs/git-ops.js +18 -22
  135. package/dist/vcs/github-app-token-manager.js +9 -8
  136. package/dist/vcs/github-pr-strategy.js +18 -11
  137. package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
  138. package/dist/vcs/gitlab-pr-strategy.js +14 -7
  139. package/dist/vcs/graphql-commit-strategy.d.ts +1 -7
  140. package/dist/vcs/graphql-commit-strategy.js +24 -32
  141. package/dist/vcs/index.d.ts +2 -1
  142. package/dist/vcs/pr-creator.d.ts +6 -9
  143. package/dist/vcs/pr-strategy-factory.d.ts +1 -1
  144. package/dist/vcs/pr-strategy-factory.js +2 -1
  145. package/dist/vcs/pr-strategy.d.ts +1 -1
  146. package/dist/vcs/pr-strategy.js +2 -3
  147. package/dist/vcs/types.d.ts +6 -10
  148. package/package.json +2 -2
  149. package/dist/config/errors.d.ts +0 -9
  150. package/dist/config/errors.js +0 -11
  151. /package/dist/{vcs → shared}/branch-utils.d.ts +0 -0
@@ -1,4 +1,3 @@
1
- import { defaultExecutor, } from "../../shared/command-executor.js";
2
1
  import { assertGitHubRepo, } from "../../shared/repo-detector.js";
3
2
  import { GhApiClient, parseApiJson, isHttp404Error, } from "../../shared/gh-api-utils.js";
4
3
  import { camelToSnake } from "../../shared/string-utils.js";
@@ -56,26 +55,19 @@ function configToGitHubPayload(settings) {
56
55
  }
57
56
  return payload;
58
57
  }
59
- /**
60
- * GitHub Repository Settings Strategy.
61
- * Manages repository settings via GitHub REST API using `gh api` CLI.
62
- * Note: Uses exec via ICommandExecutor for gh CLI integration, consistent
63
- * with other strategies in this codebase. Inputs are escaped via escapeShellArg.
64
- */
65
58
  export class GitHubRepoSettingsStrategy {
66
59
  api;
67
60
  constructor(executor, options) {
68
- this.api = new GhApiClient(executor ?? defaultExecutor, options?.retries ?? 3);
61
+ this.api = new GhApiClient(executor, options.retries ?? 3, options.cwd);
69
62
  }
70
63
  async getSettings(repoInfo, options) {
71
64
  assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
72
65
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}`;
73
- const result = await this.api.call("GET", endpoint, undefined, options);
66
+ const result = await this.api.call("GET", endpoint, { options });
74
67
  const parsed = parseApiJson(result, "repo settings response");
75
68
  const settings = parsed;
76
69
  // Extract owner type from nested API response
77
70
  settings.owner_type = parsed.owner?.type;
78
- // Fetch security settings from separate endpoints
79
71
  settings.vulnerability_alerts = await this.getVulnerabilityAlerts(repoInfo, options);
80
72
  // Pass vulnerability_alerts state - automated security fixes requires it enabled
81
73
  settings.automated_security_fixes = await this.getAutomatedSecurityFixes(repoInfo, options, settings.vulnerability_alerts);
@@ -91,30 +83,30 @@ export class GitHubRepoSettingsStrategy {
91
83
  return;
92
84
  }
93
85
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}`;
94
- await this.api.call("PATCH", endpoint, payload, options);
86
+ await this.api.call("PATCH", endpoint, { payload, options });
95
87
  }
96
88
  async setVulnerabilityAlerts(repoInfo, enable, options) {
97
89
  assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
98
90
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/vulnerability-alerts`;
99
91
  const method = enable ? "PUT" : "DELETE";
100
- await this.api.call(method, endpoint, undefined, options);
92
+ await this.api.call(method, endpoint, { options });
101
93
  }
102
94
  async setAutomatedSecurityFixes(repoInfo, enable, options) {
103
95
  assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
104
96
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/automated-security-fixes`;
105
97
  const method = enable ? "PUT" : "DELETE";
106
- await this.api.call(method, endpoint, undefined, options);
98
+ await this.api.call(method, endpoint, { options });
107
99
  }
108
100
  async setPrivateVulnerabilityReporting(repoInfo, enable, options) {
109
101
  assertGitHubRepo(repoInfo, "GitHub Repo Settings strategy");
110
102
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/private-vulnerability-reporting`;
111
103
  const method = enable ? "PUT" : "DELETE";
112
- await this.api.call(method, endpoint, undefined, options);
104
+ await this.api.call(method, endpoint, { options });
113
105
  }
114
106
  async getVulnerabilityAlerts(github, options) {
115
107
  const endpoint = `/repos/${github.owner}/${github.repo}/vulnerability-alerts`;
116
108
  try {
117
- await this.api.call("GET", endpoint, undefined, options);
109
+ await this.api.call("GET", endpoint, { options });
118
110
  return true; // 204 = enabled
119
111
  }
120
112
  catch (error) {
@@ -128,7 +120,7 @@ export class GitHubRepoSettingsStrategy {
128
120
  // Note: GitHub returns JSON with {enabled: boolean} for this endpoint
129
121
  const endpoint = `/repos/${github.owner}/${github.repo}/automated-security-fixes`;
130
122
  try {
131
- const result = await this.api.call("GET", endpoint, undefined, options);
123
+ const result = await this.api.call("GET", endpoint, { options });
132
124
  // Parse JSON response - GitHub returns {"enabled": true/false}
133
125
  if (result) {
134
126
  const data = parseApiJson(result, "automated security fixes response");
@@ -147,7 +139,7 @@ export class GitHubRepoSettingsStrategy {
147
139
  async getPrivateVulnerabilityReporting(github, options) {
148
140
  const endpoint = `/repos/${github.owner}/${github.repo}/private-vulnerability-reporting`;
149
141
  try {
150
- const result = await this.api.call("GET", endpoint, undefined, options);
142
+ const result = await this.api.call("GET", endpoint, { options });
151
143
  const data = parseApiJson(result, "private vulnerability reporting response");
152
144
  return data.enabled === true;
153
145
  }
@@ -1,2 +1,3 @@
1
1
  export { RepoSettingsProcessor, type IRepoSettingsProcessor, } from "./processor.js";
2
+ export { GitHubRepoSettingsStrategy } from "./github-repo-settings-strategy.js";
2
3
  export { type RepoSettingsPlanEntry } from "./formatter.js";
@@ -1,2 +1,4 @@
1
1
  // Processor
2
2
  export { RepoSettingsProcessor, } from "./processor.js";
3
+ // Strategy
4
+ export { GitHubRepoSettingsStrategy } from "./github-repo-settings-strategy.js";
@@ -12,9 +12,9 @@ export interface RepoSettingsProcessorResult extends BaseProcessorResult {
12
12
  }
13
13
  export declare class RepoSettingsProcessor implements IRepoSettingsProcessor {
14
14
  private readonly strategy;
15
- constructor(strategy?: IRepoSettingsStrategy);
15
+ constructor(strategy: IRepoSettingsStrategy);
16
16
  process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: RepoSettingsProcessorOptions): Promise<RepoSettingsProcessorResult>;
17
- private processSettings;
17
+ private applySettings;
18
18
  private applyChanges;
19
19
  private validateSecuritySettings;
20
20
  }
@@ -1,23 +1,22 @@
1
- import { GitHubRepoSettingsStrategy } from "./github-repo-settings-strategy.js";
2
1
  import { diffRepoSettings, hasChanges } from "./diff.js";
3
2
  import { formatRepoSettingsPlan } from "./formatter.js";
4
3
  import { withGitHubGuards, buildDryRunResult, buildApplyResult, } from "../base-processor.js";
5
4
  export class RepoSettingsProcessor {
6
5
  strategy;
7
6
  constructor(strategy) {
8
- this.strategy = strategy ?? new GitHubRepoSettingsStrategy();
7
+ this.strategy = strategy;
9
8
  }
10
9
  async process(repoConfig, repoInfo, options) {
11
10
  return withGitHubGuards(repoConfig, repoInfo, options, {
12
11
  hasDesiredSettings: (rc) => {
13
- const s = rc.settings?.repo;
14
- return !!s && Object.keys(s).length > 0;
12
+ const repoSettings = rc.settings?.repo;
13
+ return !!repoSettings && Object.keys(repoSettings).length > 0;
15
14
  },
16
15
  emptySettingsMessage: "No repo settings configured",
17
- processSettings: (githubRepo, rc, opts, token, repoName) => this.processSettings(githubRepo, rc, opts, token, repoName),
16
+ applySettings: (githubRepo, rc, opts, token, repoName) => this.applySettings(githubRepo, rc, opts, token, repoName),
18
17
  });
19
18
  }
20
- async processSettings(githubRepo, repoConfig, options, effectiveToken, repoName) {
19
+ async applySettings(githubRepo, repoConfig, options, effectiveToken, repoName) {
21
20
  const { dryRun } = options;
22
21
  const desiredSettings = repoConfig.settings.repo;
23
22
  const strategyOptions = { token: effectiveToken, host: githubRepo.host };
@@ -1,5 +1,5 @@
1
1
  import type { RepoInfo } from "../../shared/repo-detector.js";
2
- import type { GitHubRepoSettings } from "../../config/index.js";
2
+ import type { GitHubRepoSettings, RepoVisibility, SquashMergeCommitTitle, SquashMergeCommitMessage, MergeCommitTitle, MergeCommitMessage } from "../../config/index.js";
3
3
  import type { GhApiOptions } from "../../shared/gh-api-utils.js";
4
4
  /**
5
5
  * Current repository settings from GitHub API (snake_case).
@@ -12,7 +12,7 @@ export interface CurrentRepoSettings {
12
12
  has_discussions?: boolean;
13
13
  is_template?: boolean;
14
14
  allow_forking?: boolean;
15
- visibility?: string;
15
+ visibility?: RepoVisibility;
16
16
  archived?: boolean;
17
17
  allow_squash_merge?: boolean;
18
18
  allow_merge_commit?: boolean;
@@ -20,21 +20,21 @@ export interface CurrentRepoSettings {
20
20
  allow_auto_merge?: boolean;
21
21
  delete_branch_on_merge?: boolean;
22
22
  allow_update_branch?: boolean;
23
- squash_merge_commit_title?: string;
24
- squash_merge_commit_message?: string;
25
- merge_commit_title?: string;
26
- merge_commit_message?: string;
23
+ squash_merge_commit_title?: SquashMergeCommitTitle;
24
+ squash_merge_commit_message?: SquashMergeCommitMessage;
25
+ merge_commit_title?: MergeCommitTitle;
26
+ merge_commit_message?: MergeCommitMessage;
27
27
  web_commit_signoff_required?: boolean;
28
28
  default_branch?: string;
29
29
  security_and_analysis?: {
30
30
  secret_scanning?: {
31
- status: string;
31
+ status: "enabled" | "disabled";
32
32
  };
33
33
  secret_scanning_push_protection?: {
34
- status: string;
34
+ status: "enabled" | "disabled";
35
35
  };
36
36
  secret_scanning_validity_checks?: {
37
- status: string;
37
+ status: "enabled" | "disabled";
38
38
  };
39
39
  };
40
40
  owner_type?: "User" | "Organization";
@@ -64,7 +64,3 @@ export interface IRepoSettingsStrategy {
64
64
  */
65
65
  setPrivateVulnerabilityReporting(repoInfo: RepoInfo, enable: boolean, options?: GhApiOptions): Promise<void>;
66
66
  }
67
- /**
68
- * Type guard to check if an object implements IRepoSettingsStrategy.
69
- */
70
- export declare function isRepoSettingsStrategy(obj: unknown): obj is IRepoSettingsStrategy;
@@ -1,14 +1 @@
1
- /**
2
- * Type guard to check if an object implements IRepoSettingsStrategy.
3
- */
4
- export function isRepoSettingsStrategy(obj) {
5
- if (typeof obj !== "object" || obj === null) {
6
- return false;
7
- }
8
- const strategy = obj;
9
- return (typeof strategy.getSettings === "function" &&
10
- typeof strategy.updateSettings === "function" &&
11
- typeof strategy.setVulnerabilityAlerts === "function" &&
12
- typeof strategy.setAutomatedSecurityFixes === "function" &&
13
- typeof strategy.setPrivateVulnerabilityReporting === "function");
14
- }
1
+ export {};
@@ -5,7 +5,6 @@ export interface PropertyDiff {
5
5
  oldValue?: unknown;
6
6
  newValue?: unknown;
7
7
  }
8
- export declare function isObject(val: unknown): val is Record<string, unknown>;
9
8
  export declare function deepEqual(a: unknown, b: unknown): boolean;
10
9
  export declare function isArrayOfObjects(arr: unknown[]): boolean;
11
10
  /**
@@ -1,7 +1,5 @@
1
1
  // src/settings/rulesets/diff-algorithm.ts
2
- export function isObject(val) {
3
- return val !== null && typeof val === "object" && !Array.isArray(val);
4
- }
2
+ import { isPlainObject } from "../../shared/type-guards.js";
5
3
  export function deepEqual(a, b) {
6
4
  if (a === b)
7
5
  return true;
@@ -14,7 +12,7 @@ export function deepEqual(a, b) {
14
12
  return false;
15
13
  return a.every((val, i) => deepEqual(val, b[i]));
16
14
  }
17
- if (isObject(a) && isObject(b)) {
15
+ if (isPlainObject(a) && isPlainObject(b)) {
18
16
  const keysA = Object.keys(a);
19
17
  const keysB = Object.keys(b);
20
18
  if (keysA.length !== keysB.length)
@@ -24,7 +22,7 @@ export function deepEqual(a, b) {
24
22
  return false;
25
23
  }
26
24
  export function isArrayOfObjects(arr) {
27
- return arr.length > 0 && arr.every((item) => isObject(item));
25
+ return arr.length > 0 && arr.every((item) => isPlainObject(item));
28
26
  }
29
27
  /**
30
28
  * Recursively compute property-level diffs between two objects.
@@ -46,7 +44,7 @@ export function computePropertyDiffs(current, desired, parentPath = []) {
46
44
  }
47
45
  else if (!deepEqual(currentVal, desiredVal)) {
48
46
  // Changed property
49
- if (isObject(currentVal) && isObject(desiredVal)) {
47
+ if (isPlainObject(currentVal) && isPlainObject(desiredVal)) {
50
48
  // Recurse into nested objects
51
49
  diffs.push(...computePropertyDiffs(currentVal, desiredVal, path));
52
50
  }
@@ -75,7 +73,7 @@ export function computePropertyDiffs(current, desired, parentPath = []) {
75
73
  */
76
74
  function diffObjectArrays(currentArr, desiredArr, parentPath) {
77
75
  const diffs = [];
78
- const hasType = desiredArr.every((item) => isObject(item) && "type" in item);
76
+ const hasType = desiredArr.every((item) => isPlainObject(item) && "type" in item);
79
77
  if (hasType) {
80
78
  // Match by type field
81
79
  const currentByType = new Map();
@@ -139,7 +137,7 @@ function diffObjectArrays(currentArr, desiredArr, parentPath) {
139
137
  oldValue: currentArr[i],
140
138
  });
141
139
  }
142
- else if (isObject(currentArr[i]) && isObject(desiredArr[i])) {
140
+ else if (isPlainObject(currentArr[i]) && isPlainObject(desiredArr[i])) {
143
141
  const itemDiffs = computePropertyDiffs(currentArr[i], desiredArr[i], [...parentPath, label]);
144
142
  diffs.push(...itemDiffs);
145
143
  }
@@ -1,6 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { projectToDesiredShape, normalizeRuleset, } from "./diff.js";
3
- import { computePropertyDiffs, isObject, } from "./diff-algorithm.js";
3
+ import { computePropertyDiffs, } from "./diff-algorithm.js";
4
+ import { isPlainObject } from "../../shared/type-guards.js";
4
5
  /**
5
6
  * Build a tree structure from flat property diffs.
6
7
  */
@@ -67,7 +68,7 @@ function renderNestedValue(val, action, indent) {
67
68
  if (Array.isArray(val)) {
68
69
  for (let i = 0; i < val.length; i++) {
69
70
  const item = val[i];
70
- if (isObject(item)) {
71
+ if (isPlainObject(item)) {
71
72
  const obj = item;
72
73
  const typeLabel = "type" in obj ? ` (${obj.type})` : "";
73
74
  lines.push(style.color(`${indentStr}${style.symbol} [${i}]${typeLabel}:`));
@@ -78,7 +79,7 @@ function renderNestedValue(val, action, indent) {
78
79
  }
79
80
  }
80
81
  }
81
- else if (isObject(val)) {
82
+ else if (isPlainObject(val)) {
82
83
  lines.push(...renderNestedObject(val, action, indent));
83
84
  }
84
85
  return lines;
@@ -90,11 +91,11 @@ function renderNestedObject(obj, action, indent) {
90
91
  for (const [key, value] of Object.entries(obj)) {
91
92
  if (value === null || value === undefined)
92
93
  continue;
93
- if (Array.isArray(value) && value.some((v) => isObject(v))) {
94
+ if (Array.isArray(value) && value.some((v) => isPlainObject(v))) {
94
95
  lines.push(style.color(`${indentStr}${style.symbol} ${key}:`));
95
96
  lines.push(...renderNestedValue(value, action, indent + 1));
96
97
  }
97
- else if (isObject(value)) {
98
+ else if (isPlainObject(value)) {
98
99
  lines.push(style.color(`${indentStr}${style.symbol} ${key}:`));
99
100
  lines.push(...renderNestedObject(value, action, indent + 1));
100
101
  }
@@ -135,12 +136,12 @@ function renderTree(node, indent = 0) {
135
136
  }
136
137
  else {
137
138
  // Leaf node with value
138
- const hasComplexNew = isObject(child.newValue) ||
139
+ const hasComplexNew = isPlainObject(child.newValue) ||
139
140
  (Array.isArray(child.newValue) &&
140
- child.newValue.some((v) => isObject(v)));
141
- const hasComplexOld = isObject(child.oldValue) ||
141
+ child.newValue.some((v) => isPlainObject(v)));
142
+ const hasComplexOld = isPlainObject(child.oldValue) ||
142
143
  (Array.isArray(child.oldValue) &&
143
- child.oldValue.some((v) => isObject(v)));
144
+ child.oldValue.some((v) => isPlainObject(v)));
144
145
  if (child.action === "add" && hasComplexNew) {
145
146
  lines.push(style.color(`${indentStr}${style.symbol} ${child.name}:`));
146
147
  lines.push(...renderNestedValue(child.newValue, child.action, indent + 1));
@@ -188,47 +189,10 @@ export function formatPropertyTree(diffs) {
188
189
  }
189
190
  /**
190
191
  * Format a full ruleset config as tree lines (for create action).
192
+ * Delegates to renderNestedObject which handles recursive rendering.
191
193
  */
192
194
  function formatFullConfig(ruleset, indent = 2) {
193
- const lines = [];
194
- const style = getActionStyle("add");
195
- function renderValue(key, value, currentIndent) {
196
- const pad = " ".repeat(currentIndent);
197
- if (value === null || value === undefined)
198
- return;
199
- if (Array.isArray(value)) {
200
- if (value.length === 0) {
201
- lines.push(style.color(`${pad}+ ${key}: []`));
202
- }
203
- else if (value.every((v) => typeof v !== "object")) {
204
- lines.push(style.color(`${pad}+ ${key}: ${formatValue(value)}`));
205
- }
206
- else {
207
- lines.push(style.color(`${pad}+ ${key}:`));
208
- for (const item of value) {
209
- if (typeof item === "object" && item !== null) {
210
- lines.push(style.color(`${pad} + ${JSON.stringify(item)}`));
211
- }
212
- else {
213
- lines.push(style.color(`${pad} + ${formatValue(item)}`));
214
- }
215
- }
216
- }
217
- }
218
- else if (typeof value === "object") {
219
- lines.push(style.color(`${pad}+ ${key}:`));
220
- for (const [k, v] of Object.entries(value)) {
221
- renderValue(k, v, currentIndent + 1);
222
- }
223
- }
224
- else {
225
- lines.push(style.color(`${pad}+ ${key}: ${formatValue(value)}`));
226
- }
227
- }
228
- for (const [key, value] of Object.entries(ruleset)) {
229
- renderValue(key, value, indent);
230
- }
231
- return lines;
195
+ return renderNestedObject(ruleset, "add", indent);
232
196
  }
233
197
  /**
234
198
  * Format ruleset changes as a Terraform-style plan.
@@ -244,11 +208,11 @@ export function formatRulesetPlan(changes) {
244
208
  const createChanges = changes.filter((c) => c.action === "create");
245
209
  const updateChanges = changes.filter((c) => c.action === "update");
246
210
  const deleteChanges = changes.filter((c) => c.action === "delete");
247
- const unchangedChanges = changes.filter((c) => c.action === "unchanged");
211
+ const unchangedItems = changes.filter((c) => c.action === "unchanged");
248
212
  creates = createChanges.length;
249
213
  updates = updateChanges.length;
250
214
  deletes = deleteChanges.length;
251
- unchanged = unchangedChanges.length;
215
+ unchanged = unchangedItems.length;
252
216
  if (createChanges.length > 0) {
253
217
  lines.push(chalk.bold(" Create:"));
254
218
  for (const change of createChanges) {
@@ -305,7 +269,7 @@ export function formatRulesetPlan(changes) {
305
269
  }
306
270
  lines.push(""); // Blank line after deletes
307
271
  }
308
- for (const change of unchangedChanges) {
272
+ for (const change of unchangedItems) {
309
273
  entries.push({ name: change.name, action: "unchanged" });
310
274
  }
311
275
  return { lines, creates, updates, deletes, unchanged, entries };
@@ -17,33 +17,15 @@ interface GitHubRulesetPayload {
17
17
  }
18
18
  interface GitHubRulesetStrategyOptions {
19
19
  retries?: number;
20
+ cwd: string;
20
21
  }
21
- /**
22
- * GitHub Ruleset Strategy for managing repository rulesets via GitHub REST API.
23
- * Uses `gh api` CLI for authentication and API calls.
24
- */
25
22
  export declare class GitHubRulesetStrategy implements IRulesetStrategy {
26
23
  private api;
27
- constructor(executor?: ICommandExecutor, options?: GitHubRulesetStrategyOptions);
28
- /**
29
- * Lists all rulesets for a repository.
30
- */
24
+ constructor(executor: ICommandExecutor, options: GitHubRulesetStrategyOptions);
31
25
  list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubRuleset[]>;
32
- /**
33
- * Gets a single ruleset by ID.
34
- */
35
26
  get(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<GitHubRuleset>;
36
- /**
37
- * Creates a new ruleset.
38
- */
39
27
  create(repoInfo: RepoInfo, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
40
- /**
41
- * Updates an existing ruleset.
42
- */
43
28
  update(repoInfo: RepoInfo, rulesetId: number, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
44
- /**
45
- * Deletes a ruleset.
46
- */
47
29
  delete(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<void>;
48
30
  }
49
31
  export {};
@@ -1,4 +1,3 @@
1
- import { defaultExecutor, } from "../../shared/command-executor.js";
2
1
  import { assertGitHubRepo } from "../../shared/repo-detector.js";
3
2
  import { camelToSnake } from "../../shared/string-utils.js";
4
3
  import { GhApiClient, parseApiJson, } from "../../shared/gh-api-utils.js";
@@ -33,10 +32,6 @@ export function configToGitHub(name, ruleset) {
33
32
  }
34
33
  return payload;
35
34
  }
36
- /**
37
- * Default parameters for pull_request rules.
38
- * GitHub API requires all parameters to be present.
39
- */
40
35
  const PULL_REQUEST_DEFAULTS = {
41
36
  required_approving_review_count: 0,
42
37
  dismiss_stale_reviews_on_push: false,
@@ -98,59 +93,40 @@ function convertValue(value) {
98
93
  }
99
94
  return value;
100
95
  }
101
- /**
102
- * GitHub Ruleset Strategy for managing repository rulesets via GitHub REST API.
103
- * Uses `gh api` CLI for authentication and API calls.
104
- */
105
96
  export class GitHubRulesetStrategy {
106
97
  api;
107
98
  constructor(executor, options) {
108
- this.api = new GhApiClient(executor ?? defaultExecutor, options?.retries ?? 3);
99
+ this.api = new GhApiClient(executor, options.retries ?? 3, options.cwd);
109
100
  }
110
- /**
111
- * Lists all rulesets for a repository.
112
- */
113
101
  async list(repoInfo, options) {
114
102
  assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
115
103
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets`;
116
- const result = await this.api.call("GET", endpoint, undefined, options);
104
+ const result = await this.api.call("GET", endpoint, { options });
117
105
  return parseApiJson(result, "rulesets response");
118
106
  }
119
- /**
120
- * Gets a single ruleset by ID.
121
- */
122
107
  async get(repoInfo, rulesetId, options) {
123
108
  assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
124
109
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets/${rulesetId}`;
125
- const result = await this.api.call("GET", endpoint, undefined, options);
110
+ const result = await this.api.call("GET", endpoint, { options });
126
111
  return parseApiJson(result, "ruleset response");
127
112
  }
128
- /**
129
- * Creates a new ruleset.
130
- */
131
113
  async create(repoInfo, name, ruleset, options) {
132
114
  assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
133
115
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets`;
134
116
  const payload = configToGitHub(name, ruleset);
135
- const result = await this.api.call("POST", endpoint, payload, options);
117
+ const result = await this.api.call("POST", endpoint, { payload, options });
136
118
  return parseApiJson(result, "ruleset response");
137
119
  }
138
- /**
139
- * Updates an existing ruleset.
140
- */
141
120
  async update(repoInfo, rulesetId, name, ruleset, options) {
142
121
  assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
143
122
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets/${rulesetId}`;
144
123
  const payload = configToGitHub(name, ruleset);
145
- const result = await this.api.call("PUT", endpoint, payload, options);
124
+ const result = await this.api.call("PUT", endpoint, { payload, options });
146
125
  return parseApiJson(result, "ruleset response");
147
126
  }
148
- /**
149
- * Deletes a ruleset.
150
- */
151
127
  async delete(repoInfo, rulesetId, options) {
152
128
  assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
153
129
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets/${rulesetId}`;
154
- await this.api.call("DELETE", endpoint, undefined, options);
130
+ await this.api.call("DELETE", endpoint, { options });
155
131
  }
156
132
  }
@@ -1,3 +1,4 @@
1
- export { computePropertyDiffs, deepEqual, isObject, isArrayOfObjects, type PropertyDiff, } from "./diff-algorithm.js";
1
+ export { computePropertyDiffs, deepEqual, isArrayOfObjects, type PropertyDiff, } from "./diff-algorithm.js";
2
2
  export { formatPropertyTree, type RulesetPlanEntry } from "./formatter.js";
3
3
  export { RulesetProcessor, type IRulesetProcessor } from "./processor.js";
4
+ export { GitHubRulesetStrategy } from "./github-ruleset-strategy.js";
@@ -1,6 +1,8 @@
1
1
  // Diff algorithm - property-level diffing for ruleset comparisons
2
- export { computePropertyDiffs, deepEqual, isObject, isArrayOfObjects, } from "./diff-algorithm.js";
2
+ export { computePropertyDiffs, deepEqual, isArrayOfObjects, } from "./diff-algorithm.js";
3
3
  // Formatter
4
4
  export { formatPropertyTree } from "./formatter.js";
5
5
  // Processor
6
6
  export { RulesetProcessor } from "./processor.js";
7
+ // Strategy
8
+ export { GitHubRulesetStrategy } from "./github-ruleset-strategy.js";
@@ -17,7 +17,7 @@ export interface RulesetProcessorResult extends BaseProcessorResult {
17
17
  */
18
18
  export declare class RulesetProcessor implements IRulesetProcessor {
19
19
  private readonly strategy;
20
- constructor(strategy?: IRulesetStrategy);
20
+ constructor(strategy: IRulesetStrategy);
21
21
  process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: RulesetProcessorOptions): Promise<RulesetProcessorResult>;
22
- private processSettings;
22
+ private applySettings;
23
23
  }
@@ -1,4 +1,3 @@
1
- import { GitHubRulesetStrategy } from "./github-ruleset-strategy.js";
2
1
  import { diffRulesets } from "./diff.js";
3
2
  import { formatRulesetPlan } from "./formatter.js";
4
3
  import { withGitHubGuards, countActions, buildDryRunResult, buildApplyResult, } from "../base-processor.js";
@@ -9,16 +8,16 @@ import { withGitHubGuards, countActions, buildDryRunResult, buildApplyResult, }
9
8
  export class RulesetProcessor {
10
9
  strategy;
11
10
  constructor(strategy) {
12
- this.strategy = strategy ?? new GitHubRulesetStrategy();
11
+ this.strategy = strategy;
13
12
  }
14
13
  async process(repoConfig, repoInfo, options) {
15
14
  return withGitHubGuards(repoConfig, repoInfo, options, {
16
15
  hasDesiredSettings: (rc) => Object.keys(rc.settings?.rulesets ?? {}).length > 0,
17
16
  emptySettingsMessage: "No rulesets configured",
18
- processSettings: (githubRepo, rc, opts, token, repoName) => this.processSettings(githubRepo, rc, opts, token, repoName),
17
+ applySettings: (githubRepo, rc, opts, token, repoName) => this.applySettings(githubRepo, rc, opts, token, repoName),
19
18
  });
20
19
  }
21
- async processSettings(githubRepo, repoConfig, options, effectiveToken, repoName) {
20
+ async applySettings(githubRepo, repoConfig, options, effectiveToken, repoName) {
22
21
  const { dryRun, noDelete } = options;
23
22
  const settings = repoConfig.settings;
24
23
  const desiredRulesets = settings?.rulesets ?? {};
@@ -1,3 +1,4 @@
1
+ import { ValidationError } from "./errors.js";
1
2
  export function sanitizeBranchName(fileName) {
2
3
  return fileName
3
4
  .toLowerCase()
@@ -12,18 +13,18 @@ export function sanitizeBranchName(fileName) {
12
13
  */
13
14
  export function validateBranchName(branchName) {
14
15
  if (!branchName || branchName.trim() === "") {
15
- throw new Error("Branch name cannot be empty");
16
+ throw new ValidationError("Branch name cannot be empty");
16
17
  }
17
18
  if (branchName.startsWith(".") || branchName.startsWith("-")) {
18
- throw new Error('Branch name cannot start with "." or "-"');
19
+ throw new ValidationError('Branch name cannot start with "." or "-"');
19
20
  }
20
21
  // Git disallows: space, ~, ^, :, ?, *, [, \, and consecutive dots (..)
21
22
  if (/[\s~^:?*[\\]/.test(branchName) || branchName.includes("..")) {
22
- throw new Error("Branch name contains invalid characters");
23
+ throw new ValidationError("Branch name contains invalid characters");
23
24
  }
24
25
  if (branchName.endsWith("/") ||
25
26
  branchName.endsWith(".lock") ||
26
27
  branchName.endsWith(".")) {
27
- throw new Error("Branch name has invalid ending");
28
+ throw new ValidationError("Branch name has invalid ending");
28
29
  }
29
30
  }
@@ -6,8 +6,9 @@ export interface ICommandExecutor {
6
6
  exec(command: string, cwd: string, options?: ExecOptions): Promise<string>;
7
7
  }
8
8
  export declare class ShellCommandExecutor implements ICommandExecutor {
9
+ private readonly baseEnv;
10
+ constructor(baseEnv: Record<string, string | undefined>);
9
11
  exec(command: string, cwd: string, options?: ExecOptions): Promise<string>;
10
12
  }
11
- export declare const defaultExecutor: ICommandExecutor;
12
13
  /** Extract stderr string from an exec error (child_process errors attach stderr). */
13
14
  export declare function getStderr(error: unknown): string;