@paths.design/caws-cli 4.0.0 → 4.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 (76) hide show
  1. package/dist/commands/archive.d.ts +50 -0
  2. package/dist/commands/archive.d.ts.map +1 -0
  3. package/dist/commands/archive.js +353 -0
  4. package/dist/commands/iterate.d.ts.map +1 -1
  5. package/dist/commands/iterate.js +12 -13
  6. package/dist/commands/mode.d.ts +24 -0
  7. package/dist/commands/mode.d.ts.map +1 -0
  8. package/dist/commands/mode.js +259 -0
  9. package/dist/commands/plan.d.ts +49 -0
  10. package/dist/commands/plan.d.ts.map +1 -0
  11. package/dist/commands/plan.js +448 -0
  12. package/dist/commands/quality-gates.d.ts +52 -0
  13. package/dist/commands/quality-gates.d.ts.map +1 -0
  14. package/dist/commands/quality-gates.js +490 -0
  15. package/dist/commands/specs.d.ts +71 -0
  16. package/dist/commands/specs.d.ts.map +1 -0
  17. package/dist/commands/specs.js +735 -0
  18. package/dist/commands/status.d.ts +4 -3
  19. package/dist/commands/status.d.ts.map +1 -1
  20. package/dist/commands/status.js +552 -22
  21. package/dist/commands/tutorial.d.ts +55 -0
  22. package/dist/commands/tutorial.d.ts.map +1 -0
  23. package/dist/commands/tutorial.js +481 -0
  24. package/dist/commands/validate.d.ts +10 -3
  25. package/dist/commands/validate.d.ts.map +1 -1
  26. package/dist/commands/validate.js +137 -54
  27. package/dist/config/modes.d.ts +225 -0
  28. package/dist/config/modes.d.ts.map +1 -0
  29. package/dist/config/modes.js +321 -0
  30. package/dist/constants/spec-types.d.ts +41 -0
  31. package/dist/constants/spec-types.d.ts.map +1 -0
  32. package/dist/constants/spec-types.js +42 -0
  33. package/dist/index-new.d.ts +5 -0
  34. package/dist/index-new.d.ts.map +1 -0
  35. package/dist/index-new.js +317 -0
  36. package/dist/index.js +225 -10
  37. package/dist/index.js.backup +4711 -0
  38. package/dist/scaffold/git-hooks.d.ts.map +1 -1
  39. package/dist/scaffold/git-hooks.js +32 -44
  40. package/dist/scaffold/index.d.ts.map +1 -1
  41. package/dist/scaffold/index.js +19 -0
  42. package/dist/utils/quality-gates-errors.js +520 -0
  43. package/dist/utils/quality-gates.d.ts +49 -0
  44. package/dist/utils/quality-gates.d.ts.map +1 -0
  45. package/dist/utils/quality-gates.js +361 -0
  46. package/dist/utils/spec-resolver.d.ts +88 -0
  47. package/dist/utils/spec-resolver.d.ts.map +1 -0
  48. package/dist/utils/spec-resolver.js +602 -0
  49. package/package.json +6 -5
  50. package/templates/.cursor/hooks/caws-scope-guard.sh +64 -8
  51. package/templates/.cursor/hooks/validate-spec.sh +22 -12
  52. package/templates/.cursor/rules/{01-claims-verification.mdc → 00-claims-verification.mdc} +1 -1
  53. package/templates/.cursor/rules/01-working-style.mdc +50 -0
  54. package/templates/.cursor/rules/{02-testing-standards.mdc → 02-quality-gates.mdc} +84 -29
  55. package/templates/.cursor/rules/03-naming-and-refactor.mdc +33 -0
  56. package/templates/.cursor/rules/04-logging-language-style.mdc +23 -0
  57. package/templates/.cursor/rules/05-safe-defaults-guards.mdc +23 -0
  58. package/templates/.cursor/rules/06-typescript-conventions.mdc +36 -0
  59. package/templates/.cursor/rules/07-process-ops.mdc +20 -0
  60. package/templates/.cursor/rules/08-solid-and-architecture.mdc +16 -0
  61. package/templates/.cursor/rules/09-docstrings.mdc +89 -0
  62. package/templates/.cursor/rules/10-authorship-and-attribution.mdc +15 -0
  63. package/templates/.cursor/rules/11-documentation-quality-standards.mdc +390 -0
  64. package/templates/.cursor/rules/12-scope-management-waivers.mdc +385 -0
  65. package/templates/.cursor/rules/13-implementation-completeness.mdc +516 -0
  66. package/templates/.cursor/rules/14-language-agnostic-standards.mdc +588 -0
  67. package/templates/.cursor/rules/15-sophisticated-todo-detection.mdc +425 -0
  68. package/templates/.cursor/rules/README.md +93 -7
  69. package/templates/apps/tools/caws/prompt-lint.js.backup +274 -0
  70. package/templates/apps/tools/caws/provenance.js.backup +73 -0
  71. package/templates/scripts/quality-gates/check-god-objects.js +146 -0
  72. package/templates/scripts/quality-gates/run-quality-gates.js +50 -0
  73. package/templates/scripts/v3/analysis/todo_analyzer.py +1950 -0
  74. package/templates/.cursor/rules/03-infrastructure-standards.mdc +0 -251
  75. package/templates/.cursor/rules/04-documentation-integrity.mdc +0 -291
  76. 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
+ };