@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,26 +1,31 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { sanitizeCredentials } from "./sanitize-utils.js";
3
3
  export class ShellCommandExecutor {
4
+ baseEnv;
5
+ constructor(baseEnv) {
6
+ this.baseEnv = baseEnv;
7
+ }
4
8
  async exec(command, cwd, options) {
5
9
  try {
6
10
  return execSync(command, {
7
11
  cwd,
8
12
  encoding: "utf-8",
9
13
  stdio: ["pipe", "pipe", "pipe"],
10
- env: options?.env ? { ...process.env, ...options.env } : undefined,
14
+ env: options?.env
15
+ ? { ...this.baseEnv, ...options.env }
16
+ : this.baseEnv,
11
17
  }).trim();
12
18
  }
13
19
  catch (error) {
14
- // Ensure stderr is always a string for consistent error handling
20
+ // Normalise and sanitise the exec error so downstream retry logic
21
+ // sees a string stderr with no raw credentials.
15
22
  const execError = error;
16
23
  if (execError.stderr && typeof execError.stderr !== "string") {
17
24
  execError.stderr = execError.stderr.toString();
18
25
  }
19
- // Sanitize credentials from stderr before including in error
20
26
  if (execError.stderr) {
21
27
  execError.stderr = sanitizeCredentials(execError.stderr);
22
28
  }
23
- // Include sanitized stderr in error message for better debugging
24
29
  if (execError.stderr && execError.message) {
25
30
  execError.message =
26
31
  sanitizeCredentials(execError.message) + "\n" + execError.stderr;
@@ -32,7 +37,6 @@ export class ShellCommandExecutor {
32
37
  }
33
38
  }
34
39
  }
35
- export const defaultExecutor = new ShellCommandExecutor();
36
40
  /** Extract stderr string from an exec error (child_process errors attach stderr). */
37
41
  export function getStderr(error) {
38
42
  if (error != null && typeof error === "object" && "stderr" in error) {
@@ -9,6 +9,10 @@ export interface EnvInterpolationOptions {
9
9
  * and has no default value. If false, leaves the placeholder as-is.
10
10
  */
11
11
  strict: boolean;
12
+ /**
13
+ * Environment variables to resolve from.
14
+ */
15
+ env: Record<string, string | undefined>;
12
16
  }
13
17
  /**
14
18
  * Interpolate environment variables in a JSON object.
@@ -18,14 +22,10 @@ export interface EnvInterpolationOptions {
18
22
  * - ${VAR:-default} - Replace with env value, or use default if missing
19
23
  * - ${VAR:?message} - Replace with env value, or throw error with message if missing
20
24
  * - $${VAR} - Escape: outputs literal ${VAR} without interpolation
21
- *
22
- * @param json - The JSON object to process
23
- * @param options - Interpolation options (default: strict mode)
24
- * @returns A new object with interpolated values
25
25
  */
26
- export declare function interpolateEnvVars(json: Record<string, unknown>, options?: EnvInterpolationOptions): Record<string, unknown>;
26
+ export declare function interpolateEnvVars(json: Record<string, unknown>, options: EnvInterpolationOptions): Record<string, unknown>;
27
27
  /**
28
28
  * Interpolate environment variables in content of any supported type.
29
29
  * Handles objects, strings, and string arrays.
30
30
  */
31
- export declare function interpolateContent(content: Record<string, unknown> | string | string[], options?: EnvInterpolationOptions): Record<string, unknown> | string | string[];
31
+ export declare function interpolateContent(content: Record<string, unknown> | string | string[], options: EnvInterpolationOptions): Record<string, unknown> | string | string[];
@@ -4,9 +4,7 @@
4
4
  * Use $${VAR} to escape and output literal ${VAR}.
5
5
  */
6
6
  import { interpolateString, interpolateValue, } from "./interpolation-engine.js";
7
- const DEFAULT_OPTIONS = {
8
- strict: true,
9
- };
7
+ import { ValidationError } from "./errors.js";
10
8
  /**
11
9
  * Regex to match environment variable placeholders.
12
10
  * Captures:
@@ -29,26 +27,25 @@ const ENV_VAR_REGEX = /\$\{([A-Za-z_][A-Za-z0-9_.]*)(?::([?-])([^}]*))?\}/g;
29
27
  */
30
28
  const ESCAPED_VAR_REGEX = /\$\$\{((?!xfg:)[^}]+)\}/g;
31
29
  function buildEnvConfig(options) {
30
+ const envSource = options.env;
32
31
  function resolveEnvVar(match, varName, modifier, defaultOrMsg) {
33
- const envValue = process.env[varName];
34
- // Variable exists - use its value
32
+ // Resolution follows bash parameter expansion semantics:
33
+ // ${VAR} value or error, ${VAR:-fallback} → value or fallback,
34
+ // ${VAR:?msg} → value or throw with msg.
35
+ const envValue = envSource[varName];
35
36
  if (envValue !== undefined) {
36
37
  return envValue;
37
38
  }
38
- // Has default value (:-default)
39
39
  if (modifier === "-") {
40
40
  return defaultOrMsg ?? "";
41
41
  }
42
- // Required with message (:?message)
43
42
  if (modifier === "?") {
44
43
  const message = defaultOrMsg || `is required`;
45
- throw new Error(`${varName}: ${message}`);
44
+ throw new ValidationError(`${varName}: ${message}`);
46
45
  }
47
- // No modifier - check strictness
48
46
  if (options.strict) {
49
- throw new Error(`Missing required environment variable: ${varName}`);
47
+ throw new ValidationError(`Missing required environment variable: ${varName}`);
50
48
  }
51
- // Non-strict mode - leave placeholder as-is
52
49
  return match;
53
50
  }
54
51
  return {
@@ -66,19 +63,15 @@ function buildEnvConfig(options) {
66
63
  * - ${VAR:-default} - Replace with env value, or use default if missing
67
64
  * - ${VAR:?message} - Replace with env value, or throw error with message if missing
68
65
  * - $${VAR} - Escape: outputs literal ${VAR} without interpolation
69
- *
70
- * @param json - The JSON object to process
71
- * @param options - Interpolation options (default: strict mode)
72
- * @returns A new object with interpolated values
73
66
  */
74
- export function interpolateEnvVars(json, options = DEFAULT_OPTIONS) {
67
+ export function interpolateEnvVars(json, options) {
75
68
  return interpolateValue(json, buildEnvConfig(options));
76
69
  }
77
70
  /**
78
71
  * Interpolate environment variables in content of any supported type.
79
72
  * Handles objects, strings, and string arrays.
80
73
  */
81
- export function interpolateContent(content, options = DEFAULT_OPTIONS) {
74
+ export function interpolateContent(content, options) {
82
75
  const config = buildEnvConfig(options);
83
76
  if (typeof content === "string") {
84
77
  return interpolateString(content, config);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Thrown when config validation fails.
3
+ * Distinguishable from I/O errors by type, so callers and retry logic
4
+ * can treat validation failures as permanent without message-parsing.
5
+ */
6
+ export declare class ValidationError extends Error {
7
+ readonly name = "ValidationError";
8
+ constructor(message: string);
9
+ }
10
+ /**
11
+ * Thrown when a GitHub GraphQL API call fails.
12
+ * Standardizes all GraphQL error messages under one type so catch blocks
13
+ * and retry logic can identify them without message-parsing.
14
+ */
15
+ export declare class GraphQLApiError extends Error {
16
+ readonly name = "GraphQLApiError";
17
+ constructor(message: string);
18
+ }
19
+ export declare class SyncError extends Error {
20
+ readonly name = "SyncError";
21
+ constructor(message: string);
22
+ }
23
+ export declare class LifecycleError extends Error {
24
+ readonly name = "LifecycleError";
25
+ constructor(message: string);
26
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Thrown when config validation fails.
3
+ * Distinguishable from I/O errors by type, so callers and retry logic
4
+ * can treat validation failures as permanent without message-parsing.
5
+ */
6
+ export class ValidationError extends Error {
7
+ name = "ValidationError";
8
+ constructor(message) {
9
+ super(message);
10
+ }
11
+ }
12
+ /**
13
+ * Thrown when a GitHub GraphQL API call fails.
14
+ * Standardizes all GraphQL error messages under one type so catch blocks
15
+ * and retry logic can identify them without message-parsing.
16
+ */
17
+ export class GraphQLApiError extends Error {
18
+ name = "GraphQLApiError";
19
+ constructor(message) {
20
+ super(message);
21
+ }
22
+ }
23
+ export class SyncError extends Error {
24
+ name = "SyncError";
25
+ constructor(message) {
26
+ super(message);
27
+ }
28
+ }
29
+ export class LifecycleError extends Error {
30
+ name = "LifecycleError";
31
+ constructor(message) {
32
+ super(message);
33
+ }
34
+ }
@@ -1,11 +1,19 @@
1
- import type { ICommandExecutor } from "../shared/command-executor.js";
2
- import type { GitHubRepoInfo } from "../shared/repo-detector.js";
3
- import type { GitHubAppTokenManager } from "../vcs/github-app-token-manager.js";
1
+ import type { ICommandExecutor } from "./command-executor.js";
2
+ import type { GitHubRepoInfo } from "./repo-detector.js";
3
+ import type { DebugLog } from "./logger.js";
4
+ interface ITokenManager {
5
+ getTokenForRepo(repoInfo: GitHubRepoInfo): Promise<string | null>;
6
+ }
4
7
  type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
5
8
  export interface GhApiOptions {
6
9
  token?: string;
7
10
  host?: string;
8
11
  }
12
+ interface GhApiCallParams {
13
+ payload?: unknown;
14
+ options?: GhApiOptions;
15
+ paginate?: boolean;
16
+ }
9
17
  /**
10
18
  * Get the hostname flag for gh commands.
11
19
  * Returns "--hostname HOST" for GHE, empty string for github.com.
@@ -19,28 +27,27 @@ export declare function buildTokenEnv(token?: string): Record<string, string> |
19
27
  export declare class GhApiClient {
20
28
  private readonly executor;
21
29
  private readonly retries;
22
- constructor(executor: ICommandExecutor, retries: number);
23
- call(method: HttpMethod, endpoint: string, payload?: unknown, options?: GhApiOptions, paginate?: boolean): Promise<string>;
30
+ private readonly cwd;
31
+ constructor(executor: ICommandExecutor, retries: number, cwd: string);
32
+ call(method: HttpMethod, endpoint: string, params?: GhApiCallParams): Promise<string>;
24
33
  }
25
34
  /**
26
- * Resolve a GitHub token for a repo: GitHub App token → GH_TOKEN env fallback.
35
+ * Resolve a GitHub token for a repo: GitHub App token → envToken fallback.
27
36
  * Returns { token, skipped } where skipped=true means no App installation found
28
- * and no GH_TOKEN is available. Both sync and settings paths use this function.
37
+ * for this owner (token will be undefined). Both sync and settings paths use this.
29
38
  */
30
- export declare function resolveGitHubToken(repoInfo: GitHubRepoInfo, tokenManager: GitHubAppTokenManager | null, context: string, log?: {
31
- debug(msg: string): void;
32
- }, envToken?: string): Promise<{
39
+ export declare function resolveGitHubToken(repoInfo: GitHubRepoInfo, tokenManager: ITokenManager | null, context: string, log?: DebugLog, envToken?: string): Promise<{
33
40
  token: string | undefined;
34
41
  skipped: boolean;
35
42
  }>;
43
+ /**
44
+ * Check if an error message indicates an HTTP 404 response from the GitHub API.
45
+ */
46
+ export declare function isHttp404Error(error: unknown): boolean;
36
47
  /**
37
48
  * Parse a JSON API response with a contextual error message.
38
49
  * Wraps JSON.parse so callers get "Failed to parse <context>: ..." instead of
39
50
  * a bare "Unexpected token" SyntaxError.
40
51
  */
41
- /**
42
- * Check if an error message indicates an HTTP 404 response from the GitHub API.
43
- */
44
- export declare function isHttp404Error(error: unknown): boolean;
45
52
  export declare function parseApiJson<T>(response: string, context: string): T;
46
53
  export {};
@@ -1,12 +1,13 @@
1
- import { escapeShellArg } from "../shared/shell-utils.js";
2
- import { withRetry } from "../shared/retry-utils.js";
3
- import { toErrorMessage } from "../shared/type-guards.js";
1
+ import { escapeShellArg } from "./shell-utils.js";
2
+ import { withRetry } from "./retry-utils.js";
3
+ import { toErrorMessage } from "./type-guards.js";
4
+ import { SyncError } from "./errors.js";
4
5
  /**
5
6
  * Get the hostname flag for gh commands.
6
7
  * Returns "--hostname HOST" for GHE, empty string for github.com.
7
8
  */
8
9
  export function getHostnameFlag(repoInfo) {
9
- if (repoInfo.host && repoInfo.host !== "github.com") {
10
+ if (repoInfo.host !== "github.com") {
10
11
  return `--hostname ${escapeShellArg(repoInfo.host)}`;
11
12
  }
12
13
  return "";
@@ -22,7 +23,7 @@ export function buildTokenEnv(token) {
22
23
  * rather than shell-prefix string interpolation.
23
24
  */
24
25
  async function ghApiCall(method, endpoint, opts) {
25
- const { executor, retries, apiOpts, payload, paginate } = opts;
26
+ const { executor, retries, cwd, apiOpts, payload, paginate } = opts;
26
27
  const args = ["gh", "api"];
27
28
  if (method !== "GET") {
28
29
  args.push("-X", method);
@@ -40,9 +41,13 @@ async function ghApiCall(method, endpoint, opts) {
40
41
  (method === "POST" || method === "PUT" || method === "PATCH")) {
41
42
  const payloadJson = JSON.stringify(payload);
42
43
  const command = `echo ${escapeShellArg(payloadJson)} | ${baseCommand} --input -`;
43
- return await withRetry(() => executor.exec(command, process.cwd(), { env }), { retries });
44
+ return await withRetry(() => executor.exec(command, cwd, { env }), {
45
+ retries,
46
+ });
44
47
  }
45
- return await withRetry(() => executor.exec(baseCommand, process.cwd(), { env }), { retries });
48
+ return await withRetry(() => executor.exec(baseCommand, cwd, { env }), {
49
+ retries,
50
+ });
46
51
  }
47
52
  /**
48
53
  * Encapsulates executor + retries for GitHub API calls.
@@ -51,24 +56,27 @@ async function ghApiCall(method, endpoint, opts) {
51
56
  export class GhApiClient {
52
57
  executor;
53
58
  retries;
54
- constructor(executor, retries) {
59
+ cwd;
60
+ constructor(executor, retries, cwd) {
55
61
  this.executor = executor;
56
62
  this.retries = retries;
63
+ this.cwd = cwd;
57
64
  }
58
- async call(method, endpoint, payload, options, paginate) {
65
+ async call(method, endpoint, params) {
59
66
  return ghApiCall(method, endpoint, {
60
67
  executor: this.executor,
61
68
  retries: this.retries,
62
- apiOpts: options,
63
- payload,
64
- paginate,
69
+ cwd: this.cwd,
70
+ apiOpts: params?.options,
71
+ payload: params?.payload,
72
+ paginate: params?.paginate,
65
73
  });
66
74
  }
67
75
  }
68
76
  /**
69
- * Resolve a GitHub token for a repo: GitHub App token → GH_TOKEN env fallback.
77
+ * Resolve a GitHub token for a repo: GitHub App token → envToken fallback.
70
78
  * Returns { token, skipped } where skipped=true means no App installation found
71
- * and no GH_TOKEN is available. Both sync and settings paths use this function.
79
+ * for this owner (token will be undefined). Both sync and settings paths use this.
72
80
  */
73
81
  export async function resolveGitHubToken(repoInfo, tokenManager, context, log, envToken) {
74
82
  try {
@@ -81,27 +89,30 @@ export async function resolveGitHubToken(repoInfo, tokenManager, context, log, e
81
89
  return { token: appToken ?? envToken, skipped: false };
82
90
  }
83
91
  catch (error) {
84
- log?.debug(`GitHub App token resolution failed for ${context}: ${toErrorMessage(error)}; falling back to GH_TOKEN`);
92
+ const fallbackDesc = envToken
93
+ ? "falling back to GH_TOKEN"
94
+ : "no fallback token available";
95
+ log?.debug(`GitHub App token resolution failed for ${context}: ${toErrorMessage(error)}; ${fallbackDesc}`);
85
96
  return { token: envToken, skipped: false };
86
97
  }
87
98
  }
88
- /**
89
- * Parse a JSON API response with a contextual error message.
90
- * Wraps JSON.parse so callers get "Failed to parse <context>: ..." instead of
91
- * a bare "Unexpected token" SyntaxError.
92
- */
93
99
  /**
94
100
  * Check if an error message indicates an HTTP 404 response from the GitHub API.
95
101
  */
96
102
  export function isHttp404Error(error) {
97
103
  return toErrorMessage(error).includes("HTTP 404");
98
104
  }
105
+ /**
106
+ * Parse a JSON API response with a contextual error message.
107
+ * Wraps JSON.parse so callers get "Failed to parse <context>: ..." instead of
108
+ * a bare "Unexpected token" SyntaxError.
109
+ */
99
110
  export function parseApiJson(response, context) {
100
111
  try {
101
112
  return JSON.parse(response);
102
113
  }
103
- catch {
114
+ catch (error) {
104
115
  const preview = response.slice(0, 200);
105
- throw new Error(`Failed to parse ${context}: ${preview}`);
116
+ throw new SyncError(`Failed to parse ${context}: ${toErrorMessage(error)} — ${preview}`);
106
117
  }
107
118
  }
@@ -1,8 +1,15 @@
1
- export { Logger, logger, type ILogger } from "./logger.js";
1
+ export { Logger, NO_OP_DEBUG_LOG, type ILogger, type DebugLog, type DebugWarnLog, type DebugInfoLog, type DebugInfoWarnLog, } from "./logger.js";
2
2
  export { withRetry, isPermanentError, isTransientError, DEFAULT_PERMANENT_ERROR_PATTERNS, } from "./retry-utils.js";
3
- export { ShellCommandExecutor, defaultExecutor, type ICommandExecutor, } from "./command-executor.js";
3
+ export { ShellCommandExecutor, type ICommandExecutor, } from "./command-executor.js";
4
4
  export { escapeShellArg, escapeRegExp } from "./shell-utils.js";
5
5
  export { sanitizeCredentials } from "./sanitize-utils.js";
6
6
  export { interpolateEnvVars, interpolateContent, type EnvInterpolationOptions, } from "./env.js";
7
7
  export { generateWorkspaceName } from "./workspace-utils.js";
8
8
  export { detectRepoType, parseGitUrl, getRepoDisplayName, isGitHubRepo, isAzureDevOpsRepo, isGitLabRepo, type RepoInfo, type GitHubRepoInfo, type AzureDevOpsRepoInfo, type GitLabRepoInfo, } from "./repo-detector.js";
9
+ export { ValidationError, GraphQLApiError, SyncError, LifecycleError, } from "./errors.js";
10
+ export { formatStatusBadge, type FileStatus } from "./file-status.js";
11
+ export { GhApiClient, getHostnameFlag, buildTokenEnv, resolveGitHubToken, isHttp404Error, parseApiJson, type GhApiOptions, } from "./gh-api-utils.js";
12
+ export { interpolateString, interpolateValue, type InterpolationConfig, } from "./interpolation-engine.js";
13
+ export { camelToSnake } from "./string-utils.js";
14
+ export { isPlainObject, toErrorMessage, safeCleanup } from "./type-guards.js";
15
+ export { interpolateXfgContent, type XfgTemplateContext, } from "./xfg-template.js";
@@ -1,9 +1,9 @@
1
1
  // Logging
2
- export { Logger, logger } from "./logger.js";
2
+ export { Logger, NO_OP_DEBUG_LOG, } from "./logger.js";
3
3
  // Retry utilities
4
4
  export { withRetry, isPermanentError, isTransientError, DEFAULT_PERMANENT_ERROR_PATTERNS, } from "./retry-utils.js";
5
5
  // Command execution
6
- export { ShellCommandExecutor, defaultExecutor, } from "./command-executor.js";
6
+ export { ShellCommandExecutor, } from "./command-executor.js";
7
7
  // Shell utilities
8
8
  export { escapeShellArg, escapeRegExp } from "./shell-utils.js";
9
9
  // Sanitization
@@ -14,3 +14,17 @@ export { interpolateEnvVars, interpolateContent, } from "./env.js";
14
14
  export { generateWorkspaceName } from "./workspace-utils.js";
15
15
  // Repository detection
16
16
  export { detectRepoType, parseGitUrl, getRepoDisplayName, isGitHubRepo, isAzureDevOpsRepo, isGitLabRepo, } from "./repo-detector.js";
17
+ // Errors
18
+ export { ValidationError, GraphQLApiError, SyncError, LifecycleError, } from "./errors.js";
19
+ // File status
20
+ export { formatStatusBadge } from "./file-status.js";
21
+ // GitHub API utilities
22
+ export { GhApiClient, getHostnameFlag, buildTokenEnv, resolveGitHubToken, isHttp404Error, parseApiJson, } from "./gh-api-utils.js";
23
+ // Interpolation engine
24
+ export { interpolateString, interpolateValue, } from "./interpolation-engine.js";
25
+ // String utilities
26
+ export { camelToSnake } from "./string-utils.js";
27
+ // Type guards
28
+ export { isPlainObject, toErrorMessage, safeCleanup } from "./type-guards.js";
29
+ // XFG templating
30
+ export { interpolateXfgContent, } from "./xfg-template.js";
@@ -1,4 +1,24 @@
1
1
  import { FileStatus } from "./file-status.js";
2
+ /** Minimal log interface: debug only. */
3
+ export type DebugLog = {
4
+ debug(msg: string): void;
5
+ };
6
+ /** Log interface: debug + warn. */
7
+ export type DebugWarnLog = {
8
+ debug(msg: string): void;
9
+ warn(msg: string): void;
10
+ };
11
+ /** Log interface: debug + info. */
12
+ export type DebugInfoLog = {
13
+ debug(msg: string): void;
14
+ info(msg: string): void;
15
+ };
16
+ /** Log interface: debug + info + warn. */
17
+ export type DebugInfoWarnLog = {
18
+ debug(msg: string): void;
19
+ info(msg: string): void;
20
+ warn(msg: string): void;
21
+ };
2
22
  export interface ILogger {
3
23
  log(message: string): void;
4
24
  info(message: string): void;
@@ -13,7 +33,9 @@ export interface ILogger {
13
33
  error(current: number, repoName: string, error: string): void;
14
34
  }
15
35
  export declare class Logger implements ILogger {
36
+ private readonly debugEnabled;
16
37
  private stats;
38
+ constructor(debugEnabled?: boolean);
17
39
  log(message: string): void;
18
40
  setTotal(total: number): void;
19
41
  progress(current: number, repoName: string, message: string): void;
@@ -33,4 +55,5 @@ export declare class Logger implements ILogger {
33
55
  */
34
56
  diffSummary(newCount: number, modifiedCount: number, unchangedCount: number, deletedCount?: number): void;
35
57
  }
36
- export declare const logger: Logger;
58
+ /** No-op debug logger for use as a fallback when logging is optional. */
59
+ export declare const NO_OP_DEBUG_LOG: DebugLog;
@@ -1,12 +1,16 @@
1
1
  import chalk from "chalk";
2
2
  import { formatStatusBadge } from "./file-status.js";
3
3
  export class Logger {
4
+ debugEnabled;
4
5
  stats = {
5
6
  total: 0,
6
7
  succeeded: 0,
7
8
  failed: 0,
8
9
  skipped: 0,
9
10
  };
11
+ constructor(debugEnabled) {
12
+ this.debugEnabled = debugEnabled ?? false;
13
+ }
10
14
  log(message) {
11
15
  console.log(message);
12
16
  }
@@ -24,7 +28,7 @@ export class Logger {
24
28
  console.log(chalk.yellow(` ⚠ ${message}`));
25
29
  }
26
30
  debug(message) {
27
- if (process.env.DEBUG || process.env.XFG_DEBUG) {
31
+ if (this.debugEnabled) {
28
32
  console.log(chalk.dim(` [debug] ${message}`));
29
33
  }
30
34
  }
@@ -65,7 +69,7 @@ export class Logger {
65
69
  parts.push(chalk.green(`${newCount} new`));
66
70
  if (modifiedCount > 0)
67
71
  parts.push(chalk.yellow(`${modifiedCount} modified`));
68
- if (deletedCount && deletedCount > 0)
72
+ if ((deletedCount ?? 0) > 0)
69
73
  parts.push(chalk.red(`${deletedCount} deleted`));
70
74
  if (unchangedCount > 0)
71
75
  parts.push(chalk.gray(`${unchangedCount} unchanged`));
@@ -74,4 +78,5 @@ export class Logger {
74
78
  }
75
79
  }
76
80
  }
77
- export const logger = new Logger();
81
+ /** No-op debug logger for use as a fallback when logging is optional. */
82
+ export const NO_OP_DEBUG_LOG = { debug() { } };
@@ -1,3 +1,4 @@
1
+ import { ValidationError } from "./errors.js";
1
2
  // Type guards
2
3
  export function isGitHubRepo(info) {
3
4
  return info.type === "github";
@@ -14,7 +15,7 @@ export function isGitLabRepo(info) {
14
15
  */
15
16
  export function assertGitHubRepo(repoInfo, context) {
16
17
  if (!isGitHubRepo(repoInfo)) {
17
- throw new Error(`${context} requires GitHub repositories. Got: ${repoInfo.type}`);
18
+ throw new ValidationError(`${context} requires GitHub repositories. Got: ${repoInfo.type}`);
18
19
  }
19
20
  }
20
21
  /**
@@ -23,7 +24,7 @@ export function assertGitHubRepo(repoInfo, context) {
23
24
  */
24
25
  export function assertAzureDevOpsRepo(repoInfo, context) {
25
26
  if (!isAzureDevOpsRepo(repoInfo)) {
26
- throw new Error(`${context} requires Azure DevOps repositories. Got: ${repoInfo.type}`);
27
+ throw new ValidationError(`${context} requires Azure DevOps repositories. Got: ${repoInfo.type}`);
27
28
  }
28
29
  }
29
30
  /**
@@ -32,12 +33,9 @@ export function assertAzureDevOpsRepo(repoInfo, context) {
32
33
  */
33
34
  export function assertGitLabRepo(repoInfo, context) {
34
35
  if (!isGitLabRepo(repoInfo)) {
35
- throw new Error(`${context} requires GitLab repositories. Got: ${repoInfo.type}`);
36
+ throw new ValidationError(`${context} requires GitLab repositories. Got: ${repoInfo.type}`);
36
37
  }
37
38
  }
38
- /**
39
- * Extract hostname from a git URL.
40
- */
41
39
  function extractHostFromUrl(gitUrl) {
42
40
  // SSH: git@hostname:path
43
41
  const sshMatch = gitUrl.match(/^git@([^:]+):/);
@@ -116,7 +114,7 @@ export function detectRepoType(gitUrl, context) {
116
114
  return "gitlab";
117
115
  }
118
116
  // Throw for unrecognized URL formats
119
- throw new Error(`Unrecognized git URL format: ${gitUrl}. Supported formats: GitHub (git@github.com: or https://github.com/), Azure DevOps (git@ssh.dev.azure.com: or https://dev.azure.com/), and GitLab (git@gitlab.com: or https://gitlab.com/)`);
117
+ throw new ValidationError(`Unrecognized git URL format: ${gitUrl}. Supported formats: GitHub (git@github.com: or https://github.com/), Azure DevOps (git@ssh.dev.azure.com: or https://dev.azure.com/), and GitLab (git@gitlab.com: or https://gitlab.com/)`);
120
118
  }
121
119
  export function parseGitUrl(gitUrl, context) {
122
120
  const type = detectRepoType(gitUrl, context);
@@ -155,7 +153,7 @@ function parseGitHubUrl(gitUrl, host) {
155
153
  host,
156
154
  };
157
155
  }
158
- throw new Error(`Unable to parse GitHub URL: ${gitUrl}`);
156
+ throw new ValidationError(`Unable to parse GitHub URL: ${gitUrl}`);
159
157
  }
160
158
  function parseAzureDevOpsUrl(gitUrl) {
161
159
  // Handle SSH format: git@ssh.dev.azure.com:v3/organization/project/repo
@@ -184,7 +182,7 @@ function parseAzureDevOpsUrl(gitUrl) {
184
182
  project: httpsMatch[2],
185
183
  };
186
184
  }
187
- throw new Error(`Unable to parse Azure DevOps URL: ${gitUrl}`);
185
+ throw new ValidationError(`Unable to parse Azure DevOps URL: ${gitUrl}`);
188
186
  }
189
187
  function parseGitLabUrl(gitUrl) {
190
188
  // Handle SSH format: git@gitlab.com:owner/repo.git or git@gitlab.com:org/group/repo.git
@@ -203,13 +201,13 @@ function parseGitLabUrl(gitUrl) {
203
201
  const fullPath = httpsMatch[2];
204
202
  return parseGitLabPath(gitUrl, host, fullPath);
205
203
  }
206
- throw new Error(`Unable to parse GitLab URL: ${gitUrl}`);
204
+ throw new ValidationError(`Unable to parse GitLab URL: ${gitUrl}`);
207
205
  }
208
206
  function parseGitLabPath(gitUrl, host, fullPath) {
209
207
  // Split path into segments: org/group/subgroup/repo -> [org, group, subgroup, repo]
210
208
  const segments = fullPath.split("/");
211
209
  if (segments.length < 2) {
212
- throw new Error(`Unable to parse GitLab URL: ${gitUrl}`);
210
+ throw new ValidationError(`Unable to parse GitLab URL: ${gitUrl}`);
213
211
  }
214
212
  // Last segment is repo, everything else is namespace
215
213
  const repo = segments[segments.length - 1];
@@ -17,21 +17,19 @@ interface RetryOptions {
17
17
  permanentErrorPatterns?: RegExp[];
18
18
  /** Custom transient error patterns (defaults to DEFAULT_TRANSIENT_ERROR_PATTERNS) */
19
19
  transientErrorPatterns?: RegExp[];
20
+ /** Logger for retry messages (defaults to no logging) */
21
+ log?: {
22
+ info(msg: string): void;
23
+ };
20
24
  }
21
25
  /**
22
26
  * Classifies an error as permanent (should not retry) or transient (should retry).
23
- * @param error The error to classify
24
- * @param patterns Custom patterns to use (defaults to DEFAULT_PERMANENT_ERROR_PATTERNS)
25
- * @returns true if the error is permanent, false if it might be transient
26
27
  */
27
28
  export declare function isPermanentError(error: unknown, patterns?: RegExp[]): boolean;
28
29
  /**
29
30
  * Checks if an error matches known transient patterns.
30
- * @param error The error to check
31
- * @param patterns Custom patterns to use (defaults to DEFAULT_TRANSIENT_ERROR_PATTERNS)
32
- * @returns true if the error appears to be transient
33
31
  */
34
- export declare function isTransientError(error: Error, patterns?: RegExp[]): boolean;
32
+ export declare function isTransientError(error: unknown, patterns?: RegExp[]): boolean;
35
33
  /**
36
34
  * Wraps an async operation with retry logic using exponential backoff.
37
35
  * Automatically classifies errors and aborts retries for permanent failures.
@@ -1,7 +1,6 @@
1
1
  import pRetry, { AbortError } from "p-retry";
2
- import { logger } from "./logger.js";
3
2
  import { sanitizeCredentials } from "./sanitize-utils.js";
4
- import { ValidationError } from "../config/errors.js";
3
+ import { ValidationError } from "./errors.js";
5
4
  /**
6
5
  * Core permanent error patterns shared across all strategies (API, GraphQL, CLI).
7
6
  * Auth failures, permission issues, and resource-not-found errors.
@@ -68,9 +67,6 @@ const DEFAULT_TRANSIENT_ERROR_PATTERNS = [
68
67
  ];
69
68
  /**
70
69
  * Classifies an error as permanent (should not retry) or transient (should retry).
71
- * @param error The error to classify
72
- * @param patterns Custom patterns to use (defaults to DEFAULT_PERMANENT_ERROR_PATTERNS)
73
- * @returns true if the error is permanent, false if it might be transient
74
70
  */
75
71
  export function isPermanentError(error, patterns = DEFAULT_PERMANENT_ERROR_PATTERNS) {
76
72
  // Validation errors are always permanent — no point retrying bad input
@@ -90,12 +86,9 @@ export function isPermanentError(error, patterns = DEFAULT_PERMANENT_ERROR_PATTE
90
86
  }
91
87
  /**
92
88
  * Checks if an error matches known transient patterns.
93
- * @param error The error to check
94
- * @param patterns Custom patterns to use (defaults to DEFAULT_TRANSIENT_ERROR_PATTERNS)
95
- * @returns true if the error appears to be transient
96
89
  */
97
90
  export function isTransientError(error, patterns = DEFAULT_TRANSIENT_ERROR_PATTERNS) {
98
- const message = error.message;
91
+ const message = error instanceof Error ? error.message : String(error ?? "");
99
92
  const stderr = error.stderr?.toString() ?? "";
100
93
  const combined = `${message} ${stderr}`;
101
94
  for (const pattern of patterns) {
@@ -135,7 +128,7 @@ export async function withRetry(fn, options) {
135
128
  // Only log if this isn't the last attempt
136
129
  if (context.retriesLeft > 0) {
137
130
  const msg = sanitizeCredentials(context.error.message) || "Unknown error";
138
- logger.info(`Attempt ${context.attemptNumber}/${retries + 1} failed: ${msg}. Retrying...`);
131
+ options?.log?.info(`Attempt ${context.attemptNumber}/${retries + 1} failed: ${msg}. Retrying...`);
139
132
  options?.onRetry?.(context.error, context.attemptNumber);
140
133
  }
141
134
  },