@aspruyt/xfg 6.0.3 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/dist/cli/lifecycle-report-builder.d.ts +2 -2
  2. package/dist/cli/lifecycle-report-builder.js +3 -11
  3. package/dist/cli/program.d.ts +2 -1
  4. package/dist/cli/program.js +2 -3
  5. package/dist/cli/repo-sync-runner.d.ts +24 -0
  6. package/dist/cli/repo-sync-runner.js +156 -0
  7. package/dist/cli/results-collector.d.ts +1 -1
  8. package/dist/cli/results-collector.js +2 -2
  9. package/dist/cli/settings-factories.d.ts +7 -0
  10. package/dist/cli/settings-factories.js +27 -0
  11. package/dist/cli/settings-report-builder.d.ts +1 -1
  12. package/dist/cli/settings-report-builder.js +12 -23
  13. package/dist/cli/settings-runner.d.ts +2 -0
  14. package/dist/cli/settings-runner.js +87 -0
  15. package/dist/cli/sync-command.d.ts +1 -1
  16. package/dist/cli/sync-command.js +31 -372
  17. package/dist/cli/sync-report-builder.d.ts +1 -1
  18. package/dist/cli/sync-utils.d.ts +8 -0
  19. package/dist/cli/sync-utils.js +36 -0
  20. package/dist/cli/types.d.ts +5 -7
  21. package/dist/cli/unified-summary.d.ts +1 -3
  22. package/dist/cli/unified-summary.js +7 -5
  23. package/dist/cli.js +2 -1
  24. package/dist/{shared → config}/env.js +2 -2
  25. package/dist/config/extends-resolver.js +4 -3
  26. package/dist/config/file-reference-resolver.js +4 -2
  27. package/dist/config/formatter.js +18 -1
  28. package/dist/config/index.d.ts +1 -1
  29. package/dist/config/loader.js +30 -6
  30. package/dist/config/merge.d.ts +11 -1
  31. package/dist/config/merge.js +78 -6
  32. package/dist/config/normalizer.js +53 -38
  33. package/dist/config/validator.d.ts +1 -4
  34. package/dist/config/validator.js +13 -599
  35. package/dist/config/validators/file-validator.d.ts +2 -1
  36. package/dist/config/validators/file-validator.js +9 -1
  37. package/dist/config/validators/group-validator.d.ts +3 -0
  38. package/dist/config/validators/group-validator.js +167 -0
  39. package/dist/config/validators/repo-entry-validator.d.ts +2 -0
  40. package/dist/config/validators/repo-entry-validator.js +165 -0
  41. package/dist/config/validators/repo-settings-validator.js +18 -7
  42. package/dist/config/validators/ruleset-validator.js +2 -5
  43. package/dist/config/validators/shared.d.ts +11 -0
  44. package/dist/config/validators/shared.js +242 -0
  45. package/dist/lifecycle/ado-migration-source.js +2 -4
  46. package/dist/lifecycle/github-lifecycle-provider.d.ts +7 -11
  47. package/dist/lifecycle/github-lifecycle-provider.js +125 -136
  48. package/dist/lifecycle/{lifecycle-helpers.d.ts → helpers.d.ts} +5 -1
  49. package/dist/lifecycle/{lifecycle-helpers.js → helpers.js} +9 -8
  50. package/dist/lifecycle/index.d.ts +2 -2
  51. package/dist/lifecycle/index.js +1 -1
  52. package/dist/lifecycle/repo-lifecycle-factory.d.ts +2 -2
  53. package/dist/output/github-summary.js +2 -3
  54. package/dist/output/index.d.ts +4 -0
  55. package/dist/output/index.js +4 -0
  56. package/dist/output/lifecycle-report.d.ts +1 -1
  57. package/dist/output/lifecycle-report.js +5 -0
  58. package/dist/output/sync-report.d.ts +25 -3
  59. package/dist/output/sync-report.js +11 -11
  60. package/dist/settings/base-processor.d.ts +18 -7
  61. package/dist/settings/base-processor.js +26 -5
  62. package/dist/settings/code-scanning/diff.js +2 -2
  63. package/dist/settings/code-scanning/formatter.d.ts +2 -6
  64. package/dist/settings/code-scanning/formatter.js +2 -25
  65. package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +3 -7
  66. package/dist/settings/code-scanning/github-code-scanning-strategy.js +2 -2
  67. package/dist/settings/code-scanning/processor.js +6 -4
  68. package/dist/settings/code-scanning/types.d.ts +10 -8
  69. package/dist/settings/labels/github-labels-strategy.d.ts +3 -11
  70. package/dist/settings/labels/types.d.ts +12 -10
  71. package/dist/settings/repo-settings/diff.d.ts +1 -1
  72. package/dist/settings/repo-settings/diff.js +1 -1
  73. package/dist/settings/repo-settings/formatter.d.ts +2 -6
  74. package/dist/settings/repo-settings/formatter.js +4 -23
  75. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -2
  76. package/dist/settings/repo-settings/github-repo-settings-strategy.js +8 -7
  77. package/dist/settings/repo-settings/processor.js +11 -11
  78. package/dist/settings/repo-settings/types.d.ts +2 -2
  79. package/dist/settings/rulesets/diff-algorithm.js +4 -2
  80. package/dist/settings/rulesets/diff.js +2 -51
  81. package/dist/settings/rulesets/formatter.js +4 -0
  82. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +3 -3
  83. package/dist/settings/rulesets/github-ruleset-strategy.js +4 -6
  84. package/dist/settings/rulesets/index.d.ts +1 -1
  85. package/dist/settings/rulesets/index.js +0 -2
  86. package/dist/settings/rulesets/processor.js +1 -1
  87. package/dist/settings/rulesets/types.d.ts +6 -2
  88. package/dist/shared/command-executor.d.ts +4 -4
  89. package/dist/shared/command-executor.js +9 -7
  90. package/dist/shared/diff-format.d.ts +1 -0
  91. package/dist/shared/diff-format.js +10 -0
  92. package/dist/shared/errors.d.ts +7 -4
  93. package/dist/shared/errors.js +8 -8
  94. package/dist/shared/gh-api-utils.d.ts +3 -34
  95. package/dist/shared/gh-api-utils.js +23 -53
  96. package/dist/shared/gh-token-utils.d.ts +26 -0
  97. package/dist/shared/gh-token-utils.js +32 -0
  98. package/dist/shared/json-utils.js +1 -1
  99. package/dist/shared/regex-utils.d.ts +1 -0
  100. package/dist/shared/regex-utils.js +3 -0
  101. package/dist/shared/retry-utils.d.ts +1 -0
  102. package/dist/shared/retry-utils.js +13 -7
  103. package/dist/sync/auth-options-builder.js +1 -1
  104. package/dist/sync/branch-manager.js +5 -3
  105. package/dist/sync/commit-push-manager.js +2 -3
  106. package/dist/sync/diff-utils.d.ts +0 -1
  107. package/dist/sync/diff-utils.js +5 -10
  108. package/dist/sync/file-sync-orchestrator.js +0 -2
  109. package/dist/sync/file-writer.d.ts +3 -0
  110. package/dist/sync/file-writer.js +84 -81
  111. package/dist/sync/index.d.ts +0 -1
  112. package/dist/sync/index.js +0 -1
  113. package/dist/sync/manifest.js +1 -1
  114. package/dist/sync/pr-merge-handler.js +6 -6
  115. package/dist/sync/sync-workflow.js +1 -1
  116. package/dist/sync/types.d.ts +2 -2
  117. package/dist/vcs/ado-pr-strategy.d.ts +3 -5
  118. package/dist/vcs/ado-pr-strategy.js +131 -33
  119. package/dist/vcs/authenticated-git-ops.js +45 -23
  120. package/dist/vcs/git-commit-strategy.js +10 -6
  121. package/dist/vcs/git-ops.js +30 -24
  122. package/dist/vcs/github-pr-strategy.d.ts +3 -2
  123. package/dist/vcs/github-pr-strategy.js +80 -30
  124. package/dist/vcs/gitlab-pr-strategy.d.ts +2 -5
  125. package/dist/vcs/gitlab-pr-strategy.js +88 -87
  126. package/dist/vcs/graphql-commit-strategy.d.ts +1 -5
  127. package/dist/vcs/graphql-commit-strategy.js +21 -37
  128. package/dist/vcs/pr-creator.js +9 -2
  129. package/dist/vcs/pr-strategy.d.ts +2 -3
  130. package/dist/vcs/pr-strategy.js +0 -1
  131. package/dist/vcs/types.d.ts +9 -5
  132. package/package.json +5 -5
  133. package/dist/config/validators/index.d.ts +0 -3
  134. package/dist/config/validators/index.js +0 -6
  135. package/dist/output/types.d.ts +0 -20
  136. package/dist/output/types.js +0 -1
  137. package/dist/shared/shell-utils.d.ts +0 -6
  138. package/dist/shared/shell-utils.js +0 -17
  139. /package/dist/{shared → config}/env.d.ts +0 -0
  140. /package/dist/lifecycle/{lifecycle-formatter.d.ts → formatter.d.ts} +0 -0
  141. /package/dist/lifecycle/{lifecycle-formatter.js → formatter.js} +0 -0
  142. /package/dist/{vcs → shared}/sanitize-utils.d.ts +0 -0
  143. /package/dist/{vcs → shared}/sanitize-utils.js +0 -0
