specweave 0.24.6 → 0.24.9

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 (76) hide show
  1. package/CLAUDE.md +64 -0
  2. package/README.md +34 -0
  3. package/dist/src/cli/commands/init.d.ts.map +1 -1
  4. package/dist/src/cli/commands/init.js +3 -1
  5. package/dist/src/cli/commands/init.js.map +1 -1
  6. package/dist/src/cli/helpers/issue-tracker/github-multi-repo.d.ts +5 -2
  7. package/dist/src/cli/helpers/issue-tracker/github-multi-repo.d.ts.map +1 -1
  8. package/dist/src/cli/helpers/issue-tracker/github-multi-repo.js +90 -8
  9. package/dist/src/cli/helpers/issue-tracker/github-multi-repo.js.map +1 -1
  10. package/dist/src/cli/helpers/issue-tracker/github.d.ts +2 -1
  11. package/dist/src/cli/helpers/issue-tracker/github.d.ts.map +1 -1
  12. package/dist/src/cli/helpers/issue-tracker/github.js +4 -3
  13. package/dist/src/cli/helpers/issue-tracker/github.js.map +1 -1
  14. package/dist/src/cli/helpers/issue-tracker/index.d.ts.map +1 -1
  15. package/dist/src/cli/helpers/issue-tracker/index.js +26 -9
  16. package/dist/src/cli/helpers/issue-tracker/index.js.map +1 -1
  17. package/dist/src/cli/helpers/issue-tracker/types.d.ts +1 -0
  18. package/dist/src/cli/helpers/issue-tracker/types.d.ts.map +1 -1
  19. package/dist/src/cli/helpers/issue-tracker/types.js.map +1 -1
  20. package/dist/src/core/repo-structure/git-error-handler.d.ts +37 -0
  21. package/dist/src/core/repo-structure/git-error-handler.d.ts.map +1 -0
  22. package/dist/src/core/repo-structure/git-error-handler.js +214 -0
  23. package/dist/src/core/repo-structure/git-error-handler.js.map +1 -0
  24. package/dist/src/core/repo-structure/git-provider.d.ts +183 -0
  25. package/dist/src/core/repo-structure/git-provider.d.ts.map +1 -0
  26. package/dist/src/core/repo-structure/git-provider.js +57 -0
  27. package/dist/src/core/repo-structure/git-provider.js.map +1 -0
  28. package/dist/src/core/repo-structure/github-validator.d.ts +1 -0
  29. package/dist/src/core/repo-structure/github-validator.d.ts.map +1 -1
  30. package/dist/src/core/repo-structure/github-validator.js +35 -9
  31. package/dist/src/core/repo-structure/github-validator.js.map +1 -1
  32. package/dist/src/core/repo-structure/platform-registry.d.ts +114 -0
  33. package/dist/src/core/repo-structure/platform-registry.d.ts.map +1 -0
  34. package/dist/src/core/repo-structure/platform-registry.js +206 -0
  35. package/dist/src/core/repo-structure/platform-registry.js.map +1 -0
  36. package/dist/src/core/repo-structure/prompt-consolidator.d.ts +42 -0
  37. package/dist/src/core/repo-structure/prompt-consolidator.d.ts.map +1 -1
  38. package/dist/src/core/repo-structure/prompt-consolidator.js +102 -0
  39. package/dist/src/core/repo-structure/prompt-consolidator.js.map +1 -1
  40. package/dist/src/core/repo-structure/providers/azure-devops-provider.d.ts +64 -0
  41. package/dist/src/core/repo-structure/providers/azure-devops-provider.d.ts.map +1 -0
  42. package/dist/src/core/repo-structure/providers/azure-devops-provider.js +263 -0
  43. package/dist/src/core/repo-structure/providers/azure-devops-provider.js.map +1 -0
  44. package/dist/src/core/repo-structure/providers/bitbucket-provider.d.ts +55 -0
  45. package/dist/src/core/repo-structure/providers/bitbucket-provider.d.ts.map +1 -0
  46. package/dist/src/core/repo-structure/providers/bitbucket-provider.js +238 -0
  47. package/dist/src/core/repo-structure/providers/bitbucket-provider.js.map +1 -0
  48. package/dist/src/core/repo-structure/providers/github-provider.d.ts +53 -0
  49. package/dist/src/core/repo-structure/providers/github-provider.d.ts.map +1 -0
  50. package/dist/src/core/repo-structure/providers/github-provider.js +239 -0
  51. package/dist/src/core/repo-structure/providers/github-provider.js.map +1 -0
  52. package/dist/src/core/repo-structure/providers/gitlab-provider.d.ts +51 -0
  53. package/dist/src/core/repo-structure/providers/gitlab-provider.d.ts.map +1 -0
  54. package/dist/src/core/repo-structure/providers/gitlab-provider.js +251 -0
  55. package/dist/src/core/repo-structure/providers/gitlab-provider.js.map +1 -0
  56. package/dist/src/core/repo-structure/providers/index.d.ts +35 -0
  57. package/dist/src/core/repo-structure/providers/index.d.ts.map +1 -0
  58. package/dist/src/core/repo-structure/providers/index.js +68 -0
  59. package/dist/src/core/repo-structure/providers/index.js.map +1 -0
  60. package/dist/src/core/repo-structure/providers/local-provider.d.ts +61 -0
  61. package/dist/src/core/repo-structure/providers/local-provider.d.ts.map +1 -0
  62. package/dist/src/core/repo-structure/providers/local-provider.js +148 -0
  63. package/dist/src/core/repo-structure/providers/local-provider.js.map +1 -0
  64. package/dist/src/core/repo-structure/repo-structure-manager.d.ts +21 -4
  65. package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
  66. package/dist/src/core/repo-structure/repo-structure-manager.js +380 -113
  67. package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
  68. package/dist/src/core/repo-structure/url-generator.d.ts +80 -0
  69. package/dist/src/core/repo-structure/url-generator.d.ts.map +1 -0
  70. package/dist/src/core/repo-structure/url-generator.js +110 -0
  71. package/dist/src/core/repo-structure/url-generator.js.map +1 -0
  72. package/package.json +1 -1
  73. package/plugins/specweave/hooks/post-task-completion.sh +69 -175
  74. package/plugins/specweave/lib/hooks/consolidated-sync.js +183 -0
  75. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +64 -0
  76. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +96 -0
