@ateriss_/aiv-cli 1.0.1 → 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.
@@ -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
 
@@ -26,16 +26,16 @@ 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'];
31
+ const client = new GithubClient(token);
30
32
 
31
33
  console.log(chalk.bold(t().reviewTitle(prNumber)));
32
34
  console.log(chalk.dim(` ${t().reviewAccount(config.github.accountName, config.github.token_env)}\n`));
33
35
 
34
36
  const fetchSpinner = ora(t().reviewFetching(prNumber, `${owner}/${repo}`)).start();
35
37
  let prDiff: Awaited<ReturnType<GithubClient['getPRDiff']>>;
36
-
37
38
  try {
38
- const client = new GithubClient(token);
39
39
  prDiff = await client.getPRDiff(owner, repo, prNumber);
40
40
  fetchSpinner.succeed(t().reviewLoaded(chalk.cyan(prDiff.pr.title), prDiff.files.length));
41
41
  } catch (e: any) {
@@ -43,61 +43,112 @@ export async function runReview(opts: RunReviewOptions): Promise<void> {
43
43
  return;
44
44
  }
45
45
 
46
+ const result = await resolveResult(client, { owner, repo, prNumber }, { config, rules, agents: activeAgents, auto }, prDiff, opts.json);
47
+ if (!result) return;
48
+
49
+ if (opts.json) {
50
+ console.log(JSON.stringify(result, null, 2));
51
+ return;
52
+ }
53
+
54
+ renderReview(result);
55
+ if (!process.stdout.isTTY) return;
56
+
57
+ const action = await selectPostReviewAction(t().postReviewSelectAction);
58
+ if (action === 'skip') return;
59
+
60
+ if (action === 'post_comment') {
61
+ await doPostComment(client, owner, repo, prNumber, result);
62
+ return;
63
+ }
64
+
65
+ const submitted = await doSubmitReview(client, owner, repo, prNumber, action);
66
+ if (!submitted) return;
67
+
68
+ if (action === 'approve') {
69
+ await doApproveFlow(client, owner, repo, prNumber, result);
70
+ }
71
+ }
72
+
73
+ interface RepoRef { owner: string; repo: string; prNumber: number; }
74
+ interface AgentOpts { config: ResolvedConfig; rules: ReturnType<typeof loadRules>; agents: string[]; auto: boolean; }
75
+
76
+ async function resolveResult(
77
+ client: GithubClient,
78
+ ref: RepoRef,
79
+ agentOpts: AgentOpts,
80
+ prDiff: Awaited<ReturnType<GithubClient['getPRDiff']>>,
81
+ json?: boolean,
82
+ ): Promise<ReviewResult | null> {
83
+ const { config, rules, agents: activeAgents, auto } = agentOpts;
84
+ const { owner, repo, prNumber } = ref;
85
+ if (!json && process.stdout.isTTY) {
86
+ const cached = await client.findAivReview(owner, repo, prNumber);
87
+ if (cached) {
88
+ console.log(chalk.cyan(`\n ${t().reviewCachedFound}`));
89
+ const useCached = await confirmMerge(t().reviewCachedUse);
90
+ if (useCached) {
91
+ console.log(chalk.dim(` ${t().reviewCachedUsing}`));
92
+ return cached;
93
+ }
94
+ console.log(chalk.dim(` ${t().reviewCachedSkipping}`));
95
+ }
96
+ }
97
+
46
98
  const ctxSpinner = ora(t().reviewLoadingContext).start();
47
99
  const context = new ContextManager(process.cwd()).buildReviewContext(prDiff);
48
100
  ctxSpinner.succeed(t().reviewContextLoaded);
49
-
50
101
  console.log(chalk.dim(t().reviewRunningAgents(activeAgents.join(', '))));
51
102
 
52
103
  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);
104
+ return await new Orchestrator(config, rules).run(prDiff, context, activeAgents, auto);
59
105
  } catch (e: any) {
60
106
  console.log(chalk.red(t().reviewFailed(e.message)));
61
- return;
107
+ return null;
62
108
  }
109
+ }
63
110
 
