@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.
- package/dist/commands/archive.d.ts +2 -1
- package/dist/commands/archive.d.ts.map +1 -1
- package/dist/commands/archive.js +114 -6
- package/dist/commands/burnup.d.ts.map +1 -1
- package/dist/commands/burnup.js +109 -10
- package/dist/commands/diagnose.js +1 -1
- package/dist/commands/mode.js +24 -14
- package/dist/commands/provenance.js +216 -93
- package/dist/commands/quality-gates.d.ts.map +1 -1
- package/dist/commands/quality-gates.js +3 -1
- package/dist/commands/specs.js +184 -6
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +134 -10
- package/dist/commands/templates.js +2 -2
- package/dist/error-handler.js +6 -98
- package/dist/generators/jest-config-generator.js +242 -0
- package/dist/index.js +4 -7
- package/dist/minimal-cli.js +3 -1
- package/dist/scaffold/claude-hooks.js +316 -0
- package/dist/scaffold/index.js +18 -0
- package/dist/templates/.claude/README.md +190 -0
- package/dist/templates/.claude/hooks/audit.sh +96 -0
- package/dist/templates/.claude/hooks/block-dangerous.sh +90 -0
- package/dist/templates/.claude/hooks/naming-check.sh +97 -0
- package/dist/templates/.claude/hooks/quality-check.sh +68 -0
- package/dist/templates/.claude/hooks/scan-secrets.sh +85 -0
- package/dist/templates/.claude/hooks/scope-guard.sh +105 -0
- package/dist/templates/.claude/hooks/validate-spec.sh +76 -0
- package/dist/templates/.claude/settings.json +95 -0
- package/dist/test-analysis.js +203 -10
- package/dist/utils/error-categories.js +210 -0
- package/dist/utils/quality-gates-utils.js +402 -0
- package/dist/utils/typescript-detector.js +36 -90
- package/dist/validation/spec-validation.js +59 -6
- package/package.json +5 -3
- package/templates/.claude/README.md +190 -0
- package/templates/.claude/hooks/audit.sh +96 -0
- package/templates/.claude/hooks/block-dangerous.sh +90 -0
- package/templates/.claude/hooks/naming-check.sh +97 -0
- package/templates/.claude/hooks/quality-check.sh +68 -0
- package/templates/.claude/hooks/scan-secrets.sh +85 -0
- package/templates/.claude/hooks/scope-guard.sh +105 -0
- package/templates/.claude/hooks/validate-spec.sh +76 -0
- 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
|
-
//
|
|
420
|
-
const revertRate =
|
|
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
|
-
//
|
|
762
|
-
|
|
763
|
-
|
|
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
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
774
|
-
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
*
|
|
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
|
-
//
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
//
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
|
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
|
|
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,
|
|
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:
|
|
346
|
+
cwd: projectRoot,
|
|
345
347
|
env: env,
|
|
346
348
|
});
|
|
347
349
|
|
package/dist/commands/specs.js
CHANGED
|
@@ -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
|
-
|
|
168
|
-
console.log(chalk.blue('
|
|
169
|
-
return
|
|
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
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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":"
|
|
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"}
|