@@ -23,23 +23,29 @@ import { execSync } from 'child_process';
23
23
  import { execFileNoThrowSync } from '../../utils/execFileNoThrow.js';
24
24
  import { generateRepoIdSmart, ensureUniqueId, validateRepoId, suggestRepoName } from './repo-id-generator.js';
25
25
  import { SetupStateManager } from './setup-state-manager.js';
26
- import { validateRepository, validateOwner } from './github-validator.js';
27
26
  import { generateEnvFile } from '../../utils/env-file-generator.js';
28
27
  import { generateSetupSummary } from './setup-summary.js';
29
- import { getArchitecturePrompt, getParentRepoBenefits, getVisibilityPrompt } from './prompt-consolidator.js';
28
+ import { getArchitecturePrompt, getParentRepoBenefits, getVisibilityPrompt, getUrlTypePrompt } from './prompt-consolidator.js';
30
29
  import { detectRepositoryHints } from './folder-detector.js';
31
30
  import { discoverRepositories } from './repo-bulk-discovery.js';
32
31
  import { Octokit } from '@octokit/rest';
32
+ import { initializeProviders } from './providers/index.js';
33
+ import { getPlatformRegistry } from './platform-registry.js';
34
+ import { getPlatformSelectionPrompt } from './prompt-consolidator.js';
33
35
  export class RepoStructureManager {
34
36
  constructor(projectPath, githubToken) {
35
37
  this.projectPath = projectPath;
36
38
  this.githubToken = githubToken;
37
39
  this.stateManager = new SetupStateManager(projectPath);
40
+ // Initialize Git providers on instantiation
41
+ initializeProviders();
38
42
  }
39
43
  /**
40
44
  * Prompt user for repository structure decisions
45
+ *
46
+ * @param preSelectedArchitecture - Optional pre-selected architecture to skip duplicate prompts
41
47
  */
42
- async promptStructure() {
48
+ async promptStructure(preSelectedArchitecture) {
43
49
  console.log(chalk.cyan.bold('\n🏗️ Repository Architecture Setup\n'));
44
50
  console.log(chalk.gray('Let\'s configure your repository structure for optimal organization.\n'));
45
51
  // Check for resumed setup
@@ -62,27 +68,76 @@ export class RepoStructureManager {
62
68
  await this.stateManager.deleteState();
63
69
  }
64
70
  }
65
- // Step 1: Ask about architecture type using consolidator
66
- const promptData = getArchitecturePrompt();
67
- const { architecture } = await inquirer.prompt([{
71
+ // Step 1: Ask about architecture type using consolidator (SKIP if pre-selected)
72
+ let architecture;
73
+ if (preSelectedArchitecture) {
74
+ // Architecture already selected - skip duplicate prompt
75
+ architecture = preSelectedArchitecture;
76
+ console.log(chalk.green(`✓ Architecture: ${this.formatArchitectureForDisplay(architecture)}\n`));
77
+ }
78
+ else {
79
+ // Ask user for architecture choice
80
+ const promptData = getArchitecturePrompt();
81
+ const result = await inquirer.prompt([{
82
+ type: 'list',
83
+ name: 'architecture',
84
+ message: promptData.question,
85
+ choices: promptData.options.map(opt => ({
86
+ name: `${opt.label}\n${chalk.gray(opt.description)}\n${chalk.dim(opt.example)}`,
87
+ value: opt.value,
88
+ short: opt.label
89
+ })),
90
+ default: 'single'
91
+ }]);
92
+ architecture = result.architecture;
93
+ }
94
+ // Step 2: Ask about Git hosting platform
95
+ const registry = getPlatformRegistry();
96
+ const platformOptions = registry.getPlatformOptions(true); // Include unsupported platforms
97
+ const platformPromptData = getPlatformSelectionPrompt();
98
+ console.log(chalk.cyan('\n' + platformPromptData.message));
99
+ const { platform } = await inquirer.prompt([{
100
+ type: 'list',
101
+ name: 'platform',
102
+ message: platformPromptData.question,
103
+ choices: platformOptions.map(opt => ({
104
+ name: opt.disabled
105
+ ? `${opt.name}\n${chalk.gray(opt.description)}\n${chalk.yellow('⚠️ ' + opt.disabled)}`
106
+ : `${opt.name}\n${chalk.gray(opt.description)}`,
107
+ value: opt.value,
108
+ short: opt.name,
109
+ disabled: opt.disabled ? opt.disabled : false
110
+ })),
111
+ default: 'github'
112
+ }]);
113
+ // Get provider instance
114
+ const provider = registry.getProvider(platform);
115
+ if (!provider) {
116
+ throw new Error(`Platform ${platform} is not available. This should not happen!`);
117
+ }
118
+ console.log(chalk.green(`\n✓ Using ${provider.config.name} as Git hosting platform\n`));
119
+ // Step 3: Ask about Git remote URL format (SSH vs HTTPS)
120
+ const urlTypePromptData = getUrlTypePrompt();
121
+ const { urlType } = await inquirer.prompt([{
68
122
  type: 'list',
69
- name: 'architecture',
70
- message: promptData.question,
71
- choices: promptData.options.map(opt => ({
72
- name: `${opt.label}\n${chalk.gray(opt.description)}\n${chalk.dim(opt.example)}`,
123
+ name: 'urlType',
124
+ message: urlTypePromptData.question,
125
+ choices: urlTypePromptData.options.map(opt => ({
126
+ name: `${opt.label}\n${chalk.gray(opt.description)}`,
73
127
  value: opt.value,
74
128
  short: opt.label
75
129
  })),
76
- default: 'single'
130
+ default: urlTypePromptData.default
77
131
  }]);
132
+ console.log(chalk.green(`\n✓ Using ${urlType.toUpperCase()} remote URLs\n`));
78
133
  // Map ArchitectureChoice to internal architecture
79
134
  const mappedArch = this.mapArchitectureChoice(architecture);
80
135
  switch (mappedArch) {
81
136
  case 'single':
82
- return this.configureSingleRepo();
137
+ return this.configureSingleRepo(urlType, platform, provider);
83
138
  case 'parent':
84
139
  // GitHub parent repo (pushed to GitHub)
85
- return this.configureMultiRepo(true, false);
140
+ return this.configureMultiRepo(true, false, urlType, platform, provider);
86
141
  default:
87
142
  throw new Error(`Unknown architecture: ${architecture}`);
88
143
  }
@@ -100,13 +155,38 @@ export class RepoStructureManager {
100
155
  return 'single';
101
156
  }
102
157
  }
158
+ /**
159
+ * Format architecture choice for display
160
+ *
161
+ * @param choice - Architecture choice
162
+ * @returns Human-readable format
163
+ */
164
+ formatArchitectureForDisplay(choice) {
165
+ switch (choice) {
166
+ case 'single':
167
+ return 'Single repository';
168
+ case 'github-parent':
169
+ return 'Parent repo + nested repos (GitHub)';
170
+ default:
171
+ return choice;
172
+ }
173
+ }
103
174
  /**
104
175
  * Resume setup from saved state
105
176
  */
106
177
  async resumeSetup(state) {
178
+ // Default to GitHub platform for resumed setups (backward compatibility)
179
+ const registry = getPlatformRegistry();
180
+ const provider = registry.getProvider('github');
181
+ if (!provider) {
182
+ throw new Error('GitHub provider not available. This should not happen!');
183
+ }
107
184
  // Convert saved state back to config format
108
185
  const config = {
109
186
  architecture: state.architecture,
187
+ urlType: 'ssh', // Default to SSH for resumed setups
188
+ platform: 'github', // Default to GitHub for backward compatibility
189
+ provider: provider,
110
190
  parentRepo: state.parentRepo,
111
191
  repositories: state.repos.map(r => ({
112
192
  id: r.id,
@@ -126,7 +206,7 @@ export class RepoStructureManager {
126
206
  /**
127
207
  * Configure single repository
128
208
  */
129
- async configureSingleRepo() {
209
+ async configureSingleRepo(urlType = 'ssh', platform = 'github', provider) {
130
210
  console.log(chalk.cyan('\n📦 Single Repository Configuration\n'));
131
211
  // Check if repo already exists
132
212
  const hasGit = existsSync(path.join(this.projectPath, '.git'));
@@ -172,6 +252,9 @@ export class RepoStructureManager {
172
252
  }
173
253
  return {
174
254
  architecture: 'single',
255
+ urlType,
256
+ platform,
257
+ provider,
175
258
  repositories: [{
176
259
  id: 'main',
177
260
  name: repo,
@@ -237,6 +320,9 @@ export class RepoStructureManager {
237
320
  }
238
321
  return {
239
322
  architecture: 'single',
323
+ urlType,
324
+ platform,
325
+ provider,
240
326
  repositories: [{
241
327
  id: 'main',
242
328
  name: answers.repo,
@@ -253,12 +339,15 @@ export class RepoStructureManager {
253
339
  * Configure multi-repository architecture
254
340
  * @param useParent - Whether to use parent repository/folder
255
341
  * @param isLocalParent - If true, parent folder is local only (NOT pushed to GitHub)
342
+ * @param urlType - Git remote URL format (ssh or https)
343
+ * @param platform - Git hosting platform type
344
+ * @param provider - Git provider instance for API operations
256
345
  *
257
346
  * NOTE: This is primarily user-facing output (console.log/console.error).
258
347
  * All console.* calls in this method are legitimate user-facing exceptions
259
348
  * as defined in CONTRIBUTING.md (colored messages, confirmations, formatted output).
260
349
  */
261
- async configureMultiRepo(useParent = true, isLocalParent = false) {
350
+ async configureMultiRepo(useParent = true, isLocalParent = false, urlType = 'ssh', platform = 'github', provider) {
262
351
  console.log(chalk.cyan('\n🎯 Multi-Repository Configuration\n'));
263
352
  console.log(chalk.gray('This creates separate repositories for each service/component.\n'));
264
353
  // Show parent repo benefits if using parent approach
@@ -268,6 +357,9 @@ export class RepoStructureManager {
268
357
  }
269
358
  const config = {
270
359
  architecture: useParent ? 'parent' : 'multi-repo',
360
+ urlType,
361
+ platform,
362
+ provider,
271
363
  repositories: []
272
364
  };
273
365
  // Save state: architecture selected
@@ -280,8 +372,155 @@ export class RepoStructureManager {
280
372
  timestamp: new Date().toISOString(),
281
373
  envCreated: false
282
374
  });
283
- // Configure parent repository if using that approach
284
- if (useParent) {
375
+ // ========== NEW FLOW: Ask discovery strategy FIRST (before parent questions!) ==========
376
+ // Step 1: Ask how to configure implementation repositories (BEFORE parent questions)
377
+ let discoveryStrategy = 'manual';
378
+ let discoveredRepos = [];
379
+ let owner = '';
380
+ if (!isLocalParent && this.githubToken && useParent) {
381
+ console.log(chalk.cyan('\n🚀 Repository Configuration\n'));
382
+ console.log(chalk.gray('You can discover repositories automatically or enter them manually.\n'));
383
+ const { configMethod } = await inquirer.prompt([
384
+ {
385
+ type: 'list',
386
+ name: 'configMethod',
387
+ message: 'How do you want to configure these repositories?',
388
+ choices: [
389
+ {
390
+ name: `${chalk.bold('Manual entry')} - Enter parent and each repository one by one ${chalk.gray('(full control)')}`,
391
+ value: 'manual'
392
+ },
393
+ {
394
+ name: `${chalk.bold('Pattern matching')} - Discover repositories, then select parent ${chalk.gray('(faster for many repos)')}`,
395
+ value: 'pattern-first'
396
+ }
397
+ ],
398
+ default: 'manual'
399
+ }
400
+ ]);
401
+ discoveryStrategy = configMethod;
402
+ }
403
+ // Step 2: If pattern/all, discover repos FIRST, then ask which is parent
404
+ if (discoveryStrategy === 'pattern-first') {
405
+ // Get owner FIRST (needed for discovery)
406
+ console.log(chalk.cyan('\n👤 Repository Owner\n'));
407
+ const ownerPrompt = await inquirer.prompt([
408
+ {
409
+ type: 'input',
410
+ name: 'owner',
411
+ message: `${provider.config.name} owner/organization:`,
412
+ validate: async (input) => {
413
+ if (!input.trim())
414
+ return 'Owner is required';
415
+ // Validate owner exists on the platform
416
+ if (this.githubToken) {
417
+ const result = await provider.validateOwner(input, this.githubToken);
418
+ if (!result.valid) {
419
+ return result.error || `Invalid ${provider.config.name} owner`;
420
+ }
421
+ }
422
+ return true;
423
+ }
424
+ }
425
+ ]);
426
+ owner = ownerPrompt.owner;
427
+ // Discover repositories via pattern matching
428
+ const octokit = new Octokit({ auth: this.githubToken });
429
+ const isOrg = await provider.isOrganization(owner, this.githubToken);
430
+ // Retry loop for pattern adjustment
431
+ let discoveryResult = null;
432
+ while (discoveryResult === null) {
433
+ discoveryResult = await discoverRepositories(octokit, owner, isOrg, 0); // Pass 0, we'll count later
434
+ // If null, user selected "go back and adjust pattern", loop will retry
435
+ // If user selected "manual", discoveryResult will be { repositories: [], strategy: 'manual' }
436
+ }
437
+ if (discoveryResult && discoveryResult.strategy !== 'manual') {
438
+ discoveredRepos = discoveryResult.repositories;
439
+ // Now ask: Which repo is the parent?
440
+ console.log(chalk.cyan('\n🏠 Select Parent Repository\n'));
441
+ console.log(chalk.gray('Choose which repository will be the parent (contains .specweave/ structure)\n'));
442
+ const parentChoices = [
443
+ ...discoveredRepos.map((repo, index) => ({
444
+ name: `${chalk.bold(repo.name)}\n${chalk.gray(repo.description || '(no description)')}`,
445
+ value: index.toString(),
446
+ short: repo.name
447
+ })),
448
+ {
449
+ name: `${chalk.yellow('✏️ Enter parent manually')} ${chalk.gray('(not in discovered list)')}`,
450
+ value: 'manual',
451
+ short: 'Enter manually'
452
+ }
453
+ ];
454
+ const { parentSelection } = await inquirer.prompt([
455
+ {
456
+ type: 'list',
457
+ name: 'parentSelection',
458
+ message: 'Which repository is the parent?',
459
+ choices: parentChoices,
460
+ pageSize: 15
461
+ }
462
+ ]);
463
+ if (parentSelection === 'manual') {
464
+ // User wants to enter parent manually - fall back to old flow
465
+ discoveryStrategy = 'manual';
466
+ discoveredRepos = []; // Clear discovered repos
467
+ }
468
+ else {
469
+ // User selected a parent from discovered list
470
+ const parentIndex = parseInt(parentSelection);
471
+ const selectedParent = discoveredRepos[parentIndex];
472
+ // Fetch full repo details from GitHub API
473
+ let description = selectedParent.description || 'SpecWeave parent repository - specs, docs, and architecture';
474
+ let existingVisibility = selectedParent.private ? 'private' : 'public';
475
+ try {
476
+ const response = await fetch(`https://api.github.com/repos/${owner}/${selectedParent.name}`, {
477
+ headers: {
478
+ 'Authorization': `Bearer ${this.githubToken}`,
479
+ 'Accept': 'application/vnd.github+json'
480
+ }
481
+ });
482
+ if (response.ok) {
483
+ const data = await response.json();
484
+ description = data.description || description;
485
+ existingVisibility = data.private ? 'private' : 'public';
486
+ }
487
+ }
488
+ catch {
489
+ // Use defaults if fetch fails
490
+ }
491
+ // Set parent config
492
+ config.parentRepo = {
493
+ name: selectedParent.name,
494
+ owner: owner,
495
+ description: description,
496
+ visibility: existingVisibility,
497
+ createOnGitHub: false // Already exists!
498
+ };
499
+ // Remove parent from discovered repos (implementation repos = discovered - parent)
500
+ discoveredRepos.splice(parentIndex, 1);
501
+ console.log(chalk.green(`\n✓ Using existing repository: ${owner}/${selectedParent.name}\n`));
502
+ console.log(chalk.gray(`✓ Implementation repositories: ${discoveredRepos.length}\n`));
503
+ // Save state: parent repo configured
504
+ await this.saveSetupState({
505
+ version: '1.0.0',
506
+ architecture: useParent ? 'parent' : 'multi-repo',
507
+ parentRepo: config.parentRepo,
508
+ repos: [],
509
+ currentStep: 'parent-repo-configured',
510
+ timestamp: new Date().toISOString(),
511
+ envCreated: false
512
+ });
513
+ // Skip to repository configuration (lines 794+)
514
+ // We'll continue below with discoveredRepos
515
+ }
516
+ }
517
+ else {
518
+ // User selected manual - fall back to old flow
519
+ discoveryStrategy = 'manual';
520
+ }
521
+ }
522
+ // Step 3: Manual flow (existing logic) - only runs if discoveryStrategy === 'manual'
523
+ if (discoveryStrategy === 'manual' && useParent) {
285
524
  let parentAnswers;
286
525
  if (isLocalParent) {
287
526
  // Local parent: Skip GitHub questions, just ask for folder name
@@ -302,15 +541,15 @@ export class RepoStructureManager {
302
541
  {
303
542
  type: 'input',
304
543
  name: 'owner',
305
- message: 'GitHub owner/organization for IMPLEMENTATION repos:',
544
+ message: `${provider.config.name} owner/organization for IMPLEMENTATION repos:`,
306
545
  validate: async (input) => {
307
546
  if (!input.trim())
308
547
  return 'Owner is required';
309
- // Validate owner exists on GitHub
548
+ // Validate owner exists on the platform
310
549
  if (this.githubToken) {
311
- const result = await validateOwner(input, this.githubToken);
550
+ const result = await provider.validateOwner(input, this.githubToken);
312
551
  if (!result.valid) {
313
- return result.error || 'Invalid GitHub owner';
552
+ return result.error || `Invalid ${provider.config.name} owner`;
314
553
  }
315
554
  }
316
555
  return true;
@@ -351,15 +590,15 @@ export class RepoStructureManager {
351
590
  {
352
591
  type: 'input',
353
592
  name: 'owner',
354
- message: 'GitHub owner/organization:',
593
+ message: `${provider.config.name} owner/organization:`,
355
594
  validate: async (input) => {
356
595
  if (!input.trim())
357
596
  return 'Owner is required';
358
- // Validate owner exists on GitHub
597
+ // Validate owner exists on the platform
359
598
  if (this.githubToken) {
360
- const result = await validateOwner(input, this.githubToken);
599
+ const result = await provider.validateOwner(input, this.githubToken);
361
600
  if (!result.valid) {
362
- return result.error || 'Invalid GitHub owner';
601
+ return result.error || `Invalid ${provider.config.name} owner`;
363
602
  }
364
603
  }
365
604
  return true;
@@ -376,11 +615,11 @@ export class RepoStructureManager {
376
615
  validate: async (input) => {
377
616
  if (!input.trim())
378
617
  return 'Repository name is required';
379
- // Validate repository EXISTS on GitHub
618
+ // Validate repository EXISTS on the platform
380
619
  if (this.githubToken && ownerPrompt.owner) {
381
- const result = await validateRepository(ownerPrompt.owner, input, this.githubToken);
620
+ const result = await provider.validateRepository(ownerPrompt.owner, input, this.githubToken);
382
621
  if (!result.exists) {
383
- return `Repository ${ownerPrompt.owner}/${input} not found on GitHub. Please check the name or choose 'Create new'.`;
622
+ return `Repository ${ownerPrompt.owner}/${input} not found on ${provider.config.name}. Please check the name or choose 'Create new'.`;
384
623
  }
385
624
  }
386
625
  return true;
@@ -425,15 +664,15 @@ export class RepoStructureManager {
425
664
  {
426
665
  type: 'input',
427
666
  name: 'owner',
428
- message: 'GitHub owner/organization for ALL repos:',
667
+ message: `${provider.config.name} owner/organization for ALL repos:`,
429
668
  validate: async (input) => {
430
669
  if (!input.trim())
431
670
  return 'Owner is required';
432
- // Validate owner exists on GitHub
671
+ // Validate owner exists on the platform
433
672
  if (this.githubToken) {
434
- const result = await validateOwner(input, this.githubToken);
673
+ const result = await provider.validateOwner(input, this.githubToken);
435
674
  if (!result.valid) {
436
- return result.error || 'Invalid GitHub owner';
675
+ return result.error || `Invalid ${provider.config.name} owner`;
437
676
  }
438
677
  }
439
678
  return true;
@@ -452,7 +691,7 @@ export class RepoStructureManager {
452
691
  return 'Repository name is required';
453
692
  // Validate repository DOESN'T exist
454
693
  if (this.githubToken && ownerPrompt.owner) {
455
- const result = await validateRepository(ownerPrompt.owner, input, this.githubToken);
694
+ const result = await provider.validateRepository(ownerPrompt.owner, input, this.githubToken);
456
695
  if (result.exists) {
457
696
  return `Repository ${ownerPrompt.owner}/${input} already exists at ${result.url}. Please choose 'Use existing' or pick a different name.`;
458
697
  }
@@ -517,73 +756,88 @@ export class RepoStructureManager {
517
756
  envCreated: false
518
757
  });
519
758
  }
520
- // Auto-detect existing folders
521
- const hints = await detectRepositoryHints(this.projectPath);
522
- if (hints.detectedFolders.length > 0) {
523
- console.log(chalk.green(`\n✓ Detected ${hints.detectedFolders.length} service folder(s):`));
524
- hints.detectedFolders.forEach(f => console.log(chalk.gray(` • ${f}`)));
525
- console.log('');
759
+ // Step 4: Repository count and discovery (skip count question if using pattern-first)
760
+ let repoCount;
761
+ let bulkDiscoveryStrategy = discoveryStrategy === 'pattern-first' ? 'pattern' : 'manual';
762
+ if (discoveryStrategy === 'pattern-first' && discoveredRepos.length > 0) {
763
+ // Pattern discovery: repos already discovered, skip count question
764
+ repoCount = discoveredRepos.length;
765
+ console.log(chalk.green(`\n✓ Total repositories: ${repoCount + 1} (1 parent + ${repoCount} implementation)\n`));
526
766
  }
527
- // Show repository count clarification BEFORE asking
528
- if (useParent && config.parentRepo) {
529
- console.log(chalk.cyan('\n📊 Repository Count\n'));
530
- console.log(chalk.gray('You will create:'));
531
- if (isLocalParent) {
532
- console.log(chalk.white(' • 1 parent FOLDER (local only, .specweave/ gitignored)'));
533
- console.log(chalk.white('N implementation repositories (your services/apps on GitHub)'));
534
- }
535
- else {
536
- console.log(chalk.white(' • 1 parent repository (specs, docs, increments)'));
537
- console.log(chalk.white(' • N implementation repositories (your services/apps)'));
767
+ else {
768
+ // Manual entry: ask for count
769
+ // Auto-detect existing folders
770
+ const hints = await detectRepositoryHints(this.projectPath);
771
+ if (hints.detectedFolders.length > 0) {
772
+ console.log(chalk.green(`\n✓ Detected ${hints.detectedFolders.length} service folder(s):`));
773
+ hints.detectedFolders.forEach(f => console.log(chalk.gray(`${f}`)));
774
+ console.log('');
538
775
  }
539
- console.log(chalk.gray('\nNext question asks for: IMPLEMENTATION repositories ONLY (not counting parent)\n'));
540
- }
541
- // Ask how many implementation repositories
542
- const { repoCount } = await inquirer.prompt([{
543
- type: 'number',
544
- name: 'repoCount',
545
- message: useParent
546
- ? '📦 How many IMPLEMENTATION repositories? (not counting parent)'
547
- : 'How many repositories?',
548
- default: hints.suggestedCount, // Use auto-detected count
549
- validate: (input) => {
550
- if (input < 1)
551
- return useParent
552
- ? 'Need at least 1 implementation repository'
553
- : 'Need at least 2 repositories';
554
- if (input > 10)
555
- return 'Maximum 10 repositories supported';
556
- return true;
776
+ // Show repository count clarification BEFORE asking
777
+ if (useParent && config.parentRepo) {
778
+ console.log(chalk.cyan('\n📊 Repository Count\n'));
779
+ console.log(chalk.gray('You will create:'));
780
+ if (isLocalParent) {
781
+ console.log(chalk.white(' • 1 parent FOLDER (local only, .specweave/ gitignored)'));
782
+ console.log(chalk.white(' • N implementation repositories (your services/apps on GitHub)'));
557
783
  }
558
- }]);
559
- // Show summary AFTER for confirmation
560
- if (useParent && config.parentRepo) {
561
- if (isLocalParent) {
562
- console.log(chalk.green(`\n✓ Total repositories to create: ${repoCount} implementation repos`));
563
- console.log(chalk.gray(` (Parent folder is local only, not counted)\n`));
784
+ else {
785
+ console.log(chalk.white(' • 1 parent repository (specs, docs, increments)'));
786
+ console.log(chalk.white(' • N implementation repositories (your services/apps)'));
787
+ }
788
+ console.log(chalk.gray('\nNext question asks for: IMPLEMENTATION repositories ONLY (not counting parent)\n'));
564
789
  }
565
- else {
566
- const totalRepos = 1 + repoCount;
567
- console.log(chalk.green(`\n✓ Total repositories to create: ${totalRepos} (1 parent + ${repoCount} implementation)\n`));
790
+ // Ask how many implementation repositories
791
+ const promptResult = await inquirer.prompt([{
792
+ type: 'number',
793
+ name: 'repoCount',
794
+ message: useParent
795
+ ? '📦 How many IMPLEMENTATION repositories? (not counting parent)'
796
+ : 'How many repositories?',
797
+ default: hints.suggestedCount, // Use auto-detected count
798
+ validate: (input) => {
799
+ if (input < 1)
800
+ return useParent
801
+ ? 'Need at least 1 implementation repository'
802
+ : 'Need at least 2 repositories';
803
+ if (input > 10)
804
+ return 'Maximum 10 repositories supported';
805
+ return true;
806
+ }
807
+ }]);
808
+ repoCount = promptResult.repoCount;
809
+ // Show summary AFTER for confirmation
810
+ if (useParent && config.parentRepo) {
811
+ if (isLocalParent) {
812
+ console.log(chalk.green(`\n✓ Total repositories to create: ${repoCount} implementation repos`));
813
+ console.log(chalk.gray(` (Parent folder is local only, not counted)\n`));
814
+ }
815
+ else {
816
+ const totalRepos = 1 + repoCount;
817
+ console.log(chalk.green(`\n✓ Total repositories to create: ${totalRepos} (1 parent + ${repoCount} implementation)\n`));
818
+ }
568
819
  }
569
- }
570
- // Bulk repository discovery (optimization for many repos)
571
- let discoveredRepos = [];
572
- let bulkDiscoveryStrategy = 'manual';
573
- if (this.githubToken && config.parentRepo) {
574
- const octokit = new Octokit({ auth: this.githubToken });
575
- const owner = config.parentRepo.owner;
576
- const isOrg = await this.isGitHubOrganization(owner);
577
- const discoveryResult = await discoverRepositories(octokit, owner, isOrg, repoCount);
578
- if (discoveryResult) {
579
- bulkDiscoveryStrategy = discoveryResult.strategy;
580
- if (discoveryResult.strategy !== 'manual') {
581
- discoveredRepos = discoveryResult.repositories;
582
- // Update repoCount to match discovered repos
583
- if (discoveredRepos.length !== repoCount) {
584
- console.log(chalk.yellow(`\n📝 Adjusting repository count from ${repoCount} to ${discoveredRepos.length} (based on discovery)\n`));
585
- // Override repoCount with discovered count
586
- repoCount = discoveredRepos.length;
820
+ // Bulk repository discovery for manual flow (old behavior)
821
+ if (this.githubToken && config.parentRepo && discoveryStrategy === 'manual') {
822
+ const octokit = new Octokit({ auth: this.githubToken });
823
+ const owner = config.parentRepo.owner;
824
+ const isOrg = await provider.isOrganization(owner, this.githubToken);
825
+ // Retry loop for pattern adjustment
826
+ let discoveryResult = null;
827
+ while (discoveryResult === null) {
828
+ discoveryResult = await discoverRepositories(octokit, owner, isOrg, repoCount);
829
+ // If null, user selected "go back and adjust pattern", loop will retry
830
+ // If user selected "manual", discoveryResult will be { repositories: [], strategy: 'manual' }
831
+ }
832
+ if (discoveryResult) {
833
+ bulkDiscoveryStrategy = discoveryResult.strategy;
834
+ if (discoveryResult.strategy !== 'manual') {
835
+ discoveredRepos = discoveryResult.repositories;
836
+ // Update repoCount to match discovered repos
837
+ if (discoveredRepos.length !== repoCount) {
838
+ console.log(chalk.yellow(`\n📝 Adjusting repository count from ${repoCount} to ${discoveredRepos.length} (based on discovery)\n`));
839
+ repoCount = discoveredRepos.length;
840
+ }
587
841
  }
588
842
  }
589
843
  }
@@ -646,7 +900,7 @@ export class RepoStructureManager {
646
900
  return 'Repository name is required';
647
901
  // Validate repository doesn't exist (skip for discovered repos)
648
902
  if (!isDiscovered && this.githubToken && config.parentRepo) {
649
- const result = await validateRepository(config.parentRepo.owner, input, this.githubToken);
903
+ const result = await provider.validateRepository(config.parentRepo.owner, input, this.githubToken);
650
904
  if (result.exists) {
651
905
  return `Repository ${config.parentRepo.owner}/${input} already exists at ${result.url}`;
652
906
  }
@@ -772,7 +1026,7 @@ export class RepoStructureManager {
772
1026
  /**
773
1027
  * Configure monorepo
774
1028
  */
775
- async configureMonorepo() {
1029
+ async configureMonorepo(urlType = 'ssh', platform = 'github', provider) {
776
1030
  console.log(chalk.cyan('\n📚 Monorepo Configuration\n'));
777
1031
  console.log(chalk.gray('Single repository with multiple projects/packages.\n'));
778
1032
  const answers = await inquirer.prompt([
@@ -834,6 +1088,9 @@ export class RepoStructureManager {
834
1088
  const projects = answers.projects.split(',').map((p) => p.trim());
835
1089
  return {
836
1090
  architecture: 'monorepo',
1091
+ urlType,
1092
+ platform,
1093
+ provider,
837
1094
  repositories: [{
838
1095
  id: 'main',
839
1096
  name: answers.repo,
@@ -848,28 +1105,33 @@ export class RepoStructureManager {
848
1105
  };
849
1106
  }
850
1107
  /**
851
- * Create repositories on GitHub via API
1108
+ * Create repositories on Git hosting platform via API
852
1109
  */
853
- async createGitHubRepositories(config) {
1110
+ async createRepositories(config) {
854
1111
  if (!this.githubToken) {
855
- console.log(chalk.yellow('\n⚠️ No GitHub token available'));
856
- console.log(chalk.gray(' Skipping GitHub repository creation'));
1112
+ console.log(chalk.yellow(`\n⚠️ No ${config.provider.config.name} token available`));
1113
+ console.log(chalk.gray(` Skipping ${config.provider.config.name} repository creation`));
857
1114
  console.log(chalk.gray(' You can create repositories manually later\n'));
858
1115
  return;
859
1116
  }
860
- const spinner = ora('Creating GitHub repositories...').start();
1117
+ const spinner = ora(`Creating ${config.provider.config.name} repositories...`).start();
861
1118
  const created = [];
862
1119
  const failed = [];
863
1120
  // Create parent repository if needed
864
1121
  if (config.parentRepo?.createOnGitHub) {
865
1122
  try {
866
- await this.createGitHubRepo(config.parentRepo.owner, config.parentRepo.name, config.parentRepo.description, config.parentRepo.visibility);
1123
+ await config.provider.createRepository({
1124
+ owner: config.parentRepo.owner,
1125
+ name: config.parentRepo.name,
1126
+ description: config.parentRepo.description,
1127
+ visibility: config.parentRepo.visibility
1128
+ }, this.githubToken);
867
1129
  created.push(`${config.parentRepo.owner}/${config.parentRepo.name}`);
868
1130
  // Save state: parent repo created
869
1131
  await this.saveSetupState({
870
1132
  version: '1.0.0',
871
1133
  architecture: config.architecture,
872
- parentRepo: { ...config.parentRepo, url: `https://github.com/${config.parentRepo.owner}/${config.parentRepo.name}` },
1134
+ parentRepo: { ...config.parentRepo, url: config.provider.getRemoteUrl(config.parentRepo.owner, config.parentRepo.name, config.urlType) },
873
1135
  repos: [],
874
1136
  currentStep: 'parent-repo-created',
875
1137
  timestamp: new Date().toISOString(),
@@ -884,7 +1146,12 @@ export class RepoStructureManager {
884
1146
  for (const repo of config.repositories) {
885
1147
  if (repo.createOnGitHub) {
886
1148
  try {
887
- await this.createGitHubRepo(repo.owner, repo.name, repo.description, repo.visibility);
1149
+ await config.provider.createRepository({
1150
+ owner: repo.owner,
1151
+ name: repo.name,
1152
+ description: repo.description,
1153
+ visibility: repo.visibility
1154
+ }, this.githubToken);
888
1155
  created.push(`${repo.owner}/${repo.name}`);
889
1156
  }
890
1157
  catch (error) {
@@ -973,7 +1240,7 @@ export class RepoStructureManager {
973
1240
  path: r.path,
974
1241
  visibility: r.visibility,
975
1242
  displayName: r.name,
976
- url: `https://github.com/${r.owner}/${r.name}`,
1243
+ url: config.provider.getRemoteUrl(r.owner, r.name, config.urlType),
977
1244
  created: false
978
1245
  })),
979
1246
  currentStep: 'complete',
@@ -1066,14 +1333,14 @@ export class RepoStructureManager {
1066
1333
  }
1067
1334
  /**
1068
1335
  * Clone or initialize a repository
1069
- * If the repo exists on GitHub, clone it; otherwise, init + add remote
1336
+ * If the repo exists on the platform, clone it; otherwise, init + add remote
1070
1337
  */
1071
- async cloneOrInitRepository(repoPath, owner, name, createOnGitHub) {
1338
+ async cloneOrInitRepository(repoPath, owner, name, createOnGitHub, urlType = 'ssh', provider) {
1072
1339
  // If .git already exists, skip
1073
1340
  if (existsSync(path.join(repoPath, '.git'))) {
1074
1341
  return;
1075
1342
  }
1076
- const remoteUrl = `https://github.com/${owner}/${name}.git`;
1343
+ const remoteUrl = provider.getRemoteUrl(owner, name, urlType);
1077
1344
  // Check if repository exists on GitHub
1078
1345
  const repoExists = await this.repositoryExistsOnGitHub(owner, name);
1079
1346
  if (repoExists) {
@@ -1131,7 +1398,7 @@ export class RepoStructureManager {
1131
1398
  if (!existsSync(path.join(this.projectPath, '.git'))) {
1132
1399
  execFileNoThrowSync('git', ['init'], { cwd: this.projectPath });
1133
1400
  if (config.parentRepo) {
1134
- const remoteUrl = `https://github.com/${config.parentRepo.owner}/${config.parentRepo.name}.git`;
1401
+ const remoteUrl = config.provider.getRemoteUrl(config.parentRepo.owner, config.parentRepo.name, config.urlType);
1135
1402
  execFileNoThrowSync('git', ['remote', 'add', 'origin', remoteUrl], { cwd: this.projectPath });
1136
1403
  }
1137
1404
  }
@@ -1139,7 +1406,7 @@ export class RepoStructureManager {
1139
1406
  for (const repo of config.repositories) {
1140
1407
  const repoPath = path.join(this.projectPath, repo.path);
1141
1408
  // Clone or initialize repository
1142
- await this.cloneOrInitRepository(repoPath, repo.owner, repo.name, repo.createOnGitHub);
1409
+ await this.cloneOrInitRepository(repoPath, repo.owner, repo.name, repo.createOnGitHub, config.urlType, config.provider);
1143
1410
  // Create basic structure (only if repo was just initialized, not cloned)
1144
1411
  if (!repo.createOnGitHub || !await this.repositoryExistsOnGitHub(repo.owner, repo.name)) {
1145
1412
  this.createBasicRepoStructure(repoPath, repo.id);
@@ -1151,7 +1418,7 @@ export class RepoStructureManager {
1151
1418
  for (const repo of config.repositories) {
1152
1419
  const repoPath = path.join(this.projectPath, repo.path);
1153
1420
  // Clone or initialize repository
1154
- await this.cloneOrInitRepository(repoPath, repo.owner, repo.name, repo.createOnGitHub);
1421
+ await this.cloneOrInitRepository(repoPath, repo.owner, repo.name, repo.createOnGitHub, config.urlType, config.provider);
1155
1422
  // Create basic structure (only if repo was just initialized, not cloned)
1156
1423
  if (!repo.createOnGitHub || !await this.repositoryExistsOnGitHub(repo.owner, repo.name)) {
1157
1424
  this.createBasicRepoStructure(repoPath, repo.id);
@@ -1164,7 +1431,7 @@ export class RepoStructureManager {
1164
1431
  execFileNoThrowSync('git', ['init'], { cwd: this.projectPath });
1165
1432
  const repo = config.repositories[0];
1166
1433
  if (repo) {
1167
- const remoteUrl = `https://github.com/${repo.owner}/${repo.name}.git`;
1434
+ const remoteUrl = config.provider.getRemoteUrl(repo.owner, repo.name, config.urlType);
1168
1435
  execFileNoThrowSync('git', ['remote', 'add', 'origin', remoteUrl], { cwd: this.projectPath });
1169
1436
  }
1170
1437
  }