@diullei/codeguardian
Version:
Open-source developer tool to validate and enforce architectural rules, especially for AI-generated code
275 lines • 10.2 kB
JavaScript
#!/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