UNPKG

ctrlshiftleft

Version:

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

137 lines (116 loc) 5.01 kB
import { Command } from 'commander'; import chalk from 'chalk'; import ora from 'ora'; import path from 'path'; import fs from 'fs/promises'; import { glob } from 'glob'; import { LLMService } from '../core/llmService'; /** * Semantic search command for finding potential security issues */ export function searchCommand(program: Command): void { program .command('search') .description('Search codebase for potential security issues using semantic search') .argument('<query>', 'Search query (e.g., "sql injection", "xss vulnerability")') .option('-d, --directory <directory>', 'Directory to search', '.') .option('-e, --extensions <extensions>', 'File extensions to include (comma separated)', 'js,jsx,ts,tsx,html,css') .option('-l, --limit <limit>', 'Maximum number of results to return', '10') .option('-o, --output <file>', 'Output results to a JSON file') .action(async (query: string, options) => { const spinner = ora('Searching codebase...').start(); try { // Initialize LLM service for semantic search const llmService = new LLMService(); // Parse extensions const extensions = options.extensions.split(',').map((ext: string) => ext.trim()); const extensionPattern = extensions.length > 0 ? `**/*.{${extensions.join(',')}}` : '**/*.{js,jsx,ts,tsx,html,css}'; // Resolve search directory const searchDir = path.resolve(process.cwd(), options.directory); // Find all matching files spinner.text = 'Finding files to search...'; const files = await glob(extensionPattern, { cwd: searchDir, ignore: [ '**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**' ], absolute: true }); if (files.length === 0) { spinner.fail('No files found to search.'); return; } spinner.text = `Searching ${files.length} files for "${query}"...`; // Read all files content const searchResults = []; for (const file of files) { try { const content = await fs.readFile(file, 'utf8'); const relativePath = path.relative(process.cwd(), file); // Perform semantic search on content const matches = await llmService.performSemanticSearch(content, query); if (matches.length > 0) { // Add each match with file context for (const match of matches) { searchResults.push({ file: relativePath, ...match }); } } } catch (error) { // Skip files that can't be read continue; } } // Sort results by relevance score searchResults.sort((a, b) => b.score - a.score); // Limit results const limit = parseInt(options.limit, 10); const limitedResults = searchResults.slice(0, limit); if (limitedResults.length === 0) { spinner.succeed('Search completed. No matches found.'); return; } // Format results for display spinner.succeed(`Found ${limitedResults.length} matches.`); console.log('\nSearch Results:'); for (let i = 0; i < limitedResults.length; i++) { const result = limitedResults[i]; console.log(`\n${chalk.cyan(`${i + 1}. ${result.file}`)}`); console.log(` ${chalk.yellow('Relevance:')} ${Math.round(result.score * 100)}%`); console.log(` ${chalk.yellow('Line:')} ${result.line || 'N/A'}`); // Display snippet with highlighting if available if (result.snippet) { console.log(` ${chalk.yellow('Snippet:')}`); console.log(` ${result.snippet.trim().split('\n').join('\n ')}`); } // Display security context if available if (result.securityContext) { console.log(` ${chalk.yellow('Security Context:')}`); console.log(` ${result.securityContext}`); } } // Save results to file if requested if (options.output) { const outputPath = path.resolve(process.cwd(), options.output); await fs.writeFile(outputPath, JSON.stringify({ query, timestamp: new Date().toISOString(), totalFiles: files.length, matches: limitedResults }, null, 2), 'utf8'); console.log(`\n${chalk.green('✓')} Results saved to ${outputPath}`); } } catch (error) { spinner.fail(`Search failed: ${(error as Error).message}`); console.error(chalk.red((error as Error).stack)); process.exit(1); } }); }