| import {Options, AIOptions} from './options' |
| import {Prompts} from './prompts' |
| import {Bot} from './bot' |
| import {codeReview} from './review' |
| import {LocalContextEngine} from './local-context' |
| import {LocalConsoleReporter} from './console-reporter' |
| import {feedbackTracker} from './feedback-tracker' |
| import {execSync} from 'child_process' |
| import {existsSync} from 'fs' |
|
|
| interface CLIArgs { |
| staged?: boolean |
| diff?: string |
| files?: string[] |
| model?: string |
| minConfidence?: number |
| minSeverity?: 'critical' | 'major' | 'minor' | 'info' |
| disableReview?: boolean |
| disableReleaseNotes?: boolean |
| maxFiles?: number |
| pathFilters?: string[] |
| systemMessage?: string |
| language?: string |
| output?: 'console' | 'json' |
| help?: boolean |
| } |
|
|
| const HELP_TEXT = ` |
| PRIX CLI - Local AI PR Reviewer |
| |
| Usage: |
| prix-cli [options] |
| |
| Options: |
| --staged Review staged files (git diff --cached) |
| --diff <ref> Review files changed since given git ref |
| --files <paths> Review specific files (comma-separated) |
| --model <name> LLM model to use (default: llama-3.1-8b-instant) |
| --min-confidence <n> Minimum confidence threshold (0-100) |
| --min-severity <lvl> Minimum severity to report (critical|major|minor|info) |
| --disable-review Skip code review, only generate summary |
| --disable-notes Skip release notes generation |
| --max-files <n> Maximum files to review (0 = unlimited) |
| --path-filters <p> Path filters (comma-separated, e.g., "src/**,lib/**") |
| --system-message <m> Additional system message |
| --language <lang> Output language (default: en-US) |
| --output <fmt> Output format: console (default) or json |
| --help, -h Show this help message |
| |
| Examples: |
| prix-cli --staged Review staged changes |
| prix-cli --diff HEAD~1 Review changes since last commit |
| prix-cli --files src/app.ts,lib/util.ts Review specific files |
| prix-cli --min-severity major Only show major/critical issues |
| |
| Pre-commit Hook: |
| Add to .git/hooks/pre-commit: |
| npx prix-cli --staged --output console |
| ` |
|
|
| function parseArgs(argv: string[]): CLIArgs { |
| const args: CLIArgs = {} |
| for (let i = 2; i < argv.length; i++) { |
| const arg = argv[i] |
| switch (arg) { |
| case '--staged': |
| args.staged = true |
| break |
| case '--diff': |
| args.diff = argv[++i] |
| break |
| case '--files': |
| args.files = argv[++i].split(',') |
| break |
| case '--model': |
| args.model = argv[++i] |
| break |
| case '--min-confidence': |
| args.minConfidence = parseInt(argv[++i], 10) |
| break |
| case '--min-severity': |
| args.minSeverity = argv[++i] as any |
| break |
| case '--disable-review': |
| args.disableReview = true |
| break |
| case '--disable-notes': |
| args.disableReleaseNotes = true |
| break |
| case '--max-files': |
| args.maxFiles = parseInt(argv[++i], 10) |
| break |
| case '--path-filters': |
| args.pathFilters = argv[++i].split(',') |
| break |
| case '--system-message': |
| args.systemMessage = argv[++i] |
| break |
| case '--language': |
| args.language = argv[++i] |
| break |
| case '--output': |
| args.output = argv[++i] as any |
| break |
| case '--help': |
| case '-h': |
| args.help = true |
| break |
| } |
| } |
| return args |
| } |
|
|
| function getStagedFiles(): string[] { |
| try { |
| const output = execSync('git diff --cached --name-only', { |
| encoding: 'utf8', |
| cwd: process.cwd() |
| }) |
| return output.split('\n').filter(f => f.trim()) |
| } catch (e) { |
| console.error('Failed to get staged files:', e) |
| return [] |
| } |
| } |
|
|
| function getChangedFiles(sinceRef: string): string[] { |
| try { |
| const output = execSync(`git diff --name-only ${sinceRef}`, { |
| encoding: 'utf8', |
| cwd: process.cwd() |
| }) |
| return output.split('\n').filter(f => f.trim()) |
| } catch (e) { |
| console.error(`Failed to get changed files since ${sinceRef}:`, e) |
| return [] |
| } |
| } |
|
|
| async function runLocalReview(args: CLIArgs): Promise<number> { |
| const info = console.log |
| const error = console.error |
|
|
| info('๐ PRIX CLI - Starting local code review...\n') |
|
|
| if (!process.env.GROQ_API_KEY && !process.env.AI_API_KEY) { |
| error( |
| 'โ Error: GROQ_API_KEY or AI_API_KEY environment variable is required' |
| ) |
| return 1 |
| } |
|
|
| let filesToReview: string[] = [] |
|
|
| if (args.staged) { |
| filesToReview = getStagedFiles() |
| info(`๐ Reviewing ${filesToReview.length} staged files\n`) |
| } else if (args.diff) { |
| filesToReview = getChangedFiles(args.diff) |
| info( |
| `๐ Reviewing ${filesToReview.length} files changed since ${args.diff}\n` |
| ) |
| } else if (args.files) { |
| filesToReview = args.files.filter(f => existsSync(f)) |
| info(`๐ Reviewing ${filesToReview.length} specified files\n`) |
| } else { |
| error('โ Error: Must specify --staged, --diff <ref>, or --files <paths>') |
| return 1 |
| } |
|
|
| if (filesToReview.length === 0) { |
| info('โ
No files to review') |
| return 0 |
| } |
|
|
| const options = new Options( |
| false, |
| args.disableReview || false, |
| args.disableReleaseNotes || false, |
| args.maxFiles?.toString() || '0', |
| false, |
| false, |
| args.pathFilters || null, |
| args.systemMessage || '', |
| args.model || 'llama-3.1-8b-instant', |
| 'llama-3.3-70b-versatile', |
| '0.0', |
| '3', |
| '120000', |
| '6', |
| '6', |
| process.env.API_BASE_URL || 'https://api.groq.com/openai/v1', |
| args.language || 'en-US', |
| false, |
| args.minSeverity || 'minor', |
| args.minConfidence?.toString() || '0', |
| 'false', |
| 'true' |
| ) |
|
|
| const prompts = new Prompts() |
| const localContext = new LocalContextEngine(filesToReview) |
| const reporter = new LocalConsoleReporter() |
|
|
| info('๐ค Initializing AI models...') |
|
|
| let lightBot: Bot | null = null |
| let heavyBot: Bot | null = null |
|
|
| try { |
| lightBot = new Bot( |
| options, |
| new AIOptions( |
| options.lightModel, |
| options.lightTokenLimits, |
| options.modelTemperature |
| ) |
| ) |
| } catch (e) { |
| error(`โ Failed to initialize light model: ${e}`) |
| return 1 |
| } |
|
|
| try { |
| heavyBot = new Bot( |
| options, |
| new AIOptions( |
| options.heavyModel, |
| options.heavyTokenLimits, |
| options.modelTemperature |
| ) |
| ) |
| } catch (e) { |
| error(`โ Failed to initialize heavy model: ${e}`) |
| return 1 |
| } |
|
|
| info('โ
AI models initialized\n') |
|
|
| try { |
| await localContext.initialize() |
|
|
| const findings = await localContext.runReview( |
| lightBot, |
| heavyBot, |
| options, |
| prompts |
| ) |
|
|
| reporter.printFindings(findings, { |
| output: args.output || 'console', |
| minSeverity: options.minimumSeverity, |
| minConfidence: options.minConfidence |
| }) |
|
|
| const critical = findings.filter(f => f.severity === 'critical').length |
| const major = findings.filter(f => f.severity === 'major').length |
|
|
| info( |
| `\n๐ Summary: ${critical} critical, ${major} major, ${findings.length} total findings` |
| ) |
|
|
| if (critical > 0) { |
| info('\nโ ๏ธ Critical issues found! Consider fixing before committing.') |
| return 1 |
| } else if (major > 0) { |
| info('\nโน๏ธ Major issues found. Review recommended before committing.') |
| return 0 |
| } else { |
| info('\nโ
No significant issues found.') |
| return 0 |
| } |
| } catch (e) { |
| error(`\nโ Review failed: ${e}`) |
| return 1 |
| } |
| } |
|
|
| export async function main(argv: string[]): Promise<number> { |
| const args = parseArgs(argv) |
|
|
| if (args.help) { |
| console.log(HELP_TEXT) |
| return 0 |
| } |
|
|
| return runLocalReview(args) |
| } |
|
|
| if (require.main === module) { |
| main(process.argv) |
| .then(code => { |
| process.exit(code) |
| }) |
| .catch(e => { |
| console.error('Fatal error:', e) |
| process.exit(1) |
| }) |
| } |
|
|