@aspruyt/xfg 3.8.2 → 3.9.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 (35) hide show
  1. package/dist/cli/settings-command.d.ts +2 -1
  2. package/dist/cli/settings-command.js +82 -7
  3. package/dist/cli/sync-command.d.ts +2 -1
  4. package/dist/cli/sync-command.js +77 -3
  5. package/dist/config/normalizer.js +2 -0
  6. package/dist/config/types.d.ts +9 -0
  7. package/dist/config/validator.js +62 -0
  8. package/dist/lifecycle/ado-migration-source.d.ts +15 -0
  9. package/dist/lifecycle/ado-migration-source.js +37 -0
  10. package/dist/lifecycle/github-lifecycle-provider.d.ts +56 -0
  11. package/dist/lifecycle/github-lifecycle-provider.js +314 -0
  12. package/dist/lifecycle/index.d.ts +7 -0
  13. package/dist/lifecycle/index.js +6 -0
  14. package/dist/lifecycle/lifecycle-formatter.d.ts +14 -0
  15. package/dist/lifecycle/lifecycle-formatter.js +34 -0
  16. package/dist/lifecycle/lifecycle-helpers.d.ts +27 -0
  17. package/dist/lifecycle/lifecycle-helpers.js +47 -0
  18. package/dist/lifecycle/repo-lifecycle-factory.d.ts +14 -0
  19. package/dist/lifecycle/repo-lifecycle-factory.js +57 -0
  20. package/dist/lifecycle/repo-lifecycle-manager.d.ts +20 -0
  21. package/dist/lifecycle/repo-lifecycle-manager.js +139 -0
  22. package/dist/lifecycle/types.d.ts +104 -0
  23. package/dist/lifecycle/types.js +1 -0
  24. package/dist/output/lifecycle-report.d.ts +37 -0
  25. package/dist/output/lifecycle-report.js +152 -0
  26. package/dist/output/settings-report.js +36 -28
  27. package/dist/output/sync-report.js +15 -8
  28. package/dist/settings/repo-settings/diff.js +1 -0
  29. package/dist/settings/repo-settings/github-repo-settings-strategy.js +1 -0
  30. package/dist/shared/logger.d.ts +2 -0
  31. package/dist/shared/logger.js +5 -0
  32. package/dist/sync/manifest-strategy.js +3 -1
  33. package/dist/vcs/authenticated-git-ops.js +1 -1
  34. package/dist/vcs/git-ops.js +1 -1
  35. package/package.json +4 -2
