specweave 0.28.19 → 0.28.20

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 (85) hide show
  1. package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts +16 -0
  2. package/dist/plugins/specweave-ado/lib/ado-spec-sync.d.ts.map +1 -1
  3. package/dist/plugins/specweave-ado/lib/ado-spec-sync.js +63 -3
  4. package/dist/plugins/specweave-ado/lib/ado-spec-sync.js.map +1 -1
  5. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts +12 -3
  6. package/dist/plugins/specweave-ado/lib/ado-status-sync.d.ts.map +1 -1
  7. package/dist/plugins/specweave-ado/lib/ado-status-sync.js +37 -3
  8. package/dist/plugins/specweave-ado/lib/ado-status-sync.js.map +1 -1
  9. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts +8 -6
  10. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.d.ts.map +1 -1
  11. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js +230 -165
  12. package/dist/plugins/specweave-github/lib/github-increment-sync-cli.js.map +1 -1
  13. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts +10 -0
  14. package/dist/plugins/specweave-github/lib/github-status-sync.d.ts.map +1 -1
  15. package/dist/plugins/specweave-github/lib/github-status-sync.js +40 -2
  16. package/dist/plugins/specweave-github/lib/github-status-sync.js.map +1 -1
  17. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts +2 -0
  18. package/dist/plugins/specweave-github/lib/increment-issue-builder.d.ts.map +1 -1
  19. package/dist/plugins/specweave-github/lib/increment-issue-builder.js +25 -5
  20. package/dist/plugins/specweave-github/lib/increment-issue-builder.js.map +1 -1
  21. package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts +12 -0
  22. package/dist/plugins/specweave-jira/lib/jira-spec-sync.d.ts.map +1 -1
  23. package/dist/plugins/specweave-jira/lib/jira-spec-sync.js +57 -5
  24. package/dist/plugins/specweave-jira/lib/jira-spec-sync.js.map +1 -1
  25. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts +5 -1
  26. package/dist/plugins/specweave-jira/lib/jira-status-sync.d.ts.map +1 -1
  27. package/dist/plugins/specweave-jira/lib/jira-status-sync.js +12 -4
  28. package/dist/plugins/specweave-jira/lib/jira-status-sync.js.map +1 -1
  29. package/dist/src/cli/helpers/init/external-import.d.ts.map +1 -1
  30. package/dist/src/cli/helpers/init/external-import.js +186 -19
  31. package/dist/src/cli/helpers/init/external-import.js.map +1 -1
  32. package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts +115 -0
  33. package/dist/src/cli/helpers/init/jira-ado-auto-detect.d.ts.map +1 -0
  34. package/dist/src/cli/helpers/init/jira-ado-auto-detect.js +590 -0
  35. package/dist/src/cli/helpers/init/jira-ado-auto-detect.js.map +1 -0
  36. package/dist/src/config/types.d.ts +6 -6
  37. package/dist/src/core/background/index.d.ts +11 -0
  38. package/dist/src/core/background/index.d.ts.map +1 -0
  39. package/dist/src/core/background/index.js +11 -0
  40. package/dist/src/core/background/index.js.map +1 -0
  41. package/dist/src/core/background/job-manager.d.ts +65 -0
  42. package/dist/src/core/background/job-manager.d.ts.map +1 -0
  43. package/dist/src/core/background/job-manager.js +192 -0
  44. package/dist/src/core/background/job-manager.js.map +1 -0
  45. package/dist/src/core/background/types.d.ts +59 -0
  46. package/dist/src/core/background/types.d.ts.map +1 -0
  47. package/dist/src/core/background/types.js +8 -0
  48. package/dist/src/core/background/types.js.map +1 -0
  49. package/dist/src/core/repo-structure/multi-repo-configurator.d.ts +25 -0
  50. package/dist/src/core/repo-structure/multi-repo-configurator.d.ts.map +1 -0
  51. package/dist/src/core/repo-structure/multi-repo-configurator.js +614 -0
  52. package/dist/src/core/repo-structure/multi-repo-configurator.js.map +1 -0
  53. package/dist/src/core/repo-structure/repo-initializer.d.ts +40 -0
  54. package/dist/src/core/repo-structure/repo-initializer.d.ts.map +1 -0
  55. package/dist/src/core/repo-structure/repo-initializer.js +252 -0
  56. package/dist/src/core/repo-structure/repo-initializer.js.map +1 -0
  57. package/dist/src/core/repo-structure/repo-structure-manager.d.ts +3 -37
  58. package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
  59. package/dist/src/core/repo-structure/repo-structure-manager.js +23 -803
  60. package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
  61. package/dist/src/core/types/spec-metadata.d.ts +2 -0
  62. package/dist/src/core/types/spec-metadata.d.ts.map +1 -1
  63. package/dist/src/importers/import-coordinator.d.ts +20 -0
  64. package/dist/src/importers/import-coordinator.d.ts.map +1 -1
  65. package/dist/src/importers/import-coordinator.js.map +1 -1
  66. package/dist/src/init/architecture/types.d.ts +2 -2
  67. package/dist/src/init/compliance/types.d.ts +1 -1
  68. package/package.json +1 -1
  69. package/plugins/specweave/commands/specweave-jobs.md +160 -0
  70. package/plugins/specweave-ado/lib/ado-spec-sync.js +59 -3
  71. package/plugins/specweave-ado/lib/ado-spec-sync.ts +72 -3
  72. package/plugins/specweave-ado/lib/ado-status-sync.js +35 -3
  73. package/plugins/specweave-ado/lib/ado-status-sync.ts +48 -4
  74. package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +6 -0
  75. package/plugins/specweave-github/lib/github-increment-sync-cli.js +268 -155
  76. package/plugins/specweave-github/lib/github-increment-sync-cli.ts +313 -209
  77. package/plugins/specweave-github/lib/github-status-sync.js +37 -1
  78. package/plugins/specweave-github/lib/github-status-sync.ts +60 -4
  79. package/plugins/specweave-github/lib/increment-issue-builder.js +26 -5
  80. package/plugins/specweave-github/lib/increment-issue-builder.ts +36 -5
  81. package/plugins/specweave-jira/lib/jira-spec-sync.js +53 -5
  82. package/plugins/specweave-jira/lib/jira-spec-sync.ts +87 -7
  83. package/plugins/specweave-jira/lib/jira-status-sync.js +9 -3
  84. package/plugins/specweave-jira/lib/jira-status-sync.ts +15 -6
  85. package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +9 -0
