@ateriss_/aiv-cli 1.0.2 → 1.0.3
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 +89 -9
- package/dist/index.js +324 -255
- package/package.json +2 -2
- package/src/agents/architecture.ts +10 -14
- package/src/agents/base.ts +15 -7
- package/src/agents/business.ts +8 -13
- package/src/agents/security.ts +12 -14
- package/src/agents/triage.ts +66 -0
- package/src/cli/commands/check.ts +216 -0
- package/src/cli/commands/prs.ts +1 -1
- package/src/cli/commands/review.ts +7 -4
- package/src/cli/selector.ts +53 -1
- package/src/context/generator.ts +161 -65
- package/src/git/github.ts +20 -0
- package/src/git/local.ts +113 -0
- package/src/git/utils.ts +17 -8
- package/src/i18n/en.ts +19 -1
- package/src/i18n/es.ts +19 -1
- package/src/index.ts +2 -0
- package/src/orchestrator/index.ts +48 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ateriss_/aiv-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "AI-powered PR reviewer CLI — local-first, multi-agent semantic analysis",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"claude",
|
|
22
22
|
"openai"
|
|
23
23
|
],
|
|
24
|
-
"author": "",
|
|
24
|
+
"author": "Alexandra Linares Viña (Ateriss)",
|
|
25
25
|
"license": "MIT",
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@anthropic-ai/sdk": "^0.37.0",
|
|
@@ -6,23 +6,19 @@ export class ArchitectureReviewer extends BaseAgent {
|
|
|
6
6
|
|
|
7
7
|
readonly systemPrompt = `You are a principal software architect performing a code review.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- Layer violations (
|
|
13
|
-
-
|
|
14
|
-
- Single Responsibility:
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
- Scalability concerns: will this break under load or as the system grows?
|
|
18
|
-
- Consistency with existing patterns in the codebase
|
|
19
|
-
- Over-engineering or under-engineering
|
|
20
|
-
- Fragile design decisions that will require future rework
|
|
9
|
+
First, assess whether this PR contains structural changes worth reviewing (new modules, refactors, dependency changes, layer interactions). If the diff only touches styles, templates, copy, config values, or trivial one-liners with no structural impact, return riskScore: 0, empty findings, and a one-line summary like "No architectural concerns."
|
|
10
|
+
|
|
11
|
+
When the diff IS structurally relevant, focus on:
|
|
12
|
+
- Layer violations (business logic in controllers, DB logic in service layer, etc.)
|
|
13
|
+
- Unnecessary coupling or new cross-module dependencies
|
|
14
|
+
- Single Responsibility violations: files/classes doing too many things
|
|
15
|
+
- Abstraction quality: poorly named, too broad, or unnecessary abstractions
|
|
16
|
+
- Structural decisions that will require future rework
|
|
21
17
|
|
|
22
18
|
You are NOT checking syntax, linting, or security.
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
Be concise: titles ≤ 8 words, descriptions ≤ 2 sentences, suggestions ≤ 1 sentence, summary ≤ 3 sentences.
|
|
21
|
+
For INFO-level findings, write description as a single short phrase ending with " — OK" or " — noted".
|
|
26
22
|
|
|
27
23
|
Return only valid JSON as specified.`;
|
|
28
24
|
|
package/src/agents/base.ts
CHANGED
|
@@ -41,10 +41,18 @@ export abstract class BaseAgent {
|
|
|
41
41
|
? '\n\nIMPORTANT: Respond in Spanish. All string values in the JSON (summary, title, description, suggestion, possibleRegressions) must be written in Spanish.'
|
|
42
42
|
: '';
|
|
43
43
|
|
|
44
|
+
const baseBranch = ctx.diff.pr.base;
|
|
45
|
+
const headBranch = ctx.diff.pr.branch;
|
|
46
|
+
const isProductionMerge = /^(main|master|production|prod|release)$/i.test(baseBranch);
|
|
47
|
+
const branchRisk = isProductionMerge
|
|
48
|
+
? `⚠️ DIRECT MERGE TO ${baseBranch.toUpperCase()} — apply stricter scrutiny. Findings here affect production.`
|
|
49
|
+
: `Target: ${baseBranch} (non-production). Calibrate severity accordingly.`;
|
|
50
|
+
|
|
44
51
|
return `## PR: #${ctx.diff.pr.number} — ${ctx.diff.pr.title}
|
|
45
52
|
|
|
46
53
|
**Author:** ${ctx.diff.pr.author}
|
|
47
|
-
**
|
|
54
|
+
**Merge:** \`${headBranch}\` → \`${baseBranch}\`
|
|
55
|
+
**${branchRisk}**
|
|
48
56
|
**Description:** ${ctx.diff.pr.description ?? 'No description provided.'}
|
|
49
57
|
|
|
50
58
|
---
|
|
@@ -73,19 +81,19 @@ ${patches}
|
|
|
73
81
|
|
|
74
82
|
Analyze the above and return a JSON response matching this schema:
|
|
75
83
|
{
|
|
76
|
-
"summary": "
|
|
84
|
+
"summary": "2-3 sentences max",
|
|
77
85
|
"findings": [
|
|
78
86
|
{
|
|
79
87
|
"severity": "critical|high|medium|low|info",
|
|
80
88
|
"category": "string",
|
|
81
|
-
"title": "string",
|
|
82
|
-
"description": "
|
|
89
|
+
"title": "string — 8 words max",
|
|
90
|
+
"description": "1-2 sentences max",
|
|
83
91
|
"file": "string (optional)",
|
|
84
|
-
"suggestion": "
|
|
92
|
+
"suggestion": "1 sentence max (optional)"
|
|
85
93
|
}
|
|
86
94
|
],
|
|
87
95
|
"riskScore": 0-100,
|
|
88
|
-
"possibleRegressions": ["
|
|
96
|
+
"possibleRegressions": ["one short phrase each"]
|
|
89
97
|
}
|
|
90
98
|
|
|
91
99
|
Return ONLY valid JSON. No markdown fences, no explanation outside the JSON.${langInstruction}`;
|
|
@@ -124,7 +132,7 @@ Return ONLY valid JSON. No markdown fences, no explanation outside the JSON.${la
|
|
|
124
132
|
agentName: this.agentName,
|
|
125
133
|
findings,
|
|
126
134
|
summary: parsed.summary ?? '',
|
|
127
|
-
riskScore: Math.max(0, Math.min(100, parseInt(parsed.riskScore ?? '0'))),
|
|
135
|
+
riskScore: Math.max(0, Math.min(100, Number.parseInt(parsed.riskScore ?? '0'))),
|
|
128
136
|
};
|
|
129
137
|
}
|
|
130
138
|
}
|
package/src/agents/business.ts
CHANGED
|
@@ -6,23 +6,18 @@ export class BusinessReviewer extends BaseAgent {
|
|
|
6
6
|
|
|
7
7
|
readonly systemPrompt = `You are a senior business analyst and domain expert performing a code review.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
- Business logic correctness
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- Side effects on other business flows (billing, notifications, state machines, etc.)
|
|
16
|
-
- Missing required steps (auditing, logging, approvals, notifications)
|
|
9
|
+
First, assess whether this PR contains changes relevant to business logic (data mutations, calculations, state transitions, domain rules, flows, validations). If the diff is purely cosmetic (styles, colors, text copy, console.log removal, import reordering, type annotations only), return riskScore: 0, empty findings, and a one-line summary like "No business logic changes."
|
|
10
|
+
|
|
11
|
+
When the diff IS business-relevant, focus on:
|
|
12
|
+
- Business logic correctness and domain rule violations
|
|
13
|
+
- Functional regressions that break existing user-facing behavior
|
|
14
|
+
- Missing required steps (auditing, logging, approvals) per rules.yml
|
|
17
15
|
- Incorrect calculations, status transitions, or conditional logic
|
|
18
|
-
- Data integrity concerns (missing validations, incorrect defaults)
|
|
19
16
|
|
|
20
17
|
You are NOT a linter, NOT a security scanner. You analyze MEANING, not syntax.
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Be concrete. Reference specific lines or functions when relevant.
|
|
25
|
-
Assign a riskScore from 0 (no risk) to 100 (critical business risk).
|
|
19
|
+
Be concise: titles ≤ 8 words, descriptions ≤ 2 sentences, suggestions ≤ 1 sentence, summary ≤ 3 sentences.
|
|
20
|
+
For INFO-level findings (positive or neutral observations), write description as a single short phrase ending with " — OK" or " — noted".
|
|
26
21
|
|
|
27
22
|
Return only valid JSON as specified.`;
|
|
28
23
|
|
package/src/agents/security.ts
CHANGED
|
@@ -6,24 +6,22 @@ export class SecurityReviewer extends BaseAgent {
|
|
|
6
6
|
|
|
7
7
|
readonly systemPrompt = `You are a senior application security engineer performing a code review.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
-
|
|
13
|
-
- Injection vulnerabilities (SQL, NoSQL, command
|
|
14
|
-
-
|
|
15
|
-
- Sensitive data exposure (logging secrets, returning PII, insecure storage)
|
|
16
|
-
- Cryptographic issues (weak algorithms, hardcoded secrets, insecure RNG)
|
|
9
|
+
First, assess whether this PR touches code with security implications (auth, API endpoints, data storage, input handling, dependencies, secrets, permissions). If the diff only changes styles, UI copy, non-sensitive config values, or cosmetic code, return riskScore: 0, empty findings, and a one-line summary like "No security-relevant changes."
|
|
10
|
+
|
|
11
|
+
When the diff IS security-relevant, focus on:
|
|
12
|
+
- Auth/authorization flaws (missing checks, privilege escalation)
|
|
13
|
+
- Injection vulnerabilities (SQL, NoSQL, command injection)
|
|
14
|
+
- Sensitive data exposure (logging secrets, returning PII)
|
|
17
15
|
- Input validation gaps (missing sanitization, unsafe deserialization)
|
|
18
|
-
- Race conditions or TOCTOU vulnerabilities
|
|
19
16
|
- Broken access control in API endpoints
|
|
20
|
-
-
|
|
21
|
-
- Dependency security (new packages with known issues)
|
|
17
|
+
- New dependencies with known vulnerabilities
|
|
22
18
|
|
|
23
|
-
Mark findings
|
|
19
|
+
Mark findings in sensitive_modules from rules.yml with higher severity.
|
|
20
|
+
Name the vulnerability class (OWASP) when applicable.
|
|
21
|
+
Assign a riskScore from 0 (no issues) to 100 (block immediately).
|
|
24
22
|
|
|
25
|
-
Be
|
|
26
|
-
|
|
23
|
+
Be concise: titles ≤ 8 words, descriptions ≤ 2 sentences, suggestions ≤ 1 sentence, summary ≤ 3 sentences.
|
|
24
|
+
For INFO-level findings, write description as a single short phrase ending with " — OK" or " — noted".
|
|
27
25
|
|
|
28
26
|
Return only valid JSON as specified.`;
|
|
29
27
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { PRDiff } from '../types';
|
|
2
|
+
import { LLMProvider } from '../providers/base';
|
|
3
|
+
|
|
4
|
+
export type TriageResult = {
|
|
5
|
+
agents: Array<'business' | 'architecture' | 'security'>;
|
|
6
|
+
reasoning: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const ALL_AGENTS: TriageResult['agents'] = ['business', 'architecture', 'security'];
|
|
10
|
+
|
|
11
|
+
const SYSTEM_PROMPT = `You are a code review triage specialist.
|
|
12
|
+
Given a PR's metadata and changed file list, decide which review agents are needed.
|
|
13
|
+
|
|
14
|
+
Available agents:
|
|
15
|
+
- business: domain logic, business rules, calculations, state transitions, data integrity
|
|
16
|
+
- architecture: layer violations, module coupling, structural patterns, dependency direction
|
|
17
|
+
- security: auth, injection, data exposure, input validation, access control, secrets
|
|
18
|
+
|
|
19
|
+
Rules:
|
|
20
|
+
- Only include an agent if the changes are genuinely relevant to its domain
|
|
21
|
+
- Cosmetic changes (styles, colors, copy, console.log removal, comment edits) need NO agents — return []
|
|
22
|
+
- CSS/SCSS/style-only changes → skip all agents
|
|
23
|
+
- Purely adding tests with no production code change → business only (regression check), skip security and architecture
|
|
24
|
+
- Config-only changes (env vars, CI files) → security only if secrets/permissions involved, otherwise skip
|
|
25
|
+
- When in doubt about an agent, skip it — it's better to miss a low-risk finding than waste tokens
|
|
26
|
+
|
|
27
|
+
Return ONLY valid JSON: { "agents": ["security", "business"], "reasoning": "one sentence" }
|
|
28
|
+
No markdown fences. No explanation outside the JSON.`;
|
|
29
|
+
|
|
30
|
+
export async function triageAgents(diff: PRDiff, provider: LLMProvider): Promise<TriageResult> {
|
|
31
|
+
const fileList = diff.files
|
|
32
|
+
.map(f => `${f.status.toUpperCase()} ${f.filename} (+${f.additions}/-${f.deletions})`)
|
|
33
|
+
.join('\n');
|
|
34
|
+
|
|
35
|
+
const message = `PR #${diff.pr.number}: ${diff.pr.title}
|
|
36
|
+
Branch: ${diff.pr.branch} → ${diff.pr.base}
|
|
37
|
+
Description: ${diff.pr.description ?? 'none'}
|
|
38
|
+
|
|
39
|
+
Changed files (${diff.files.length}):
|
|
40
|
+
${fileList}`;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const response = await provider.complete(
|
|
44
|
+
[{ role: 'user', content: message }],
|
|
45
|
+
SYSTEM_PROMPT,
|
|
46
|
+
256,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const match = /\{[\s\S]*\}/.exec(response.content.trim());
|
|
50
|
+
if (!match) return fallback();
|
|
51
|
+
|
|
52
|
+
const parsed = JSON.parse(match[0]) as { agents?: unknown; reasoning?: string };
|
|
53
|
+
const agents = (Array.isArray(parsed.agents) ? parsed.agents : [])
|
|
54
|
+
.filter((a): a is TriageResult['agents'][number] =>
|
|
55
|
+
a === 'business' || a === 'architecture' || a === 'security'
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
return { agents, reasoning: parsed.reasoning ?? '' };
|
|
59
|
+
} catch {
|
|
60
|
+
return fallback();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function fallback(): TriageResult {
|
|
65
|
+
return { agents: ALL_AGENTS, reasoning: 'triage failed — running all agents' };
|
|
66
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { resolveConfig, loadRules, getGithubToken, isInitialized } from '../../config';
|
|
7
|
+
import type { ReviewResult, AgentFinding } from '../../types';
|
|
8
|
+
import { GithubClient } from '../../git/github';
|
|
9
|
+
import { detectRepoInfo, isGitRepo } from '../../git/utils';
|
|
10
|
+
import { getCurrentBranch, detectBaseBranch, buildLocalPRDiff, getLastCommitTitle, hasUnpushedCommits } from '../../git/local';
|
|
11
|
+
import { Orchestrator } from '../../orchestrator';
|
|
12
|
+
import { ContextManager } from '../../context/manager';
|
|
13
|
+
import { renderReview } from '../renderer';
|
|
14
|
+
import { selectPrecheckAction, promptPRDetails } from '../selector';
|
|
15
|
+
import { t } from '../../i18n';
|
|
16
|
+
|
|
17
|
+
function terminalLink(label: string, filePath: string): string {
|
|
18
|
+
const url = 'file:///' + filePath.replaceAll('\\', '/');
|
|
19
|
+
return `]8;;${url}${label}]8;;`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function checkCommand(): Command {
|
|
23
|
+
return new Command('check')
|
|
24
|
+
.alias('c')
|
|
25
|
+
.description('Analyze local diff before creating a PR')
|
|
26
|
+
.argument('[base]', 'Base branch to compare against (default: main/master)')
|
|
27
|
+
.option('--owner <owner>', 'GitHub owner (for PR creation)')
|
|
28
|
+
.option('--repo <repo>', 'GitHub repo (for PR creation)')
|
|
29
|
+
.option('--agent <agents...>', 'Run specific agents only (business, architecture, security)')
|
|
30
|
+
.option('--json', 'Output raw JSON result')
|
|
31
|
+
.action(async (baseArg: string | undefined, opts) => {
|
|
32
|
+
if (!isInitialized()) {
|
|
33
|
+
console.log(chalk.red(t().notInitialized));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const cwd = process.cwd();
|
|
38
|
+
|
|
39
|
+
if (!isGitRepo(cwd)) {
|
|
40
|
+
console.log(chalk.red(t().precheckNotGitRepo));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const head = getCurrentBranch(cwd);
|
|
45
|
+
const base = baseArg ?? detectBaseBranch(cwd);
|
|
46
|
+
|
|
47
|
+
console.log(chalk.bold(t().precheckTitle(head, base)));
|
|
48
|
+
|
|
49
|
+
const diffSpinner = ora(t().precheckBuilding(base)).start();
|
|
50
|
+
let prDiff: ReturnType<typeof buildLocalPRDiff>;
|
|
51
|
+
try {
|
|
52
|
+
prDiff = buildLocalPRDiff(cwd, base);
|
|
53
|
+
if (prDiff.files.length === 0) {
|
|
54
|
+
diffSpinner.warn(chalk.yellow(t().precheckNoChanges));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
diffSpinner.succeed(t().precheckDiffBuilt(prDiff.files.length));
|
|
58
|
+
} catch (e: any) {
|
|
59
|
+
diffSpinner.fail(chalk.red(t().precheckDiffFailed(e.message)));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const config = resolveConfig();
|
|
64
|
+
const rules = loadRules();
|
|
65
|
+
const activeAgents = opts.agent ?? ['business', 'architecture', 'security'];
|
|
66
|
+
const auto = !opts.agent;
|
|
67
|
+
|
|
68
|
+
const ctxSpinner = ora(t().reviewLoadingContext).start();
|
|
69
|
+
const context = new ContextManager(cwd).buildReviewContext(prDiff);
|
|
70
|
+
ctxSpinner.succeed(t().reviewContextLoaded);
|
|
71
|
+
console.log(chalk.dim(t().reviewRunningAgents(activeAgents.join(', '))));
|
|
72
|
+
|
|
73
|
+
let result: ReviewResult;
|
|
74
|
+
try {
|
|
75
|
+
result = await new Orchestrator(config, rules).run(prDiff, context, activeAgents, auto);
|
|
76
|
+
} catch (e: any) {
|
|
77
|
+
console.log(chalk.red(t().reviewFailed(e.message)));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (opts.json) {
|
|
82
|
+
console.log(JSON.stringify(result, null, 2));
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
renderReview(result);
|
|
87
|
+
if (!process.stdout.isTTY) return;
|
|
88
|
+
|
|
89
|
+
const needsPush = hasUnpushedCommits(cwd, head);
|
|
90
|
+
const hasFindings = result.riskScore > 0 || result.agents.some(a => a.findings.length > 0);
|
|
91
|
+
const action = await selectPrecheckAction(t().precheckSelectAction, needsPush, hasFindings);
|
|
92
|
+
|
|
93
|
+
if (action === 'skip') return;
|
|
94
|
+
|
|
95
|
+
if (action === 'save_checklist') {
|
|
96
|
+
await doSaveChecklist(cwd, result, head, base);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (action === 'create_pr') {
|
|
101
|
+
await doCreatePR(cwd, opts, config, result, head, base, needsPush);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function doCreatePR(
|
|
107
|
+
cwd: string,
|
|
108
|
+
opts: any,
|
|
109
|
+
config: ReturnType<typeof resolveConfig>,
|
|
110
|
+
result: ReviewResult,
|
|
111
|
+
head: string,
|
|
112
|
+
base: string,
|
|
113
|
+
needsPush: boolean,
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
let token: string;
|
|
116
|
+
try {
|
|
117
|
+
token = getGithubToken(config);
|
|
118
|
+
} catch {
|
|
119
|
+
console.log(chalk.red(t().prsMissingToken(config.github.token_env)));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const repoInfo = detectRepoInfo(cwd);
|
|
124
|
+
const owner = opts.owner ?? config.github.owner ?? repoInfo?.owner;
|
|
125
|
+
const repo = opts.repo ?? config.github.repo ?? repoInfo?.repo;
|
|
126
|
+
|
|
127
|
+
if (!owner || !repo) {
|
|
128
|
+
console.log(chalk.red(t().precheckMissingConfig));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (needsPush) {
|
|
133
|
+
const pushSpinner = ora(`Pushing ${chalk.cyan(head)}...`).start();
|
|
134
|
+
try {
|
|
135
|
+
const { execSync } = await import('node:child_process');
|
|
136
|
+
execSync(`git push origin ${head}`, { cwd, stdio: 'pipe' });
|
|
137
|
+
pushSpinner.succeed(chalk.green(`Branch ${chalk.cyan(head)} pushed.`));
|
|
138
|
+
} catch (e: any) {
|
|
139
|
+
pushSpinner.fail(chalk.red(`Push failed: ${e.message}`));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const defaultTitle = getLastCommitTitle(cwd) || head;
|
|
145
|
+
const { title, body } = await promptPRDetails(defaultTitle);
|
|
146
|
+
|
|
147
|
+
const spinner = ora(t().precheckCreatingPR).start();
|
|
148
|
+
try {
|
|
149
|
+
const pr = await new GithubClient(token).createPR(owner, repo, title, body, head, base);
|
|
150
|
+
spinner.succeed(chalk.green(t().precheckPRCreated(pr.url)));
|
|
151
|
+
} catch (e: any) {
|
|
152
|
+
spinner.fail(chalk.red(t().precheckPRFailed(e.message)));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function doSaveChecklist(cwd: string, result: ReviewResult, head: string, base: string): Promise<void> {
|
|
157
|
+
const aivDir = path.join(cwd, '.aiv');
|
|
158
|
+
if (!fs.existsSync(aivDir)) fs.mkdirSync(aivDir, { recursive: true });
|
|
159
|
+
|
|
160
|
+
const filePath = path.join(aivDir, 'checklist.md');
|
|
161
|
+
const content = buildChecklist(result, head, base);
|
|
162
|
+
|
|
163
|
+
const spinner = ora(t().precheckSavingChecklist).start();
|
|
164
|
+
try {
|
|
165
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
166
|
+
const link = terminalLink('.aiv/checklist.md', filePath);
|
|
167
|
+
spinner.succeed(chalk.green(t().precheckChecklistSaved(link)));
|
|
168
|
+
} catch (e: any) {
|
|
169
|
+
spinner.fail(chalk.red(`Failed to save checklist: ${e.message}`));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function findingLine(f: AgentFinding, bold: boolean): string[] {
|
|
174
|
+
const loc = f.file ? ` — \`${f.file}\`` : '';
|
|
175
|
+
const label = bold ? `**[${f.severity.toUpperCase()}]**` : `[${f.severity.toUpperCase()}]`;
|
|
176
|
+
const row = `- [ ] ${label} ${f.title}${loc}`;
|
|
177
|
+
return f.suggestion ? [row, ` > ${f.suggestion}`] : [row];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function section(heading: string, items: string[]): string[] {
|
|
181
|
+
return [`## ${heading}`, '', ...items, ''];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function buildChecklist(result: ReviewResult, head: string, base: string): string {
|
|
185
|
+
const date = new Date(result.generatedAt).toISOString().split('T')[0];
|
|
186
|
+
const all = [...result.securityIssues, ...result.businessRisks, ...result.architectureIssues];
|
|
187
|
+
|
|
188
|
+
const critical = all.filter(f => f.severity === 'critical');
|
|
189
|
+
const high = all.filter(f => f.severity === 'high');
|
|
190
|
+
const medium = all.filter(f => f.severity === 'medium');
|
|
191
|
+
const low = all.filter(f => f.severity === 'low' || f.severity === 'info');
|
|
192
|
+
|
|
193
|
+
const body: string[] = [];
|
|
194
|
+
|
|
195
|
+
if (critical.length > 0 || high.length > 0) {
|
|
196
|
+
body.push(...section('Critical & High', [...critical, ...high].flatMap(f => findingLine(f, true))));
|
|
197
|
+
}
|
|
198
|
+
if (medium.length > 0) {
|
|
199
|
+
body.push(...section('Medium', medium.flatMap(f => findingLine(f, true))));
|
|
200
|
+
}
|
|
201
|
+
if (low.length > 0) {
|
|
202
|
+
body.push(...section('Low / Info', low.flatMap(f => findingLine(f, false))));
|
|
203
|
+
}
|
|
204
|
+
if (result.possibleRegressions.length > 0) {
|
|
205
|
+
body.push(...section('Possible Regressions', result.possibleRegressions.map(r => `- [ ] ${r}`)));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return [
|
|
209
|
+
`# aiv Check: ${head} → ${base}`,
|
|
210
|
+
`Generated: ${date} | Risk: ${result.riskLabel} (${result.riskScore}/100)`,
|
|
211
|
+
'',
|
|
212
|
+
...body,
|
|
213
|
+
'---',
|
|
214
|
+
'*Generated by [aiv](https://www.npmjs.com/package/@ateriss_/aiv-cli)*',
|
|
215
|
+
].join('\n');
|
|
216
|
+
}
|
package/src/cli/commands/prs.ts
CHANGED
|
@@ -63,7 +63,7 @@ export function prsCommand(): Command {
|
|
|
63
63
|
chalk.cyan(`#${pr.number}`),
|
|
64
64
|
truncate(pr.title, 50),
|
|
65
65
|
chalk.dim(pr.author),
|
|
66
|
-
chalk.dim(pr.branch),
|
|
66
|
+
chalk.dim(pr.branch) + chalk.dim(' → ') + chalk.cyan(pr.base),
|
|
67
67
|
chalk.green(`+${pr.additions}`) + chalk.dim('/') + chalk.red(`-${pr.deletions}`),
|
|
68
68
|
chalk.dim(formatDate(pr.createdAt)),
|
|
69
69
|
]);
|
|
@@ -26,6 +26,7 @@ export interface RunReviewOptions {
|
|
|
26
26
|
export async function runReview(opts: RunReviewOptions): Promise<void> {
|
|
27
27
|
const { prNumber, owner, repo, config, token } = opts;
|
|
28
28
|
const rules = loadRules();
|
|
29
|
+
const auto = !opts.agents;
|
|
29
30
|
const activeAgents = opts.agents ?? ['business', 'architecture', 'security'];
|
|
30
31
|
const client = new GithubClient(token);
|
|
31
32
|
|
|
@@ -42,7 +43,7 @@ export async function runReview(opts: RunReviewOptions): Promise<void> {
|
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
const result = await resolveResult(client, { owner, repo, prNumber }, config, rules,
|
|
46
|
+
const result = await resolveResult(client, { owner, repo, prNumber }, { config, rules, agents: activeAgents, auto }, prDiff, opts.json);
|
|
46
47
|
if (!result) return;
|
|
47
48
|
|
|
48
49
|
if (opts.json) {
|
|
@@ -70,14 +71,16 @@ export async function runReview(opts: RunReviewOptions): Promise<void> {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
interface RepoRef { owner: string; repo: string; prNumber: number; }
|
|
74
|
+
interface AgentOpts { config: ResolvedConfig; rules: ReturnType<typeof loadRules>; agents: string[]; auto: boolean; }
|
|
73
75
|
|
|
74
76
|
async function resolveResult(
|
|
75
77
|
client: GithubClient,
|
|
76
78
|
ref: RepoRef,
|
|
77
|
-
|
|
79
|
+
agentOpts: AgentOpts,
|
|
78
80
|
prDiff: Awaited<ReturnType<GithubClient['getPRDiff']>>,
|
|
79
|
-
|
|
81
|
+
json?: boolean,
|
|
80
82
|
): Promise<ReviewResult | null> {
|
|
83
|
+
const { config, rules, agents: activeAgents, auto } = agentOpts;
|
|
81
84
|
const { owner, repo, prNumber } = ref;
|
|
82
85
|
if (!json && process.stdout.isTTY) {
|
|
83
86
|
const cached = await client.findAivReview(owner, repo, prNumber);
|
|
@@ -98,7 +101,7 @@ async function resolveResult(
|
|
|
98
101
|
console.log(chalk.dim(t().reviewRunningAgents(activeAgents.join(', '))));
|
|
99
102
|
|
|
100
103
|
try {
|
|
101
|
-
return await new Orchestrator(config, rules).run(prDiff, context, activeAgents);
|
|
104
|
+
return await new Orchestrator(config, rules).run(prDiff, context, activeAgents, auto);
|
|
102
105
|
} catch (e: any) {
|
|
103
106
|
console.log(chalk.red(t().reviewFailed(e.message)));
|
|
104
107
|
return null;
|
package/src/cli/selector.ts
CHANGED
|
@@ -94,13 +94,65 @@ export async function selectMergeStrategy(message: string): Promise<MergeStrateg
|
|
|
94
94
|
return answers['strategy'] as MergeStrategy;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
export type PrecheckAction = 'create_pr' | 'save_checklist' | 'skip';
|
|
98
|
+
|
|
99
|
+
export async function selectPrecheckAction(message: string, needsPush: boolean, hasFindings: boolean): Promise<PrecheckAction> {
|
|
100
|
+
const inquirer = await getInquirer();
|
|
101
|
+
const prLabel = needsPush
|
|
102
|
+
? chalk.cyan('🚀 Push y crear PR en GitHub')
|
|
103
|
+
: chalk.cyan('🚀 Crear PR en GitHub');
|
|
104
|
+
|
|
105
|
+
const checklistChoice = hasFindings
|
|
106
|
+
? [{ name: chalk.yellow('📋 Guardar checklist en .aiv/'), value: 'save_checklist', short: 'Guardar Checklist' }]
|
|
107
|
+
: [];
|
|
108
|
+
|
|
109
|
+
const choices = [
|
|
110
|
+
{ name: prLabel, value: 'create_pr', short: 'Crear PR' },
|
|
111
|
+
...checklistChoice,
|
|
112
|
+
new inquirer.Separator('─'.repeat(42)),
|
|
113
|
+
{ name: chalk.dim('↩ Skip'), value: 'skip', short: 'Skip' },
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const answers = await inquirer.prompt([{
|
|
117
|
+
type: 'list',
|
|
118
|
+
name: 'action',
|
|
119
|
+
message,
|
|
120
|
+
choices,
|
|
121
|
+
pageSize: 4,
|
|
122
|
+
loop: false,
|
|
123
|
+
}]);
|
|
124
|
+
return answers['action'] as PrecheckAction;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function promptPRDetails(defaultTitle: string): Promise<{ title: string; body: string }> {
|
|
128
|
+
const inquirer = await getInquirer();
|
|
129
|
+
const answers = await inquirer.prompt([
|
|
130
|
+
{
|
|
131
|
+
type: 'input',
|
|
132
|
+
name: 'title',
|
|
133
|
+
message: 'PR title:',
|
|
134
|
+
default: defaultTitle,
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
type: 'input',
|
|
138
|
+
name: 'body',
|
|
139
|
+
message: 'PR description (optional, Enter to skip):',
|
|
140
|
+
default: '',
|
|
141
|
+
},
|
|
142
|
+
]);
|
|
143
|
+
return {
|
|
144
|
+
title: answers['title'] as string,
|
|
145
|
+
body: answers['body'] as string,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
97
149
|
export async function confirmReview(pr: PullRequest, label: string): Promise<boolean> {
|
|
98
150
|
const inquirer = await getInquirer();
|
|
99
151
|
|
|
100
152
|
const answers = await inquirer.prompt([{
|
|
101
153
|
type: 'confirm',
|
|
102
154
|
name: 'ok',
|
|
103
|
-
message: `${label} ${chalk.cyan(
|
|
155
|
+
message: `${label} ${chalk.cyan('#' + pr.number)} — ${pr.title}?`,
|
|
104
156
|
default: true,
|
|
105
157
|
}]);
|
|
106
158
|
|