@paths.design/caws-cli 4.0.0 → 5.0.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.js +353 -0
- package/dist/commands/iterate.js +12 -13
- package/dist/commands/mode.js +259 -0
- package/dist/commands/plan.js +448 -0
- package/dist/commands/quality-gates.js +490 -0
- package/dist/commands/specs.js +735 -0
- package/dist/commands/status.js +552 -22
- package/dist/commands/tutorial.js +481 -0
- package/dist/commands/validate.js +137 -54
- package/dist/commands/waivers.js +101 -26
- package/dist/config/modes.js +321 -0
- package/dist/constants/spec-types.js +42 -0
- package/dist/index.js +225 -10
- package/dist/scaffold/git-hooks.js +32 -44
- package/dist/scaffold/index.js +19 -0
- package/dist/utils/quality-gates-errors.js +520 -0
- package/dist/utils/quality-gates.js +361 -0
- package/dist/utils/spec-resolver.js +602 -0
- package/dist/waivers-manager.js +49 -4
- package/package.json +6 -5
- package/templates/.cursor/hooks/caws-scope-guard.sh +64 -8
- package/templates/.cursor/hooks/validate-spec.sh +22 -12
- package/templates/.cursor/rules/{01-claims-verification.mdc → 00-claims-verification.mdc} +1 -1
- package/templates/.cursor/rules/01-working-style.mdc +50 -0
- package/templates/.cursor/rules/{02-testing-standards.mdc → 02-quality-gates.mdc} +84 -29
- package/templates/.cursor/rules/03-naming-and-refactor.mdc +33 -0
- package/templates/.cursor/rules/04-logging-language-style.mdc +23 -0
- package/templates/.cursor/rules/05-safe-defaults-guards.mdc +23 -0
- package/templates/.cursor/rules/06-typescript-conventions.mdc +36 -0
- package/templates/.cursor/rules/07-process-ops.mdc +20 -0
- package/templates/.cursor/rules/08-solid-and-architecture.mdc +16 -0
- package/templates/.cursor/rules/09-docstrings.mdc +89 -0
- package/templates/.cursor/rules/10-authorship-and-attribution.mdc +15 -0
- package/templates/.cursor/rules/11-documentation-quality-standards.mdc +390 -0
- package/templates/.cursor/rules/12-scope-management-waivers.mdc +385 -0
- package/templates/.cursor/rules/13-implementation-completeness.mdc +516 -0
- package/templates/.cursor/rules/14-language-agnostic-standards.mdc +588 -0
- package/templates/.cursor/rules/15-sophisticated-todo-detection.mdc +425 -0
- package/templates/.cursor/rules/README.md +93 -7
- package/templates/scripts/quality-gates/check-god-objects.js +146 -0
- package/templates/scripts/quality-gates/run-quality-gates.js +50 -0
- package/templates/scripts/v3/analysis/todo_analyzer.py +1950 -0
- package/dist/budget-derivation.d.ts +0 -74
- package/dist/budget-derivation.d.ts.map +0 -1
- package/dist/cicd-optimizer.d.ts +0 -142
- package/dist/cicd-optimizer.d.ts.map +0 -1
- package/dist/commands/burnup.d.ts +0 -6
- package/dist/commands/burnup.d.ts.map +0 -1
- package/dist/commands/diagnose.d.ts +0 -52
- package/dist/commands/diagnose.d.ts.map +0 -1
- package/dist/commands/evaluate.d.ts +0 -8
- package/dist/commands/evaluate.d.ts.map +0 -1
- package/dist/commands/init.d.ts +0 -5
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/iterate.d.ts +0 -8
- package/dist/commands/iterate.d.ts.map +0 -1
- package/dist/commands/provenance.d.ts +0 -32
- package/dist/commands/provenance.d.ts.map +0 -1
- package/dist/commands/quality-monitor.d.ts +0 -17
- package/dist/commands/quality-monitor.d.ts.map +0 -1
- package/dist/commands/status.d.ts +0 -43
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/templates.d.ts +0 -74
- package/dist/commands/templates.d.ts.map +0 -1
- package/dist/commands/tool.d.ts +0 -13
- package/dist/commands/tool.d.ts.map +0 -1
- package/dist/commands/troubleshoot.d.ts +0 -8
- package/dist/commands/troubleshoot.d.ts.map +0 -1
- package/dist/commands/validate.d.ts +0 -8
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/waivers.d.ts +0 -8
- package/dist/commands/waivers.d.ts.map +0 -1
- package/dist/commands/workflow.d.ts +0 -85
- package/dist/commands/workflow.d.ts.map +0 -1
- package/dist/config/index.d.ts +0 -29
- package/dist/config/index.d.ts.map +0 -1
- package/dist/error-handler.d.ts +0 -164
- package/dist/error-handler.d.ts.map +0 -1
- package/dist/generators/jest-config.d.ts +0 -32
- package/dist/generators/jest-config.d.ts.map +0 -1
- package/dist/generators/working-spec.d.ts +0 -13
- package/dist/generators/working-spec.d.ts.map +0 -1
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/minimal-cli.d.ts +0 -3
- package/dist/minimal-cli.d.ts.map +0 -1
- package/dist/policy/PolicyManager.d.ts +0 -104
- package/dist/policy/PolicyManager.d.ts.map +0 -1
- package/dist/scaffold/cursor-hooks.d.ts +0 -7
- package/dist/scaffold/cursor-hooks.d.ts.map +0 -1
- package/dist/scaffold/git-hooks.d.ts +0 -20
- package/dist/scaffold/git-hooks.d.ts.map +0 -1
- package/dist/scaffold/index.d.ts +0 -20
- package/dist/scaffold/index.d.ts.map +0 -1
- package/dist/spec/SpecFileManager.d.ts +0 -146
- package/dist/spec/SpecFileManager.d.ts.map +0 -1
- package/dist/test-analysis.d.ts +0 -182
- package/dist/test-analysis.d.ts.map +0 -1
- package/dist/tool-interface.d.ts +0 -236
- package/dist/tool-interface.d.ts.map +0 -1
- package/dist/tool-loader.d.ts +0 -77
- package/dist/tool-loader.d.ts.map +0 -1
- package/dist/tool-validator.d.ts +0 -72
- package/dist/tool-validator.d.ts.map +0 -1
- package/dist/utils/detection.d.ts +0 -7
- package/dist/utils/detection.d.ts.map +0 -1
- package/dist/utils/finalization.d.ts +0 -17
- package/dist/utils/finalization.d.ts.map +0 -1
- package/dist/utils/project-analysis.d.ts +0 -14
- package/dist/utils/project-analysis.d.ts.map +0 -1
- package/dist/utils/typescript-detector.d.ts +0 -63
- package/dist/utils/typescript-detector.d.ts.map +0 -1
- package/dist/validation/spec-validation.d.ts +0 -43
- package/dist/validation/spec-validation.d.ts.map +0 -1
- package/dist/waivers-manager.d.ts +0 -167
- package/dist/waivers-manager.d.ts.map +0 -1
- package/templates/.cursor/rules/03-infrastructure-standards.mdc +0 -251
- package/templates/.cursor/rules/04-documentation-integrity.mdc +0 -291
- package/templates/.cursor/rules/05-production-readiness-checklist.mdc +0 -214
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CAWS Specs Command
|
|
3
|
+
* Manage multiple spec files for better organization and discoverability
|
|
4
|
+
* @author @darianrosebrook
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs-extra');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const yaml = require('js-yaml');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const { safeAsync, outputResult } = require('../error-handler');
|
|
12
|
+
const { SPEC_TYPES } = require('../constants/spec-types');
|
|
13
|
+
|
|
14
|
+
// Import suggestFeatureBreakdown from spec-resolver
|
|
15
|
+
const { suggestFeatureBreakdown } = require('../utils/spec-resolver');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Specs directory structure
|
|
19
|
+
*/
|
|
20
|
+
const SPECS_DIR = '.caws/specs';
|
|
21
|
+
const SPECS_REGISTRY = '.caws/specs/registry.json';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Load specs registry
|
|
25
|
+
* @returns {Promise<Object>} Registry data
|
|
26
|
+
*/
|
|
27
|
+
async function loadSpecsRegistry() {
|
|
28
|
+
try {
|
|
29
|
+
if (!(await fs.pathExists(SPECS_REGISTRY))) {
|
|
30
|
+
return {
|
|
31
|
+
version: '1.0.0',
|
|
32
|
+
specs: {},
|
|
33
|
+
lastUpdated: new Date().toISOString(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const registry = JSON.parse(await fs.readFile(SPECS_REGISTRY, 'utf8'));
|
|
38
|
+
return registry;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
return {
|
|
41
|
+
version: '1.0.0',
|
|
42
|
+
specs: {},
|
|
43
|
+
lastUpdated: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Save specs registry
|
|
50
|
+
* @param {Object} registry - Registry data
|
|
51
|
+
* @returns {Promise<void>}
|
|
52
|
+
*/
|
|
53
|
+
async function saveSpecsRegistry(registry) {
|
|
54
|
+
await fs.ensureDir(path.dirname(SPECS_REGISTRY));
|
|
55
|
+
registry.lastUpdated = new Date().toISOString();
|
|
56
|
+
await fs.writeFile(SPECS_REGISTRY, JSON.stringify(registry, null, 2));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* List all spec files in the specs directory
|
|
61
|
+
* @returns {Promise<Array>} Array of spec file info
|
|
62
|
+
*/
|
|
63
|
+
async function listSpecFiles() {
|
|
64
|
+
if (!(await fs.pathExists(SPECS_DIR))) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const files = await fs.readdir(SPECS_DIR, { recursive: true });
|
|
69
|
+
const yamlFiles = files.filter((file) => file.endsWith('.yaml') || file.endsWith('.yml'));
|
|
70
|
+
|
|
71
|
+
const specs = [];
|
|
72
|
+
for (const file of yamlFiles) {
|
|
73
|
+
const filePath = path.join(SPECS_DIR, file);
|
|
74
|
+
try {
|
|
75
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
76
|
+
const spec = yaml.load(content);
|
|
77
|
+
|
|
78
|
+
specs.push({
|
|
79
|
+
id: spec.id || path.basename(file, path.extname(file)),
|
|
80
|
+
path: file,
|
|
81
|
+
type: spec.type || 'feature',
|
|
82
|
+
title: spec.title || 'Untitled',
|
|
83
|
+
status: spec.status || 'draft',
|
|
84
|
+
risk_tier: spec.risk_tier || 'T3',
|
|
85
|
+
mode: spec.mode || 'development',
|
|
86
|
+
created_at: spec.created_at || new Date().toISOString(),
|
|
87
|
+
updated_at: spec.updated_at || new Date().toISOString(),
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Skip invalid spec files
|
|
91
|
+
console.warn(`Warning: Could not parse spec file ${file}: ${error.message}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return specs;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Create a new spec file
|
|
100
|
+
* @param {string} id - Spec identifier
|
|
101
|
+
* @param {Object} options - Creation options
|
|
102
|
+
* @returns {Promise<Object>} Created spec info
|
|
103
|
+
*/
|
|
104
|
+
async function createSpec(id, options = {}) {
|
|
105
|
+
const {
|
|
106
|
+
type = 'feature',
|
|
107
|
+
title = `New ${type}`,
|
|
108
|
+
risk_tier = 3, // Default to numeric 3 (low-risk)
|
|
109
|
+
mode = 'development',
|
|
110
|
+
template = null,
|
|
111
|
+
force = false, // Override existing specs
|
|
112
|
+
interactive = false, // Ask for confirmation on conflicts
|
|
113
|
+
} = options;
|
|
114
|
+
|
|
115
|
+
// Convert string tiers to numeric (handle both 'T3' and 3)
|
|
116
|
+
let numericRiskTier = risk_tier;
|
|
117
|
+
if (typeof risk_tier === 'string') {
|
|
118
|
+
const tierMap = { T1: 1, T2: 2, T3: 3 };
|
|
119
|
+
numericRiskTier = tierMap[risk_tier] || 3; // Default to 3 if invalid
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check for existing spec
|
|
123
|
+
const existingSpecPath = path.join(SPECS_DIR, `${id}.yaml`);
|
|
124
|
+
const specExists = await fs.pathExists(existingSpecPath);
|
|
125
|
+
|
|
126
|
+
// Handle conflict resolution
|
|
127
|
+
let answer = null;
|
|
128
|
+
|
|
129
|
+
if (specExists && !force) {
|
|
130
|
+
if (interactive) {
|
|
131
|
+
console.log(chalk.yellow(`⚠️ Spec '${id}' already exists.`));
|
|
132
|
+
console.log(chalk.gray(` Path: ${existingSpecPath}`));
|
|
133
|
+
|
|
134
|
+
// Load existing spec to show details
|
|
135
|
+
try {
|
|
136
|
+
const existingContent = await fs.readFile(existingSpecPath, 'utf8');
|
|
137
|
+
const existingSpec = yaml.load(existingContent);
|
|
138
|
+
console.log(chalk.gray(` Title: ${existingSpec.title || 'Untitled'}`));
|
|
139
|
+
console.log(chalk.gray(` Status: ${existingSpec.status || 'draft'}`));
|
|
140
|
+
console.log(
|
|
141
|
+
chalk.gray(
|
|
142
|
+
` Created: ${new Date(existingSpec.created_at || Date.now()).toLocaleDateString()}`
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.log(chalk.gray(` (Could not load existing spec details)`));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Ask for conflict resolution
|
|
150
|
+
answer = await askConflictResolution();
|
|
151
|
+
|
|
152
|
+
if (answer === 'cancel') {
|
|
153
|
+
console.log(chalk.blue('ℹ️ Spec creation canceled.'));
|
|
154
|
+
return null;
|
|
155
|
+
} else if (answer === 'rename') {
|
|
156
|
+
// Generate new name with timestamp suffix
|
|
157
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
|
158
|
+
const newId = `${id}-${timestamp}`;
|
|
159
|
+
console.log(chalk.blue(`📝 Creating spec with new name: ${newId}`));
|
|
160
|
+
return await createSpec(newId, { ...options, interactive: false });
|
|
161
|
+
} else if (answer === 'merge') {
|
|
162
|
+
console.log(chalk.yellow('🔄 Merge functionality not yet implemented.'));
|
|
163
|
+
console.log(chalk.blue('💡 For now, consider creating with a different name.'));
|
|
164
|
+
return null;
|
|
165
|
+
} else if (answer === 'override') {
|
|
166
|
+
console.log(chalk.yellow('⚠️ Overriding existing spec...'));
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
console.error(chalk.red(`❌ Spec '${id}' already exists.`));
|
|
170
|
+
console.error(
|
|
171
|
+
chalk.yellow('💡 Use --force to override, or --interactive for conflict resolution.')
|
|
172
|
+
);
|
|
173
|
+
throw new Error(`Spec '${id}' already exists. Use --force to override.`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// If we got here via override choice, proceed with creation
|
|
178
|
+
if (specExists && (force || answer === 'override')) {
|
|
179
|
+
console.log(chalk.yellow('⚠️ Overriding existing spec...'));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Ensure specs directory exists
|
|
183
|
+
await fs.ensureDir(SPECS_DIR);
|
|
184
|
+
|
|
185
|
+
// Generate spec content
|
|
186
|
+
const specContent = {
|
|
187
|
+
id,
|
|
188
|
+
type,
|
|
189
|
+
title,
|
|
190
|
+
status: 'draft',
|
|
191
|
+
risk_tier: numericRiskTier,
|
|
192
|
+
mode,
|
|
193
|
+
created_at: new Date().toISOString(),
|
|
194
|
+
updated_at: new Date().toISOString(),
|
|
195
|
+
acceptance_criteria: [],
|
|
196
|
+
...(template || {}),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// Create file path
|
|
200
|
+
const fileName = `${id}.yaml`;
|
|
201
|
+
const filePath = path.join(SPECS_DIR, fileName);
|
|
202
|
+
|
|
203
|
+
// Write spec file
|
|
204
|
+
await fs.writeFile(filePath, yaml.dump(specContent, { indent: 2 }));
|
|
205
|
+
|
|
206
|
+
// Update registry
|
|
207
|
+
const registry = await loadSpecsRegistry();
|
|
208
|
+
registry.specs[id] = {
|
|
209
|
+
path: fileName,
|
|
210
|
+
type,
|
|
211
|
+
status: 'draft',
|
|
212
|
+
created_at: specContent.created_at,
|
|
213
|
+
updated_at: specContent.updated_at,
|
|
214
|
+
};
|
|
215
|
+
await saveSpecsRegistry(registry);
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
id,
|
|
219
|
+
path: fileName,
|
|
220
|
+
type,
|
|
221
|
+
title,
|
|
222
|
+
status: 'draft',
|
|
223
|
+
risk_tier: numericRiskTier,
|
|
224
|
+
mode,
|
|
225
|
+
created_at: specContent.created_at,
|
|
226
|
+
updated_at: specContent.updated_at,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Load a specific spec file
|
|
232
|
+
* @param {string} id - Spec identifier
|
|
233
|
+
* @returns {Promise<Object|null>} Spec data or null
|
|
234
|
+
*/
|
|
235
|
+
async function loadSpec(id) {
|
|
236
|
+
const registry = await loadSpecsRegistry();
|
|
237
|
+
|
|
238
|
+
if (!registry.specs[id]) {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const specPath = path.join(SPECS_DIR, registry.specs[id].path);
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const content = await fs.readFile(specPath, 'utf8');
|
|
246
|
+
return yaml.load(content);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Update a spec file
|
|
254
|
+
* @param {string} id - Spec identifier
|
|
255
|
+
* @param {Object} updates - Updates to apply
|
|
256
|
+
* @returns {Promise<boolean>} Success status
|
|
257
|
+
*/
|
|
258
|
+
async function updateSpec(id, updates = {}) {
|
|
259
|
+
const spec = await loadSpec(id);
|
|
260
|
+
|
|
261
|
+
if (!spec) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Apply updates
|
|
266
|
+
const updatedSpec = {
|
|
267
|
+
...spec,
|
|
268
|
+
...updates,
|
|
269
|
+
updated_at: new Date().toISOString(),
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Update registry
|
|
273
|
+
const registry = await loadSpecsRegistry();
|
|
274
|
+
registry.specs[id].updated_at = updatedSpec.updated_at;
|
|
275
|
+
if (updates.status) {
|
|
276
|
+
registry.specs[id].status = updates.status;
|
|
277
|
+
}
|
|
278
|
+
await saveSpecsRegistry(registry);
|
|
279
|
+
|
|
280
|
+
// Write back to file
|
|
281
|
+
const specPath = path.join(SPECS_DIR, registry.specs[id].path);
|
|
282
|
+
await fs.writeFile(specPath, yaml.dump(updatedSpec, { indent: 2 }));
|
|
283
|
+
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Delete a spec file
|
|
289
|
+
* @param {string} id - Spec identifier
|
|
290
|
+
* @returns {Promise<boolean>} Success status
|
|
291
|
+
*/
|
|
292
|
+
async function deleteSpec(id) {
|
|
293
|
+
const registry = await loadSpecsRegistry();
|
|
294
|
+
|
|
295
|
+
if (!registry.specs[id]) {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const specPath = path.join(SPECS_DIR, registry.specs[id].path);
|
|
300
|
+
|
|
301
|
+
// Remove file
|
|
302
|
+
await fs.remove(specPath);
|
|
303
|
+
|
|
304
|
+
// Update registry
|
|
305
|
+
delete registry.specs[id];
|
|
306
|
+
await saveSpecsRegistry(registry);
|
|
307
|
+
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Display specs in a formatted table
|
|
313
|
+
* @param {Array} specs - Array of spec objects
|
|
314
|
+
*/
|
|
315
|
+
function displaySpecsTable(specs) {
|
|
316
|
+
console.log(chalk.bold.cyan('\n📋 CAWS Specs'));
|
|
317
|
+
console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
|
|
318
|
+
|
|
319
|
+
if (specs.length === 0) {
|
|
320
|
+
console.log(chalk.gray(' No specs found. Create one with: caws specs create <id>'));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Header
|
|
325
|
+
console.log(chalk.bold('ID'.padEnd(15) + 'Type'.padEnd(10) + 'Status'.padEnd(12) + 'Title'));
|
|
326
|
+
console.log(chalk.gray('─'.repeat(80)));
|
|
327
|
+
|
|
328
|
+
// Sort specs by type and status priority
|
|
329
|
+
const statusPriority = { active: 0, draft: 1, completed: 2, archived: 3 };
|
|
330
|
+
const sortedSpecs = specs.sort((a, b) => {
|
|
331
|
+
const typeDiff = a.type.localeCompare(b.type);
|
|
332
|
+
if (typeDiff !== 0) return typeDiff;
|
|
333
|
+
return (statusPriority[a.status] || 999) - (statusPriority[b.status] || 999);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
sortedSpecs.forEach((spec) => {
|
|
337
|
+
const specType = SPEC_TYPES[spec.type] || SPEC_TYPES.feature;
|
|
338
|
+
const typeColor = specType.color;
|
|
339
|
+
|
|
340
|
+
const statusColor =
|
|
341
|
+
spec.status === 'active'
|
|
342
|
+
? chalk.green
|
|
343
|
+
: spec.status === 'draft'
|
|
344
|
+
? chalk.yellow
|
|
345
|
+
: spec.status === 'completed'
|
|
346
|
+
? chalk.blue
|
|
347
|
+
: chalk.gray;
|
|
348
|
+
|
|
349
|
+
console.log(
|
|
350
|
+
spec.id.padEnd(15) +
|
|
351
|
+
typeColor(spec.type.padEnd(9)) +
|
|
352
|
+
statusColor(spec.status.padEnd(11)) +
|
|
353
|
+
chalk.white(spec.title)
|
|
354
|
+
);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
console.log('');
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Display detailed spec information
|
|
362
|
+
* @param {Object} spec - Spec object
|
|
363
|
+
*/
|
|
364
|
+
function displaySpecDetails(spec) {
|
|
365
|
+
const specType = SPEC_TYPES[spec.type] || SPEC_TYPES.feature;
|
|
366
|
+
const typeColor = specType.color;
|
|
367
|
+
|
|
368
|
+
console.log(chalk.bold.cyan(`\n📋 Spec Details: ${spec.id}`));
|
|
369
|
+
console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
|
|
370
|
+
|
|
371
|
+
console.log(`${specType.icon} ${typeColor(spec.type.toUpperCase())} - ${spec.title}`);
|
|
372
|
+
console.log(
|
|
373
|
+
chalk.gray(` Status: ${spec.status} | Risk Tier: ${spec.risk_tier} | Mode: ${spec.mode}`)
|
|
374
|
+
);
|
|
375
|
+
console.log(chalk.gray(` Created: ${new Date(spec.created_at).toLocaleDateString()}`));
|
|
376
|
+
console.log(chalk.gray(` Updated: ${new Date(spec.updated_at).toLocaleDateString()}`));
|
|
377
|
+
|
|
378
|
+
if (spec.description) {
|
|
379
|
+
console.log(chalk.gray(`\n Description: ${spec.description}`));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (spec.acceptance_criteria && spec.acceptance_criteria.length > 0) {
|
|
383
|
+
console.log(chalk.gray(`\n Acceptance Criteria (${spec.acceptance_criteria.length}):`));
|
|
384
|
+
spec.acceptance_criteria.forEach((criterion, index) => {
|
|
385
|
+
const status = criterion.completed ? chalk.green('✓') : chalk.red('○');
|
|
386
|
+
console.log(
|
|
387
|
+
chalk.gray(` ${status} ${criterion.description || criterion.title || `A${index + 1}`}`)
|
|
388
|
+
);
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (spec.contracts && spec.contracts.length > 0) {
|
|
393
|
+
console.log(chalk.gray(`\n Contracts (${spec.contracts.length}):`));
|
|
394
|
+
spec.contracts.forEach((contract) => {
|
|
395
|
+
console.log(chalk.gray(` 📄 ${contract.type}: ${contract.path}`));
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
console.log('');
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Migrate from legacy working-spec.yaml to feature-specific specs
|
|
404
|
+
* @param {Object} options - Migration options
|
|
405
|
+
* @returns {Promise<Object>} Migration result
|
|
406
|
+
*/
|
|
407
|
+
async function migrateFromLegacy(options = {}) {
|
|
408
|
+
const fs = require('fs-extra');
|
|
409
|
+
const path = require('path');
|
|
410
|
+
const yaml = require('js-yaml');
|
|
411
|
+
const chalk = require('chalk');
|
|
412
|
+
|
|
413
|
+
const legacyPath = path.join(process.cwd(), '.caws', 'working-spec.yaml');
|
|
414
|
+
|
|
415
|
+
if (!(await fs.pathExists(legacyPath))) {
|
|
416
|
+
throw new Error('No legacy working-spec.yaml found to migrate');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
console.log(chalk.blue('🔄 Migrating from legacy single-spec to multi-spec...'));
|
|
420
|
+
|
|
421
|
+
const legacyContent = await fs.readFile(legacyPath, 'utf8');
|
|
422
|
+
const legacySpec = yaml.load(legacyContent);
|
|
423
|
+
|
|
424
|
+
// Suggest feature breakdown based on acceptance criteria
|
|
425
|
+
const features = suggestFeatureBreakdown(legacySpec);
|
|
426
|
+
|
|
427
|
+
console.log(chalk.green(`\n✅ Found ${features.length} potential features to extract:`));
|
|
428
|
+
features.forEach((feature, index) => {
|
|
429
|
+
console.log(chalk.yellow(` ${index + 1}. ${feature.id} - ${feature.title}`));
|
|
430
|
+
console.log(chalk.gray(` Scope: ${feature.scope.in.join(', ')}`));
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
// Interactive selection or use provided feature IDs
|
|
434
|
+
let selectedFeatures = features;
|
|
435
|
+
|
|
436
|
+
if (options.interactive) {
|
|
437
|
+
// For now, just use all suggested features
|
|
438
|
+
// In a full implementation, this would prompt for selection
|
|
439
|
+
console.log(chalk.blue('\n📋 Using all suggested features for migration'));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (options.features && options.features.length > 0) {
|
|
443
|
+
selectedFeatures = features.filter((f) => options.features.includes(f.id));
|
|
444
|
+
console.log(chalk.blue(`\n📋 Migrating selected features: ${options.features.join(', ')}`));
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Create each feature spec
|
|
448
|
+
const createdSpecs = [];
|
|
449
|
+
for (const feature of selectedFeatures) {
|
|
450
|
+
try {
|
|
451
|
+
await createSpec(feature.id, {
|
|
452
|
+
type: 'feature',
|
|
453
|
+
title: feature.title,
|
|
454
|
+
risk_tier: 'T3', // Default tier
|
|
455
|
+
mode: 'development',
|
|
456
|
+
template: feature,
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
createdSpecs.push(feature.id);
|
|
460
|
+
console.log(chalk.green(` ✅ Created spec: ${feature.id}`));
|
|
461
|
+
} catch (error) {
|
|
462
|
+
console.log(chalk.red(` ❌ Failed to create spec ${feature.id}: ${error.message}`));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
console.log(
|
|
467
|
+
chalk.green(`\n🎉 Migration completed! Created ${createdSpecs.length} feature specs.`)
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
if (createdSpecs.length > 0) {
|
|
471
|
+
console.log(chalk.blue('\n💡 Next steps:'));
|
|
472
|
+
console.log(chalk.gray(' 1. Review and customize each feature spec'));
|
|
473
|
+
console.log(chalk.gray(' 2. Update agents to use --spec-id <feature-id>'));
|
|
474
|
+
console.log(chalk.gray(' 3. Consider archiving legacy working-spec.yaml when ready'));
|
|
475
|
+
console.log(chalk.blue('\n Example: caws validate --spec-id user-auth'));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
migrated: createdSpecs.length,
|
|
480
|
+
total: selectedFeatures.length,
|
|
481
|
+
createdSpecs,
|
|
482
|
+
legacySpec: legacySpec.id,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Ask user how to resolve spec creation conflicts
|
|
488
|
+
* @returns {Promise<string>} User's choice: 'cancel', 'rename', 'merge', 'override'
|
|
489
|
+
*/
|
|
490
|
+
async function askConflictResolution() {
|
|
491
|
+
return new Promise((resolve) => {
|
|
492
|
+
const readline = require('readline');
|
|
493
|
+
|
|
494
|
+
console.log(chalk.blue('\n🔄 Conflict Resolution Options:'));
|
|
495
|
+
console.log(chalk.gray(" 1. Cancel - Don't create the spec"));
|
|
496
|
+
console.log(chalk.gray(' 2. Rename - Create with auto-generated name'));
|
|
497
|
+
console.log(chalk.gray(' 3. Merge - Merge with existing spec (not implemented)'));
|
|
498
|
+
console.log(chalk.gray(' 4. Override - Replace existing spec (use --force)'));
|
|
499
|
+
|
|
500
|
+
console.log(chalk.yellow('\nEnter your choice (1-4) or the option name:'));
|
|
501
|
+
|
|
502
|
+
const rl = readline.createInterface({
|
|
503
|
+
input: process.stdin,
|
|
504
|
+
output: process.stdout,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
rl.question('> ', (answer) => {
|
|
508
|
+
rl.close();
|
|
509
|
+
|
|
510
|
+
const trimmed = answer.trim().toLowerCase();
|
|
511
|
+
|
|
512
|
+
// Handle numeric choices
|
|
513
|
+
if (trimmed === '1' || trimmed === 'cancel') {
|
|
514
|
+
resolve('cancel');
|
|
515
|
+
} else if (trimmed === '2' || trimmed === 'rename') {
|
|
516
|
+
resolve('rename');
|
|
517
|
+
} else if (trimmed === '3' || trimmed === 'merge') {
|
|
518
|
+
resolve('merge');
|
|
519
|
+
} else if (trimmed === '4' || trimmed === 'override') {
|
|
520
|
+
resolve('override');
|
|
521
|
+
} else {
|
|
522
|
+
console.log(chalk.red('❌ Invalid choice. Defaulting to cancel.'));
|
|
523
|
+
resolve('cancel');
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Specs command handler
|
|
531
|
+
* @param {string} action - Action to perform (list, create, show, update, delete, conflicts, migrate)
|
|
532
|
+
* @param {Object} options - Command options
|
|
533
|
+
*/
|
|
534
|
+
async function specsCommand(action, options = {}) {
|
|
535
|
+
return safeAsync(
|
|
536
|
+
async () => {
|
|
537
|
+
switch (action) {
|
|
538
|
+
case 'list': {
|
|
539
|
+
const specs = await listSpecFiles();
|
|
540
|
+
displaySpecsTable(specs);
|
|
541
|
+
|
|
542
|
+
return outputResult({
|
|
543
|
+
command: 'specs list',
|
|
544
|
+
count: specs.length,
|
|
545
|
+
specs: specs.map((s) => ({ id: s.id, type: s.type, status: s.status })),
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
case 'conflicts': {
|
|
550
|
+
const { checkScopeConflicts } = require('../utils/spec-resolver');
|
|
551
|
+
const registry = await loadSpecsRegistry();
|
|
552
|
+
const specIds = Object.keys(registry.specs ?? {});
|
|
553
|
+
|
|
554
|
+
if (specIds.length < 2) {
|
|
555
|
+
console.log(chalk.blue('ℹ️ No scope conflicts possible with fewer than 2 specs'));
|
|
556
|
+
return outputResult({
|
|
557
|
+
command: 'specs conflicts',
|
|
558
|
+
conflictCount: 0,
|
|
559
|
+
conflicts: [],
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
console.log(chalk.blue(`🔍 Checking scope conflicts between ${specIds.length} specs...`));
|
|
564
|
+
const conflicts = await checkScopeConflicts(specIds);
|
|
565
|
+
|
|
566
|
+
if (conflicts.length === 0) {
|
|
567
|
+
console.log(chalk.green('✅ No scope conflicts detected'));
|
|
568
|
+
} else {
|
|
569
|
+
console.log(
|
|
570
|
+
chalk.yellow(
|
|
571
|
+
`⚠️ Found ${conflicts.length} scope conflict${conflicts.length > 1 ? 's' : ''}:`
|
|
572
|
+
)
|
|
573
|
+
);
|
|
574
|
+
conflicts.forEach((conflict) => {
|
|
575
|
+
console.log(chalk.red(` ${conflict.spec1} ↔ ${conflict.spec2}:`));
|
|
576
|
+
conflict.conflicts.forEach((pathConflict) => {
|
|
577
|
+
console.log(chalk.gray(` ${pathConflict}`));
|
|
578
|
+
});
|
|
579
|
+
});
|
|
580
|
+
console.log(
|
|
581
|
+
chalk.blue('\n💡 Tip: Use non-overlapping scope.in paths to avoid conflicts')
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return outputResult({
|
|
586
|
+
command: 'specs conflicts',
|
|
587
|
+
conflictCount: conflicts.length,
|
|
588
|
+
conflicts,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
case 'migrate': {
|
|
593
|
+
const result = await migrateFromLegacy(options);
|
|
594
|
+
|
|
595
|
+
return outputResult({
|
|
596
|
+
command: 'specs migrate',
|
|
597
|
+
...result,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
case 'create': {
|
|
602
|
+
if (!options.id) {
|
|
603
|
+
throw new Error('Spec ID is required. Usage: caws specs create <id>');
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const newSpec = await createSpec(options.id, {
|
|
607
|
+
type: options.type,
|
|
608
|
+
title: options.title,
|
|
609
|
+
risk_tier: options.tier,
|
|
610
|
+
mode: options.mode,
|
|
611
|
+
force: options.force,
|
|
612
|
+
interactive: options.interactive,
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
if (!newSpec) {
|
|
616
|
+
// User canceled or creation failed
|
|
617
|
+
return outputResult({
|
|
618
|
+
command: 'specs create',
|
|
619
|
+
canceled: true,
|
|
620
|
+
message: 'Spec creation was canceled or failed',
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
console.log(chalk.green(`✅ Created spec: ${newSpec.id}`));
|
|
625
|
+
displaySpecDetails(newSpec);
|
|
626
|
+
|
|
627
|
+
return outputResult({
|
|
628
|
+
command: 'specs create',
|
|
629
|
+
spec: newSpec,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
case 'show': {
|
|
634
|
+
if (!options.id) {
|
|
635
|
+
throw new Error('Spec ID is required. Usage: caws specs show <id>');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const spec = await loadSpec(options.id);
|
|
639
|
+
if (!spec) {
|
|
640
|
+
throw new Error(`Spec '${options.id}' not found`);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
displaySpecDetails(spec);
|
|
644
|
+
|
|
645
|
+
return outputResult({
|
|
646
|
+
command: 'specs show',
|
|
647
|
+
spec: { id: spec.id, type: spec.type, status: spec.status },
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
case 'update': {
|
|
652
|
+
if (!options.id) {
|
|
653
|
+
throw new Error('Spec ID is required. Usage: caws specs update <id>');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const updates = {};
|
|
657
|
+
if (options.status) updates.status = options.status;
|
|
658
|
+
if (options.title) updates.title = options.title;
|
|
659
|
+
if (options.description) updates.description = options.description;
|
|
660
|
+
|
|
661
|
+
const updated = await updateSpec(options.id, updates);
|
|
662
|
+
if (!updated) {
|
|
663
|
+
throw new Error(`Spec '${options.id}' not found`);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
console.log(chalk.green(`✅ Updated spec: ${options.id}`));
|
|
667
|
+
|
|
668
|
+
return outputResult({
|
|
669
|
+
command: 'specs update',
|
|
670
|
+
spec: options.id,
|
|
671
|
+
updates,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
case 'delete': {
|
|
676
|
+
if (!options.id) {
|
|
677
|
+
throw new Error('Spec ID is required. Usage: caws specs delete <id>');
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const deleted = await deleteSpec(options.id);
|
|
681
|
+
if (!deleted) {
|
|
682
|
+
throw new Error(`Spec '${options.id}' not found`);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
console.log(chalk.green(`✅ Deleted spec: ${options.id}`));
|
|
686
|
+
|
|
687
|
+
return outputResult({
|
|
688
|
+
command: 'specs delete',
|
|
689
|
+
spec: options.id,
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
case 'types': {
|
|
694
|
+
console.log(chalk.bold.cyan('\n📋 Available Spec Types'));
|
|
695
|
+
console.log(chalk.cyan('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n'));
|
|
696
|
+
|
|
697
|
+
Object.entries(SPEC_TYPES).forEach(([type, info]) => {
|
|
698
|
+
console.log(`${info.icon} ${info.color(type.padEnd(10))} - ${info.description}`);
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
console.log('');
|
|
702
|
+
|
|
703
|
+
return outputResult({
|
|
704
|
+
command: 'specs types',
|
|
705
|
+
types: Object.keys(SPEC_TYPES),
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
default:
|
|
710
|
+
throw new Error(
|
|
711
|
+
`Unknown specs action: ${action}. Use: list, create, show, update, delete, conflicts, migrate, types`
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
},
|
|
715
|
+
`specs ${action}`,
|
|
716
|
+
true
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
module.exports = {
|
|
721
|
+
specsCommand,
|
|
722
|
+
loadSpecsRegistry,
|
|
723
|
+
saveSpecsRegistry,
|
|
724
|
+
listSpecFiles,
|
|
725
|
+
createSpec,
|
|
726
|
+
loadSpec,
|
|
727
|
+
updateSpec,
|
|
728
|
+
deleteSpec,
|
|
729
|
+
displaySpecsTable,
|
|
730
|
+
displaySpecDetails,
|
|
731
|
+
askConflictResolution,
|
|
732
|
+
SPECS_DIR,
|
|
733
|
+
SPECS_REGISTRY,
|
|
734
|
+
SPEC_TYPES,
|
|
735
|
+
};
|