@@ -0,0 +1,314 @@
1
+ import { escapeShellArg } from "../shared/shell-utils.js";
2
+ import { defaultExecutor, } from "../shared/command-executor.js";
3
+ import { withRetry } from "../shared/retry-utils.js";
4
+ import { isGitHubRepo, } from "../shared/repo-detector.js";
5
+ import { logger } from "../shared/logger.js";
6
+ /**
7
+ * Error messages that indicate "repo not found" vs actual errors.
8
+ */
9
+ const REPO_NOT_FOUND_PATTERNS = [
10
+ "Could not resolve to a Repository",
11
+ "Not Found",
12
+ "404",
13
+ ];
14
+ /**
15
+ * Check if an error indicates repo not found (vs network/auth error).
16
+ */
17
+ function isRepoNotFoundError(error) {
18
+ const message = error instanceof Error
19
+ ? error.message + (error.stderr ?? "")
20
+ : String(error);
21
+ return REPO_NOT_FOUND_PATTERNS.some((pattern) => message.includes(pattern));
22
+ }
23
+ /**
24
+ * Get the hostname flag for gh commands.
25
+ * Returns "--hostname HOST" for GHE, empty string for github.com.
26
+ */
27
+ function getHostnameFlag(repoInfo) {
28
+ if (repoInfo.host && repoInfo.host !== "github.com") {
29
+ return `--hostname ${escapeShellArg(repoInfo.host)}`;
30
+ }
31
+ return "";
32
+ }
33
+ /**
34
+ * Default timeout for waiting for fork readiness (60 seconds).
35
+ */
36
+ const FORK_READY_TIMEOUT_MS = 60_000;
37
+ /**
38
+ * Interval between fork readiness checks (2 seconds).
39
+ */
40
+ const FORK_POLL_INTERVAL_MS = 2_000;
41
+ export class GitHubLifecycleProvider {
42
+ platform = "github";
43
+ executor;
44
+ retries;
45
+ cwd;
46
+ forkReadyTimeoutMs;
47
+ forkPollIntervalMs;
48
+ constructor(options) {
49
+ const opts = options ?? {};
50
+ this.executor = opts.executor ?? defaultExecutor;
51
+ this.retries = opts.retries ?? 3;
52
+ this.cwd = opts.cwd ?? process.cwd();
53
+ this.forkReadyTimeoutMs = opts.forkReadyTimeoutMs ?? FORK_READY_TIMEOUT_MS;
54
+ this.forkPollIntervalMs = opts.forkPollIntervalMs ?? FORK_POLL_INTERVAL_MS;
55
+ }
56
+ /**
57
+ * Check if a GitHub owner is an organization (vs user).
58
+ * Uses gh api to query the user/org endpoint.
59
+ */
60
+ async isOrganization(owner, repoInfo, token) {
61
+ const tokenPrefix = this.buildTokenPrefix(token);
62
+ const hostnameFlag = getHostnameFlag(repoInfo);
63
+ const hostnamePart = hostnameFlag ? `${hostnameFlag} ` : "";
64
+ const command = `${tokenPrefix}gh api ${hostnamePart}users/${escapeShellArg(owner)}`;
65
+ try {
66
+ const stdout = await withRetry(() => this.executor.exec(command, this.cwd), { retries: this.retries });
67
+ const data = JSON.parse(stdout);
68
+ return data.type === "Organization";
69
+ }
70
+ catch (error) {
71
+ // If we can't determine, assume it's an org (safer - uses --org flag).
72
+ // This may cause fork to fail with a misleading error for personal accounts.
73
+ const errMsg = error instanceof Error ? error.message : String(error);
74
+ logger.debug(`Could not determine if '${owner}' is an organization, defaulting to org behavior: ${errMsg}`);
75
+ logger.info(`Warning: Could not verify if '${owner}' is an organization or user account. ` +
76
+ `If fork fails, check your authentication (gh auth status) and ensure the ` +
77
+ `target owner is correct.`);
78
+ return true;
79
+ }
80
+ }
81
+ assertGitHub(repoInfo) {
82
+ if (!isGitHubRepo(repoInfo)) {
83
+ throw new Error(`GitHubLifecycleProvider requires GitHub repo, got: ${repoInfo.type}`);
84
+ }
85
+ }
86
+ /**
87
+ * Build GH_TOKEN prefix for gh CLI commands.
88
+ * Returns "GH_TOKEN=<escaped_token> " when token is provided, "" otherwise.
89
+ * Token is escaped via escapeShellArg to prevent injection.
90
+ */
91
+ buildTokenPrefix(token) {
92
+ return token ? `GH_TOKEN=${escapeShellArg(token)} ` : "";
93
+ }
94
+ async exists(repoInfo, token) {
95
+ this.assertGitHub(repoInfo);
96
+ const tokenPrefix = this.buildTokenPrefix(token);
97
+ const hostnameFlag = getHostnameFlag(repoInfo);
98
+ const hostnamePart = hostnameFlag ? `${hostnameFlag} ` : "";
99
+ const command = `${tokenPrefix}gh api ${hostnamePart}repos/${escapeShellArg(repoInfo.owner)}/${escapeShellArg(repoInfo.repo)}`;
100
+ try {
101
+ // Note: withRetry already classifies 404/not-found as permanent errors,
102
+ // so retries are aborted immediately for non-existent repos.
103
+ await withRetry(() => this.executor.exec(command, this.cwd), {
104
+ retries: this.retries,
105
+ });
106
+ return true;
107
+ }
108
+ catch (error) {
109
+ // Distinguish "repo not found" from actual errors
110
+ if (isRepoNotFoundError(error)) {
111
+ return false;
112
+ }
113
+ // Re-throw network/auth errors
114
+ throw error;
115
+ }
116
+ }
117
+ async create(repoInfo, settings, token) {
118
+ this.assertGitHub(repoInfo);
119
+ const tokenPrefix = this.buildTokenPrefix(token);
120
+ const parts = [
121
+ `${tokenPrefix}gh repo create`,
122
+ escapeShellArg(`${repoInfo.owner}/${repoInfo.repo}`),
123
+ ];
124
+ // Visibility flag (default to private for safety)
125
+ if (settings?.visibility === "public") {
126
+ parts.push("--public");
127
+ }
128
+ else if (settings?.visibility === "internal") {
129
+ parts.push("--internal");
130
+ }
131
+ else {
132
+ parts.push("--private");
133
+ }
134
+ // Description
135
+ if (settings?.description) {
136
+ parts.push("--description", escapeShellArg(settings.description));
137
+ }
138
+ // Disable features if specified
139
+ if (settings?.hasIssues === false) {
140
+ parts.push("--disable-issues");
141
+ }
142
+ if (settings?.hasWiki === false) {
143
+ parts.push("--disable-wiki");
144
+ }
145
+ // Add --add-readme to establish the default branch via an initial commit.
146
+ // This avoids empty repos where HEAD doesn't resolve.
147
+ parts.push("--add-readme");
148
+ const command = parts.join(" ");
149
+ await withRetry(() => this.executor.exec(command, this.cwd), {
150
+ retries: this.retries,
151
+ });
152
+ // Delete the README so xfg sync starts from a clean state.
153
+ await this.deleteReadme(repoInfo, token);
154
+ }
155
+ async fork(upstream, target, settings, token) {
156
+ this.assertGitHub(upstream);
157
+ this.assertGitHub(target);
158
+ // Guard: cannot fork a repo to the same owner
159
+ if (upstream.owner.toLowerCase() === target.owner.toLowerCase()) {
160
+ throw new Error(`Cannot fork ${upstream.owner}/${upstream.repo} to the same owner '${target.owner}'. ` +
161
+ `The upstream and target owners must be different.`);
162
+ }
163
+ // Determine if target owner is an organization or user
164
+ const isOrg = await this.isOrganization(target.owner, target, token);
165
+ const tokenPrefix = this.buildTokenPrefix(token);
166
+ // Build fork command
167
+ // For orgs: gh repo fork <upstream> --org <target-org> --fork-name <name> --clone=false
168
+ // For users: gh repo fork <upstream> --fork-name <name> --clone=false
169
+ const parts = [
170
+ `${tokenPrefix}gh repo fork`,
171
+ escapeShellArg(`${upstream.owner}/${upstream.repo}`),
172
+ ];
173
+ if (isOrg) {
174
+ parts.push("--org", escapeShellArg(target.owner));
175
+ }
176
+ parts.push("--fork-name", escapeShellArg(target.repo), "--clone=false");
177
+ const forkCommand = parts.join(" ");
178
+ await withRetry(() => this.executor.exec(forkCommand, this.cwd), {
179
+ retries: this.retries,
180
+ });
181
+ // GitHub forks are async - wait for the fork to be ready for git operations
182
+ await this.waitForForkReady(target, this.forkReadyTimeoutMs, this.forkPollIntervalMs, token);
183
+ // Apply settings after fork (visibility, description, etc.)
184
+ if (settings?.visibility || settings?.description) {
185
+ await this.applyRepoSettings(target, settings, token);
186
+ }
187
+ }
188
+ /**
189
+ * Wait for a forked repo to become available via the GitHub API.
190
+ * GitHub forks are created asynchronously; polls exists() with a timeout.
191
+ */
192
+ async waitForForkReady(repoInfo, timeoutMs = FORK_READY_TIMEOUT_MS, intervalMs = FORK_POLL_INTERVAL_MS, token) {
193
+ const deadline = Date.now() + timeoutMs;
194
+ while (Date.now() < deadline) {
195
+ try {
196
+ const ready = await this.exists(repoInfo, token);
197
+ if (ready) {
198
+ return;
199
+ }
200
+ }
201
+ catch {
202
+ // Ignore transient errors during polling
203
+ }
204
+ const remaining = deadline - Date.now();
205
+ if (remaining <= 0)
206
+ break;
207
+ await new Promise((resolve) => setTimeout(resolve, Math.min(intervalMs, remaining)));
208
+ }
209
+ throw new Error(`Timed out waiting for fork ${repoInfo.owner}/${repoInfo.repo} to become available ` +
210
+ `after ${timeoutMs / 1000}s. The fork may still be processing on GitHub.`);
211
+ }
212
+ /**
213
+ * Apply settings to an existing repo using gh repo edit.
214
+ */
215
+ async applyRepoSettings(repoInfo, settings, token) {
216
+ const tokenPrefix = this.buildTokenPrefix(token);
217
+ const parts = [
218
+ `${tokenPrefix}gh repo edit`,
219
+ escapeShellArg(`${repoInfo.owner}/${repoInfo.repo}`),
220
+ ];
221
+ if (settings.visibility) {
222
+ parts.push("--visibility", settings.visibility, "--accept-visibility-change-consequences");
223
+ }
224
+ if (settings.description) {
225
+ parts.push("--description", escapeShellArg(settings.description));
226
+ }
227
+ const command = parts.join(" ");
228
+ await withRetry(() => this.executor.exec(command, this.cwd), {
229
+ retries: this.retries,
230
+ });
231
+ }
232
+ async receiveMigration(repoInfo, sourceDir, settings, token) {
233
+ this.assertGitHub(repoInfo);
234
+ const tokenPrefix = this.buildTokenPrefix(token);
235
+ // Remove existing "origin" remote if present (e.g., from git clone --mirror).
236
+ // gh repo create --source --push needs to set its own origin remote.
237
+ try {
238
+ await this.executor.exec(`git -C ${escapeShellArg(sourceDir)} remote remove origin`, this.cwd);
239
+ }
240
+ catch {
241
+ // No origin remote — nothing to remove
242
+ }
243
+ // Remove all non-standard refs that GitHub rejects on push.
244
+ // Mirror clones include ALL refs from the source, but GitHub only
245
+ // accepts branches (refs/heads/*) and tags (refs/tags/*).
246
+ // Other refs like refs/pull/* (GitHub), refs/merge-requests/* (GitLab),
247
+ // refs/keep-around/* etc. must be removed.
248
+ try {
249
+ const allRefs = await this.executor.exec(`git -C ${escapeShellArg(sourceDir)} for-each-ref --format='%(refname)'`, this.cwd);
250
+ for (const ref of allRefs.split("\n").filter((r) => r.trim())) {
251
+ const trimmed = ref.trim();
252
+ if (!trimmed.startsWith("refs/heads/") &&
253
+ !trimmed.startsWith("refs/tags/")) {
254
+ await this.executor.exec(`git -C ${escapeShellArg(sourceDir)} update-ref -d ${escapeShellArg(trimmed)}`, this.cwd);
255
+ }
256
+ }
257
+ }
258
+ catch {
259
+ // No refs to remove — ignore
260
+ }
261
+ // Use gh repo create --source --push to create and mirror in one step.
262
+ // For bare repos (from git clone --mirror), --push mirrors all refs.
263
+ // This uses gh CLI authentication, avoiding raw git auth issues with GHE.
264
+ const parts = [
265
+ `${tokenPrefix}gh repo create`,
266
+ escapeShellArg(`${repoInfo.owner}/${repoInfo.repo}`),
267
+ "--source",
268
+ escapeShellArg(sourceDir),
269
+ "--push",
270
+ ];
271
+ // Visibility flag (default to private for safety)
272
+ if (settings?.visibility === "public") {
273
+ parts.push("--public");
274
+ }
275
+ else if (settings?.visibility === "internal") {
276
+ parts.push("--internal");
277
+ }
278
+ else {
279
+ parts.push("--private");
280
+ }
281
+ // Description
282
+ if (settings?.description) {
283
+ parts.push("--description", escapeShellArg(settings.description));
284
+ }
285
+ // Disable features if specified
286
+ if (settings?.hasIssues === false) {
287
+ parts.push("--disable-issues");
288
+ }
289
+ if (settings?.hasWiki === false) {
290
+ parts.push("--disable-wiki");
291
+ }
292
+ const command = parts.join(" ");
293
+ await withRetry(() => this.executor.exec(command, this.cwd), {
294
+ retries: this.retries,
295
+ });
296
+ }
297
+ /**
298
+ * Delete the README.md that --add-readme creates.
299
+ * This leaves the repo with a default branch established (from the initial
300
+ * commit) but no files, so xfg sync starts from a clean state.
301
+ */
302
+ async deleteReadme(repoInfo, token) {
303
+ const tokenPrefix = this.buildTokenPrefix(token);
304
+ const hostnameFlag = getHostnameFlag(repoInfo);
305
+ const hostnamePart = hostnameFlag ? `${hostnameFlag} ` : "";
306
+ const apiPath = `repos/${escapeShellArg(repoInfo.owner)}/${escapeShellArg(repoInfo.repo)}`;
307
+ // Get the SHA of the README.md created by --add-readme
308
+ const fileInfo = await withRetry(() => this.executor.exec(`${tokenPrefix}gh api ${hostnamePart}${apiPath}/contents/README.md --jq '.sha'`, this.cwd), { retries: this.retries });
309
+ const sha = fileInfo.trim();
310
+ // Delete the README.md to leave the repo clean
311
+ await withRetry(() => this.executor.exec(`${tokenPrefix}gh api ${hostnamePart}${apiPath}/contents/README.md ` +
312
+ `--method DELETE -f message='Remove initialization file' -f sha=${escapeShellArg(sha)}`, this.cwd), { retries: this.retries });
313
+ }
314
+ }
@@ -0,0 +1,7 @@
1
+ export type { LifecyclePlatform, LifecycleResult, LifecycleOptions, CreateRepoSettings, IRepoLifecycleProvider, IMigrationSource, IRepoLifecycleFactory, IRepoLifecycleManager, } from "./types.js";
2
+ export { GitHubLifecycleProvider, type GitHubLifecycleProviderOptions, } from "./github-lifecycle-provider.js";
3
+ export { AdoMigrationSource } from "./ado-migration-source.js";
4
+ export { RepoLifecycleFactory } from "./repo-lifecycle-factory.js";
5
+ export { RepoLifecycleManager } from "./repo-lifecycle-manager.js";
6
+ export { formatLifecycleAction, type FormatOptions, } from "./lifecycle-formatter.js";
7
+ export { runLifecycleCheck, toCreateRepoSettings, type LifecycleCheckOptions, type LifecycleCheckResult, } from "./lifecycle-helpers.js";
@@ -0,0 +1,6 @@
1
+ export { GitHubLifecycleProvider, } from "./github-lifecycle-provider.js";
2
+ export { AdoMigrationSource } from "./ado-migration-source.js";
3
+ export { RepoLifecycleFactory } from "./repo-lifecycle-factory.js";
4
+ export { RepoLifecycleManager } from "./repo-lifecycle-manager.js";
5
+ export { formatLifecycleAction, } from "./lifecycle-formatter.js";
6
+ export { runLifecycleCheck, toCreateRepoSettings, } from "./lifecycle-helpers.js";
@@ -0,0 +1,14 @@
1
+ import type { LifecycleResult } from "./types.js";
2
+ export interface FormatOptions {
3
+ upstream?: string;
4
+ source?: string;
5
+ settings?: {
6
+ visibility?: string;
7
+ description?: string;
8
+ };
9
+ }
10
+ /**
11
+ * Format lifecycle action for output (used in both dry-run and real execution).
12
+ * Returns empty array if action is "existed" (no output needed).
13
+ */
14
+ export declare function formatLifecycleAction(result: LifecycleResult, options?: FormatOptions): string[];
@@ -0,0 +1,34 @@
1
+ import chalk from "chalk";
2
+ import { getRepoDisplayName } from "../shared/repo-detector.js";
3
+ /**
4
+ * Format lifecycle action for output (used in both dry-run and real execution).
5
+ * Returns empty array if action is "existed" (no output needed).
6
+ */
7
+ export function formatLifecycleAction(result, options) {
8
+ if (result.action === "existed") {
9
+ return [];
10
+ }
11
+ const lines = [];
12
+ const repoDisplay = getRepoDisplayName(result.repoInfo);
13
+ switch (result.action) {
14
+ case "created":
15
+ lines.push(chalk.green(`+ CREATE ${repoDisplay}`));
16
+ break;
17
+ case "forked":
18
+ lines.push(chalk.green(`+ FORK ${options?.upstream ?? "upstream"} -> ${repoDisplay}`));
19
+ break;
20
+ case "migrated":
21
+ lines.push(chalk.green(`+ MIGRATE ${options?.source ?? "source"} -> ${repoDisplay}`));
22
+ break;
23
+ }
24
+ // Add settings details if provided
25
+ if (options?.settings) {
26
+ if (options.settings.visibility) {
27
+ lines.push(` visibility: ${options.settings.visibility}`);
28
+ }
29
+ if (options.settings.description) {
30
+ lines.push(` description: "${options.settings.description}"`);
31
+ }
32
+ }
33
+ return lines;
34
+ }
@@ -0,0 +1,27 @@
1
+ import type { RepoConfig, GitHubRepoSettings } from "../config/types.js";
2
+ import type { RepoInfo } from "../shared/repo-detector.js";
3
+ import type { IRepoLifecycleManager, CreateRepoSettings, LifecycleResult } from "./types.js";
4
+ export interface LifecycleCheckOptions {
5
+ dryRun: boolean;
6
+ /** Base work directory (combined with repoIndex to compute full path). */
7
+ workDir?: string;
8
+ githubHosts?: string[];
9
+ /** Pre-resolved work directory. If provided, used directly instead of computing from workDir + repoIndex. */
10
+ resolvedWorkDir?: string;
11
+ /** Auth token (GitHub App installation token or PAT) for gh CLI commands */
12
+ token?: string;
13
+ }
14
+ /**
15
+ * Build CreateRepoSettings from GitHubRepoSettings.
16
+ * Extracts only the fields relevant for repo creation.
17
+ */
18
+ export declare function toCreateRepoSettings(repo: GitHubRepoSettings | undefined): CreateRepoSettings | undefined;
19
+ export interface LifecycleCheckResult {
20
+ lifecycleResult: LifecycleResult;
21
+ outputLines: string[];
22
+ }
23
+ /**
24
+ * Run lifecycle check for a single repo.
25
+ * Returns the lifecycle result and formatted output lines.
26
+ */
27
+ export declare function runLifecycleCheck(repoConfig: RepoConfig, repoInfo: RepoInfo, repoIndex: number, options: LifecycleCheckOptions, lifecycleManager: IRepoLifecycleManager, repoSettings?: GitHubRepoSettings): Promise<LifecycleCheckResult>;
@@ -0,0 +1,47 @@
1
+ import { resolve, join } from "node:path";
2
+ import { generateWorkspaceName } from "../shared/workspace-utils.js";
3
+ import { formatLifecycleAction } from "./lifecycle-formatter.js";
4
+ /**
5
+ * Build CreateRepoSettings from GitHubRepoSettings.
6
+ * Extracts only the fields relevant for repo creation.
7
+ */
8
+ export function toCreateRepoSettings(repo) {
9
+ if (!repo)
10
+ return undefined;
11
+ const result = {};
12
+ if (repo.visibility !== undefined)
13
+ result.visibility = repo.visibility;
14
+ if (repo.description !== undefined)
15
+ result.description = repo.description;
16
+ if (repo.hasIssues !== undefined)
17
+ result.hasIssues = repo.hasIssues;
18
+ if (repo.hasWiki !== undefined)
19
+ result.hasWiki = repo.hasWiki;
20
+ return Object.keys(result).length > 0 ? result : undefined;
21
+ }
22
+ /**
23
+ * Run lifecycle check for a single repo.
24
+ * Returns the lifecycle result and formatted output lines.
25
+ */
26
+ export async function runLifecycleCheck(repoConfig, repoInfo, repoIndex, options, lifecycleManager, repoSettings) {
27
+ const workDir = options.resolvedWorkDir ??
28
+ resolve(join(options.workDir ?? "./tmp", generateWorkspaceName(repoIndex)));
29
+ const createSettings = toCreateRepoSettings(repoSettings);
30
+ const lifecycleResult = await lifecycleManager.ensureRepo(repoConfig, repoInfo, {
31
+ dryRun: options.dryRun,
32
+ workDir,
33
+ githubHosts: options.githubHosts,
34
+ token: options.token,
35
+ }, createSettings);
36
+ const outputLines = formatLifecycleAction(lifecycleResult, {
37
+ upstream: repoConfig.upstream,
38
+ source: repoConfig.source,
39
+ settings: createSettings
40
+ ? {
41
+ visibility: createSettings.visibility,
42
+ description: createSettings.description,
43
+ }
44
+ : undefined,
45
+ });
46
+ return { lifecycleResult, outputLines };
47
+ }
@@ -0,0 +1,14 @@
1
+ import { ICommandExecutor } from "../shared/command-executor.js";
2
+ import type { IRepoLifecycleFactory, IRepoLifecycleProvider, IMigrationSource, LifecyclePlatform } from "./types.js";
3
+ /**
4
+ * Factory for creating lifecycle providers and migration sources.
5
+ */
6
+ export declare class RepoLifecycleFactory implements IRepoLifecycleFactory {
7
+ private readonly providers;
8
+ private readonly sources;
9
+ private readonly executor;
10
+ private readonly retries;
11
+ constructor(executor?: ICommandExecutor, retries?: number);
12
+ getProvider(platform: LifecyclePlatform): IRepoLifecycleProvider;
13
+ getMigrationSource(platform: LifecyclePlatform): IMigrationSource;
14
+ }
@@ -0,0 +1,57 @@
1
+ import { defaultExecutor, } from "../shared/command-executor.js";
2
+ import { GitHubLifecycleProvider } from "./github-lifecycle-provider.js";
3
+ import { AdoMigrationSource } from "./ado-migration-source.js";
4
+ /**
5
+ * Factory for creating lifecycle providers and migration sources.
6
+ */
7
+ export class RepoLifecycleFactory {
8
+ providers = new Map();
9
+ sources = new Map();
10
+ executor;
11
+ retries;
12
+ constructor(executor, retries) {
13
+ this.executor = executor ?? defaultExecutor;
14
+ this.retries = retries ?? 3;
15
+ }
16
+ getProvider(platform) {
17
+ // Check cache first
18
+ const cached = this.providers.get(platform);
19
+ if (cached) {
20
+ return cached;
21
+ }
22
+ // Create provider
23
+ let provider;
24
+ switch (platform) {
25
+ case "github":
26
+ provider = new GitHubLifecycleProvider({
27
+ executor: this.executor,
28
+ retries: this.retries,
29
+ });
30
+ break;
31
+ default:
32
+ throw new Error(`Platform '${platform}' not supported as target for lifecycle operations. ` +
33
+ `Currently supported: github`);
34
+ }
35
+ this.providers.set(platform, provider);
36
+ return provider;
37
+ }
38
+ getMigrationSource(platform) {
39
+ // Check cache first
40
+ const cached = this.sources.get(platform);
41
+ if (cached) {
42
+ return cached;
43
+ }
44
+ // Create source
45
+ let source;
46
+ switch (platform) {
47
+ case "azure-devops":
48
+ source = new AdoMigrationSource(this.executor, this.retries);
49
+ break;
50
+ default:
51
+ throw new Error(`Platform '${platform}' not supported as migration source. ` +
52
+ `Currently supported: azure-devops`);
53
+ }
54
+ this.sources.set(platform, source);
55
+ return source;
56
+ }
57
+ }
@@ -0,0 +1,20 @@
1
+ import { type RepoInfo } from "../shared/repo-detector.js";
2
+ import type { RepoConfig } from "../config/types.js";
3
+ import type { IRepoLifecycleManager, IRepoLifecycleFactory, LifecycleResult, LifecycleOptions, CreateRepoSettings } from "./types.js";
4
+ /**
5
+ * Orchestrates repo lifecycle operations before sync.
6
+ */
7
+ export declare class RepoLifecycleManager implements IRepoLifecycleManager {
8
+ private readonly factory;
9
+ constructor(factory?: IRepoLifecycleFactory, retries?: number);
10
+ ensureRepo(repoConfig: RepoConfig, repoInfo: RepoInfo, options: LifecycleOptions, settings?: CreateRepoSettings): Promise<LifecycleResult>;
11
+ private create;
12
+ private fork;
13
+ private migrate;
14
+ /**
15
+ * Polls provider.exists() until the repo is visible, with timeout.
16
+ * GitHub's API may return success from create/fork before the git
17
+ * backend has fully propagated, causing subsequent clone to 403.
18
+ */
19
+ private waitForRepoReady;
20
+ }