@paths.design/caws-cli 7.0.3 ā 8.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/quality-gates.js +147 -9
- package/dist/commands/specs.js +108 -13
- package/dist/commands/tool.js +0 -1
- package/dist/scaffold/git-hooks.js +159 -58
- package/dist/scaffold/index.js +1 -1
- package/dist/templates/.caws/tools/README.md +1 -0
- package/dist/utils/git-lock.js +1 -0
- package/dist/utils/project-analysis.js +176 -16
- package/dist/utils/quality-gates.js +7 -6
- package/dist/utils/spec-resolver.js +4 -0
- package/dist/utils/yaml-validation.js +1 -0
- package/package.json +1 -1
- package/templates/.caws/tools/README.md +1 -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/archive.d.ts +0 -50
- package/dist/commands/archive.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/mode.d.ts +0 -24
- package/dist/commands/mode.d.ts.map +0 -1
- package/dist/commands/plan.d.ts +0 -49
- package/dist/commands/plan.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-gates.d.ts +0 -52
- package/dist/commands/quality-gates.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/specs.d.ts +0 -71
- package/dist/commands/specs.d.ts.map +0 -1
- package/dist/commands/status.d.ts +0 -44
- 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/tutorial.d.ts +0 -55
- package/dist/commands/tutorial.d.ts.map +0 -1
- package/dist/commands/validate.d.ts +0 -15
- 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/config/modes.d.ts +0 -225
- package/dist/config/modes.d.ts.map +0 -1
- package/dist/constants/spec-types.d.ts +0 -41
- package/dist/constants/spec-types.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-new.d.ts +0 -5
- package/dist/index-new.d.ts.map +0 -1
- package/dist/index-new.js +0 -317
- package/dist/index.d.ts +0 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.backup +0 -4711
- 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/quality-gates.d.ts +0 -49
- package/dist/utils/quality-gates.d.ts.map +0 -1
- package/dist/utils/spec-resolver.d.ts +0 -88
- package/dist/utils/spec-resolver.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
|
@@ -44,14 +44,66 @@ async function qualityGatesCommand(options = {}) {
|
|
|
44
44
|
const packagesDir = path.dirname(cliPackageDir);
|
|
45
45
|
const monorepoRunner = path.join(packagesDir, 'quality-gates', 'run-quality-gates.mjs');
|
|
46
46
|
|
|
47
|
-
// Option 2: Check
|
|
47
|
+
// Option 2: Check globally installed CLI for bundled quality gates
|
|
48
|
+
let globalCliPath = null;
|
|
49
|
+
try {
|
|
50
|
+
const { execSync } = require('child_process');
|
|
51
|
+
const whichCaws = execSync('which caws', { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
52
|
+
if (whichCaws) {
|
|
53
|
+
// Resolve symlink to actual path
|
|
54
|
+
const realPath = fs.realpathSync(whichCaws);
|
|
55
|
+
const globalCliDir = path.dirname(realPath);
|
|
56
|
+
// Check for bundled quality gates in global CLI installation
|
|
57
|
+
const possibleBundledPaths = [
|
|
58
|
+
path.join(
|
|
59
|
+
globalCliDir,
|
|
60
|
+
'..',
|
|
61
|
+
'lib',
|
|
62
|
+
'node_modules',
|
|
63
|
+
'@paths.design',
|
|
64
|
+
'caws-cli',
|
|
65
|
+
'node_modules',
|
|
66
|
+
'@paths.design',
|
|
67
|
+
'quality-gates',
|
|
68
|
+
'run-quality-gates.mjs'
|
|
69
|
+
),
|
|
70
|
+
path.join(
|
|
71
|
+
globalCliDir,
|
|
72
|
+
'..',
|
|
73
|
+
'lib',
|
|
74
|
+
'node_modules',
|
|
75
|
+
'@paths.design',
|
|
76
|
+
'quality-gates',
|
|
77
|
+
'run-quality-gates.mjs'
|
|
78
|
+
),
|
|
79
|
+
path.join(
|
|
80
|
+
globalCliDir,
|
|
81
|
+
'..',
|
|
82
|
+
'node_modules',
|
|
83
|
+
'@paths.design',
|
|
84
|
+
'quality-gates',
|
|
85
|
+
'run-quality-gates.mjs'
|
|
86
|
+
),
|
|
87
|
+
];
|
|
88
|
+
for (const bundledPath of possibleBundledPaths) {
|
|
89
|
+
if (fs.existsSync(bundledPath)) {
|
|
90
|
+
globalCliPath = bundledPath;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
// Ignore errors finding global CLI
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Option 3: Check VS Code extension bundled (if running from extension context)
|
|
48
100
|
const vscodeExtensionPath =
|
|
49
101
|
process.env.VSCODE_EXTENSION_PATH || process.env.VSCODE_EXTENSION_DIR;
|
|
50
102
|
const bundledRunner = vscodeExtensionPath
|
|
51
103
|
? path.join(vscodeExtensionPath, 'bundled', 'quality-gates', 'run-quality-gates.mjs')
|
|
52
104
|
: null;
|
|
53
105
|
|
|
54
|
-
// Option
|
|
106
|
+
// Option 4: Check node_modules for quality-gates package (prioritize published package)
|
|
55
107
|
const nodeModulesPaths = [
|
|
56
108
|
// Published npm package (priority)
|
|
57
109
|
path.join(
|
|
@@ -69,6 +121,8 @@ async function qualityGatesCommand(options = {}) {
|
|
|
69
121
|
// Try all possible paths in order
|
|
70
122
|
if (fs.existsSync(monorepoRunner)) {
|
|
71
123
|
qualityGatesRunner = monorepoRunner;
|
|
124
|
+
} else if (globalCliPath) {
|
|
125
|
+
qualityGatesRunner = globalCliPath;
|
|
72
126
|
} else if (bundledRunner && fs.existsSync(bundledRunner)) {
|
|
73
127
|
qualityGatesRunner = bundledRunner;
|
|
74
128
|
} else {
|
|
@@ -132,14 +186,98 @@ async function qualityGatesCommand(options = {}) {
|
|
|
132
186
|
}
|
|
133
187
|
}
|
|
134
188
|
|
|
135
|
-
//
|
|
189
|
+
// Option 5: Try npx (no installation required) - works if Node.js is available
|
|
136
190
|
if (!qualityGatesRunner) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
'
|
|
141
|
-
|
|
142
|
-
|
|
191
|
+
try {
|
|
192
|
+
const { execSync } = require('child_process');
|
|
193
|
+
// Check if npx is available
|
|
194
|
+
execSync('command -v npx', { encoding: 'utf8', stdio: 'ignore' });
|
|
195
|
+
|
|
196
|
+
Output.info('Using npx to run quality gates (no installation required)...');
|
|
197
|
+
|
|
198
|
+
// Build npx command - the package exposes 'caws-quality-gates' bin command
|
|
199
|
+
// Use npx to download and run without installing
|
|
200
|
+
const npxArgs = ['npx', '--yes', '@paths.design/quality-gates'];
|
|
201
|
+
|
|
202
|
+
// Map CLI options to runner options
|
|
203
|
+
if (options.ci) {
|
|
204
|
+
npxArgs.push('--ci');
|
|
205
|
+
}
|
|
206
|
+
if (options.json) {
|
|
207
|
+
npxArgs.push('--json');
|
|
208
|
+
}
|
|
209
|
+
if (options.gates && options.gates.trim()) {
|
|
210
|
+
npxArgs.push('--gates', options.gates.trim());
|
|
211
|
+
}
|
|
212
|
+
if (options.fix) {
|
|
213
|
+
npxArgs.push('--fix');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
Output.progress('Executing quality gates via npx...');
|
|
217
|
+
Output.info(`Command: ${npxArgs.join(' ')}`);
|
|
218
|
+
|
|
219
|
+
// Execute via npx
|
|
220
|
+
const { execSync: execSyncNpx } = require('child_process');
|
|
221
|
+
execSyncNpx(npxArgs.join(' '), {
|
|
222
|
+
stdio: 'inherit',
|
|
223
|
+
cwd: projectRoot,
|
|
224
|
+
env: {
|
|
225
|
+
...process.env,
|
|
226
|
+
CAWS_CLI_INTEGRATION: 'true',
|
|
227
|
+
CAWS_CLI_VERSION: require(path.join(cliPackageDir, 'package.json')).version,
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
Output.success('Quality gates completed successfully');
|
|
232
|
+
return;
|
|
233
|
+
} catch (npxError) {
|
|
234
|
+
// npx not available or failed - continue to error message
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// If still no runner found, provide helpful error with language-agnostic suggestions
|
|
239
|
+
if (!qualityGatesRunner) {
|
|
240
|
+
// Check if Node.js/npx is available (language-agnostic check)
|
|
241
|
+
let hasNodeJs = false;
|
|
242
|
+
try {
|
|
243
|
+
const { execSync } = require('child_process');
|
|
244
|
+
execSync('command -v node', { encoding: 'utf8', stdio: 'ignore' });
|
|
245
|
+
execSync('command -v npx', { encoding: 'utf8', stdio: 'ignore' });
|
|
246
|
+
hasNodeJs = true;
|
|
247
|
+
} catch (e) {
|
|
248
|
+
// Node.js/npx not available
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const suggestions = [];
|
|
252
|
+
|
|
253
|
+
if (hasNodeJs) {
|
|
254
|
+
// Node.js available - suggest npx (works for any language, no installation)
|
|
255
|
+
suggestions.push(
|
|
256
|
+
'Use npx (no installation required): npx --yes @paths.design/quality-gates'
|
|
257
|
+
);
|
|
258
|
+
suggestions.push('Install globally: npm install -g @paths.design/quality-gates');
|
|
259
|
+
suggestions.push('Install locally: npm install --save-dev @paths.design/quality-gates');
|
|
260
|
+
} else {
|
|
261
|
+
// Node.js not available - suggest installation or alternatives
|
|
262
|
+
suggestions.push('Install Node.js to use quality gates: https://nodejs.org/');
|
|
263
|
+
suggestions.push(
|
|
264
|
+
'Then use: npx --yes @paths.design/quality-gates (no installation required)'
|
|
265
|
+
);
|
|
266
|
+
suggestions.push('Or install globally: npm install -g @paths.design/quality-gates');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Language-agnostic fallback options (if they exist)
|
|
270
|
+
const pythonScript = path.join(projectRoot, 'scripts', 'simple_gates.py');
|
|
271
|
+
const makefile = path.join(projectRoot, 'Makefile');
|
|
272
|
+
|
|
273
|
+
if (fs.existsSync(pythonScript)) {
|
|
274
|
+
suggestions.push(`Use project script: python3 ${pythonScript} all --tier 2`);
|
|
275
|
+
}
|
|
276
|
+
if (fs.existsSync(makefile)) {
|
|
277
|
+
suggestions.push('Use Makefile target: make caws-gates');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
suggestions.push('Run from CAWS monorepo root (if developing CAWS itself)');
|
|
143
281
|
|
|
144
282
|
throw new Error(
|
|
145
283
|
'Quality gates runner not found.\n\n' +
|
package/dist/commands/specs.js
CHANGED
|
@@ -154,9 +154,13 @@ async function createSpec(id, options = {}) {
|
|
|
154
154
|
console.log(chalk.blue('ā¹ļø Spec creation canceled.'));
|
|
155
155
|
return null;
|
|
156
156
|
} else if (answer === 'rename') {
|
|
157
|
-
// Generate new name with
|
|
158
|
-
|
|
159
|
-
const
|
|
157
|
+
// Generate new name with valid PREFIX-NUMBER format
|
|
158
|
+
// Extract prefix from existing ID or use default
|
|
159
|
+
const prefixMatch = id.match(/^([A-Z]+)-\d+$/);
|
|
160
|
+
const prefix = prefixMatch ? prefixMatch[1] : 'FEAT';
|
|
161
|
+
// Generate sequential number based on timestamp
|
|
162
|
+
const number = Date.now().toString().slice(-6); // Last 6 digits of timestamp
|
|
163
|
+
const newId = `${prefix}-${number}`;
|
|
160
164
|
console.log(chalk.blue(`š Creating spec with new name: ${newId}`));
|
|
161
165
|
return await createSpec(newId, { ...options, interactive: false });
|
|
162
166
|
} else if (answer === 'merge') {
|
|
@@ -183,9 +187,10 @@ async function createSpec(id, options = {}) {
|
|
|
183
187
|
// Ensure specs directory exists
|
|
184
188
|
await fs.ensureDir(SPECS_DIR);
|
|
185
189
|
|
|
186
|
-
// Generate spec content
|
|
187
|
-
|
|
188
|
-
|
|
190
|
+
// Generate spec content with all required fields
|
|
191
|
+
// Merge template carefully to preserve required fields and structure
|
|
192
|
+
const defaultSpec = {
|
|
193
|
+
id, // Always use the provided id parameter
|
|
189
194
|
type,
|
|
190
195
|
title,
|
|
191
196
|
status: 'draft',
|
|
@@ -193,8 +198,64 @@ async function createSpec(id, options = {}) {
|
|
|
193
198
|
mode,
|
|
194
199
|
created_at: new Date().toISOString(),
|
|
195
200
|
updated_at: new Date().toISOString(),
|
|
196
|
-
|
|
201
|
+
// Required fields for validation
|
|
202
|
+
blast_radius: {
|
|
203
|
+
modules: [],
|
|
204
|
+
data_migration: false,
|
|
205
|
+
},
|
|
206
|
+
operational_rollback_slo: '5m',
|
|
207
|
+
scope: {
|
|
208
|
+
in: ['src/', 'tests/'],
|
|
209
|
+
out: ['node_modules/', 'dist/', 'build/'],
|
|
210
|
+
},
|
|
211
|
+
invariants: ['System maintains data consistency'],
|
|
212
|
+
acceptance: [], // Note: validation expects 'acceptance', not 'acceptance_criteria'
|
|
213
|
+
acceptance_criteria: [], // Keep for backward compatibility
|
|
214
|
+
non_functional: {
|
|
215
|
+
a11y: [],
|
|
216
|
+
perf: {},
|
|
217
|
+
security: [],
|
|
218
|
+
},
|
|
219
|
+
contracts: [],
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
// Merge template, but preserve required structure
|
|
223
|
+
// Map template.criteria to acceptance if present
|
|
224
|
+
const templateAcceptance = template?.criteria || template?.acceptance;
|
|
225
|
+
|
|
226
|
+
const specContent = {
|
|
227
|
+
...defaultSpec,
|
|
197
228
|
...(template || {}),
|
|
229
|
+
// Always preserve these critical fields
|
|
230
|
+
id, // Never allow template to override id
|
|
231
|
+
// Map criteria to acceptance if template uses criteria
|
|
232
|
+
acceptance: templateAcceptance || defaultSpec.acceptance,
|
|
233
|
+
acceptance_criteria: templateAcceptance || defaultSpec.acceptance_criteria,
|
|
234
|
+
// Deep merge scope if template provides it
|
|
235
|
+
scope: template?.scope
|
|
236
|
+
? {
|
|
237
|
+
in: template.scope.in || defaultSpec.scope.in,
|
|
238
|
+
out: template.scope.out || defaultSpec.scope.out,
|
|
239
|
+
}
|
|
240
|
+
: defaultSpec.scope,
|
|
241
|
+
// Deep merge blast_radius if template provides it
|
|
242
|
+
blast_radius: template?.blast_radius
|
|
243
|
+
? {
|
|
244
|
+
modules: template.blast_radius.modules || defaultSpec.blast_radius.modules,
|
|
245
|
+
data_migration:
|
|
246
|
+
template.blast_radius.data_migration !== undefined
|
|
247
|
+
? template.blast_radius.data_migration
|
|
248
|
+
: defaultSpec.blast_radius.data_migration,
|
|
249
|
+
}
|
|
250
|
+
: defaultSpec.blast_radius,
|
|
251
|
+
// Deep merge non_functional if template provides it
|
|
252
|
+
non_functional: template?.non_functional
|
|
253
|
+
? {
|
|
254
|
+
a11y: template.non_functional.a11y || defaultSpec.non_functional.a11y,
|
|
255
|
+
perf: template.non_functional.perf || defaultSpec.non_functional.perf,
|
|
256
|
+
security: template.non_functional.security || defaultSpec.non_functional.security,
|
|
257
|
+
}
|
|
258
|
+
: defaultSpec.non_functional,
|
|
198
259
|
};
|
|
199
260
|
|
|
200
261
|
// Create file path
|
|
@@ -442,9 +503,10 @@ function displaySpecDetails(spec) {
|
|
|
442
503
|
/**
|
|
443
504
|
* Migrate from legacy working-spec.yaml to feature-specific specs
|
|
444
505
|
* @param {Object} options - Migration options
|
|
506
|
+
* @param {Function} [createSpecFn] - Function to create specs (for testing)
|
|
445
507
|
* @returns {Promise<Object>} Migration result
|
|
446
508
|
*/
|
|
447
|
-
async function migrateFromLegacy(options = {}) {
|
|
509
|
+
async function migrateFromLegacy(options = {}, createSpecFn = createSpec) {
|
|
448
510
|
const fs = require('fs-extra');
|
|
449
511
|
const path = require('path');
|
|
450
512
|
const yaml = require('js-yaml');
|
|
@@ -461,6 +523,14 @@ async function migrateFromLegacy(options = {}) {
|
|
|
461
523
|
const legacyContent = await fs.readFile(legacyPath, 'utf8');
|
|
462
524
|
const legacySpec = yaml.load(legacyContent);
|
|
463
525
|
|
|
526
|
+
if (!legacySpec) {
|
|
527
|
+
throw new Error('Legacy working-spec.yaml is empty or invalid');
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!legacySpec.acceptance || !Array.isArray(legacySpec.acceptance)) {
|
|
531
|
+
throw new Error('Legacy working-spec.yaml must have an acceptance array');
|
|
532
|
+
}
|
|
533
|
+
|
|
464
534
|
// Suggest feature breakdown based on acceptance criteria
|
|
465
535
|
const features = suggestFeatureBreakdown(legacySpec);
|
|
466
536
|
|
|
@@ -480,15 +550,32 @@ async function migrateFromLegacy(options = {}) {
|
|
|
480
550
|
}
|
|
481
551
|
|
|
482
552
|
if (options.features && options.features.length > 0) {
|
|
553
|
+
// Filter by original feature IDs (before transformation)
|
|
483
554
|
selectedFeatures = features.filter((f) => options.features.includes(f.id));
|
|
484
|
-
|
|
555
|
+
if (selectedFeatures.length === 0) {
|
|
556
|
+
const errorMsg = `No features found matching: ${options.features.join(', ')}. Available features: ${features.map((f) => f.id).join(', ')}`;
|
|
557
|
+
console.log(chalk.yellow(`ā ļø ${errorMsg}`));
|
|
558
|
+
throw new Error(errorMsg);
|
|
559
|
+
} else {
|
|
560
|
+
console.log(chalk.blue(`\nš Migrating selected features: ${options.features.join(', ')}`));
|
|
561
|
+
}
|
|
485
562
|
}
|
|
486
563
|
|
|
487
564
|
// Create each feature spec
|
|
488
565
|
const createdSpecs = [];
|
|
566
|
+
let featureCounter = 1;
|
|
489
567
|
for (const feature of selectedFeatures) {
|
|
490
568
|
try {
|
|
491
|
-
|
|
569
|
+
// Transform feature ID to proper format (PREFIX-NUMBER) if needed
|
|
570
|
+
let specId = feature.id;
|
|
571
|
+
if (!/^[A-Z]+-\d+$/.test(specId)) {
|
|
572
|
+
// Convert 'auth' -> 'FEAT-001', 'payment' -> 'FEAT-002', etc.
|
|
573
|
+
const prefix = specId.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
574
|
+
specId = `${prefix || 'FEAT'}-${String(featureCounter).padStart(3, '0')}`;
|
|
575
|
+
featureCounter++;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
await createSpecFn(specId, {
|
|
492
579
|
type: 'feature',
|
|
493
580
|
title: feature.title,
|
|
494
581
|
risk_tier: 'T3', // Default tier
|
|
@@ -496,10 +583,14 @@ async function migrateFromLegacy(options = {}) {
|
|
|
496
583
|
template: feature,
|
|
497
584
|
});
|
|
498
585
|
|
|
499
|
-
createdSpecs.push(
|
|
500
|
-
console.log(chalk.green(` ā
Created spec: ${
|
|
586
|
+
createdSpecs.push(specId);
|
|
587
|
+
console.log(chalk.green(` ā
Created spec: ${specId}`));
|
|
501
588
|
} catch (error) {
|
|
589
|
+
// Log full error details for debugging
|
|
502
590
|
console.log(chalk.red(` ā Failed to create spec ${feature.id}: ${error.message}`));
|
|
591
|
+
if (process.env.DEBUG_MIGRATION) {
|
|
592
|
+
console.log(chalk.gray(` Error details: ${error.stack}`));
|
|
593
|
+
}
|
|
503
594
|
}
|
|
504
595
|
}
|
|
505
596
|
|
|
@@ -628,7 +719,11 @@ async function specsCommand(action, options = {}) {
|
|
|
628
719
|
}
|
|
629
720
|
|
|
630
721
|
case 'migrate': {
|
|
631
|
-
|
|
722
|
+
// Allow tests to inject createSpec function
|
|
723
|
+
const createSpecFn = options._createSpecFn || createSpec;
|
|
724
|
+
const migrationOptions = { ...options };
|
|
725
|
+
delete migrationOptions._createSpecFn; // Remove test-only option
|
|
726
|
+
const result = await migrateFromLegacy(migrationOptions, createSpecFn);
|
|
632
727
|
|
|
633
728
|
return outputResult({
|
|
634
729
|
command: 'specs migrate',
|