supernal-coding
Version:
Comprehensive development workflow CLI with kanban task management, project validation, git safety hooks, and cross-project distribution system
597 lines (506 loc) • 19.7 kB
JavaScript
/**
* Workflow Guard System
*
* Prevents common workflow violations and ensures proper requirement-driven development
*/
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const { execSync } = require('child_process');
class WorkflowGuard {
constructor(options = {}) {
this.projectRoot = options.projectRoot || process.cwd();
this.verbose = options.verbose || false;
}
/**
* Check if currently on main branch
*/
isOnMainBranch() {
try {
const branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
return branch === 'main' || branch === 'master';
} catch (error) {
return false;
}
}
/**
* Check if commit has requirement reference
*/
hasRequirementReference(commitMessage) {
const reqPattern = /\b(REQ-\d+|req-\d+)\b/i;
return reqPattern.test(commitMessage);
}
/**
* Check if changes are significant (not just docs or minor fixes)
*/
areChangesSignificant(files) {
const significantPatterns = [
/^cli\/commands\//,
/^cli\/index\.js$/,
/\.js$/,
/\.ts$/,
/package\.json$/
];
const minorPatterns = [
/\.md$/,
/README/i,
/CHANGELOG/i,
/\.gitignore$/,
/\.txt$/
];
const significantFiles = files.filter(file =>
significantPatterns.some(pattern => pattern.test(file))
);
const onlyMinorFiles = files.every(file =>
minorPatterns.some(pattern => pattern.test(file))
);
return significantFiles.length > 0 && !onlyMinorFiles;
}
/**
* Pre-commit check
*/
async preCommitCheck() {
console.log(chalk.blue('🔍 Workflow Guard: Pre-commit validation'));
const issues = [];
const warnings = [];
// Check if on main branch
const onMain = this.isOnMainBranch();
// Get staged files
const stagedFiles = this.getStagedFiles();
// Check if changes are significant
const significantChanges = this.areChangesSignificant(stagedFiles);
if (this.verbose) {
console.log(`Debug: onMain=${onMain}, stagedFiles=${JSON.stringify(stagedFiles)}, significantChanges=${significantChanges}`);
}
if (onMain && significantChanges) {
issues.push({
type: 'MAIN_BRANCH_DEVELOPMENT',
message: 'Significant development work detected on main branch',
suggestion: 'Create feature branch: git checkout -b feature/req-XXX-description'
});
}
// Check commit message for requirement reference
const commitMessage = this.getCommitMessage();
if (significantChanges && !this.hasRequirementReference(commitMessage)) {
warnings.push({
type: 'MISSING_REQUIREMENT_REFERENCE',
message: 'No requirement reference found in commit message',
suggestion: 'Include REQ-XXX in commit message or create requirement first'
});
}
// Check for new features without requirements
const hasNewFeatures = this.detectNewFeatures(stagedFiles);
if (hasNewFeatures && !this.hasRequirementReference(commitMessage)) {
// Try to suggest related requirements
const relatedReqs = await this.searchRelatedRequirements(stagedFiles, commitMessage);
issues.push({
type: 'NEW_FEATURE_WITHOUT_REQUIREMENT',
message: 'New feature detected without requirement reference',
suggestion: relatedReqs.length > 0
? `Search existing requirements first: ${relatedReqs.slice(0,3).join(', ')} or create new: sc req new "Feature Name"`
: 'Search existing requirements first: sc req search "keywords" or create new: sc req new "Feature Name"'
});
}
return { issues, warnings, onMain, significantChanges };
}
/**
* Get staged files for commit
*/
getStagedFiles() {
try {
const output = execSync('git diff --cached --name-only', { encoding: 'utf8' });
return output.trim().split('\n').filter(Boolean);
} catch (error) {
return [];
}
}
/**
* Get modified (unstaged) files
*/
getModifiedFiles() {
try {
const output = execSync('git diff --name-only', { encoding: 'utf8' });
return output.trim().split('\n').filter(Boolean);
} catch (error) {
return [];
}
}
/**
* Get commit message (for pre-commit hooks)
*/
getCommitMessage() {
try {
// Try to read from COMMIT_EDITMSG if available
const commitMsgFile = path.join(this.projectRoot, '.git', 'COMMIT_EDITMSG');
if (fs.existsSync(commitMsgFile)) {
return fs.readFileSync(commitMsgFile, 'utf8').trim();
}
} catch (error) {
// Ignore errors
}
return '';
}
/**
* Detect if staged files indicate new features
*/
detectNewFeatures(files) {
const newFeatureIndicators = [
/^cli\/commands\/[^/]+\/[^/]+\.js$/, // New command files
/^cli\/index\.js$/, // CLI modifications
/package\.json$/ // New dependencies
];
return files.some(file =>
newFeatureIndicators.some(pattern => pattern.test(file))
);
}
/**
* Display workflow guidance
*/
showWorkflowGuidance() {
console.log(chalk.blue.bold('📋 Proper Workflow Guide'));
console.log(chalk.blue('=' .repeat(50)));
console.log('');
console.log(chalk.yellow('✅ Correct Process:'));
console.log('1. Search for existing requirements first:');
console.log(` ${chalk.cyan('sc req search "feature keywords"')}`);
console.log('');
console.log('2. Create requirement if none found:');
console.log(` ${chalk.cyan('sc req new "Feature Name" --epic=epic-name --priority=high')}`);
console.log('');
console.log('3. Start work on requirement:');
console.log(` ${chalk.cyan('sc req start-work REQ-XXX')}`);
console.log('');
console.log('4. Commit requirement to main:');
console.log(` ${chalk.cyan('sc git-smart req-commit REQ-XXX "Started work"')}`);
console.log('');
console.log('5. Create feature branch:');
console.log(` ${chalk.cyan('git checkout -b feature/req-XXX-feature-name')}`);
console.log('');
console.log('6. Implement feature on branch');
console.log('');
console.log('7. Merge safely:');
console.log(` ${chalk.cyan('sc git-smart merge')}`);
console.log('');
console.log(chalk.red('❌ What NOT to do:'));
console.log('• Implement features directly on main');
console.log('• Create requirements after implementation');
console.log('• Skip requirement creation for new features');
console.log('');
}
/**
* Install pre-commit hook
*/
async installPreCommitHook() {
const hookPath = path.join(this.projectRoot, '.git', 'hooks', 'pre-commit');
const hookContent = `#!/bin/sh
# Supernal Coding Workflow Guard
node cli/commands/development/workflow-guard.js pre-commit
if [ $? -ne 0 ]; then
echo ""
echo "🔍 Run 'sc workflow guide' for proper workflow guidance"
exit 1
fi
`;
await fs.writeFile(hookPath, hookContent);
await fs.chmod(hookPath, '755');
console.log(chalk.green('✅ Pre-commit hook installed'));
console.log(` ${hookPath}`);
}
/**
* Main execution
*/
async execute(action) {
switch (action) {
case 'pre-commit':
return this.runPreCommitValidation();
case 'pre-add':
return this.runPreAddValidation();
case 'install-hooks':
return this.installPreCommitHook();
case 'guide':
return this.showWorkflowGuidance();
case 'check':
return this.runWorkflowCheck();
default:
return this.showHelp();
}
}
/**
* Run pre-add validation (prevent staging on main without force)
*/
async runPreAddValidation() {
const onMain = this.isOnMainBranch();
const filesToAdd = process.argv.slice(3); // Get files to be added
if (!onMain) {
// Still check for template validation on feature branches
await this.validateTemplates(filesToAdd.length > 0 ? filesToAdd : this.getModifiedFiles());
if (this.verbose) {
console.log(chalk.green('✅ Pre-add: On feature branch, staging allowed'));
}
return;
}
// Check if --force flag is present
const hasForce = process.argv.includes('--force') || process.argv.includes('-f');
if (hasForce) {
console.log(chalk.yellow('⚠️ Pre-add: Force flag detected, allowing staging on main'));
return;
}
// First, validate templates in requirement files
await this.validateTemplates(filesToAdd.length > 0 ? filesToAdd : this.getModifiedFiles());
// Determine if changes are significant
const significantChanges = filesToAdd.length === 0 ||
this.areChangesSignificant(filesToAdd.length > 0 ? filesToAdd : this.getModifiedFiles());
if (significantChanges) {
console.log(chalk.red('❌ STAGING BLOCKED: Significant changes on main branch'));
console.log(chalk.red('=' .repeat(50)));
console.log('');
console.log(chalk.yellow('🚫 You are trying to stage significant changes on the main branch.'));
console.log(chalk.yellow(' This violates the proper development workflow.'));
console.log('');
// Suggest related requirements if available
const relatedReqs = await this.searchRelatedRequirements(
filesToAdd.length > 0 ? filesToAdd : this.getModifiedFiles(),
''
);
if (relatedReqs.length > 0) {
console.log(chalk.blue('💡 Found potentially related requirements:'));
relatedReqs.slice(0, 3).forEach(req => {
console.log(` • ${chalk.cyan(req)}`);
});
console.log('');
}
console.log(chalk.yellow('✅ Proper workflow:'));
console.log('1. Search existing requirements:');
console.log(` ${chalk.cyan('sc req search "keywords"')}`);
console.log('2. Create/update requirement:');
console.log(` ${chalk.cyan('sc req new "Feature Name"')} or ${chalk.cyan('sc req update REQ-XXX')}`);
console.log('3. Start work on requirement:');
console.log(` ${chalk.cyan('sc req start-work REQ-XXX')}`);
console.log('');
console.log(chalk.yellow('🆘 Emergency override:'));
console.log(` ${chalk.cyan('git add --force <files>')} (use with extreme caution)`);
console.log('');
process.exit(1);
}
// Allow minor changes (requirements, documentation)
console.log(chalk.green('✅ Minor changes on main allowed'));
}
/**
* Validate templates in requirement files
*/
async validateTemplates(files = []) {
try {
const { TemplateValidator } = require('../validation/template-validator');
const validator = new TemplateValidator({
projectRoot: this.projectRoot,
verbose: this.verbose
});
// Filter for requirement files
const reqFiles = files.filter(file =>
file.includes('requirements') && file.endsWith('.md') && file.includes('req-')
);
if (reqFiles.length === 0) {
return; // No requirement files to validate
}
if (this.verbose) {
console.log(chalk.blue(`🔍 Validating ${reqFiles.length} requirement template(s)...`));
}
const results = await validator.validateFiles(reqFiles.map(f =>
path.isAbsolute(f) ? f : path.join(this.projectRoot, f)
));
const summary = validator.getSummary(results);
if (summary.shouldBlock) {
console.log(chalk.red('❌ TEMPLATE VALIDATION FAILED'));
console.log(chalk.red('=' .repeat(50)));
console.log('');
console.log(validator.formatResults(results, { verbose: this.verbose }));
console.log(chalk.yellow('🔧 To fix template issues:'));
console.log(` ${chalk.cyan('sc req validate REQ-XXX')} # Validate specific requirement`);
console.log(` ${chalk.cyan('sc test validate templates')} # Validate all templates`);
console.log('');
console.log(chalk.yellow('🆘 Emergency override:'));
console.log(` ${chalk.cyan('git add --force <files>')} # Bypass template validation`);
console.log('');
process.exit(1);
}
if (this.verbose && summary.warnings > 0) {
console.log(chalk.yellow(`⚠️ ${summary.warnings} template warnings found`));
console.log(validator.formatResults(results, { verbose: false }));
}
} catch (error) {
console.log(chalk.yellow(`⚠️ Template validation failed: ${error.message}`));
if (this.verbose) {
console.error(error.stack);
}
}
}
/**
* Validate templates in staged files
*/
async validateStagedTemplates() {
try {
const { TemplateValidator } = require('../validation/template-validator');
const validator = new TemplateValidator({
projectRoot: this.projectRoot,
verbose: this.verbose
});
const results = await validator.validateStagedFiles();
const summary = validator.getSummary(results);
if (summary.shouldBlock) {
console.log(chalk.red('❌ STAGED TEMPLATE VALIDATION FAILED'));
console.log(chalk.red('=' .repeat(50)));
console.log('');
console.log(validator.formatResults(results, { verbose: this.verbose }));
console.log(chalk.yellow('🔧 To fix before committing:'));
console.log(` ${chalk.cyan('sc req validate REQ-XXX')} # Validate specific requirement`);
console.log(` ${chalk.cyan('git restore --staged <file>')} # Unstage incomplete template`);
console.log('');
console.log(chalk.yellow('🆘 Emergency override:'));
console.log(` ${chalk.cyan('git commit --no-verify')} # Bypass validation (not recommended)`);
console.log('');
process.exit(1);
}
} catch (error) {
if (this.verbose) {
console.log(chalk.yellow(`⚠️ Staged template validation failed: ${error.message}`));
}
}
}
/**
* Run pre-commit validation
*/
async runPreCommitValidation() {
// First validate templates in staged files
await this.validateStagedTemplates();
const { issues, warnings, onMain, significantChanges } = await this.preCommitCheck();
if (issues.length > 0) {
console.log(chalk.red('❌ Workflow violations detected:'));
issues.forEach(issue => {
console.log(` • ${issue.message}`);
console.log(` ${chalk.gray(issue.suggestion)}`);
});
console.log('');
this.showWorkflowGuidance();
process.exit(1);
}
if (warnings.length > 0) {
console.log(chalk.yellow('⚠️ Workflow warnings:'));
warnings.forEach(warning => {
console.log(` • ${warning.message}`);
console.log(` ${chalk.gray(warning.suggestion)}`);
});
console.log('');
}
if (onMain && significantChanges) {
console.log(chalk.blue('💡 Consider creating a feature branch for this work'));
}
return true;
}
/**
* Run workflow check (can be called manually)
*/
async runWorkflowCheck() {
console.log(chalk.blue('🔍 Workflow Status Check'));
console.log(chalk.blue('=' .repeat(40)));
const onMain = this.isOnMainBranch();
console.log(`Current branch: ${onMain ? chalk.red('main') : chalk.green('feature branch')}`);
if (onMain) {
console.log(chalk.yellow('⚠️ You are on the main branch'));
console.log(' Consider creating a feature branch for development work');
}
// Check for uncommitted changes
try {
execSync('git diff --quiet && git diff --cached --quiet');
console.log(chalk.green('✅ No uncommitted changes'));
} catch (error) {
console.log(chalk.yellow('⚠️ You have uncommitted changes'));
const staged = this.getStagedFiles();
if (staged.length > 0) {
console.log(` Staged files: ${staged.join(', ')}`);
}
}
console.log('');
console.log(chalk.blue('💡 Quick actions:'));
console.log(` ${chalk.cyan('sc workflow guide')} # Show workflow guidance`);
console.log(` ${chalk.cyan('sc req new "Title"')} # Create new requirement`);
console.log(` ${chalk.cyan('sc git-smart status')} # Check git status`);
}
/**
* Search for related requirements based on files and commit message
*/
async searchRelatedRequirements(files, commitMessage) {
try {
const keywords = this.extractKeywords(files, commitMessage);
if (keywords.length === 0) return [];
const reqPattern = path.join(this.projectRoot, 'supernal-coding', 'requirements', '**', '*.md');
const { execSync } = require('child_process');
// Search for requirements containing these keywords
const searchTerms = keywords.slice(0, 3).join('|'); // Limit to top 3 keywords
const grepCommand = `grep -l -i "${searchTerms}" ${reqPattern} 2>/dev/null || true`;
const results = execSync(grepCommand, { encoding: 'utf8' });
const reqFiles = results.trim().split('\n').filter(Boolean);
// Extract requirement IDs from filenames
const reqIds = reqFiles.map(file => {
const match = file.match(/req-(\d+)/i);
return match ? `REQ-${match[1]}` : null;
}).filter(Boolean);
return [...new Set(reqIds)]; // Remove duplicates
} catch (error) {
return [];
}
}
/**
* Extract keywords from file paths and commit message for requirement search
*/
extractKeywords(files, commitMessage) {
const keywords = new Set();
// Extract from file paths
files.forEach(file => {
const pathParts = file.split('/');
pathParts.forEach(part => {
// Extract meaningful words (ignore common terms)
const words = part.replace(/\.(js|ts|md|json)$/, '')
.split(/[-_.]/)
.filter(word => word.length > 2 && !['cli', 'commands', 'index', 'test', 'spec'].includes(word.toLowerCase()));
words.forEach(word => keywords.add(word.toLowerCase()));
});
});
// Extract from commit message
if (commitMessage) {
const messageWords = commitMessage.toLowerCase()
.replace(/[^\w\s]/g, ' ')
.split(/\s+/)
.filter(word => word.length > 3 && !['test', 'fix', 'add', 'update', 'remove'].includes(word));
messageWords.forEach(word => keywords.add(word));
}
return Array.from(keywords).slice(0, 5); // Return top 5 keywords
}
/**
* Show help
*/
showHelp() {
console.log(chalk.blue.bold('🛡️ Workflow Guard'));
console.log(chalk.blue('=' .repeat(40)));
console.log('');
console.log('Commands:');
console.log(` ${chalk.cyan('pre-commit')} # Validate pre-commit (used by git hook)`);
console.log(` ${chalk.cyan('install-hooks')} # Install git pre-commit hook`);
console.log(` ${chalk.cyan('guide')} # Show workflow guidance`);
console.log(` ${chalk.cyan('check')} # Check current workflow status`);
console.log('');
console.log('Examples:');
console.log(` ${chalk.cyan('node cli/commands/development/workflow-guard.js guide')}`);
console.log(` ${chalk.cyan('node cli/commands/development/workflow-guard.js check')}`);
}
}
// CLI execution
if (require.main === module) {
const action = process.argv[2] || 'help';
const guard = new WorkflowGuard({ verbose: process.argv.includes('--verbose') });
guard.execute(action).catch(error => {
console.error(chalk.red('Error:'), error.message);
process.exit(1);
});
}
module.exports = WorkflowGuard;