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
JavaScript
#!/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, '.') +
'$');