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