UNPKG

ctrlshiftleft

Version:

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

252 lines (204 loc) 8.47 kB
#!/usr/bin/env node /** * Ctrl+Shift+Left Badge Generator * * Generates SVG badges for security score, test coverage, and QA checklist completion * to be displayed in the repository README. */ const fs = require('fs'); const path = require('path'); // Output directory for badges const BADGES_DIR = path.join(__dirname, '..', 'badges'); // Ensure badges directory exists if (!fs.existsSync(BADGES_DIR)) { fs.mkdirSync(BADGES_DIR, { recursive: true }); } /** * Generates an SVG badge * * @param {string} label - Left side label * @param {string} message - Right side message * @param {string} color - Color for the right side (green, yellow, red, etc.) * @returns {string} SVG content */ function generateBadge(label, message, color) { // Calculate widths based on text length const labelWidth = 10 + label.length * 6.5; const messageWidth = 10 + message.length * 6.5; const totalWidth = labelWidth + messageWidth; return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${totalWidth}" height="20" role="img" aria-label="${label}: ${message}"> <title>${label}: ${message}</title> <linearGradient id="s" x2="0" y2="100%"> <stop offset="0" stop-color="#bbb" stop-opacity=".1"/> <stop offset="1" stop-opacity=".1"/> </linearGradient> <clipPath id="r"> <rect width="${totalWidth}" height="20" rx="3" fill="#fff"/> </clipPath> <g clip-path="url(#r)"> <rect width="${labelWidth}" height="20" fill="#555"/> <rect x="${labelWidth}" width="${messageWidth}" height="20" fill="${color}"/> <rect width="${totalWidth}" height="20" fill="url(#s)"/> </g> <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"> <text aria-hidden="true" x="${labelWidth/2 * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${label.length * 60}">${label}</text> <text x="${labelWidth/2 * 10}" y="140" transform="scale(.1)" fill="#fff" textLength="${label.length * 60}">${label}</text> <text aria-hidden="true" x="${(labelWidth + messageWidth/2) * 10}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="${message.length * 60}">${message}</text> <text x="${(labelWidth + messageWidth/2) * 10}" y="140" transform="scale(.1)" fill="#fff" textLength="${message.length * 60}">${message}</text> </g> </svg>`; } /** * Creates a security score badge based on analysis results */ function generateSecurityBadge() { let critical = 0; let high = 0; let medium = 0; let total = 0; // Parse security reports const securityReportsDir = path.join(__dirname, '..', 'security-reports'); if (fs.existsSync(securityReportsDir)) { const files = fs.readdirSync(securityReportsDir).filter(file => file.endsWith('.md')); files.forEach(file => { const content = fs.readFileSync(path.join(securityReportsDir, file), 'utf8'); // Extract counts from summaries const criticalMatch = content.match(/Critical:\s*(\d+)/); const highMatch = content.match(/High:\s*(\d+)/); const mediumMatch = content.match(/Medium:\s*(\d+)/); if (criticalMatch) critical += parseInt(criticalMatch[1]); if (highMatch) high += parseInt(highMatch[1]); if (mediumMatch) medium += parseInt(mediumMatch[1]); total++; }); } // Calculate security score (0-100) // Formula: 100 - (critical * 10 + high * 5 + medium * 2) let score = 100 - (critical * 10 + high * 5 + medium * 2); score = Math.max(0, Math.min(100, score)); // Ensure score is between 0-100 // Determine color based on score let color; if (score >= 90) color = 'brightgreen'; else if (score >= 70) color = 'green'; else if (score >= 50) color = 'yellow'; else if (score >= 30) color = 'orange'; else color = 'red'; // Generate badge const badge = generateBadge('security', `${score}/100`, color); fs.writeFileSync(path.join(BADGES_DIR, 'security.svg'), badge); console.log(`Security badge created: ${score}/100`); return score; } /** * Creates a test coverage badge based on generated tests */ function generateTestBadge() { let components = new Set(); let testFiles = 0; // Find component files const srcDir = path.join(__dirname, '..', 'demo', 'src', 'components'); if (fs.existsSync(srcDir)) { const files = fs.readdirSync(srcDir) .filter(file => file.endsWith('.tsx') || file.endsWith('.jsx') || file.endsWith('.js')); files.forEach(file => { components.add(path.basename(file, path.extname(file))); }); } // Count test files const testsDir = path.join(__dirname, '..', 'tests'); if (fs.existsSync(testsDir)) { const findTestFiles = (dir) => { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { const entryPath = path.join(dir, entry.name); if (entry.isDirectory()) { findTestFiles(entryPath); } else if (entry.isFile() && entry.name.endsWith('.spec.ts')) { testFiles++; } } }; findTestFiles(testsDir); } // Calculate coverage percentage const componentCount = components.size; const coverage = componentCount > 0 ? Math.round((testFiles / componentCount) * 100) : 0; // Determine color based on coverage let color; if (coverage >= 90) color = 'brightgreen'; else if (coverage >= 70) color = 'green'; else if (coverage >= 50) color = 'yellow'; else if (coverage >= 30) color = 'orange'; else color = 'red'; // Generate badge const badge = generateBadge('tests', `${coverage}%`, color); fs.writeFileSync(path.join(BADGES_DIR, 'tests.svg'), badge); console.log(`Test badge created: ${coverage}% coverage`); return coverage; } /** * Creates a QA checklist badge based on checklist results */ function generateChecklistBadge() { let pass = 0; let fail = 0; let review = 0; let total = 0; // Parse checklist files const checklistDir = path.join(__dirname, '..', 'checklists'); if (fs.existsSync(checklistDir)) { const files = fs.readdirSync(checklistDir).filter(file => file.endsWith('.md')); files.forEach(file => { const content = fs.readFileSync(path.join(checklistDir, file), 'utf8'); // Extract counts from summaries const passMatch = content.match(/Passed:\s*(\d+)/); const failMatch = content.match(/Failed:\s*(\d+)/); const reviewMatch = content.match(/Needs Review:\s*(\d+)/); if (passMatch) pass += parseInt(passMatch[1]); if (failMatch) fail += parseInt(failMatch[1]); if (reviewMatch) review += parseInt(reviewMatch[1]); total++; }); } // Calculate completion percentage (100% = all items passing) total = pass + fail + review; const completion = total > 0 ? Math.round((pass / total) * 100) : 0; // Determine color based on completion let color; if (completion >= 90) color = 'brightgreen'; else if (completion >= 70) color = 'green'; else if (completion >= 50) color = 'yellow'; else if (completion >= 30) color = 'orange'; else color = 'red'; // Generate badge const badge = generateBadge('checklist', `${completion}%`, color); fs.writeFileSync(path.join(BADGES_DIR, 'checklist.svg'), badge); console.log(`QA checklist badge created: ${completion}% complete`); return completion; } /** * Main function */ function main() { console.log('Ctrl+Shift+Left Badge Generator'); console.log('=============================='); const security = generateSecurityBadge(); const tests = generateTestBadge(); const checklist = generateChecklistBadge(); // Generate README badge section const badgeSection = `## Security & QA Status [![Security Score](./badges/security.svg)](./security-reports/) [![Test Coverage](./badges/tests.svg)](./tests/) [![QA Checklist](./badges/checklist.svg)](./checklists/) _Generated by Ctrl+Shift+Left Badge Generator_ `; // Save badge section to file for easy inclusion in README fs.writeFileSync(path.join(BADGES_DIR, 'README-badges.md'), badgeSection); console.log('\nBadge section for README created in badges/README-badges.md'); console.log('\nTo include in your README.md, add:'); console.log('```md'); console.log(badgeSection); console.log('```'); } // Run the generator main();