UNPKG

@diullei/codeguardian

Version:

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

275 lines 10.2 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 findGitRoot_1 = require("../utils/findGitRoot"); 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 (disables auto-discovery)', }) .option('C', { type: 'string', description: 'Change to directory before running (like git -C)', }) .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, }) .option('claude-code-hook', { type: 'boolean', description: 'Claude Code hook compatibility mode: exit with code 2 on violations, run silently on success', default: false, }) .epilogue('Examples:\n' + ' # Auto-discover repository root and configuration files\n' + ' $ codeguardian check\n\n' + ' # Change to directory first (like git -C)\n' + ' $ codeguardian check -C /path/to/repo\n\n' + ' # Use explicit repository path (no auto-discovery)\n' + ' $ codeguardian check --repo /path/to/repo\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); } }) .demandCommand(1, 'You need at least one command before moving on') .help() .version() .strict(); async function runValidation(args) { const startTime = Date.now(); const repoPath = (0, findGitRoot_1.resolveRepositoryPath)(args.repo, args.C); const loader = new config_1.ConfigurationLoader(); const configurations = await loader.loadConfigurations(args.config, repoPath, args.exclude); const configurationFiles = configurations.map(config => path.relative(repoPath, config.path)); const factory = (0, config_1.createRuleFactory)(); const repository = new adapters_1.GitRepository(repoPath); let baseBranch = args.base; if (args.base === 'main') { baseBranch = await repository.getDefaultBranch(); } const diff = await repository.getDiff(baseBranch, 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(repoPath, 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, originalCliArgs: { config: args.config, exclude: args.exclude, repo: args.repo, C: args.C, base: args.base, head: args.head, mode: args.mode, }, }; const reporter = args.format === 'json' ? new reporters_1.JsonReporter() : new reporters_1.ConsoleReporter({ claudeCodeHook: args.claudeCodeHook }); if (args.format !== 'json' && !args.claudeCodeHook) { console.log(`Found ${configurations.length} configuration file(s):`); const sortedFiles = [...configurationFiles].sort(); sortedFiles.forEach(file => { console.log(` - ${file}`); }); console.log(); } await reporter.report(report); if (!overallPassed) { process.exit(args.claudeCodeHook ? 2 : 1); } } cli.parse(); //# sourceMappingURL=index.js.map