UNPKG

ctrlshiftleft

Version:

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

249 lines (210 loc) • 8.22 kB
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; } }