@aspruyt/xfg 6.0.0 → 6.0.2

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 (99) hide show
  1. package/dist/cli/sync-command.js +33 -35
  2. package/dist/cli/types.d.ts +12 -11
  3. package/dist/{output → cli}/unified-summary.d.ts +3 -3
  4. package/dist/{output → cli}/unified-summary.js +4 -4
  5. package/dist/config/file-reference-resolver.js +24 -56
  6. package/dist/config/normalizer.js +29 -40
  7. package/dist/config/validator.js +94 -102
  8. package/dist/lifecycle/ado-migration-source.d.ts +1 -1
  9. package/dist/lifecycle/ado-migration-source.js +1 -1
  10. package/dist/lifecycle/github-lifecycle-provider.d.ts +5 -6
  11. package/dist/lifecycle/github-lifecycle-provider.js +50 -20
  12. package/dist/lifecycle/lifecycle-formatter.d.ts +2 -1
  13. package/dist/lifecycle/lifecycle-formatter.js +1 -1
  14. package/dist/lifecycle/lifecycle-helpers.d.ts +1 -1
  15. package/dist/lifecycle/repo-lifecycle-manager.d.ts +1 -1
  16. package/dist/lifecycle/repo-lifecycle-manager.js +16 -6
  17. package/dist/lifecycle/types.d.ts +30 -8
  18. package/dist/output/lifecycle-report.d.ts +4 -2
  19. package/dist/output/settings-report.d.ts +4 -4
  20. package/dist/repo/detector.d.ts +8 -0
  21. package/dist/{shared/repo-detector.js → repo/detector.js} +1 -4
  22. package/dist/repo/index.d.ts +4 -0
  23. package/dist/repo/index.js +3 -0
  24. package/dist/{shared/repo-metadata-provider.d.ts → repo/metadata-provider.d.ts} +3 -3
  25. package/dist/{shared/repo-metadata-provider.js → repo/metadata-provider.js} +3 -3
  26. package/dist/{shared/repo-detector.d.ts → repo/types.d.ts} +1 -7
  27. package/dist/repo/types.js +1 -0
  28. package/dist/{shared/repo-info-utils.d.ts → repo/utils.d.ts} +1 -1
  29. package/dist/{shared/repo-info-utils.js → repo/utils.js} +1 -1
  30. package/dist/settings/base-processor.d.ts +1 -1
  31. package/dist/settings/base-processor.js +1 -1
  32. package/dist/settings/code-scanning/github-code-scanning-strategy.d.ts +1 -1
  33. package/dist/settings/code-scanning/github-code-scanning-strategy.js +1 -1
  34. package/dist/settings/code-scanning/processor.d.ts +2 -2
  35. package/dist/settings/code-scanning/types.d.ts +1 -1
  36. package/dist/settings/index.d.ts +1 -1
  37. package/dist/settings/labels/formatter.js +16 -11
  38. package/dist/settings/labels/github-labels-strategy.d.ts +1 -1
  39. package/dist/settings/labels/github-labels-strategy.js +1 -1
  40. package/dist/settings/labels/processor.d.ts +1 -1
  41. package/dist/settings/labels/types.d.ts +1 -1
  42. package/dist/settings/repo-settings/diff.d.ts +1 -1
  43. package/dist/settings/repo-settings/diff.js +1 -1
  44. package/dist/settings/repo-settings/formatter.js +2 -4
  45. package/dist/settings/repo-settings/github-repo-settings-strategy.d.ts +4 -4
  46. package/dist/settings/repo-settings/github-repo-settings-strategy.js +4 -4
  47. package/dist/settings/repo-settings/processor.d.ts +2 -2
  48. package/dist/settings/repo-settings/processor.js +5 -5
  49. package/dist/settings/repo-settings/types.d.ts +4 -4
  50. package/dist/settings/rulesets/diff-algorithm.js +1 -1
  51. package/dist/settings/rulesets/formatter.js +0 -3
  52. package/dist/settings/rulesets/github-ruleset-strategy.d.ts +1 -1
  53. package/dist/settings/rulesets/github-ruleset-strategy.js +1 -1
  54. package/dist/settings/rulesets/processor.d.ts +1 -1
  55. package/dist/settings/rulesets/types.d.ts +1 -1
  56. package/dist/shared/command-executor.js +3 -3
  57. package/dist/shared/gh-api-utils.d.ts +7 -4
  58. package/dist/shared/gh-api-utils.js +2 -2
  59. package/dist/shared/retry-utils.js +1 -1
  60. package/dist/shared/xfg-template.d.ts +22 -2
  61. package/dist/sync/auth-options-builder.d.ts +1 -1
  62. package/dist/sync/auth-options-builder.js +1 -1
  63. package/dist/sync/branch-manager.d.ts +1 -1
  64. package/dist/sync/commit-push-manager.d.ts +2 -2
  65. package/dist/sync/commit-push-manager.js +5 -3
  66. package/dist/sync/file-sync-orchestrator.d.ts +1 -1
  67. package/dist/sync/file-sync-strategy.d.ts +1 -1
  68. package/dist/sync/file-writer.js +44 -10
  69. package/dist/sync/repository-processor.d.ts +1 -1
  70. package/dist/sync/repository-session.d.ts +1 -1
  71. package/dist/sync/sync-workflow.d.ts +1 -1
  72. package/dist/sync/sync-workflow.js +2 -1
  73. package/dist/sync/types.d.ts +7 -4
  74. package/dist/vcs/{azure-pr-strategy.d.ts → ado-pr-strategy.d.ts} +2 -2
  75. package/dist/vcs/{azure-pr-strategy.js → ado-pr-strategy.js} +4 -4
  76. package/dist/vcs/authenticated-git-ops.d.ts +2 -0
  77. package/dist/vcs/authenticated-git-ops.js +6 -0
  78. package/dist/vcs/commit-strategy-selector.d.ts +1 -1
  79. package/dist/vcs/commit-strategy-selector.js +1 -1
  80. package/dist/vcs/file-mode-fixup-commit-strategy.d.ts +8 -6
  81. package/dist/vcs/file-mode-fixup-commit-strategy.js +79 -30
  82. package/dist/vcs/git-ops.d.ts +15 -3
  83. package/dist/vcs/git-ops.js +57 -24
  84. package/dist/vcs/github-app-token-manager.d.ts +1 -1
  85. package/dist/vcs/github-pr-strategy.d.ts +1 -1
  86. package/dist/vcs/github-pr-strategy.js +4 -4
  87. package/dist/vcs/gitlab-pr-strategy.d.ts +1 -1
  88. package/dist/vcs/gitlab-pr-strategy.js +4 -4
  89. package/dist/vcs/graphql-commit-strategy.js +8 -3
  90. package/dist/vcs/index.d.ts +1 -1
  91. package/dist/vcs/pr-creator.d.ts +1 -1
  92. package/dist/vcs/pr-strategy-factory.d.ts +1 -1
  93. package/dist/vcs/pr-strategy-factory.js +3 -3
  94. package/dist/vcs/pr-strategy.d.ts +1 -1
  95. package/dist/vcs/pr-strategy.js +1 -1
  96. package/dist/vcs/types.d.ts +10 -3
  97. package/package.json +3 -3
  98. /package/dist/{shared → vcs}/sanitize-utils.d.ts +0 -0
  99. /package/dist/{shared → vcs}/sanitize-utils.js +0 -0
