UNPKG

ctrlshiftleft

Version:

AI-powered toolkit for embedding QA and security testing into development workflows

758 lines (667 loc) â€ĸ 24.8 kB
#!/usr/bin/env node /** * Ctrl+Shift+Left Overview Generator * Creates a comprehensive security and QA dashboard for the entire project */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); // Configuration const SOURCE_DIR = process.argv[2] || '/Users/johngaspar/CascadeProjects/ctrlshiftleft/demo/src'; const OUTPUT_FILE = process.argv[3] || '/Users/johngaspar/CascadeProjects/ctrlshiftleft/vscode-ext-test/project-overview.html'; const TOOLS_DIR = __dirname; // Project statistics structure const projectStats = { componentsAnalyzed: 0, securityIssues: { critical: 0, high: 0, medium: 0, low: 0, info: 0, total: 0 }, qaChecklist: { passed: 0, failed: 0, needsReview: 0, total: 0 }, testCoverage: { totalComponents: 0, componentsWithTests: 0, coveragePercentage: 0 }, components: [] }; /** * Find all components in the source directory * @param {string} dir - Directory to search * @returns {string[]} - Array of component file paths */ function findComponents(dir) { if (!fs.existsSync(dir)) { console.error(`Directory not found: ${dir}`); return []; } const components = []; // Get all files in the directory const files = fs.readdirSync(dir, { withFileTypes: true }); // Process each file or directory files.forEach(file => { const fullPath = path.join(dir, file.name); if (file.isDirectory()) { // Recursively search subdirectories components.push(...findComponents(fullPath)); } else if (file.name.match(/\.(jsx?|tsx?)$/)) { // Only include JS/TS component files components.push(fullPath); } }); return components; } /** * Analyze a component for security issues * @param {string} componentPath - Path to the component file * @returns {Object} - Analysis results */ function analyzeComponentSecurity(componentPath) { console.log(`Analyzing security for: ${componentPath}`); const analyzer = path.join(TOOLS_DIR, 'analyze-security.js'); try { // Run the security analyzer execSync(`node ${analyzer} ${componentPath}`, { stdio: 'pipe' }); // Read the generated report const reportPath = path.join(TOOLS_DIR, 'security-report.md'); if (fs.existsSync(reportPath)) { const reportContent = fs.readFileSync(reportPath, 'utf8'); // Parse severity counts const securityIssues = { critical: (reportContent.match(/🚨 Critical: (\d+)/)?.[1] || "0"), high: (reportContent.match(/âš ī¸ High: (\d+)/)?.[1] || "0"), medium: (reportContent.match(/âš ī¸ Medium: (\d+)/)?.[1] || "0"), low: (reportContent.match(/â„šī¸ Low: (\d+)/)?.[1] || "0"), info: (reportContent.match(/â„šī¸ Info: (\d+)/)?.[1] || "0") }; // Convert to numbers Object.keys(securityIssues).forEach(key => { securityIssues[key] = parseInt(securityIssues[key], 10); }); // Calculate total securityIssues.total = Object.values(securityIssues).reduce((sum, count) => sum + count, 0); return { securityIssues, reportPath }; } } catch (error) { console.error(`Error analyzing ${componentPath}:`, error.message); } return { securityIssues: { critical: 0, high: 0, medium: 0, low: 0, info: 0, total: 0 }, reportPath: null }; } /** * Generate QA checklist for a component * @param {string} componentPath - Path to the component file * @returns {Object} - Checklist results */ function generateComponentChecklist(componentPath) { console.log(`Generating checklist for: ${componentPath}`); const generator = path.join(TOOLS_DIR, 'generate-checklist.js'); try { // Run the checklist generator execSync(`node ${generator} ${componentPath}`, { stdio: 'pipe' }); // Read the generated checklist const checklistPath = path.join(TOOLS_DIR, 'checklist.md'); if (fs.existsSync(checklistPath)) { const checklistContent = fs.readFileSync(checklistPath, 'utf8'); // Parse checklist stats const qaChecklist = { passed: (checklistContent.match(/✅ Passed: (\d+)/)?.[1] || "0"), failed: (checklistContent.match(/❌ Failed: (\d+)/)?.[1] || "0"), needsReview: (checklistContent.match(/âš ī¸ Needs Review: (\d+)/)?.[1] || "0") }; // Convert to numbers Object.keys(qaChecklist).forEach(key => { qaChecklist[key] = parseInt(qaChecklist[key], 10); }); // Calculate total qaChecklist.total = Object.values(qaChecklist).reduce((sum, count) => sum + count, 0); return { qaChecklist, checklistPath }; } } catch (error) { console.error(`Error generating checklist for ${componentPath}:`, error.message); } return { qaChecklist: { passed: 0, failed: 0, needsReview: 0, total: 0 }, checklistPath: null }; } /** * Check if tests exist for a component * @param {string} componentPath - Path to the component file * @returns {Object} - Test coverage information */ function checkTestCoverage(componentPath) { const componentName = path.basename(componentPath, path.extname(componentPath)); const demoDir = path.dirname(componentPath); const projectRoot = path.dirname(path.dirname(demoDir)); // Common test locations const possibleTestLocations = [ path.join(projectRoot, 'tests', `${componentName}.test.ts`), path.join(projectRoot, 'tests', `${componentName}.test.js`), path.join(projectRoot, 'tests', `${componentName}.spec.ts`), path.join(projectRoot, 'tests', `${componentName}.spec.js`), path.join(projectRoot, 'tests', componentName, 'index.test.js'), path.join(projectRoot, 'tests', componentName, 'index.spec.js'), path.join(demoDir, '__tests__', `${componentName}.test.js`), path.join(demoDir, '__tests__', `${componentName}.test.tsx`) ]; // Check if any test exists const testExists = possibleTestLocations.some(loc => fs.existsSync(loc)); return { hasTests: testExists, testLocations: possibleTestLocations.filter(loc => fs.existsSync(loc)) }; } /** * Get basic component information * @param {string} componentPath - Path to the component file * @returns {Object} - Component information */ function getComponentInfo(componentPath) { const componentName = path.basename(componentPath, path.extname(componentPath)); const relativePath = path.relative(SOURCE_DIR, componentPath); let complexity = 'Low'; let linesOfCode = 0; try { const content = fs.readFileSync(componentPath, 'utf8'); const lines = content.split('\n'); linesOfCode = lines.length; // Simple complexity heuristic based on code length and patterns if (linesOfCode > 300) { complexity = 'High'; } else if (linesOfCode > 150) { complexity = 'Medium'; } // Check for complex patterns const complexPatterns = [ /useEffect/g, /useState/g, /fetch\(/g, /\.reduce/g, /\.map/g, /async/g, /await/g ]; let complexityScore = 0; complexPatterns.forEach(pattern => { const matches = content.match(pattern); if (matches) { complexityScore += matches.length; } }); if (complexityScore > 10) { complexity = 'High'; } else if (complexityScore > 5) { complexity = 'Medium'; } return { name: componentName, path: relativePath, linesOfCode, complexity }; } catch (error) { console.error(`Error analyzing component ${componentPath}:`, error.message); return { name: componentName, path: relativePath, linesOfCode: 0, complexity: 'Unknown' }; } } /** * Generate HTML report * @returns {string} - HTML content */ function generateHTML() { // Function to get severity class const getSeverityClass = (count, threshold) => { if (count === 0) return 'good'; if (count <= threshold) return 'warning'; return 'critical'; }; const html = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Ctrl+Shift+Left Project Overview</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; background-color: #f5f5f5; } .container { max-width: 1200px; margin: 0 auto; background-color: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } header { text-align: center; margin-bottom: 40px; } h1 { color: #2c3e50; margin-bottom: 10px; } .timestamp { color: #7f8c8d; font-size: 14px; } .overview-cards { display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 40px; } .card { flex: 1; min-width: 250px; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); background-color: white; } .card h2 { margin-top: 0; color: #2c3e50; font-size: 18px; border-bottom: 1px solid #eee; padding-bottom: 10px; } .stats { display: flex; justify-content: space-between; flex-wrap: wrap; } .stat-item { text-align: center; padding: 10px; flex: 1; min-width: 80px; } .stat-value { font-size: 24px; font-weight: bold; margin-bottom: 5px; } .stat-label { font-size: 12px; color: #7f8c8d; } .good { color: #27ae60; } .warning { color: #f39c12; } .critical { color: #e74c3c; } table { width: 100%; border-collapse: collapse; margin-bottom: 30px; } th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #ddd; } th { background-color: #f8f9fa; font-weight: 600; } tbody tr:hover { background-color: #f5f5f5; } .security-badge { display: inline-block; padding: 3px 10px; border-radius: 12px; font-size: 12px; font-weight: bold; color: white; } .critical-bg { background-color: #e74c3c; } .high-bg { background-color: #e67e22; } .medium-bg { background-color: #f39c12; } .low-bg { background-color: #3498db; } .info-bg { background-color: #7f8c8d; } .pass-bg { background-color: #27ae60; } .progress-bar { height: 8px; background-color: #ecf0f1; border-radius: 4px; overflow: hidden; margin-top: 5px; } .progress-fill { height: 100%; background-color: #3498db; } .progress-fill.good { background-color: #27ae60; } .progress-fill.warning { background-color: #f39c12; } .progress-fill.critical { background-color: #e74c3c; } footer { text-align: center; margin-top: 40px; color: #7f8c8d; font-size: 14px; } .recommendations { background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; } .recommendations h2 { margin-top: 0; } .recommendation-item { margin-bottom: 15px; } .recommendation-item:last-child { margin-bottom: 0; } </style> </head> <body> <div class="container"> <header> <h1>Ctrl+Shift+Left Project Overview</h1> <p class="timestamp">Generated on ${new Date().toLocaleString()}</p> </header> <div class="overview-cards"> <div class="card"> <h2>Security Analysis</h2> <div class="stats"> <div class="stat-item"> <div class="stat-value ${getSeverityClass(projectStats.securityIssues.critical, 0)}">${projectStats.securityIssues.critical}</div> <div class="stat-label">Critical</div> </div> <div class="stat-item"> <div class="stat-value ${getSeverityClass(projectStats.securityIssues.high, 1)}">${projectStats.securityIssues.high}</div> <div class="stat-label">High</div> </div> <div class="stat-item"> <div class="stat-value ${getSeverityClass(projectStats.securityIssues.medium, 2)}">${projectStats.securityIssues.medium}</div> <div class="stat-label">Medium</div> </div> <div class="stat-item"> <div class="stat-value ${getSeverityClass(projectStats.securityIssues.low, 5)}">${projectStats.securityIssues.low + projectStats.securityIssues.info}</div> <div class="stat-label">Low/Info</div> </div> </div> </div> <div class="card"> <h2>QA Checklist</h2> <div class="stats"> <div class="stat-item"> <div class="stat-value good">${projectStats.qaChecklist.passed}</div> <div class="stat-label">Passed</div> </div> <div class="stat-item"> <div class="stat-value critical">${projectStats.qaChecklist.failed}</div> <div class="stat-label">Failed</div> </div> <div class="stat-item"> <div class="stat-value warning">${projectStats.qaChecklist.needsReview}</div> <div class="stat-label">Review</div> </div> </div> <div class="progress-bar"> <div class="progress-fill ${projectStats.qaChecklist.passed / Math.max(1, projectStats.qaChecklist.total) > 0.7 ? 'good' : 'warning'}" style="width: ${Math.round(projectStats.qaChecklist.passed / Math.max(1, projectStats.qaChecklist.total) * 100)}%"></div> </div> </div> <div class="card"> <h2>Test Coverage</h2> <div class="stats"> <div class="stat-item"> <div class="stat-value ${projectStats.testCoverage.coveragePercentage > 80 ? 'good' : projectStats.testCoverage.coveragePercentage > 50 ? 'warning' : 'critical'}">${projectStats.testCoverage.coveragePercentage}%</div> <div class="stat-label">Coverage</div> </div> <div class="stat-item"> <div class="stat-value">${projectStats.testCoverage.componentsWithTests}/${projectStats.testCoverage.totalComponents}</div> <div class="stat-label">Components</div> </div> </div> <div class="progress-bar"> <div class="progress-fill ${projectStats.testCoverage.coveragePercentage > 80 ? 'good' : projectStats.testCoverage.coveragePercentage > 50 ? 'warning' : 'critical'}" style="width: ${projectStats.testCoverage.coveragePercentage}%"></div> </div> </div> </div> <div class="recommendations"> <h2>Key Recommendations</h2> ${generateRecommendations()} </div> <h2>Component Analysis</h2> <table> <thead> <tr> <th>Component</th> <th>Status</th> <th>Security Issues</th> <th>QA Status</th> <th>Tests</th> <th>Complexity</th> </tr> </thead> <tbody> ${projectStats.components.map(component => ` <tr> <td> <strong>${component.name}</strong><br> <small>${component.path}</small> </td> <td> ${getComponentStatusBadge(component)} </td> <td> ${getSecurityBadges(component.securityIssues)} </td> <td> <span class="security-badge ${getQAStatusClass(component.qaChecklist)}"> ${component.qaChecklist.passed} / ${component.qaChecklist.total} </span> </td> <td> ${component.hasTests ? '<span class="security-badge pass-bg">Yes</span>' : '<span class="security-badge critical-bg">No</span>'} </td> <td> ${component.complexity === 'High' ? '<span class="security-badge medium-bg">High</span>' : component.complexity === 'Medium' ? '<span class="security-badge low-bg">Medium</span>' : '<span class="security-badge info-bg">Low</span>'} </td> </tr> `).join('')} </tbody> </table> <footer> <p>Generated by Ctrl+Shift+Left - Embedding QA and security testing early in development</p> </footer> </div> </body> </html>`; return html; } /** * Generate recommendations based on statistics * @returns {string} - HTML content with recommendations */ function generateRecommendations() { const recommendations = []; // Critical security issues recommendation if (projectStats.securityIssues.critical > 0) { recommendations.push(`<div class="recommendation-item"> <strong>CRITICAL:</strong> Address ${projectStats.securityIssues.critical} critical security vulnerabilities immediately. </div>`); } // High security issues recommendation if (projectStats.securityIssues.high > 0) { recommendations.push(`<div class="recommendation-item"> <strong>HIGH PRIORITY:</strong> Fix ${projectStats.securityIssues.high} high severity security issues. </div>`); } // Test coverage recommendation if (projectStats.testCoverage.coveragePercentage < 50) { recommendations.push(`<div class="recommendation-item"> <strong>TESTING:</strong> Improve test coverage (currently at ${projectStats.testCoverage.coveragePercentage}%). Focus on components without tests. </div>`); } // QA checklist recommendation if (projectStats.qaChecklist.failed > 0) { recommendations.push(`<div class="recommendation-item"> <strong>QA:</strong> Address ${projectStats.qaChecklist.failed} failed QA checklist items. </div>`); } // If no specific recommendations, provide general guidance if (recommendations.length === 0) { recommendations.push(`<div class="recommendation-item"> <strong>GOOD WORK:</strong> No critical issues detected. Continue following security and QA best practices. </div>`); } return recommendations.join(''); } /** * Get security badge HTML based on severity counts * @param {Object} securityIssues - Security issue counts * @returns {string} - HTML content */ function getSecurityBadges(securityIssues) { const badges = []; if (securityIssues.critical > 0) { badges.push(`<span class="security-badge critical-bg">Critical: ${securityIssues.critical}</span>`); } if (securityIssues.high > 0) { badges.push(`<span class="security-badge high-bg">High: ${securityIssues.high}</span>`); } if (securityIssues.medium > 0) { badges.push(`<span class="security-badge medium-bg">Medium: ${securityIssues.medium}</span>`); } if ((securityIssues.low + securityIssues.info) > 0) { badges.push(`<span class="security-badge low-bg">Low/Info: ${securityIssues.low + securityIssues.info}</span>`); } if (badges.length === 0) { badges.push('<span class="security-badge pass-bg">No Issues</span>'); } return badges.join(' '); } /** * Get component status badge * @param {Object} component - Component data * @returns {string} - HTML content */ function getComponentStatusBadge(component) { if (component.securityIssues.critical > 0 || component.securityIssues.high > 0) { return '<span class="security-badge critical-bg">At Risk</span>'; } if (component.securityIssues.medium > 0 || component.qaChecklist.failed > component.qaChecklist.passed) { return '<span class="security-badge medium-bg">Needs Work</span>'; } if (!component.hasTests) { return '<span class="security-badge medium-bg">Untested</span>'; } return '<span class="security-badge pass-bg">Good</span>'; } /** * Get QA status class based on checklist results * @param {Object} qaChecklist - QA checklist data * @returns {string} - CSS class */ function getQAStatusClass(qaChecklist) { const passRate = qaChecklist.passed / Math.max(1, qaChecklist.total); if (passRate > 0.8) return 'pass-bg'; if (passRate > 0.6) return 'medium-bg'; if (passRate > 0.4) return 'high-bg'; return 'critical-bg'; } /** * Main function */ function main() { console.log('Ctrl+Shift+Left Project Overview Generator'); console.log('=========================================='); // Find all components in the source directory const components = findComponents(SOURCE_DIR); console.log(`Found ${components.length} components in ${SOURCE_DIR}`); // Update project statistics projectStats.testCoverage.totalComponents = components.length; // Process each component components.forEach(componentPath => { // Get basic component info const info = getComponentInfo(componentPath); // Analyze component security const securityResult = analyzeComponentSecurity(componentPath); // Generate QA checklist const checklistResult = generateComponentChecklist(componentPath); // Check test coverage const testResult = checkTestCoverage(componentPath); // Update project stats projectStats.componentsAnalyzed++; // Add security issues Object.keys(securityResult.securityIssues).forEach(key => { projectStats.securityIssues[key] += securityResult.securityIssues[key]; }); // Add QA checklist stats Object.keys(checklistResult.qaChecklist).forEach(key => { projectStats.qaChecklist[key] += checklistResult.qaChecklist[key]; }); // Update test coverage if (testResult.hasTests) { projectStats.testCoverage.componentsWithTests++; } // Add component to list projectStats.components.push({ ...info, securityIssues: securityResult.securityIssues, qaChecklist: checklistResult.qaChecklist, hasTests: testResult.hasTests, testLocations: testResult.testLocations }); }); // Calculate test coverage percentage projectStats.testCoverage.coveragePercentage = Math.round( (projectStats.testCoverage.componentsWithTests / Math.max(1, projectStats.testCoverage.totalComponents)) * 100 ); // Generate HTML report const html = generateHTML(); // Write report to file fs.writeFileSync(OUTPUT_FILE, html); console.log(`Project overview generated: ${OUTPUT_FILE}`); // Try to open the report in the default browser try { if (process.platform === 'darwin') { // macOS execSync(`open "${OUTPUT_FILE}"`); } else if (process.platform === 'win32') { // Windows execSync(`start "" "${OUTPUT_FILE}"`); } else { // Linux execSync(`xdg-open "${OUTPUT_FILE}"`); } } catch (error) { console.log('Could not automatically open the report.'); } } // Run the generator main();