@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,9 +1,8 @@
1
1
  import { escapeShellArg } from "../shared/shell-utils.js";
2
- import { defaultExecutor, } from "../shared/command-executor.js";
3
- import { withRetry, DEFAULT_PERMANENT_ERROR_PATTERNS, } from "../shared/retry-utils.js";
2
+ import { withRetry, isPermanentError, DEFAULT_PERMANENT_ERROR_PATTERNS, } from "../shared/retry-utils.js";
4
3
  import { assertGitHubRepo, } from "../shared/repo-detector.js";
5
- import { logger } from "../shared/logger.js";
6
4
  import { toErrorMessage } from "../shared/type-guards.js";
5
+ import { LifecycleError } from "../shared/errors.js";
7
6
  import { buildTokenEnv, getHostnameFlag } from "../shared/gh-api-utils.js";
8
7
  /**
9
8
  * Error messages that indicate "repo not found" vs actual errors.
@@ -43,13 +42,16 @@ export class GitHubLifecycleProvider {
43
42
  cwd;
44
43
  forkReadyTimeoutMs;
45
44
  forkPollIntervalMs;
45
+ log;
46
46
  constructor(options) {
47
- const opts = options ?? {};
48
- this.executor = opts.executor ?? defaultExecutor;
49
- this.retries = opts.retries ?? 3;
50
- this.cwd = opts.cwd ?? process.cwd();
51
- this.forkReadyTimeoutMs = opts.forkReadyTimeoutMs ?? FORK_READY_TIMEOUT_MS;
52
- this.forkPollIntervalMs = opts.forkPollIntervalMs ?? FORK_POLL_INTERVAL_MS;
47
+ this.executor = options.executor;
48
+ this.retries = options.retries ?? 3;
49
+ this.cwd = options.cwd;
50
+ this.forkReadyTimeoutMs =
51
+ options.forkReadyTimeoutMs ?? FORK_READY_TIMEOUT_MS;
52
+ this.forkPollIntervalMs =
53
+ options.forkPollIntervalMs ?? FORK_POLL_INTERVAL_MS;
54
+ this.log = options.log;
53
55
  }
54
56
  /**
55
57
  * Check if a GitHub owner is an organization (vs user).
@@ -64,11 +66,19 @@ export class GitHubLifecycleProvider {
64
66
  return data.type === "Organization";
65
67
  }
66
68
  catch (error) {
67
- // If we can't determine, assume it's an org (safer - uses --org flag).
69
+ // Two-tier error handling:
70
+ // 1. Permanent errors (auth failures, 403/401) are rethrown — retrying
71
+ // or falling through would mask a real credentials problem.
72
+ // 2. Transient errors (network timeouts, 5xx) fall through to assume
73
+ // the owner is an organization, which is the safer default because
74
+ // --org is required for org forks but harmless if wrong.
75
+ if (isPermanentError(error)) {
76
+ throw error;
77
+ }
68
78
  // This may cause fork to fail with a misleading error for personal accounts.
69
79
  const errMsg = toErrorMessage(error);
70
- logger.debug(`Could not determine if '${owner}' is an organization, defaulting to org behavior: ${errMsg}`);
71
- logger.warn(`Could not verify if '${owner}' is an organization or user account. ` +
80
+ this.log?.debug(`Could not determine if '${owner}' is an organization, defaulting to org behavior: ${errMsg}`);
81
+ this.log?.warn(`Could not verify if '${owner}' is an organization or user account. ` +
72
82
  `If fork fails, check your authentication (gh auth status) and ensure the ` +
73
83
  `target owner is correct.`);
74
84
  return true;
@@ -170,7 +180,7 @@ export class GitHubLifecycleProvider {
170
180
  this.assertGitHub(target);
171
181
  // Guard: cannot fork a repo to the same owner
172
182
  if (upstream.owner.toLowerCase() === target.owner.toLowerCase()) {
173
- throw new Error(`Cannot fork ${upstream.owner}/${upstream.repo} to the same owner '${target.owner}'. ` +
183
+ throw new LifecycleError(`Cannot fork ${upstream.owner}/${upstream.repo} to the same owner '${target.owner}'. ` +
174
184
  `The upstream and target owners must be different.`);
175
185
  }
176
186
  // Determine if target owner is an organization or user
@@ -219,14 +229,14 @@ export class GitHubLifecycleProvider {
219
229
  }
220
230
  }
221
231
  catch (error) {
222
- logger.debug(`Polling fork readiness: ${toErrorMessage(error)}`);
232
+ this.log?.debug(`Polling fork readiness: ${toErrorMessage(error)}`);
223
233
  }
224
234
  const remaining = deadline - Date.now();
225
235
  if (remaining <= 0)
226
236
  break;
227
237
  await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, remaining)));
228
238
  }
229
- throw new Error(`Timed out waiting for fork ${repoInfo.owner}/${repoInfo.repo} to become available ` +
239
+ throw new LifecycleError(`Timed out waiting for fork ${repoInfo.owner}/${repoInfo.repo} to become available ` +
230
240
  `after ${timeoutMs / 1000}s. The fork may still be processing on GitHub.`);
231
241
  }
232
242
  /**
@@ -258,7 +268,7 @@ export class GitHubLifecycleProvider {
258
268
  await this.executor.exec(`git -C ${escapeShellArg(sourceDir)} remote remove origin`, this.cwd);
259
269
  }
260
270
  catch (error) {
261
- logger.debug(`Cleanup: remote remove origin skipped - ${toErrorMessage(error)}`);
271
+ this.log?.debug(`Cleanup: remote remove origin skipped - ${toErrorMessage(error)}`);
262
272
  }
263
273
  // Remove all non-standard refs that GitHub rejects on push.
264
274
  // Mirror clones include ALL refs from the source, but GitHub only
@@ -276,14 +286,14 @@ export class GitHubLifecycleProvider {
276
286
  }
277
287
  }
278
288
  catch (error) {
279
- logger.debug(`Cleanup: ref cleanup skipped - ${toErrorMessage(error)}`);
289
+ this.log?.debug(`Cleanup: ref cleanup skipped - ${toErrorMessage(error)}`);
280
290
  }
281
291
  // Rename default branch in mirror clone if requested.
282
292
  if (settings?.defaultBranch) {
283
293
  const headRef = (await this.executor.exec(`git -C ${escapeShellArg(sourceDir)} symbolic-ref HEAD`, this.cwd)).trim();
284
294
  const prefix = "refs/heads/";
285
295
  if (!headRef.startsWith(prefix)) {
286
- throw new Error(`Mirror clone HEAD symbolic-ref is '${headRef}', expected to start with '${prefix}'. ` +
296
+ throw new LifecycleError(`Mirror clone HEAD symbolic-ref is '${headRef}', expected to start with '${prefix}'. ` +
287
297
  `Cannot rename default branch.`);
288
298
  }
289
299
  const sourceBranch = headRef.slice(prefix.length);
@@ -360,7 +370,7 @@ export class GitHubLifecycleProvider {
360
370
  }
361
371
  }
362
372
  catch (error) {
363
- logger.debug(`Polling default branch: ${toErrorMessage(error)}`);
373
+ this.log?.debug(`Polling default branch: ${toErrorMessage(error)}`);
364
374
  }
365
375
  await new Promise((resolve) => setTimeout(resolve, pollMs));
366
376
  }
@@ -1,5 +1,6 @@
1
1
  import chalk from "chalk";
2
2
  import { getRepoDisplayName } from "../shared/repo-detector.js";
3
+ import { SyncError } from "../shared/errors.js";
3
4
  /**
4
5
  * Format lifecycle action for output (used in both dry-run and real execution).
5
6
  * Returns empty array if action is "existed" (no output needed).
@@ -22,7 +23,7 @@ export function formatLifecycleAction(result, options) {
22
23
  break;
23
24
  default: {
24
25
  const _exhaustive = result.action;
25
- throw new Error(`Unknown lifecycle action: ${_exhaustive}`);
26
+ throw new SyncError(`Unknown lifecycle action: ${_exhaustive}`);
26
27
  }
27
28
  }
28
29
  // Add settings details if provided
@@ -10,6 +10,10 @@ interface LifecycleCheckOptions {
10
10
  resolvedWorkDir?: string;
11
11
  /** Auth token (GitHub App installation token or PAT) for gh CLI commands */