64
- if (!process.stdout.isTTY) return;
65
-
66
- const action = await selectPostReviewAction(t().postReviewSelectAction);
67
- if (action === 'skip') return;
111
+ async function doPostComment(client: GithubClient, owner: string, repo: string, prNumber: number, result: ReviewResult): Promise<void> {
112
+ const spinner = ora(t().postReviewPostingComment).start();
113
+ try {
114
+ await client.postComment(owner, repo, prNumber, buildAivComment(result));
115
+ spinner.succeed(chalk.green(t().postReviewCommentPosted(prNumber)));
116
+ } catch (e: any) {
117
+ spinner.fail(chalk.red(t().postReviewCommentFailed(e.message)));
118
+ }
119
+ }
68
120
 
121
+ async function doSubmitReview(client: GithubClient, owner: string, repo: string, prNumber: number, action: 'approve' | 'request_changes'): Promise<boolean> {
69
122
  const event = action === 'approve' ? 'APPROVE' : 'REQUEST_CHANGES';
70
- const submitSpinner = ora(t().postReviewSubmitting).start();
71
-
123
+ const spinner = ora(t().postReviewSubmitting).start();
72
124
  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
- }
125
+ await client.submitReview(owner, repo, prNumber, event);
126
+ const msg = action === 'approve' ? t().postReviewApproved(prNumber) : t().postReviewChangesRequested(prNumber);
127
+ const color = action === 'approve' ? chalk.green : chalk.yellow;
128
+ spinner.succeed(color(msg));
129
+ return true;
79
130
  } catch (e: any) {
80
- submitSpinner.fail(chalk.red(t().postReviewFailed(e.message)));
81
- return;
131
+ spinner.fail(chalk.red(t().postReviewFailed(e.message)));
132
+ return false;
82
133
  }
134
+ }
83
135
 
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
- }
136
+ async function doApproveFlow(client: GithubClient, owner: string, repo: string, prNumber: number, result: ReviewResult): Promise<void> {
137
+ const wantsMerge = await confirmMerge(t().postReviewMergeConfirm);
138
+ if (wantsMerge) {
139
+ await doPostComment(client, owner, repo, prNumber, result);
140
+ const strategy = await selectMergeStrategy(t().postReviewSelectMerge);
141
+ const mergeSpinner = ora(t().postReviewMerging(prNumber)).start();
142
+ try {
143
+ await client.mergePR(owner, repo, prNumber, strategy);
144
+ mergeSpinner.succeed(chalk.green(t().postReviewMerged(prNumber)));
145
+ } catch (e: any) {
146
+ mergeSpinner.fail(chalk.red(t().postReviewMergeFailed(e.message)));
95
147
  }
96
-
97
- const refreshSpinner = ora(t().postReviewRefreshing).start();
98
- await refreshContextFiles(process.cwd());
99
- refreshSpinner.succeed(chalk.green(t().postReviewRefreshed));
100
148
  }
149
+ const refreshSpinner = ora(t().postReviewRefreshing).start();
150
+ await refreshContextFiles(process.cwd());
151
+ refreshSpinner.succeed(chalk.green(t().postReviewRefreshed));
101
152
  }
102
153
 
103
154
  // ─── 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
 
@@ -93,13 +94,65 @@ export async function selectMergeStrategy(message: string): Promise<MergeStrateg
93
94
  return answers['strategy'] as MergeStrategy;
94
95
  }
