@ateriss_/aiv-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,118 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { table } from 'table';
5
+ import { resolveConfig, getGithubToken, isInitialized } from '../../config';
6
+ import { GithubClient } from '../../git/github';
7
+ import { detectRepoInfo } from '../../git/utils';
8
+ import { selectPR, confirmReview } from '../selector';
9
+ import { runReview } from './review';
10
+ import { t } from '../../i18n';
11
+
12
+ export function prsCommand(): Command {
13
+ return new Command('prs')
14
+ .alias('p')
15
+ .description('List open pull requests and optionally launch a review')
16
+ .option('--limit <n>', 'Max number of PRs to show', '20')
17
+ .option('--owner <owner>', 'GitHub owner (overrides auto-detect)')
18
+ .option('--repo <repo>', 'GitHub repo (overrides auto-detect)')
19
+ .option('--no-select', 'Skip interactive selector, just list PRs')
20
+ .action(async (opts) => {
21
+ if (!isInitialized()) {
22
+ console.log(chalk.red(t().notInitialized));
23
+ return;
24
+ }
25
+
26
+ const config = resolveConfig();
27
+ let token: string;
28
+ try {
29
+ token = getGithubToken(config);
30
+ } catch {
31
+ console.log(chalk.red(t().prsMissingToken(config.github.token_env)));
32
+ return;
33
+ }
34
+
35
+ const repoInfo = detectRepoInfo(process.cwd());
36
+ const owner = opts.owner ?? config.github.owner ?? repoInfo?.owner;
37
+ const repo = opts.repo ?? config.github.repo ?? repoInfo?.repo;
38
+
39
+ if (!owner || !repo) {
40
+ console.log(chalk.red(t().repoNotDetected));
41
+ return;
42
+ }
43
+
44
+ const spinner = ora(t().prsFetching(chalk.cyan(`${owner}/${repo}`))).start();
45
+ let prs: Awaited<ReturnType<GithubClient['listPRs']>>;
46
+
47
+ try {
48
+ const client = new GithubClient(token);
49
+ prs = await client.listPRs(owner, repo, Number.parseInt(opts.limit));
50
+ spinner.stop();
51
+ } catch (e: any) {
52
+ spinner.fail(chalk.red(t().prsFailed(e.message)));
53
+ return;
54
+ }
55
+
56
+ if (prs.length === 0) {
57
+ console.log(chalk.yellow(t().prsNoneFound));
58
+ return;
59
+ }
60
+
61
+ // ── Table ──────────────────────────────────────────────────────────────
62
+ const rows = prs.map(pr => [
63
+ chalk.cyan(`#${pr.number}`),
64
+ truncate(pr.title, 50),
65
+ chalk.dim(pr.author),
66
+ chalk.dim(pr.branch),
67
+ chalk.green(`+${pr.additions}`) + chalk.dim('/') + chalk.red(`-${pr.deletions}`),
68
+ chalk.dim(formatDate(pr.createdAt)),
69
+ ]);
70
+
71
+ const header = [
72
+ chalk.bold(t().prsColPR),
73
+ chalk.bold(t().prsColTitle),
74
+ chalk.bold(t().prsColAuthor),
75
+ chalk.bold(t().prsColBranch),
76
+ chalk.bold(t().prsColChanges),
77
+ chalk.bold(t().prsColCreated),
78
+ ];
79
+
80
+ console.log('\n' + table([header, ...rows], {
81
+ border: {
82
+ topBody: '─', topJoin: '┬', topLeft: '╭', topRight: '╮',
83
+ bottomBody: '─', bottomJoin: '┴', bottomLeft: '╰', bottomRight: '╯',
84
+ bodyLeft: '│', bodyRight: '│', bodyJoin: '│',
85
+ joinBody: '─', joinLeft: '├', joinRight: '┤', joinJoin: '┼',
86
+ },
87
+ columnDefault: { paddingLeft: 1, paddingRight: 1 },
88
+ }));
89
+
90
+ console.log(chalk.dim(` ${t().prsFooter(prs.length).trim()}`));
91
+ console.log(chalk.dim(` ${t().reviewAccount(config.github.accountName, config.github.token_env)}\n`));
92
+
93
+ // ── Interactive selector ───────────────────────────────────────────────
94
+ if (opts.select === false || !process.stdout.isTTY) return;
95
+
96
+ const selected = await selectPR(prs, t().selectorSelectPR);
97
+ if (!selected) {
98
+ console.log(chalk.dim(t().selectorCancelled));
99
+ return;
100
+ }
101
+
102
+ const confirmed = await confirmReview(selected, t().selectorConfirmReview);
103
+ if (!confirmed) {
104
+ console.log(chalk.dim(t().selectorCancelled));
105
+ return;
106
+ }
107
+
108
+ await runReview({ prNumber: selected.number, owner, repo, config, token });
109
+ });
110
+ }
111
+
112
+ function truncate(str: string, max: number): string {
113
+ return str.length > max ? str.slice(0, max - 1) + '…' : str;
114
+ }
115
+
116
+ function formatDate(iso: string): string {
117
+ return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: '2-digit' });
118
+ }
@@ -0,0 +1,171 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { resolveConfig, loadRules, getGithubToken, isInitialized } from '../../config';
5
+ import type { ResolvedConfig } from '../../types';
6
+ import { GithubClient } from '../../git/github';
7
+ import { detectRepoInfo } from '../../git/utils';
8
+ import { Orchestrator } from '../../orchestrator';
9
+ import { ContextManager, refreshContextFiles } from '../../context/manager';
10
+ import { renderReview } from '../renderer';
11
+ import { selectPR, selectPostReviewAction } from '../selector';
12
+ import { t } from '../../i18n';
13
+
14
+ // ─── Shared review runner (used by prs.ts and review command) ─────────────────
15
+
16
+ export interface RunReviewOptions {
17
+ prNumber: number;
18
+ owner: string;
19
+ repo: string;
20
+ config: ResolvedConfig;
21
+ token: string;
22
+ agents?: string[];
23
+ json?: boolean;
24
+ }
25
+
26
+ export async function runReview(opts: RunReviewOptions): Promise<void> {
27
+ const { prNumber, owner, repo, config, token } = opts;
28
+ const rules = loadRules();
29
+ const activeAgents = opts.agents ?? ['business', 'architecture', 'security'];
30
+
31
+ console.log(chalk.bold(t().reviewTitle(prNumber)));
32
+ console.log(chalk.dim(` ${t().reviewAccount(config.github.accountName, config.github.token_env)}\n`));
33
+
34
+ const fetchSpinner = ora(t().reviewFetching(prNumber, `${owner}/${repo}`)).start();
35
+ let prDiff: Awaited<ReturnType<GithubClient['getPRDiff']>>;
36
+
37
+ try {
38
+ const client = new GithubClient(token);
39
+ prDiff = await client.getPRDiff(owner, repo, prNumber);
40
+ fetchSpinner.succeed(t().reviewLoaded(chalk.cyan(prDiff.pr.title), prDiff.files.length));
41
+ } catch (e: any) {
42
+ fetchSpinner.fail(chalk.red(t().reviewFetchFailed(e.message)));
43
+ return;
44
+ }
45
+
46
+ const ctxSpinner = ora(t().reviewLoadingContext).start();
47
+ const context = new ContextManager(process.cwd()).buildReviewContext(prDiff);
48
+ ctxSpinner.succeed(t().reviewContextLoaded);
49
+
50
+ console.log(chalk.dim(t().reviewRunningAgents(activeAgents.join(', '))));
51
+
52
+ try {
53
+ const result = await new Orchestrator(config, rules).run(prDiff, context, activeAgents);
54
+ if (opts.json) {
55
+ console.log(JSON.stringify(result, null, 2));
56
+ return;
57
+ }
58
+ renderReview(result);
59
+ } catch (e: any) {
60
+ console.log(chalk.red(t().reviewFailed(e.message)));
61
+ return;
62
+ }
63
+
64
+ if (!process.stdout.isTTY) return;
65
+
66
+ const action = await selectPostReviewAction(t().postReviewSelectAction);
67
+ if (action === 'skip') return;
68
+
69
+ const event = action === 'approve' ? 'APPROVE' : 'REQUEST_CHANGES';
70
+ const submitSpinner = ora(t().postReviewSubmitting).start();
71
+
72
+ try {
73
+ await new GithubClient(token).submitReview(owner, repo, prNumber, event);
74
+ if (action === 'approve') {
75
+ submitSpinner.succeed(chalk.green(t().postReviewApproved(prNumber)));
76
+ } else {
77
+ submitSpinner.succeed(chalk.yellow(t().postReviewChangesRequested(prNumber)));
78
+ }
79
+ } catch (e: any) {
80
+ submitSpinner.fail(chalk.red(t().postReviewFailed(e.message)));
81
+ return;
82
+ }
83
+
84
+ if (action === 'approve') {
85
+ const refreshSpinner = ora(t().postReviewRefreshing).start();
86
+ await refreshContextFiles(process.cwd());
87
+ refreshSpinner.succeed(chalk.green(t().postReviewRefreshed));
88
+ }
89
+ }
90
+
91
+ // ─── CLI command ──────────────────────────────────────────────────────────────
92
+
93
+ export function reviewCommand(): Command {
94
+ return new Command('review')
95
+ .alias('r')
96
+ .description('Run AI review on a pull request')
97
+ .argument('[pr-number]', 'PR number (omit to pick interactively)')
98
+ .option('--owner <owner>', 'GitHub owner')
99
+ .option('--repo <repo>', 'GitHub repo')
100
+ .option('--agent <agents...>', 'Run specific agents only (business, architecture, security)')
101
+ .option('--json', 'Output raw JSON result')
102
+ .action(async (prArg: string | undefined, opts) => {
103
+ if (!isInitialized()) {
104
+ console.log(chalk.red(t().notInitialized));
105
+ return;
106
+ }
107
+
108
+ const config = resolveConfig();
109
+ let token: string;
110
+ try {
111
+ token = getGithubToken(config);
112
+ } catch {
113
+ console.log(chalk.red(t().prsMissingToken(config.github.token_env)));
114
+ return;
115
+ }
116
+
117
+ const repoInfo = detectRepoInfo(process.cwd());
118
+ const owner = opts.owner ?? config.github.owner ?? repoInfo?.owner;
119
+ const repo = opts.repo ?? config.github.repo ?? repoInfo?.repo;
120
+
121
+ if (!owner || !repo) {
122
+ console.log(chalk.red(t().repoNotDetected));
123
+ return;
124
+ }
125
+
126
+ let prNumber: number;
127
+
128
+ if (prArg === undefined) {
129
+ // No number — fetch PRs and show interactive selector
130
+ const spinner = ora(t().prsFetching(chalk.cyan(`${owner}/${repo}`))).start();
131
+ let prs: Awaited<ReturnType<GithubClient['listPRs']>>;
132
+ try {
133
+ prs = await new GithubClient(token).listPRs(owner, repo, 30);
134
+ spinner.stop();
135
+ } catch (e: any) {
136
+ spinner.fail(chalk.red(t().prsFailed(e.message)));
137
+ return;
138
+ }
139
+
140
+ if (prs.length === 0) {
141
+ console.log(chalk.yellow(t().prsNoneFound));
142
+ return;
143
+ }
144
+
145
+ const selected = await selectPR(prs, t().selectorSelectPR);
146
+ if (!selected) {
147
+ console.log(chalk.dim(t().selectorCancelled));
148
+ return;
149
+ }
150
+ prNumber = selected.number;
151
+ } else {
152
+ // PR number provided directly
153
+ prNumber = Number.parseInt(prArg);
154
+ if (Number.isNaN(prNumber)) {
155
+ console.log(chalk.red(t().invalidPrNumber));
156
+ return;
157
+ }
158
+ }
159
+
160
+ await runReview({
161
+ prNumber,
162
+ owner,
163
+ repo,
164
+ config,
165
+ token,
166
+ agents: opts.agent,
167
+ json: opts.json,
168
+ });
169
+ });
170
+ }
171
+
@@ -0,0 +1,102 @@
1
+ import chalk from 'chalk';
2
+ import { ReviewResult, AgentFinding } from '../types';
3
+ import { t } from '../i18n';
4
+
5
+ export function renderReview(result: ReviewResult): void {
6
+ const tr = t();
7
+ const riskColor = riskChalk(result.riskLabel);
8
+ const riskBadge = riskColor(`${result.riskScore}/100 [${result.riskLabel}]`);
9
+
10
+ console.log('\n' + chalk.bold('━'.repeat(60)));
11
+ console.log(chalk.bold(tr.renderReviewTitle(result.prNumber, result.prTitle)));
12
+ console.log(chalk.bold('━'.repeat(60)));
13
+
14
+ console.log(`\n ${chalk.bold(tr.renderRiskScore)} ${riskBadge}`);
15
+ console.log(` ${chalk.bold(tr.renderGenerated)} ${chalk.dim(result.generatedAt)}\n`);
16
+
17
+ console.log(chalk.bold(tr.renderExecutiveSummary));
18
+ console.log(chalk.dim(' ' + '─'.repeat(56)));
19
+ console.log(indent(result.executiveSummary, 2));
20
+
21
+ if (result.securityIssues.length > 0) {
22
+ console.log('\n' + chalk.bold.red(tr.renderSecurityIssues));
23
+ console.log(chalk.dim(' ' + '─'.repeat(56)));
24
+ renderFindings(result.securityIssues);
25
+ }
26
+
27
+ if (result.businessRisks.length > 0) {
28
+ console.log('\n' + chalk.bold.yellow(tr.renderBusinessRisks));
29
+ console.log(chalk.dim(' ' + '─'.repeat(56)));
30
+ renderFindings(result.businessRisks);
31
+ }
32
+
33
+ if (result.architectureIssues.length > 0) {
34
+ console.log('\n' + chalk.bold.blue(tr.renderArchitectureIssues));
35
+ console.log(chalk.dim(' ' + '─'.repeat(56)));
36
+ renderFindings(result.architectureIssues);
37
+ }
38
+
39
+ if (result.possibleRegressions.length > 0) {
40
+ console.log('\n' + chalk.bold.magenta(tr.renderRegressions));
41
+ console.log(chalk.dim(' ' + '─'.repeat(56)));
42
+ result.possibleRegressions.forEach(r => {
43
+ console.log(` ${chalk.magenta('◆')} ${r}`);
44
+ });
45
+ }
46
+
47
+ console.log('\n' + chalk.bold(tr.renderAgentSummaries));
48
+ console.log(chalk.dim(' ' + '─'.repeat(56)));
49
+ result.agents.forEach(agent => {
50
+ const score = riskChalk(scoreLabel(agent.riskScore));
51
+ const scoreBadge = score(`[${agent.riskScore}/100]`);
52
+ console.log(`\n ${chalk.bold(agent.agentName.toUpperCase())} ${scoreBadge}`);
53
+ console.log(indent(agent.summary, 2));
54
+ });
55
+
56
+ console.log('\n' + chalk.bold('━'.repeat(60)) + '\n');
57
+ }
58
+
59
+ function renderFindings(findings: AgentFinding[]): void {
60
+ const tr = t();
61
+ findings.forEach(f => {
62
+ const sev = severityBadge(f.severity);
63
+ const file = f.file ? chalk.dim(` → ${f.file}`) : '';
64
+ console.log(`\n ${sev} ${chalk.bold(f.title)}${file}`);
65
+ console.log(indent(f.description, 2));
66
+ if (f.suggestion) {
67
+ console.log(` ${chalk.dim(tr.renderSuggestion)} ${f.suggestion}`);
68
+ }
69
+ });
70
+ }
71
+
72
+ function severityBadge(sev: string): string {
73
+ const tr = t();
74
+ switch (sev) {
75
+ case 'critical': return chalk.bgRed.white(` ${tr.severityCritical} `);
76
+ case 'high': return chalk.red(`[${tr.severityHigh}]`);
77
+ case 'medium': return chalk.yellow(`[${tr.severityMedium}]`);
78
+ case 'low': return chalk.blue(`[${tr.severityLow}]`);
79
+ default: return chalk.dim(`[${tr.severityInfo}]`);
80
+ }
81
+ }
82
+
83
+ function riskChalk(label: string) {
84
+ switch (label) {
85
+ case 'CRITICAL': return chalk.bgRed.white;
86
+ case 'HIGH': return chalk.red;
87
+ case 'MEDIUM': return chalk.yellow;
88
+ default: return chalk.green;
89
+ }
90
+ }
91
+
92
+ function scoreLabel(score: number): 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' {
93
+ if (score >= 80) return 'CRITICAL';
94
+ if (score >= 60) return 'HIGH';
95
+ if (score >= 30) return 'MEDIUM';
96
+ return 'LOW';
97
+ }
98
+
99
+ function indent(text: string, spaces: number): string {
100
+ const pad = ' '.repeat(spaces);
101
+ return text.split('\n').map(l => pad + l).join('\n');
102
+ }
@@ -0,0 +1,78 @@
1
+ import chalk from 'chalk';
2
+ import { PullRequest } from '../types';
3
+
4
+ // Inquirer v9 is ESM-only — must be loaded via dynamic import in CJS projects
5
+ async function getInquirer() {
6
+ const mod = await import('inquirer');
7
+ return (mod as any).default as {
8
+ prompt: (questions: object[]) => Promise<Record<string, unknown>>;
9
+ Separator: new (line?: string) => object;
10
+ };
11
+ }
12
+
13
+ const CANCEL = -1;
14
+
15
+ function prLabel(pr: PullRequest): string {
16
+ const num = chalk.cyan(`#${pr.number}`);
17
+ const title = pr.title.length > 52 ? pr.title.slice(0, 51) + '…' : pr.title;
18
+ const meta = chalk.dim(`${pr.author} · ${pr.branch}`);
19
+ const diff = chalk.green(`+${pr.additions}`) + chalk.dim('/') + chalk.red(`-${pr.deletions}`);
20
+ return `${num} ${title} ${meta} ${diff}`;
21
+ }
22
+
23
+ export async function selectPR(prs: PullRequest[], message: string): Promise<PullRequest | null> {
24
+ const inquirer = await getInquirer();
25
+
26
+ const choices = [
27
+ ...prs.map(pr => ({ name: prLabel(pr), value: pr.number, short: `#${pr.number}` })),
28
+ new inquirer.Separator('─'.repeat(62)),
29
+ { name: chalk.dim('↩ Cancel'), value: CANCEL, short: 'Cancel' },
30
+ ];
31
+
32
+ const answers = await inquirer.prompt([{
33
+ type: 'list',
34
+ name: 'prNumber',
35
+ message,
36
+ choices,
37
+ pageSize: 14,
38
+ loop: false,
39
+ }]);
40
+
41
+ const selected = answers['prNumber'] as number;
42
+ return selected === CANCEL ? null : (prs.find(pr => pr.number === selected) ?? null);
43
+ }
44
+
45
+ export type PostReviewAction = 'approve' | 'request_changes' | 'skip';
46
+
47
+ export async function selectPostReviewAction(message: string): Promise<PostReviewAction> {
48
+ const inquirer = await getInquirer();
49
+
50
+ const answers = await inquirer.prompt([{
51
+ type: 'list',
52
+ name: 'action',
53
+ message,
54
+ choices: [
55
+ { name: chalk.green('✔ Approve PR'), value: 'approve', short: 'Approve' },
56
+ { name: chalk.yellow('⚑ Request Changes'), value: 'request_changes', short: 'Request Changes' },
57
+ new inquirer.Separator('─'.repeat(42)),
58
+ { name: chalk.dim('↩ Skip'), value: 'skip', short: 'Skip' },
59
+ ],
60
+ pageSize: 4,
61
+ loop: false,
62
+ }]);
63
+
64
+ return answers['action'] as PostReviewAction;
65
+ }
66
+
67
+ export async function confirmReview(pr: PullRequest, label: string): Promise<boolean> {
68
+ const inquirer = await getInquirer();
69
+
70
+ const answers = await inquirer.prompt([{
71
+ type: 'confirm',
72
+ name: 'ok',
73
+ message: `${label} ${chalk.cyan(`#${pr.number}`)} — ${pr.title}?`,
74
+ default: true,
75
+ }]);
76
+
77
+ return Boolean(answers['ok']);
78
+ }