@paths.design/caws-cli 8.0.1 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/commands/archive.d.ts +2 -1
  2. package/dist/commands/archive.d.ts.map +1 -1
  3. package/dist/commands/archive.js +114 -6
  4. package/dist/commands/burnup.d.ts.map +1 -1
  5. package/dist/commands/burnup.js +109 -10
  6. package/dist/commands/diagnose.js +1 -1
  7. package/dist/commands/mode.js +24 -14
  8. package/dist/commands/provenance.js +216 -93
  9. package/dist/commands/quality-gates.d.ts.map +1 -1
  10. package/dist/commands/quality-gates.js +3 -1
  11. package/dist/commands/specs.js +184 -6
  12. package/dist/commands/status.d.ts.map +1 -1
  13. package/dist/commands/status.js +134 -10
  14. package/dist/commands/templates.js +2 -2
  15. package/dist/error-handler.js +6 -98
  16. package/dist/generators/jest-config-generator.js +242 -0
  17. package/dist/index.js +4 -7
  18. package/dist/minimal-cli.js +3 -1
  19. package/dist/scaffold/claude-hooks.js +316 -0
  20. package/dist/scaffold/index.js +18 -0
  21. package/dist/templates/.claude/README.md +190 -0
  22. package/dist/templates/.claude/hooks/audit.sh +96 -0
  23. package/dist/templates/.claude/hooks/block-dangerous.sh +90 -0
  24. package/dist/templates/.claude/hooks/naming-check.sh +97 -0
  25. package/dist/templates/.claude/hooks/quality-check.sh +68 -0
  26. package/dist/templates/.claude/hooks/scan-secrets.sh +85 -0
  27. package/dist/templates/.claude/hooks/scope-guard.sh +105 -0
  28. package/dist/templates/.claude/hooks/validate-spec.sh +76 -0
  29. package/dist/templates/.claude/settings.json +95 -0
  30. package/dist/test-analysis.js +203 -10
  31. package/dist/utils/error-categories.js +210 -0
  32. package/dist/utils/quality-gates-utils.js +402 -0
  33. package/dist/utils/typescript-detector.js +36 -90
  34. package/dist/validation/spec-validation.js +59 -6
  35. package/package.json +5 -3
  36. package/templates/.claude/README.md +190 -0
  37. package/templates/.claude/hooks/audit.sh +96 -0
  38. package/templates/.claude/hooks/block-dangerous.sh +90 -0
  39. package/templates/.claude/hooks/naming-check.sh +97 -0
  40. package/templates/.claude/hooks/quality-check.sh +68 -0
  41. package/templates/.claude/hooks/scan-secrets.sh +85 -0
  42. package/templates/.claude/hooks/scope-guard.sh +105 -0
  43. package/templates/.claude/hooks/validate-spec.sh +76 -0
  44. package/templates/.claude/settings.json +95 -0
@@ -4,12 +4,43 @@
4
4
  * @author @darianrosebrook
5
5
  */
6
6
 
7
+ /* global fetch */
8
+
7
9
  const fs = require('fs-extra');
8
10
  const path = require('path');
9
11
  const crypto = require('crypto');
10
12
  const yaml = require('js-yaml');
13
+ const { execSync } = require('child_process');
11
14
  const { commandWrapper } = require('../utils/command-wrapper');
12
15
 