@@ -14,24 +14,22 @@
14
14
  * - Organize specs per project/team
15
15
  * - Split tasks between repositories
16
16
  */
17
- import { existsSync, mkdirSync, writeFileSync } from 'fs';
17
+ import { existsSync } from 'fs';
18
18
  import path from 'path';
19
19
  import chalk from 'chalk';
20
- import { select, input, confirm, number } from '@inquirer/prompts';
20
+ import { select, input, confirm } from '@inquirer/prompts';
21
21
  import ora from 'ora';
22
22
  import { execSync } from 'child_process';
23
- import { execFileNoThrowSync } from '../../utils/execFileNoThrow.js';
24
- import { normalizeRepoName, suggestRepoName } from './repo-id-generator.js';
25
23
  import { SetupStateManager } from './setup-state-manager.js';
26
24
  import { generateEnvFile } from '../../utils/env-file-generator.js';
27
25
  import { generateSetupSummary } from './setup-summary.js';
28
- import { getArchitecturePrompt, getParentRepoBenefits, getVisibilityPrompt, getUrlTypePrompt } from './prompt-consolidator.js';
29
- import { detectRepositoryHints } from './folder-detector.js';
30
- import { discoverRepositories } from './repo-bulk-discovery.js';
31
- import { Octokit } from '@octokit/rest';
26
+ import { getArchitecturePrompt, getVisibilityPrompt, getUrlTypePrompt } from './prompt-consolidator.js';
32
27
  import { initializeProviders } from './providers/index.js';
33
28
  import { getPlatformRegistry } from './platform-registry.js';
34
29
  import { getPlatformSelectionPrompt } from './prompt-consolidator.js';
30
+ // Extracted modules to reduce file size and prevent crashes
31
+ import { configureMultiRepo } from './multi-repo-configurator.js';
32
+ import { initializeLocalRepos as initLocalRepos, createSpecWeaveStructure as createSpecWeaveStruct } from './repo-initializer.js';
35
33
  export class RepoStructureManager {
36
34
  constructor(projectPath, githubToken) {
37
35
  this.projectPath = projectPath;
@@ -125,7 +123,16 @@ export class RepoStructureManager {
125
123
  return this.configureSingleRepo(urlType, platform, provider);
126
124
  case 'parent':
127
125
  // GitHub parent repo (pushed to GitHub)
128
- return this.configureMultiRepo(true, false, urlType, platform, provider);
126
+ return configureMultiRepo({
127
+ projectPath: this.projectPath,
128
+ githubToken: this.githubToken,
129
+ stateManager: this.stateManager,
130
+ useParent: true,
131
+ isLocalParent: false,
132
+ urlType,
133
+ platform: platform,
134
+ provider
135
+ });
129
136
  default:
130
137
  throw new Error(`Unknown architecture: ${architecture}`);
131
138
  }
@@ -313,612 +320,6 @@ export class RepoStructureManager {
313
320
  }]
314
321
  };
315
322
  }
