ctrlshiftleft
Version:
AI-powered toolkit for embedding QA and security testing into development workflows
249 lines (210 loc) ⢠8.22 kB
text/typescript
import fs from 'fs/promises';
import path from 'path';
import { glob } from 'glob';
import chalk from 'chalk';
import { LLMService } from './llmService';
import { Checklist, ChecklistItem } from '../types/checklistTypes';
import { generateSecurityRiskReport, formatSeverityBadge } from '../utils/securityRiskUtils';
interface ChecklistGeneratorOptions {
type: string;
format: string;
}
export interface ChecklistResult {
items: ChecklistItem[];
itemCount: number;
file: string;
categories: string[];
securityItemsCount?: number;
}
export class ChecklistGenerator {
private options: ChecklistGeneratorOptions;
private llmService: LLMService;
constructor(options: ChecklistGeneratorOptions = { type: 'all', format: 'json' }) {
this.options = {
type: options.type || 'all', // 'qa', 'security', or 'all'
format: options.format || 'json'
};
this.llmService = new LLMService();
}
/**
* Generate QA and security checklist from source code
* @param sourcePath Path to source file or directory
* @param outputDir Optional output directory to save the checklist
* @returns Generated checklist result with additional metadata
*/
async generateChecklist(sourcePath: string, outputDir?: string): Promise<ChecklistResult> {
// Get all source files if sourcePath is a directory
const sourceFiles = await this.getSourceFiles(sourcePath);
// Initialize checklist
const checklist: Checklist = {
title: 'Generated QA and Security Checklist',
description: `Auto-generated ${this.options.type} checklist for ${path.basename(sourcePath)}`,
categories: [],
items: []
};
// Process each source file
for (const sourceFile of sourceFiles) {
const sourceCode = await fs.readFile(sourceFile, 'utf8');
const relativeSourcePath = path.relative(process.cwd(), sourceFile);
// Skip files that don't need checklists
if (this.shouldSkipFile(sourceFile)) {
continue;
}
// Generate checklist with LLM
const fileChecklist = await this.llmService.generateChecklist(
sourceCode,
relativeSourcePath,
this.options.type
);
// Merge into main checklist
this.mergeChecklists(checklist, fileChecklist);
}
// Save checklist to file if outputDir is provided
let outputFile = '';
if (outputDir) {
// Create output directory if it doesn't exist
await fs.mkdir(outputDir, { recursive: true });
// Generate output filename based on source path
const baseName = path.basename(sourcePath).replace(/\.(js|jsx|ts|tsx)$/, '');
outputFile = path.join(outputDir, `${baseName}-checklist.json`);
// Write checklist to file
await fs.writeFile(outputFile, JSON.stringify(checklist, null, 2), 'utf8');
}
// Generate security risk report if security items exist
const securityItems = checklist.items.filter(
item => item.category === 'Security' && item.severity
);
// Print summary of findings
console.log(chalk.bold('\nš Checklist Generation Results:'));
console.log(`Total items: ${chalk.cyan(checklist.items.length)}`);
const securityCount = securityItems.length;
if (securityCount > 0) {
// Count issues by severity
const criticalCount = securityItems.filter(item => item.severity === 'critical').length;
const highCount = securityItems.filter(item => item.severity === 'high').length;
const mediumCount = securityItems.filter(item => item.severity === 'medium').length;
const lowCount = securityItems.filter(item => item.severity === 'low').length;
console.log(`Security issues: ${chalk.cyan(securityCount)}`);
if (criticalCount > 0) console.log(` ${formatSeverityBadge('critical')} ${criticalCount}`);
if (highCount > 0) console.log(` ${formatSeverityBadge('high')} ${highCount}`);
if (mediumCount > 0) console.log(` ${formatSeverityBadge('medium')} ${mediumCount}`);
if (lowCount > 0) console.log(` ${formatSeverityBadge('low')} ${lowCount}`);
// Display detailed security risk report
if (securityItems.some(item => item.riskScore)) {
console.log(generateSecurityRiskReport(securityItems));
}
} else {
console.log(chalk.green('ā No security issues found'));
}
// Show output file location if saved
if (outputFile) {
console.log(chalk.blue(`\nChecklist saved to: ${outputFile}`));
}
// Return checklist result with additional metadata
return {
items: checklist.items,
itemCount: checklist.items.length,
categories: checklist.categories,
file: outputFile,
securityItemsCount: securityItems.length
};
}
/**
* Get all source files from a path (file or directory)
*/
private async getSourceFiles(sourcePath: string): Promise<string[]> {
const stats = await fs.stat(sourcePath);
if (stats.isFile()) {
return [sourcePath];
}
if (stats.isDirectory()) {
// Find all JS/TS files but exclude test files and node_modules
return glob(`${sourcePath}/**/*.{js,jsx,ts,tsx}`, {
ignore: [
'**/node_modules/**',
'**/*.test.{js,jsx,ts,tsx}',
'**/*.spec.{js,jsx,ts,tsx}',
'**/test/**',
'**/tests/**',
'**/dist/**',
'**/build/**'
]
});
}
return [];
}
/**
* Determine if a file should be skipped for checklist generation
*/
private shouldSkipFile(filePath: string): boolean {
const filename = path.basename(filePath).toLowerCase();
return filename.includes('.test.') ||
filename.includes('.spec.') ||
filename.endsWith('.d.ts');
}
/**
* Merge file checklist into main checklist
*/
private mergeChecklists(mainChecklist: Checklist, fileChecklist: any): void {
// Merge categories
if (Array.isArray(fileChecklist.categories)) {
for (const category of fileChecklist.categories) {
if (!mainChecklist.categories.includes(category)) {
mainChecklist.categories.push(category);
}
}
}
// Merge items
if (Array.isArray(fileChecklist.items)) {
for (const item of fileChecklist.items) {
// Ensure unique IDs by prefixing with file-specific identifier if needed
if (mainChecklist.items.some(existingItem => existingItem.id === item.id)) {
item.id = `${item.id}-${mainChecklist.items.length + 1}`;
}
mainChecklist.items.push(item);
}
}
}
/**
* Convert JSON checklist to markdown format
*/
private convertToMarkdown(checklist: Checklist): any {
// Keep the original JSON for processing, but add a markdown property
checklist.markdown = this.generateMarkdown(checklist);
return checklist;
}
/**
* Generate markdown representation of checklist
*/
private generateMarkdown(checklist: Checklist): string {
let markdown = `# ${checklist.title}\n\n`;
markdown += `${checklist.description}\n\n`;
// Group items by category
const itemsByCategory: Record<string, any[]> = {};
for (const category of checklist.categories) {
itemsByCategory[category] = [];
}
for (const item of checklist.items) {
const category = item.category || 'Uncategorized';
if (!itemsByCategory[category]) {
itemsByCategory[category] = [];
}
itemsByCategory[category].push(item);
}
// Generate markdown for each category
for (const category of Object.keys(itemsByCategory)) {
markdown += `## ${category}\n\n`;
for (const item of itemsByCategory[category]) {
markdown += `### ${item.title}\n\n`;
markdown += `- **ID**: ${item.id}\n`;
markdown += `- **Severity**: ${item.severity}\n`;
markdown += `- **Description**: ${item.description}\n`;
if (item.verification) {
markdown += `- **Verification**: ${item.verification}\n`;
}
markdown += '\n';
}
}
return markdown;
}
}