@aspruyt/xfg 6.1.0 → 6.3.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 (174) 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 +43 -6
  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/secrets-command.d.ts +25 -0
  10. package/dist/cli/secrets-command.js +75 -0
  11. package/dist/cli/settings-factories.d.ts +8 -0
  12. package/dist/cli/settings-factories.js +32 -0
  13. package/dist/cli/settings-report-builder.d.ts +7 -2
  14. package/dist/cli/settings-report-builder.js +28 -20
  15. package/dist/cli/settings-runner.d.ts +2 -0
  16. package/dist/cli/settings-runner.js +94 -0
  17. package/dist/cli/sync-command.d.ts +1 -1
  18. package/dist/cli/sync-command.js +31 -372
  19. package/dist/cli/sync-report-builder.d.ts +1 -1
  20. package/dist/cli/sync-utils.d.ts +8 -0
  21. package/dist/cli/sync-utils.js +36 -0
  22. package/dist/cli/types.d.ts +8 -8
  23. package/dist/cli/unified-summary.d.ts +1 -3
  24. package/dist/cli/unified-summary.js +7 -5
  25. package/dist/cli.js +2 -1
  26. package/dist/{shared → config}/env.js +2 -2
  27. package/dist/config/extends-resolver.js +4 -3
  28. package/dist/config/file-reference-resolver.js +4 -2
  29. package/dist/config/formatter.js +1 -0
  30. package/dist/config/index.d.ts +2 -2
  31. package/dist/config/index.js +1 -1
  32. package/dist/config/loader.js +30 -6
  33. package/dist/config/merge.d.ts +11 -1
  34. package/dist/config/merge.js +78 -6
  35. package/dist/config/normalizer.js +129 -49
  36. package/dist/config/types.d.ts +20 -0
  37. package/dist/config/validator.d.ts +5 -4
  38. package/dist/config/validator.js +187 -614
  39. package/dist/config/validators/file-validator.d.ts +2 -1
  40. package/dist/config/validators/file-validator.js +9 -1
  41. package/dist/config/validators/group-validator.d.ts +3 -0
  42. package/dist/config/validators/group-validator.js +167 -0
  43. package/dist/config/validators/repo-entry-validator.d.ts +2 -0
  44. package/dist/config/validators/repo-entry-validator.js +165 -0
  45. package/dist/config/validators/repo-settings-validator.js +18 -7
  46. package/dist/config/validators/ruleset-validator.js +2 -5
  47. package/dist/config/validators/shared.d.ts +11 -0
  48. package/dist/config/validators/shared.js +242 -0
  49. package/dist/lifecycle/ado-migration-source.js +2 -4
  50. package/dist/lifecycle/github-lifecycle-provider.d.ts +7 -11
  51. package/dist/lifecycle/github-lifecycle-provider.js +125 -136
  52. package/dist/lifecycle/{lifecycle-helpers.d.ts → helpers.d.ts} +5 -1
  53. package/dist/lifecycle/{lifecycle-helpers.js → helpers.js} +9 -8
  54. package/dist/lifecycle/index.d.ts +2 -2
  55. package/dist/lifecycle/index.js +1 -1
  56. package/dist/lifecycle/repo-lifecycle-factory.d.ts +2 -2
  57. package/dist/output/github-summary.js +2 -3
  58. package/dist/output/index.d.ts +4 -0
  59. package/dist/output/index.js +4 -0
  60. package/dist/output/lifecycle-report.d.ts +1 -1
  61. package/dist/output/lifecycle-report.js +5 -0
  62. package/dist/output/settings-report.d.ts +11 -0
  63. package/dist/output/settings-report.js +24 -0
  64. package/dist/output/sync-report.d.ts +25 -3
  65. package/dist/output/sync-report.js +11 -11
  66. package/dist/secrets/encryption.d.ts +9 -0
  67. package/dist/secrets/encryption.js +29 -0
  68. package/dist/secrets/github-secrets-strategy.d.ts +17 -0
  69. package/dist/secrets/github-secrets-strategy.js +38 -0
  70. package/dist/secrets/index.d.ts +5 -0
  71. package/dist/secrets/index.js +3 -0
  72. package/dist/secrets/processor.d.ts +31 -0
  73. package/dist/secrets/processor.js +115 -0
  74. package/dist/secrets/types.d.ts +21 -0
  75. package/dist/settings/base-processor.d.ts +18 -7
  76. package/dist/settings/base-processor.js +26 -5
  77. package/dist/settings/code-scanning/diff.js +2 -2
  78. package/dist/settings/code-scanning/formatter.d.ts +2 -6
  79. package/dist/settings/code-scanning/formatter.js +2 -25
  80. package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +3 -7
  81. package/dist/settings/code-scanning/github-code-scanning-strategy.js +2 -2
  82. package/dist/settings/code-scanning/processor.js +6 -4
  83. package/dist/settings/code-scanning/types.d.ts +10 -8
  84. package/dist/settings/index.d.ts +1 -0
  85. package/dist/settings/index.js +2 -0
  86. package/dist/settings/labels/github-labels-strategy.d.ts +3 -11
  87. package/dist/settings/labels/types.d.ts +12 -10
  88. package/dist/settings/repo-settings/diff.d.ts +1 -1
  89. package/dist/settings/repo-settings/diff.js +1 -1
  90. package/dist/settings/repo-settings/formatter.d.ts +2 -6
  91. package/dist/settings/repo-settings/formatter.js +4 -23
  92. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +2 -2
  93. package/dist/settings/repo-settings/github-repo-settings-strategy.js +8 -7
  94. package/dist/settings/repo-settings/processor.js +11 -11
  95. package/dist/settings/repo-settings/types.d.ts +2 -2
  96. package/dist/settings/rulesets/diff-algorithm.js +4 -2
  97. package/dist/settings/rulesets/diff.js +2 -51
  98. package/dist/settings/rulesets/formatter.js +4 -0
  99. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +3 -3
  100. package/dist/settings/rulesets/github-ruleset-strategy.js +4 -6
  101. package/dist/settings/rulesets/index.d.ts +1 -1
  102. package/dist/settings/rulesets/index.js +0 -2
  103. package/dist/settings/rulesets/processor.js +1 -1
  104. package/dist/settings/rulesets/types.d.ts +6 -2
  105. package/dist/settings/variables/diff.d.ts +10 -0
  106. package/dist/settings/variables/diff.js +39 -0
  107. package/dist/settings/variables/formatter.d.ts +16 -0
  108. package/dist/settings/variables/formatter.js +70 -0
  109. package/dist/settings/variables/github-variables-strategy.d.ts +17 -0
  110. package/dist/settings/variables/github-variables-strategy.js +40 -0
  111. package/dist/settings/variables/index.d.ts +4 -0
  112. package/dist/settings/variables/index.js +2 -0
  113. package/dist/settings/variables/processor.d.ts +19 -0
  114. package/dist/settings/variables/processor.js +60 -0
  115. package/dist/settings/variables/types.d.ts +18 -0
  116. package/dist/settings/variables/types.js +1 -0
  117. package/dist/shared/command-executor.d.ts +4 -4
  118. package/dist/shared/command-executor.js +9 -7
  119. package/dist/shared/diff-format.d.ts +1 -0
  120. package/dist/shared/diff-format.js +10 -0
  121. package/dist/shared/env-resolver.d.ts +16 -0
  122. package/dist/shared/env-resolver.js +33 -0
  123. package/dist/shared/errors.d.ts +7 -4
  124. package/dist/shared/errors.js +8 -8
  125. package/dist/shared/gh-api-utils.d.ts +3 -34
  126. package/dist/shared/gh-api-utils.js +23 -53
  127. package/dist/shared/gh-token-utils.d.ts +26 -0
  128. package/dist/shared/gh-token-utils.js +32 -0
  129. package/dist/shared/json-utils.js +1 -1
  130. package/dist/shared/regex-utils.d.ts +1 -0
  131. package/dist/shared/regex-utils.js +3 -0
  132. package/dist/shared/retry-utils.d.ts +1 -0
  133. package/dist/shared/retry-utils.js +13 -7
  134. package/dist/sync/auth-options-builder.js +1 -1
  135. package/dist/sync/branch-manager.js +5 -3
  136. package/dist/sync/commit-push-manager.js +2 -3
  137. package/dist/sync/diff-utils.d.ts +0 -1
  138. package/dist/sync/diff-utils.js +5 -10
  139. package/dist/sync/file-sync-orchestrator.js +0 -2
  140. package/dist/sync/file-writer.d.ts +3 -0
  141. package/dist/sync/file-writer.js +84 -81
  142. package/dist/sync/index.d.ts +0 -1
  143. package/dist/sync/index.js +0 -1
  144. package/dist/sync/manifest.js +1 -1
  145. package/dist/sync/pr-merge-handler.js +6 -6
  146. package/dist/sync/sync-workflow.js +1 -1
  147. package/dist/sync/types.d.ts +2 -2
  148. package/dist/vcs/ado-pr-strategy.d.ts +3 -5
  149. package/dist/vcs/ado-pr-strategy.js +131 -33
  150. package/dist/vcs/authenticated-git-ops.js +45 -23
  151. package/dist/vcs/git-commit-strategy.js +10 -6
  152. package/dist/vcs/git-ops.js +30 -24
  153. package/dist/vcs/github-pr-strategy.d.ts +3 -2
  154. package/dist/vcs/github-pr-strategy.js +80 -30
  155. package/dist/vcs/gitlab-pr-strategy.d.ts +2 -5
  156. package/dist/vcs/gitlab-pr-strategy.js +88 -87
  157. package/dist/vcs/graphql-commit-strategy.d.ts +1 -5
  158. package/dist/vcs/graphql-commit-strategy.js +21 -37
  159. package/dist/vcs/pr-creator.js +9 -2
  160. package/dist/vcs/pr-strategy.d.ts +2 -3
  161. package/dist/vcs/pr-strategy.js +0 -1
  162. package/dist/vcs/types.d.ts +9 -5
  163. package/package.json +7 -5
  164. package/dist/config/validators/index.d.ts +0 -3
  165. package/dist/config/validators/index.js +0 -6
  166. package/dist/output/types.d.ts +0 -20
  167. package/dist/shared/shell-utils.d.ts +0 -6
  168. package/dist/shared/shell-utils.js +0 -17
  169. /package/dist/{shared → config}/env.d.ts +0 -0
  170. /package/dist/lifecycle/{lifecycle-formatter.d.ts → formatter.d.ts} +0 -0
  171. /package/dist/lifecycle/{lifecycle-formatter.js → formatter.js} +0 -0
  172. /package/dist/{output → secrets}/types.js +0 -0
  173. /package/dist/{vcs → shared}/sanitize-utils.d.ts +0 -0
  174. /package/dist/{vcs → shared}/sanitize-utils.js +0 -0
