scai 0.1.55 → 0.1.57
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 +14 -13
- package/dist/commands/AskCmd.js +3 -3
- package/dist/commands/CommitSuggesterCmd.js +2 -0
- package/dist/commands/IndexCmd.js +12 -27
- package/dist/commands/InspectCmd.js +2 -3
- package/dist/commands/ResetDbCmd.js +16 -11
- package/dist/commands/ReviewCmd.js +170 -30
- package/dist/commands/SummaryCmd.js +4 -3
- package/dist/commands/SwitchCmd.js +73 -0
- package/dist/config.js +157 -37
- package/dist/constants.js +12 -15
- package/dist/daemon/daemonBatch.js +3 -3
- package/dist/daemon/daemonWorker.js +2 -1
- package/dist/db/client.js +30 -8
- package/dist/db/fileIndex.js +5 -1
- package/dist/db/functionExtractors/extractFromJava.js +2 -1
- package/dist/db/functionExtractors/extractFromJs.js +2 -1
- package/dist/db/functionExtractors/extractFromXML.js +2 -1
- package/dist/db/functionExtractors/index.js +2 -1
- package/dist/db/schema.js +2 -1
- package/dist/github/postComments.js +69 -0
- package/dist/index.js +50 -23
- package/dist/pipeline/modules/reviewModule.js +2 -1
- package/dist/scripts/migrateDb.js +2 -1
- package/dist/utils/fileTree.js +4 -2
- package/dist/utils/normalizePath.js +9 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ scai runs entirely on your machine and doesn't require cloud APIs or API keys. T
|
|
|
57
57
|
No more struggling to write pull request descriptions by hand. `scai git review` automatically generates a rich summary of your changes, complete with context, suggestions, and rationale.
|
|
58
58
|
|
|
59
59
|
> ⚠️ These features are in **beta** — feedback welcome!
|
|
60
|
+
Ping [@ticcr](https://bsky.app/profile/ticcr.xyz) on Bluesky — I'd love to hear your thoughts!
|
|
60
61
|
|
|
61
62
|
---
|
|
62
63
|
|
|
@@ -87,7 +88,7 @@ To interact with GitHub and create pull requests, `scai` needs a personal access
|
|
|
87
88
|
3. **Set the index dir:**
|
|
88
89
|
|
|
89
90
|
```bash
|
|
90
|
-
scai
|
|
91
|
+
scai index set /path/to/repo
|
|
91
92
|
```
|
|
92
93
|
|
|
93
94
|
This is the repo from which scai will look up pull requests that can be reviewed.
|
|
@@ -116,10 +117,16 @@ SCAI supports an integrated review flow for GitHub pull requests. To get started
|
|
|
116
117
|
1. **Set your working index directory (once per repo):**
|
|
117
118
|
|
|
118
119
|
```sh
|
|
119
|
-
scai set
|
|
120
|
+
scai index set /path/to/repo
|
|
120
121
|
```
|
|
121
122
|
|
|
122
123
|
2. **Authenticate with GitHub:**
|
|
124
|
+
```sh
|
|
125
|
+
scai git review
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This command will query you for the Personal Access Token and set it for you.
|
|
129
|
+
You may also do this with the auth commands below
|
|
123
130
|
|
|
124
131
|
```sh
|
|
125
132
|
scai auth set
|
|
@@ -170,25 +177,19 @@ You might consider renaming `sessionManager` to better reflect its dual role in
|
|
|
170
177
|
|
|
171
178
|
|
|
172
179
|
|
|
173
|
-
### 🔧 How to Use `scai git
|
|
180
|
+
### 🔧 How to Use `scai git commit`
|
|
174
181
|
|
|
175
182
|
Use AI to suggest a meaningful commit message based on your staged code:
|
|
176
183
|
|
|
177
184
|
```bash
|
|
178
185
|
git add .
|
|
179
|
-
scai git
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
To automatically commit with the selected suggestion:
|
|
183
|
-
|
|
184
|
-
```bash
|
|
185
|
-
scai git sugg --commit
|
|
186
|
+
scai git commit
|
|
186
187
|
```
|
|
187
188
|
|
|
188
189
|
You can also include a changelog entry along with the commit:
|
|
189
190
|
|
|
190
191
|
```bash
|
|
191
|
-
scai git
|
|
192
|
+
scai git commit --changelog
|
|
192
193
|
```
|
|
193
194
|
|
|
194
195
|
This will:
|
|
@@ -287,13 +288,13 @@ You won't gain much value from the index unless you scope it to one repository.
|
|
|
287
288
|
1. **Set index directory:**
|
|
288
289
|
|
|
289
290
|
```bash
|
|
290
|
-
scai set
|
|
291
|
+
scai index set /path/to/repo
|
|
291
292
|
```
|
|
292
293
|
|
|
293
294
|
2. **Index your repo (once):**
|
|
294
295
|
|
|
295
296
|
```bash
|
|
296
|
-
scai index
|
|
297
|
+
scai index start
|
|
297
298
|
```
|
|
298
299
|
|
|
299
300
|
3. The daemon is designed to **consume minimal resources** and run unobtrusively. You can control it with:
|
package/dist/commands/AskCmd.js
CHANGED
|
@@ -7,7 +7,7 @@ import { generate } from '../lib/generate.js';
|
|
|
7
7
|
import { buildContextualPrompt } from '../utils/buildContextualPrompt.js';
|
|
8
8
|
import { generateFocusedFileTree } from '../utils/fileTree.js';
|
|
9
9
|
import { log } from '../utils/log.js';
|
|
10
|
-
import { PROMPT_LOG_PATH, SCAI_HOME,
|
|
10
|
+
import { PROMPT_LOG_PATH, SCAI_HOME, RELATED_FILES_LIMIT, MAX_SUMMARY_LINES, getIndexDir } from '../constants.js';
|
|
11
11
|
export async function runAskCommand(query) {
|
|
12
12
|
if (!query) {
|
|
13
13
|
query = await promptOnce('💬 Ask your question:\n');
|
|
@@ -17,7 +17,7 @@ export async function runAskCommand(query) {
|
|
|
17
17
|
console.error('❌ No question provided.\n👉 Usage: scai ask "your question"');
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
console.log(`📁 Using index root: ${
|
|
20
|
+
console.log(`📁 Using index root: ${getIndexDir}`);
|
|
21
21
|
console.log(`🔍 Searching for: "${query}"\n`);
|
|
22
22
|
// 🟩 STEP 1: Semantic Search
|
|
23
23
|
const start = Date.now();
|
|
@@ -103,7 +103,7 @@ export async function runAskCommand(query) {
|
|
|
103
103
|
// 🟩 STEP 6: Generate file tree
|
|
104
104
|
let fileTree = '';
|
|
105
105
|
try {
|
|
106
|
-
fileTree = generateFocusedFileTree(
|
|
106
|
+
fileTree = generateFocusedFileTree(filepath, 2);
|
|
107
107
|
}
|
|
108
108
|
catch (e) {
|
|
109
109
|
console.warn('⚠️ Could not generate file tree:', e);
|
|
@@ -63,6 +63,7 @@ export async function suggestCommitMessage(options) {
|
|
|
63
63
|
console.log('⚠️ No staged changes to suggest a message for.');
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
|
+
// Handle changelog generation if the flag is provided
|
|
66
67
|
if (options.changelog) {
|
|
67
68
|
let entryFinalized = false;
|
|
68
69
|
while (!entryFinalized) {
|
|
@@ -122,6 +123,7 @@ export async function suggestCommitMessage(options) {
|
|
|
122
123
|
console.log("👉 Please stage your changes with 'git add <files>' and rerun the command.");
|
|
123
124
|
return;
|
|
124
125
|
}
|
|
126
|
+
// Automatically commit the suggested message
|
|
125
127
|
execSync(`git commit -m "${message.replace(/"/g, '\\"')}"`, { stdio: 'inherit' });
|
|
126
128
|
console.log('✅ Committed with selected message.');
|
|
127
129
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// indexCmd.ts
|
|
1
2
|
import fg from 'fast-glob';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { initSchema } from '../db/schema.js';
|
|
@@ -6,14 +7,13 @@ import { detectFileType } from '../fileRules/detectFileType.js';
|
|
|
6
7
|
import { startDaemon } from './DaemonCmd.js';
|
|
7
8
|
import { IGNORED_FOLDER_GLOBS } from '../fileRules/ignoredPaths.js';
|
|
8
9
|
import { Config } from '../config.js';
|
|
9
|
-
import { DB_PATH } from '../constants.js';
|
|
10
10
|
import { log } from '../utils/log.js';
|
|
11
11
|
import lockfile from 'proper-lockfile';
|
|
12
12
|
import { classifyFile } from '../fileRules/classifyFile.js';
|
|
13
|
-
|
|
13
|
+
import { getDbPathForRepo } from '../db/client.js';
|
|
14
14
|
async function lockDb() {
|
|
15
15
|
try {
|
|
16
|
-
const lock = await lockfile.lock(
|
|
16
|
+
const lock = await lockfile.lock(getDbPathForRepo());
|
|
17
17
|
return lock;
|
|
18
18
|
}
|
|
19
19
|
catch (err) {
|
|
@@ -21,7 +21,7 @@ async function lockDb() {
|
|
|
21
21
|
throw err;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
-
export async function runIndexCommand(
|
|
24
|
+
export async function runIndexCommand() {
|
|
25
25
|
try {
|
|
26
26
|
initSchema();
|
|
27
27
|
}
|
|
@@ -29,31 +29,17 @@ export async function runIndexCommand(targetDir, options = {}) {
|
|
|
29
29
|
console.error('❌ Failed to initialize schema:', err);
|
|
30
30
|
process.exit(1);
|
|
31
31
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
resolvedDir = path.resolve(targetDir || process.cwd());
|
|
36
|
-
console.warn('⚠️ Running in --force mode. Config will not be updated.');
|
|
37
|
-
}
|
|
38
|
-
else if (targetDir) {
|
|
39
|
-
// User provided a directory: resolve and persist to config
|
|
40
|
-
resolvedDir = path.resolve(targetDir);
|
|
41
|
-
Config.setIndexDir(resolvedDir);
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
// Use configured indexDir or fallback to cwd
|
|
45
|
-
resolvedDir = Config.getIndexDir() || process.cwd();
|
|
46
|
-
Config.setIndexDir(resolvedDir); // persist if not yet saved
|
|
47
|
-
}
|
|
48
|
-
log(`📂 Indexing files in: ${resolvedDir}`);
|
|
32
|
+
const indexDir = Config.getIndexDir() || process.cwd();
|
|
33
|
+
Config.setIndexDir(indexDir); // persist if not already saved
|
|
34
|
+
log(`📂 Indexing files in: ${indexDir}`);
|
|
49
35
|
const files = await fg('**/*.*', {
|
|
50
|
-
cwd:
|
|
36
|
+
cwd: indexDir,
|
|
51
37
|
ignore: IGNORED_FOLDER_GLOBS,
|
|
52
38
|
absolute: true,
|
|
53
39
|
});
|
|
54
40
|
const countByExt = {};
|
|
55
41
|
let count = 0;
|
|
56
|
-
const release = await lockDb();
|
|
42
|
+
const release = await lockDb();
|
|
57
43
|
for (const file of files) {
|
|
58
44
|
const classification = classifyFile(file);
|
|
59
45
|
if (classification !== 'valid') {
|
|
@@ -62,10 +48,10 @@ export async function runIndexCommand(targetDir, options = {}) {
|
|
|
62
48
|
}
|
|
63
49
|
try {
|
|
64
50
|
const type = detectFileType(file);
|
|
65
|
-
indexFile(file, null, type);
|
|
51
|
+
indexFile(file, null, type);
|
|
66
52
|
const ext = path.extname(file);
|
|
67
53
|
countByExt[ext] = (countByExt[ext] || 0) + 1;
|
|
68
|
-
log(`📄 Indexed: ${path.relative(
|
|
54
|
+
log(`📄 Indexed: ${path.relative(indexDir, file)}`);
|
|
69
55
|
count++;
|
|
70
56
|
}
|
|
71
57
|
catch (err) {
|
|
@@ -74,7 +60,6 @@ export async function runIndexCommand(targetDir, options = {}) {
|
|
|
74
60
|
}
|
|
75
61
|
log('📊 Indexed files by extension:', JSON.stringify(countByExt, null, 2));
|
|
76
62
|
log(`✅ Done. Indexed ${count} files.`);
|
|
77
|
-
await release();
|
|
78
|
-
// Auto-start daemon if not already running
|
|
63
|
+
await release();
|
|
79
64
|
startDaemon();
|
|
80
65
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { db } from '../db/client.js';
|
|
2
1
|
import path from 'path';
|
|
3
2
|
import fs from 'fs';
|
|
4
3
|
import { log } from '../utils/log.js';
|
|
4
|
+
import { getDbForRepo } from '../db/client.js';
|
|
5
5
|
export async function runInspectCommand(fileArg) {
|
|
6
6
|
if (!fileArg) {
|
|
7
7
|
log('❌ Please provide a file path to inspect.');
|
|
@@ -12,6 +12,7 @@ export async function runInspectCommand(fileArg) {
|
|
|
12
12
|
log(`❌ File does not exist: ${resolvedPath}`);
|
|
13
13
|
process.exit(1);
|
|
14
14
|
}
|
|
15
|
+
const db = getDbForRepo();
|
|
15
16
|
const file = db
|
|
16
17
|
.prepare(`SELECT * FROM files WHERE REPLACE(path, '\\', '/') = ?`)
|
|
17
18
|
.get(resolvedPath);
|
|
@@ -29,12 +30,10 @@ export async function runInspectCommand(fileArg) {
|
|
|
29
30
|
console.log(`📌 Functions extracted: ${isExtracted ? '✅' : '❌'}`);
|
|
30
31
|
console.log(`📆 Extracted at: ${file.functions_extracted_at || '❌ Not yet'}`);
|
|
31
32
|
console.log(`⚙️ Processing status: ${file.processing_status || 'unknown'}`);
|
|
32
|
-
// 📝 Show summary preview
|
|
33
33
|
if (file.summary) {
|
|
34
34
|
console.log('\n📝 Summary:');
|
|
35
35
|
console.log(file.summary.slice(0, 300) + (file.summary.length > 300 ? '...' : ''));
|
|
36
36
|
}
|
|
37
|
-
// 🧑💻 Show extracted functions
|
|
38
37
|
const functions = db
|
|
39
38
|
.prepare(`SELECT name, start_line, end_line FROM functions WHERE file_id = ? ORDER BY start_line ASC`)
|
|
40
39
|
.all(file.id);
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import lockfile from 'proper-lockfile';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { backupScaiFolder } from '../db/backup.js'; // <-- New import
|
|
4
|
+
import { backupScaiFolder } from '../db/backup.js';
|
|
5
|
+
import { getDbPathForRepo, getDbForRepo } from '../db/client.js';
|
|
7
6
|
export async function resetDatabase() {
|
|
8
7
|
console.log('🔁 Backing up existing .scai folder...');
|
|
9
8
|
await backupScaiFolder();
|
|
9
|
+
const dbPath = getDbPathForRepo();
|
|
10
|
+
// Close the DB connection
|
|
10
11
|
try {
|
|
12
|
+
const db = getDbForRepo();
|
|
11
13
|
db.close();
|
|
12
14
|
console.log('🔒 Closed SQLite database connection.');
|
|
13
15
|
}
|
|
14
16
|
catch (err) {
|
|
15
17
|
console.warn('⚠️ Could not close database:', err instanceof Error ? err.message : err);
|
|
16
18
|
}
|
|
19
|
+
// Release lockfile if present
|
|
17
20
|
try {
|
|
18
|
-
const releaseLock = await lockfile.unlock(
|
|
21
|
+
const releaseLock = await lockfile.unlock(dbPath).catch(() => null);
|
|
19
22
|
if (releaseLock) {
|
|
20
23
|
console.log('🔓 Released database lock.');
|
|
21
24
|
}
|
|
@@ -23,27 +26,29 @@ export async function resetDatabase() {
|
|
|
23
26
|
catch (err) {
|
|
24
27
|
console.warn('⚠️ Failed to release database lock:', err instanceof Error ? err.message : err);
|
|
25
28
|
}
|
|
26
|
-
|
|
29
|
+
// Delete DB file
|
|
30
|
+
if (fs.existsSync(dbPath)) {
|
|
27
31
|
try {
|
|
28
|
-
fs.unlinkSync(
|
|
29
|
-
console.log(`🧹 Deleted existing database at ${
|
|
32
|
+
fs.unlinkSync(dbPath);
|
|
33
|
+
console.log(`🧹 Deleted existing database at ${dbPath}`);
|
|
30
34
|
}
|
|
31
35
|
catch (err) {
|
|
32
36
|
console.error('❌ Failed to delete DB file:', err instanceof Error ? err.message : err);
|
|
33
|
-
return;
|
|
34
37
|
}
|
|
35
38
|
}
|
|
36
39
|
else {
|
|
37
|
-
console.log('ℹ️ No existing database found at:',
|
|
40
|
+
console.log('ℹ️ No existing database found at:', dbPath);
|
|
38
41
|
}
|
|
42
|
+
// Ensure directory exists
|
|
39
43
|
try {
|
|
40
|
-
fs.mkdirSync(path.dirname(
|
|
44
|
+
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
41
45
|
console.log('📁 Ensured that the database directory exists.');
|
|
42
46
|
}
|
|
43
47
|
catch (err) {
|
|
44
48
|
console.warn('⚠️ Could not ensure DB directory exists:', err instanceof Error ? err.message : err);
|
|
45
49
|
}
|
|
46
|
-
|
|
50
|
+
// Clean up lock directory
|
|
51
|
+
const lockDir = `${dbPath}.lock`;
|
|
47
52
|
if (fs.existsSync(lockDir)) {
|
|
48
53
|
try {
|
|
49
54
|
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import readline from 'readline';
|
|
2
|
-
import { reviewModule } from '../pipeline/modules/reviewModule.js';
|
|
3
2
|
import { fetchOpenPullRequests, fetchPullRequestDiff, getGitHubUsername, submitReview } from '../github/github.js';
|
|
4
3
|
import { getRepoDetails } from '../github/repo.js';
|
|
5
4
|
import { ensureGitHubAuth } from '../github/auth.js';
|
|
5
|
+
import { postReviewComment } from '../github/postComments.js';
|
|
6
|
+
import { reviewModule } from '../pipeline/modules/reviewModule.js';
|
|
7
|
+
import chalk from 'chalk';
|
|
6
8
|
// Function to fetch the PRs with requested reviews for a specific branch (default to 'main')
|
|
7
9
|
export async function getPullRequestsForReview(token, owner, repo, username, branch = 'main', filterForUser = true) {
|
|
8
10
|
const prs = await fetchOpenPullRequests(token, owner, repo);
|
|
@@ -47,7 +49,7 @@ function askUserToPickPR(prs) {
|
|
|
47
49
|
console.log("⚠️ No pull requests with review requested.");
|
|
48
50
|
return resolve(null);
|
|
49
51
|
}
|
|
50
|
-
console.log("\n📦 Open Pull Requests with review requested:");
|
|
52
|
+
console.log(chalk.blue("\n📦 Open Pull Requests with review requested:"));
|
|
51
53
|
prs.forEach((pr, i) => {
|
|
52
54
|
console.log(`${i + 1}) #${pr.number} - ${pr.title}`);
|
|
53
55
|
});
|
|
@@ -55,14 +57,43 @@ function askUserToPickPR(prs) {
|
|
|
55
57
|
input: process.stdin,
|
|
56
58
|
output: process.stdout,
|
|
57
59
|
});
|
|
58
|
-
|
|
60
|
+
const askQuestion = () => {
|
|
61
|
+
rl.question(`\n👉 Choose a PR to review [1-${prs.length}]: `, (answer) => {
|
|
62
|
+
const index = parseInt(answer, 10);
|
|
63
|
+
if (!isNaN(index) && index >= 1 && index <= prs.length) {
|
|
64
|
+
resolve(index - 1); // Return array index, not PR number
|
|
65
|
+
rl.close();
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
console.log('⚠️ Invalid selection. Please enter a number between 1 and ' + prs.length);
|
|
69
|
+
askQuestion(); // Retry asking for input
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
askQuestion(); // Initial call to ask the user
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// Ask user to choose review method: whole PR or chunk-by-chunk
|
|
77
|
+
function askReviewMethod() {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
const rl = readline.createInterface({
|
|
80
|
+
input: process.stdin,
|
|
81
|
+
output: process.stdout,
|
|
82
|
+
});
|
|
83
|
+
console.log("\n🔍 Choose review method:");
|
|
84
|
+
console.log('1) Review whole PR at once');
|
|
85
|
+
console.log('2) Review chunk by chunk');
|
|
86
|
+
rl.question(`👉 Choose an option [1-2]: `, (answer) => {
|
|
59
87
|
rl.close();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
88
|
+
if (answer === '1') {
|
|
89
|
+
resolve('whole');
|
|
90
|
+
}
|
|
91
|
+
else if (answer === '2') {
|
|
92
|
+
resolve('chunk');
|
|
63
93
|
}
|
|
64
94
|
else {
|
|
65
|
-
|
|
95
|
+
console.log('⚠️ Invalid selection. Defaulting to "whole".');
|
|
96
|
+
resolve('whole');
|
|
66
97
|
}
|
|
67
98
|
});
|
|
68
99
|
});
|
|
@@ -119,12 +150,52 @@ function promptCustomReview() {
|
|
|
119
150
|
});
|
|
120
151
|
});
|
|
121
152
|
}
|
|
153
|
+
function chunkDiff(diff) {
|
|
154
|
+
const rawChunks = diff.split(/^diff --git /m).filter(Boolean);
|
|
155
|
+
return rawChunks.map(chunk => {
|
|
156
|
+
const fullChunk = 'diff --git ' + chunk;
|
|
157
|
+
// Try to extract the file name from the diff header
|
|
158
|
+
const match = fullChunk.match(/^diff --git a\/(.+?) b\/.+?\n/);
|
|
159
|
+
const filePath = match ? match[1] : 'unknown';
|
|
160
|
+
return { filePath, content: fullChunk };
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// Function to color diff lines
|
|
164
|
+
function colorDiffLine(line) {
|
|
165
|
+
if (line.startsWith('+')) {
|
|
166
|
+
return chalk.green(line); // New lines (added)
|
|
167
|
+
}
|
|
168
|
+
else if (line.startsWith('-')) {
|
|
169
|
+
return chalk.red(line); // Deleted lines
|
|
170
|
+
}
|
|
171
|
+
else if (line.startsWith('@@')) {
|
|
172
|
+
return chalk.yellow(line); // Modified lines (with context)
|
|
173
|
+
}
|
|
174
|
+
return line; // Default case (unchanged lines)
|
|
175
|
+
}
|
|
176
|
+
// Updated reviewChunk function to apply coloring
|
|
177
|
+
export async function reviewChunk(chunk, chunkIndex, totalChunks) {
|
|
178
|
+
console.log(chalk.yellow(`\n🔍 Reviewing chunk ${chunkIndex + 1} of ${totalChunks}:`));
|
|
179
|
+
console.log(`File: ${chunk.filePath}`);
|
|
180
|
+
// Split the chunk content into lines and color each line accordingly
|
|
181
|
+
const coloredDiff = chunk.content.split('\n').map(colorDiffLine).join('\n');
|
|
182
|
+
// Log the colored diff with line numbers and progress
|
|
183
|
+
console.log(`Starting line number: 30`); // Adjust based on actual starting line logic
|
|
184
|
+
console.log(coloredDiff);
|
|
185
|
+
// Get the review suggestion from the model (as before)
|
|
186
|
+
const suggestion = await reviewModule.run({ content: chunk.content, filepath: chunk.filePath });
|
|
187
|
+
console.log("\n💡 AI-suggested review:\n");
|
|
188
|
+
console.log(suggestion.content);
|
|
189
|
+
// Ask user to approve or reject the chunk
|
|
190
|
+
const reviewChoice = await askReviewApproval('Do you approve this code chunk?');
|
|
191
|
+
return reviewChoice;
|
|
192
|
+
}
|
|
122
193
|
export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
|
|
123
194
|
try {
|
|
124
195
|
console.log("🔍 Fetching pull requests and diffs...");
|
|
125
|
-
const token = await ensureGitHubAuth();
|
|
196
|
+
const token = await ensureGitHubAuth(); // Get GitHub token
|
|
126
197
|
const username = await getGitHubUsername(token);
|
|
127
|
-
const { owner, repo } = getRepoDetails();
|
|
198
|
+
const { owner, repo } = getRepoDetails(); // Get repository details (owner, repo name)
|
|
128
199
|
console.log(`👤 Authenticated user: ${username}`);
|
|
129
200
|
console.log(`📦 GitHub repo: ${owner}/${repo}`);
|
|
130
201
|
console.log(`🔍 Filtering ${showAll ? "all" : "user-specific"} PRs for branch: ${branch}`);
|
|
@@ -143,32 +214,101 @@ export async function reviewPullRequestCmd(branch = 'main', showAll = false) {
|
|
|
143
214
|
console.log(`🔍 Fetching diff for PR #${pr.number}...`);
|
|
144
215
|
prDiff = await fetchPullRequestDiff(pr, token);
|
|
145
216
|
}
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
console.log(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
217
|
+
const chunkComments = {}; // Declare here for global access
|
|
218
|
+
const reviewMethod = await askReviewMethod(); // Ask user for review method (whole or chunk)
|
|
219
|
+
if (reviewMethod === 'whole') {
|
|
220
|
+
console.log(`🔍 Reviewing the entire PR at once...`);
|
|
221
|
+
const suggestion = await reviewModule.run({ content: prDiff, filepath: 'Whole PR Diff' });
|
|
222
|
+
console.log("\n💡 AI-suggested review:\n");
|
|
223
|
+
console.log(suggestion.content);
|
|
224
|
+
// Store the entire review choice in chunkComments
|
|
225
|
+
const finalReviewChoice = await askReviewApproval('Do you approve, reject, or leave a final review for this PR?');
|
|
226
|
+
if (finalReviewChoice === 'approve') {
|
|
227
|
+
console.log(`✅ Review for PR #${pr.number} approved.`);
|
|
228
|
+
await postReviewComments(pr, chunkComments, token);
|
|
229
|
+
await submitReview(pr.number, 'PR approved', 'APPROVE');
|
|
230
|
+
}
|
|
231
|
+
else if (finalReviewChoice === 'reject') {
|
|
232
|
+
console.log(`❌ Review for PR #${pr.number} rejected.`);
|
|
233
|
+
await postReviewComments(pr, chunkComments, token);
|
|
234
|
+
await submitReview(pr.number, 'Changes requested', 'REQUEST_CHANGES');
|
|
235
|
+
}
|
|
236
|
+
else if (finalReviewChoice === 'cancel') {
|
|
237
|
+
console.log(`🚪 Review process cancelled.`);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const customReview = await promptCustomReview();
|
|
242
|
+
console.log(`💬 Custom review: ${customReview}`);
|
|
243
|
+
await postReviewComments(pr, chunkComments, token);
|
|
244
|
+
await submitReview(pr.number, customReview, 'COMMENT');
|
|
245
|
+
}
|
|
163
246
|
}
|
|
164
247
|
else {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
console.log(
|
|
168
|
-
|
|
248
|
+
const chunks = chunkDiff(prDiff); // Split the diff into chunks
|
|
249
|
+
// Log the total number of chunks
|
|
250
|
+
console.log(chalk.cyan(`🔍 Total Chunks: ${chunks.length}`));
|
|
251
|
+
// Iterate over each chunk, passing the index and total chunk count
|
|
252
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
253
|
+
const chunk = chunks[i];
|
|
254
|
+
// Pass the chunk, index (i), and totalChunks to the reviewChunk function
|
|
255
|
+
const reviewChoice = await reviewChunk(chunk, i, chunks.length); // Review each chunk
|
|
256
|
+
if (reviewChoice === 'approve') {
|
|
257
|
+
console.log('✅ Approving this chunk.');
|
|
258
|
+
chunkComments[chunk.filePath] = ['Approved'];
|
|
259
|
+
}
|
|
260
|
+
else if (reviewChoice === 'reject') {
|
|
261
|
+
console.log('❌ Rejecting this chunk.');
|
|
262
|
+
chunkComments[chunk.filePath] = ['Rejected'];
|
|
263
|
+
}
|
|
264
|
+
else if (reviewChoice === 'cancel') {
|
|
265
|
+
console.log('🚪 Review process cancelled.');
|
|
266
|
+
return; // Exit if user cancels
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
console.log('✍️ Custom review added.');
|
|
270
|
+
const customReview = await promptCustomReview();
|
|
271
|
+
console.log(`💬 Custom review: ${customReview}`);
|
|
272
|
+
chunkComments[chunk.filePath] = [customReview];
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// After chunk review, ask for the final approval decision
|
|
276
|
+
const finalReviewChoice = await askReviewApproval('Do you approve, reject, or leave a final review for this PR?');
|
|
277
|
+
if (finalReviewChoice === 'approve') {
|
|
278
|
+
console.log(`✅ Review for PR #${pr.number} approved.`);
|
|
279
|
+
await postReviewComments(pr, chunkComments, token);
|
|
280
|
+
await submitReview(pr.number, 'PR approved', 'APPROVE');
|
|
281
|
+
}
|
|
282
|
+
else if (finalReviewChoice === 'reject') {
|
|
283
|
+
console.log(`❌ Review for PR #${pr.number} rejected.`);
|
|
284
|
+
await postReviewComments(pr, chunkComments, token);
|
|
285
|
+
await submitReview(pr.number, 'Changes requested', 'REQUEST_CHANGES');
|
|
286
|
+
}
|
|
287
|
+
else if (finalReviewChoice === 'cancel') {
|
|
288
|
+
console.log(`🚪 Review process cancelled.`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
const customReview = await promptCustomReview();
|
|
293
|
+
console.log(`💬 Custom review: ${customReview}`);
|
|
294
|
+
await postReviewComments(pr, chunkComments, token);
|
|
295
|
+
await submitReview(pr.number, customReview, 'COMMENT');
|
|
296
|
+
}
|
|
169
297
|
}
|
|
170
298
|
}
|
|
171
299
|
catch (err) {
|
|
172
300
|
console.error("❌ Error reviewing PR:", err.message);
|
|
173
301
|
}
|
|
174
302
|
}
|
|
303
|
+
// Function to post all comments to GitHub after the review
|
|
304
|
+
async function postReviewComments(pr, chunkComments, token) {
|
|
305
|
+
const { owner, repo } = getRepoDetails(); // Get the repo details
|
|
306
|
+
for (const chunk in chunkComments) {
|
|
307
|
+
const comments = chunkComments[chunk];
|
|
308
|
+
const fileName = chunk; // Use chunk's file path
|
|
309
|
+
const lineNumber = 10; // Extract the actual line number from the chunk content
|
|
310
|
+
for (const comment of comments) {
|
|
311
|
+
await postReviewComment(token, owner, repo, pr.number, fileName, lineNumber, comment);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -7,7 +7,7 @@ import { summarizeCode } from '../utils/summarizer.js';
|
|
|
7
7
|
import { detectFileType } from '../fileRules/detectFileType.js';
|
|
8
8
|
import { generateEmbedding } from '../lib/generateEmbedding.js';
|
|
9
9
|
import { sanitizeQueryForFts } from '../utils/sanitizeQuery.js';
|
|
10
|
-
import {
|
|
10
|
+
import { getDbForRepo } from '../db/client.js';
|
|
11
11
|
export async function summarizeFile(filepath) {
|
|
12
12
|
let content = '';
|
|
13
13
|
let filePathResolved;
|
|
@@ -71,9 +71,10 @@ export async function summarizeFile(filepath) {
|
|
|
71
71
|
console.log('💾 Summary saved to local database.');
|
|
72
72
|
const embedding = await generateEmbedding(response.summary);
|
|
73
73
|
if (embedding) {
|
|
74
|
+
const db = getDbForRepo();
|
|
74
75
|
db.prepare(`
|
|
75
|
-
|
|
76
|
-
|
|
76
|
+
UPDATE files SET embedding = ? WHERE path = ?
|
|
77
|
+
`).run(JSON.stringify(embedding), filePathResolved.replace(/\\/g, '/'));
|
|
77
78
|
console.log('📐 Embedding saved to database.');
|
|
78
79
|
}
|
|
79
80
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// File: src/commands/switch.ts
|
|
2
|
+
import readline from 'readline';
|
|
3
|
+
import { Config, writeConfig } from '../config.js';
|
|
4
|
+
import { normalizePath, getRepoKeyForPath } from '../utils/normalizePath.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
export function runSwitchCommand(inputPathOrKey) {
|
|
7
|
+
const config = Config.getRaw();
|
|
8
|
+
const normalizedInput = normalizePath(inputPathOrKey);
|
|
9
|
+
// Try to match by key directly
|
|
10
|
+
if (config.repos[normalizedInput]) {
|
|
11
|
+
config.activeRepo = normalizedInput;
|
|
12
|
+
// Update GitHub token
|
|
13
|
+
Config.setGitHubToken(config.repos[normalizedInput].githubToken ?? '');
|
|
14
|
+
console.log(`✅ Switched active repo to key: ${normalizedInput}`);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
// Try to match by indexDir path
|
|
18
|
+
const repoKey = getRepoKeyForPath(inputPathOrKey, config);
|
|
19
|
+
if (!repoKey) {
|
|
20
|
+
console.error(`❌ No repo found matching path or key: "${inputPathOrKey}"`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
config.activeRepo = repoKey;
|
|
24
|
+
// Update GitHub token
|
|
25
|
+
Config.setGitHubToken(config.repos[repoKey]?.githubToken ?? '');
|
|
26
|
+
console.log(`✅ Switched active repo to path match: ${repoKey}`);
|
|
27
|
+
}
|
|
28
|
+
// Ensure the active repo change is saved back to the config
|
|
29
|
+
writeConfig(config);
|
|
30
|
+
}
|
|
31
|
+
export async function runInteractiveSwitch() {
|
|
32
|
+
const config = Config.getRaw();
|
|
33
|
+
const keys = Object.keys(config.repos || {});
|
|
34
|
+
if (!keys.length) {
|
|
35
|
+
console.log('⚠️ No repositories configured.');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
// Auto-switch to the other repo if only 2 are present
|
|
39
|
+
if (keys.length === 2) {
|
|
40
|
+
const current = config.activeRepo;
|
|
41
|
+
const other = keys.find(k => k !== current);
|
|
42
|
+
if (other) {
|
|
43
|
+
runSwitchCommand(other);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Otherwise, show interactive selection
|
|
48
|
+
console.log('\n📁 Available Repositories:\n');
|
|
49
|
+
keys.forEach((key, i) => {
|
|
50
|
+
const isActive = config.activeRepo === key ? chalk.green('(active)') : '';
|
|
51
|
+
const dir = config.repos[key]?.indexDir ?? '';
|
|
52
|
+
// Color the number using chalk.blue and make active repo green
|
|
53
|
+
const numberedRepo = chalk.blue(`${i + 1})`);
|
|
54
|
+
// Highlight the active repo in green and list it
|
|
55
|
+
console.log(`${numberedRepo} ${key} ${isActive}`);
|
|
56
|
+
// Use light grey for the indexDir
|
|
57
|
+
console.log(` ↳ ${chalk.grey(dir)}`);
|
|
58
|
+
});
|
|
59
|
+
const rl = readline.createInterface({
|
|
60
|
+
input: process.stdin,
|
|
61
|
+
output: process.stdout,
|
|
62
|
+
});
|
|
63
|
+
rl.question('\n👉 Select a repository number to activate: ', (answer) => {
|
|
64
|
+
rl.close();
|
|
65
|
+
const index = parseInt(answer.trim(), 10) - 1;
|
|
66
|
+
if (isNaN(index) || index < 0 || index >= keys.length) {
|
|
67
|
+
console.log('❌ Invalid selection.');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const selectedKey = keys[index];
|
|
71
|
+
runSwitchCommand(selectedKey);
|
|
72
|
+
});
|
|
73
|
+
}
|