@ateriss_/aiv-cli 1.0.1 β 1.0.2
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.
- package/README.md +78 -63
- package/dist/index.js +330 -110
- package/package.json +10 -3
- package/src/cli/commands/review.ts +89 -41
- package/src/cli/renderer.ts +61 -0
- package/src/cli/selector.ts +6 -5
- package/src/git/github.ts +33 -1
- package/src/i18n/en.ts +7 -0
- package/src/i18n/es.ts +7 -0
- package/tsup.config.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name":
|
|
3
|
-
"version": "1.0.
|
|
2
|
+
"name": "@ateriss_/aiv-cli",
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "AI-powered PR reviewer CLI β local-first, multi-agent semantic analysis",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
"start": "node dist/index.js",
|
|
14
14
|
"prepublishOnly": "npm run build"
|
|
15
15
|
},
|
|
16
|
-
"keywords": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ai",
|
|
18
|
+
"pr-review",
|
|
19
|
+
"cli",
|
|
20
|
+
"github",
|
|
21
|
+
"claude",
|
|
22
|
+
"openai"
|
|
23
|
+
],
|
|
17
24
|
"author": "",
|
|
18
25
|
"license": "MIT",
|
|
19
26
|
"dependencies": {
|
|
@@ -2,12 +2,12 @@ import { Command } from 'commander';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import ora from 'ora';
|
|
4
4
|
import { resolveConfig, loadRules, getGithubToken, isInitialized } from '../../config';
|
|
5
|
-
import type { ResolvedConfig } from '../../types';
|
|
5
|
+
import type { ResolvedConfig, ReviewResult } from '../../types';
|
|
6
6
|
import { GithubClient } from '../../git/github';
|
|
7
7
|
import { detectRepoInfo } from '../../git/utils';
|
|
8
8
|
import { Orchestrator } from '../../orchestrator';
|
|
9
9
|
import { ContextManager, refreshContextFiles } from '../../context/manager';
|
|
10
|
-
import { renderReview } from '../renderer';
|
|
10
|
+
import { renderReview, buildAivComment } from '../renderer';
|
|
11
11
|
import { selectPR, selectPostReviewAction, confirmMerge, selectMergeStrategy } from '../selector';
|
|
12
12
|
import { t } from '../../i18n';
|
|
13
13
|
|
|
@@ -27,15 +27,14 @@ export async function runReview(opts: RunReviewOptions): Promise<void> {
|
|
|
27
27
|
const { prNumber, owner, repo, config, token } = opts;
|
|
28
28
|
const rules = loadRules();
|
|
29
29
|
const activeAgents = opts.agents ?? ['business', 'architecture', 'security'];
|
|
30
|
+
const client = new GithubClient(token);
|
|
30
31
|
|
|
31
32
|
console.log(chalk.bold(t().reviewTitle(prNumber)));
|
|
32
33
|
console.log(chalk.dim(` ${t().reviewAccount(config.github.accountName, config.github.token_env)}\n`));
|
|
33
34
|
|
|
34
35
|
const fetchSpinner = ora(t().reviewFetching(prNumber, `${owner}/${repo}`)).start();
|
|
35
36
|
let prDiff: Awaited<ReturnType<GithubClient['getPRDiff']>>;
|
|
36
|
-
|
|
37
37
|
try {
|
|
38
|
-
const client = new GithubClient(token);
|
|
39
38
|
prDiff = await client.getPRDiff(owner, repo, prNumber);
|
|
40
39
|
fetchSpinner.succeed(t().reviewLoaded(chalk.cyan(prDiff.pr.title), prDiff.files.length));
|
|
41
40
|
} catch (e: any) {
|
|
@@ -43,61 +42,110 @@ export async function runReview(opts: RunReviewOptions): Promise<void> {
|
|
|
43
42
|
return;
|
|
44
43
|
}
|
|
45
44
|
|
|
45
|
+
const result = await resolveResult(client, { owner, repo, prNumber }, config, rules, prDiff, activeAgents, opts.json);
|
|
46
|
+
if (!result) return;
|
|
47
|
+
|
|
48
|
+
if (opts.json) {
|
|
49
|
+
console.log(JSON.stringify(result, null, 2));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
renderReview(result);
|
|
54
|
+
if (!process.stdout.isTTY) return;
|
|
55
|
+
|
|
56
|
+
const action = await selectPostReviewAction(t().postReviewSelectAction);
|
|
57
|
+
if (action === 'skip') return;
|
|
58
|
+
|
|
59
|
+
if (action === 'post_comment') {
|
|
60
|
+
await doPostComment(client, owner, repo, prNumber, result);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const submitted = await doSubmitReview(client, owner, repo, prNumber, action);
|
|
65
|
+
if (!submitted) return;
|
|
66
|
+
|
|
67
|
+
if (action === 'approve') {
|
|
68
|
+
await doApproveFlow(client, owner, repo, prNumber, result);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface RepoRef { owner: string; repo: string; prNumber: number; }
|
|
73
|
+
|
|
74
|
+
async function resolveResult(
|
|
75
|
+
client: GithubClient,
|
|
76
|
+
ref: RepoRef,
|
|
77
|
+
config: ResolvedConfig, rules: ReturnType<typeof loadRules>,
|
|
78
|
+
prDiff: Awaited<ReturnType<GithubClient['getPRDiff']>>,
|
|
79
|
+
activeAgents: string[], json?: boolean,
|
|
80
|
+
): Promise<ReviewResult | null> {
|
|
81
|
+
const { owner, repo, prNumber } = ref;
|
|
82
|
+
if (!json && process.stdout.isTTY) {
|
|
83
|
+
const cached = await client.findAivReview(owner, repo, prNumber);
|
|
84
|
+
if (cached) {
|
|
85
|
+
console.log(chalk.cyan(`\n ${t().reviewCachedFound}`));
|
|
86
|
+
const useCached = await confirmMerge(t().reviewCachedUse);
|
|
87
|
+
if (useCached) {
|
|
88
|
+
console.log(chalk.dim(` ${t().reviewCachedUsing}`));
|
|
89
|
+
return cached;
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk.dim(` ${t().reviewCachedSkipping}`));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
46
95
|
const ctxSpinner = ora(t().reviewLoadingContext).start();
|
|
47
96
|
const context = new ContextManager(process.cwd()).buildReviewContext(prDiff);
|
|
48
97
|
ctxSpinner.succeed(t().reviewContextLoaded);
|
|
49
|
-
|
|
50
98
|
console.log(chalk.dim(t().reviewRunningAgents(activeAgents.join(', '))));
|
|
51
99
|
|
|
52
100
|
try {
|
|
53
|
-
|
|
54
|
-
if (opts.json) {
|
|
55
|
-
console.log(JSON.stringify(result, null, 2));
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
renderReview(result);
|
|
101
|
+
return await new Orchestrator(config, rules).run(prDiff, context, activeAgents);
|
|
59
102
|
} catch (e: any) {
|
|
60
103
|
console.log(chalk.red(t().reviewFailed(e.message)));
|
|
61
|
-
return;
|
|
104
|
+
return null;
|
|
62
105
|
}
|
|
106
|
+
}
|
|
63
107
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
108
|
+
async function doPostComment(client: GithubClient, owner: string, repo: string, prNumber: number, result: ReviewResult): Promise<void> {
|
|
109
|
+
const spinner = ora(t().postReviewPostingComment).start();
|
|
110
|
+
try {
|
|
111
|
+
await client.postComment(owner, repo, prNumber, buildAivComment(result));
|
|
112
|
+
spinner.succeed(chalk.green(t().postReviewCommentPosted(prNumber)));
|
|
113
|
+
} catch (e: any) {
|
|
114
|
+
spinner.fail(chalk.red(t().postReviewCommentFailed(e.message)));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
68
117
|
|
|
118
|
+
async function doSubmitReview(client: GithubClient, owner: string, repo: string, prNumber: number, action: 'approve' | 'request_changes'): Promise<boolean> {
|
|
69
119
|
const event = action === 'approve' ? 'APPROVE' : 'REQUEST_CHANGES';
|
|
70
|
-
const
|
|
71
|
-
|
|
120
|
+
const spinner = ora(t().postReviewSubmitting).start();
|
|
72
121
|
try {
|
|
73
|
-
await
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
122
|
+
await client.submitReview(owner, repo, prNumber, event);
|
|
123
|
+
const msg = action === 'approve' ? t().postReviewApproved(prNumber) : t().postReviewChangesRequested(prNumber);
|
|
124
|
+
const color = action === 'approve' ? chalk.green : chalk.yellow;
|
|
125
|
+
spinner.succeed(color(msg));
|
|
126
|
+
return true;
|
|
79
127
|
} catch (e: any) {
|
|
80
|
-
|
|
81
|
-
return;
|
|
128
|
+
spinner.fail(chalk.red(t().postReviewFailed(e.message)));
|
|
129
|
+
return false;
|
|
82
130
|
}
|
|
131
|
+
}
|
|
83
132
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
133
|
+
async function doApproveFlow(client: GithubClient, owner: string, repo: string, prNumber: number, result: ReviewResult): Promise<void> {
|
|
134
|
+
const wantsMerge = await confirmMerge(t().postReviewMergeConfirm);
|
|
135
|
+
if (wantsMerge) {
|
|
136
|
+
await doPostComment(client, owner, repo, prNumber, result);
|
|
137
|
+
const strategy = await selectMergeStrategy(t().postReviewSelectMerge);
|
|
138
|
+
const mergeSpinner = ora(t().postReviewMerging(prNumber)).start();
|
|
139
|
+
try {
|
|
140
|
+
await client.mergePR(owner, repo, prNumber, strategy);
|
|
141
|
+
mergeSpinner.succeed(chalk.green(t().postReviewMerged(prNumber)));
|
|
142
|
+
} catch (e: any) {
|
|
143
|
+
mergeSpinner.fail(chalk.red(t().postReviewMergeFailed(e.message)));
|
|
95
144
|
}
|
|
96
|
-
|
|
97
|
-
const refreshSpinner = ora(t().postReviewRefreshing).start();
|
|
98
|
-
await refreshContextFiles(process.cwd());
|
|
99
|
-
refreshSpinner.succeed(chalk.green(t().postReviewRefreshed));
|
|
100
145
|
}
|
|
146
|
+
const refreshSpinner = ora(t().postReviewRefreshing).start();
|
|
147
|
+
await refreshContextFiles(process.cwd());
|
|
148
|
+
refreshSpinner.succeed(chalk.green(t().postReviewRefreshed));
|
|
101
149
|
}
|
|
102
150
|
|
|
103
151
|
// βββ CLI command ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
package/src/cli/renderer.ts
CHANGED
|
@@ -2,6 +2,67 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { ReviewResult, AgentFinding } from '../types';
|
|
3
3
|
import { t } from '../i18n';
|
|
4
4
|
|
|
5
|
+
const SEVERITY_EMOJI: Record<string, string> = {
|
|
6
|
+
critical: 'π΄', high: 'π ', medium: 'π‘', low: 'π΅', info: 'βͺ',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const RISK_EMOJI: Record<string, string> = {
|
|
10
|
+
CRITICAL: 'π΄', HIGH: 'π ', MEDIUM: 'π‘', LOW: 'π’',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function buildAivComment(result: ReviewResult): string {
|
|
14
|
+
const encoded = Buffer.from(JSON.stringify(result)).toString('base64');
|
|
15
|
+
const header = `<!-- aiv-review:${encoded} -->`;
|
|
16
|
+
|
|
17
|
+
const riskEmoji = RISK_EMOJI[result.riskLabel] ?? 'βͺ';
|
|
18
|
+
const lines: string[] = [
|
|
19
|
+
header,
|
|
20
|
+
'',
|
|
21
|
+
`## π€ aiv Review β PR #${result.prNumber} Β· ${result.prTitle}`,
|
|
22
|
+
'',
|
|
23
|
+
`**Risk Score:** ${riskEmoji} \`${result.riskScore}/100 ${result.riskLabel}\` Β· Generated: ${result.generatedAt}`,
|
|
24
|
+
'',
|
|
25
|
+
'---',
|
|
26
|
+
'',
|
|
27
|
+
'### Executive Summary',
|
|
28
|
+
result.executiveSummary,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
if (result.securityIssues.length > 0) {
|
|
32
|
+
lines.push('', '---', '', '### π΄ Security Issues');
|
|
33
|
+
for (const f of result.securityIssues) lines.push(...findingMd(f));
|
|
34
|
+
}
|
|
35
|
+
if (result.businessRisks.length > 0) {
|
|
36
|
+
lines.push('', '---', '', '### π‘ Business Risks');
|
|
37
|
+
for (const f of result.businessRisks) lines.push(...findingMd(f));
|
|
38
|
+
}
|
|
39
|
+
if (result.architectureIssues.length > 0) {
|
|
40
|
+
lines.push('', '---', '', '### π΅ Architecture Issues');
|
|
41
|
+
for (const f of result.architectureIssues) lines.push(...findingMd(f));
|
|
42
|
+
}
|
|
43
|
+
if (result.possibleRegressions.length > 0) {
|
|
44
|
+
lines.push('', '---', '', '### β οΈ Possible Regressions');
|
|
45
|
+
for (const r of result.possibleRegressions) lines.push(`- ${r}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
lines.push('', '---', '', '<details>', '<summary>Agent Summaries</summary>', '');
|
|
49
|
+
for (const a of result.agents) {
|
|
50
|
+
lines.push(`**${a.agentName.toUpperCase()}** \`[${a.riskScore}/100]\``, a.summary, '');
|
|
51
|
+
}
|
|
52
|
+
lines.push('</details>', '', '---', '*Generated by [aiv](https://www.npmjs.com/package/@ateriss_/aiv-cli)*');
|
|
53
|
+
|
|
54
|
+
return lines.join('\n');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findingMd(f: AgentFinding): string[] {
|
|
58
|
+
const emoji = SEVERITY_EMOJI[f.severity] ?? 'βͺ';
|
|
59
|
+
const out = [`\n**${emoji} [${f.severity.toUpperCase()}] ${f.title}**`];
|
|
60
|
+
if (f.file) out.push(`\`${f.file}\``);
|
|
61
|
+
out.push(f.description);
|
|
62
|
+
if (f.suggestion) out.push(`> π‘ ${f.suggestion}`);
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
5
66
|
export function renderReview(result: ReviewResult): void {
|
|
6
67
|
const tr = t();
|
|
7
68
|
const riskColor = riskChalk(result.riskLabel);
|
package/src/cli/selector.ts
CHANGED
|
@@ -42,7 +42,7 @@ export async function selectPR(prs: PullRequest[], message: string): Promise<Pul
|
|
|
42
42
|
return selected === CANCEL ? null : (prs.find(pr => pr.number === selected) ?? null);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
export type PostReviewAction = 'approve' | 'request_changes' | 'skip';
|
|
45
|
+
export type PostReviewAction = 'approve' | 'request_changes' | 'post_comment' | 'skip';
|
|
46
46
|
export type MergeStrategy = 'merge' | 'squash' | 'rebase';
|
|
47
47
|
|
|
48
48
|
export async function selectPostReviewAction(message: string): Promise<PostReviewAction> {
|
|
@@ -53,12 +53,13 @@ export async function selectPostReviewAction(message: string): Promise<PostRevie
|
|
|
53
53
|
name: 'action',
|
|
54
54
|
message,
|
|
55
55
|
choices: [
|
|
56
|
-
{ name: chalk.green('β Approve PR'),
|
|
57
|
-
{ name: chalk.yellow('β Request Changes'),
|
|
56
|
+
{ name: chalk.green('β Approve PR'), value: 'approve', short: 'Approve' },
|
|
57
|
+
{ name: chalk.yellow('β Request Changes'), value: 'request_changes', short: 'Request Changes' },
|
|
58
|
+
{ name: chalk.cyan('π¬ Post as PR comment'), value: 'post_comment', short: 'Post Comment' },
|
|
58
59
|
new inquirer.Separator('β'.repeat(42)),
|
|
59
|
-
{ name: chalk.dim('β© Skip'),
|
|
60
|
+
{ name: chalk.dim('β© Skip'), value: 'skip', short: 'Skip' },
|
|
60
61
|
],
|
|
61
|
-
pageSize:
|
|
62
|
+
pageSize: 5,
|
|
62
63
|
loop: false,
|
|
63
64
|
}]);
|
|
64
65
|
|
package/src/git/github.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { PullRequest, PRDiff, PRFile } from '../types';
|
|
1
|
+
import { PullRequest, PRDiff, PRFile, ReviewResult } from '../types';
|
|
2
|
+
|
|
3
|
+
const AIV_COMMENT_TAG = '<!-- aiv-review:';
|
|
2
4
|
|
|
3
5
|
const GITHUB_API = 'https://api.github.com';
|
|
4
6
|
|
|
@@ -59,6 +61,36 @@ export class GithubClient {
|
|
|
59
61
|
return { pr, files, rawDiff };
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
async findAivReview(owner: string, repo: string, prNumber: number): Promise<ReviewResult | null> {
|
|
65
|
+
const url = `${GITHUB_API}/repos/${owner}/${repo}/issues/${prNumber}/comments?per_page=100`;
|
|
66
|
+
const res = await this.fetch(url);
|
|
67
|
+
if (!res.ok) return null;
|
|
68
|
+
|
|
69
|
+
const comments = await res.json() as any[];
|
|
70
|
+
for (const c of comments) {
|
|
71
|
+
const body: string = c.body ?? '';
|
|
72
|
+
if (!body.startsWith(AIV_COMMENT_TAG)) continue;
|
|
73
|
+
const end = body.indexOf(' -->');
|
|
74
|
+
if (end === -1) continue;
|
|
75
|
+
try {
|
|
76
|
+
const encoded = body.slice(AIV_COMMENT_TAG.length, end).trim();
|
|
77
|
+
return JSON.parse(Buffer.from(encoded, 'base64').toString('utf8')) as ReviewResult;
|
|
78
|
+
} catch { continue; }
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async postComment(owner: string, repo: string, prNumber: number, body: string): Promise<void> {
|
|
84
|
+
const url = `${GITHUB_API}/repos/${owner}/${repo}/issues/${prNumber}/comments`;
|
|
85
|
+
const { default: fetch } = await import('node-fetch');
|
|
86
|
+
const res = await fetch(url, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { ...this.headers, 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({ body }),
|
|
90
|
+
}) as unknown as Response;
|
|
91
|
+
if (!res.ok) await this.throwError(res, `post comment on PR #${prNumber}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
62
94
|
async mergePR(
|
|
63
95
|
owner: string,
|
|
64
96
|
repo: string,
|
package/src/i18n/en.ts
CHANGED
|
@@ -183,6 +183,13 @@ export const en = {
|
|
|
183
183
|
postReviewMergeStrategyMerge: 'Merge commit',
|
|
184
184
|
postReviewMergeStrategySquash: 'Squash and merge',
|
|
185
185
|
postReviewMergeStrategyRebase: 'Rebase and merge',
|
|
186
|
+
postReviewPostingComment: 'Posting review as PR comment...',
|
|
187
|
+
postReviewCommentPosted: (n: number) => ` Review posted as comment on PR #${n}.`,
|
|
188
|
+
postReviewCommentFailed: (msg: string) => `Failed to post comment: ${msg}`,
|
|
189
|
+
reviewCachedFound: 'Found a previous aiv analysis on this PR.',
|
|
190
|
+
reviewCachedUse: 'Use cached analysis?',
|
|
191
|
+
reviewCachedUsing: 'Using cached analysis.',
|
|
192
|
+
reviewCachedSkipping: 'Running fresh analysis...',
|
|
186
193
|
|
|
187
194
|
// ββ context generate βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
188
195
|
contextGenerateTitle: '\n Generating context and rules with AI...\n',
|
package/src/i18n/es.ts
CHANGED
|
@@ -185,6 +185,13 @@ export const es: TranslationKeys = {
|
|
|
185
185
|
postReviewMergeStrategyMerge: 'Merge commit',
|
|
186
186
|
postReviewMergeStrategySquash: 'Squash and merge',
|
|
187
187
|
postReviewMergeStrategyRebase: 'Rebase and merge',
|
|
188
|
+
postReviewPostingComment: 'Publicando revisiΓ³n como comentario del PR...',
|
|
189
|
+
postReviewCommentPosted: (n: number) => ` RevisiΓ³n publicada como comentario en el PR #${n}.`,
|
|
190
|
+
postReviewCommentFailed: (msg: string) => `Error al publicar comentario: ${msg}`,
|
|
191
|
+
reviewCachedFound: 'Se encontrΓ³ un anΓ‘lisis previo de aiv en este PR.',
|
|
192
|
+
reviewCachedUse: 'ΒΏUsar el anΓ‘lisis en cachΓ©?',
|
|
193
|
+
reviewCachedUsing: 'Usando anΓ‘lisis en cachΓ©.',
|
|
194
|
+
reviewCachedSkipping: 'Ejecutando anΓ‘lisis nuevo...',
|
|
188
195
|
|
|
189
196
|
// ββ context generate βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
190
197
|
contextGenerateTitle: '\n Generando contexto y reglas con IA...\n',
|