95
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
+
96
149
  export async function confirmReview(pr: PullRequest, label: string): Promise<boolean> {
97
150
  const inquirer = await getInquirer();
98
151
 
99
152
  const answers = await inquirer.prompt([{
100
153
  type: 'confirm',
101
154
  name: 'ok',
102
- message: `${label} ${chalk.cyan(`#${pr.number}`)} — ${pr.title}?`,
155
+ message: `${label} ${chalk.cyan('#' + pr.number)} — ${pr.title}?`,
103
156
  default: true,
104
157
  }]);
105
158
 
@@ -3,8 +3,23 @@ import * as path from 'node:path';
3
3
  import { LLMProvider } from '../providers/base';
4
4
  import { treePath } from '../config';
5
5
 
6
- const IGNORED = new Set(['node_modules', '.git', 'dist', 'build', '.aiv', '.next', 'coverage']);
7
- const SOURCE_EXT = /\.(ts|js|tsx|jsx|py|go|java|rb|php|cs|rs)$/;
6
+ const IGNORED = new Set(['node_modules', '.git', 'dist', 'build', '.aiv', '.next', 'coverage', '.cache', 'out', '__pycache__']);
7
+ const SOURCE_EXT = /\.(ts|js|tsx|jsx|py|go|java|rb|php|cs|rs|kt|swift)$/;
8
+ const CONFIG_EXT = /\.(json|yaml|yml|toml|env\.example|env\.sample)$/;
9
+
10
+ const EDITORIAL_HEADER = `<!-- =========================================================
11
+ aiv context.md — auto-generated by \`aiv context generate\`
12
+
13
+ READ THIS FILE and edit it before your first review.
14
+ The more accurate this file is, the better the AI findings will be.
15
+
16
+ Key sections to review:
17
+ - Business Rules: these directly drive agent findings — add real invariants
18
+ - Sensitive Zones: add any path that handles money, auth, or PII
19
+ - Architecture: correct any wrong assumptions about your layer structure
20
+ ========================================================= -->
21
+
22
+ `;
8
23
 
9
24
  export async function generateContextAndRules(
10
25
  cwd: string,
@@ -14,61 +29,98 @@ export async function generateContextAndRules(
14
29
 
15
30
  const treeFile = treePath(cwd);
16
31
  const treeData = fs.existsSync(treeFile)
17
- ? fs.readFileSync(treeFile, 'utf8').slice(0, 6000)
32
+ ? fs.readFileSync(treeFile, 'utf8').slice(0, 10000)
18
33
  : '(tree not available — run aiv context refresh first)';
19
34
 
20
- const pkgInfo = readPackageJson(cwd);
21
- const samples = readSampleFiles(cwd);
35
+ const pkgInfo = readPackageJson(cwd);
36
+ const readme = readReadme(cwd);
37
+ const configs = readConfigFiles(cwd);
38
+ const sources = readSourceFiles(cwd);
22
39
 
23
- const userMessage = `
24
- Project name: ${name}
40
+ const userMessage = `Project name: ${name}
25
41
 
26
- package.json summary:
42
+ ## package.json
27
43
  ${pkgInfo}
28
44
 
29
- Project file tree (tree.json):
45
+ ## README (if present)
46
+ ${readme}
47
+
48
+ ## Config files (tsconfig, env.example, etc.)
49
+ ${configs}
50
+
51
+ ## Project file tree
30
52
  ${treeData}
31
53
 
32
- Sample source files:
33
- ${samples}
34
-
35
- Generate context.md and rules.yml suitable for AI PR reviewers for this project.
36
- Return ONLY valid JSON with exactly these two fields:
37
- {
38
- "context": "<full context.md content as a string>",
39
- "rules": "<full rules.yml content as a string>"
40
- }`;
41
-
42
- const systemPrompt = `You are an expert software architect.
43
- Given a project's structure, dependencies, and sample code, produce:
44
-
45
- 1. context.md — Markdown file with these sections:
46
- ## Architecture — overall pattern (monorepo, monolith, microservices, layers, etc.)
47
- ## Modules — key modules or directories and their responsibilities
48
- ## Technologiesmain frameworks and libraries in use
49
- ## Critical Dependencies — security-sensitive or infrastructure packages
50
- ## Sensitive Zones — paths that require extra scrutiny (auth, payments, migrations, etc.)
51
- ## Business Rules inferred invariants the codebase enforces (edit this section after generation)
52
- ## System Summaryone paragraph description
53
-
54
- 2. rules.yml — YAML file with:
55
- sensitive_modules: [list of folder/module names that are sensitive]
56
- business_rules:
57
- <module>:
58
- required_calls: [functions that must always be called in this module]
59
- required_checks: [validations that must be present]
60
- forbidden_patterns: [patterns that must never appear]
61
-
62
- Be specific. Infer real module names from the file tree. Do not invent generic placeholders.
63
- Return ONLY the JSON object — no explanation, no markdown fences.`;
54
+ ## Source files sample
55
+ ${sources}
56
+
57
+ Analyze this project thoroughly and generate context.md and rules.yml for AI PR reviewers.
58
+ Return ONLY valid JSON: { "context": "<context.md content>", "rules": "<rules.yml content>" }`;
59
+
60
+ const systemPrompt = `You are an expert software architect analyzing a codebase to generate AI reviewer context.
61
+
62
+ Your output must help AI agents understand:
63
+ 1. How the project is structured and what each layer/module does
64
+ 2. What business domain it operates in and what invariants must hold
65
+ 3. Which parts are security-sensitive or require extra care
66
+ 4. What architectural patterns the project follows (so deviations can be flagged)
67
+
68
+ Generate two files:
69
+
70
+ ### context.mdinclude ALL of these sections:
71
+
72
+ ## Business Context
73
+ Describe the business domain (e-commerce, fintech, healthcare, SaaS, etc.) and what the system does.
74
+ State the most critical business invariants rules that must NEVER be violated (e.g., "balances must never go negative", "all payments require an audit log").
75
+
76
+ ## Architecture
77
+ Describe the overall pattern: monolith/microservices/monorepo, layering (handlers → services → repositories), module structure, main data flows.
78
+ Name the layers explicitly so agents can flag violations.
79
+
80
+ ## Modules
81
+ List each significant module/directory with a one-line description of its responsibility.
82
+
83
+ ## Technologies
84
+ Main frameworks, ORMs, auth libraries, messaging systems, etc.
85
+
86
+ ## Critical Dependencies
87
+ Security-sensitive packages (auth, crypto, payments, HTTP clients).
88
+
89
+ ## Sensitive Zones
90
+ Exact file paths or directories that handle: authentication, payments/billing, PII, database migrations, permissions.
91
+ Format as a list: "- src/auth/ — JWT issuance and session management"
92
+
93
+ ## Business Rules
94
+ IMPORTANT: infer real domain rules from the code. Examples:
95
+ - "All mutations in src/payments/ must call auditLog()"
96
+ - "User balance must only be modified via BalanceService, never directly"
97
+ - "Orders cannot transition from COMPLETED to any other state"
98
+ Be specific. Vague rules produce vague findings.
99
+
100
+ ## System Summary
101
+ Two-sentence description of what the system does and who uses it.
102
+
103
+ ### rules.yml — infer real module names and rules:
104
+
105
+ sensitive_modules: [list actual folder names that are sensitive]
106
+ business_rules:
107
+ <real_module_name>:
108
+ required_calls: [actual function names that must be called]
109
+ required_checks: [actual validation functions/guards]
110
+ forbidden_patterns: [actual antipatterns found or expected]
111
+
112
+ Use only real names from the file tree. No generic placeholders like "your-module".
113
+ Return ONLY the JSON object — no markdown fences, no explanation outside the JSON.`;
64
114
 
65
115
  const response = await provider.complete(
66
116
  [{ role: 'user', content: userMessage }],
67
117
  systemPrompt,
68
- 4000,
118
+ 6000,
69
119
  );
70
120
 
71
- return parseResponse(response.content);
121
+ const result = parseResponse(response.content);
122
+ result.context = EDITORIAL_HEADER + result.context;
123
+ return result;
72
124
  }
73
125
 
74
126
  function parseResponse(raw: string): { context: string; rules: string } {
@@ -92,47 +144,91 @@ function readPackageJson(cwd: string): string {
92
144
  if (!fs.existsSync(file)) return '(not found)';
93
145
  try {
94
146
  const pkg = JSON.parse(fs.readFileSync(file, 'utf8'));
95
- const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).slice(0, 30);
96
- return `name: ${pkg.name ?? '?'}\nscripts: ${Object.keys(pkg.scripts ?? {}).join(', ')}\ndependencies: ${deps.join(', ')}`;
147
+ const deps = Object.keys({ ...pkg.dependencies, ...pkg.devDependencies });
148
+ return `name: ${pkg.name ?? '?'}\nversion: ${pkg.version ?? '?'}\nscripts: ${Object.keys(pkg.scripts ?? {}).join(', ')}\ndependencies (${deps.length}): ${deps.join(', ')}`;
97
149
  } catch {
98
150
  return '(unreadable)';
99
151
  }
100
152
  }
101
153
 
102
- function readSampleFiles(cwd: string): string {
103
- const srcRoots = ['src', 'app', 'lib', 'packages', 'modules']
154
+ function readReadme(cwd: string): string {
155
+ for (const name of ['README.md', 'readme.md', 'README.txt', 'README']) {
156
+ const file = path.join(cwd, name);
157
+ if (fs.existsSync(file)) {
158
+ try { return fs.readFileSync(file, 'utf8').slice(0, 3000); } catch {}
159
+ }
160
+ }
161
+ return '(not found)';
162
+ }
163
+
164
+ function readConfigFiles(cwd: string): string {
165
+ const candidates = [
166
+ 'tsconfig.json', 'jsconfig.json',
167
+ '.env.example', '.env.sample', '.env.template',
168
+ 'docker-compose.yml', 'docker-compose.yaml',
169
+ 'angular.json', 'nest-cli.json', 'vite.config.ts', 'vite.config.js',
170
+ 'next.config.js', 'next.config.ts',
171
+ ];
172
+
173
+ const parts: string[] = [];
174
+ for (const name of candidates) {
175
+ const file = path.join(cwd, name);
176
+ if (!fs.existsSync(file)) continue;
177
+ try {
178
+ const content = fs.readFileSync(file, 'utf8').slice(0, 800);
179
+ parts.push(`--- ${name} ---\n${content}`);
180
+ } catch {}
181
+ if (parts.length >= 4) break;
182
+ }
183
+ return parts.join('\n\n') || '(none found)';
184
+ }
185
+
186
+ function readSourceFiles(cwd: string): string {
187
+ const srcRoots = ['src', 'app', 'lib', 'packages', 'modules', 'server', 'api']
104
188
  .map(d => path.join(cwd, d))
105
189
  .filter(d => { try { return fs.statSync(d).isDirectory(); } catch { return false; } });
106
190
 
107
191
  const files: string[] = [];
108
- for (const root of srcRoots.slice(0, 2)) {
109
- collectSourceFiles(root, files);
110
- if (files.length >= 12) break;
192
+
193
+ // Prioritize key files: services, controllers, models, auth, guards, interceptors
194
+ const priority = /\.(service|controller|model|entity|repository|guard|middleware|interceptor|router|handler|resolver)\.(ts|js)$/i;
195
+
196
+ for (const root of srcRoots) {
197
+ collectSourceFiles(root, files, priority);
198
+ }
199
+ for (const root of srcRoots) {
200
+ collectSourceFiles(root, files, null);
201
+ if (files.length >= 40) break;
111
202
  }
112
203
 
113
- return files.slice(0, 12).map(f => {
204
+ const unique = [...new Set(files)].slice(0, 40);
205
+ return unique.map(f => {
114
206
  const rel = path.relative(cwd, f);
115
- const content = fs.readFileSync(f, 'utf8').slice(0, 400);
207
+ const content = fs.readFileSync(f, 'utf8').slice(0, 600);
116
208
  return `--- ${rel} ---\n${content}`;
117
- }).join('\n\n').slice(0, 7000);
209
+ }).join('\n\n').slice(0, 20000);
118
210
  }
119
211
 
120
- function collectSourceFiles(dir: string, out: string[], depth = 0): void {
121
- if (depth > 3 || out.length >= 12) return;
212
+ function collectSourceFiles(dir: string, out: string[], filter: RegExp | null, depth = 0): void {
213
+ if (depth > 5 || out.length >= 40) return;
122
214
  let entries: string[];
123
215
  try { entries = fs.readdirSync(dir); } catch { return; }
124
216
 
125
217
  for (const entry of entries) {
126
218
  if (IGNORED.has(entry)) continue;
127
- const full = path.join(dir, entry);
128
- try {
129
- const stat = fs.statSync(full);
130
- if (stat.isFile() && SOURCE_EXT.test(entry)) {
131
- out.push(full);
132
- } else if (stat.isDirectory()) {
133
- collectSourceFiles(full, out, depth + 1);
134
- }
135
- } catch {}
136
- if (out.length >= 12) break;
219
+ processEntry(path.join(dir, entry), entry, out, filter, depth);
220
+ if (out.length >= 40) break;
137
221
  }
138
222
  }
223
+
224
+ function processEntry(full: string, name: string, out: string[], filter: RegExp | null, depth: number): void {
225
+ try {
226
+ const stat = fs.statSync(full);
227
+ if (stat.isFile()) {
228
+ const matches = filter ? filter.test(name) : SOURCE_EXT.test(name);
229
+ if (matches && !out.includes(full)) out.push(full);
230
+ } else if (stat.isDirectory()) {
231
+ collectSourceFiles(full, out, filter, depth + 1);
232
+ }
233
+ } catch {}
234
+ }