@@ -134,79 +134,87 @@ function buildRootSettingsContext(config) {
134
134
  /**
135
135
  * Validates settings object containing rulesets, labels, and repo settings.
136
136
  */
137
- function validateSettings(settings, context, rootCtx) {
138
- if (!isPlainObject(settings)) {
139
- throw new ValidationError(`${context}: settings must be an object`);
137
+ function validateSettingsRulesets(settings, context, rootCtx) {
138
+ if (settings.rulesets === undefined)
139
+ return;
140
+ if (!isPlainObject(settings.rulesets)) {
141
+ throw new ValidationError(`${context}: rulesets must be an object`);
140
142
  }
141
- if (settings.rulesets !== undefined) {
142
- if (!isPlainObject(settings.rulesets)) {
143
- throw new ValidationError(`${context}: rulesets must be an object`);
144
- }
145
- const rulesets = settings.rulesets;
146
- for (const [name, ruleset] of Object.entries(rulesets)) {
147
- // Skip reserved key
148
- if (name === "inherit")
149
- continue;
150
- if (ruleset === false) {
151
- if (rootCtx && !rootCtx.rulesetNames.includes(name)) {
152
- throw new ValidationError(`${context}: Cannot opt out of '${name}' - not defined in root settings.rulesets`);
153
- }
154
- continue; // Skip further validation for false entries
143
+ const rulesets = settings.rulesets;
144
+ for (const [name, ruleset] of Object.entries(rulesets)) {
145
+ if (name === "inherit")
146
+ continue;
147
+ if (ruleset === false) {
148
+ if (rootCtx && !rootCtx.rulesetNames.includes(name)) {
149
+ throw new ValidationError(`${context}: Cannot opt out of '${name}' - not defined in root settings.rulesets`);
155
150
  }
156
- validateRuleset(ruleset, name, context);
151
+ continue;
157
152
  }
153
+ validateRuleset(ruleset, name, context);
158
154
  }
159
- if (settings.labels !== undefined) {
160
- if (!isPlainObject(settings.labels)) {
161
- throw new ValidationError(`${context}: labels must be an object`);
162
- }
163
- const labels = settings.labels;
164
- for (const [name, label] of Object.entries(labels)) {
165
- if (name === "inherit")
166
- continue;
167
- if (label === false) {
168
- if (rootCtx && !rootCtx.labelNames.includes(name)) {
169
- throw new ValidationError(`${context}: Cannot opt out of label '${name}' - not defined in root settings.labels`);
170
- }
171
- continue;
155
+ }
156
+ function validateSettingsLabels(settings, context, rootCtx) {
157
+ if (settings.labels === undefined)
158
+ return;
159
+ if (!isPlainObject(settings.labels)) {
160
+ throw new ValidationError(`${context}: labels must be an object`);
161
+ }
162
+ const labels = settings.labels;
163
+ for (const [name, label] of Object.entries(labels)) {
164
+ if (name === "inherit")
165
+ continue;
166
+ if (label === false) {
167
+ if (rootCtx && !rootCtx.labelNames.includes(name)) {
168
+ throw new ValidationError(`${context}: Cannot opt out of label '${name}' - not defined in root settings.labels`);
172
169
  }
173
- validateLabel(label, name, context);
170
+ continue;
174
171
  }
172
+ validateLabel(label, name, context);
175
173
  }
174
+ }
175
+ function validateSettingsDeleteOrphaned(settings, context) {
176
176
  if (settings.deleteOrphaned !== undefined &&
177
177
  typeof settings.deleteOrphaned !== "boolean") {
178
178
  throw new ValidationError(`${context}: settings.deleteOrphaned must be a boolean`);
179
179
  }
180
- if (settings.repo !== undefined) {
181
- if (settings.repo === false) {
182
- if (!rootCtx) {
183
- // Root level — repo: false not valid here
184
- throw new ValidationError(`${context}: repo: false is not valid at root level. Define repo settings or remove the field.`);
185
- }
186
- // Per-repo level check root has repo settings to opt out of
187
- if (!rootCtx.hasRepoSettings) {
188
- throw new ValidationError(`${context}: Cannot opt out of repo settings — not defined in root settings.repo`);
189
- }
190
- // Valid opt-out, skip further repo validation
180
+ }
181
+ function validateSettingsRepo(settings, context, rootCtx) {
182
+ if (settings.repo === undefined)
183
+ return;
184
+ if (settings.repo === false) {
185
+ if (!rootCtx) {
186
+ throw new ValidationError(`${context}: repo: false is not valid at root level. Define repo settings or remove the field.`);
191
187
  }
192
- else {
193
- validateRepoSettings(settings.repo, context);
188
+ if (!rootCtx.hasRepoSettings) {
189
+ throw new ValidationError(`${context}: Cannot opt out of repo settings — not defined in root settings.repo`);
194
190
  }
191
+ return;
195
192
  }
196
- if (settings.codeScanning !== undefined) {
197
- if (settings.codeScanning === false) {
198
- if (!rootCtx) {
199
- throw new ValidationError(`${context}: codeScanning: false is not valid at root level. Define codeScanning settings or remove the field.`);
200
- }
201
- // Per-repo level — check root has codeScanning settings to opt out of
202
- if (!rootCtx.hasCodeScanningSettings) {
203
- throw new ValidationError(`${context}: Cannot opt out of code scanning settings not defined in root settings.codeScanning`);
204
- }
193
+ validateRepoSettings(settings.repo, context);
194
+ }
195
+ function validateSettingsCodeScanning(settings, context, rootCtx) {
196
+ if (settings.codeScanning === undefined)
197
+ return;
198
+ if (settings.codeScanning === false) {
199
+ if (!rootCtx) {
200
+ throw new ValidationError(`${context}: codeScanning: false is not valid at root level. Define codeScanning settings or remove the field.`);
205
201
  }
206
- else {
207
- validateCodeScanningSettings(settings.codeScanning, `${context} codeScanning`);
202
+ if (!rootCtx.hasCodeScanningSettings) {
203
+ throw new ValidationError(`${context}: Cannot opt out of code scanning settings — not defined in root settings.codeScanning`);
208
204
  }
205
+ return;
206
+ }
207
+ validateCodeScanningSettings(settings.codeScanning, `${context} codeScanning`);
208
+ }
209
+ function validateSettings(settings, context, rootCtx) {
210
+ if (!isPlainObject(settings)) {
211
+ throw new ValidationError(`${context}: settings must be an object`);
209
212
  }
213
+ validateSettingsRulesets(settings, context, rootCtx);
214
+ validateSettingsLabels(settings, context, rootCtx);
215
+ validateSettingsDeleteOrphaned(settings, context);
216
+ validateSettingsRepo(settings, context, rootCtx);
217
+ validateSettingsCodeScanning(settings, context, rootCtx);
210
218
  }
211
219
  const VALID_CODE_SCANNING_STATES = ["configured", "not-configured"];
212
220
  const VALID_CODE_SCANNING_QUERY_SUITES = ["default", "extended"];
@@ -565,9 +573,9 @@ function validateRepoFiles(config, repo, index, repoLabel) {
565
573
  for (const groupName of expandedGroups) {
566
574
  const group = config.groups[groupName];
567
575
  if (group?.files) {
568
- for (const fn of Object.keys(group.files)) {
569
- if (fn !== "inherit")
570
- knownFiles.add(fn);
576
+ for (const fileName of Object.keys(group.files)) {
577
+ if (fileName !== "inherit")
578
+ knownFiles.add(fileName);
571
579
  }
572
580
  }
573
581
  }
@@ -575,9 +583,9 @@ function validateRepoFiles(config, repo, index, repoLabel) {
575
583
  if (config.conditionalGroups) {
576
584
  for (const cg of config.conditionalGroups) {
577
585
  if (cg.files) {
578
- for (const fn of Object.keys(cg.files)) {
579
- if (fn !== "inherit")
580
- knownFiles.add(fn);
586
+ for (const fileName of Object.keys(cg.files)) {
587
+ if (fileName !== "inherit")
588
+ knownFiles.add(fileName);
581
589
  }
582
590
  }
583
591
  }
@@ -604,6 +612,28 @@ function validateRepoFiles(config, repo, index, repoLabel) {
604
612
  validateFileConfigFields(fileOverride, fileName, `Repo ${repoLabel}:`);
605
613
  }
606
614
  }
615
+ function enrichSettingsContext(rootCtx, settings) {
616
+ if (!settings)
617
+ return;
618
+ if (settings.rulesets) {
619
+ for (const name of Object.keys(settings.rulesets)) {
620
+ if (name !== "inherit")
621
+ rootCtx.rulesetNames.push(name);
622
+ }
623
+ }
624
+ if (settings.labels) {
625
+ for (const name of Object.keys(settings.labels)) {
626
+ if (name !== "inherit")
627
+ rootCtx.labelNames.push(name);
628
+ }
629
+ }
630
+ if (settings.repo !== undefined && settings.repo !== false) {
631
+ rootCtx.hasRepoSettings = true;
632
+ }
633
+ if (settings.codeScanning !== undefined && settings.codeScanning !== false) {
634
+ rootCtx.hasCodeScanningSettings = true;
635
+ }
636
+ }
607
637
  function validateRepoSettingsEntry(config, repo, repoLabel) {
608
638
  if (repo.settings === undefined)
609
639
  return;
@@ -611,50 +641,12 @@ function validateRepoSettingsEntry(config, repo, repoLabel) {
611
641
  if (repo.groups && config.groups) {
612
642
  const expandedGroups = expandRepoGroups(repo.groups, config.groups);
613
643
  for (const groupName of expandedGroups) {
614
- const group = config.groups[groupName];
615
- if (group?.settings?.rulesets) {
616
- for (const name of Object.keys(group.settings.rulesets)) {
617
- if (name !== "inherit")
618
- rootCtx.rulesetNames.push(name);
619
- }
620
- }
621
- if (group?.settings?.labels) {
622
- for (const name of Object.keys(group.settings.labels)) {
623
- if (name !== "inherit")
624
- rootCtx.labelNames.push(name);
625
- }
626
- }
627
- if (group?.settings?.repo !== undefined &&
628
- group.settings.repo !== false) {
629
- rootCtx.hasRepoSettings = true;
630
- }
631
- if (group?.settings?.codeScanning !== undefined &&
632
- group.settings.codeScanning !== false) {
633
- rootCtx.hasCodeScanningSettings = true;
634
- }
644
+ enrichSettingsContext(rootCtx, config.groups[groupName]?.settings);
635
645
  }
636
646
  }
637
647
  if (config.conditionalGroups) {
638
648
  for (const cg of config.conditionalGroups) {
639
- if (cg.settings?.rulesets) {
640
- for (const name of Object.keys(cg.settings.rulesets)) {
641
- if (name !== "inherit")
642
- rootCtx.rulesetNames.push(name);
643
- }
644
- }
645
- if (cg.settings?.labels) {
646
- for (const name of Object.keys(cg.settings.labels)) {
647
- if (name !== "inherit")
648
- rootCtx.labelNames.push(name);
649
- }
650
- }
651
- if (cg.settings?.repo !== undefined && cg.settings.repo !== false) {
652
- rootCtx.hasRepoSettings = true;
653
- }
654
- if (cg.settings?.codeScanning !== undefined &&
655
- cg.settings.codeScanning !== false) {
656
- rootCtx.hasCodeScanningSettings = true;
657
- }
649
+ enrichSettingsContext(rootCtx, cg.settings);
658
650
  }
659
651
  }
660
652
  validateSettings(repo.settings, `Repo ${repoLabel}`, rootCtx);
@@ -1,5 +1,5 @@
1
1
  import type { ICommandExecutor } from "../shared/command-executor.js";
2
- import { type RepoInfo } from "../shared/repo-detector.js";
2
+ import { type RepoInfo } from "../repo/index.js";
3
3
  import type { IMigrationSource, LifecyclePlatform } from "./types.js";
4
4
  /**
5
5
  * Azure DevOps implementation of IMigrationSource.
@@ -2,7 +2,7 @@ import { escapeShellArg } from "../shared/shell-utils.js";
2
2
  import { withRetry } from "../shared/retry-utils.js";
3
3
  import { toErrorMessage } from "../shared/type-guards.js";
4
4
  import { LifecycleError } from "../shared/errors.js";
5
- import { isAzureDevOpsRepo, } from "../shared/repo-detector.js";
5
+ import { isAzureDevOpsRepo, } from "../repo/index.js";
6
6
  /**
7
7
  * Azure DevOps implementation of IMigrationSource.
8
8
  * Uses git clone --mirror to get all refs for migration.
@@ -1,7 +1,6 @@
1
1
  import type { ICommandExecutor } from "../shared/command-executor.js";
2
- import { type RepoInfo } from "../shared/repo-detector.js";
3
2
  import type { DebugWarnLog } from "../shared/logger.js";
4
- import type { IRepoLifecycleProvider, LifecyclePlatform, CreateRepoSettings } from "./types.js";
3
+ import type { IRepoLifecycleProvider, LifecyclePlatform, LifecycleExistsParams, LifecycleCreateParams, LifecycleForkParams, LifecycleReceiveMigrationParams } from "./types.js";
5
4
  /**
6
5
  * GitHub implementation of IRepoLifecycleProvider.
7
6
  * Uses gh CLI for all operations.
@@ -37,9 +36,9 @@ export declare class GitHubLifecycleProvider implements IRepoLifecycleProvider {
37
36
  * (e.g., "gh api --hostname host repos/owner/repo").
38
37
  */
39
38
  private buildGhApiPrefix;
40
- exists(repoInfo: RepoInfo, token?: string): Promise<boolean>;
41
- create(repoInfo: RepoInfo, settings?: CreateRepoSettings, token?: string): Promise<void>;
42
- fork(upstream: RepoInfo, target: RepoInfo, settings?: CreateRepoSettings, token?: string): Promise<void>;
39
+ exists(params: LifecycleExistsParams): Promise<boolean>;
40
+ create(params: LifecycleCreateParams): Promise<void>;
41
+ fork(params: LifecycleForkParams): Promise<void>;
43
42
  /**
44
43
  * Wait for a forked repo to become available via the GitHub API.
45
44
  * GitHub forks are created asynchronously; polls exists() with a timeout.
@@ -49,7 +48,7 @@ export declare class GitHubLifecycleProvider implements IRepoLifecycleProvider {
49
48
  * Apply settings to an existing repo using gh repo edit.
50
49
  */
51
50
  private applyRepoSettings;
52
- receiveMigration(repoInfo: RepoInfo, sourceDir: string, settings?: CreateRepoSettings, token?: string): Promise<void>;
51
+ receiveMigration(params: LifecycleReceiveMigrationParams): Promise<void>;
53
52
  /**
54
53
  * Rename a branch via the GitHub branch rename API.
55
54
  * GitHub automatically updates the default branch pointer.
@@ -1,6 +1,6 @@
1
1
  import { escapeShellArg } from "../shared/shell-utils.js";
2
2
  import { withRetry, isPermanentError, DEFAULT_PERMANENT_ERROR_PATTERNS, } from "../shared/retry-utils.js";
3
- import { assertGitHubRepo, } from "../shared/repo-detector.js";
3
+ import { assertGitHubRepo, } from "../repo/index.js";
4
4
  import { toErrorMessage } from "../shared/type-guards.js";
5
5
  import { LifecycleError } from "../shared/errors.js";
6
6
  import { buildTokenEnv, getHostnameFlag } from "../shared/gh-api-utils.js";
@@ -29,8 +29,15 @@ const FORK_READY_TIMEOUT_MS = 60_000;
29
29
  /**
30
30
  * After repo creation, GitHub may return 404 due to eventual consistency.
31
31
  * Exclude 404/not-found from permanent errors so withRetry retries them.
32
+ * The test string must trigger all not-found-family patterns in
33
+ * CORE_PERMANENT_ERROR_PATTERNS: /404\b/, /not\s*found/i,
34
+ * /repository\s*not\s*found/i, and /does\s*not\s*exist/i.
32
35
  */
33
- const POST_CREATE_PERMANENT_PATTERNS = DEFAULT_PERMANENT_ERROR_PATTERNS.filter((p) => !p.test("404 Not Found"));
36
+ const POST_CREATE_TEST_STRING = "404 Repository not found. Resource does not exist";
37
+ const POST_CREATE_PERMANENT_PATTERNS = [
38
+ ...DEFAULT_PERMANENT_ERROR_PATTERNS.filter((p) => !p.test(POST_CREATE_TEST_STRING)),
39
+ /already\s*exists/i,
40
+ ];
34
41
  /**
35
42
  * Interval between fork readiness checks (2 seconds).
36
43
  */
@@ -119,7 +126,8 @@ export class GitHubLifecycleProvider {
119
126
  const apiPath = `repos/${escapeShellArg(repoInfo.owner)}/${escapeShellArg(repoInfo.repo)}`;
120
127
  return { tokenEnv, prefix: `gh api ${hostnamePart}`, apiPath };
121
128
  }
122
- async exists(repoInfo, token) {
129
+ async exists(params) {
130
+ const { repo: repoInfo, token } = params;
123
131
  this.assertGitHub(repoInfo);
124
132
  const { tokenEnv, prefix, apiPath } = this.buildGhApiPrefix(repoInfo, token);
125
133
  const command = `${prefix}${apiPath}`;
@@ -140,7 +148,8 @@ export class GitHubLifecycleProvider {
140
148
  throw error;
141
149
  }
142
150
  }
143
- async create(repoInfo, settings, token) {
151
+ async create(params) {
152
+ const { repo: repoInfo, settings, token } = params;
144
153
  this.assertGitHub(repoInfo);
145
154
  const tokenEnv = buildTokenEnv(token);
146
155
  const parts = [
@@ -175,7 +184,8 @@ export class GitHubLifecycleProvider {
175
184
  // Delete the README so xfg sync starts from a clean state.
176
185
  await this.deleteReadme(repoInfo, token);
177
186
  }
178
- async fork(upstream, target, settings, token) {
187
+ async fork(params) {
188
+ const { upstream, target, settings, token } = params;
179
189
  this.assertGitHub(upstream);
180
190
  this.assertGitHub(target);
181
191
  // Guard: cannot fork a repo to the same owner
@@ -223,7 +233,7 @@ export class GitHubLifecycleProvider {
223
233
  const deadline = Date.now() + timeoutMs;
224
234
  while (Date.now() < deadline) {
225
235
  try {
226
- const ready = await this.exists(repoInfo, token);
236
+ const ready = await this.exists({ repo: repoInfo, token });
227
237
  if (ready) {
228
238
  return;
229
239
  }
@@ -259,7 +269,8 @@ export class GitHubLifecycleProvider {
259
269
  retries: this.retries,
260
270
  });
261
271
  }
262
- async receiveMigration(repoInfo, sourceDir, settings, token) {
272
+ async receiveMigration(params) {
273
+ const { repo: repoInfo, sourceDir, settings, token } = params;
263
274
  this.assertGitHub(repoInfo);
264
275
  const tokenEnv = buildTokenEnv(token);
265
276
  // Remove existing "origin" remote if present (e.g., from git clone --mirror).
@@ -302,21 +313,40 @@ export class GitHubLifecycleProvider {
302
313
  await this.executor.exec(`git -C ${escapeShellArg(sourceDir)} symbolic-ref HEAD refs/heads/${escapeShellArg(settings.defaultBranch)}`, this.cwd);
303
314
  }
304
315
  }
305
- // Use gh repo create --source --push to create and mirror in one step.
306
- // For bare repos (from git clone --mirror), --push mirrors all refs.
307
- // This uses gh CLI authentication, avoiding raw git auth issues with GHE.
308
- const parts = [
309
- "gh repo create",
310
- escapeShellArg(`${repoInfo.owner}/${repoInfo.repo}`),
311
- "--source",
312
- escapeShellArg(sourceDir),
313
- "--push",
314
- ];
315
- buildRepoCreateFlags(parts, settings);
316
- const command = parts.join(" ");
317
- await withRetry(() => this.executor.exec(command, this.cwd, { env: tokenEnv }), {
316
+ // Split create and push into two steps. gh repo create --source --push
317
+ // does both atomically, but if the git backend hasn't propagated after
318
+ // the GraphQL create, the push fails with "Repository not found". A
319
+ // retry then hits "Name already exists" on the create step.
320
+ const repoSlug = `${repoInfo.owner}/${repoInfo.repo}`;
321
+ const createParts = ["gh repo create", escapeShellArg(repoSlug)];
322
+ buildRepoCreateFlags(createParts, settings);
323
+ try {
324
+ await withRetry(() => this.executor.exec(createParts.join(" "), this.cwd, {
325
+ env: tokenEnv,
326
+ }), {
327
+ retries: this.retries,
328
+ permanentErrorPatterns: POST_CREATE_PERMANENT_PATTERNS,
329
+ log: this.log
330
+ ? { info: (m) => this.log.warn(m) }
331
+ : undefined,
332
+ });
333
+ }
334
+ catch (error) {
335
+ if (!/already\s*exists/i.test(toErrorMessage(error))) {
336
+ throw error;
337
+ }
338
+ }
339
+ // Push mirror content via authenticated URL. Retries handle the git
340
+ // backend propagation delay (POST_CREATE_PERMANENT_PATTERNS allows
341
+ // retry on 404/not-found).
342
+ const remoteUrl = token
343
+ ? `https://x-access-token:${token}@${repoInfo.host}/${repoSlug}.git`
344
+ : `https://${repoInfo.host}/${repoSlug}.git`;
345
+ await this.executor.exec(`git -C ${escapeShellArg(sourceDir)} remote add origin ${escapeShellArg(remoteUrl)}`, this.cwd);
346
+ await withRetry(() => this.executor.exec(`git -C ${escapeShellArg(sourceDir)} push --mirror origin`, this.cwd, { env: tokenEnv }), {
318
347
  retries: this.retries,
319
348
  permanentErrorPatterns: POST_CREATE_PERMANENT_PATTERNS,
349
+ log: this.log ? { info: (m) => this.log.warn(m) } : undefined,
320
350
  });
321
351
  }
322
352
  /**
@@ -1,9 +1,10 @@
1
1
  import type { LifecycleResult } from "./types.js";
2
+ import type { RepoVisibility } from "../config/index.js";
2
3
  interface FormatOptions {
3
4
  upstream?: string;
4
5
  source?: string;
5
6
  settings?: {
6
- visibility?: string;
7
+ visibility?: RepoVisibility;
7
8
  description?: string;
8
9
  };
9
10
  }
@@ -1,5 +1,5 @@
1
1
  import chalk from "chalk";
2
- import { getRepoDisplayName } from "../shared/repo-detector.js";
2
+ import { getRepoDisplayName } from "../repo/index.js";
3
3
  import { SyncError } from "../shared/errors.js";
4
4
  /**
5
5
  * Format lifecycle action for output (used in both dry-run and real execution).
@@ -1,5 +1,5 @@
1
1
  import type { RepoConfig, GitHubRepoSettings } from "../config/index.js";
2
- import type { RepoInfo } from "../shared/repo-detector.js";
2
+ import type { RepoInfo } from "../repo/index.js";
3
3
  import type { IRepoLifecycleManager, CreateRepoSettings, LifecycleResult } from "./types.js";
4
4
  interface LifecycleCheckOptions {
5
5
  dryRun: boolean;
@@ -1,4 +1,4 @@
1
- import { type RepoInfo } from "../shared/repo-detector.js";
1
+ import { type RepoInfo } from "../repo/index.js";
2
2
  import { type DebugInfoWarnLog } from "../shared/logger.js";
3
3
  import type { ICommandExecutor } from "../shared/command-executor.js";
4
4
  import type { RepoConfig } from "../config/index.js";
@@ -1,6 +1,6 @@
1
1
  import { join } from "node:path";
2
2
  import { rm } from "node:fs/promises";
3
- import { parseGitUrl } from "../shared/repo-detector.js";
3
+ import { parseGitUrl } from "../repo/index.js";
4
4
  import { safeCleanup } from "../shared/cleanup-utils.js";
5
5
  import { LifecycleError } from "../shared/errors.js";
6
6
  import { NO_OP_DEBUG_LOG } from "../shared/logger.js";
@@ -31,7 +31,7 @@ export class RepoLifecycleManager {
31
31
  return { repoInfo, action: "existed" };
32
32
  }
33
33
  const { token } = options;
34
- const exists = await provider.exists(repoInfo, token);
34
+ const exists = await provider.exists({ repo: repoInfo, token });
35
35
  if (exists) {
36
36
  // Repo exists - nothing to do (ignore upstream/source)
37
37
  return {
@@ -59,7 +59,7 @@ export class RepoLifecycleManager {
59
59
  skipped: true,
60
60
  };
61
61
  }
62
- await provider.create(repoInfo, settings, options.token);
62
+ await provider.create({ repo: repoInfo, settings, token: options.token });
63
63
  await this.waitForRepoReady(provider, repoInfo, options.token);
64
64
  return {
65
65
  repoInfo,
@@ -80,7 +80,12 @@ export class RepoLifecycleManager {
80
80
  const upstreamInfo = parseGitUrl(repoConfig.upstream, {
81
81
  githubHosts: options.githubHosts,
82
82
  });
83
- await provider.fork(upstreamInfo, repoInfo, settings, options.token);
83
+ await provider.fork({
84
+ upstream: upstreamInfo,
85
+ target: repoInfo,
86
+ settings,
87
+ token: options.token,
88
+ });
84
89
  await this.waitForRepoReady(provider, repoInfo, options.token);
85
90
  return {
86
91
  repoInfo,
@@ -106,7 +111,12 @@ export class RepoLifecycleManager {
106
111
  await source.cloneForMigration(sourceInfo, sourceDir);
107
112
  // Create target and push content
108
113
  const provider = this.factory.getProvider(repoInfo.type);
109
- await provider.receiveMigration(repoInfo, sourceDir, settings, options.token);
114
+ await provider.receiveMigration({
115
+ repo: repoInfo,
116
+ sourceDir,
117
+ settings,
118
+ token: options.token,
119
+ });
110
120
  await this.waitForRepoReady(provider, repoInfo, options.token);
111
121
  return {
112
122
  repoInfo,
@@ -125,7 +135,7 @@ export class RepoLifecycleManager {
125
135
  async waitForRepoReady(provider, repoInfo, token, timeoutMs = 15000, pollMs = 1000) {
126
136
  const start = Date.now();
127
137
  while (Date.now() - start < timeoutMs) {
128
- if (await provider.exists(repoInfo, token)) {
138
+ if (await provider.exists({ repo: repoInfo, token })) {
129
139
  return;
130
140
  }
131
141
  await new Promise((resolve) => setTimeout(resolve, pollMs));
@@ -1,11 +1,12 @@
1
- import type { RepoInfo } from "../shared/repo-detector.js";
2
- import type { RepoConfig } from "../config/index.js";
3
- export type LifecyclePlatform = "github" | "azure-devops" | "gitlab";
1
+ import type { RepoInfo, RepoPlatform } from "../repo/index.js";
2
+ import type { RepoConfig, RepoVisibility } from "../config/index.js";
3
+ export type LifecyclePlatform = RepoPlatform;
4
4
  export interface LifecycleResult {
5
5
  repoInfo: RepoInfo;
6
6
  action: "existed" | "created" | "forked" | "migrated";
7
7
  skipped?: boolean;
8
8
  }
9
+ export type LifecycleActionKind = LifecycleResult["action"];
9
10
  export interface LifecycleOptions {
10
11
  dryRun: boolean;
11
12
  workDir: string;
@@ -18,12 +19,33 @@ export interface LifecycleOptions {
18
19
  * Subset of GitHubRepoSettings that makes sense for creation.
19
20
  */
20
21
  export interface CreateRepoSettings {
21
- visibility?: "public" | "private" | "internal";
22
+ visibility?: RepoVisibility;
22
23
  description?: string;
23
24
  hasIssues?: boolean;
24
25
  hasWiki?: boolean;
25
26
  defaultBranch?: string;
26
27
  }
28
+ export interface LifecycleExistsParams {
29
+ repo: RepoInfo;
30
+ token?: string;
31
+ }
32
+ export interface LifecycleCreateParams {
33
+ repo: RepoInfo;
34
+ settings?: CreateRepoSettings;
35
+ token?: string;
36
+ }
37
+ export interface LifecycleForkParams {
38
+ upstream: RepoInfo;
39
+ target: RepoInfo;
40
+ settings?: CreateRepoSettings;
41
+ token?: string;
42
+ }
43
+ export interface LifecycleReceiveMigrationParams {
44
+ repo: RepoInfo;
45
+ sourceDir: string;
46
+ settings?: CreateRepoSettings;
47
+ token?: string;
48
+ }
27
49
  /**
28
50
  * Provider for platform-specific lifecycle operations.
29
51
  * Implementations handle create/fork/receive for a specific platform.
@@ -34,20 +56,20 @@ export interface IRepoLifecycleProvider {
34
56
  * Check if a repository exists on this platform.
35
57
  * @throws LifecycleError on network/auth failures (NOT for "repo not found")
36
58
  */
37
- exists(repoInfo: RepoInfo, token?: string): Promise<boolean>;
59
+ exists(params: LifecycleExistsParams): Promise<boolean>;
38
60
  /**
39
61
  * Create an empty repository.
40
62
  */
41
- create(repoInfo: RepoInfo, settings?: CreateRepoSettings, token?: string): Promise<void>;
63
+ create(params: LifecycleCreateParams): Promise<void>;
42
64
  /**
43
65
  * Fork from an upstream repository.
44
66
  * Optional - not all platforms support forking.
45
67
  */
46
- fork?(upstream: RepoInfo, target: RepoInfo, settings?: CreateRepoSettings, token?: string): Promise<void>;
68
+ fork?(params: LifecycleForkParams): Promise<void>;
47
69
  /**
48
70
  * Receive migrated content (repo already created, push content).
49
71
  */
50
- receiveMigration(repoInfo: RepoInfo, sourceDir: string, settings?: CreateRepoSettings, token?: string): Promise<void>;
72
+ receiveMigration(params: LifecycleReceiveMigrationParams): Promise<void>;
51
73
  }
52
74
  /**
53
75
  * Source for migration operations.
@@ -1,3 +1,5 @@
1
+ import type { LifecycleActionKind } from "../lifecycle/types.js";
2
+ import type { RepoVisibility } from "../config/index.js";
1
3
  export interface LifecycleReport {
2
4
  actions: LifecycleAction[];
3
5
  totals: {
@@ -9,11 +11,11 @@ export interface LifecycleReport {
9
11
  }
10
12
  export interface LifecycleAction {
11
13
  repoName: string;
12
- action: "existed" | "created" | "forked" | "migrated";
14
+ action: LifecycleActionKind;
13
15
  upstream?: string;
14
16
  source?: string;
15
17
  settings?: {
16
- visibility?: string;
18
+ visibility?: RepoVisibility;
17
19
  description?: string;
18
20
  };
19
21
  }
@@ -1,4 +1,4 @@
1
- import type { PropertyDiff } from "../settings/index.js";
1
+ import type { PropertyDiff, ActiveAction } from "../settings/index.js";
2
2
  import type { Ruleset, Label } from "../config/index.js";
3
3
  export interface SettingsReport {
4
4
  repos: RepoChanges[];
@@ -28,19 +28,19 @@ export interface RepoChanges {
28
28
  }
29
29
  export interface SettingChange {
30
30
  name: string;
31
- action: "create" | "update";
31
+ action: Exclude<ActiveAction, "delete">;
32
32
  oldValue?: unknown;
33
33
  newValue: unknown;
34
34
  }
35
35
  export interface RulesetChange {
36
36
  name: string;
37
- action: "create" | "update" | "delete";
37
+ action: ActiveAction;
38
38
  propertyDiffs?: PropertyDiff[];
39
39
  config?: Ruleset;
40
40
  }
41
41
  export interface LabelChange {
42
42
  name: string;
43
- action: "create" | "update" | "delete";
43
+ action: ActiveAction;
44
44
  newName?: string;
45
45
  propertyChanges?: {
46
46
  property: string;