supernal-coding
Version:
Comprehensive development workflow CLI with kanban task management, project validation, git safety hooks, and cross-project distribution system
1,296 lines (1,070 loc) β’ 45.2 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const { execSync } = require('child_process');
class RequirementManager {
constructor() {
this.projectRoot = process.cwd();
this.requirementsPath = path.join(this.projectRoot, 'supernal-coding', 'requirements');
this.templatesPath = path.join(this.projectRoot, 'templates');
}
async getNextRequirementId() {
// Find all existing requirement files and get the highest ID
const reqFiles = await this.getAllRequirementFiles();
let highestId = 0;
for (const file of reqFiles) {
const match = file.match(/req-(\d+)/);
if (match) {
const id = parseInt(match[1], 10);
if (id > highestId) {
highestId = id;
}
}
}
return highestId + 1;
}
async getAllRequirementFiles() {
const files = [];
async function scanDir(dir) {
if (!await fs.pathExists(dir)) return;
const items = await fs.readdir(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
await scanDir(fullPath);
} else if (item.name.endsWith('.md') && item.name.includes('req-')) {
files.push(item.name);
}
}
}
await scanDir(this.requirementsPath);
return files;
}
async loadTemplate(templateName) {
const templatePath = path.join(this.templatesPath, templateName);
if (await fs.pathExists(templatePath)) {
return await fs.readFile(templatePath, 'utf8');
}
// Fallback to basic template if template not found
return this.getDefaultTemplate();
}
getDefaultTemplate() {
return `---
id: REQ-{{id}}
title: {{requirement-name}}
status: pending
priority: {{priority}}
epic: {{epic-name}}
dependencies: []
created: {{date}}
updated: {{date}}
---
# REQ-{{id}}: {{requirement-name}}
## Overview
{{functionality}}
## User Story
**As a** {{user-type}}
**I want** {{functionality}}
**So that** {{benefit}}
## Acceptance Criteria
- [ ] {{precondition}}
- [ ] {{action}}
- [ ] {{expected-result}}
## Technical Implementation
{{technical-details}}
## Test Strategy
{{test-strategy}}
## Notes
{{implementation-notes}}
`;
}
async createRequirement(title, options = {}) {
try {
// First, check for similar existing requirements
await this.checkForSimilarRequirements(title, options);
const reqId = await this.getNextRequirementId();
const normalizedId = reqId.toString().padStart(3, '0');
// Determine category from epic or default to workflow
const category = options.epic ? this.epicToCategory(options.epic) : 'workflow';
const reqDir = path.join(this.requirementsPath, category);
// Ensure directory exists
await fs.ensureDir(reqDir);
// Create requirement file with proper kebab-case name
const kebabName = title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
const fileName = `req-${normalizedId}-${kebabName}.md`;
const filePath = path.join(reqDir, fileName);
// Check if file already exists
if (await fs.pathExists(filePath)) {
throw new Error(`Requirement file already exists: ${filePath}`);
}
// Load template and replace placeholders
const template = await this.loadTemplate('requirement-template.md');
const content = template
.replace(/\{\{id\}\}/g, normalizedId)
.replace(/\{\{requirement-name\}\}/g, title)
.replace(/\{\{epic-name\}\}/g, options.epic || 'not-assigned')
.replace(/\{\{date\}\}/g, new Date().toISOString().split('T')[0])
.replace(/\{\{priority\}\}/g, this.priorityNameToNumber(options.priority) || '2')
.replace(/\{\{user-type\}\}/g, options['user-type'] || options.userType || 'developer')
.replace(/\{\{functionality\}\}/g, options.functionality || 'Functionality to be defined')
.replace(/\{\{benefit\}\}/g, options.benefit || 'Benefit to be defined')
.replace(/\{\{request-type\}\}/g, options['request-type'] || options.requestType || 'feature')
.replace(/\{\{precondition\}\}/g, options.precondition || 'Precondition to be defined')
.replace(/\{\{action\}\}/g, options.action || 'Action to be defined')
.replace(/\{\{expected-result\}\}/g, options.expectedResult || 'Expected result to be defined')
.replace(/\{\{technical-details\}\}/g, options.technicalDetails || 'Technical implementation details to be added')
.replace(/\{\{test-strategy\}\}/g, options.testStrategy || 'Test strategy to be defined')
.replace(/\{\{implementation-notes\}\}/g, options.implementationNotes || 'Implementation notes to be added');
// Write file
await fs.writeFile(filePath, content);
console.log(chalk.green(`β
Requirement REQ-${normalizedId} created successfully!`));
console.log(chalk.blue(`π File: ${filePath}`));
if (options.epic) {
console.log(chalk.blue(`π Epic: ${options.epic}`));
}
console.log(chalk.yellow(`\nβ οΈ TEMPLATE CREATED - NEEDS UPDATING:`));
console.log(chalk.yellow(` 1. Replace placeholder content with actual requirements`));
console.log(chalk.yellow(` 2. Fill in Gherkin scenarios with specific test cases`));
console.log(chalk.yellow(` 3. Add technical implementation details`));
console.log(chalk.yellow(` 4. Run: sc req validate ${normalizedId} to check completeness`));
console.log(chalk.blue(`\nπ‘ Next steps: Edit the file above, then run validation`));
return {
id: normalizedId,
filePath,
category
};
} catch (error) {
console.error(chalk.red(`β Error creating requirement: ${error.message}`));
throw error;
}
}
async listRequirements(options = {}) {
try {
const files = await this.getAllRequirementFiles();
const requirements = [];
for (const file of files) {
const reqPath = await this.findRequirementFile(file);
if (reqPath) {
const content = await fs.readFile(reqPath, 'utf8');
const frontmatter = this.extractFrontmatter(content);
if (!options.status || frontmatter.status === options.status) {
if (!options.epic || frontmatter.epic === options.epic) {
requirements.push({
file,
path: reqPath,
...frontmatter
});
}
}
}
}
// Sort by ID
requirements.sort((a, b) => {
const aId = parseInt(a.id?.replace('REQ-', '') || '0', 10);
const bId = parseInt(b.id?.replace('REQ-', '') || '0', 10);
return aId - bId;
});
if (requirements.length === 0) {
console.log(chalk.yellow('No requirements found matching criteria.'));
return;
}
console.log(chalk.bold('\nπ Requirements:\n'));
for (const req of requirements) {
const statusColor = this.getStatusColor(req.status);
const priorityIcon = this.getPriorityIcon(req.priority);
console.log(`${priorityIcon} ${statusColor(req.status?.toUpperCase() || 'UNKNOWN')} ${chalk.bold(req.id || 'NO-ID')} ${req.title || file}`);
if (req.epic && req.epic !== 'not-assigned') {
console.log(` ${chalk.dim('Epic:')} ${chalk.cyan(req.epic)}`);
}
}
return requirements;
} catch (error) {
console.error(chalk.red(`β Error listing requirements: ${error.message}`));
throw error;
}
}
async updateRequirement(reqId, updates) {
try {
const reqFile = await this.findRequirementById(reqId);
if (!reqFile) {
throw new Error(`Requirement ${reqId} not found`);
}
const content = await fs.readFile(reqFile, 'utf8');
const { frontmatter, body } = this.parseContent(content);
// Update frontmatter
const updatedFrontmatter = {
...frontmatter,
...updates,
updated: new Date().toISOString().split('T')[0]
};
// Reconstruct file
const newContent = this.reconstructContent(updatedFrontmatter, body);
await fs.writeFile(reqFile, newContent);
console.log(chalk.green(`β
Requirement ${reqId} updated successfully!`));
} catch (error) {
console.error(chalk.red(`β Error updating requirement: ${error.message}`));
throw error;
}
}
async deleteRequirement(reqId, options = {}) {
try {
const reqFile = await this.findRequirementById(reqId);
if (!reqFile) {
throw new Error(`Requirement ${reqId} not found`);
}
if (!options.force) {
// Move to archive instead of deleting
const archivePath = path.join(path.dirname(reqFile), 'archive');
await fs.ensureDir(archivePath);
const fileName = path.basename(reqFile);
const archiveFile = path.join(archivePath, `${fileName}.archived-${Date.now()}`);
await fs.move(reqFile, archiveFile);
console.log(chalk.yellow(`π¦ Requirement ${reqId} archived to: ${archiveFile}`));
} else {
await fs.remove(reqFile);
console.log(chalk.red(`ποΈ Requirement ${reqId} permanently deleted!`));
}
} catch (error) {
console.error(chalk.red(`β Error deleting requirement: ${error.message}`));
throw error;
}
}
// Helper methods
normalizeReqId(reqId) {
// Handle various formats: "REQ-039", "req-039", "039", 39
if (!reqId) return null;
const idStr = reqId.toString();
// If it's already in REQ-XXX format, extract the number
const match = idStr.match(/^(?:REQ-|req-)?(\d+)$/i);
if (match) {
return match[1].padStart(3, '0');
}
// If it's just a number, pad it
if (/^\d+$/.test(idStr)) {
return idStr.padStart(3, '0');
}
return null;
}
async findRequirementById(reqId) {
const normalizedId = this.normalizeReqId(reqId);
if (!normalizedId) {
return null;
}
const files = await this.getAllRequirementFiles();
const targetFile = files.find(f => f.includes(`req-${normalizedId}`));
if (targetFile) {
return await this.findRequirementFile(targetFile);
}
return null;
}
async findRequirementFile(fileName) {
// Search in all subdirectories
async function searchDir(dir) {
if (!await fs.pathExists(dir)) return null;
const items = await fs.readdir(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory()) {
const found = await searchDir(fullPath);
if (found) return found;
} else if (item.name === fileName) {
return fullPath;
}
}
return null;
}
return await searchDir(this.requirementsPath);
}
extractFrontmatter(content) {
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return {};
const frontmatterText = match[1];
const frontmatter = {};
frontmatterText.split('\n').forEach(line => {
const [key, ...valueParts] = line.split(':');
if (key && valueParts.length > 0) {
frontmatter[key.trim()] = valueParts.join(':').trim();
}
});
return frontmatter;
}
parseContent(content) {
const match = content.match(/^(---\n[\s\S]*?\n---)\n([\s\S]*)$/);
if (!match) {
return { frontmatter: {}, body: content };
}
const frontmatterSection = match[1];
const body = match[2];
const frontmatter = this.extractFrontmatter(frontmatterSection);
return { frontmatter, body };
}
reconstructContent(frontmatter, body) {
const frontmatterLines = Object.entries(frontmatter)
.map(([key, value]) => `${key}: ${value}`)
.join('\n');
return `---\n${frontmatterLines}\n---\n${body}`;
}
epicToCategory(epic) {
// Map epic names to categories
const mapping = {
'enhanced-workflow-system': 'workflow',
'dashboard-system': 'core',
'agent-workflow-enhancement': 'workflow',
'supernal-code-package': 'infrastructure'
};
return mapping[epic] || 'workflow';
}
priorityNameToNumber(priority) {
const mapping = {
'critical': '0',
'high': '1',
'medium': '2',
'low': '3',
'deferred': '4'
};
return mapping[priority?.toLowerCase()] || priority;
}
getStatusColor(status) {
const colors = {
'pending': chalk.yellow,
'in-progress': chalk.blue,
'done': chalk.green,
'cancelled': chalk.red,
'blocked': chalk.magenta
};
return colors[status?.toLowerCase()] || chalk.white;
}
getPriorityIcon(priority) {
const icons = {
'0': 'π¨', // Critical
'1': 'π₯', // High
'2': 'π', // Medium
'3': 'π', // Low
'4': 'βΈοΈ' // Deferred
};
return icons[priority?.toString()] || 'π';
}
async showHelp() {
console.log(chalk.bold('\nπ§ Requirement Management Commands\n'));
console.log(chalk.cyan('sc requirement new') + ' <title> [options]');
console.log(' Create a new requirement');
console.log(' Options:');
console.log(' --epic <name> Epic name (kebab-case)');
console.log(' --priority <level> Priority: critical, high, medium, low, deferred');
console.log(' --functionality <text> What functionality is needed');
console.log(' --benefit <text> What benefit this provides');
console.log(' --user-type <type> User type (developer, agent, etc.)');
console.log();
console.log(chalk.cyan('sc requirement list') + ' [options]');
console.log(' List all requirements');
console.log(' Options:');
console.log(' --status <status> Filter by status');
console.log(' --epic <name> Filter by epic');
console.log();
console.log(chalk.cyan('sc requirement show') + ' <id>');
console.log(' Show details for a specific requirement');
console.log();
console.log(chalk.cyan('sc requirement update') + ' <id> [options]');
console.log(' Update a requirement');
console.log(' Options:');
console.log(' --status <status> Update status');
console.log(' --priority <level> Update priority');
console.log(' --epic <name> Update epic');
console.log();
console.log(chalk.cyan('sc requirement validate') + ' <id>');
console.log(' Validate requirement completeness and quality');
console.log();
console.log(chalk.cyan('sc requirement generate-tests') + ' <id>');
console.log(' Generate test files from requirement Gherkin scenarios');
console.log();
console.log(chalk.cyan('sc requirement validate-coverage') + ' <id>');
console.log(' Analyze test coverage for specific requirement acceptance criteria');
console.log();
console.log(chalk.cyan('sc requirement coverage-report'));
console.log(' Generate comprehensive test coverage report for all requirements');
console.log();
console.log(chalk.cyan('sc requirement start-work') + ' <id>');
console.log(' Create git branch and prepare for implementation');
console.log();
console.log(chalk.cyan('sc requirement delete') + ' <id> [options]');
console.log(' Delete or archive a requirement');
console.log(' Options:');
console.log(' --force Permanently delete (default: archive)');
console.log();
console.log(chalk.bold('Examples:'));
console.log(' sc requirement new "User Authentication" --epic=enhanced-workflow-system --priority=high');
console.log(' sc requirement list --status=pending');
console.log(' sc requirement validate 036');
console.log(' sc requirement generate-tests 036');
console.log(' sc requirement validate-coverage 036');
console.log(' sc requirement coverage-report');
console.log(' sc requirement start-work 036');
console.log(' sc requirement update 036 --status=done');
console.log(' sc requirement delete 036');
}
async validateRequirement(reqId) {
try {
const reqFile = await this.findRequirementById(reqId);
if (!reqFile) {
throw new Error(`Requirement ${reqId} not found`);
}
const content = await fs.readFile(reqFile, 'utf8');
let validationScore = 0;
let maxScore = 0;
let issues = [];
// Check for required sections
const requiredSections = [
{ pattern: /# REQ-\d+:/, name: 'Requirement title', points: 2 },
{ pattern: /## Overview/, name: 'Overview section', points: 2 },
{ pattern: /## User Story/, name: 'User story', points: 2 },
{ pattern: /## Acceptance Criteria/, name: 'Acceptance criteria', points: 3 },
{ pattern: /```gherkin/, name: 'Gherkin scenarios', points: 3 },
{ pattern: /Given.*When.*Then/, name: 'Given-When-Then structure', points: 3 },
{ pattern: /## Technical Implementation/, name: 'Technical implementation', points: 2 },
{ pattern: /## Test Strategy/, name: 'Test strategy', points: 2 }
];
console.log(chalk.blue(`\nπ Validating REQ-${reqId}:`));
for (const section of requiredSections) {
maxScore += section.points;
if (section.pattern.test(content)) {
validationScore += section.points;
console.log(chalk.green(`β
${section.name} (${section.points} pts)`));
} else {
console.log(chalk.red(`β ${section.name} (${section.points} pts)`));
issues.push(`Missing: ${section.name}`);
}
}
// Additional quality checks
const scenarios = (content.match(/Scenario:/g) || []).length;
if (scenarios < 2) {
issues.push(`Only ${scenarios} scenario(s) found. Consider adding more scenarios for edge cases.`);
} else {
console.log(chalk.green(`β
Good scenario coverage (${scenarios} scenarios)`));
validationScore += 1;
maxScore += 1;
}
// Check for placeholders
const placeholders = content.match(/\{\{.*?\}\}/g) || [];
if (placeholders.length > 0) {
issues.push(`${placeholders.length} placeholder(s) found: ${placeholders.slice(0, 3).join(', ')}`);
} else {
console.log(chalk.green('β
No placeholders found'));
validationScore += 1;
maxScore += 1;
}
// Calculate score
const percentage = Math.round((validationScore / maxScore) * 100);
console.log(chalk.blue(`\nπ Validation Score: ${validationScore}/${maxScore} (${percentage}%)`));
if (percentage >= 90) {
console.log(chalk.green('π Excellent! Requirement is well-defined.'));
} else if (percentage >= 70) {
console.log(chalk.yellow('β οΈ Good, but could be improved.'));
} else {
console.log(chalk.red('β Needs significant improvement.'));
}
if (issues.length > 0) {
console.log(chalk.red('\nπ¨ Issues to address:'));
for (const issue of issues) {
console.log(chalk.red(` β’ ${issue}`));
}
}
return { score: percentage, issues };
} catch (error) {
console.error(chalk.red(`β Error validating requirement: ${error.message}`));
throw error;
}
}
async generateTests(reqId) {
try {
const reqFile = await this.findRequirementById(reqId);
if (!reqFile) {
throw new Error(`Requirement ${reqId} not found`);
}
const content = await fs.readFile(reqFile, 'utf8');
const normalizedIdForTestDir = this.normalizeReqId(reqId);
const testDir = path.join(this.projectRoot, 'tests', 'requirements', `req-${normalizedIdForTestDir}`);
// Create test directory
await fs.ensureDir(testDir);
// Create test files
const normalizedId = this.normalizeReqId(reqId);
const featureFile = path.join(testDir, `req-${normalizedId}.feature`);
const stepsFile = path.join(testDir, `req-${normalizedId}.steps.js`);
const unitTestFile = path.join(testDir, `req-${normalizedId}.unit.test.js`);
const e2eTestFile = path.join(testDir, `req-${normalizedId}.e2e.test.js`);
// Extract and create feature file from Gherkin scenarios
const gherkinMatches = content.match(/```gherkin\n([\s\S]*?)\n```/g);
if (gherkinMatches && gherkinMatches.length > 0) {
const gherkinContent = gherkinMatches.map(match =>
match.replace(/```gherkin\n/, '').replace(/\n```/, '')
).join('\n\n');
await fs.writeFile(featureFile, gherkinContent);
}
// Create test templates
await fs.writeFile(stepsFile, this.createStepsTemplate(normalizedId));
await fs.writeFile(unitTestFile, this.createUnitTestTemplate(normalizedId));
await fs.writeFile(e2eTestFile, this.createE2ETestTemplate(normalizedId));
console.log(chalk.green(`β
Test files generated for REQ-${normalizedId}`));
console.log(chalk.blue(`π Test directory: ${testDir}`));
console.log(chalk.blue(`π Files created:`));
console.log(chalk.blue(` - req-${normalizedId}.feature`));
console.log(chalk.blue(` - req-${normalizedId}.steps.js`));
console.log(chalk.blue(` - req-${normalizedId}.unit.test.js`));
console.log(chalk.blue(` - req-${normalizedId}.e2e.test.js`));
} catch (error) {
console.error(chalk.red(`β Error generating tests: ${error.message}`));
throw error;
}
}
/**
* Check for similar existing requirements before creating new ones
*/
async checkForSimilarRequirements(title, options = {}) {
try {
console.log(chalk.blue('π Checking for similar existing requirements...'));
// Extract keywords from title and epic
const keywords = [];
keywords.push(...title.toLowerCase().split(' ').filter(word => word.length > 3));
if (options.epic) {
keywords.push(...options.epic.toLowerCase().split('-'));
}
if (keywords.length === 0) return;
const uniqueKeywords = [...new Set(keywords)];
const searchResults = await this.performSimilaritySearch(uniqueKeywords.join(' '), true);
if (searchResults.length > 0) {
console.log(chalk.yellow('\nβ οΈ POTENTIAL DUPLICATES FOUND!'));
console.log(chalk.yellow('=' .repeat(50)));
console.log(chalk.red(`\nπ¨ Found ${searchResults.length} similar requirement(s) that might already cover this functionality:`));
searchResults.slice(0, 5).forEach((result, index) => {
console.log(`\n${chalk.cyan(`${index + 1}.`)} ${chalk.bold(result.id)}: ${chalk.white(result.title)}`);
console.log(` ${chalk.gray('Status:')} ${this.getStatusColor(result.status)} ${chalk.gray('| Epic:')} ${chalk.blue(result.epic)}`);
if (result.context) {
console.log(` ${chalk.gray('Context:')} ${result.context}`);
}
});
console.log(chalk.yellow('\nπ‘ Consider:'));
console.log(` ${chalk.cyan('sc req show REQ-XXX')} # Review existing requirements`);
console.log(` ${chalk.cyan('sc req update REQ-XXX')} # Enhance existing requirement instead`);
console.log(` ${chalk.cyan('# Press Ctrl+C to cancel')} # Cancel if duplicate found`);
// Give user time to consider
console.log(chalk.blue('\nβ±οΈ Continuing in 5 seconds... (Press Ctrl+C to cancel)'));
await new Promise(resolve => setTimeout(resolve, 5000));
console.log(chalk.green('β
Proceeding with new requirement creation...\n'));
}
} catch (error) {
console.log(chalk.yellow('β οΈ Could not check for similar requirements:', error.message));
}
}
/**
* Perform similarity search (internal method)
*/
async performSimilaritySearch(searchTerms, skipOutput = false) {
const reqsDir = path.join(this.projectRoot, 'supernal-coding', 'requirements');
if (!await fs.pathExists(reqsDir)) return [];
const { execSync } = require('child_process');
const searchPattern = searchTerms.split(' ').filter(term => term.length > 2).join('|');
try {
const grepCommand = `grep -r -l -i "${searchPattern}" "${reqsDir}" --include="*.md" 2>/dev/null || true`;
const results = execSync(grepCommand, { encoding: 'utf8' });
const matchingFiles = results.trim().split('\n').filter(Boolean);
if (matchingFiles.length === 0) return [];
const searchResults = [];
for (const file of matchingFiles.slice(0, 10)) { // Limit for performance
try {
const content = await fs.readFile(file, 'utf8');
const frontmatter = this.extractFrontmatter(content);
const reqId = frontmatter.id || 'Unknown';
const title = frontmatter.title || 'No title';
const status = frontmatter.status || 'Unknown';
const epic = frontmatter.epic || 'No epic';
const contextLines = this.getSearchContext(content, searchPattern);
searchResults.push({
id: reqId,
title,
status,
epic,
context: contextLines[0] || '',
file: path.relative(this.projectRoot, file)
});
} catch (error) {
// Skip files with errors
}
}
return searchResults;
} catch (error) {
return [];
}
}
async searchRequirements(keywords) {
try {
if (!keywords || keywords.length === 0) {
console.log(chalk.yellow('β οΈ Please provide search keywords'));
console.log(chalk.blue('Usage: sc req search "keyword1 keyword2"'));
return;
}
const searchTerms = Array.isArray(keywords) ? keywords.join(' ') : keywords;
console.log(chalk.blue(`π Searching requirements for: "${searchTerms}"`));
console.log(chalk.blue('=' .repeat(60)));
const reqsDir = path.join(this.projectRoot, 'supernal-coding', 'requirements');
if (!await fs.pathExists(reqsDir)) {
console.log(chalk.yellow('β οΈ No requirements directory found'));
return;
}
// Search in all requirement files
const { execSync } = require('child_process');
const searchPattern = searchTerms.split(' ').filter(term => term.length > 2).join('|');
try {
const grepCommand = `grep -r -l -i "${searchPattern}" "${reqsDir}" --include="*.md" 2>/dev/null || true`;
const results = execSync(grepCommand, { encoding: 'utf8' });
const matchingFiles = results.trim().split('\n').filter(Boolean);
if (matchingFiles.length === 0) {
console.log(chalk.yellow('π No requirements found matching your search'));
console.log(chalk.blue('\nπ‘ Try:'));
console.log(` ${chalk.cyan('sc req search "broader terms"')} # Use broader search terms`);
console.log(` ${chalk.cyan('sc req list')} # List all requirements`);
console.log(` ${chalk.cyan('sc req new "Your Feature"')} # Create new requirement`);
return;
}
console.log(chalk.green(`π Found ${matchingFiles.length} matching requirement(s):\n`));
for (const file of matchingFiles) {
try {
const content = await fs.readFile(file, 'utf8');
const frontmatter = this.extractFrontmatter(content);
const reqId = frontmatter.id || 'Unknown';
const title = frontmatter.title || 'No title';
const status = frontmatter.status || 'Unknown';
const epic = frontmatter.epic || 'No epic';
const priority = frontmatter.priority || 'Unknown';
// Show context lines with search terms highlighted
const contextLines = this.getSearchContext(content, searchPattern);
console.log(`${chalk.cyan('β')} ${chalk.bold(reqId)}: ${chalk.white(title)}`);
console.log(` ${chalk.gray('Status:')} ${this.getStatusColor(status)} ${chalk.gray('| Priority:')} ${this.getPriorityColor(priority)} ${chalk.gray('| Epic:')} ${chalk.blue(epic)}`);
if (contextLines.length > 0) {
console.log(` ${chalk.gray('Context:')} ${contextLines[0]}`);
}
console.log(` ${chalk.gray('File:')} ${chalk.dim(path.relative(this.projectRoot, file))}`);
console.log('');
} catch (error) {
console.log(`${chalk.red('β')} Error reading: ${path.relative(this.projectRoot, file)}`);
}
}
console.log(chalk.blue('π‘ Actions:'));
console.log(` ${chalk.cyan('sc req show REQ-XXX')} # View specific requirement`);
console.log(` ${chalk.cyan('sc req start-work REQ-XXX')} # Start working on requirement`);
console.log(` ${chalk.cyan('sc req new "New Feature"')} # Create new requirement if none match`);
} catch (error) {
console.log(chalk.red('β Search failed:', error.message));
}
} catch (error) {
console.log(chalk.red('β Error searching requirements:', error.message));
}
}
/**
* Get search context showing lines containing search terms
*/
getSearchContext(content, searchPattern) {
const lines = content.split('\n');
const regex = new RegExp(searchPattern, 'gi');
const contextLines = [];
for (const line of lines) {
if (regex.test(line)) {
// Clean up the line and highlight matches
const cleanLine = line.trim().replace(/^#+\s*/, '').replace(/^\*\s*/, '');
if (cleanLine.length > 10 && !cleanLine.startsWith('---')) {
contextLines.push(cleanLine.substring(0, 80) + (cleanLine.length > 80 ? '...' : ''));
if (contextLines.length >= 2) break; // Limit context
}
}
}
return contextLines;
}
/**
* Get colored status display
*/
getStatusColor(status) {
switch (status?.toLowerCase()) {
case 'done':
case 'implemented':
return chalk.green(status);
case 'in-progress':
case 'active':
return chalk.yellow(status);
case 'draft':
return chalk.blue(status);
case 'blocked':
return chalk.red(status);
default:
return chalk.gray(status);
}
}
/**
* Get colored priority display
*/
getPriorityColor(priority) {
switch (priority?.toLowerCase()) {
case 'critical':
return chalk.red.bold(priority);
case 'high':
return chalk.red(priority);
case 'medium':
return chalk.yellow(priority);
case 'low':
return chalk.blue(priority);
default:
return chalk.gray(priority);
}
}
async startWork(reqId) {
try {
const reqFile = await this.findRequirementById(reqId);
if (!reqFile) {
throw new Error(`Requirement ${reqId} not found`);
}
const content = await fs.readFile(reqFile, 'utf8');
// Extract requirement title for branch name
const titleMatch = content.match(/title: (.+)/);
const title = titleMatch ? titleMatch[1] : 'unknown-requirement';
const kebabName = title.toLowerCase().replace(/[^a-z0-9]+/g, '-');
// Create git branch - use normalized ID to avoid double prefixes
const normalizedId = this.normalizeReqId(reqId);
const branchName = `feature/req-${normalizedId}-${kebabName}`;
try {
// Check if we're on main and have uncommitted changes
const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
const hasUncommitted = this.hasUncommittedChanges();
if (hasUncommitted && currentBranch === 'main') {
console.log(chalk.yellow('β οΈ You have uncommitted changes on main branch'));
console.log(chalk.blue('π Automatically committing requirement to main first...'));
// Auto-commit requirement to main
await this.autoCommitRequirement(reqId, 'Started work on requirement');
}
// Create and switch to feature branch
execSync(`git checkout -b ${branchName}`, { cwd: this.projectRoot, stdio: 'pipe' });
console.log(chalk.green(`β
Created and switched to branch: ${branchName}`));
// Update requirement with branch info
const frontmatter = this.extractFrontmatter(content);
frontmatter.branch = branchName;
frontmatter.status = 'In Progress';
frontmatter.updated = new Date().toISOString().split('T')[0];
const { body } = this.parseContent(content);
const updatedContent = this.reconstructContent(frontmatter, body);
await fs.writeFile(reqFile, updatedContent);
console.log(chalk.blue(`π Updated requirement with branch information`));
console.log(chalk.green(`π― Status changed to: In Progress`));
console.log(chalk.yellow(`\nπ Ready to implement! Next steps:`));
console.log(chalk.yellow(`1. Implement the requirement following the acceptance criteria`));
console.log(chalk.yellow(`2. Run tests: npm test -- req-${normalizedId}`));
console.log(chalk.yellow(`3. Commit with format: "REQ-${normalizedId}: your commit message"`));
console.log(chalk.yellow(`4. Generate tests: sc req generate-tests ${normalizedId}`));
console.log(chalk.yellow(`5. When complete: sc git-smart merge`));
} catch (gitError) {
console.log(chalk.red(`β Git error: ${gitError.message}`));
console.log(chalk.yellow(`π‘ Make sure you're in a git repository and have no uncommitted changes`));
}
} catch (error) {
console.error(chalk.red(`β Error starting work: ${error.message}`));
throw error;
}
}
// Test template generators
createStepsTemplate(reqId) {
return `const { Given, When, Then } = require('@cucumber/cucumber');
// REQ-${reqId} Step Definitions
// Generated on ${new Date().toISOString().split('T')[0]}
Given('I have a precondition', function () {
// Implement precondition setup
this.pending();
});
When('I perform an action', function () {
// Implement the action
this.pending();
});
Then('I should see the expected result', function () {
// Implement assertion
this.pending();
});
// Add more step definitions based on your Gherkin scenarios
`;
}
createUnitTestTemplate(reqId) {
return `// REQ-${reqId} Unit Tests
// Generated on ${new Date().toISOString().split('T')[0]}
describe('REQ-${reqId} Unit Tests', () => {
beforeEach(() => {
// Setup before each test
});
afterEach(() => {
// Cleanup after each test
});
test('should implement core functionality', () => {
// Implement unit test
expect(true).toBe(true); // Replace with actual test
});
test('should handle error cases', () => {
// Implement error handling test
expect(true).toBe(true); // Replace with actual test
});
// Add more unit tests based on acceptance criteria
});
`;
}
async validateCoverage(reqId) {
const TestCoverageManager = require('./test-coverage');
const manager = new TestCoverageManager();
try {
const coverage = await manager.analyzeCoverage(reqId);
manager.displayCoverageResults(coverage);
} catch (error) {
console.error(chalk.red('β Coverage validation failed:', error.message));
process.exit(1);
}
}
async generateCoverageReport() {
const TestCoverageManager = require('./test-coverage');
const manager = new TestCoverageManager();
try {
const report = await manager.generateCoverageReport();
manager.displayCoverageReport(report);
} catch (error) {
console.error(chalk.red('β Coverage report generation failed:', error.message));
process.exit(1);
}
}
/**
* Check for uncommitted git changes
*/
hasUncommittedChanges() {
try {
const result = execSync('git status --porcelain', { encoding: 'utf8' });
return result.trim().length > 0;
} catch (error) {
return false;
}
}
/**
* Auto-commit requirement changes to main branch
*/
async autoCommitRequirement(reqId, message) {
try {
const normalizedId = this.normalizeReqId(reqId);
const reqFile = await this.findRequirementById(reqId);
if (reqFile) {
// Add only the requirement file
execSync(`git add "${reqFile}"`, { cwd: this.projectRoot });
// Commit with proper REQ format
const commitMessage = `REQ-${normalizedId}: ${message}`;
execSync(`git commit -m "${commitMessage}"`, { cwd: this.projectRoot });
console.log(chalk.green(`β
Committed requirement to main: ${commitMessage}`));
}
} catch (error) {
console.log(chalk.yellow(`β οΈ Could not auto-commit requirement: ${error.message}`));
throw error;
}
}
/**
* Smart workflow automation - combines multiple steps
*/
async smartStartWork(reqId, options = {}) {
try {
console.log(chalk.blue.bold('π Smart Workflow: Starting work on ' + reqId));
console.log(chalk.blue('=' .repeat(50)));
// 1. Auto-commit any requirement changes to main
const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
if (currentBranch === 'main' && this.hasUncommittedChanges()) {
console.log(chalk.blue('π Step 1: Committing requirement changes to main...'));
await this.autoCommitRequirement(reqId, 'Updated requirement for development start');
}
// 2. Start work (creates branch, updates requirement)
console.log(chalk.blue('πΏ Step 2: Creating feature branch and updating requirement...'));
await this.startWork(reqId);
// 3. Auto-commit the updated requirement with branch info
if (this.hasUncommittedChanges()) {
console.log(chalk.blue('πΎ Step 3: Committing requirement status update...'));
const normalizedId = this.normalizeReqId(reqId);
execSync(`git add .`, { cwd: this.projectRoot });
execSync(`git commit -m "REQ-${normalizedId}: Update status to In Progress and add branch info"`, { cwd: this.projectRoot });
console.log(chalk.green('β
Requirement status committed to feature branch'));
}
console.log(chalk.green.bold('\nπ Smart workflow complete! Ready to implement.'));
} catch (error) {
console.error(chalk.red(`β Smart workflow failed: ${error.message}`));
throw error;
}
}
createE2ETestTemplate(reqId) {
return `// REQ-${reqId} End-to-End Tests
// Generated on ${new Date().toISOString().split('T')[0]}
const { test, expect } = require('@playwright/test');
test.describe('REQ-${reqId} E2E Tests', () => {
test.beforeEach(async ({ page }) => {
// Setup before each test
});
test('should complete end-to-end user flow', async ({ page }) => {
// Implement E2E test
// await page.goto('your-app-url');
// await page.click('selector');
// await expect(page.locator('result')).toBeVisible();
});
test('should handle edge cases', async ({ page }) => {
// Implement edge case testing
});
// Add more E2E tests based on user stories
});
`;
}
}
// CLI Interface
async function handleRequirementCommand(action, ...args) {
const manager = new RequirementManager();
try {
// If no action provided, default to list
if (!action) {
console.log(chalk.blue('π No action specified, showing requirements list:\n'));
action = 'list';
}
switch (action) {
case 'new':
case 'create':
if (args.length === 0) {
console.error(chalk.red('β Title is required for new requirement'));
process.exit(1);
}
const title = args[0];
const options = parseOptions(args.slice(1));
await manager.createRequirement(title, options);
break;
case 'list':
case 'ls':
const listOptions = parseOptions(args || []);
await manager.listRequirements(listOptions);
break;
case 'show':
case 'get':
if (args.length === 0) {
console.error(chalk.red('β Requirement ID is required'));
process.exit(1);
}
const reqId = args[0];
const reqFile = await manager.findRequirementById(reqId);
if (reqFile) {
const content = await fs.readFile(reqFile, 'utf8');
console.log(content);
} else {
console.error(chalk.red(`β Requirement ${reqId} not found`));
}
break;
case 'update':
if (args.length < 1) {
console.error(chalk.red('β Requirement ID is required'));
process.exit(1);
}
const updateId = args[0];
const updateOptions = parseOptions(args.slice(1) || []);
await manager.updateRequirement(updateId, updateOptions);
break;
case 'delete':
case 'remove':
if (args.length === 0) {
console.error(chalk.red('β Requirement ID is required'));
process.exit(1);
}
const deleteId = args[0];
const deleteOptions = parseOptions(args.slice(1) || []);
await manager.deleteRequirement(deleteId, deleteOptions);
break;
case 'validate':
if (args.length === 0) {
console.error(chalk.red('β Requirement ID is required for validation'));
process.exit(1);
}
const validateId = args[0];
await manager.validateRequirement(validateId);
break;
case 'generate-tests':
if (args.length === 0) {
console.error(chalk.red('β Requirement ID is required for test generation'));
process.exit(1);
}
const generateTestsId = args[0];
await manager.generateTests(generateTestsId);
break;
case 'validate-coverage':
if (args.length === 0) {
console.error(chalk.red('β Requirement ID is required for coverage validation'));
process.exit(1);
}
const coverageId = args[0];
await manager.validateCoverage(coverageId);
break;
case 'coverage-report':
await manager.generateCoverageReport();
break;
case 'start-work':
if (args.length === 0) {
console.error(chalk.red('β Requirement ID is required for starting work'));
process.exit(1);
}
const startWorkId = args[0];
await manager.startWork(startWorkId);
break;
case 'smart-start':
if (args.length === 0) {
console.error(chalk.red('β Requirement ID is required for smart workflow start'));
process.exit(1);
}
const smartStartId = args[0];
await manager.smartStartWork(smartStartId);
break;
case 'search':
case 'find':
if (args.length === 0) {
console.error(chalk.red('β Search keywords are required'));
console.log(chalk.blue('Usage: sc req search "keywords"'));
process.exit(1);
}
await manager.searchRequirements(args.join(' '));
break;
case 'help':
await manager.showHelp();
break;
default:
console.log(chalk.red(`β Unknown action: "${action}"`));
console.log(chalk.blue('Available actions:\n'));
await manager.showHelp();
break;
}
} catch (error) {
console.error(chalk.red(`β Command failed: ${error.message}`));
process.exit(1);
}
}
function parseOptions(args) {
const options = {};
// Ensure args is an array
if (!Array.isArray(args)) {
args = [];
}
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg && arg.startsWith('--')) {
const key = arg.slice(2);
const value = args[i + 1];
if (value && !value.startsWith('--')) {
options[key] = value;
i++; // Skip the value in next iteration
} else {
options[key] = true;
}
}
}
return options;
}
module.exports = {
RequirementManager,
handleRequirementCommand
};
// If called directly
if (require.main === module) {
const [,, action, ...args] = process.argv;
handleRequirementCommand(action, ...args);
}