UNPKG

devibe

Version:

Intelligent repository cleanup with auto mode, AI learning, markdown consolidation, auto-consolidate workflow, context-aware classification, and cost optimization

1,162 lines (1,154 loc) โ€ข 89 kB
#!/usr/bin/env node import { Command } from 'commander'; import { GitDetector } from './git-detector.js'; import { SecretScanner } from './secret-scanner.js'; import { FileClassifier } from './file-classifier.js'; import { OperationPlanner, OperationExecutor } from './operation-executor.js'; import { BackupManager } from './backup-manager.js'; import { BuildDetector, BuildValidationService } from './build-validator.js'; import { ConfigManager } from './config.js'; import { AIClassifierFactory } from './ai-classifier.js'; import { createTestOrganizer } from './test-organizer.js'; import { RulePackValidator, formatValidationResult } from './rulepack-validator.js'; import { RepoBestPracticesAnalyzer, formatBestPracticesReport } from './repo-best-practices.js'; import { getKeyManager } from './ai-key-manager.js'; import { getPreferencesManager } from './user-preferences.js'; import { AVAILABLE_MODELS, selectModel, compareModels, estimateCost } from './ai-model-config.js'; import * as fs from 'fs/promises'; import * as path from 'path'; const program = new Command(); program .name('devibe') .description(`UnVibe - Repository cleanup utility for vibe coding sessions ๐Ÿš€ RECOMMENDED: devibe --auto (with AI for 90% accuracy) AI ASSISTANT GUIDE: When the user mentions "run devibe" or asks about cleanup: 1. Run: devibe --auto โ†’ โญ BEST: Auto-organize + compress markdown (with AI) 2. Run: devibe --auto --dry-run โ†’ Preview what would happen 3. Run: devibe scan โ†’ Check for hardcoded secrets (31 types detected) 4. Run: devibe enforce โ†’ Enforce scripts/ and documents/ folders 5. Run: devibe validate โ†’ Test that builds still work 6. Run: devibe organize-tests โ†’ Organize tests by category (unit, e2e, etc.) Auto Mode (RECOMMENDED): - devibe --auto โ†’ Auto-organize + compress markdown with AI (90% accuracy) - devibe --auto --dry-run โ†’ Preview changes before execution - devibe --auto --no-ai โ†’ Use heuristics only (65% accuracy, no API key needed) - devibe --auto --consolidate-docs none โ†’ Disable markdown compression What --auto does: โœ“ Organizes root files into proper directories โœ“ Updates .gitignore files automatically โœ“ Compresses all markdown files into one (with backups) โœ“ Includes related .txt/.log files via AI analysis โœ“ Cleans up backup artifacts โœ“ 100% reversible with backups Markdown compression (included in --auto): โ€ข Consolidates all *.md files into CONSOLIDATED_DOCUMENTATION.md โ€ข Analyzes .txt and .log files with AI (decides what to include) โ€ข Backs up originals to .devibe/backups/ โ€ข Creates BACKUP_INDEX.md for tracking โ€ข Deletes original markdown files after backup โ€ข Updates README.md automatically Test commands: - devibe detect-tests โ†’ List all test files and their categories - devibe organize-tests โ†’ Move tests to organized directories - devibe organize-tests --report โ†’ Generate test organization report Context: This tool cleans up messy repos after AI coding sessions by organizing root files, enforcing folder structure, and detecting secrets - all with 100% reversible backups. Works with or without git repositories.`) .version('3.1.1') .option('--auto', 'Auto-organize repository + compress markdown (RECOMMENDED with AI)', false) .option('--dry-run', 'Preview what would be done without making changes', false) .option('--no-ai', 'Disable AI and use heuristics only (65% vs 90% accuracy)') .option('--consolidate-docs <mode>', 'Control markdown compression: none to disable (default: compress)', 'safe') .option('-p, --path <path>', 'Repository path', process.cwd()) .option('-v, --verbose', 'Enable verbose debug output', false) .action(async (options) => { // Handle --auto mode if (options.auto) { const { AutoExecutor } = await import('./auto-executor.js'); const autoExecutor = new AutoExecutor(); // Handle --no-ai flag: if user explicitly disabled AI, skip all prompts const userDisabledAI = options.ai === false; // --no-ai was explicitly passed if (userDisabledAI) { console.log('\n๐Ÿค– Quick Auto-Organize: Using heuristics (--no-ai specified)\n'); // Temporarily disable AI for this run const oldAnthropicKey = process.env.ANTHROPIC_API_KEY; const oldOpenAIKey = process.env.OPENAI_API_KEY; const oldGoogleKey = process.env.GOOGLE_API_KEY; delete process.env.ANTHROPIC_API_KEY; delete process.env.OPENAI_API_KEY; delete process.env.GOOGLE_API_KEY; } else { // Check if AI is available const aiAvailable = await AIClassifierFactory.isAvailable(); if (aiAvailable) { console.log('\n๐Ÿค– Quick Auto-Organize: AI-powered classification enabled\n'); } // If AI not available, let AutoExecutor handle prompting (it has the smart logic) } try { const result = await autoExecutor.execute({ path: options.path, dryRun: options.dryRun || false, verbose: options.verbose, consolidateDocs: options.consolidateDocs || 'safe', onProgress: (current, total, message) => { if (options.verbose) { console.log(` [${current}/${total}] ${message}`); } else if (current === total) { console.log(`โœ… ${message}\n`); } }, }); if (result.success) { console.log(`Files analyzed: ${result.filesAnalyzed}`); console.log(`Operations completed: ${result.operationsCompleted}`); console.log(`Duration: ${(result.duration / 1000).toFixed(2)}s\n`); if (result.backupManifestId) { console.log(`๐Ÿ“ฆ Backup created: ${result.backupManifestId}`); console.log(` Restore with: devibe restore ${result.backupManifestId}\n`); } } else { console.error(`\nโŒ Auto-organize failed:\n`); for (const error of result.errors) { console.error(` ${error}`); } console.error(''); process.exit(1); } } catch (error) { console.error(`\nโŒ Error: ${error instanceof Error ? error.message : String(error)}\n`); process.exit(1); } return; } // Default action: show status (original behavior) const cwd = options.path; const detector = new GitDetector(); const result = await detector.detectRepositories(cwd); console.log('\n๐Ÿ“Š UnVibe Status\n'); if (result.repositories.length === 0) { console.log('โ„น๏ธ No git repository detected (devibe works without git too)'); console.log(`Working directory: ${cwd}\n`); } else { console.log(`Git repositories: ${result.repositories.length}`); console.log(`Monorepo: ${result.hasMultipleRepos ? 'Yes' : 'No'}\n`); } // Check AI availability const aiAvailable = await AIClassifierFactory.isAvailable(); const provider = await AIClassifierFactory.getPreferredProvider(); console.log('AI Classification:'); if (aiAvailable && provider) { console.log(` โœ“ ${provider === 'anthropic' ? 'Anthropic Claude' : provider === 'google' ? 'Google Gemini' : 'OpenAI GPT-4'} available (90% accuracy)`); } else { console.log(' โš ๏ธ AI unavailable - using heuristics (65% accuracy)'); console.log(' To enable: Run `devibe ai-key add <provider> <api-key>`'); } console.log(); // Check for build script in package.json (if Node.js project) try { const packageJsonPath = path.join(cwd, 'package.json'); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8')); console.log('Build Configuration:'); if (packageJson.scripts?.build) { console.log(` โœ“ Build script configured: "${packageJson.scripts.build}"`); } else { console.log(' โš ๏ธ No build script found'); console.log(' To enable validation: Add "build" script to package.json'); } console.log(); } catch { // Not a Node.js project or no package.json } console.log('Suggested commands:'); console.log(' devibe --auto Quick auto-organize'); console.log(' devibe scan Scan for secrets'); console.log(' devibe plan Plan file distribution'); console.log(' devibe best-practices Analyze repo best practices\n'); }); program .command('detect') .description('Detect git repositories in current directory') .option('-p, --path <path>', 'Path to scan', process.cwd()) .action(async (options) => { const detector = new GitDetector(); const result = await detector.detectRepositories(options.path); console.log('\nGit Repository Detection:\n'); console.log(`Found ${result.repositories.length} repositories`); console.log(`Multiple repos: ${result.hasMultipleRepos ? 'Yes' : 'No'}\n`); for (const repo of result.repositories) { console.log(`${repo.isRoot ? '๐Ÿ“ฆ ROOT:' : ' ๐Ÿ“ Sub:'} ${repo.path}`); } if (result.repositories.length === 0) { console.log('\nโš ๏ธ No git repositories found.'); console.log('Run "git init" to create one.'); } }); program .command('scan') .description('Scan for secrets in files') .option('-p, --path <path>', 'Path to scan', process.cwd()) .action(async (options) => { const scanner = new SecretScanner(); const files = await findSourceFiles(options.path); if (files.length === 0) { console.log('\nโš ๏ธ No source files found to scan.'); return; } console.log(`\nScanning ${files.length} files for secrets...\n`); const result = await scanner.scanFiles(files); console.log(`โœ“ Scanned ${result.filesScanned} files in ${result.duration}ms\n`); if (result.secretsFound === 0) { console.log('โœ“ No secrets detected. You\'re all good!'); } else { console.log(`โš ๏ธ Found ${result.secretsFound} potential secrets:\n`); console.log(` Critical: ${result.summary.critical}`); console.log(` High: ${result.summary.high}`); console.log(` Medium: ${result.summary.medium}`); console.log(` Low: ${result.summary.low}\n`); // Show first 5 findings const topFindings = result.findings.slice(0, 5); for (const finding of topFindings) { console.log(` ${getSeverityIcon(finding.severity)} ${finding.file}:${finding.line}`); console.log(` Type: ${finding.type}`); console.log(` Context: ${finding.context}`); console.log(` Fix: ${finding.recommendation}\n`); } if (result.findings.length > 5) { console.log(` ... and ${result.findings.length - 5} more findings.\n`); } console.log('โš ๏ธ Please review and remove secrets before committing.\n'); } }); program .command('update-gitignore') .description('Update .gitignore files to exclude .devibe and .unvibe directories') .option('-p, --path <path>', 'Repository path', process.cwd()) .action(async (options) => { const { GitIgnoreManager } = await import('./gitignore-manager.js'); const detector = new GitDetector(); console.log('\n๐Ÿ“ Updating .gitignore files...\n'); const repoResult = await detector.detectRepositories(options.path); if (repoResult.repositories.length === 0) { console.log('โŒ No git repositories found.\n'); return; } const manager = new GitIgnoreManager(); const result = await manager.updateAllRepositories(repoResult.repositories); console.log('Results:\n'); if (result.created.length > 0) { console.log(`โœ… Created .gitignore in ${result.created.length} repositories:`); for (const repoPath of result.created) { console.log(` ${repoPath}`); } console.log(); } if (result.updated.length > 0) { console.log(`โœ… Updated .gitignore in ${result.updated.length} repositories:`); for (const repoPath of result.updated) { console.log(` ${repoPath}`); } console.log(); } if (result.skipped.length > 0) { console.log(`โญ๏ธ Skipped ${result.skipped.length} repositories (already configured):`); for (const repoPath of result.skipped) { console.log(` ${repoPath}`); } console.log(); } if (result.errors.length > 0) { console.log(`โŒ Failed to update ${result.errors.length} repositories:`); for (const error of result.errors) { console.log(` ${error.path}: ${error.error}`); } console.log(); } const totalProcessed = result.created.length + result.updated.length; if (totalProcessed > 0) { console.log(`โœ… Successfully processed ${totalProcessed} repositories\n`); } }); program .command('plan') .description('Plan root file distribution (dry-run)') .option('-p, --path <path>', 'Repository path', process.cwd()) .option('-v, --verbose', 'Enable verbose debug output', false) .option('--auto', 'Automatically organize without prompts', false) .option('--no-ai', 'Use heuristics only (no AI)', false) .option('--consolidate-docs <mode>', 'Consolidate markdown docs: safe or aggressive', 'none') .option('--no-usage-check', 'Skip usage detection for faster processing', false) .action(async (options) => { // Handle --auto mode if (options.auto) { const { AutoExecutor } = await import('./auto-executor.js'); const autoExecutor = new AutoExecutor(); // Handle --no-ai flag with auto mode if (options.ai === false) { console.log('\n๐Ÿค– Auto Mode: Organizing automatically using heuristics (no AI)\n'); // Temporarily disable AI for this run const oldAnthropicKey = process.env.ANTHROPIC_API_KEY; const oldOpenAIKey = process.env.OPENAI_API_KEY; const oldGoogleKey = process.env.GOOGLE_API_KEY; delete process.env.ANTHROPIC_API_KEY; delete process.env.OPENAI_API_KEY; delete process.env.GOOGLE_API_KEY; } else { console.log('\n๐Ÿค– Auto Mode: AI will analyze and plan all operations automatically\n'); } try { const preview = await autoExecutor.preview({ path: options.path, verbose: options.verbose, onProgress: (current, total, message) => { if (options.verbose) { console.log(` [${current}/${total}] ${message}`); } else { const percentage = Math.round((current / total) * 100); process.stdout.write(`\r Progress: ${percentage}% - ${message.substring(0, 60).padEnd(60)}`); } }, }); if (!options.verbose) { process.stdout.write('\r' + ' '.repeat(80) + '\r'); } console.log(`\nโœ“ AI analysis complete!\n`); if (preview.operations.length === 0) { console.log('โœ“ No operations needed. Repository is clean!\n'); return; } if (preview.warnings.length > 0) { console.log('โš ๏ธ Warnings:\n'); for (const warning of preview.warnings) { console.log(` ${warning}`); } console.log(''); } console.log(`Found ${preview.operations.length} operations:\n`); const moveOps = preview.operations.filter(op => op.type === 'move'); const deleteOps = preview.operations.filter(op => op.type === 'delete'); if (moveOps.length > 0) { console.log(`๐Ÿ“ฆ MOVE Operations (${moveOps.length}):\n`); for (const op of moveOps) { console.log(` ${path.basename(op.sourcePath)}`); if (op.targetPath) { console.log(` โ†’ ${op.targetPath}`); } console.log(` ${op.reason}\n`); } } if (deleteOps.length > 0) { console.log(`๐Ÿ—‘๏ธ DELETE Operations (${deleteOps.length}):\n`); for (const op of deleteOps) { console.log(` ${path.basename(op.sourcePath)}`); console.log(` ${op.reason}\n`); } } console.log(`Estimated duration: ${preview.estimatedDuration}ms\n`); console.log('Run "devibe execute --auto" to apply these changes.\n'); } catch (error) { console.error(`\nโŒ Error: ${error instanceof Error ? error.message : String(error)}\n`); process.exit(1); } return; } // Original plan logic const detector = new GitDetector(); const classifier = new FileClassifier(); // Conditionally create usage detector let usageDetector = undefined; if (options.usageCheck !== false) { const { UsageDetector } = await import('./usage-detector.js'); usageDetector = new UsageDetector(); } const planner = new OperationPlanner(detector, classifier, usageDetector); console.log('\n๐Ÿ“‹ Planning root file distribution...\n'); // Show AI startup banner if AI is available const aiAvailable = await AIClassifierFactory.isAvailable(); if (aiAvailable) { const { showAIStartupBanner } = await import('./ai-cost-advisor.js'); // Estimate file count (quick scan of root directory) try { const entries = await fs.readdir(options.path, { withFileTypes: true }); const estimatedFiles = entries.filter(e => e.isFile() && !e.name.startsWith('.') && !['package.json', 'package-lock.json', 'tsconfig.json', 'README.md', 'LICENSE'].includes(e.name)).length; await showAIStartupBanner(estimatedFiles || 10); } catch { await showAIStartupBanner(10); // Default estimate if scan fails } } else { console.log('โš ๏ธ AI unavailable - using heuristics only'); console.log(' To enable AI: devibe ai-key add <provider> <api-key>\n'); } // Progress callback let lastProgressLine = ''; const startTime = Date.now(); let lastFileTime = startTime; const plan = await planner.planRootFileDistribution(options.path, (current, total, file) => { const now = Date.now(); const fileTime = now - lastFileTime; const avgTime = (now - startTime) / current; const remaining = Math.round((avgTime * (total - current)) / 1000); lastFileTime = now; if (options.verbose) { // Verbose mode: show each file on new line with timing console.log(` [${current}/${total}] Processing: ${file} (${fileTime}ms, ~${remaining}s remaining)`); } else { // Normal mode: progress bar const percentage = Math.round((current / total) * 100); const progressBar = 'โ–ˆ'.repeat(Math.floor(percentage / 2)) + 'โ–‘'.repeat(50 - Math.floor(percentage / 2)); const progressLine = `\r Progress: [${progressBar}] ${percentage}% (${current}/${total}) - ${file.substring(0, 40).padEnd(40)}`; // Clear previous line and write new one if (lastProgressLine) { process.stdout.write('\r' + ' '.repeat(lastProgressLine.length) + '\r'); } process.stdout.write(progressLine); lastProgressLine = progressLine; } }); // Clear progress line and move to new line if (!options.verbose && lastProgressLine) { process.stdout.write('\r' + ' '.repeat(lastProgressLine.length) + '\r'); } console.log('โœ“ Analysis complete!\n'); if (plan.operations.length === 0) { console.log('โœ“ No operations needed. Repository is clean!\n'); return; } // Show warnings first if (plan.warnings.length > 0) { console.log('โš ๏ธ Warnings:\n'); for (const warning of plan.warnings) { console.log(` ${warning}`); } console.log(''); } console.log(`Found ${plan.operations.length} operations:\n`); // Separate operations by type const moveOps = plan.operations.filter(op => op.type === 'move'); const deleteOps = plan.operations.filter(op => op.type === 'delete'); if (moveOps.length > 0) { console.log(`๐Ÿ“ฆ MOVE Operations (${moveOps.length}):\n`); for (const op of moveOps) { console.log(` ${path.basename(op.sourcePath)}`); if (op.targetPath) { console.log(` โ†’ ${op.targetPath}`); } console.log(` ${op.reason}`); if (op.warning) { console.log(` โš ๏ธ ${op.warning}`); } console.log(''); } } if (deleteOps.length > 0) { console.log(`๐Ÿ—‘๏ธ DELETE Operations (${deleteOps.length}):\n`); for (const op of deleteOps) { console.log(` ${path.basename(op.sourcePath)}`); console.log(` ${op.reason}\n`); } } console.log(`Estimated duration: ${plan.estimatedDuration}ms`); console.log(`Backup required: ${plan.backupRequired ? 'Yes' : 'No'}\n`); console.log('Run "devibe execute" to apply these changes.\n'); }); program .command('execute') .description('Execute planned operations') .option('-p, --path <path>', 'Repository path', process.cwd()) .option('--dry-run', 'Show what would be done without making changes', false) .option('-v, --verbose', 'Enable verbose debug output', false) .option('--auto', 'Automatically execute without prompts', false) .option('--no-ai', 'Use heuristics only (no AI)', false) .option('--consolidate-docs <mode>', 'Consolidate markdown docs: safe or aggressive', 'none') .option('--no-usage-check', 'Skip usage detection for faster processing', false) .action(async (options) => { // Handle --auto mode if (options.auto) { const { AutoExecutor } = await import('./auto-executor.js'); const autoExecutor = new AutoExecutor(); // Handle --no-ai flag with auto mode if (options.ai === false) { console.log('\n๐Ÿค– Auto Mode: Automatically executing using heuristics (no AI)\n'); // Temporarily disable AI for this run const oldAnthropicKey = process.env.ANTHROPIC_API_KEY; const oldOpenAIKey = process.env.OPENAI_API_KEY; const oldGoogleKey = process.env.GOOGLE_API_KEY; delete process.env.ANTHROPIC_API_KEY; delete process.env.OPENAI_API_KEY; delete process.env.GOOGLE_API_KEY; } else { console.log('\n๐Ÿค– Auto Mode: AI will automatically execute all operations\n'); } if (options.dryRun) { console.log('โš ๏ธ Running in DRY-RUN mode - no changes will be made\n'); } try { const result = await autoExecutor.execute({ path: options.path, dryRun: options.dryRun, verbose: options.verbose, consolidateDocs: options.consolidateDocs || 'none', onProgress: (current, total, message) => { if (options.verbose) { console.log(` [${current}/${total}] ${message}`); } else { const percentage = Math.round((current / total) * 100); process.stdout.write(`\r Progress: ${percentage}% - ${message.substring(0, 60).padEnd(60)}`); } }, }); if (!options.verbose) { process.stdout.write('\r' + ' '.repeat(80) + '\r'); } if (result.success) { console.log(`\nโœ… Auto cleanup complete!\n`); console.log(`Files analyzed: ${result.filesAnalyzed}`); console.log(`Operations completed: ${result.operationsCompleted}`); console.log(`Duration: ${(result.duration / 1000).toFixed(2)}s\n`); if (result.backupManifestId && !options.dryRun) { console.log(`๐Ÿ“ฆ Backup created: ${result.backupManifestId}`); console.log(` Restore with: devibe restore ${result.backupManifestId}\n`); } } else { console.log(`\nโš ๏ธ Auto cleanup completed with errors\n`); console.log(`Operations completed: ${result.operationsCompleted}`); console.log(`Operations failed: ${result.operationsFailed}\n`); if (result.errors.length > 0) { console.log('Errors:'); for (const error of result.errors) { console.log(` โŒ ${error}`); } console.log(); } process.exit(1); } } catch (error) { console.error(`\nโŒ Error: ${error instanceof Error ? error.message : String(error)}\n`); process.exit(1); } return; } // Original execute logic const detector = new GitDetector(); const classifier = new FileClassifier(); // Conditionally create usage detector let usageDetector = undefined; if (options.usageCheck !== false) { const { UsageDetector } = await import('./usage-detector.js'); usageDetector = new UsageDetector(); } const planner = new OperationPlanner(detector, classifier, usageDetector); const backupManager = new BackupManager(path.join(options.path, '.unvibe', 'backups')); const executor = new OperationExecutor(backupManager); console.log(`\n${options.dryRun ? '๐Ÿ” DRY RUN: ' : 'โšก '}Executing operations...\n`); // Show AI startup banner if AI is available const aiAvailable = await AIClassifierFactory.isAvailable(); if (aiAvailable) { const { showAIStartupBanner } = await import('./ai-cost-advisor.js'); // Estimate file count (quick scan of root directory) try { const entries = await fs.readdir(options.path, { withFileTypes: true }); const estimatedFiles = entries.filter(e => e.isFile() && !e.name.startsWith('.') && !['package.json', 'package-lock.json', 'tsconfig.json', 'README.md', 'LICENSE'].includes(e.name)).length; await showAIStartupBanner(estimatedFiles || 10); } catch { await showAIStartupBanner(10); // Default estimate if scan fails } } else { console.log('โš ๏ธ AI unavailable - using heuristics only'); console.log(' To enable AI: devibe ai-key add <provider> <api-key>\n'); } if (options.usageCheck === false) { console.log('โš ๏ธ Usage detection disabled\n'); } // Progress callback let lastProgressLine = ''; const startTime = Date.now(); let lastFileTime = startTime; const plan = await planner.planRootFileDistribution(options.path, (current, total, file) => { const now = Date.now(); const fileTime = now - lastFileTime; const avgTime = (now - startTime) / current; const remaining = Math.round((avgTime * (total - current)) / 1000); lastFileTime = now; if (options.verbose) { // Verbose mode: show each file on new line with timing console.log(` [${current}/${total}] Analyzing: ${file} (${fileTime}ms, ~${remaining}s remaining)`); } else { // Normal mode: progress bar const percentage = Math.round((current / total) * 100); const progressBar = 'โ–ˆ'.repeat(Math.floor(percentage / 2)) + 'โ–‘'.repeat(50 - Math.floor(percentage / 2)); const progressLine = `\r Analyzing: [${progressBar}] ${percentage}% (${current}/${total}) - ${file.substring(0, 40).padEnd(40)}`; if (lastProgressLine) { process.stdout.write('\r' + ' '.repeat(lastProgressLine.length) + '\r'); } process.stdout.write(progressLine); lastProgressLine = progressLine; } }); if (!options.verbose && lastProgressLine) { process.stdout.write('\r' + ' '.repeat(lastProgressLine.length) + '\r'); } console.log('โœ“ Analysis complete!\n'); if (plan.operations.length === 0) { console.log('โœ“ No operations to execute.\n'); return; } // Show warnings before executing if (plan.warnings.length > 0) { console.log('โš ๏ธ Warnings:\n'); for (const warning of plan.warnings) { console.log(` ${warning}`); } console.log(''); } const result = await executor.execute(plan, options.dryRun); if (result.success) { console.log(`โœ“ Successfully completed ${result.operationsCompleted} operations\n`); if (result.backupManifestId && !options.dryRun) { console.log(`๐Ÿ“ฆ Backup created: ${result.backupManifestId}\n`); console.log(` Restore with: devibe restore ${result.backupManifestId}\n`); } } else { console.log(`โš ๏ธ Completed ${result.operationsCompleted}, failed ${result.operationsFailed}\n`); for (const error of result.errors) { console.log(` โŒ ${error}`); } } }); program .command('enforce') .description('Enforce folder structure (scripts/, documents/)') .option('-p, --path <path>', 'Repository path', process.cwd()) .option('--dry-run', 'Show what would be done', false) .action(async (options) => { const detector = new GitDetector(); const classifier = new FileClassifier(); const planner = new OperationPlanner(detector, classifier); const backupManager = new BackupManager(path.join(options.path, '.unvibe', 'backups')); const executor = new OperationExecutor(backupManager); console.log(`\n${options.dryRun ? '๐Ÿ” DRY RUN: ' : '๐Ÿ“ '}Enforcing folder structure...\n`); const plan = await planner.planFolderEnforcement(options.path); if (plan.operations.length === 0) { console.log('โœ“ Folder structure is already compliant!\n'); return; } console.log(`Planning ${plan.operations.length} operations:\n`); for (const op of plan.operations) { console.log(` ${op.type.toUpperCase()}: ${path.basename(op.sourcePath)}`); if (op.targetPath) { console.log(` โ†’ ${op.targetPath}`); } } console.log(); const result = await executor.execute(plan, options.dryRun); if (result.success) { console.log(`โœ“ Folder structure enforced successfully!\n`); } else { console.log(`โš ๏ธ Some operations failed. See errors above.\n`); } }); program .command('validate') .description('Validate build systems') .option('-p, --path <path>', 'Repository path', process.cwd()) .action(async (options) => { const detector = new BuildDetector(); const validator = new BuildValidationService(); console.log('\n๐Ÿ”ง Detecting build systems...\n'); const technologies = await detector.detect(options.path); if (technologies.length === 0) { console.log('โš ๏ธ No recognized build systems found.\n'); return; } console.log(`Found: ${technologies.join(', ')}\n`); console.log('Running validations...\n'); const results = await validator.validateAllBuilds(options.path); for (const [tech, result] of results) { const icon = result.success ? 'โœ“' : 'โœ—'; console.log(`${icon} ${tech}: ${result.success ? 'PASSED' : 'FAILED'} (${result.duration}ms)`); if (result.recommendation) { console.log(` ${result.recommendation}\n`); } else if (!result.success) { console.log(` ${result.stderr}\n`); } } console.log(); }); program .command('restore <manifest-id>') .description('Restore from backup') .option('-p, --path <path>', 'Repository path', process.cwd()) .action(async (manifestId, options) => { const backupManager = new BackupManager(path.join(options.path, '.unvibe', 'backups')); console.log(`\nโ™ป๏ธ Restoring from backup ${manifestId}...\n`); try { await backupManager.restore(manifestId); console.log('โœ“ Restore completed successfully!\n'); } catch (error) { console.log(`โŒ Restore failed: ${error.message}\n`); } }); program .command('backups') .description('List all backups') .option('-p, --path <path>', 'Repository path', process.cwd()) .action(async (options) => { const backupManager = new BackupManager(path.join(options.path, '.unvibe', 'backups')); console.log('\n๐Ÿ“ฆ Available Backups:\n'); const backups = await backupManager.listBackups(); if (backups.length === 0) { console.log(' No backups found.\n'); return; } for (const backup of backups) { console.log(` ${backup.id}`); console.log(` Date: ${backup.timestamp.toLocaleString()}`); console.log(` Operations: ${backup.operations.length}`); console.log(` Reversible: ${backup.reversible ? 'Yes' : 'No'}\n`); } }); program .command('yolo') .description('Quick auto-organize (same as --auto)') .option('-p, --path <path>', 'Repository path', process.cwd()) .option('-v, --verbose', 'Enable verbose debug output', false) .option('--consolidate-docs <mode>', 'Consolidate markdown docs: safe or aggressive', 'safe') .action(async (options) => { console.log('\nโšก YOLO MODE - Quick Auto-Organize\n'); console.log('๐Ÿ’ก Tip: "devibe yolo" is equivalent to "devibe --auto"\n'); const { AutoExecutor } = await import('./auto-executor.js'); const autoExecutor = new AutoExecutor(); try { const result = await autoExecutor.execute({ path: options.path, dryRun: false, verbose: options.verbose, consolidateDocs: options.consolidateDocs || 'safe', onProgress: (current, total, message) => { if (options.verbose) { console.log(` [${current}/${total}] ${message}`); } else if (current === total) { console.log(`โœ… ${message}\n`); } }, }); if (result.success) { console.log(`Files analyzed: ${result.filesAnalyzed}`); console.log(`Operations completed: ${result.operationsCompleted}`); console.log(`Duration: ${(result.duration / 1000).toFixed(2)}s\n`); if (result.backupManifestId) { console.log(`๐Ÿ“ฆ Backup created: ${result.backupManifestId}`); console.log(` Restore with: devibe restore ${result.backupManifestId}\n`); } console.log('โœ… YOLO mode completed successfully!\n'); } else { console.error(`\nโŒ Auto-organize failed:\n`); for (const error of result.errors) { console.error(` ${error}`); } console.error(''); process.exit(1); } } catch (error) { console.error(`\nโŒ Error: ${error instanceof Error ? error.message : String(error)}\n`); process.exit(1); } }); program .command('init') .description('Initialize UnVibe configuration') .option('-p, --path <path>', 'Repository path', process.cwd()) .action(async (options) => { console.log('\nโš™๏ธ Initializing UnVibe configuration...\n'); try { await ConfigManager.create(options.path); console.log('โœ“ Created .unvibe.config.js\n'); console.log('Edit this file to customize UnVibe behavior.\n'); } catch (error) { console.log(`โŒ Failed to create config: ${error.message}\n`); } }); program .command('setup-hooks') .description('Setup Git hooks for automated checks') .action(async () => { console.log('\n๐Ÿ”ง Setting up Git hooks...\n'); try { const { execSync } = await import('child_process'); const scriptPath = path.join(__dirname, '../scripts/setup-hooks.sh'); execSync('bash ' + scriptPath, { stdio: 'inherit' }); } catch (error) { console.log(`โŒ Failed to setup hooks: ${error.message}\n`); console.log('Manual setup:'); console.log(' bash scripts/setup-hooks.sh\n'); } }); program .command('organize-tests') .description('Organize test files by category (unit, integration, e2e, etc.)') .option('-p, --path <path>', 'Repository path', process.cwd()) .option('--dry-run', 'Preview changes without executing') .option('--report', 'Generate a report of current test organization') .action(async (options) => { console.log('\n๐Ÿงช Test Organization\n'); const config = await ConfigManager.load(options.path); const testOrganizer = createTestOrganizer(config); if (!testOrganizer) { console.log('โŒ Test organization is not configured.'); console.log(' Run "devibe init" to create a configuration file.\n'); return; } // Generate report if requested if (options.report) { console.log('Analyzing test files...\n'); const report = await testOrganizer.generateReport(options.path); console.log(report); return; } // Plan test organization console.log('Planning test organization...\n'); const plan = await testOrganizer.planTestOrganization(options.path); if (plan.operations.length === 0) { console.log('โœ“ All tests are already organized!\n'); return; } console.log(`Found ${plan.operations.length} test files to organize:\n`); // Group operations by target directory const byDirectory = new Map(); for (const op of plan.operations) { const targetDir = path.dirname(op.targetPath); if (!byDirectory.has(targetDir)) { byDirectory.set(targetDir, []); } byDirectory.get(targetDir).push(op); } // Display grouped operations for (const [targetDir, ops] of byDirectory.entries()) { console.log(`๐Ÿ“ ${targetDir} (${ops.length} files)`); for (const op of ops.slice(0, 5)) { const fileName = path.basename(op.sourcePath); console.log(` โ€ข ${fileName}`); } if (ops.length > 5) { console.log(` ... and ${ops.length - 5} more`); } console.log(); } if (options.dryRun) { console.log('๐Ÿ” Dry run mode - no changes made.\n'); console.log('Run without --dry-run to execute these operations.\n'); return; } // Execute the plan const backupManager = new BackupManager(path.join(options.path, '.unvibe', 'backups')); const executor = new OperationExecutor(backupManager); console.log('Executing test organization...\n'); const result = await executor.execute(plan, false); if (result.success) { console.log(`โœ… Successfully organized ${result.operationsCompleted} test files!\n`); if (result.backupManifestId) { console.log(`๐Ÿ“ฆ Backup: ${result.backupManifestId}`); console.log(` Restore with: devibe restore ${result.backupManifestId}\n`); } } else { console.log(`โš ๏ธ Completed with errors:\n`); for (const error of result.errors) { console.log(` โŒ ${error}`); } console.log(); } }); program .command('detect-tests') .description('Detect all test files in the repository') .option('-p, --path <path>', 'Repository path', process.cwd()) .action(async (options) => { console.log('\n๐Ÿ” Detecting test files...\n'); const config = await ConfigManager.load(options.path); const testOrganizer = createTestOrganizer(config); if (!testOrganizer) { console.log('โŒ Test organization is not configured.\n'); return; } const testFiles = await testOrganizer.detectTestFiles(options.path); console.log(`Found ${testFiles.length} test files:\n`); for (const testFile of testFiles) { const category = await testOrganizer.categorizeTest(testFile); console.log(`[${category.toUpperCase().padEnd(12)}] ${testFile}`); } console.log(); }); program .command('validate-rulepack') .description('Validate a rule pack file against the specification') .argument('<file>', 'Path to rule pack YAML/JSON file') .action(async (file) => { console.log('\n๐Ÿ” Validating Rule Pack...\n'); try { // Read and parse the rule pack const content = await fs.readFile(file, 'utf-8'); let rulePack; if (file.endsWith('.yaml') || file.endsWith('.yml')) { // For YAML, we'd need a YAML parser (js-yaml) // For now, show helpful message console.log('โ„น๏ธ YAML support requires js-yaml package'); console.log(' For now, please use JSON format or convert YAML to JSON\n'); return; } else { rulePack = JSON.parse(content); } // Validate const validator = new RulePackValidator(); const result = await validator.validate(rulePack); // Format and display console.log(formatValidationResult(result)); if (!result.valid) { process.exit(1); } } catch (error) { console.log(`โŒ Failed to validate rule pack: ${error.message}\n`); process.exit(1); } }); program .command('best-practices') .description('Analyze repository against industry best practices') .option('-p, --path <path>', 'Repository path', process.cwd()) .option('--json', 'Output as JSON') .action(async (options) => { console.log('\n๐Ÿ“Š Analyzing Repository Best Practices...\n'); const analyzer = new RepoBestPracticesAnalyzer(); const report = await analyzer.analyze(options.path); if (options.json) { console.log(JSON.stringify(report, null, 2)); } else { console.log(formatBestPracticesReport(report)); // Summary recommendations if (report.score < 90) { console.log('\n๐Ÿ’ก Quick Wins (Auto-fixable):'); const autoFixable = report.checks.filter(c => !c.passed && c.autoFixable); autoFixable.slice(0, 5).forEach(check => { console.log(` โ€ข ${check.name}`); if (check.recommendation) { console.log(` ${check.recommendation}`); } }); console.log(); } // Exit code based on critical issues if (report.summary.critical > 0) { console.log('โŒ Fix critical issues before proceeding\n'); process.exit(1); } else if (report.score >= 75) { console.log('โœ… Repository meets minimum best practices\n'); } else { console.log('โš ๏ธ Consider addressing high-priority issues\n'); } } }); program .command('check-pr') .description('Check if repository is ready for PR/push to main') .option('-p, --path <path>', 'Repository path', process.cwd()) .action(async (options) => { console.log('\n๐Ÿ” Pre-Push Check (simulating GitHub CI)...\n'); let hasErrors = false; // Step 1: Secret Scan console.log('1๏ธโƒฃ Scanning for secrets...'); const scanner = new SecretScanner(); const files = await findSourceFiles(options.path); if (files.length > 0) { const scanResult = await scanner.scanFiles(files); if (scanResult.summary.critical > 0) { console.log(` โŒ CRITICAL: Found ${scanResult.summary.critical} critical secrets!\n`); hasErrors = true; // Show first 3 critical findings const criticalFindings = scanResult.findings .filter((f) => f.severity === 'critical') .slice(0, 3); for (const finding of criticalFindings) { console.log(` ${finding.file}:${finding.line}`); console.log(` ${finding.type}: ${finding.context}\n`); } } else { console.log(' โœ“ No critical secrets found\n'); } } // Step 2: Build Check console.log('2๏ธโƒฃ Checking build...'); try { const { execSync } = await import('child_process'); execSync('npm run build', { stdio: 'ignore', cwd: options.path }); console.log(' โœ“ Build successful\n'); } catch { console.log(' โš ๏ธ Build failed or not configured\n'); } // Step 3: Tests console.log('3๏ธโƒฃ Running tests...'); try { const { execSync } = await import('child_process'); execSync('npm test', { stdio: 'ignore', cwd: options.path }); console.log(' โœ“ All tests passed\n'); } catch { console.log(' โŒ Tests failed\n'); hasErrors = true; } // Step 4: Folder Structure console.log('4๏ธโƒฃ Checking folder structure...'); const detector = new GitDetector(); const classifier = new FileClassifier(); const planner = new OperationPlanner(detector, classifier); const enforcePlan = await planner.planFolderEnforcement(options.path); if (enforcePlan.operations.length > 0) { console.log(` โš ๏ธ WARNING: ${enforcePlan.operations.length} folder structure issues\n`); } else { console.log(' โœ“ Folder structure compliant\n'); } // Final Result console.log('โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”'); if (hasErrors) { console.log('โŒ PUSH BLOCKED: Fix errors above before pushing to main\n'); process.exit(1); } else { console.log('โœ… All checks passed! Safe to push to main\n'); } }); function getSeverityIcon(severity) { switch (severity) { case 'critical': return '๐Ÿ”ด'; case 'high': return '๐ŸŸ '; case 'medium': return '๐ŸŸก'; case 'low': return '๐Ÿ”ต'; default: return 'โšช'; } } async function findSourceFiles(dir) { const files = []; const extensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.go', '.java', '.env']; // Detect git repositories to find .gitignore scope const detector = new GitDetector(); const repoResult = await detector.detectRepositories(dir); const gitRoots = repoResult.repositories.map(r => r.path); async function scan(currentDir) { const entries = await fs.readdir(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); // Find the closest git repository root for this file const gitRoot = findClosestGitRoot(fullPath, gitRoots); // Skip if matches .gitignore from the appropriate repository if (gitRoot && await isIgnoredByGit(fullPath, gitRoot)) { continue; } if (entry.isDirectory()) { if (entry.name !== 'node_modules' && entry.name !== '.git' && entry.name !== 'dist' && entry.name !== 'build') { await scan(fullPath); } } else if (entry.isFile()) { const ext = path.extname(entry.name); if (extensions.includes(ext) || entry.name === '.env') { files.push(fullPath); } } } } await scan(dir); return files; } function findClosestGitRoot(filePath, gitRoots) { // Find the git root that contains this file and is the deepest (most specific) let closest = null; let maxDepth = -1; for (const root of gitRoots) { if (filePath.startsWith(root)) { const depth = root.split(path.sep).length; if (depth > maxDepth) { maxDepth = depth; closest = root; } } } return closest; } async function isIgnoredByGit(filePath, gitRoot) { const gitignorePath = path.join(gitRoot, '.gitignore'); try { const content = await fs.readFile(gitignorePath, 'utf-8'); const patterns = content .split('\n') .map(line => line.trim()) .filter(line => line && !line.startsWith('#')); // Get path relative to git root const relativePath = path.relative(gitRoot, filePath); for (const pattern of patterns) { // Simple pattern matching (supports * wildcards and exact matches) const regex = new RegExp('^' + pattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\?/g, '.') + '(/.*)?$'); if (regex.test(relativePath)) { return true; } // Also check basename for patterns without / if (!pattern.includes('/')) { const basename = path.basename(relativePath); const basenameRegex = new RegExp('^' + pattern .replace(/\./g, '\\.') .replace(/\*/g, '.*') .replace(/\?/g, '.') + '$');