Spaces:
Paused
Paused
| 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) | |
| }) | |
| } | |