16
+ /**
17
+ * Get quality gates status from saved report
18
+ * @returns {Object} Quality gates status
19
+ */
20
+ function getQualityGatesStatus() {
21
+ const reportPath = path.join(process.cwd(), '.caws', 'quality-gates-report.json');
22
+
23
+ if (fs.existsSync(reportPath)) {
24
+ try {
25
+ const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
26
+ return {
27
+ status: report.passed ? 'passing' : 'failing',
28
+ last_validated: report.timestamp || new Date().toISOString(),
29
+ violations: report.violations || 0,
30
+ gates: report.gates || {}
31
+ };
32
+ } catch (error) {
33
+ // Fall through to default
34
+ }
35
+ }
36
+
37
+ return {
38
+ status: 'not_validated',
39
+ last_validated: null,
40
+ violations: null
41
+ };
42
+ }
43
+
13
44
  /**
14
45
  * Provenance command handler
15
46
  * @param {string} subcommand - The subcommand to execute
@@ -87,12 +118,7 @@ async function updateProvenance(options) {
87
118
  mode: spec.mode,
88
119
  waiver_ids: spec.waiver_ids || [],
89
120
  },
90
- quality_gates: {
91
- // This would be populated by recent validation results
92
- // For now, we'll mark as unknown
93
- status: 'unknown',
94
- last_validated: new Date().toISOString(),
95
- },
121
+ quality_gates: getQualityGatesStatus(),
96
122
  agent: {
97
123
  type: detectAgentType(),
98
124
  confidence_level: null, // Would be populated by agent actions
@@ -407,6 +433,67 @@ function analyzeQualityMetrics(aiEntries) {
407
433
  /**
408
434
  * Analyze checkpoint usage patterns
409
435
  */