316
- /**
317
- * Configure multi-repository architecture
318
- * @param useParent - Whether to use parent repository/folder
319
- * @param isLocalParent - If true, parent folder is local only (NOT pushed to GitHub)
320
- * @param urlType - Git remote URL format (ssh or https)
321
- * @param platform - Git hosting platform type
322
- * @param provider - Git provider instance for API operations
323
- *
324
- * NOTE: This is primarily user-facing output (console.log/console.error).
325
- * All console.* calls in this method are legitimate user-facing exceptions
326
- * as defined in CONTRIBUTING.md (colored messages, confirmations, formatted output).
327
- */
328
- async configureMultiRepo(useParent = true, isLocalParent = false, urlType = 'ssh', platform = 'github', provider) {
329
- console.log(chalk.cyan('\n🎯 Multi-Repository Configuration\n'));
330
- console.log(chalk.gray('This creates separate repositories for each service/component.\n'));
331
- // Show parent repo benefits if using parent approach
332
- if (useParent) {
333
- console.log(chalk.blue(getParentRepoBenefits()));
334
- console.log('');
335
- }
336
- const config = {
337
- architecture: useParent ? 'parent' : 'multi-repo',
338
- urlType,
339
- platform,
340
- provider,
341
- repositories: []
342
- };
343
- // Save state: architecture selected
344
- await this.saveSetupState({
345
- version: '1.0.0',
346
- architecture: useParent ? 'parent' : 'multi-repo',
347
- parentRepo: undefined,
348
- repos: [],
349
- currentStep: 'architecture-selected',
350
- timestamp: new Date().toISOString(),
351
- envCreated: false
352
- });
353
- // ========== NEW FLOW: Ask discovery strategy FIRST (before parent questions!) ==========
354
- // Step 1: Ask how to configure implementation repositories (BEFORE parent questions)
355
- let discoveryStrategy = 'manual';
356
- let discoveredRepos = [];
357
- let owner = '';
358
- if (!isLocalParent && this.githubToken && useParent) {
359
- console.log(chalk.cyan('\n🚀 Repository Discovery\n'));
360
- console.log(chalk.gray('You\'re setting up multiple repositories. We can discover them automatically!\n'));
361
- const configMethod = await select({
362
- message: 'How do you want to configure repositories?',
363
- choices: [
364
- {
365
- name: [
366
- chalk.bold.green('🎯 Bulk Discovery (RECOMMENDED)'),
367
- chalk.gray(' Automatically discover repos from ' + provider.config.name),
368
- chalk.gray(' • Select parent from discovered list'),
369
- chalk.gray(' • Auto-configure implementation repos'),
370
- chalk.gray(' • Supports: all, pattern, regex filtering'),
371
- ''
372
- ].join('\n'),
373
- value: 'bulk-discovery'
374
- },
375
- {
376
- name: [
377
- chalk.bold('✏️ Manual Entry'),
378
- chalk.gray(' Enter each repository manually'),
379
- chalk.gray(' • Full control over settings'),
380
- chalk.gray(' • Best for new repos or custom setup'),
381
- ''
382
- ].join('\n'),
383
- value: 'manual'
384
- }
385
- ],
386
- default: 'bulk-discovery'
387
- });
388
- discoveryStrategy = configMethod;
389
- }
390
- // Step 2: If bulk discovery, discover repos FIRST, then ask which is parent
391
- if (discoveryStrategy === 'bulk-discovery') {
392
- // Get owner FIRST (needed for discovery)
393
- console.log(chalk.cyan('\n👤 Repository Owner\n'));
394
- owner = await input({
395
- message: `${provider.config.name} owner/organization:`,
396
- validate: async (val) => {
397
- if (!val.trim())
398
- return 'Owner is required';
399
- // Validate owner exists on the platform
400
- if (this.githubToken) {
401
- const result = await provider.validateOwner(val, this.githubToken);
402
- if (!result.valid) {
403
- return result.error || `Invalid ${provider.config.name} owner`;
404
- }
405
- }
406
- return true;
407
- }
408
- });
409
- // Discover repositories via pattern matching
410
- const octokit = new Octokit({ auth: this.githubToken });
411
- const isOrg = await provider.isOrganization(owner, this.githubToken);
412
- // Retry loop for pattern adjustment
413
- let discoveryResult = null;
414
- while (discoveryResult === null) {
415
- // Discovery-first flow: skip count validation since user discovers THEN selects
416
- discoveryResult = await discoverRepositories(octokit, owner, isOrg, 0, { skipValidation: true });
417
- // If null, user selected "go back and adjust pattern", loop will retry
418
- // If user selected "manual", discoveryResult will be { repositories: [], strategy: 'manual' }
419
- }
420
- if (discoveryResult && discoveryResult.strategy !== 'manual') {
421
- discoveredRepos = discoveryResult.repositories;
422
- // Now ask: Which repo is the parent?
423
- console.log(chalk.cyan('\n🏠 Select Parent Repository\n'));
424
- console.log(chalk.gray('Choose which repository will be the parent (contains .specweave/ structure)\n'));
425
- const parentChoices = [
426
- ...discoveredRepos.map((repo, index) => ({
427
- name: `${chalk.bold(repo.name)}\n${chalk.gray(repo.description || '(no description)')}`,
428
- value: index.toString(),
429
- short: repo.name
430
- })),
431
- {
432
- name: `${chalk.yellow('✏️ Enter parent manually')} ${chalk.gray('(not in discovered list)')}`,
433
- value: 'manual',
434
- short: 'Enter manually'
435
- }
436
- ];
437
- const parentSelection = await select({
438
- message: 'Which repository is the parent?',
439
- choices: parentChoices,
440
- pageSize: 15
441
- });
442
- if (parentSelection === 'manual') {
443
- // User wants to enter parent manually - fall back to old flow
444
- discoveryStrategy = 'manual';
445
- discoveredRepos = []; // Clear discovered repos
446
- }
447
- else {
448
- // User selected a parent from discovered list
449
- const parentIndex = parseInt(parentSelection);
450
- const selectedParent = discoveredRepos[parentIndex];
451
- // Fetch full repo details from GitHub API
452
- let description = selectedParent.description || 'SpecWeave parent repository - specs, docs, and architecture';
453
- let existingVisibility = selectedParent.private ? 'private' : 'public';
454
- try {
455
- const response = await fetch(`https://api.github.com/repos/${owner}/${selectedParent.name}`, {
456
- headers: {
457
- 'Authorization': `Bearer ${this.githubToken}`,
458
- 'Accept': 'application/vnd.github+json'
459
- }
460
- });
461
- if (response.ok) {
462
- const data = await response.json();
463
- description = data.description || description;
464
- existingVisibility = data.private ? 'private' : 'public';
465
- }
466
- }
467
- catch {
468
- // Use defaults if fetch fails
469
- }
470
- // Set parent config
471
- config.parentRepo = {
472
- name: selectedParent.name,
473
- owner: owner,
474
- description: description,
475
- visibility: existingVisibility,
476
- createOnGitHub: false // Already exists!
477
- };
478
- // Remove parent from discovered repos (implementation repos = discovered - parent)
479
- discoveredRepos.splice(parentIndex, 1);
480
- console.log(chalk.green(`\n✓ Using existing repository: ${owner}/${selectedParent.name}\n`));
481
- console.log(chalk.gray(`✓ Implementation repositories: ${discoveredRepos.length}\n`));
482
- // Save state: parent repo configured
483
- await this.saveSetupState({
484
- version: '1.0.0',
485
- architecture: useParent ? 'parent' : 'multi-repo',
486
- parentRepo: config.parentRepo,
487
- repos: [],
488
- currentStep: 'parent-repo-configured',
489
- timestamp: new Date().toISOString(),
490
- envCreated: false
491
- });
492
- // Skip to repository configuration (lines 794+)
493
- // We'll continue below with discoveredRepos
494
- }
495
- }
496
- else {
497
- // User selected manual - fall back to old flow
498
- discoveryStrategy = 'manual';
499
- }
500
- }
501
- // Step 3: Manual flow (existing logic) - only runs if discoveryStrategy === 'manual'
502
- if (discoveryStrategy === 'manual' && useParent) {
503
- let parentAnswers;
504
- if (isLocalParent) {
505
- // Local parent: Skip GitHub questions, just ask for folder name
506
- console.log(chalk.blue('\n💡 Local Parent Folder Setup'));
507
- console.log(chalk.gray('This folder will contain .specweave/ but will NOT be pushed to GitHub.\n'));
508
- const parentNameAnswer = await input({
509
- message: 'Parent folder name:',
510
- default: `${path.basename(this.projectPath)}`,
511
- validate: (val) => {
512
- if (!val.trim())
513
- return 'Folder name is required';
514
- return true;
515
- }
516
- });
517
- const ownerForLocalAnswer = await input({
518
- message: `${provider.config.name} owner/organization for IMPLEMENTATION repos:`,
519
- validate: async (val) => {
520
- if (!val.trim())
521
- return 'Owner is required';
522
- // Validate owner exists on the platform
523
- if (this.githubToken) {
524
- const result = await provider.validateOwner(val, this.githubToken);
525
- if (!result.valid) {
526
- return result.error || `Invalid ${provider.config.name} owner`;
527
- }
528
- }
529
- return true;
530
- }
531
- });
532
- parentAnswers = {
533
- parentName: parentNameAnswer,
534
- owner: ownerForLocalAnswer
535
- };
536
- // Set defaults for local parent
537
- parentAnswers.description = 'Local parent folder (not synced to GitHub)';
538
- parentAnswers.createOnGitHub = false; // Never create GitHub repo for local parent
539
- }
540
- else {
541
- // GitHub parent: First ask if using existing or creating new
542
- const parentChoice = await select({
543
- message: 'Parent repository setup:',
544
- choices: [
545
- {
546
- name: `${chalk.green('Use existing parent repository')}\n${chalk.gray('Connect to an existing GitHub repo that already has .specweave/ structure')}`,
547
- value: 'existing'
548
- },
549
- {
550
- name: `${chalk.blue('Create new parent repository')}\n${chalk.gray('Create a new GitHub repo for specs, docs, and architecture')}`,
551
- value: 'new'
552
- }
553
- ],
554
- default: 'new'
555
- });
556
- if (parentChoice === 'existing') {
557
- // Using existing parent repository
558
- console.log(chalk.cyan('\n📋 Existing Parent Repository\n'));
559
- // Ask for owner first
560
- const existingOwner = await input({
561
- message: `${provider.config.name} owner/organization:`,
562
- validate: async (val) => {
563
- if (!val.trim())
564
- return 'Owner is required';
565
- // Validate owner exists on the platform
566
- if (this.githubToken) {
567
- const result = await provider.validateOwner(val, this.githubToken);
568
- if (!result.valid) {
569
- return result.error || `Invalid ${provider.config.name} owner`;
570
- }
571
- }
572
- return true;
573
- }
574
- });
575
- const ownerPrompt = { owner: existingOwner };
576
- // Ask for existing repo name
577
- const existingParentName = await input({
578
- message: 'Existing parent repository name:',
579
- default: `${path.basename(this.projectPath)}-parent`,
580
- validate: async (val) => {
581
- if (!val.trim())
582
- return 'Repository name is required';
583
- // Validate repository EXISTS on the platform
584
- if (this.githubToken && ownerPrompt.owner) {
585
- const result = await provider.validateRepository(ownerPrompt.owner, val, this.githubToken);
586
- if (!result.exists) {
587
- return `Repository ${ownerPrompt.owner}/${val} not found on ${provider.config.name}. Please check the name or choose 'Create new'.`;
588
- }
589
- }
590
- return true;
591
- }
592
- });
593
- const repoPrompt = { parentName: existingParentName };
594
- // Fetch description and visibility from GitHub API (or use defaults)
595
- let description = 'SpecWeave parent repository - specs, docs, and architecture';
596
- let existingVisibility = 'private';
597
- if (this.githubToken) {
598
- try {
599
- const response = await fetch(`https://api.github.com/repos/${ownerPrompt.owner}/${repoPrompt.parentName}`, {
600
- headers: {
601
- 'Authorization': `Bearer ${this.githubToken}`,
602
- 'Accept': 'application/vnd.github+json'
603
- }
604
- });
605
- if (response.ok) {
606
- const data = await response.json();
607
- description = data.description || description;
608
- existingVisibility = data.private ? 'private' : 'public';
609
- }
610
- }
611
- catch {
612
- // Use defaults if fetch fails
613
- }
614
- }
615
- parentAnswers = {
616
- owner: ownerPrompt.owner,
617
- parentName: repoPrompt.parentName,
618
- description: description,
619
- createOnGitHub: false, // Don't create, it already exists!
620
- visibility: existingVisibility // Use existing visibility from GitHub
621
- };
622
- console.log(chalk.green(`\n✓ Using existing repository: ${ownerPrompt.owner}/${repoPrompt.parentName}\n`));
623
- }
624
- else {
625
- // Creating new parent repository
626
- console.log(chalk.cyan('\n✨ New Parent Repository\n'));
627
- // Ask for owner (separate prompt to avoid validator issues)
628
- const newOwner = await input({
629
- message: `${provider.config.name} owner/organization for ALL repos:`,
630
- validate: async (val) => {
631
- if (!val.trim())
632
- return 'Owner is required';
633
- // Validate owner exists on the platform
634
- if (this.githubToken) {
635
- const result = await provider.validateOwner(val, this.githubToken);
636
- if (!result.valid) {
637
- return result.error || `Invalid ${provider.config.name} owner`;
638
- }
639
- }
640
- return true;
641
- }
642
- });
643
- const ownerPrompt = { owner: newOwner };
644
- // Now ask remaining questions, using the owner value
645
- const newParentName = await input({
646
- message: 'Parent repository name:',
647
- default: `${path.basename(this.projectPath)}-parent`,
648
- validate: async (val) => {
649
- if (!val.trim())
650
- return 'Repository name is required';
651
- // Validate repository DOESN'T exist
652
- if (this.githubToken && ownerPrompt.owner) {
653
- const result = await provider.validateRepository(ownerPrompt.owner, val, this.githubToken);
654
- if (result.exists) {
655
- return `Repository ${ownerPrompt.owner}/${val} already exists at ${result.url}. Please choose 'Use existing' or pick a different name.`;
656
- }
657
- }
658
- return true;
659
- }
660
- });
661
- const newParentDesc = await input({
662
- message: 'Parent repository description:',
663
- default: 'SpecWeave parent repository - specs, docs, and architecture'
664
- });
665
- const createParentOnGitHub = await confirm({
666
- message: 'Create parent repository on GitHub?',
667
- default: true
668
- });
669
- // Merge the answers
670
- parentAnswers = {
671
- ...ownerPrompt,
672
- parentName: newParentName,
673
- description: newParentDesc,
674
- createOnGitHub: createParentOnGitHub
675
- };
676
- }
677
- }
678
- // Ask about visibility for parent repo (only if creating NEW repo on GitHub)
679
- let parentVisibility = 'private';
680
- if (!isLocalParent && parentAnswers.createOnGitHub) {
681
- // Only prompt for visibility when creating a NEW repository
682
- const parentVisibilityPrompt = getVisibilityPrompt(parentAnswers.parentName);
683
- parentVisibility = await select({
684
- message: parentVisibilityPrompt.question,
685
- choices: parentVisibilityPrompt.options.map(opt => ({
686
- name: `${opt.label}\n${chalk.gray(opt.description)}`,
687
- value: opt.value
688
- })),
689
- default: parentVisibilityPrompt.default
690
- });
691
- }
692
- else if (!isLocalParent && parentAnswers.visibility) {
693
- // Use existing repository's visibility (fetched from GitHub API)
694
- parentVisibility = parentAnswers.visibility;
695
- }
696
- config.parentRepo = {
697
- name: parentAnswers.parentName,
698
- owner: parentAnswers.owner,
699
- description: parentAnswers.description,
700
- visibility: parentVisibility,
701
- createOnGitHub: parentAnswers.createOnGitHub
702
- };
703
- // Save state: parent repo configured
704
- await this.saveSetupState({
705
- version: '1.0.0',
706
- architecture: useParent ? 'parent' : 'multi-repo',
707
- parentRepo: config.parentRepo,
708
- repos: [],
709
- currentStep: 'parent-repo-configured',
710
- timestamp: new Date().toISOString(),
711
- envCreated: false
712
- });
713
- }
714
- // Step 4: Repository count and discovery (skip count question if using bulk discovery)
715
- let repoCount;
716
- let bulkDiscoveryStrategy = discoveryStrategy === 'bulk-discovery' ? 'pattern' : 'manual';
717
- if (discoveryStrategy === 'bulk-discovery' && discoveredRepos.length > 0) {
718
- // Bulk discovery: repos already discovered, skip count question
719
- repoCount = discoveredRepos.length;
720
- console.log(chalk.green(`\n✓ Total repositories: ${repoCount + 1} (1 parent + ${repoCount} implementation)\n`));
721
- }
722
- else {
723
- // Manual entry: ask for count
724
- // Auto-detect existing folders
725
- const hints = await detectRepositoryHints(this.projectPath);
726
- if (hints.detectedFolders.length > 0) {
727
- console.log(chalk.green(`\n✓ Detected ${hints.detectedFolders.length} service folder(s):`));
728
- hints.detectedFolders.forEach(f => console.log(chalk.gray(` • ${f}`)));
729
- console.log('');
730
- }
731
- // Show repository count clarification BEFORE asking
732
- if (useParent && config.parentRepo) {
733
- console.log(chalk.cyan('\n📊 Repository Count\n'));
734
- console.log(chalk.gray('You will create:'));
735
- if (isLocalParent) {
736
- console.log(chalk.white(' • 1 parent FOLDER (local only, .specweave/ gitignored)'));
737
- console.log(chalk.white(' • N implementation repositories (your services/apps on GitHub)'));
738
- }
739
- else {
740
- console.log(chalk.white(' • 1 parent repository (specs, docs, increments)'));
741
- console.log(chalk.white(' • N implementation repositories (your services/apps)'));
742
- }
743
- console.log(chalk.gray('\nNext question asks for: IMPLEMENTATION repositories ONLY (not counting parent)\n'));
744
- }
745
- // Ask how many implementation repositories
746
- const repoCountAnswer = await number({
747
- message: useParent
748
- ? '📦 How many IMPLEMENTATION repositories? (not counting parent)'
749
- : 'How many repositories?',
750
- default: hints.suggestedCount, // Use auto-detected count
751
- validate: (val) => {
752
- if (val === undefined || val < 1)
753
- return useParent
754
- ? 'Need at least 1 implementation repository'
755
- : 'Need at least 2 repositories';
756
- if (val > 10)
757
- return 'Maximum 10 repositories supported';
758
- return true;
759
- }
760
- });
761
- repoCount = repoCountAnswer ?? hints.suggestedCount;
762
- // Show summary AFTER for confirmation
763
- if (useParent && config.parentRepo) {
764
- if (isLocalParent) {
765
- console.log(chalk.green(`\n✓ Total repositories to create: ${repoCount} implementation repos`));
766
- console.log(chalk.gray(` (Parent folder is local only, not counted)\n`));
767
- }
768
- else {
769
- const totalRepos = 1 + repoCount;
770
- console.log(chalk.green(`\n✓ Total repositories to create: ${totalRepos} (1 parent + ${repoCount} implementation)\n`));
771
- }
772
- }
773
- // Bulk repository discovery for manual flow (old behavior)
774
- if (this.githubToken && config.parentRepo && discoveryStrategy === 'manual') {
775
- const octokit = new Octokit({ auth: this.githubToken });
776
- const owner = config.parentRepo.owner;
777
- const isOrg = await provider.isOrganization(owner, this.githubToken);
778
- // Retry loop for pattern adjustment
779
- let discoveryResult = null;
780
- while (discoveryResult === null) {
781
- discoveryResult = await discoverRepositories(octokit, owner, isOrg, repoCount);
782
- // If null, user selected "go back and adjust pattern", loop will retry
783
- // If user selected "manual", discoveryResult will be { repositories: [], strategy: 'manual' }
784
- }
785
- if (discoveryResult) {
786
- bulkDiscoveryStrategy = discoveryResult.strategy;
787
- if (discoveryResult.strategy !== 'manual') {
788
- discoveredRepos = discoveryResult.repositories;
789
- // Update repoCount to match discovered repos
790
- if (discoveredRepos.length !== repoCount) {
791
- console.log(chalk.yellow(`\n📝 Adjusting repository count from ${repoCount} to ${discoveredRepos.length} (based on discovery)\n`));
792
- repoCount = discoveredRepos.length;
793
- }
794
- }
795
- }
796
- }
797
- }
798
- // Configure each repository
799
- console.log(chalk.cyan('\n📦 Configure Each Repository:\n'));
800
- const usedIds = new Set();
801
- const configuredRepoNames = []; // Track configured repo names for smart ID generation
802
- for (let i = 0; i < repoCount; i++) {
803
- const discoveredRepo = discoveredRepos[i]; // May be undefined if manual
804
- const isDiscovered = bulkDiscoveryStrategy !== 'manual' && discoveredRepo;
805
- console.log(chalk.white(`\nRepository ${i + 1} of ${repoCount}:`));
806
- // Smart suggestion for ALL repos (not just first one!)
807
- const projectName = path.basename(this.projectPath);
808
- const suggestedName = isDiscovered ? discoveredRepo.name : suggestRepoName(projectName, i, repoCount);
809
- // If discovered, auto-use without confirmation (repos exist on GitHub)
810
- if (isDiscovered) {
811
- // Use normalized repo name as ID (repo names are unique in GitHub)
812
- const repoId = normalizeRepoName(discoveredRepo.name);
813
- console.log(chalk.green(` ✓ Using: ${chalk.bold(discoveredRepo.name)} ${chalk.gray(`(id: ${repoId})`)}`));
814
- usedIds.add(repoId);
815
- configuredRepoNames.push(discoveredRepo.name);
816
- config.repositories.push({
817
- id: repoId,
818
- name: discoveredRepo.name,
819
- owner: discoveredRepo.owner,
820
- description: discoveredRepo.description || `${discoveredRepo.name} service`,
821
- path: discoveredRepo.name,
822
- visibility: discoveredRepo.private ? 'private' : 'public',
823
- createOnGitHub: false,
824
- isNested: useParent
825
- });
826
- continue;
827
- }
828
- // Manual entry (original behavior)
829
- const repoName = await input({
830
- message: 'Repository name:',
831
- default: suggestedName,
832
- validate: async (val) => {
833
- if (!val.trim())
834
- return 'Repository name is required';
835
- // Validate repository doesn't exist (skip for discovered repos)
836
- if (!isDiscovered && this.githubToken && config.parentRepo) {
837
- const result = await provider.validateRepository(config.parentRepo.owner, val, this.githubToken);
838
- if (result.exists) {
839
- return `Repository ${config.parentRepo.owner}/${val} already exists at ${result.url}`;
840
- }
841
- }
842
- return true;
843
- }
844
- });
845
- const repoDescription = await input({
846
- message: 'Repository description:',
847
- default: `${path.basename(repoName)} service`
848
- });
849
- // Skip "Create on GitHub?" for discovered repos - they already exist!
850
- let repoCreateOnGitHub = false;
851
- if (!isDiscovered) {
852
- repoCreateOnGitHub = await confirm({
853
- message: 'Create this repository on GitHub?',
854
- default: true
855
- });
856
- }
857
- const repoAnswers = {
858
- name: repoName,
859
- description: repoDescription,
860
- createOnGitHub: repoCreateOnGitHub
861
- };
862
- // Use normalized repo name as ID (repo names are unique)
863
- const id = normalizeRepoName(repoAnswers.name);
864
- console.log(chalk.green(` ✓ Repository ID: ${chalk.bold(id)}`));
865
- usedIds.add(id);
866
- configuredRepoNames.push(repoAnswers.name);
867
- // Ask about visibility only if creating a new repository
868
- let visibility = 'private';
869
- if (repoAnswers.createOnGitHub) {
870
- const visibilityPrompt = getVisibilityPrompt(repoAnswers.name);
871
- visibility = await select({
872
- message: visibilityPrompt.question,
873
- choices: visibilityPrompt.options.map(opt => ({
874
- name: `${opt.label}\n${chalk.gray(opt.description)}`,
875
- value: opt.value
876
- })),
877
- default: visibilityPrompt.default
878
- });
879
- }
880
- config.repositories.push({
881
- id: id,
882
- name: repoAnswers.name,
883
- owner: config.parentRepo?.owner || '',
884
- description: repoAnswers.description,
885
- path: repoAnswers.name, // Use full repo name as folder (not short ID)
886
- visibility: visibility,
887
- createOnGitHub: repoAnswers.createOnGitHub,
888
- isNested: useParent
889
- });
890
- // Save state after each repo
891
- await this.saveSetupState({
892
- version: '1.0.0',
893
- architecture: useParent ? 'parent' : 'multi-repo',
894
- parentRepo: config.parentRepo,
895
- repos: config.repositories.map(r => ({
896
- id: r.id,
897
- repo: r.name,
898
- owner: r.owner,
899
- path: r.path,
900
- visibility: r.visibility,
901
- displayName: r.name,
902
- created: false
903
- })),
904
- currentStep: `repo-${i + 1}-configured`,
905
- timestamp: new Date().toISOString(),
906
- envCreated: false
907
- });
908
- }
909
- return config;
910
- }
911
- /**
912
- * Save setup state for Ctrl+C recovery
913
- */
914
- async saveSetupState(state) {
915
- try {
916
- await this.stateManager.saveState(state);
917
- }
918
- catch (error) {
919
- console.warn(chalk.yellow(`⚠️ Failed to save setup state: ${error.message}`));
920
- }
921
- }
922
323
  /**
923
324
  * Configure monorepo
924
325
  */