@@ -0,0 +1,70 @@
1
+ import chalk from "chalk";
2
+ import { countActions } from "../base-processor.js";
3
+ export function formatVariablesPlan(changes) {
4
+ const lines = [];
5
+ const entries = [];
6
+ const { create: creates, update: updates, delete: deletes, unchanged, } = countActions(changes);
7
+ const grouped = {
8
+ create: [],
9
+ update: [],
10
+ delete: [],
11
+ unchanged: [],
12
+ };
13
+ for (const c of changes) {
14
+ grouped[c.action].push(c);
15
+ }
16
+ if (grouped.create.length > 0) {
17
+ lines.push(chalk.bold(" Create:"));
18
+ for (const change of grouped.create) {
19
+ lines.push(chalk.green(` + variable "${change.name}"`));
20
+ if (change.newValue !== undefined) {
21
+ lines.push(chalk.green(` value: "${change.newValue}"`));
22
+ }
23
+ entries.push({
24
+ name: change.name,
25
+ action: "create",
26
+ newValue: change.newValue,
27
+ });
28
+ lines.push("");
29
+ }
30
+ }
31
+ if (grouped.update.length > 0) {
32
+ lines.push(chalk.bold(" Update:"));
33
+ for (const change of grouped.update) {
34
+ lines.push(chalk.yellow(` ~ variable "${change.name}"`));
35
+ if (change.oldValue !== undefined && change.newValue !== undefined) {
36
+ lines.push(chalk.yellow(` value: "${change.oldValue}" → "${change.newValue}"`));
37
+ }
38
+ entries.push({
39
+ name: change.name,
40
+ action: "update",
41
+ oldValue: change.oldValue,
42
+ newValue: change.newValue,
43
+ });
44
+ lines.push("");
45
+ }
46
+ }
47
+ if (grouped.delete.length > 0) {
48
+ lines.push(chalk.bold(" Delete:"));
49
+ for (const change of grouped.delete) {
50
+ lines.push(chalk.red(` - variable "${change.name}"`));
51
+ entries.push({ name: change.name, action: "delete" });
52
+ }
53
+ lines.push("");
54
+ }
55
+ for (const change of grouped.unchanged) {
56
+ entries.push({ name: change.name, action: "unchanged" });
57
+ }
58
+ const total = creates + updates + deletes;
59
+ if (total > 0) {
60
+ const parts = [];
61
+ if (creates > 0)
62
+ parts.push(`${creates} to create`);
63
+ if (updates > 0)
64
+ parts.push(`${updates} to update`);
65
+ if (deletes > 0)
66
+ parts.push(`${deletes} to delete`);
67
+ lines.push(` Plan: ${total} variables (${parts.join(", ")})`);
68
+ }
69
+ return { lines, creates, updates, deletes, unchanged, entries };
70
+ }
@@ -0,0 +1,17 @@
1
+ import type { ICommandExecutor } from "../../shared/command-executor.js";
2
+ import { type RepoInfo } from "../../repo/index.js";
3
+ import { type GhApiOptions } from "../../shared/gh-api-utils.js";
4
+ import type { IVariablesStrategy, GitHubVariable } from "./types.js";
5
+ interface GitHubVariablesStrategyOptions {
6
+ retries?: number;
7
+ cwd: string;
8
+ }
9
+ export declare class GitHubVariablesStrategy implements IVariablesStrategy {
10
+ private api;
11
+ constructor(executor: ICommandExecutor, options: GitHubVariablesStrategyOptions);
12
+ list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubVariable[]>;
13
+ create(repoInfo: RepoInfo, name: string, value: string, options?: GhApiOptions): Promise<void>;
14
+ update(repoInfo: RepoInfo, name: string, value: string, options?: GhApiOptions): Promise<void>;
15
+ delete(repoInfo: RepoInfo, name: string, options?: GhApiOptions): Promise<void>;
16
+ }
17
+ export {};
@@ -0,0 +1,40 @@
1
+ import { assertGitHubRepo } from "../../repo/index.js";
2
+ import { GhApiClient } from "../../shared/gh-api-utils.js";
3
+ import { parseApiJson } from "../../shared/json-utils.js";
4
+ export class GitHubVariablesStrategy {
5
+ api;
6
+ constructor(executor, options) {
7
+ this.api = new GhApiClient(executor, options.retries ?? 3, options.cwd);
8
+ }
9
+ async list(repoInfo, options) {
10
+ assertGitHubRepo(repoInfo, "GitHub Variables strategy");
11
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/variables`;
12
+ const result = await this.api.call("GET", endpoint, {
13
+ options,
14
+ paginate: true,
15
+ });
16
+ const response = parseApiJson(result, "variables response");
17
+ return response.variables ?? [];
18
+ }
19
+ async create(repoInfo, name, value, options) {
20
+ assertGitHubRepo(repoInfo, "GitHub Variables strategy");
21
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/variables`;
22
+ await this.api.call("POST", endpoint, {
23
+ payload: { name, value },
24
+ options,
25
+ });
26
+ }
27
+ async update(repoInfo, name, value, options) {
28
+ assertGitHubRepo(repoInfo, "GitHub Variables strategy");
29
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/variables/${encodeURIComponent(name)}`;
30
+ await this.api.call("PATCH", endpoint, {
31
+ payload: { value },
32
+ options,
33
+ });
34
+ }
35
+ async delete(repoInfo, name, options) {
36
+ assertGitHubRepo(repoInfo, "GitHub Variables strategy");
37
+ const endpoint = `/repos/${repoInfo.owner}/${repoInfo.repo}/actions/variables/${encodeURIComponent(name)}`;
38
+ await this.api.call("DELETE", endpoint, { options });
39
+ }
40
+ }
@@ -0,0 +1,4 @@
1
+ export { type VariableChange, type VariableAction } from "./diff.js";
2
+ export { type VariablesPlanEntry } from "./formatter.js";
3
+ export { VariablesProcessor, type IVariablesProcessor } from "./processor.js";
4
+ export { GitHubVariablesStrategy } from "./github-variables-strategy.js";
@@ -0,0 +1,2 @@
1
+ export { VariablesProcessor } from "./processor.js";
2
+ export { GitHubVariablesStrategy } from "./github-variables-strategy.js";
@@ -0,0 +1,19 @@
1
+ import type { RepoConfig } from "../../config/index.js";
2
+ import type { RepoInfo } from "../../repo/index.js";
3
+ import { type VariablesPlanResult } from "./formatter.js";
4
+ import type { IVariablesStrategy } from "./types.js";
5
+ import { type BaseProcessorOptions, type BaseProcessorResult, type ISettingsProcessor, type ChangeCounts } from "../base-processor.js";
6
+ export type IVariablesProcessor = ISettingsProcessor<VariablesProcessorOptions, VariablesProcessorResult>;
7
+ export interface VariablesProcessorOptions extends BaseProcessorOptions {
8
+ noDelete?: boolean;
9
+ }
10
+ export interface VariablesProcessorResult extends BaseProcessorResult {
11
+ changes?: ChangeCounts;
12
+ planOutput?: VariablesPlanResult;
13
+ }
14
+ export declare class VariablesProcessor implements IVariablesProcessor {
15
+ private readonly strategy;
16
+ constructor(strategy: IVariablesStrategy);
17
+ process(repoConfig: RepoConfig, repoInfo: RepoInfo, options: VariablesProcessorOptions): Promise<VariablesProcessorResult>;
18
+ private applySettings;
19
+ }
@@ -0,0 +1,60 @@
1
+ import { diffVariables } from "./diff.js";
2
+ import { formatVariablesPlan } from "./formatter.js";
3
+ import { withGitHubGuards, countActions, buildDryRunResult, buildApplyResult, } from "../base-processor.js";
4
+ export class VariablesProcessor {
5
+ strategy;
6
+ constructor(strategy) {
7
+ this.strategy = strategy;
8
+ }
9
+ async process(repoConfig, repoInfo, options) {
10
+ return withGitHubGuards(repoConfig, repoInfo, options, {
11
+ hasDesiredSettings: (rc) => {
12
+ const vars = rc.settings?.variables ?? {};
13
+ const { deleteOrphaned, ...entries } = vars;
14
+ return Object.keys(entries).length > 0 || deleteOrphaned === true;
15
+ },
16
+ emptySettingsMessage: "No variables configured",
17
+ applySettings: (githubRepo, rc, opts, token, repoName) => this.applySettings(githubRepo, rc, opts, token, repoName),
18
+ });
19
+ }
20
+ async applySettings(githubRepo, repoConfig, options, effectiveToken, repoName) {
21
+ const { dryRun, noDelete } = options;
22
+ const settings = repoConfig.settings;
23
+ const { deleteOrphaned: varDeleteOrphaned = false, ...desiredVariables } = (settings?.variables ?? {});
24
+ const deleteOrphaned = varDeleteOrphaned && !(noDelete ?? false);
25
+ const strategyOptions = { token: effectiveToken, host: githubRepo.host };
26
+ const currentVariables = await this.strategy.list(githubRepo, strategyOptions);
27
+ const changes = diffVariables(currentVariables, desiredVariables, deleteOrphaned);
28
+ const changeCounts = countActions(changes);
29
+ const planOutput = formatVariablesPlan(changes);
30
+ if (dryRun) {
31
+ return buildDryRunResult(repoName, changeCounts, { planOutput });
32
+ }
33
+ let appliedCount = 0;
34
+ for (const change of changes) {
35
+ switch (change.action) {
36
+ case "create":
37
+ if (change.newValue !== undefined) {
38
+ await this.strategy.create(githubRepo, change.name, change.newValue, strategyOptions);
39
+ appliedCount++;
40
+ }
41
+ break;
42
+ case "update":
43
+ if (change.newValue !== undefined) {
44
+ await this.strategy.update(githubRepo, change.name, change.newValue, strategyOptions);
45
+ appliedCount++;
46
+ }
47
+ break;
48
+ case "delete":
49
+ await this.strategy.delete(githubRepo, change.name, strategyOptions);
50
+ appliedCount++;
51
+ break;
52
+ case "unchanged":
53
+ break;
54
+ }
55
+ }
56
+ return buildApplyResult(repoName, changeCounts, appliedCount, {
57
+ planOutput,
58
+ });
59
+ }
60
+ }
@@ -0,0 +1,18 @@
1
+ import type { RepoInfo } from "../../repo/index.js";
2
+ import type { GhApiOptions } from "../../shared/gh-api-utils.js";
3
+ export interface GitHubVariable {
4
+ name: string;
5
+ value: string;
6
+ created_at: string;
7
+ updated_at: string;
8
+ }
9
+ export interface GitHubVariablesListResponse {
10
+ total_count: number;
11
+ variables: GitHubVariable[];
12
+ }
13
+ export interface IVariablesStrategy {
14
+ list(repoInfo: RepoInfo, options?: GhApiOptions): Promise<GitHubVariable[]>;
15
+ create(repoInfo: RepoInfo, name: string, value: string, options?: GhApiOptions): Promise<void>;
16
+ update(repoInfo: RepoInfo, name: string, value: string, options?: GhApiOptions): Promise<void>;
17
+ delete(repoInfo: RepoInfo, name: string, options?: GhApiOptions): Promise<void>;
18
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -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
+ }
@@ -0,0 +1,16 @@
1
+ export interface IEnvResolver {
2
+ resolve(envName: string): string;
3
+ resolveAll(entries: {
4
+ name: string;
5
+ envVar: string;
6
+ }[]): Map<string, string>;
7
+ }
8
+ export declare class EnvResolver implements IEnvResolver {
9
+ private readonly env;
10
+ constructor(env: Record<string, string | undefined>);
11
+ resolve(envName: string): string;
12
+ resolveAll(entries: {
13
+ name: string;
14
+ envVar: string;
15
+ }[]): Map<string, string>;
16
+ }
@@ -0,0 +1,33 @@
1
+ export class EnvResolver {
2
+ env;
3
+ constructor(env) {
4
+ this.env = env;
5
+ }
6
+ resolve(envName) {
7
+ const value = this.env[envName];
8
+ if (value === undefined) {
9
+ throw new Error(`Environment variable '${envName}' is not set.`);
10
+ }
11
+ if (value === "") {
12
+ throw new Error(`Environment variable '${envName}' is empty.`);
13
+ }
14
+ return value;
15
+ }
16
+ resolveAll(entries) {
17
+ const missing = new Set();
18
+ const result = new Map();
19
+ for (const { name, envVar } of entries) {
20
+ const value = this.env[envVar];
21
+ if (value === undefined || value === "") {
22
+ missing.add(envVar);
23
+ }
24
+ else {
25
+ result.set(name, value);
26
+ }
27
+ }
28
+ if (missing.size > 0) {
29
+ throw new Error(`Missing environment variables: ${[...missing].join(", ")}`);
30
+ }
31
+ return result;
32
+ }
33
+ }
@@ -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 {};