UNPKG

devibe

Version:

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

732 lines 33.4 kB
/** * Repository Best Practices Analyzer * Checks repositories against industry best practices */ import * as fs from 'fs/promises'; import * as path from 'path'; export class RepoBestPracticesAnalyzer { async analyze(repoPath) { const checks = []; // Documentation Checks checks.push(...await this.checkDocumentation(repoPath)); // Git Best Practices checks.push(...await this.checkGit(repoPath)); // Security Best Practices checks.push(...await this.checkSecurity(repoPath)); // Structure Best Practices checks.push(...await this.checkStructure(repoPath)); // Dependencies Best Practices checks.push(...await this.checkDependencies(repoPath)); // CI/CD Best Practices checks.push(...await this.checkCICD(repoPath)); // Licensing Best Practices checks.push(...await this.checkLicensing(repoPath)); // Code Quality Best Practices checks.push(...await this.checkQuality(repoPath)); const passed = checks.filter(c => c.passed).length; const failed = checks.filter(c => !c.passed).length; const summary = { critical: checks.filter(c => !c.passed && c.severity === 'critical').length, high: checks.filter(c => !c.passed && c.severity === 'high').length, medium: checks.filter(c => !c.passed && c.severity === 'medium').length, low: checks.filter(c => !c.passed && c.severity === 'low').length, }; // Calculate weighted score const score = this.calculateScore(checks); return { totalChecks: checks.length, passed, failed, score, checks, summary }; } // ======================================================================== // Documentation Best Practices // ======================================================================== async checkDocumentation(repoPath) { const checks = []; // README.md const hasReadme = await this.fileExists(path.join(repoPath, 'README.md')); checks.push({ id: 'doc-readme', category: 'documentation', name: 'README.md exists', description: 'Project should have a README.md file', severity: 'critical', passed: hasReadme, message: hasReadme ? 'README.md found' : 'README.md is missing', recommendation: hasReadme ? undefined : 'Create a README.md with project overview, installation, and usage instructions', autoFixable: true }); if (hasReadme) { const readmeContent = await fs.readFile(path.join(repoPath, 'README.md'), 'utf-8'); const readmeSize = readmeContent.length; // README should be substantial (not just placeholder) checks.push({ id: 'doc-readme-content', category: 'documentation', name: 'README has substantial content', description: 'README should have meaningful documentation (>500 chars)', severity: 'high', passed: readmeSize > 500, message: readmeSize > 500 ? `README has ${readmeSize} characters` : `README is too short (${readmeSize} characters)`, recommendation: readmeSize > 500 ? undefined : 'Expand README with: project description, installation steps, usage examples, API docs', autoFixable: false }); // Check for essential sections const hasInstallation = /##?\s*(install|getting started|setup)/i.test(readmeContent); checks.push({ id: 'doc-readme-installation', category: 'documentation', name: 'README has installation section', description: 'README should include installation/setup instructions', severity: 'medium', passed: hasInstallation, message: hasInstallation ? 'Installation section found' : 'No installation section', recommendation: hasInstallation ? undefined : 'Add ## Installation or ## Getting Started section', autoFixable: false }); const hasUsage = /##?\s*usage/i.test(readmeContent); checks.push({ id: 'doc-readme-usage', category: 'documentation', name: 'README has usage section', description: 'README should include usage examples', severity: 'medium', passed: hasUsage, message: hasUsage ? 'Usage section found' : 'No usage section', recommendation: hasUsage ? undefined : 'Add ## Usage section with code examples', autoFixable: false }); } // CHANGELOG.md const hasChangelog = await this.fileExists(path.join(repoPath, 'CHANGELOG.md')); checks.push({ id: 'doc-changelog', category: 'documentation', name: 'CHANGELOG.md exists', description: 'Project should track changes in CHANGELOG.md', severity: 'low', passed: hasChangelog, message: hasChangelog ? 'CHANGELOG.md found' : 'CHANGELOG.md is missing', recommendation: hasChangelog ? undefined : 'Create CHANGELOG.md following Keep a Changelog format', autoFixable: true }); // CONTRIBUTING.md const hasContributing = await this.fileExists(path.join(repoPath, 'CONTRIBUTING.md')); checks.push({ id: 'doc-contributing', category: 'documentation', name: 'CONTRIBUTING.md exists', description: 'Open source projects should have contribution guidelines', severity: 'low', passed: hasContributing, message: hasContributing ? 'CONTRIBUTING.md found' : 'CONTRIBUTING.md is missing', recommendation: hasContributing ? undefined : 'Add CONTRIBUTING.md with PR guidelines, code style, and development setup', autoFixable: true }); // API Documentation (for libraries) const hasApiDocs = await this.fileExists(path.join(repoPath, 'docs', 'API.md')); const packageJson = await this.readPackageJson(repoPath); const isLibrary = packageJson?.main || packageJson?.exports; if (isLibrary) { checks.push({ id: 'doc-api', category: 'documentation', name: 'API documentation exists', description: 'Libraries should document their public API', severity: 'medium', passed: hasApiDocs, message: hasApiDocs ? 'API docs found' : 'No API documentation', recommendation: hasApiDocs ? undefined : 'Create docs/API.md or use JSDoc/TSDoc for auto-generated docs', autoFixable: false }); } return checks; } // ======================================================================== // Git Best Practices // ======================================================================== async checkGit(repoPath) { const checks = []; // .gitignore const hasGitignore = await this.fileExists(path.join(repoPath, '.gitignore')); checks.push({ id: 'git-gitignore', category: 'git', name: '.gitignore exists', description: 'Repository should have .gitignore file', severity: 'critical', passed: hasGitignore, message: hasGitignore ? '.gitignore found' : '.gitignore is missing', recommendation: hasGitignore ? undefined : 'Create .gitignore with node_modules/, dist/, .env, etc.', autoFixable: true }); if (hasGitignore) { const gitignoreContent = await fs.readFile(path.join(repoPath, '.gitignore'), 'utf-8'); // Should ignore node_modules const ignoresNodeModules = gitignoreContent.includes('node_modules'); checks.push({ id: 'git-ignore-node-modules', category: 'git', name: '.gitignore includes node_modules', description: 'Should ignore node_modules directory', severity: 'high', passed: ignoresNodeModules, message: ignoresNodeModules ? 'node_modules ignored' : 'node_modules not ignored', recommendation: ignoresNodeModules ? undefined : 'Add "node_modules/" to .gitignore', autoFixable: true }); // Should ignore build outputs const ignoresBuild = /dist|build|out/i.test(gitignoreContent); checks.push({ id: 'git-ignore-build', category: 'git', name: '.gitignore includes build outputs', description: 'Should ignore compiled/build directories', severity: 'high', passed: ignoresBuild, message: ignoresBuild ? 'Build outputs ignored' : 'Build outputs not ignored', recommendation: ignoresBuild ? undefined : 'Add "dist/", "build/", "out/" to .gitignore', autoFixable: true }); // Should ignore .env files const ignoresEnv = /\.env/.test(gitignoreContent); checks.push({ id: 'git-ignore-env', category: 'git', name: '.gitignore includes .env files', description: 'Should ignore environment files with secrets', severity: 'critical', passed: ignoresEnv, message: ignoresEnv ? 'Environment files ignored' : 'Environment files not ignored', recommendation: ignoresEnv ? undefined : 'Add ".env*" to .gitignore to prevent secret leaks', autoFixable: true }); } // .gitattributes (for line endings) const hasGitattributes = await this.fileExists(path.join(repoPath, '.gitattributes')); checks.push({ id: 'git-gitattributes', category: 'git', name: '.gitattributes exists', description: 'Recommended for consistent line endings across platforms', severity: 'low', passed: hasGitattributes, message: hasGitattributes ? '.gitattributes found' : '.gitattributes missing', recommendation: hasGitattributes ? undefined : 'Add .gitattributes with "* text=auto eol=lf"', autoFixable: true }); return checks; } // ======================================================================== // Security Best Practices // ======================================================================== async checkSecurity(repoPath) { const checks = []; // .env.example const hasEnvExample = await this.fileExists(path.join(repoPath, '.env.example')); const hasEnv = await this.fileExists(path.join(repoPath, '.env')); if (hasEnv || hasEnvExample) { checks.push({ id: 'security-env-example', category: 'security', name: '.env.example exists', description: 'Should provide .env.example as template', severity: 'medium', passed: hasEnvExample, message: hasEnvExample ? '.env.example found' : '.env.example missing', recommendation: hasEnvExample ? undefined : 'Create .env.example with placeholder values (no real secrets)', autoFixable: true }); } // SECURITY.md const hasSecurity = await this.fileExists(path.join(repoPath, 'SECURITY.md')); checks.push({ id: 'security-policy', category: 'security', name: 'SECURITY.md exists', description: 'Public projects should have security policy', severity: 'low', passed: hasSecurity, message: hasSecurity ? 'Security policy found' : 'Security policy missing', recommendation: hasSecurity ? undefined : 'Add SECURITY.md with vulnerability reporting instructions', autoFixable: true }); // package.json - no scripts with sudo const packageJson = await this.readPackageJson(repoPath); if (packageJson?.scripts) { const hasSudoScript = Object.values(packageJson.scripts).some((script) => typeof script === 'string' && /sudo/.test(script)); checks.push({ id: 'security-no-sudo', category: 'security', name: 'No sudo in npm scripts', description: 'Scripts should not require sudo/root access', severity: 'high', passed: !hasSudoScript, message: hasSudoScript ? 'Found scripts using sudo' : 'No sudo in scripts', recommendation: hasSudoScript ? 'Remove sudo requirements from npm scripts' : undefined, autoFixable: false }); } // Dependency security (lockfile exists) const hasPackageLock = await this.fileExists(path.join(repoPath, 'package-lock.json')); const hasYarnLock = await this.fileExists(path.join(repoPath, 'yarn.lock')); const hasPnpmLock = await this.fileExists(path.join(repoPath, 'pnpm-lock.yaml')); const hasLockfile = hasPackageLock || hasYarnLock || hasPnpmLock; if (packageJson) { checks.push({ id: 'security-lockfile', category: 'security', name: 'Dependency lockfile exists', description: 'Lockfile ensures reproducible builds and security', severity: 'high', passed: hasLockfile, message: hasLockfile ? 'Lockfile found' : 'No lockfile (package-lock.json, yarn.lock, or pnpm-lock.yaml)', recommendation: hasLockfile ? undefined : 'Commit your package manager\'s lockfile', autoFixable: true }); } return checks; } // ======================================================================== // Structure Best Practices // ======================================================================== async checkStructure(repoPath) { const checks = []; // src/ directory const hasSrc = await this.directoryExists(path.join(repoPath, 'src')); const packageJson = await this.readPackageJson(repoPath); if (packageJson) { checks.push({ id: 'structure-src', category: 'structure', name: 'src/ directory exists', description: 'Source code should be in src/ directory', severity: 'medium', passed: hasSrc, message: hasSrc ? 'src/ directory found' : 'No src/ directory', recommendation: hasSrc ? undefined : 'Move source code to src/ directory', autoFixable: false }); } // tests/ directory const hasTests = await this.directoryExists(path.join(repoPath, 'tests')) || await this.directoryExists(path.join(repoPath, 'test')) || await this.directoryExists(path.join(repoPath, '__tests__')); checks.push({ id: 'structure-tests', category: 'structure', name: 'Test directory exists', description: 'Tests should be organized in tests/ or __tests__/', severity: 'high', passed: hasTests, message: hasTests ? 'Test directory found' : 'No test directory', recommendation: hasTests ? undefined : 'Create tests/ directory and organize tests', autoFixable: true }); // docs/ directory (for projects with documentation) const hasDocs = await this.directoryExists(path.join(repoPath, 'docs')); checks.push({ id: 'structure-docs', category: 'structure', name: 'docs/ directory exists', description: 'Additional documentation should be in docs/', severity: 'low', passed: hasDocs, message: hasDocs ? 'docs/ directory found' : 'No docs/ directory', recommendation: hasDocs ? undefined : 'Create docs/ for additional documentation', autoFixable: true }); // No source files in root const entries = await fs.readdir(repoPath, { withFileTypes: true }); const rootSourceFiles = entries.filter(e => e.isFile() && /\.(ts|js|tsx|jsx)$/.test(e.name) && !e.name.includes('.config') && !e.name.includes('.test')); checks.push({ id: 'structure-no-root-source', category: 'structure', name: 'No source files in root', description: 'Source files should be in src/, not root directory', severity: 'medium', passed: rootSourceFiles.length === 0, message: rootSourceFiles.length === 0 ? 'No source files in root' : `Found ${rootSourceFiles.length} source files in root: ${rootSourceFiles.map(f => f.name).join(', ')}`, recommendation: rootSourceFiles.length === 0 ? undefined : 'Move source files to src/ directory', autoFixable: false }); return checks; } // ======================================================================== // Dependencies Best Practices // ======================================================================== async checkDependencies(repoPath) { const checks = []; const packageJson = await this.readPackageJson(repoPath); if (!packageJson) { return checks; } // Engines field (Node.js version) const hasEngines = !!packageJson.engines; checks.push({ id: 'deps-engines', category: 'dependencies', name: 'Specifies Node.js version', description: 'package.json should specify required Node.js version', severity: 'medium', passed: hasEngines, message: hasEngines ? `Node version specified: ${packageJson.engines?.node}` : 'No Node.js version specified', recommendation: hasEngines ? undefined : 'Add "engines": { "node": ">=18.0.0" } to package.json', autoFixable: true }); // No wildcards in dependencies const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies }; const hasWildcards = Object.values(allDeps || {}).some((version) => version === '*' || version === 'latest'); checks.push({ id: 'deps-no-wildcards', category: 'dependencies', name: 'No wildcard versions', description: 'Dependencies should have specific version ranges', severity: 'high', passed: !hasWildcards, message: hasWildcards ? 'Found wildcard versions (* or latest)' : 'All dependencies have specific versions', recommendation: hasWildcards ? 'Replace "*" and "latest" with specific semver ranges (e.g., "^1.0.0")' : undefined, autoFixable: false }); // Separate dev dependencies const hasDevDeps = !!packageJson.devDependencies && Object.keys(packageJson.devDependencies).length > 0; const hasDeps = !!packageJson.dependencies && Object.keys(packageJson.dependencies).length > 0; if (hasDeps) { checks.push({ id: 'deps-dev-separated', category: 'dependencies', name: 'Development dependencies separated', description: 'Dev dependencies should be in devDependencies', severity: 'low', passed: hasDevDeps, message: hasDevDeps ? 'devDependencies used' : 'No devDependencies section', recommendation: hasDevDeps ? undefined : 'Move build tools and test frameworks to devDependencies', autoFixable: false }); } return checks; } // ======================================================================== // CI/CD Best Practices // ======================================================================== async checkCICD(repoPath) { const checks = []; // GitHub Actions const hasGitHubActions = await this.directoryExists(path.join(repoPath, '.github', 'workflows')); // GitLab CI const hasGitLabCI = await this.fileExists(path.join(repoPath, '.gitlab-ci.yml')); // Circle CI const hasCircleCI = await this.fileExists(path.join(repoPath, '.circleci', 'config.yml')); // Travis CI const hasTravisCI = await this.fileExists(path.join(repoPath, '.travis.yml')); const hasCI = hasGitHubActions || hasGitLabCI || hasCircleCI || hasTravisCI; checks.push({ id: 'cicd-exists', category: 'ci-cd', name: 'CI/CD configured', description: 'Project should have automated testing/deployment', severity: 'medium', passed: hasCI, message: hasCI ? 'CI/CD configuration found' : 'No CI/CD configuration', recommendation: hasCI ? undefined : 'Add GitHub Actions, GitLab CI, or other CI/CD platform', autoFixable: true }); // Pre-commit hooks const hasHusky = await this.fileExists(path.join(repoPath, '.husky')); const packageJson = await this.readPackageJson(repoPath); const hasPreCommitScript = packageJson?.scripts?.['pre-commit']; const hasPreCommitHooks = hasHusky || hasPreCommitScript; checks.push({ id: 'cicd-pre-commit', category: 'ci-cd', name: 'Pre-commit hooks configured', description: 'Should validate code before commits', severity: 'low', passed: hasPreCommitHooks, message: hasPreCommitHooks ? 'Pre-commit hooks found' : 'No pre-commit hooks', recommendation: hasPreCommitHooks ? undefined : 'Add Husky or similar for pre-commit linting/testing', autoFixable: true }); return checks; } // ======================================================================== // Licensing Best Practices // ======================================================================== async checkLicensing(repoPath) { const checks = []; // LICENSE file const hasLicense = await this.fileExists(path.join(repoPath, 'LICENSE')) || await this.fileExists(path.join(repoPath, 'LICENSE.md')) || await this.fileExists(path.join(repoPath, 'LICENSE.txt')); checks.push({ id: 'license-file', category: 'licensing', name: 'LICENSE file exists', description: 'Open source projects must have a license', severity: 'high', passed: hasLicense, message: hasLicense ? 'LICENSE file found' : 'LICENSE file missing', recommendation: hasLicense ? undefined : 'Add LICENSE file (MIT, Apache-2.0, GPL-3.0, etc.)', autoFixable: true }); // package.json license field const packageJson = await this.readPackageJson(repoPath); const hasLicenseField = !!packageJson?.license; if (packageJson) { checks.push({ id: 'license-package-json', category: 'licensing', name: 'License in package.json', description: 'package.json should specify license', severity: 'medium', passed: hasLicenseField, message: hasLicenseField ? `License: ${packageJson.license}` : 'No license field in package.json', recommendation: hasLicenseField ? undefined : 'Add "license": "MIT" (or your chosen license) to package.json', autoFixable: true }); } return checks; } // ======================================================================== // Code Quality Best Practices // ======================================================================== async checkQuality(repoPath) { const checks = []; const packageJson = await this.readPackageJson(repoPath); if (!packageJson) { return checks; } // Linting configuration const hasEslint = await this.fileExists(path.join(repoPath, '.eslintrc.js')) || await this.fileExists(path.join(repoPath, '.eslintrc.json')) || await this.fileExists(path.join(repoPath, 'eslint.config.js')) || !!packageJson.eslintConfig; checks.push({ id: 'quality-eslint', category: 'quality', name: 'ESLint configured', description: 'JavaScript/TypeScript projects should use ESLint', severity: 'medium', passed: hasEslint, message: hasEslint ? 'ESLint configuration found' : 'No ESLint configuration', recommendation: hasEslint ? undefined : 'Add .eslintrc.js or eslint.config.js', autoFixable: true }); // Formatting const hasPrettier = await this.fileExists(path.join(repoPath, '.prettierrc')) || await this.fileExists(path.join(repoPath, '.prettierrc.json')) || await this.fileExists(path.join(repoPath, 'prettier.config.js')) || !!packageJson.prettier; checks.push({ id: 'quality-prettier', category: 'quality', name: 'Prettier configured', description: 'Projects should use automatic code formatting', severity: 'low', passed: hasPrettier, message: hasPrettier ? 'Prettier configuration found' : 'No Prettier configuration', recommendation: hasPrettier ? undefined : 'Add .prettierrc for consistent code formatting', autoFixable: true }); // TypeScript const hasTypeScript = await this.fileExists(path.join(repoPath, 'tsconfig.json')); if (hasTypeScript) { checks.push({ id: 'quality-typescript', category: 'quality', name: 'TypeScript configured', description: 'TypeScript projects should have tsconfig.json', severity: 'medium', passed: hasTypeScript, message: 'TypeScript configuration found', recommendation: undefined, autoFixable: false }); } // Test scripts const hasTestScript = !!packageJson.scripts?.test; checks.push({ id: 'quality-test-script', category: 'quality', name: 'Test script defined', description: 'package.json should have "test" script', severity: 'high', passed: hasTestScript, message: hasTestScript ? 'Test script found' : 'No test script in package.json', recommendation: hasTestScript ? undefined : 'Add "test" script to package.json (e.g., "vitest run")', autoFixable: true }); // Build script (for libraries/apps) const hasBuildScript = !!packageJson.scripts?.build; const isApp = packageJson.main || packageJson.bin || packageJson.exports; if (isApp) { checks.push({ id: 'quality-build-script', category: 'quality', name: 'Build script defined', description: 'Libraries/apps should have "build" script', severity: 'medium', passed: hasBuildScript, message: hasBuildScript ? 'Build script found' : 'No build script in package.json', recommendation: hasBuildScript ? undefined : 'Add "build" script to package.json (e.g., "tsc")', autoFixable: true }); } // EditorConfig const hasEditorConfig = await this.fileExists(path.join(repoPath, '.editorconfig')); checks.push({ id: 'quality-editorconfig', category: 'quality', name: 'EditorConfig configured', description: 'Recommended for consistent editor settings', severity: 'low', passed: hasEditorConfig, message: hasEditorConfig ? 'EditorConfig found' : 'No .editorconfig', recommendation: hasEditorConfig ? undefined : 'Add .editorconfig for consistent indentation, line endings, etc.', autoFixable: true }); return checks; } // ======================================================================== // Utility Methods // ======================================================================== async fileExists(filePath) { try { const stat = await fs.stat(filePath); return stat.isFile(); } catch { return false; } } async directoryExists(dirPath) { try { const stat = await fs.stat(dirPath); return stat.isDirectory(); } catch { return false; } } async readPackageJson(repoPath) { try { const content = await fs.readFile(path.join(repoPath, 'package.json'), 'utf-8'); return JSON.parse(content); } catch { return null; } } calculateScore(checks) { // Weighted scoring system const weights = { critical: 10, high: 5, medium: 2, low: 1 }; let totalWeight = 0; let achievedWeight = 0; for (const check of checks) { const weight = weights[check.severity]; totalWeight += weight; if (check.passed) { achievedWeight += weight; } } return totalWeight === 0 ? 100 : Math.round((achievedWeight / totalWeight) * 100); } } /** * Format best practices report for CLI output */ export function formatBestPracticesReport(report) { let output = ''; output += '📊 Repository Best Practices Report\n\n'; output += `Score: ${report.score}/100 ${getScoreEmoji(report.score)}\n`; output += `Passed: ${report.passed}/${report.totalChecks}\n`; output += `Failed: ${report.failed}/${report.totalChecks}\n\n`; if (report.summary.critical > 0) { output += `🔴 Critical Issues: ${report.summary.critical}\n`; } if (report.summary.high > 0) { output += `🟠 High Priority: ${report.summary.high}\n`; } if (report.summary.medium > 0) { output += `🟡 Medium Priority: ${report.summary.medium}\n`; } if (report.summary.low > 0) { output += `🔵 Low Priority: ${report.summary.low}\n`; } output += '\n'; // Group by category const byCategory = new Map(); for (const check of report.checks) { if (!byCategory.has(check.category)) { byCategory.set(check.category, []); } byCategory.get(check.category).push(check); } for (const [category, checks] of byCategory) { const passed = checks.filter(c => c.passed).length; const total = checks.length; output += `\n${getCategoryEmoji(category)} ${category.toUpperCase()} (${passed}/${total})\n`; output += '─'.repeat(50) + '\n'; for (const check of checks.filter(c => !c.passed)) { output += `\n${getSeverityIcon(check.severity)} ${check.name}\n`; output += ` ${check.message}\n`; if (check.recommendation) { output += ` 💡 ${check.recommendation}\n`; } if (check.autoFixable) { output += ` ✨ Auto-fixable\n`; } } } return output; } function getScoreEmoji(score) { if (score >= 90) return '🏆'; if (score >= 75) return '✅'; if (score >= 60) return '⚠️'; return '❌'; } function getSeverityIcon(severity) { switch (severity) { case 'critical': return '🔴'; case 'high': return '🟠'; case 'medium': return '🟡'; case 'low': return '🔵'; default: return '⚪'; } } function getCategoryEmoji(category) { switch (category) { case 'documentation': return '📚'; case 'git': return '🔧'; case 'security': return '🔒'; case 'structure': return '📁'; case 'dependencies': return '📦'; case 'ci-cd': return '🚀'; case 'licensing': return '⚖️'; case 'quality': return '✨'; default: return '📋'; } } //# sourceMappingURL=repo-best-practices.js.map