@@ -1015,7 +416,7 @@ export class RepoStructureManager {
1015
416
  }, this.githubToken);
1016
417
  created.push(`${config.parentRepo.owner}/${config.parentRepo.name}`);
1017
418
  // Save state: parent repo created
1018
- await this.saveSetupState({
419
+ await this.stateManager.saveState({
1019
420
  version: '1.0.0',
1020
421
  architecture: config.architecture,
1021
422
  parentRepo: { ...config.parentRepo, url: config.provider.getRemoteUrl(config.parentRepo.owner, config.parentRepo.name, config.urlType) },
@@ -1083,7 +484,7 @@ export class RepoStructureManager {
1083
484
  console.log(chalk.gray(' TIP: gh CLI auth is recommended over tokens'));
1084
485
  console.log(chalk.yellow(' ⚠️ DO NOT commit .env to git (contains secrets!)'));
1085
486
  // Save state: env created
1086
- await this.saveSetupState({
487
+ await this.stateManager.saveState({
1087
488
  version: '1.0.0',
1088
489
  architecture: config.architecture,
1089
490
  parentRepo: config.parentRepo,
@@ -1192,200 +593,19 @@ export class RepoStructureManager {
1192
593
  }
1193
594
  return false;
1194
595
  }
1195
- /**
1196
- * Check if a repository exists on GitHub
1197
- */
1198
- async repositoryExistsOnGitHub(owner, repo) {
1199
- if (!this.githubToken) {
1200
- return false;
1201
- }
1202
- try {
1203
- const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
1204
- headers: {
1205
- 'Authorization': `Bearer ${this.githubToken}`,
1206
- 'Accept': 'application/vnd.github+json'
1207
- }
1208
- });
1209
- return response.ok;
1210
- }
1211
- catch {
1212
- return false;
1213
- }
1214
- }
1215
- /**
1216
- * Clone or initialize a repository
1217
- * If the repo exists on the platform, clone it; otherwise, init + add remote
1218
- */
1219
- async cloneOrInitRepository(repoPath, owner, name, createOnGitHub, urlType = 'ssh', provider) {
1220
- // If .git already exists, skip
1221
- if (existsSync(path.join(repoPath, '.git'))) {
1222
- return;
1223
- }
1224
- const remoteUrl = provider.getRemoteUrl(owner, name, urlType);
1225
- // Check if repository exists on GitHub
1226
- const repoExists = await this.repositoryExistsOnGitHub(owner, name);
1227
- if (repoExists) {
1228
- // Repository exists - clone it
1229
- console.log(chalk.gray(` → Cloning existing repository from GitHub...`));
1230
- try {
1231
- // Remove directory if it exists and is empty
1232
- if (existsSync(repoPath)) {
1233
- const files = require('fs').readdirSync(repoPath);
1234
- if (files.length === 0) {
1235
- require('fs').rmdirSync(repoPath);
1236
- }
1237
- }
1238
- // Clone the repository
1239
- const parentDir = path.dirname(repoPath);
1240
- const repoName = path.basename(repoPath);
1241
- execFileNoThrowSync('git', ['clone', remoteUrl, repoName], { cwd: parentDir });
1242
- console.log(chalk.green(` ✓ Cloned ${owner}/${name}`));
1243
- }
1244
- catch (error) {
1245
- console.log(chalk.yellow(` ⚠️ Clone failed: ${error.message}`));
1246
- console.log(chalk.gray(` → Falling back to init + remote`));
1247
- // Fallback: init + add remote
1248
- if (!existsSync(repoPath)) {
1249
- mkdirSync(repoPath, { recursive: true });
1250
- }
1251
- execFileNoThrowSync('git', ['init'], { cwd: repoPath });
1252
- execFileNoThrowSync('git', ['remote', 'add', 'origin', remoteUrl], { cwd: repoPath });
1253
- }
1254
- }
1255
- else {
1256
- // Repository doesn't exist - init + add remote
1257
- if (!createOnGitHub) {
1258
- console.log(chalk.gray(` → Repository will be created later (skipping for now)`));
1259
- return;
1260
- }
1261
- console.log(chalk.gray(` → Initializing empty git repository...`));
1262
- if (!existsSync(repoPath)) {
1263
- mkdirSync(repoPath, { recursive: true });
1264
- }
1265
- execFileNoThrowSync('git', ['init'], { cwd: repoPath });
1266
- execFileNoThrowSync('git', ['remote', 'add', 'origin', remoteUrl], { cwd: repoPath });
1267
- console.log(chalk.green(` ✓ Initialized ${owner}/${name}`));
1268
- }
1269
- }
1270
596
  /**
1271
597
  * Initialize local git repositories
598
+ * Delegates to extracted repo-initializer module
1272
599
  */
1273
600
  async initializeLocalRepos(config) {
1274
- const spinner = ora('Initializing local repositories...').start();
1275
- // Create directory structure based on architecture
1276
- if (config.architecture === 'parent') {
1277
- // Parent repo approach: ROOT-LEVEL cloning (not services/!)
1278
- // Initialize parent repo at root
1279
- if (!existsSync(path.join(this.projectPath, '.git'))) {
1280
- execFileNoThrowSync('git', ['init'], { cwd: this.projectPath });
1281
- if (config.parentRepo) {
1282
- const remoteUrl = config.provider.getRemoteUrl(config.parentRepo.owner, config.parentRepo.name, config.urlType);
1283
- execFileNoThrowSync('git', ['remote', 'add', 'origin', remoteUrl], { cwd: this.projectPath });
1284
- }
1285
- }
1286
- // Initialize implementation repos at ROOT LEVEL
1287
- for (const repo of config.repositories) {
1288
- const repoPath = path.join(this.projectPath, repo.path);
1289
- // Clone or initialize repository
1290
- await this.cloneOrInitRepository(repoPath, repo.owner, repo.name, repo.createOnGitHub, config.urlType, config.provider);
1291
- // Create basic structure (only if repo was just initialized, not cloned)
1292
- if (!repo.createOnGitHub || !await this.repositoryExistsOnGitHub(repo.owner, repo.name)) {
1293
- this.createBasicRepoStructure(repoPath, repo.id);
1294
- }
1295
- }
1296
- }
1297
- else if (config.architecture === 'multi-repo') {
1298
- // Standard multi-repo: repos as subdirectories
1299
- for (const repo of config.repositories) {
1300
- const repoPath = path.join(this.projectPath, repo.path);
1301
- // Clone or initialize repository
1302
- await this.cloneOrInitRepository(repoPath, repo.owner, repo.name, repo.createOnGitHub, config.urlType, config.provider);
1303
- // Create basic structure (only if repo was just initialized, not cloned)
1304
- if (!repo.createOnGitHub || !await this.repositoryExistsOnGitHub(repo.owner, repo.name)) {
1305
- this.createBasicRepoStructure(repoPath, repo.id);
1306
- }
1307
- }
1308
- }
1309
- else {
1310
- // Single repo or monorepo
1311
- if (!existsSync(path.join(this.projectPath, '.git'))) {
1312
- execFileNoThrowSync('git', ['init'], { cwd: this.projectPath });
1313
- const repo = config.repositories[0];
1314
- if (repo) {
1315
- const remoteUrl = config.provider.getRemoteUrl(repo.owner, repo.name, config.urlType);
1316
- execFileNoThrowSync('git', ['remote', 'add', 'origin', remoteUrl], { cwd: this.projectPath });
1317
- }
1318
- }
1319
- // For monorepo, create project directories
1320
- if (config.architecture === 'monorepo' && config.monorepoProjects) {
1321
- for (const project of config.monorepoProjects) {
1322
- const projectPath = path.join(this.projectPath, 'packages', project);
1323
- if (!existsSync(projectPath)) {
1324
- mkdirSync(projectPath, { recursive: true });
1325
- }
1326
- this.createBasicRepoStructure(projectPath, project);
1327
- }
1328
- }
1329
- }
1330
- spinner.succeed('Local repositories initialized');
1331
- }
1332
- /**
1333
- * Create basic structure for a repository/project
1334
- *
1335
- * NOTE: Only creates src/ and tests/ folders for nested projects.
1336
- * Documentation lives in .specweave/docs/internal/specs/{project-id}/ (source of truth)
1337
- */
1338
- createBasicRepoStructure(repoPath, projectId) {
1339
- // Create basic directories (NO docs/ - documentation lives in .specweave/)
1340
- const dirs = ['src', 'tests'];
1341
- for (const dir of dirs) {
1342
- const dirPath = path.join(repoPath, dir);
1343
- if (!existsSync(dirPath)) {
1344
- mkdirSync(dirPath, { recursive: true });
1345
- }
1346
- }
1347
- // Create README.md
1348
- const readmePath = path.join(repoPath, 'README.md');
1349
- if (!existsSync(readmePath)) {
1350
- const readmeContent = `# ${projectId}\n\n${projectId} service/component.\n\nPart of SpecWeave multi-repository project.\n`;
1351
- writeFileSync(readmePath, readmeContent);
1352
- }
1353
- // Create .gitignore
1354
- const gitignorePath = path.join(repoPath, '.gitignore');
1355
- if (!existsSync(gitignorePath)) {
1356
- const gitignoreContent = `node_modules/\ndist/\n.env\n.DS_Store\n*.log\n`;
1357
- writeFileSync(gitignorePath, gitignoreContent);
1358
- }
601
+ return initLocalRepos(config, this.projectPath, this.githubToken);
1359
602
  }
1360
603
  /**
1361
- * Create SpecWeave project structure (simplified - ONLY specs folders)
1362
- *
1363
- * NOTE: As of v0.X.X (increment 0026), we ONLY create specs/ folders.
1364
- * No modules/, team/, architecture/, legacy/ folders.
604
+ * Create SpecWeave project structure
605
+ * Delegates to extracted repo-initializer module
1365
606
  */
1366
607
  async createSpecWeaveStructure(config) {
1367
- const specweavePath = path.join(this.projectPath, '.specweave');
1368
- // Create project-specific spec folders (ONLY specs/, no nested folders)
1369
- if (config.architecture === 'monorepo' && config.monorepoProjects) {
1370
- // Monorepo: create specs folder for each project
1371
- for (const project of config.monorepoProjects) {
1372
- const projectSpecPath = path.join(specweavePath, 'docs', 'internal', 'specs', project.toLowerCase());
1373
- if (!existsSync(projectSpecPath)) {
1374
- mkdirSync(projectSpecPath, { recursive: true });
1375
- }
1376
- console.log(chalk.gray(` ✓ Created project structure: ${project}`));
1377
- }
1378
- }
1379
- else if (config.architecture === 'multi-repo' || config.architecture === 'parent') {
1380
- // Multi-repo: create specs folder for each repository
1381
- for (const repo of config.repositories) {
1382
- const projectSpecPath = path.join(specweavePath, 'docs', 'internal', 'specs', repo.id);
1383
- if (!existsSync(projectSpecPath)) {
1384
- mkdirSync(projectSpecPath, { recursive: true });
1385
- }
1386
- console.log(chalk.gray(` ✓ Created project structure: ${repo.path}`));
1387
- }
1388
- }
608
+ return createSpecWeaveStruct(config, this.projectPath);
1389
609
  }
1390
610
  }
1391
611
  //# sourceMappingURL=repo-structure-manager.js.map