ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
758 lines (667 loc) âĸ 24.8 kB
JavaScript
#!/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 = `
<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();