ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
252 lines (204 loc) • 8.47 kB
JavaScript
#!/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-reports/)
[](./tests/)
[](./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();