PRIX / src /cli.ts
yamxxx1's picture
Upload 139 files
7da7e2c verified
Raw
History Blame
8.21 kB
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: meta-llama/llama-4-scout-17b-16e-instruct)
--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 || 'meta-llama/llama-4-scout-17b-16e-instruct',
'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)
})
}