12
12
  token?: string;
13
+ /** Index used to generate workspace directory name when resolvedWorkDir is not provided. */
14
+ repoIndex: number;
15
+ lifecycleManager: IRepoLifecycleManager;
16
+ repoSettings?: GitHubRepoSettings;
13
17
  }
14
18
  /**
15
19
  * Build CreateRepoSettings from GitHubRepoSettings.
@@ -24,5 +28,5 @@ interface LifecycleCheckResult {
24
28
  * Run lifecycle check for a single repo.
25
29
  * Returns the lifecycle result and formatted output lines.
26
30
  */
27
- export declare function runLifecycleCheck(repoConfig: RepoConfig, repoInfo: RepoInfo, repoIndex: number, options: LifecycleCheckOptions, lifecycleManager: IRepoLifecycleManager, repoSettings?: GitHubRepoSettings): Promise<LifecycleCheckResult>;
31
+ export declare function runLifecycleCheck(repoConfig: RepoConfig, repoInfo: RepoInfo, options: LifecycleCheckOptions): Promise<LifecycleCheckResult>;
28
32
  export {};
@@ -25,11 +25,11 @@ export function toCreateRepoSettings(repo) {
25
25
  * Run lifecycle check for a single repo.
26
26
  * Returns the lifecycle result and formatted output lines.
27
27
  */
28
- export async function runLifecycleCheck(repoConfig, repoInfo, repoIndex, options, lifecycleManager, repoSettings) {
28
+ export async function runLifecycleCheck(repoConfig, repoInfo, options) {
29
29
  const workDir = options.resolvedWorkDir ??
30
- resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(repoIndex)));
31
- const createSettings = toCreateRepoSettings(repoSettings);
32
- const lifecycleResult = await lifecycleManager.ensureRepo(repoConfig, repoInfo, {
30
+ resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(options.repoIndex)));
31
+ const createSettings = toCreateRepoSettings(options.repoSettings);
32
+ const lifecycleResult = await options.lifecycleManager.ensureRepo(repoConfig, repoInfo, {
33
33
  dryRun: options.dryRun,
34
34
  workDir,
35
35
  githubHosts: options.githubHosts,
@@ -1,14 +1,14 @@
1
1
  import { ICommandExecutor } from "../shared/command-executor.js";
2
+ import type { DebugWarnLog } from "../shared/logger.js";
2
3
  import type { IRepoLifecycleFactory, IRepoLifecycleProvider, IMigrationSource, LifecyclePlatform } from "./types.js";
3
- /**
4
- * Factory for creating lifecycle providers and migration sources.
5
- */
6
4
  export declare class RepoLifecycleFactory implements IRepoLifecycleFactory {
7
5
  private readonly providers;
8
6
  private readonly sources;
9
7
  private readonly executor;
10
8
  private readonly retries;
11
- constructor(executor?: ICommandExecutor, retries?: number);
9
+ private readonly cwd;
10
+ private readonly log?;
11
+ constructor(executor: ICommandExecutor, retries: number | undefined, cwd: string, log?: DebugWarnLog);
12
12
  getProvider(platform: LifecyclePlatform): IRepoLifecycleProvider;
13
13
  getMigrationSource(platform: LifecyclePlatform): IMigrationSource;
14
14
  }
@@ -1,17 +1,18 @@
1
- import { defaultExecutor, } from "../shared/command-executor.js";
1
+ import { LifecycleError } from "../shared/errors.js";
2
2
  import { GitHubLifecycleProvider } from "./github-lifecycle-provider.js";
3
3
  import { AdoMigrationSource } from "./ado-migration-source.js";
4
- /**
5
- * Factory for creating lifecycle providers and migration sources.
6
- */
7
4
  export class RepoLifecycleFactory {
8
5
  providers = new Map();
9
6
  sources = new Map();
10
7
  executor;
11
8
  retries;
12
- constructor(executor, retries) {
13
- this.executor = executor ?? defaultExecutor;
9
+ cwd;
10
+ log;
11
+ constructor(executor, retries, cwd, log) {
12
+ this.executor = executor;
14
13
  this.retries = retries ?? 3;
14
+ this.cwd = cwd;
15
+ this.log = log;
15
16
  }
16
17
  getProvider(platform) {
17
18
  // Check cache first
@@ -26,10 +27,12 @@ export class RepoLifecycleFactory {
26
27
  provider = new GitHubLifecycleProvider({
27
28
  executor: this.executor,
28
29
  retries: this.retries,
30
+ cwd: this.cwd,
31
+ log: this.log,
29
32
  });
30
33
  break;
31
34
  default:
32
- throw new Error(`Platform '${platform}' not supported as target for lifecycle operations. ` +
35
+ throw new LifecycleError(`Platform '${platform}' not supported as target for lifecycle operations. ` +
33
36
  `Currently supported: github`);
34
37
  }
35
38
  this.providers.set(platform, provider);
@@ -45,10 +48,10 @@ export class RepoLifecycleFactory {
45
48
  let source;
46
49
  switch (platform) {
47
50
  case "azure-devops":
48
- source = new AdoMigrationSource(this.executor, this.retries);
51
+ source = new AdoMigrationSource(this.executor, this.retries, this.cwd);
49
52
  break;
50
53
  default:
51
- throw new Error(`Platform '${platform}' not supported as migration source. ` +
54
+ throw new LifecycleError(`Platform '${platform}' not supported as migration source. ` +
52
55
  `Currently supported: azure-devops`);
53
56
  }
54
57
  this.sources.set(platform, source);
@@ -1,4 +1,6 @@
1
1
  import { type RepoInfo } from "../shared/repo-detector.js";
2
+ import { type DebugInfoWarnLog } from "../shared/logger.js";
3
+ import type { ICommandExecutor } from "../shared/command-executor.js";
2
4
  import type { RepoConfig } from "../config/types.js";
3
5
  import type { IRepoLifecycleManager, IRepoLifecycleFactory, LifecycleResult, LifecycleOptions, CreateRepoSettings } from "./types.js";
4
6
  /**
@@ -6,7 +8,8 @@ import type { IRepoLifecycleManager, IRepoLifecycleFactory, LifecycleResult, Lif
6
8
  */
7
9
  export declare class RepoLifecycleManager implements IRepoLifecycleManager {
8
10
  private readonly factory;
9
- constructor(factory?: IRepoLifecycleFactory, retries?: number);
11
+ private readonly log?;
12
+ constructor(factory: IRepoLifecycleFactory | undefined, executor: ICommandExecutor, retries: number | undefined, cwd: string, log?: DebugInfoWarnLog);
10
13
  ensureRepo(repoConfig: RepoConfig, repoInfo: RepoInfo, options: LifecycleOptions, settings?: CreateRepoSettings): Promise<LifecycleResult>;
11
14
  private create;
12
15
  private fork;
@@ -1,16 +1,20 @@
1
1
  import { join } from "node:path";
2
2
  import { rm } from "node:fs/promises";
3
3
  import { parseGitUrl } from "../shared/repo-detector.js";
4
- import { logger } from "../shared/logger.js";
5
4
  import { safeCleanup } from "../shared/type-guards.js";
5
+ import { LifecycleError } from "../shared/errors.js";
6
+ import { NO_OP_DEBUG_LOG } from "../shared/logger.js";
6
7
  import { RepoLifecycleFactory } from "./repo-lifecycle-factory.js";
7
8
  /**
8
9
  * Orchestrates repo lifecycle operations before sync.
9
10
  */
10
11
  export class RepoLifecycleManager {
11
12
  factory;
12
- constructor(factory, retries) {
13
- this.factory = factory ?? new RepoLifecycleFactory(undefined, retries);
13
+ log;
14
+ constructor(factory, executor, retries, cwd, log) {
15
+ this.factory =
16
+ factory ?? new RepoLifecycleFactory(executor, retries, cwd, log);
17
+ this.log = log;
14
18
  }
15
19
  async ensureRepo(repoConfig, repoInfo, options, settings) {
16
20
  let provider;
@@ -23,7 +27,7 @@ export class RepoLifecycleManager {
23
27
  throw error;
24
28
  }
25
29
  // Platform doesn't support lifecycle operations yet - log and skip
26
- logger.debug(`Lifecycle: skipping unsupported platform "${repoInfo.type}"`);
30
+ this.log?.debug(`Lifecycle: skipping unsupported platform "${repoInfo.type}"`);
27
31
  return { repoInfo, action: "existed" };
28
32
  }
29
33
  const { token } = options;
@@ -71,7 +75,7 @@ export class RepoLifecycleManager {
71
75
  };
72
76
  }
73
77
  if (!provider.fork) {
74
- throw new Error(`Platform '${repoInfo.type}' does not support forking`);
78
+ throw new LifecycleError(`Platform '${repoInfo.type}' does not support forking`);
75
79
  }
76
80
  const upstreamInfo = parseGitUrl(repoConfig.upstream, {
77
81
  githubHosts: options.githubHosts,
@@ -110,7 +114,7 @@ export class RepoLifecycleManager {
110
114
  };
111
115
  }
112
116
  finally {
113
- await safeCleanup(() => rm(sourceDir, { recursive: true, force: true }), `failed to remove ${sourceDir}`, logger);
117
+ await safeCleanup(() => rm(sourceDir, { recursive: true, force: true }), `failed to remove ${sourceDir}`, this.log ?? NO_OP_DEBUG_LOG);
114
118
  }
115
119
  }
116
120
  /**
@@ -127,6 +131,6 @@ export class RepoLifecycleManager {
127
131
  await new Promise((resolve) => setTimeout(resolve, pollMs));
128
132
  }
129
133
  // Timed out — proceed anyway and let downstream operations handle it
130
- logger.info(`Repo ${repoInfo.owner}/${repoInfo.repo} not yet visible after ${timeoutMs}ms, proceeding`);
134
+ this.log?.info(`Repo ${repoInfo.owner}/${repoInfo.repo} not yet visible after ${timeoutMs}ms, proceeding`);
131
135
  }
132
136
  }
@@ -1,20 +1,11 @@
1
1
  import type { RepoInfo } from "../shared/repo-detector.js";
2
2
  import type { RepoConfig } from "../config/types.js";
3
- /**
4
- * Supported platforms for lifecycle operations.
5
- */
6
3
  export type LifecyclePlatform = "github" | "azure-devops" | "gitlab";
7
- /**
8
- * Result of a lifecycle operation.
9
- */
10
4
  export interface LifecycleResult {
11
5
  repoInfo: RepoInfo;
12
6
  action: "existed" | "created" | "forked" | "migrated";
13
7
  skipped?: boolean;
14
8
  }
15
- /**
16
- * Options for lifecycle operations.
17
- */
18
9
  export interface LifecycleOptions {
19
10
  dryRun: boolean;
20
11
  workDir: string;
@@ -70,9 +61,6 @@ export interface IMigrationSource {
70
61
  */
71
62
  cloneForMigration(repoInfo: RepoInfo, workDir: string): Promise<void>;
72
63
  }
73
- /**
74
- * Factory for getting providers by platform.
75
- */
76
64
  export interface IRepoLifecycleFactory {
77
65
  /**
78
66
  * Get lifecycle provider for a platform.
@@ -85,9 +73,6 @@ export interface IRepoLifecycleFactory {
85
73
  */
86
74
  getMigrationSource(platform: LifecyclePlatform): IMigrationSource;
87
75
  }
88
- /**
89
- * Manager that orchestrates lifecycle operations before sync.
90
- */
91
76
  export interface IRepoLifecycleManager {
92
77
  /**
93
78
  * Ensure repository exists, creating/forking/migrating if needed.
@@ -1,3 +1,4 @@
1
+ import type { DebugLog } from "../shared/logger.js";
1
2
  export type MergeOutcome = "manual" | "auto" | "force" | "direct";
2
3
  export interface FileChanges {
3
4
  added: number;
@@ -17,7 +18,7 @@ export interface RulesetPlanDetail {
17
18
  }
18
19
  export interface RepoSettingsPlanDetail {
19
20
  property: string;
20
- action: "add" | "change";
21
+ action: "create" | "update";
21
22
  }
22
23
  export interface LabelsPlanDetail {
23
24
  name: string;
@@ -45,10 +46,10 @@ export interface SummaryData {
45
46
  results: RepoResult[];
46
47
  }
47
48
  export declare function formatSummary(data: SummaryData): string;
48
- export declare function isGitHubActions(): boolean;
49
+ export declare function shouldWriteSummary(summaryPath: string | undefined): boolean;
49
50
  /**
50
51
  * Append markdown content to GITHUB_STEP_SUMMARY.
51
- * No-op outside GitHub Actions.
52
+ * No-op if summaryPath is not provided.
52
53
  */
53
- export declare function writeGitHubStepSummary(markdown: string): void;
54
- export declare function writeSummary(data: SummaryData): void;
54
+ export declare function writeGitHubStepSummary(markdown: string, summaryPath: string | undefined, log?: DebugLog): void;
55
+ export declare function writeSummary(data: SummaryData, summaryPath: string | undefined): void;
@@ -1,4 +1,5 @@
1
1
  import { appendFileSync } from "node:fs";
2
+ import { toErrorMessage } from "../shared/type-guards.js";
2
3
  function escapeMarkdown(text) {
3
4
  // Escape backslashes first, then pipes (order matters to prevent double-escaping)
4
5
  return text.replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
@@ -15,14 +16,14 @@ function formatChangesColumn(result) {
15
16
  // For settings results, derive changes from plan details
16
17
  const parts = [];
17
18
  if (result.rulesetPlanDetails && result.rulesetPlanDetails.length > 0) {
18
- parts.push(formatRulesetPlanSummary(result.rulesetPlanDetails));
19
+ parts.push(formatPlanSummary(result.rulesetPlanDetails));
19
20
  }
20
21
  if (result.repoSettingsPlanDetails &&
21
22
  result.repoSettingsPlanDetails.length > 0) {
22
- parts.push(formatSettingsPlanSummary(result.repoSettingsPlanDetails));
23
+ parts.push(formatPlanSummary(result.repoSettingsPlanDetails));
23
24
  }
24
25
  if (result.labelsPlanDetails && result.labelsPlanDetails.length > 0) {
25
- parts.push(formatLabelsPlanSummary(result.labelsPlanDetails));
26
+ parts.push(formatPlanSummary(result.labelsPlanDetails));
26
27
  }
27
28
  return parts.length > 0 ? parts.join("; ") : "-";
28
29
  }
@@ -80,52 +81,30 @@ function formatRulesetProperties(detail) {
80
81
  }
81
82
  return "-";
82
83
  }
83
- function formatRulesetPlanSummary(details) {
84
- const creates = details.filter((d) => d.action === "create").length;
85
- const updates = details.filter((d) => d.action === "update").length;
86
- const deletes = details.filter((d) => d.action === "delete").length;
84
+ function formatPlanSummary(details) {
85
+ const counts = {};
86
+ for (const d of details) {
87
+ counts[d.action] = (counts[d.action] ?? 0) + 1;
88
+ }
87
89
  const parts = [];
88
- if (creates > 0)
89
- parts.push(`${creates} to create`);
90
- if (updates > 0)
91
- parts.push(`${updates} to update`);
92
- if (deletes > 0)
93
- parts.push(`${deletes} to delete`);
90
+ if (counts.create)
91
+ parts.push(`${counts.create} to create`);
92
+ if (counts.update)
93
+ parts.push(`${counts.update} to update`);
94
+ if (counts.delete)
95
+ parts.push(`${counts.delete} to delete`);
94
96
  return parts.join(", ") || "no changes";
95
97
  }
96
98
  function formatSettingsAction(action) {
97
99
  switch (action) {
98
- case "add":
99
- return "+ Add";
100
- case "change":
101
- return "~ Change";
100
+ case "create":
101
+ return "+ Create";
102
+ case "update":
103
+ return "~ Update";
102
104
  default:
103
105
  return action;
104
106
  }
105
107
  }
106
- function formatSettingsPlanSummary(details) {
107
- const adds = details.filter((d) => d.action === "add").length;
108
- const changes = details.filter((d) => d.action === "change").length;
109
- const parts = [];
110
- if (adds > 0)
111
- parts.push(`${adds} to add`);
112
- if (changes > 0)
113
- parts.push(`${changes} to change`);
114
- return parts.join(", ") || "no changes";
115
- }
116
- function formatLabelsPlanSummary(details) {
117
- const creates = details.filter((d) => d.action === "create").length;
118
- const updates = details.filter((d) => d.action === "update").length;
119
- const deletes = details.filter((d) => d.action === "delete").length;
120
- const parts = [];
121
- if (creates > 0)
122
- parts.push(`${creates} to create`);
123
- if (updates > 0)
124
- parts.push(`${updates} to update`);
125
- if (deletes > 0)
126
- parts.push(`${deletes} to delete`);
127
- return parts.join(", ") || "no changes";
128
- }
129
108
  export function formatSummary(data) {
130
109
  const lines = [];
131
110
  // Header
@@ -168,7 +147,7 @@ export function formatSummary(data) {
168
147
  if (result.rulesetPlanDetails && result.rulesetPlanDetails.length > 0) {
169
148
  lines.push("");
170
149
  lines.push("<details>");
171
- lines.push(`<summary>${result.repoName} — Rulesets: ${formatRulesetPlanSummary(result.rulesetPlanDetails)}</summary>`);
150
+ lines.push(`<summary>${result.repoName} — Rulesets: ${formatPlanSummary(result.rulesetPlanDetails)}</summary>`);
172
151
  lines.push("");
173
152
  lines.push("| Ruleset | Action | Properties |");
174
153
  lines.push("|---------|--------|------------|");
@@ -182,7 +161,7 @@ export function formatSummary(data) {
182
161
  result.repoSettingsPlanDetails.length > 0) {
183
162
  lines.push("");
184
163
  lines.push("<details>");
185
- lines.push(`<summary>${result.repoName} — Repo Settings: ${formatSettingsPlanSummary(result.repoSettingsPlanDetails)}</summary>`);
164
+ lines.push(`<summary>${result.repoName} — Repo Settings: ${formatPlanSummary(result.repoSettingsPlanDetails)}</summary>`);
186
165
  lines.push("");
187
166
  lines.push("| Setting | Action |");
188
167
  lines.push("|---------|--------|");
@@ -195,7 +174,7 @@ export function formatSummary(data) {
195
174
  if (result.labelsPlanDetails && result.labelsPlanDetails.length > 0) {
196
175
  lines.push("");
197
176
  lines.push("<details>");
198
- lines.push(`<summary>${result.repoName} — Labels: ${formatLabelsPlanSummary(result.labelsPlanDetails)}</summary>`);
177
+ lines.push(`<summary>${result.repoName} — Labels: ${formatPlanSummary(result.labelsPlanDetails)}</summary>`);
199
178
  lines.push("");
200
179
  lines.push("| Label | Action |");
201
180
  lines.push("|-------|--------|");
@@ -221,20 +200,25 @@ export function formatSummary(data) {
221
200
  }
222
201
  return lines.join("\n");
223
202
  }
224
- export function isGitHubActions() {
225
- return !!process.env.GITHUB_STEP_SUMMARY;
203
+ export function shouldWriteSummary(summaryPath) {
204
+ return !!summaryPath;
226
205
  }
227
206
  /**
228
207
  * Append markdown content to GITHUB_STEP_SUMMARY.
229
- * No-op outside GitHub Actions.
208
+ * No-op if summaryPath is not provided.
230
209
  */
231
- export function writeGitHubStepSummary(markdown) {
232
- const summaryPath = process.env.GITHUB_STEP_SUMMARY;
233
- if (!summaryPath)
210
+ export function writeGitHubStepSummary(markdown, summaryPath, log) {
211
+ const path = summaryPath;
212
+ if (!path)
234
213
  return;
235
- appendFileSync(summaryPath, "\n" + markdown + "\n");
214
+ try {
215
+ appendFileSync(path, "\n" + markdown + "\n");
216
+ }
217
+ catch (error) {
218
+ log?.debug(`Failed to write GitHub step summary: ${toErrorMessage(error)}`);
219
+ }
236
220
  }
237
- export function writeSummary(data) {
221
+ export function writeSummary(data, summaryPath) {
238
222
  const markdown = formatSummary(data);
239
- writeGitHubStepSummary(markdown);
223
+ writeGitHubStepSummary(markdown, summaryPath);
240
224
  }
@@ -1,5 +1,5 @@
1
- export { formatSyncReportCLI, formatSyncReportMarkdown, writeSyncReportSummary, type SyncReport, type RepoFileChanges, type FileChange, } from "./sync-report.js";
1
+ export { formatSyncReportCLI, formatSyncReportMarkdown, writeSyncReportSummary, type SyncReport, type RepoFileChanges, type ReportFileChange, } from "./sync-report.js";
2
2
  export { formatSettingsReportCLI, formatSettingsReportMarkdown, writeSettingsReportSummary, type SettingsReport, type RepoChanges, type RulesetChange, type SettingChange, } from "./settings-report.js";
3
3
  export { formatUnifiedSummaryMarkdown, writeUnifiedSummary, } from "./unified-summary.js";
4
- export { formatSummary, isGitHubActions, writeSummary, type MergeOutcome, type FileChanges, type RulesetPlanDetail, type RepoSettingsPlanDetail, type RepoResult, type SummaryData, } from "./github-summary.js";
4
+ export { formatSummary, shouldWriteSummary, writeSummary, type MergeOutcome, type FileChanges, type RulesetPlanDetail, type RepoSettingsPlanDetail, type RepoResult, type SummaryData, } from "./github-summary.js";
5
5
  export { getMergeOutcome, toFileChanges, buildRepoResult, buildErrorResult, } from "./summary-utils.js";
@@ -5,6 +5,6 @@ export { formatSettingsReportCLI, formatSettingsReportMarkdown, writeSettingsRep
5
5
  // Unified summary (lifecycle + sync + settings in one diff block)
6
6
  export { formatUnifiedSummaryMarkdown, writeUnifiedSummary, } from "./unified-summary.js";
7
7
  // GitHub Actions summary
8
- export { formatSummary, isGitHubActions, writeSummary, } from "./github-summary.js";
8
+ export { formatSummary, shouldWriteSummary, writeSummary, } from "./github-summary.js";
9
9
  // Summary utilities
10
10
  export { getMergeOutcome, toFileChanges, buildRepoResult, buildErrorResult, } from "./summary-utils.js";
@@ -17,21 +17,11 @@ export interface LifecycleAction {
17
17
  description?: string;
18
18
  };
19
19
  }
20
- export interface LifecycleReportInput {
21
- repoName: string;
22
- action: "existed" | "created" | "forked" | "migrated";
23
- upstream?: string;
24
- source?: string;
25
- settings?: {
26
- visibility?: string;
27
- description?: string;
28
- };
29
- }
30
- export declare function buildLifecycleReport(results: LifecycleReportInput[]): LifecycleReport;
20
+ export declare function buildLifecycleReport(results: LifecycleAction[]): LifecycleReport;
31
21
  /**
32
22
  * Returns true if the report has any non-"existed" actions worth displaying.
33
23
  */
34
24
  export declare function hasLifecycleChanges(report: LifecycleReport): boolean;
35
25
  export declare function formatLifecycleReportCLI(report: LifecycleReport): string[];
36
26
  export declare function formatLifecycleReportMarkdown(report: LifecycleReport, dryRun: boolean): string;
37
- export declare function writeLifecycleReportSummary(report: LifecycleReport, dryRun: boolean): void;
27
+ export declare function writeLifecycleReportSummary(report: LifecycleReport, dryRun: boolean, summaryPath: string | undefined): void;