UNPKG

@diullei/codeguardian

Version:

Open-source developer tool to validate and enforce architectural rules, especially for AI-generated code

334 lines 12.7 kB
#!/usr/bin/env node "use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const yargs_1 = __importDefault(require("yargs")); const helpers_1 = require("yargs/helpers"); const fs_1 = require("fs"); const path = __importStar(require("path")); const adapters_1 = require("../adapters"); const config_1 = require("../config"); const core_1 = require("../core"); const reporters_1 = require("../reporters"); const prompt_generator_1 = require("../prompt-generator"); const cli = (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)) .scriptName('codeguardian') .usage('$0 <cmd> [args]') .command('check', 'Check code changes against rules (analyzes Git diff between branches)', yargs => { return yargs .group(['config', 'exclude'], 'Configuration Options:') .group(['repo', 'base', 'head'], 'Repository Options:') .group(['format'], 'Output Options:') .option('config', { alias: 'c', type: 'string', description: 'Path to rule configuration file or glob pattern', example: 'codeguardian check -c "rules/*.yaml"', }) .option('exclude', { alias: 'e', type: 'array', description: 'Glob patterns to exclude from configuration file search', example: 'codeguardian check --exclude "examples/**" "test-*"', }) .option('repo', { alias: 'r', type: 'string', description: 'Path to the repository to validate', default: '.', }) .option('base', { alias: 'b', type: 'string', description: 'Base branch/commit for comparison', default: 'main', }) .option('head', { type: 'string', description: 'Head branch/commit to compare', default: 'HEAD', }) .option('format', { alias: 'f', type: 'string', choices: ['console', 'json'], description: 'Output format for validation results', default: 'console', }) .option('mode', { alias: 'm', type: 'string', choices: ['diff', 'all', 'staged'], description: 'What to check: diff (changes between branches), all (entire working directory), staged (staged files only)', default: 'diff', }) .option('skip-missing-ast-grep', { type: 'boolean', description: 'Skip AST-based rules and show a warning if the ast-grep CLI is not installed', default: false, }) .epilogue('Examples:\n' + ' # Auto-discover configuration files\n' + ' $ codeguardian check\n\n' + ' # Use specific configuration file\n' + ' $ codeguardian check -c rules.yaml\n\n' + ' # Use glob pattern to check multiple files\n' + ' $ codeguardian check -c "**/*.cg.yaml"\n\n' + ' # Exclude test directories from search\n' + ' $ codeguardian check --exclude "examples/**" "test-*"\n\n' + ' # Multiple exclude patterns\n' + ' $ codeguardian check -e "**/test/**" -e "**/vendor/**"'); }, async (args) => { try { await runValidation(args); } catch (error) { console.error('Error:', error instanceof Error ? error.message : String(error)); process.exit(1); } }) .command('generate-prompt', 'Generate a prompt for an AI to write validation rules', (yargs) => { return yargs .option('mode', { alias: 'm', type: 'string', choices: ['task', 'rule'], description: 'Generation mode: task (validation for a specific task) or rule (general rule from description)', default: 'task', }) .option('task', { type: 'string', description: 'The task description for the AI (used in task mode).', }) .option('task-file', { type: 'string', description: 'Path to a file containing the task description (used in task mode).', }) .option('description', { alias: 'd', type: 'string', description: 'Rule description for generating a validation rule (used in rule mode).', }) .option('output', { alias: 'o', type: 'string', description: 'Path to save the generated output (prompt in task mode, rule YAML in rule mode).', }) .check((argv) => { if (argv.mode === 'task' && !argv.task && !argv.taskFile) { throw new Error('In task mode, you must provide the task description using either --task or --task-file.'); } if (argv.mode === 'rule' && !argv.description) { throw new Error('In rule mode, you must provide a rule description using --description.'); } return true; }) .conflicts('task', 'task-file') .epilogue('Examples:\n' + ' # Generate prompt for task validation (default mode)\n' + ' $ codeguardian generate-prompt --task "Add user authentication"\n\n' + ' # Generate a rule from description\n' + ' $ codeguardian generate-prompt --mode rule --description "No console.log in production code"\n\n' + ' # Save output to file\n' + ' $ codeguardian generate-prompt -m rule -d "Enforce clean architecture" -o clean-arch.cg.yaml'); }, async (args) => { if (!process.env.CODEGUARDIAN_EXPERIMENTAL) { console.error('Error: The generate-prompt command is experimental and requires the CODEGUARDIAN_EXPERIMENTAL environment variable to be set.'); console.error('To enable experimental features, run:'); console.error(' export CODEGUARDIAN_EXPERIMENTAL=1'); process.exit(1); } await runPromptGeneration(args); }) .demandCommand(1, 'You need at least one command before moving on') .help() .version() .strict(); async function runValidation(args) { const startTime = Date.now(); const loader = new config_1.ConfigurationLoader(); const configurations = await loader.loadConfigurations(args.config, args.repo, args.exclude); if (args.format !== 'json') { console.log(`Found ${configurations.length} configuration file(s):`); const sortedConfigs = [...configurations].sort((a, b) => { const pathA = path.relative(args.repo, a.path); const pathB = path.relative(args.repo, b.path); return pathA.localeCompare(pathB); }); sortedConfigs.forEach(config => { console.log(` - ${path.relative(args.repo, config.path)}`); }); console.log(); } const factory = (0, config_1.createRuleFactory)(); const repository = new adapters_1.GitRepository(path.resolve(args.repo)); const diff = await repository.getDiff(args.base, args.head || 'HEAD'); const context = { repository, diff, cache: new core_1.ResultCache(), config: { type: 'placeholder' }, mode: args.mode || 'diff', cliArgs: { skipMissingAstGrep: args.skipMissingAstGrep, }, }; const allResults = []; let totalConfigsPassed = 0; let totalConfigsFailed = 0; let totalViolations = 0; let totalFiles = 0; let totalIndividualRules = 0; let individualRulesPassed = 0; let individualRulesFailed = 0; for (const loadedConfig of configurations) { const configData = loadedConfig.content; const yamlContent = await fs_1.promises.readFile(loadedConfig.path, 'utf-8'); const rule = factory.loadFromYAML(yamlContent); if ('countRules' in rule && typeof rule.countRules === 'function') { totalIndividualRules += rule.countRules(); } else { totalIndividualRules += 1; } const result = await rule.evaluate(context); if (result.subResults) { for (const subResult of result.subResults) { if (subResult.passed) { individualRulesPassed++; } else { individualRulesFailed++; } } } else { if (result.passed) { individualRulesPassed++; } else { individualRulesFailed++; } } if (result.passed) { totalConfigsPassed++; } else { totalConfigsFailed++; } totalViolations += result.violations?.length || 0; allResults.push({ ruleId: configData.id || rule.id, ruleDescription: configData.description || `Rules from ${path.basename(loadedConfig.path)}`, configFile: path.relative(args.repo, loadedConfig.path), passed: result.passed, violations: (result.violations || []).map(v => ({ file: v.file, line: v.line, column: v.column, message: v.message, severity: v.severity, context: v.context, })), }); } if (args.mode === 'all') { const allFiles = await repository.getFiles(diff, 'all'); totalFiles = allFiles.length; } else if (args.mode === 'staged') { const stagedFiles = await repository.getFiles(diff, 'staged'); totalFiles = stagedFiles.length; } else { totalFiles = diff.files.length; } const duration = Date.now() - startTime; const overallPassed = totalConfigsFailed === 0; const report = { passed: overallPassed, summary: { totalFiles, passedRules: individualRulesPassed, failedRules: individualRulesFailed, violations: totalViolations, totalIndividualRules: totalIndividualRules, }, results: allResults, diff, duration, }; const reporter = args.format === 'json' ? new reporters_1.JsonReporter() : new reporters_1.ConsoleReporter(); await reporter.report(report); if (!overallPassed) { process.exit(1); } } async function runPromptGeneration(args) { try { const generator = new prompt_generator_1.PromptGenerator(); let output; if (args.mode === 'rule') { output = await generator.generateRule(args.description); } else { let taskContent; if (args.taskFile) { taskContent = await fs_1.promises.readFile(args.taskFile, 'utf-8'); } else { taskContent = args.task; } output = await generator.generateTaskPrompt(taskContent); } if (args.output) { await fs_1.promises.writeFile(args.output, output); const messageType = args.mode === 'rule' ? 'Rule' : 'Prompt'; console.log(`${messageType} successfully saved to: ${args.output}`); } else { console.log(output); } } catch (error) { console.error('Error generating output:', error instanceof Error ? error.message : String(error)); process.exit(1); } } cli.parse(); //# sourceMappingURL=index.js.map