@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "name": "@ateriss_/aiv-cli",
3
- "version": "1.0.1",
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": ["ai", "pr-review", "cli", "github", "claude", "openai"],
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
- 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);
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
- if (!process.stdout.isTTY) return;
65
-
66
- const action = await selectPostReviewAction(t().postReviewSelectAction);
67
- if (action === 'skip') return;
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 submitSpinner = ora(t().postReviewSubmitting).start();
71
-
120
+ const spinner = ora(t().postReviewSubmitting).start();
72
121
  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
- }
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
- submitSpinner.fail(chalk.red(t().postReviewFailed(e.message)));
81
- return;
128
+ spinner.fail(chalk.red(t().postReviewFailed(e.message)));
129
+ return false;
82
130
  }
131
+ }
83
132
 
84
- if (action === 'approve') {
85
- const wantsMerge = await confirmMerge(t().postReviewMergeConfirm);
86
- if (wantsMerge) {
87
- const strategy = await selectMergeStrategy(t().postReviewSelectMerge);
88
- const mergeSpinner = ora(t().postReviewMerging(prNumber)).start();
89
- try {
90
- await new GithubClient(token).mergePR(owner, repo, prNumber, strategy);
91
- mergeSpinner.succeed(chalk.green(t().postReviewMerged(prNumber)));
92
- } catch (e: any) {
93
- mergeSpinner.fail(chalk.red(t().postReviewMergeFailed(e.message)));
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 ──────────────────────────────────────────────────────────────
@@ -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);
@@ -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'), value: 'approve', short: 'Approve' },
57
- { name: chalk.yellow('βš‘ Request Changes'), value: 'request_changes', short: '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'), value: 'skip', short: 'Skip' },
60
+ { name: chalk.dim('↩ Skip'), value: 'skip', short: 'Skip' },
60
61
  ],
61
- pageSize: 4,
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',
package/tsup.config.ts CHANGED
@@ -9,4 +9,5 @@ export default defineConfig({
9
9
  splitting: false,
10
10
  sourcemap: false,
11
11
  dts: false,
12
+ noExternal: [/.*/],
12
13
  });