@@ -78,14 +78,16 @@ function diffObjectArrays(currentArr, desiredArr, parentPath) {
78
78
  const currentByType = new Map();
79
79
  for (let i = 0; i < currentArr.length; i++) {
80
80
  const item = currentArr[i];
81
- const type = item.type;
81
+ const type = typeof item.type === "string" ? item.type : String(item.type ?? "");
82
82
  if (type)
83
83
  currentByType.set(type, { item, index: i });
84
84
  }
85
85
  const matchedTypes = new Set();
86
86
  for (let i = 0; i < desiredArr.length; i++) {
87
87
  const desiredItem = desiredArr[i];
88
- const type = desiredItem.type;
88
+ const type = typeof desiredItem.type === "string"
89
+ ? desiredItem.type
90
+ : String(desiredItem.type ?? "");
89
91
  const label = `[${i}] (${type})`;
90
92
  const currentEntry = currentByType.get(type);
91
93
  if (currentEntry) {
@@ -1,7 +1,9 @@
1
1
  import { RULESET_COMPARABLE_FIELDS } from "../../config/index.js";
2
+ import { findMatchKey } from "../../config/merge.js";
2
3
  import { isPlainObject } from "../../shared/type-guards.js";
3
4
  import { camelToSnake } from "../../shared/string-utils.js";
4
5
  import { countActions } from "../base-processor.js";
6
+ import { deepEqual } from "./diff-algorithm.js";
5
7
  /**
6
8
  * Normalizes a value recursively, converting keys to a consistent format (snake_case).
7
9
  * This allows comparing GitHub API responses (snake_case) with config (camelCase).
@@ -54,37 +56,6 @@ function normalizeConfigRuleset(ruleset) {
54
56
  };
55
57
  return normalizeRuleset(withDefaults);
56
58
  }
57
- /**
58
- * Performs deep equality comparison of two normalized values.
59
- */
60
- function deepEqual(a, b) {
61
- if (a === b) {
62
- return true;
63
- }
64
- if (a === null || b === null || a === undefined || b === undefined) {
65
- return a === b;
66
- }
67
- if (typeof a !== typeof b) {
68
- return false;
69
- }
70
- if (Array.isArray(a) && Array.isArray(b)) {
71
- if (a.length !== b.length) {
72
- return false;
73
- }
74
- return a.every((val, i) => deepEqual(val, b[i]));
75
- }
76
- if (typeof a === "object" && typeof b === "object") {
77
- const objA = a;
78
- const objB = b;
79
- const keysA = Object.keys(objA);
80
- const keysB = Object.keys(objB);
81
- if (keysA.length !== keysB.length) {
82
- return false;
83
- }
84
- return keysA.every((key) => deepEqual(objA[key], objB[key]));
85
- }
86
- return false;
87
- }
88
59
  /**
89
60
  * Projects `current` onto the shape of `desired`.
90
61
  * Only keeps keys/structure present in `desired`, filtering out API noise.
@@ -129,26 +100,6 @@ function projectObjects(current, desired) {
129
100
  }
130
101
  return result;
131
102
  }
132
- /**
133
- * Candidate keys for matching array items by identity rather than index.
134
- * Order matters — first key found across all items wins.
135
- */
136
- const MATCH_KEY_CANDIDATES = ["type", "actor_id"];
137
- /**
138
- * Finds a key that uniquely identifies items in both arrays.
139
- * Returns the first candidate key present in every item of both arrays, or undefined.
140
- */
141
- function findMatchKey(current, desired) {
142
- const allItems = [...current, ...desired];
143
- if (allItems.length === 0)
144
- return undefined;
145
- for (const candidate of MATCH_KEY_CANDIDATES) {
146
- const everyItemHasKey = allItems.every((item) => isPlainObject(item) && candidate in item);
147
- if (everyItemHasKey)
148
- return candidate;
149
- }
150
- return undefined;
151
- }
152
103
  function projectArrays(current, desired) {
153
104
  // Primitive arrays — return current as-is
154
105
  if (desired.length === 0 || !isPlainObject(desired[0])) {
@@ -114,6 +114,10 @@ function getActionStyle(action) {
114
114
  return { symbol: "-", color: chalk.red };
115
115
  case "change":
116
116
  return { symbol: "~", color: chalk.yellow };
117
+ default: {
118
+ const _ = action;
119
+ throw new Error(`Unknown DiffAction: ${String(_)}`);
120
+ }
117
121
  }
118
122
  }
119
123
  function hasComplexValue(value) {
@@ -2,7 +2,7 @@ import type { ICommandExecutor } from "../../shared/command-executor.js";
2
2
  import { type RepoInfo } from "../../repo/index.js";
3
3
  import { type GhApiOptions } from "../../shared/gh-api-utils.js";
4
4
  import type { Ruleset } from "../../config/index.js";
5
- import type { IRulesetStrategy, GitHubRuleset, GitHubBypassActor, GitHubRulesetConditions, GitHubRule, RulesetUpdateParams } from "./types.js";
5
+ import type { IRulesetStrategy, GitHubRuleset, GitHubBypassActor, GitHubRulesetConditions, GitHubRule, RulesetCreateParams, RulesetUpdateParams } from "./types.js";
6
6
  /**
7
7
  * Converts camelCase config ruleset to snake_case GitHub API format.
8
8
  */
@@ -24,8 +24,8 @@ export declare class GitHubRulesetStrategy implements IRulesetStrategy {
24
24
  constructor(executor: ICommandExecutor, options: GitHubRulesetStrategyOptions);
25
25
  list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubRuleset[]>;
26
26
  get(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<GitHubRuleset>;
27
- create(repoInfo: RepoInfo, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
28
- update(repoInfo: RepoInfo, params: RulesetUpdateParams, options?: GhApiOptions): Promise<GitHubRuleset>;
27
+ create(repoInfo: RepoInfo, params: RulesetCreateParams, options?: GhApiOptions): Promise<void>;
28
+ update(repoInfo: RepoInfo, params: RulesetUpdateParams, options?: GhApiOptions): Promise<void>;
29
29
  delete(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<void>;
30
30
  }
31
31
  export {};
@@ -111,22 +111,20 @@ export class GitHubRulesetStrategy {
111
111
  const result = await this.api.call("GET", endpoint, { options });
112
112
  return parseApiJson(result, "ruleset response");
113
113
  }
114
- async create(repoInfo, name, ruleset, options) {
114
+ async create(repoInfo, params, options) {
115
115
  assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
116
116
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets`;
117
- const payload = configToGitHub(name, ruleset);
118
- const result = await this.api.call("POST", endpoint, { payload, options });
119
- return parseApiJson(result, "ruleset response");
117
+ const payload = configToGitHub(params.name, params.ruleset);
118
+ await this.api.call("POST", endpoint, { payload, options });
120
119
  }
121
120
  async update(repoInfo, params, options) {
122
121
  assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
123
122
  const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/rulesets/${params.rulesetId}`;
124
123
  const payload = configToGitHub(params.name, params.ruleset);
125
- const result = await this.api.call("PUT", endpoint, {
124
+ await this.api.call("PUT", endpoint, {
126
125
  payload,
127
126
  options,
128
127
  });
129
- return parseApiJson(result, "ruleset response");
130
128
  }
131
129
  async delete(repoInfo, rulesetId, options) {
132
130
  assertGitHubRepo(repoInfo, "GitHub Ruleset strategy");
@@ -1,4 +1,4 @@
1
- export { computePropertyDiffs, deepEqual, isArrayOfObjects, type PropertyDiff, } from "./diff-algorithm.js";
1
+ export { type PropertyDiff } from "./diff-algorithm.js";
2
2
  export { type RulesetPlanEntry } from "./formatter.js";
3
3
  export { RulesetProcessor, type IRulesetProcessor } from "./processor.js";
4
4
  export { GitHubRulesetStrategy } from "./github-ruleset-strategy.js";
@@ -1,5 +1,3 @@
1
- // Diff algorithm - property-level diffing for ruleset comparisons
2
- export { computePropertyDiffs, deepEqual, isArrayOfObjects, } from "./diff-algorithm.js";
3
1
  // Processor
4
2
  export { RulesetProcessor } from "./processor.js";
5
3
  // Strategy
@@ -50,7 +50,7 @@ export class RulesetProcessor {
50
50
  switch (change.action) {
51
51
  case "create":
52
52
  if (change.desired) {
53
- await this.strategy.create(githubRepo, change.name, change.desired, strategyOptions);
53
+ await this.strategy.create(githubRepo, { name: change.name, ruleset: change.desired }, strategyOptions);
54
54
  appliedCount++;
55
55
  }
56
56
  break;
@@ -30,6 +30,10 @@ export interface GitHubRule {
30
30
  type: string;
31
31
  parameters?: Record<string, unknown>;
32
32
  }
33
+ export interface RulesetCreateParams {
34
+ name: string;
35
+ ruleset: Ruleset;
36
+ }
33
37
  export interface RulesetUpdateParams {
34
38
  rulesetId: number;
35
39
  name: string;
@@ -38,7 +42,7 @@ export interface RulesetUpdateParams {
38
42
  export interface IRulesetStrategy {
39
43
  list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubRuleset[]>;
40
44
  get(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<GitHubRuleset>;
41
- create(repoInfo: RepoInfo, name: string, ruleset: Ruleset, options?: GhApiOptions): Promise<GitHubRuleset>;
42
- update(repoInfo: RepoInfo, params: RulesetUpdateParams, options?: GhApiOptions): Promise<GitHubRuleset>;
45
+ create(repoInfo: RepoInfo, params: RulesetCreateParams, options?: GhApiOptions): Promise<void>;
46
+ update(repoInfo: RepoInfo, params: RulesetUpdateParams, options?: GhApiOptions): Promise<void>;
43
47
  delete(repoInfo: RepoInfo, rulesetId: number, options?: GhApiOptions): Promise<void>;
44
48
  }
@@ -1,14 +1,14 @@
1
1
  export interface ExecOptions {
2
- /** Additional environment variables to set for the command */
3
2
  env?: Record<string, string>;
3
+ input?: string;
4
4
  }
5
5
  export interface ICommandExecutor {
6
- exec(command: string, cwd: string, options?: ExecOptions): Promise<string>;
6
+ exec(executable: string, args: string[], cwd: string, options?: ExecOptions): Promise<string>;
7
7
  }
8
- export declare class ShellCommandExecutor implements ICommandExecutor {
8
+ export declare class ProcessExecutor implements ICommandExecutor {
9
9
  private readonly baseEnv;
10
10
  constructor(baseEnv: Record<string, string | undefined>);
11
- exec(command: string, cwd: string, options?: ExecOptions): Promise<string>;
11
+ exec(executable: string, args: string[], cwd: string, options?: ExecOptions): Promise<string>;
12
12
  }
13
13
  /** Extract stderr string from an exec error (child_process errors attach stderr). */
14
14
  export declare function getStderr(error: unknown): string;
@@ -1,24 +1,23 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { sanitizeCredentials } from "../vcs/sanitize-utils.js";
3
- export class ShellCommandExecutor {
2
+ import { sanitizeCredentials } from "./sanitize-utils.js";
3
+ export class ProcessExecutor {
4
4
  baseEnv;
5
5
  constructor(baseEnv) {
6
6
  this.baseEnv = baseEnv;
7
7
  }
8
- async exec(command, cwd, options) {
8
+ async exec(executable, args, cwd, options) {
9
9
  try {
10
- return execFileSync("sh", ["-c", command], {
10
+ return execFileSync(executable, args, {
11
11
  cwd,
12
12
  encoding: "utf-8",
13
13
  stdio: ["pipe", "pipe", "pipe"],
14
+ input: options?.input,
14
15
  env: options?.env
15
16
  ? { ...this.baseEnv, ...options.env }
16
17
  : this.baseEnv,
17
18
  }).trim();
18
19
  }
19
20
  catch (error) {
20
- // Normalise and sanitise the exec error so downstream retry logic
21
- // sees a string stderr with no raw credentials.
22
21
  const execError = error;
23
22
  if (execError.stderr && typeof execError.stderr !== "string") {
24
23
  execError.stderr = execError.stderr.toString();
@@ -41,7 +40,10 @@ export class ShellCommandExecutor {
41
40
  export function getStderr(error) {
42
41
  if (error != null && typeof error === "object" && "stderr" in error) {
43
42
  const { stderr } = error;
44
- return typeof stderr === "string" ? stderr : "";
43
+ if (typeof stderr === "string")
44
+ return stderr;
45
+ if (Buffer.isBuffer(stderr))
46
+ return stderr.toString();
45
47
  }
46
48
  return "";
47
49
  }
@@ -0,0 +1 @@
1
+ export declare function formatDiffLine(line: string): string;
@@ -0,0 +1,10 @@
1
+ import chalk from "chalk";
2
+ export function formatDiffLine(line) {
3
+ if (line.startsWith("+"))
4
+ return chalk.green(line);
5
+ if (line.startsWith("-"))
6
+ return chalk.red(line);
7
+ if (line.startsWith("@@"))
8
+ return chalk.cyan(line);
9
+ return line;
10
+ }
@@ -5,7 +5,7 @@
5
5
  */
6
6
  export declare class ValidationError extends Error {
7
7
  readonly name = "ValidationError";
8
- constructor(message: string);
8
+ constructor(message: string, options?: ErrorOptions);
9
9
  }
10
10
  /**
11
11
  * Thrown when a GitHub GraphQL API call fails.
@@ -14,13 +14,16 @@ export declare class ValidationError extends Error {
14
14
  */
15
15
  export declare class GraphQLApiError extends Error {
16
16
  readonly name = "GraphQLApiError";
17
- constructor(message: string);
17
+ constructor(message: string, options?: ErrorOptions);
18
18
  }
19
19
  export declare class SyncError extends Error {
20
20
  readonly name = "SyncError";
21
- constructor(message: string);
21
+ constructor(message: string, options?: ErrorOptions);
22
+ }
23
+ export interface RateLimitedError {
24
+ retryAfter?: number;
22
25
  }
23
26
  export declare class LifecycleError extends Error {
24
27
  readonly name = "LifecycleError";
25
- constructor(message: string);
28
+ constructor(message: string, options?: ErrorOptions);
26
29
  }
@@ -5,8 +5,8 @@
5
5
  */
6
6
  export class ValidationError extends Error {
7
7
  name = "ValidationError";
8
- constructor(message) {
9
- super(message);
8
+ constructor(message, options) {
9
+ super(message, options);
10
10
  }
11
11
  }
12
12
  /**
@@ -16,19 +16,19 @@ export class ValidationError extends Error {
16
16
  */
17
17
  export class GraphQLApiError extends Error {
18
18
  name = "GraphQLApiError";
19
- constructor(message) {
20
- super(message);
19
+ constructor(message, options) {
20
+ super(message, options);
21
21
  }
22
22
  }
23
23
  export class SyncError extends Error {
24
24
  name = "SyncError";
25
- constructor(message) {
26
- super(message);
25
+ constructor(message, options) {
26
+ super(message, options);
27
27
  }
28
28
  }
29
29
  export class LifecycleError extends Error {
30
30
  name = "LifecycleError";
31
- constructor(message) {
32
- super(message);
31
+ constructor(message, options) {
32
+ super(message, options);
33
33
  }
34
34
  }
@@ -1,12 +1,8 @@
1
1
  import type { ICommandExecutor } from "./command-executor.js";
2
- import type { DebugWarnLog } from "./logger.js";
3
2
  export interface GitHubApiTarget {
4
3
  host: string;
5
4
  owner: string;
6
5
  }
7
- interface ITokenManager {
8
- getTokenForRepo(repoInfo: GitHubApiTarget): Promise<string | null>;
9
- }
10
6
  type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
11
7
  export interface GhApiOptions {
12
8
  token?: string;
@@ -20,10 +16,10 @@ interface GhApiCallParams {
20
16
  _retryDelay?: (ms: number) => Promise<void>;
21
17
  }
22
18
  /**
23
- * Get the hostname flag for gh commands.
24
- * Returns "--hostname HOST" for GHE, empty string for github.com.
19
+ * Build the hostname args for gh commands.
20
+ * Returns ["--hostname", HOST] for GHE, empty array for github.com.
25
21
  */
26
- export declare function getHostnameFlag(repoInfo: Pick<GitHubApiTarget, "host">): string;
22
+ export declare function buildHostnameArgs(repoInfo: Pick<GitHubApiTarget, "host">): string[];
27
23
  export declare function buildTokenEnv(token?: string): Record<string, string> | undefined;
28
24
  /**
29
25
  * Strips HTTP response headers from `gh api --include` output.
@@ -31,13 +27,6 @@ export declare function buildTokenEnv(token?: string): Record<string, string> |
31
27
  * If no blank line is found, returns the full string (no headers present).
32
28
  */
33
29
  export declare function parseResponseBody(raw: string): string;
34
- /**
35
- * Parses Retry-After header from an exec error's stdout and attaches it
36
- * as error.retryAfter (number of seconds). Only extracts the numeric value
37
- * to avoid leaking tokens from other headers.
38
- *
39
- * No-op if stdout is absent or does not contain a numeric Retry-After header.
40
- */
41
30
  export declare function attachRetryAfter(error: unknown): void;
42
31
  /**
43
32
  * Extracts GitHub API validation error details from `gh api --include` stdout
@@ -59,24 +48,4 @@ export declare class GhApiClient {
59
48
  constructor(executor: ICommandExecutor, retries: number, cwd: string);
60
49
  call(method: HttpMethod, endpoint: string, params?: GhApiCallParams): Promise<string>;
61
50
  }
62
- interface ResolveGitHubTokenOptions {
63
- repoInfo: GitHubApiTarget;
64
- tokenManager: ITokenManager | null;
65
- context: string;
66
- log?: DebugWarnLog;
67
- envToken?: string;
68
- }
69
- /**
70
- * Resolve a GitHub token for a repo: GitHub App token → envToken fallback.
71
- * Returns { token, skipped } where skipped=true means no App installation found
72
- * for this owner (token will be undefined). Both sync and settings paths use this.
73
- */
74
- export declare function resolveGitHubToken(options: ResolveGitHubTokenOptions): Promise<{
75
- token: string | undefined;
76
- skipped: boolean;
77
- }>;
78
- /**
79
- * Check if an error message indicates an HTTP 404 response from the GitHub API.
80
- */
81
- export declare function isHttp404Error(error: unknown): boolean;
82
51
  export {};
@@ -1,15 +1,13 @@
1
- import { escapeShellArg } from "./shell-utils.js";
2
1
  import { withRetry } from "./retry-utils.js";
3
- import { toErrorMessage } from "./type-guards.js";
4
2
  /**
5
- * Get the hostname flag for gh commands.
6
- * Returns "--hostname HOST" for GHE, empty string for github.com.
3
+ * Build the hostname args for gh commands.
4
+ * Returns ["--hostname", HOST] for GHE, empty array for github.com.
7
5
  */
8
- export function getHostnameFlag(repoInfo) {
6
+ export function buildHostnameArgs(repoInfo) {
9
7
  if (repoInfo.host !== "github.com") {
10
- return `--hostname ${escapeShellArg(repoInfo.host)}`;
8
+ return ["--hostname", repoInfo.host];
11
9
  }
12
- return "";
10
+ return [];
13
11
  }
14
12
  export function buildTokenEnv(token) {
15
13
  return token ? { GH_TOKEN: token } : undefined;
@@ -38,10 +36,17 @@ export function parseResponseBody(raw) {
38
36
  *
39
37
  * No-op if stdout is absent or does not contain a numeric Retry-After header.
40
38
  */
39
+ function hasStdout(error) {
40
+ return (typeof error === "object" &&
41
+ error !== null &&
42
+ "stdout" in error &&
43
+ (typeof error.stdout === "string" ||
44
+ Buffer.isBuffer(error.stdout)));
45
+ }
41
46
  export function attachRetryAfter(error) {
42
- const stdout = error.stdout;
43
- if (!stdout)
47
+ if (!hasStdout(error))
44
48
  return;
49
+ const stdout = error.stdout;
45
50
  const stdoutStr = typeof stdout === "string" ? stdout : stdout.toString();
46
51
  const match = stdoutStr.match(/^retry-after:\s*(\d+)\s*$/im);
47
52
  if (match) {
@@ -57,9 +62,9 @@ export function attachRetryAfter(error) {
57
62
  * No-op if stdout is absent or does not contain parseable error JSON.
58
63
  */
59
64
  export function attachValidationDetails(error) {
60
- const stdout = error.stdout;
61
- if (!stdout)
65
+ if (!hasStdout(error))
62
66
  return;
67
+ const stdout = error.stdout;
63
68
  const stdoutStr = typeof stdout === "string" ? stdout : stdout.toString();
64
69
  const body = parseResponseBody(stdoutStr);
65
70
  try {
@@ -93,7 +98,7 @@ export function attachValidationDetails(error) {
93
98
  */
94
99
  async function ghApiCall(method, endpoint, opts) {
95
100
  const { executor, retries, cwd, apiOpts, payload, paginate } = opts;
96
- const args = ["gh", "api"];
101
+ const args = ["api"];
97
102
  if (method !== "GET") {
98
103
  args.push("-X", method);
99
104
  }
@@ -104,14 +109,13 @@ async function ghApiCall(method, endpoint, opts) {
104
109
  args.push("--include");
105
110
  }
106
111
  if (apiOpts?.host && apiOpts.host !== "github.com") {
107
- args.push("--hostname", escapeShellArg(apiOpts.host));
112
+ args.push("--hostname", apiOpts.host);
108
113
  }
109
- args.push(escapeShellArg(endpoint));
110
- const baseCommand = args.join(" ");
114
+ args.push(endpoint);
111
115
  const env = buildTokenEnv(apiOpts?.token);
112
- const execAndParse = async (command) => {
116
+ const execAndParse = async (execArgs, execOptions) => {
113
117
  try {
114
- const raw = await executor.exec(command, cwd, { env });
118
+ const raw = await executor.exec("gh", execArgs, cwd, execOptions);
115
119
  return paginate ? raw : parseResponseBody(raw);
116
120
  }
117
121
  catch (error) {
@@ -129,10 +133,9 @@ async function ghApiCall(method, endpoint, opts) {
129
133
  if (payload &&
130
134
  (method === "POST" || method === "PUT" || method === "PATCH")) {
131
135
  const payloadJson = JSON.stringify(payload);
132
- const command = `echo ${escapeShellArg(payloadJson)} | ${baseCommand} --input -`;
133
- return withRetry(() => execAndParse(command), retryOpts);
136
+ return withRetry(() => execAndParse([...args, "--input", "-"], { env, input: payloadJson }), retryOpts);
134
137
  }
135
- return withRetry(() => execAndParse(baseCommand), retryOpts);
138
+ return withRetry(() => execAndParse(args, { env }), retryOpts);
136
139
  }
137
140
  /**
138
141
  * Encapsulates executor + retries for GitHub API calls.
@@ -159,36 +162,3 @@ export class GhApiClient {
159
162
  });
160
163
  }
161
164
  }
162
- /**
163
- * Resolve a GitHub token for a repo: GitHub App token → envToken fallback.
164
- * Returns { token, skipped } where skipped=true means no App installation found
165
- * for this owner (token will be undefined). Both sync and settings paths use this.
166
- */
167
- export async function resolveGitHubToken(options) {
168
- const { repoInfo, tokenManager, context, log, envToken } = options;
169
- try {
170
- const appToken = await tokenManager?.getTokenForRepo(repoInfo);
171
- if (appToken === null) {
172
- // null = no installation found for this owner
173
- return { token: undefined, skipped: true };
174
- }
175
- // string = app token; undefined = no manager configured
176
- return { token: appToken ?? envToken, skipped: false };
177
- }
178
- catch (error) {
179
- const errorMsg = `GitHub App token resolution failed for ${context}: ${toErrorMessage(error)}`;
180
- if (envToken) {
181
- log?.debug(`${errorMsg}; falling back to GH_TOKEN`);
182
- }
183
- else {
184
- log?.warn(`${errorMsg}; no fallback token available`);
185
- }
186
- return { token: envToken, skipped: false };
187
- }
188
- }
189
- /**
190
- * Check if an error message indicates an HTTP 404 response from the GitHub API.
191
- */
192
- export function isHttp404Error(error) {
193
- return toErrorMessage(error).includes("HTTP 404");
194
- }
@@ -0,0 +1,26 @@
1
+ import type { DebugWarnLog } from "./logger.js";
2
+ import type { GitHubApiTarget } from "./gh-api-utils.js";
3
+ interface ITokenManager {
4
+ getTokenForRepo(repoInfo: GitHubApiTarget): Promise<string | null>;
5
+ }
6
+ export interface ResolveGitHubTokenOptions {
7
+ repoInfo: GitHubApiTarget;
8
+ tokenManager: ITokenManager | null;
9
+ context: string;
10
+ log?: DebugWarnLog;
11
+ envToken?: string;
12
+ }
13
+ /**
14
+ * Resolve a GitHub token for a repo: GitHub App token → envToken fallback.
15
+ * Returns { token, skipped } where skipped=true means no App installation found
16
+ * for this owner (token will be undefined). Both sync and settings paths use this.
17
+ */
18
+ export declare function resolveGitHubToken(options: ResolveGitHubTokenOptions): Promise<{
19
+ token: string | undefined;
20
+ skipped: boolean;
21
+ }>;
22
+ /**
23
+ * Check if an error message indicates an HTTP 404 response from the GitHub API.
24
+ */
25
+ export declare function isHttp404Error(error: unknown): boolean;
26
+ export {};
@@ -0,0 +1,32 @@
1
+ import { toErrorMessage } from "./type-guards.js";
2
+ /**
3
+ * Resolve a GitHub token for a repo: GitHub App token → envToken fallback.
4
+ * Returns { token, skipped } where skipped=true means no App installation found
5
+ * for this owner (token will be undefined). Both sync and settings paths use this.
6
+ */
7
+ export async function resolveGitHubToken(options) {
8
+ const { repoInfo, tokenManager, context, log, envToken } = options;
9
+ try {
10
+ const appToken = await tokenManager?.getTokenForRepo(repoInfo);
11
+ if (appToken === null) {
12
+ return { token: undefined, skipped: true };
13
+ }
14
+ return { token: appToken ?? envToken, skipped: false };
15
+ }
16
+ catch (error) {
17
+ const errorMsg = `GitHub App token resolution failed for ${context}: ${toErrorMessage(error)}`;
18
+ if (envToken) {
19
+ log?.debug(`${errorMsg}; falling back to GH_TOKEN`);
20
+ }
21
+ else {
22
+ log?.warn(`${errorMsg}; no fallback token available`);
23
+ }
24
+ return { token: envToken, skipped: false };
25
+ }
26
+ }
27
+ /**
28
+ * Check if an error message indicates an HTTP 404 response from the GitHub API.
29
+ */
30
+ export function isHttp404Error(error) {
31
+ return toErrorMessage(error).includes("HTTP 404");
32
+ }
@@ -11,6 +11,6 @@ export function parseApiJson(response, context) {
11
11
  }
12
12
  catch (error) {
13
13
  const preview = response.slice(0, 200);
14
- throw new SyncError(`Failed to parse ${context}: ${toErrorMessage(error)} — ${preview}`);
14
+ throw new SyncError(`Failed to parse ${context}: ${toErrorMessage(error)} — ${preview}`, { cause: error });
15
15
  }
16
16
  }
@@ -0,0 +1 @@
1
+ export declare function escapeRegExp(str: string): string;
@@ -0,0 +1,3 @@
1
+ export function escapeRegExp(str) {
2
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3
+ }
@@ -3,6 +3,7 @@
3
3
  * Auth failures, permission issues, and resource-not-found errors.
4
4
  */
5
5
  export declare const CORE_PERMANENT_ERROR_PATTERNS: RegExp[];
6
+ export declare const BRANCH_PROTECTION_ERROR_PATTERNS: RegExp[];
6
7
  /**
7
8
  * Default patterns indicating permanent errors that should NOT be retried.
8
9
  * Extends CORE_PERMANENT_ERROR_PATTERNS with git-CLI-specific patterns.