UNPKG

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
#!/usr/bin/env node 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); }