436
+ /**
437
+ * Calculate actual revert rate from git history
438
+ * Analyzes commits for revert patterns and calculates the percentage
439
+ * @param {number} maxCommits - Maximum number of commits to analyze
440
+ * @returns {number} Revert rate as a decimal (0.0 - 1.0)
441
+ */
442
+ function calculateRevertRate(maxCommits = 500) {
443
+ try {
444
+ // Get total commit count (limited)
445
+ const logOutput = execSync(`git log --oneline -n ${maxCommits} 2>/dev/null`, {
446
+ encoding: 'utf8',
447
+ cwd: process.cwd(),
448
+ }).trim();
449
+
450
+ if (!logOutput) {
451
+ return 0;
452
+ }
453
+
454
+ const totalCommits = logOutput.split('\n').filter(Boolean).length;
455
+
456
+ // Count revert commits (commits with "revert" in the message)
457
+ const revertOutput = execSync(
458
+ `git log --oneline -n ${maxCommits} --grep="[Rr]evert" 2>/dev/null || true`,
459
+ {
460
+ encoding: 'utf8',
461
+ cwd: process.cwd(),
462
+ }
463
+ ).trim();
464
+
465
+ const revertCommits = revertOutput ? revertOutput.split('\n').filter(Boolean).length : 0;
466
+
467
+ // Also check for reset/force-push patterns in reflog if available
468
+ let additionalReverts = 0;
469
+ try {
470
+ const reflogOutput = execSync(`git reflog --oneline -n ${maxCommits} 2>/dev/null || true`, {
471
+ encoding: 'utf8',
472
+ cwd: process.cwd(),
473
+ }).trim();
474
+
475
+ if (reflogOutput) {
476
+ const reflogLines = reflogOutput.split('\n').filter(Boolean);
477
+ additionalReverts = reflogLines.filter(
478
+ (line) => line.includes('reset:') || line.includes('checkout:')
479
+ ).length;
480
+ // Weight reflog reverts less since they may not be actual code reverts
481
+ additionalReverts = Math.floor(additionalReverts * 0.1);
482
+ }
483
+ } catch {
484
+ // Reflog not available or failed
485
+ }
486
+
487
+ const totalReverts = revertCommits + additionalReverts;
488
+ const revertRate = totalCommits > 0 ? totalReverts / totalCommits : 0;
489
+
490
+ return Math.min(1.0, revertRate); // Cap at 100%
491
+ } catch {
492
+ // Git not available or not a repo
493
+ return 0;
494
+ }
495
+ }
496
+
410
497
  function analyzeCheckpointUsage(aiEntries) {
411
498
  const entriesWithCheckpoints = aiEntries.filter(
412
499
  (entry) => entry.checkpoints?.available && entry.checkpoints.checkpoints?.length > 0
@@ -416,8 +503,8 @@ function analyzeCheckpointUsage(aiEntries) {
416
503
  .filter((entry) => entry.checkpoints?.available)
417
504
  .reduce((sum, entry) => sum + (entry.checkpoints.checkpoints?.length || 0), 0);
418
505
 
419
- // Mock revert rate - in real implementation, this would track actual reverts
420
- const revertRate = 0.15; // 15% estimated revert rate
506
+ // Calculate actual revert rate from git history
507
+ const revertRate = calculateRevertRate();
421
508
 
422
509
  return {
423
510
  entriesWithCheckpoints,
@@ -753,57 +840,63 @@ function provideAIInsights(contributionPatterns, qualityMetrics, checkpointAnaly
753
840
 
754
841
  /**
755
842
  * Get Cursor AI code tracking data for a commit
843
+ * Uses Cursor's AI Code Tracking API (Enterprise feature)
844
+ * @see https://cursor.com/docs/account/teams/ai-code-tracking-api
756
845
  * @param {string} commitHash - Git commit hash to analyze
757
846
  * @returns {Promise<Object>} AI code tracking data
758
847
  */
759
848
  async function getCursorTrackingData(commitHash) {
849
+ const apiUrl = process.env.CURSOR_TRACKING_API;
850
+ const apiKey = process.env.CURSOR_API_KEY;
851
+
852
+ if (!apiUrl || !apiKey) {
853
+ return {
854
+ available: false,
855
+ reason: 'Cursor API not configured. Set CURSOR_TRACKING_API and CURSOR_API_KEY environment variables.',
856
+ documentation: 'https://cursor.com/docs/account/teams/ai-code-tracking-api'
857
+ };
858
+ }
859
+
760
860
  try {
761
- // Check if Cursor tracking API is available
762
- if (!process.env.CURSOR_TRACKING_API || !process.env.CURSOR_PROJECT_ID) {
763
- return { available: false, reason: 'Cursor tracking API not configured' };
861
+ // Basic auth: base64(apiKey:) - Cursor API uses API key with empty password
862
+ const auth = Buffer.from(`${apiKey}:`).toString('base64');
863
+
864
+ const response = await fetch(`${apiUrl}/analytics/ai-code/commits`, {
865
+ method: 'GET',
866
+ headers: {
867
+ 'Authorization': `Basic ${auth}`,
868
+ 'Content-Type': 'application/json',
869
+ }
870
+ });
871
+
872
+ if (!response.ok) {
873
+ return {
874
+ available: false,
875
+ reason: `Cursor API error: ${response.status} ${response.statusText}`
876
+ };
764
877
  }
765
878
 
766
- // In a real implementation, this would call the Cursor API
767
- // For now, we'll return a mock structure showing what data would be available
768
- const mockTrackingData = {
879
+ const data = await response.json();
880
+
881
+ // Find commit-specific data if available
882
+ const commitData = data.commits?.find(c => c.commit_hash === commitHash) || data;
883
+
884
+ // Transform API response to our internal format
885
+ return {
769
886
  available: true,
770
887
  commit_hash: commitHash,
771
- ai_code_breakdown: {
772
- tab_completions: {
773
- lines_added: 45,
774
- percentage: 35,
775
- files_affected: ['src/utils.js', 'tests/utils.test.js'],
776
- },
777
- composer_chat: {
778
- lines_added: 78,
779
- percentage: 60,
780
- files_affected: ['src/new-feature.js', 'src/api.js'],
781
- checkpoints_created: 3,
782
- },
783
- manual_human: {
784
- lines_added: 5,
785
- percentage: 5,
786
- files_affected: ['README.md'],
787
- },
888
+ ai_code_breakdown: commitData.ai_code_breakdown || {
889
+ tab_completions: { lines_added: 0, percentage: 0, files_affected: [] },
890
+ composer_chat: { lines_added: 0, percentage: 0, files_affected: [], checkpoints_created: 0 },
891
+ manual_human: { lines_added: 0, percentage: 0, files_affected: [] },
788
892
  },
789
- change_groups: [
790
- {
791
- change_id: 'cg_12345',
792
- type: 'composer_session',
793
- lines_ai_generated: 42,
794
- lines_human_edited: 8,
795
- confidence_score: 0.85,
796
- timestamp: new Date().toISOString(),
797
- },
798
- ],
799
- quality_metrics: {
800
- ai_code_quality_score: 0.78,
801
- human_override_rate: 0.12,
802
- acceptance_rate: 0.94,
893
+ change_groups: commitData.change_groups || [],
894
+ quality_metrics: commitData.quality_metrics || {
895
+ ai_code_quality_score: 0,
896
+ human_override_rate: 0,
897
+ acceptance_rate: 0,
803
898
  },
804
899
  };
805
-
806
- return mockTrackingData;
807
900
  } catch (error) {
808
901
  return {
809
902
  available: false,
@@ -888,62 +981,92 @@ async function initProvenance(options) {
888
981
 
889
982
  /**
890
983
  * Get Cursor Composer/Chat checkpoint data
891
- * @returns {Promise<Array>} Array of checkpoint data
984
+ * Reads from local .cursor/ directory since checkpoints are stored locally by Cursor Agent
985
+ * @returns {Promise<Object>} Checkpoint data
892
986
  */
893
987
  async function getCursorCheckpoints() {
988
+ const cursorDir = path.join(process.cwd(), '.cursor');
989
+
990
+ if (!fs.existsSync(cursorDir)) {
991
+ return {
992
+ available: false,
993
+ reason: 'No .cursor directory found. Checkpoints are only available when using Cursor IDE.'
994
+ };
995
+ }
996
+
894
997
  try {
895
- // Check if Cursor checkpoint API is available
896
- if (!process.env.CURSOR_CHECKPOINT_API) {
897
- return { available: false, reason: 'Cursor checkpoint API not configured' };
998
+ // Look for checkpoint metadata in .cursor directory
999
+ // Cursor stores checkpoints locally during Composer/Agent sessions
1000
+ const checkpointPatterns = [
1001
+ '.cursor/**/checkpoint*.json',
1002
+ '.cursor/**/checkpoints.json',
1003
+ '.cursor/composer/checkpoints/*.json',
1004
+ '.cursor/agent/checkpoints/*.json',
1005
+ ];
1006
+
1007
+ let checkpointFiles = [];
1008
+ for (const pattern of checkpointPatterns) {
1009
+ const glob = require('glob');
1010
+ const matches = glob.sync(pattern, { cwd: process.cwd(), absolute: true });
1011
+ checkpointFiles = checkpointFiles.concat(matches);
898
1012
  }
899
1013
 
900
- // In a real implementation, this would call the Cursor checkpoint API
901
- // For now, we'll return a mock structure
902
- const mockCheckpoints = [
903
- {
904
- id: 'cp_001',
905
- timestamp: new Date(Date.now() - 3600000).toISOString(), // 1 hour ago
906
- description: 'Initial AI-generated function structure',
907
- changes_summary: {
908
- lines_added: 25,
909
- lines_modified: 0,
910
- files_affected: ['src/new-feature.js'],
911
- },
912
- ai_confidence: 0.82,
913
- can_revert: true,
914
- },
915
- {
916
- id: 'cp_002',
917
- timestamp: new Date(Date.now() - 1800000).toISOString(), // 30 min ago
918
- description: 'Added error handling and validation',
919
- changes_summary: {
920
- lines_added: 15,
921
- lines_modified: 8,
922
- files_affected: ['src/new-feature.js', 'tests/new-feature.test.js'],
923
- },
924
- ai_confidence: 0.91,
925
- can_revert: true,
926
- },
927
- {
928
- id: 'cp_003',
929
- timestamp: new Date().toISOString(), // Current
930
- description: 'Final implementation with documentation',
931
- changes_summary: {
932
- lines_added: 12,
933
- lines_modified: 5,
934
- files_affected: ['src/new-feature.js', 'README.md'],
935
- },
936
- ai_confidence: 0.88,
937
- can_revert: false, // Latest checkpoint
938
- },
939
- ];
1014
+ // Remove duplicates
1015
+ checkpointFiles = [...new Set(checkpointFiles)];
1016
+
1017
+ if (checkpointFiles.length === 0) {
1018
+ return {
1019
+ available: false,
1020
+ reason: 'No checkpoints found in current session. Checkpoints are created during Cursor Composer/Agent sessions.'
1021
+ };
1022
+ }
1023
+
1024
+ // Parse checkpoint files and aggregate data
1025
+ const checkpoints = [];
1026
+ for (const file of checkpointFiles) {
1027
+ try {
1028
+ const content = await fs.readFile(file, 'utf8');
1029
+ const data = JSON.parse(content);
1030
+
1031
+ // Handle both single checkpoint and array of checkpoints
1032
+ if (Array.isArray(data)) {
1033
+ checkpoints.push(...data);
1034
+ } else if (data.checkpoints) {
1035
+ checkpoints.push(...data.checkpoints);
1036
+ } else if (data.id || data.timestamp) {
1037
+ checkpoints.push(data);
1038
+ }
1039
+ } catch (parseError) {
1040
+ // Skip invalid checkpoint files
1041
+ continue;
1042
+ }
1043
+ }
1044
+
1045
+ if (checkpoints.length === 0) {
1046
+ return {
1047
+ available: false,
1048
+ reason: 'No valid checkpoints found in checkpoint files.'
1049
+ };
1050
+ }
1051
+
1052
+ // Sort by timestamp (newest first)
1053
+ checkpoints.sort((a, b) => {
1054
+ const timeA = new Date(a.timestamp || 0).getTime();
1055
+ const timeB = new Date(b.timestamp || 0).getTime();
1056
+ return timeB - timeA;
1057
+ });
1058
+
1059
+ // Mark latest checkpoint as non-revertible
1060
+ if (checkpoints.length > 0) {
1061
+ checkpoints[0].can_revert = false;
1062
+ }
940
1063
 
941
- return { available: true, checkpoints: mockCheckpoints };
1064
+ return { available: true, checkpoints };
942
1065
  } catch (error) {
943
1066
  return {
944
1067
  available: false,
945
1068
  error: error.message,
946
- reason: 'Failed to retrieve Cursor checkpoint data',
1069
+ reason: 'Failed to read Cursor checkpoint data',
947
1070
  };
948
1071
  }
949
1072
  }
@@ -1 +1 @@
1
- {"version":3,"file":"quality-gates.d.ts","sourceRoot":"","sources":["../../src/commands/quality-gates.js"],"names":[],"mappings":"AAqBA;;;GAGG;AACH,iEA4ZC"}
1
+ {"version":3,"file":"quality-gates.d.ts","sourceRoot":"","sources":["../../src/commands/quality-gates.js"],"names":[],"mappings":"AAqBA;;;GAGG;AACH,iEA8ZC"}
@@ -339,9 +339,11 @@ async function qualityGatesCommand(options = {}) {
339
339
  Output.info(`Command: ${args.join(' ')}`);
340
340
 
341
341
  // Execute the quality gates runner with timeout
342
+ // CRITICAL: Must run from projectRoot (user's current directory) so that
343
+ // git commands resolve correctly to the user's repository, not the CLI installation
342
344
  const child = spawn(args[0], args.slice(1), {
343
345
  stdio: 'inherit',
344
- cwd: packagesDir,
346
+ cwd: projectRoot,
345
347
  env: env,
346
348
  });
347
349
 
@@ -164,9 +164,9 @@ async function createSpec(id, options = {}) {
164
164
  console.log(chalk.blue(`📝 Creating spec with new name: ${newId}`));
165
165
  return await createSpec(newId, { ...options, interactive: false });
166
166
  } else if (answer === 'merge') {
167
- console.log(chalk.yellow('🔄 Merge functionality not yet implemented.'));
168
- console.log(chalk.blue('💡 For now, consider creating with a different name.'));
169
- return null;
167
+ // Merge new spec data with existing spec
168
+ console.log(chalk.blue('🔄 Merging with existing spec...'));
169
+ return await mergeSpec(id, options);
170
170
  } else if (answer === 'override') {
171
171
  console.log(chalk.yellow('⚠️ Overriding existing spec...'));
172
172
  }
@@ -385,6 +385,133 @@ async function updateSpec(id, updates = {}) {
385
385
  return true;
386
386
  }
387
387
 
388
+ /**
389
+ * Merge new spec data with an existing spec
390
+ * Combines acceptance criteria, updates metadata, preserves history
391
+ * @param {string} id - Spec identifier
392
+ * @param {Object} options - Options including new spec data to merge
393
+ * @returns {Promise<Object>} Merged spec
394
+ */
395
+ async function mergeSpec(id, options = {}) {
396
+ const existingSpec = await loadSpec(id);
397
+ if (!existingSpec) {
398
+ throw new Error(`Spec '${id}' not found`);
399
+ }
400
+
401
+ console.log(chalk.blue(`\n📋 Merging into existing spec: ${id}`));
402
+ console.log(chalk.gray('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
403
+
404
+ // Show existing spec summary
405
+ console.log(chalk.gray(`Existing spec:`));
406
+ console.log(chalk.gray(` Title: ${existingSpec.title}`));
407
+ console.log(chalk.gray(` Status: ${existingSpec.status}`));
408
+ console.log(
409
+ chalk.gray(` Acceptance Criteria: ${existingSpec.acceptance_criteria?.length || 0}`)
410
+ );
411
+ console.log('');
412
+
413
+ // Prepare merge data from options
414
+ const {
415
+ title: newTitle,
416
+ description: newDescription,
417
+ acceptance_criteria: newCriteria,
418
+ mode: newMode,
419
+ risk_tier: newRiskTier,
420
+ } = options;
421
+
422
+ const mergedSpec = { ...existingSpec };
423
+
424
+ // Track what was merged
425
+ const mergeLog = [];
426
+
427
+ // Merge title (prefer new if provided)
428
+ if (newTitle && newTitle !== existingSpec.title) {
429
+ mergedSpec.title = newTitle;
430
+ mergeLog.push(`Title updated: "${existingSpec.title}" → "${newTitle}"`);
431
+ }
432
+
433
+ // Merge description
434
+ if (newDescription) {
435
+ if (existingSpec.description) {
436
+ mergedSpec.description = `${existingSpec.description}\n\n---\n\n${newDescription}`;
437
+ mergeLog.push('Description appended');
438
+ } else {
439
+ mergedSpec.description = newDescription;
440
+ mergeLog.push('Description added');
441
+ }
442
+ }
443
+
444
+ // Merge acceptance criteria (append new ones, avoid duplicates)
445
+ if (newCriteria && Array.isArray(newCriteria) && newCriteria.length > 0) {
446
+ const existingCriteria = existingSpec.acceptance_criteria || [];
447
+ const existingIds = new Set(existingCriteria.map((c) => c.id));
448
+
449
+ const criteriaToAdd = newCriteria.filter((c) => !existingIds.has(c.id));
450
+ if (criteriaToAdd.length > 0) {
451
+ mergedSpec.acceptance_criteria = [...existingCriteria, ...criteriaToAdd];
452
+ mergeLog.push(`Added ${criteriaToAdd.length} new acceptance criteria`);
453
+ }
454
+
455
+ // Also update the 'acceptance' array if it exists
456
+ if (existingSpec.acceptance) {
457
+ const existingAcceptIds = new Set(existingSpec.acceptance.map((a) => a.id));
458
+ const acceptToAdd = newCriteria.filter((c) => !existingAcceptIds.has(c.id));
459
+ if (acceptToAdd.length > 0) {
460
+ mergedSpec.acceptance = [...existingSpec.acceptance, ...acceptToAdd];
461
+ }
462
+ }
463
+ }
464
+
465
+ // Merge mode (prefer higher tier if both provided)
466
+ if (newMode && newMode !== existingSpec.mode) {
467
+ // Mode priority: crisis > standard > minimal
468
+ const modePriority = { minimal: 1, standard: 2, crisis: 3 };
469
+ if ((modePriority[newMode] || 0) > (modePriority[existingSpec.mode] || 0)) {
470
+ mergedSpec.mode = newMode;
471
+ mergeLog.push(`Mode upgraded: ${existingSpec.mode} → ${newMode}`);
472
+ }
473
+ }
474
+
475
+ // Merge risk tier (prefer higher risk if both provided)
476
+ if (newRiskTier && newRiskTier !== existingSpec.risk_tier) {
477
+ // Risk priority: T1 > T2 > T3
478
+ const riskPriority = { T3: 1, T2: 2, T1: 3, 3: 1, 2: 2, 1: 3 };
479
+ if ((riskPriority[newRiskTier] || 0) > (riskPriority[existingSpec.risk_tier] || 0)) {
480
+ mergedSpec.risk_tier = newRiskTier;
481
+ mergeLog.push(`Risk tier updated: ${existingSpec.risk_tier} → ${newRiskTier}`);
482
+ }
483
+ }
484
+
485
+ // Update metadata
486
+ mergedSpec.updated_at = new Date().toISOString();
487
+
488
+ // Add merge history entry
489
+ if (!mergedSpec.history) {
490
+ mergedSpec.history = [];
491
+ }
492
+ mergedSpec.history.push({
493
+ action: 'merge',
494
+ timestamp: new Date().toISOString(),
495
+ changes: mergeLog,
496
+ });
497
+
498
+ // Save merged spec
499
+ await updateSpec(id, mergedSpec);
500
+
501
+ // Display merge results
502
+ console.log(chalk.green('✅ Merge completed:'));
503
+ if (mergeLog.length > 0) {
504
+ mergeLog.forEach((change) => {
505
+ console.log(chalk.gray(` • ${change}`));
506
+ });
507
+ } else {
508
+ console.log(chalk.gray(' • No changes needed (specs were identical)'));
509
+ }
510
+ console.log('');
511
+
512
+ return mergedSpec;
513
+ }
514
+
388
515
  /**
389
516
  * Delete a spec file
390
517
  * @param {string} id - Spec identifier
@@ -544,9 +671,12 @@ async function migrateFromLegacy(options = {}, createSpecFn = createSpec) {
544
671
  let selectedFeatures = features;
545
672
 
546
673
  if (options.interactive) {
547
- // For now, just use all suggested features
548
- // In a full implementation, this would prompt for selection
549
- console.log(chalk.blue('\n📋 Using all suggested features for migration'));
674
+ selectedFeatures = await selectFeaturesInteractively(features);
675
+ if (selectedFeatures.length === 0) {
676
+ console.log(chalk.yellow('⚠️ No features selected. Migration cancelled.'));
677
+ return { migrated: 0, total: features.length, createdSpecs: [], legacySpec: legacySpec.id };
678
+ }
679
+ console.log(chalk.blue(`\n📋 Migrating ${selectedFeatures.length} selected features`));
550
680
  }
551
681
 
552
682
  if (options.features && options.features.length > 0) {
@@ -614,6 +744,54 @@ async function migrateFromLegacy(options = {}, createSpecFn = createSpec) {
614
744
  };
615
745
  }
616
746
 
747
+ /**
748
+ * Interactive feature selection for migration
749
+ * @param {Array} features - Array of suggested features
750
+ * @returns {Promise<Array>} Selected features
751
+ */
752
+ async function selectFeaturesInteractively(features) {
753
+ const readline = require('readline');
754
+ const rl = readline.createInterface({
755
+ input: process.stdin,
756
+ output: process.stdout,
757
+ });
758
+
759
+ console.log(chalk.cyan('\n📋 Select features to migrate:\n'));
760
+ features.forEach((f, i) => {
761
+ const scope = f.scope?.in?.join(', ') || 'N/A';
762
+ console.log(` ${chalk.yellow(i + 1)}. ${chalk.bold(f.id || f.name)} - ${f.title || f.description}`);
763
+ console.log(chalk.gray(` Scope: ${scope}`));
764
+ });
765
+ console.log(chalk.cyan(`\nEnter numbers separated by commas, or 'all' for all features:`));
766
+ console.log(chalk.gray(`Example: 1,3,5 or all`));
767
+
768
+ try {
769
+ const answer = await question(rl, '> ');
770
+ const trimmed = answer.trim().toLowerCase();
771
+
772
+ if (trimmed === 'all' || trimmed === '*') {
773
+ return features;
774
+ }
775
+
776
+ if (trimmed === '' || trimmed === 'none' || trimmed === 'q' || trimmed === 'quit') {
777
+ return [];
778
+ }
779
+
780
+ // Parse comma-separated numbers
781
+ const indices = trimmed
782
+ .split(',')
783
+ .map(n => parseInt(n.trim(), 10) - 1)
784
+ .filter(i => !isNaN(i) && i >= 0 && i < features.length);
785
+
786
+ // Remove duplicates and sort
787
+ const uniqueIndices = [...new Set(indices)].sort((a, b) => a - b);
788
+
789
+ return features.filter((_, i) => uniqueIndices.includes(i));
790
+ } finally {
791
+ await closeReadline(rl);
792
+ }
793
+ }
794
+
617
795
  /**
618
796
  * Ask user how to resolve spec creation conflicts
619
797
  * @returns {Promise<string>} User's choice: 'cancel', 'rename', 'merge', 'override'
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.js"],"names":[],"mappings":"AA8uBA;;;GAGG;AACH,2DA+IC;AAp3BD;;;;GAIG;AACH,2CAHW,MAAM,GACJ,OAAO,CAAC,MAAO,IAAI,CAAC,CAahC;AAgBD;;;GAGG;AACH,iCAFa,OAAO,KAAQ,CAgC3B;AAED;;;GAGG;AACH,uCAFa,OAAO,KAAQ,CA+B3B;AAED;;;GAGG;AACH,oCAFa,OAAO,KAAQ,CA0D3B;AAED;;;GAGG;AACH,qCAFa,OAAO,KAAQ,CAS3B;AAwBD;;;GAGG;AACH,+CAgGC;AAED;;;;;GAKG;AACH,4DAHW,MAAM,GACJ,MAAM,EAAE,CAoCpB"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.js"],"names":[],"mappings":"AA02BA;;;GAGG;AACH,2DA+IC;AA/+BD;;;;GAIG;AACH,2CAHW,MAAM,GACJ,OAAO,CAAC,MAAO,IAAI,CAAC,CAahC;AAgBD;;;GAGG;AACH,iCAFa,OAAO,KAAQ,CAgC3B;AAED;;;GAGG;AACH,uCAFa,OAAO,KAAQ,CA+B3B;AAED;;;GAGG;AACH,oCAFa,OAAO,KAAQ,CA0D3B;AAED;;;GAGG;AACH,qCAFa,OAAO,KAAQ,CAO3B;AA8HD;;;GAGG;AACH,+CAgGC;AAED;;;;;GAKG;AACH,4DAHW,MAAM,GACJ,MAAM,EAAE,CAoCpB"}