@ariso-ai/ivan 1.0.5 → 1.0.6
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/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +202 -3
- package/dist/config.js.map +1 -1
- package/dist/database/migrations/009_create_repository_table.d.ts +3 -0
- package/dist/database/migrations/009_create_repository_table.d.ts.map +1 -0
- package/dist/database/migrations/009_create_repository_table.js +15 -0
- package/dist/database/migrations/009_create_repository_table.js.map +1 -0
- package/dist/database/migrations/010_add_repository_id_to_jobs_and_tasks.d.ts +3 -0
- package/dist/database/migrations/010_add_repository_id_to_jobs_and_tasks.d.ts.map +1 -0
- package/dist/database/migrations/010_add_repository_id_to_jobs_and_tasks.js +13 -0
- package/dist/database/migrations/010_add_repository_id_to_jobs_and_tasks.js.map +1 -0
- package/dist/database/migrations/011_create_learnings_table.d.ts +3 -0
- package/dist/database/migrations/011_create_learnings_table.d.ts.map +1 -0
- package/dist/database/migrations/011_create_learnings_table.js +16 -0
- package/dist/database/migrations/011_create_learnings_table.js.map +1 -0
- package/dist/database/migrations/012_create_learning_embeddings_table.d.ts +3 -0
- package/dist/database/migrations/012_create_learning_embeddings_table.d.ts.map +1 -0
- package/dist/database/migrations/012_create_learning_embeddings_table.js +13 -0
- package/dist/database/migrations/012_create_learning_embeddings_table.js.map +1 -0
- package/dist/database/migrations/index.d.ts.map +1 -1
- package/dist/database/migrations/index.js +5 -1
- package/dist/database/migrations/index.js.map +1 -1
- package/dist/database/types.d.ts +11 -0
- package/dist/database/types.d.ts.map +1 -1
- package/dist/services/address-executor.d.ts.map +1 -1
- package/dist/services/address-executor.js +8 -21
- package/dist/services/address-executor.js.map +1 -1
- package/dist/services/address-task-executor.d.ts.map +1 -1
- package/dist/services/address-task-executor.js +5 -6
- package/dist/services/address-task-executor.js.map +1 -1
- package/dist/services/git-interfaces.d.ts +67 -0
- package/dist/services/git-interfaces.d.ts.map +1 -0
- package/dist/services/git-interfaces.js +2 -0
- package/dist/services/git-interfaces.js.map +1 -0
- package/dist/services/git-manager-cli.d.ts +33 -0
- package/dist/services/git-manager-cli.d.ts.map +1 -0
- package/dist/services/git-manager-cli.js +734 -0
- package/dist/services/git-manager-cli.js.map +1 -0
- package/dist/services/git-manager-pat.d.ts +36 -0
- package/dist/services/git-manager-pat.d.ts.map +1 -0
- package/dist/services/git-manager-pat.js +667 -0
- package/dist/services/git-manager-pat.js.map +1 -0
- package/dist/services/github-api-client.d.ts +115 -0
- package/dist/services/github-api-client.d.ts.map +1 -0
- package/dist/services/github-api-client.js +256 -0
- package/dist/services/github-api-client.js.map +1 -0
- package/dist/services/index.d.ts +9 -2
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +12 -2
- package/dist/services/index.js.map +1 -1
- package/dist/services/job-manager.d.ts +3 -3
- package/dist/services/job-manager.d.ts.map +1 -1
- package/dist/services/job-manager.js +14 -10
- package/dist/services/job-manager.js.map +1 -1
- package/dist/services/learn-executor.d.ts +13 -0
- package/dist/services/learn-executor.d.ts.map +1 -0
- package/dist/services/learn-executor.js +293 -0
- package/dist/services/learn-executor.js.map +1 -0
- package/dist/services/learning-service.d.ts +30 -0
- package/dist/services/learning-service.d.ts.map +1 -0
- package/dist/services/learning-service.js +88 -0
- package/dist/services/learning-service.js.map +1 -0
- package/dist/services/pr-service-cli.d.ts +12 -0
- package/dist/services/pr-service-cli.d.ts.map +1 -0
- package/dist/services/pr-service-cli.js +291 -0
- package/dist/services/pr-service-cli.js.map +1 -0
- package/dist/services/pr-service-pat.d.ts +15 -0
- package/dist/services/pr-service-pat.d.ts.map +1 -0
- package/dist/services/pr-service-pat.js +255 -0
- package/dist/services/pr-service-pat.js.map +1 -0
- package/dist/services/repository-manager-cli.d.ts +17 -0
- package/dist/services/repository-manager-cli.d.ts.map +1 -0
- package/dist/services/repository-manager-cli.js +148 -0
- package/dist/services/repository-manager-cli.js.map +1 -0
- package/dist/services/repository-manager-pat.d.ts +17 -0
- package/dist/services/repository-manager-pat.d.ts.map +1 -0
- package/dist/services/repository-manager-pat.js +148 -0
- package/dist/services/repository-manager-pat.js.map +1 -0
- package/dist/services/repository-manager.d.ts +7 -0
- package/dist/services/repository-manager.d.ts.map +1 -1
- package/dist/services/repository-manager.js +47 -0
- package/dist/services/repository-manager.js.map +1 -1
- package/dist/services/service-factory.d.ts +27 -0
- package/dist/services/service-factory.d.ts.map +1 -0
- package/dist/services/service-factory.js +79 -0
- package/dist/services/service-factory.js.map +1 -0
- package/dist/services/task-executor.d.ts.map +1 -1
- package/dist/services/task-executor.js +39 -68
- package/dist/services/task-executor.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { OpenAIService } from './openai-service.js';
|
|
4
|
+
import { ConfigManager } from '../config.js';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { promises as fs } from 'fs';
|
|
7
|
+
import { GitHubAPIClient } from './github-api-client.js';
|
|
8
|
+
export class GitManagerPAT {
|
|
9
|
+
workingDir;
|
|
10
|
+
openaiService = null;
|
|
11
|
+
configManager;
|
|
12
|
+
originalWorkingDir;
|
|
13
|
+
githubClient;
|
|
14
|
+
owner;
|
|
15
|
+
repo;
|
|
16
|
+
constructor(workingDir, pat) {
|
|
17
|
+
this.workingDir = workingDir;
|
|
18
|
+
this.originalWorkingDir = workingDir;
|
|
19
|
+
this.configManager = new ConfigManager();
|
|
20
|
+
this.githubClient = new GitHubAPIClient(pat);
|
|
21
|
+
// Get repository info from git remote
|
|
22
|
+
const repoInfo = GitHubAPIClient.getRepoInfoFromRemote(workingDir);
|
|
23
|
+
this.owner = repoInfo.owner;
|
|
24
|
+
this.repo = repoInfo.repo;
|
|
25
|
+
}
|
|
26
|
+
getOpenAIService() {
|
|
27
|
+
if (!this.openaiService) {
|
|
28
|
+
this.openaiService = new OpenAIService();
|
|
29
|
+
}
|
|
30
|
+
return this.openaiService;
|
|
31
|
+
}
|
|
32
|
+
ensureGitRepo() {
|
|
33
|
+
try {
|
|
34
|
+
execSync('git rev-parse --git-dir', {
|
|
35
|
+
cwd: this.workingDir,
|
|
36
|
+
stdio: 'ignore'
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
throw new Error(`Directory is not a git repository: ${this.workingDir}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
validateGitHubCliInstallation() {
|
|
44
|
+
// PAT-based implementation doesn't require GitHub CLI
|
|
45
|
+
// This method is a no-op for PAT implementation
|
|
46
|
+
}
|
|
47
|
+
validateGitHubCliAuthentication() {
|
|
48
|
+
// PAT-based implementation doesn't use GitHub CLI auth
|
|
49
|
+
// We could validate the PAT here if needed
|
|
50
|
+
}
|
|
51
|
+
async createBranch(branchName) {
|
|
52
|
+
this.ensureGitRepo();
|
|
53
|
+
try {
|
|
54
|
+
const escapedBranchName = branchName.replace(/"/g, '\\"');
|
|
55
|
+
execSync(`git checkout -b "${escapedBranchName}"`, {
|
|
56
|
+
cwd: this.workingDir,
|
|
57
|
+
stdio: 'pipe'
|
|
58
|
+
});
|
|
59
|
+
console.log(chalk.green(`✅ Created and switched to branch: ${branchName}`));
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw new Error(`Failed to create branch ${branchName}: ${error}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async commitChanges(message) {
|
|
66
|
+
this.ensureGitRepo();
|
|
67
|
+
try {
|
|
68
|
+
const status = execSync('git status --porcelain', {
|
|
69
|
+
cwd: this.workingDir,
|
|
70
|
+
encoding: 'utf8'
|
|
71
|
+
});
|
|
72
|
+
if (!status.trim()) {
|
|
73
|
+
console.log(chalk.yellow('⚠️ No changes to commit'));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
execSync('git add --all', {
|
|
77
|
+
cwd: this.workingDir,
|
|
78
|
+
stdio: 'pipe'
|
|
79
|
+
});
|
|
80
|
+
const escapedMessage = message
|
|
81
|
+
.replace(/\\/g, '\\\\')
|
|
82
|
+
.replace(/"/g, '\\"')
|
|
83
|
+
.replace(/`/g, '\\`')
|
|
84
|
+
.replace(/\$/g, '\\$')
|
|
85
|
+
.replace(/!/g, '\\!');
|
|
86
|
+
const commitMessage = `${escapedMessage}\n\nCo-authored-by: ivan-agent <ivan-agent@users.noreply.github.com>`;
|
|
87
|
+
execSync(`git commit -m "${commitMessage}"`, {
|
|
88
|
+
cwd: this.workingDir,
|
|
89
|
+
stdio: 'pipe'
|
|
90
|
+
});
|
|
91
|
+
console.log(chalk.green(`✅ Committed changes: ${message}`));
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
throw new Error(`Failed to commit changes: ${error}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async createEmptyCommit(message) {
|
|
98
|
+
this.ensureGitRepo();
|
|
99
|
+
try {
|
|
100
|
+
const escapedMessage = message
|
|
101
|
+
.replace(/\\/g, '\\\\')
|
|
102
|
+
.replace(/"/g, '\\"')
|
|
103
|
+
.replace(/`/g, '\\`')
|
|
104
|
+
.replace(/\$/g, '\\$')
|
|
105
|
+
.replace(/!/g, '\\!');
|
|
106
|
+
const commitMessage = `${escapedMessage}\n\nCo-authored-by: ivan-agent <ivan-agent@users.noreply.github.com>`;
|
|
107
|
+
execSync(`git commit --allow-empty -m "${commitMessage}"`, {
|
|
108
|
+
cwd: this.workingDir,
|
|
109
|
+
stdio: 'pipe'
|
|
110
|
+
});
|
|
111
|
+
console.log(chalk.green(`✅ Created empty commit: ${message}`));
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
throw new Error(`Failed to create empty commit: ${error}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async pushBranch(branchName) {
|
|
118
|
+
this.ensureGitRepo();
|
|
119
|
+
try {
|
|
120
|
+
const escapedBranchName = branchName.replace(/"/g, '\\"');
|
|
121
|
+
execSync(`git push -u origin "${escapedBranchName}"`, {
|
|
122
|
+
cwd: this.workingDir,
|
|
123
|
+
stdio: 'pipe'
|
|
124
|
+
});
|
|
125
|
+
console.log(chalk.green(`✅ Pushed branch to origin: ${branchName}`));
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
throw new Error(`Failed to push branch ${branchName}: ${error}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async createPullRequest(title, body) {
|
|
132
|
+
this.ensureGitRepo();
|
|
133
|
+
const bodyWithAttribution = `${body}\n\n---\n*Co-authored with @ivan-agent*`;
|
|
134
|
+
const MAX_BODY_LENGTH = 65536;
|
|
135
|
+
let finalBody = bodyWithAttribution;
|
|
136
|
+
if (finalBody.length > MAX_BODY_LENGTH) {
|
|
137
|
+
const attributionText = '\n\n---\n*Co-authored with @ivan-agent*';
|
|
138
|
+
const truncationText = '\n\n... (description truncated to fit GitHub limits)';
|
|
139
|
+
const maxOriginalBodyLength = MAX_BODY_LENGTH - attributionText.length - truncationText.length;
|
|
140
|
+
finalBody = body.substring(0, maxOriginalBodyLength) + truncationText + attributionText;
|
|
141
|
+
}
|
|
142
|
+
try {
|
|
143
|
+
const currentBranch = this.getCurrentBranch();
|
|
144
|
+
const mainBranch = this.getMainBranch();
|
|
145
|
+
// Retry creating the PR with exponential backoff
|
|
146
|
+
// GitHub API needs time to process the push before the ref is readable
|
|
147
|
+
let pr;
|
|
148
|
+
let lastError;
|
|
149
|
+
const maxRetries = 5;
|
|
150
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
151
|
+
try {
|
|
152
|
+
// Wait before attempting (exponential backoff: 1s, 2s, 4s, 8s, 16s)
|
|
153
|
+
if (attempt > 0) {
|
|
154
|
+
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 16000);
|
|
155
|
+
console.log(chalk.gray(`⏳ Waiting ${delay / 1000}s for GitHub to process the push...`));
|
|
156
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
157
|
+
}
|
|
158
|
+
// Verify branch exists via git ls-remote before attempting PR creation
|
|
159
|
+
try {
|
|
160
|
+
const remoteBranches = execSync(`git ls-remote --heads origin ${currentBranch}`, {
|
|
161
|
+
cwd: this.workingDir,
|
|
162
|
+
encoding: 'utf8'
|
|
163
|
+
});
|
|
164
|
+
if (!remoteBranches.trim()) {
|
|
165
|
+
throw new Error(`Branch ${currentBranch} not found on remote`);
|
|
166
|
+
}
|
|
167
|
+
console.log(chalk.gray(`✓ Verified branch ${currentBranch} exists on remote`));
|
|
168
|
+
}
|
|
169
|
+
catch (verifyError) {
|
|
170
|
+
console.log(chalk.yellow(`⚠️ Could not verify branch on remote: ${verifyError}`));
|
|
171
|
+
}
|
|
172
|
+
// Create PR using GitHub API
|
|
173
|
+
pr = await this.githubClient.createPullRequest(this.owner, this.repo, title, finalBody, currentBranch, mainBranch, true // draft
|
|
174
|
+
);
|
|
175
|
+
// Success! Break out of retry loop
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
lastError = error;
|
|
180
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
181
|
+
// Only retry if it's the "not all refs are readable" error
|
|
182
|
+
if (errorMessage.includes('not all refs are readable') && attempt < maxRetries - 1) {
|
|
183
|
+
console.log(chalk.yellow(`⚠️ Branch not yet visible to GitHub API (attempt ${attempt + 1}/${maxRetries})`));
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
// If it's a different error or we've exhausted retries, throw
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (!pr) {
|
|
191
|
+
throw lastError || new Error('Failed to create PR after retries');
|
|
192
|
+
}
|
|
193
|
+
const prUrl = pr.url;
|
|
194
|
+
console.log(chalk.green(`✅ Created pull request: ${prUrl}`));
|
|
195
|
+
// Try to assign to ivan-agent
|
|
196
|
+
try {
|
|
197
|
+
await this.githubClient.updatePR(this.owner, this.repo, pr.number, {
|
|
198
|
+
assignees: ['ivan-agent']
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Ignore assignment errors
|
|
203
|
+
}
|
|
204
|
+
// Generate and add specific review comment
|
|
205
|
+
await this.addReviewComment(prUrl, pr.number);
|
|
206
|
+
return prUrl;
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
throw new Error(`Failed to create pull request: ${error}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
getChangedFiles(from, to) {
|
|
213
|
+
this.ensureGitRepo();
|
|
214
|
+
try {
|
|
215
|
+
if (from && to) {
|
|
216
|
+
const files = execSync(`git diff --name-only ${from} ${to}`, {
|
|
217
|
+
cwd: this.workingDir,
|
|
218
|
+
encoding: 'utf8'
|
|
219
|
+
});
|
|
220
|
+
return files.split('\n').filter(line => line.trim());
|
|
221
|
+
}
|
|
222
|
+
else if (from) {
|
|
223
|
+
const files = execSync(`git diff --name-only ${from}`, {
|
|
224
|
+
cwd: this.workingDir,
|
|
225
|
+
encoding: 'utf8'
|
|
226
|
+
});
|
|
227
|
+
return files.split('\n').filter(line => line.trim());
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
const status = execSync('git status --porcelain', {
|
|
231
|
+
cwd: this.workingDir,
|
|
232
|
+
encoding: 'utf8'
|
|
233
|
+
});
|
|
234
|
+
if (!status.trim()) {
|
|
235
|
+
return [];
|
|
236
|
+
}
|
|
237
|
+
const files = status.trim().split('\n').map(line => {
|
|
238
|
+
return line.substring(3).trim();
|
|
239
|
+
}).filter(Boolean);
|
|
240
|
+
return files;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
getDiff(from, to) {
|
|
248
|
+
this.ensureGitRepo();
|
|
249
|
+
try {
|
|
250
|
+
if (from && to) {
|
|
251
|
+
const diff = execSync(`git diff ${from} ${to}`, {
|
|
252
|
+
cwd: this.workingDir,
|
|
253
|
+
encoding: 'utf8'
|
|
254
|
+
});
|
|
255
|
+
return diff;
|
|
256
|
+
}
|
|
257
|
+
else if (from) {
|
|
258
|
+
const diff = execSync(`git diff ${from}`, {
|
|
259
|
+
cwd: this.workingDir,
|
|
260
|
+
encoding: 'utf8'
|
|
261
|
+
});
|
|
262
|
+
return diff;
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
execSync('git add -A', {
|
|
266
|
+
cwd: this.workingDir,
|
|
267
|
+
stdio: 'pipe'
|
|
268
|
+
});
|
|
269
|
+
const diff = execSync('git diff --cached', {
|
|
270
|
+
cwd: this.workingDir,
|
|
271
|
+
encoding: 'utf8'
|
|
272
|
+
});
|
|
273
|
+
execSync('git reset', {
|
|
274
|
+
cwd: this.workingDir,
|
|
275
|
+
stdio: 'pipe'
|
|
276
|
+
});
|
|
277
|
+
return diff;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
catch {
|
|
281
|
+
return '';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
getCurrentBranch() {
|
|
285
|
+
this.ensureGitRepo();
|
|
286
|
+
try {
|
|
287
|
+
return execSync('git branch --show-current', {
|
|
288
|
+
cwd: this.workingDir,
|
|
289
|
+
encoding: 'utf8'
|
|
290
|
+
}).trim();
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
return '';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
getMainBranch() {
|
|
297
|
+
this.ensureGitRepo();
|
|
298
|
+
// For PAT implementation, we use local git commands for synchronous behavior
|
|
299
|
+
// The GitHub API call would require async, but this method needs to be sync
|
|
300
|
+
// to match the interface
|
|
301
|
+
try {
|
|
302
|
+
execSync('git rev-parse --verify main', {
|
|
303
|
+
cwd: this.workingDir,
|
|
304
|
+
stdio: 'ignore'
|
|
305
|
+
});
|
|
306
|
+
return 'main';
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
try {
|
|
310
|
+
execSync('git rev-parse --verify master', {
|
|
311
|
+
cwd: this.workingDir,
|
|
312
|
+
stdio: 'ignore'
|
|
313
|
+
});
|
|
314
|
+
return 'master';
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
return 'main';
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async cleanupAndSyncMain() {
|
|
322
|
+
const workDir = this.originalWorkingDir;
|
|
323
|
+
try {
|
|
324
|
+
execSync('git rev-parse --git-dir', {
|
|
325
|
+
cwd: workDir,
|
|
326
|
+
stdio: 'ignore'
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
throw new Error(`Directory is not a git repository: ${workDir}`);
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const status = execSync('git status --porcelain', {
|
|
334
|
+
cwd: workDir,
|
|
335
|
+
encoding: 'utf8'
|
|
336
|
+
});
|
|
337
|
+
if (status.trim()) {
|
|
338
|
+
console.log(chalk.yellow('⚠️ Stashing uncommitted changes'));
|
|
339
|
+
execSync('git stash push -u -m "Ivan: stashing before cleanup"', {
|
|
340
|
+
cwd: workDir,
|
|
341
|
+
stdio: 'pipe'
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
execSync('git clean -fd', {
|
|
345
|
+
cwd: workDir,
|
|
346
|
+
stdio: 'pipe'
|
|
347
|
+
});
|
|
348
|
+
execSync('git checkout main', {
|
|
349
|
+
cwd: workDir,
|
|
350
|
+
stdio: 'pipe'
|
|
351
|
+
});
|
|
352
|
+
execSync('git pull origin main', {
|
|
353
|
+
cwd: workDir,
|
|
354
|
+
stdio: 'pipe'
|
|
355
|
+
});
|
|
356
|
+
console.log(chalk.green('✅ Cleaned up and synced with main branch'));
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
throw new Error(`Failed to cleanup and sync main: ${error}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
generateBranchName(taskDescription) {
|
|
363
|
+
const sanitized = taskDescription
|
|
364
|
+
.toLowerCase()
|
|
365
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
366
|
+
.replace(/\s+/g, '-')
|
|
367
|
+
.substring(0, 50);
|
|
368
|
+
const timestamp = Date.now().toString().slice(-6);
|
|
369
|
+
return `ivan/${sanitized}-${timestamp}`;
|
|
370
|
+
}
|
|
371
|
+
async getPRInfo(prNumber) {
|
|
372
|
+
try {
|
|
373
|
+
const pr = await this.githubClient.getPR(this.owner, this.repo, prNumber);
|
|
374
|
+
return {
|
|
375
|
+
headRefName: pr.headRefName,
|
|
376
|
+
number: pr.number,
|
|
377
|
+
title: pr.title,
|
|
378
|
+
url: pr.url
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
throw new Error(`Failed to get PR info for #${prNumber}: ${error}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async addReviewComment(prUrl, prNumber) {
|
|
386
|
+
try {
|
|
387
|
+
const currentBranch = this.getCurrentBranch();
|
|
388
|
+
const mainBranch = this.getMainBranch();
|
|
389
|
+
const diff = execSync(`git diff ${mainBranch}...${currentBranch}`, {
|
|
390
|
+
cwd: this.workingDir,
|
|
391
|
+
encoding: 'utf8',
|
|
392
|
+
maxBuffer: 10 * 1024 * 1024
|
|
393
|
+
});
|
|
394
|
+
const changedFiles = execSync(`git diff --name-only ${mainBranch}...${currentBranch}`, {
|
|
395
|
+
cwd: this.workingDir,
|
|
396
|
+
encoding: 'utf8'
|
|
397
|
+
}).trim().split('\n').filter(f => f.trim());
|
|
398
|
+
const reviewInstructions = await this.generateReviewInstructions(diff, changedFiles);
|
|
399
|
+
const reviewAgent = this.configManager.getReviewAgent();
|
|
400
|
+
const reviewComment = `${reviewAgent} ${reviewInstructions}`;
|
|
401
|
+
await this.githubClient.addPRComment(this.owner, this.repo, prNumber, reviewComment);
|
|
402
|
+
console.log(chalk.green(`✅ Added specific review request for ${reviewAgent}`));
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
console.log(chalk.yellow(`⚠️ Could not add review comment: ${error}`));
|
|
406
|
+
try {
|
|
407
|
+
const reviewAgent = this.configManager.getReviewAgent();
|
|
408
|
+
await this.githubClient.addPRComment(this.owner, this.repo, prNumber, `${reviewAgent} please review the changes and verify the implementation meets requirements`);
|
|
409
|
+
}
|
|
410
|
+
catch (fallbackError) {
|
|
411
|
+
console.log(chalk.yellow(`⚠️ Could not add fallback review comment: ${fallbackError}`));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async generateReviewInstructions(diff, changedFiles) {
|
|
416
|
+
try {
|
|
417
|
+
if (!diff || diff.trim().length === 0) {
|
|
418
|
+
console.log(chalk.yellow('⚠️ No diff found between branches for review instructions'));
|
|
419
|
+
return 'please review the changes in this PR and verify the implementation meets requirements';
|
|
420
|
+
}
|
|
421
|
+
const openaiService = this.getOpenAIService();
|
|
422
|
+
const client = await openaiService.getClient();
|
|
423
|
+
const prompt = `You are reviewing a new pull request. Based on the following diff and changed files, generate a concise, specific review request that tells the reviewer what to focus on.
|
|
424
|
+
|
|
425
|
+
Changed files:
|
|
426
|
+
${changedFiles.join('\n')}
|
|
427
|
+
|
|
428
|
+
Diff:
|
|
429
|
+
${diff.substring(0, 8000)}${diff.length > 8000 ? '\n... (diff truncated)' : ''}
|
|
430
|
+
|
|
431
|
+
Generate a brief (1-2 sentences) review request that:
|
|
432
|
+
1. Mentions the key changes or features implemented
|
|
433
|
+
2. Asks the reviewer to verify specific aspects of the implementation
|
|
434
|
+
3. Is conversational and clear
|
|
435
|
+
|
|
436
|
+
Example format: "please review the new task executor implementation and verify that error handling properly captures all edge cases"
|
|
437
|
+
|
|
438
|
+
Return ONLY the review request text, without any prefix like "Please review" since the review agent will already be prepended.`;
|
|
439
|
+
const completion = await client.chat.completions.create({
|
|
440
|
+
model: 'gpt-4o-mini',
|
|
441
|
+
messages: [
|
|
442
|
+
{
|
|
443
|
+
role: 'system',
|
|
444
|
+
content: 'You are a helpful assistant that generates specific code review requests for new pull requests.'
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
role: 'user',
|
|
448
|
+
content: prompt
|
|
449
|
+
}
|
|
450
|
+
],
|
|
451
|
+
temperature: 0.3,
|
|
452
|
+
max_tokens: 150
|
|
453
|
+
});
|
|
454
|
+
const reviewRequest = completion.choices[0]?.message?.content?.trim();
|
|
455
|
+
if (!reviewRequest) {
|
|
456
|
+
return 'please review the changes and verify the implementation meets requirements';
|
|
457
|
+
}
|
|
458
|
+
return reviewRequest;
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
console.error('Error generating review instructions:', error);
|
|
462
|
+
return 'please review the changes and verify the implementation meets requirements';
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
async createWorktree(branchName) {
|
|
466
|
+
this.ensureGitRepo();
|
|
467
|
+
try {
|
|
468
|
+
const repoName = path.basename(this.originalWorkingDir);
|
|
469
|
+
const worktreeBasePath = path.join(path.dirname(this.originalWorkingDir), `.${repoName}-ivan-worktrees`);
|
|
470
|
+
const worktreePath = path.join(worktreeBasePath, branchName);
|
|
471
|
+
await fs.mkdir(worktreeBasePath, { recursive: true, mode: 0o755 });
|
|
472
|
+
try {
|
|
473
|
+
execSync(`git worktree remove --force "${worktreePath}"`, {
|
|
474
|
+
cwd: this.originalWorkingDir,
|
|
475
|
+
stdio: 'ignore'
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
catch {
|
|
479
|
+
// Ignore if worktree doesn't exist
|
|
480
|
+
}
|
|
481
|
+
execSync('git worktree prune', {
|
|
482
|
+
cwd: this.originalWorkingDir,
|
|
483
|
+
stdio: 'pipe'
|
|
484
|
+
});
|
|
485
|
+
const escapedBranchName = branchName.replace(/"/g, '\\"');
|
|
486
|
+
const escapedPath = worktreePath.replace(/"/g, '\\"');
|
|
487
|
+
let branchExists = false;
|
|
488
|
+
try {
|
|
489
|
+
execSync(`git rev-parse --verify "${escapedBranchName}"`, {
|
|
490
|
+
cwd: this.originalWorkingDir,
|
|
491
|
+
stdio: 'ignore'
|
|
492
|
+
});
|
|
493
|
+
branchExists = true;
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
branchExists = false;
|
|
497
|
+
}
|
|
498
|
+
try {
|
|
499
|
+
if (branchExists) {
|
|
500
|
+
console.log(chalk.gray(`Creating worktree from existing branch: ${branchName}`));
|
|
501
|
+
execSync(`git worktree add "${escapedPath}" "${escapedBranchName}"`, {
|
|
502
|
+
cwd: this.originalWorkingDir,
|
|
503
|
+
stdio: 'pipe'
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
console.log(chalk.gray(`Creating new branch in worktree: ${branchName}`));
|
|
508
|
+
execSync(`git worktree add -b "${escapedBranchName}" "${escapedPath}"`, {
|
|
509
|
+
cwd: this.originalWorkingDir,
|
|
510
|
+
stdio: 'pipe'
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch (worktreeError) {
|
|
515
|
+
const errorMessage = worktreeError instanceof Error ? worktreeError.message : String(worktreeError);
|
|
516
|
+
if (errorMessage.includes('already exists')) {
|
|
517
|
+
console.log(chalk.yellow('⚠️ Worktree already exists. Removing and recreating...'));
|
|
518
|
+
try {
|
|
519
|
+
execSync(`git worktree remove --force "${escapedPath}"`, {
|
|
520
|
+
cwd: this.originalWorkingDir,
|
|
521
|
+
stdio: 'pipe'
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
// Ignore removal errors
|
|
526
|
+
}
|
|
527
|
+
execSync('git worktree prune', {
|
|
528
|
+
cwd: this.originalWorkingDir,
|
|
529
|
+
stdio: 'pipe'
|
|
530
|
+
});
|
|
531
|
+
if (branchExists) {
|
|
532
|
+
console.log(chalk.gray(`Recreating worktree from existing branch: ${branchName}`));
|
|
533
|
+
execSync(`git worktree add "${escapedPath}" "${escapedBranchName}"`, {
|
|
534
|
+
cwd: this.originalWorkingDir,
|
|
535
|
+
stdio: 'pipe'
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
console.log(chalk.gray(`Creating new branch in worktree: ${branchName}`));
|
|
540
|
+
execSync(`git worktree add -b "${escapedBranchName}" "${escapedPath}"`, {
|
|
541
|
+
cwd: this.originalWorkingDir,
|
|
542
|
+
stdio: 'pipe'
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
throw worktreeError;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
try {
|
|
551
|
+
await fs.access(worktreePath);
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
throw new Error(`Worktree was not created successfully at ${worktreePath}`);
|
|
555
|
+
}
|
|
556
|
+
if (process.platform !== 'win32') {
|
|
557
|
+
try {
|
|
558
|
+
const stats = await fs.stat(this.originalWorkingDir);
|
|
559
|
+
await fs.chmod(worktreePath, stats.mode);
|
|
560
|
+
execSync(`find "${escapedPath}" -type f -exec chmod u+rw {} \\;`, {
|
|
561
|
+
cwd: path.dirname(worktreePath),
|
|
562
|
+
stdio: 'ignore'
|
|
563
|
+
});
|
|
564
|
+
execSync(`find "${escapedPath}" -type d -exec chmod u+rwx {} \\;`, {
|
|
565
|
+
cwd: path.dirname(worktreePath),
|
|
566
|
+
stdio: 'ignore'
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
catch (permError) {
|
|
570
|
+
console.log(chalk.yellow(`⚠️ Could not set optimal permissions on worktree: ${permError}`));
|
|
571
|
+
try {
|
|
572
|
+
execSync(`chmod -R 755 "${escapedPath}"`, {
|
|
573
|
+
stdio: 'ignore'
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
catch {
|
|
577
|
+
// Ignore permission errors
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
try {
|
|
582
|
+
const userName = execSync('git config user.name || true', {
|
|
583
|
+
cwd: this.originalWorkingDir,
|
|
584
|
+
encoding: 'utf8'
|
|
585
|
+
}).trim();
|
|
586
|
+
const userEmail = execSync('git config user.email || true', {
|
|
587
|
+
cwd: this.originalWorkingDir,
|
|
588
|
+
encoding: 'utf8'
|
|
589
|
+
}).trim();
|
|
590
|
+
if (userName) {
|
|
591
|
+
execSync(`git config user.name "${userName}"`, {
|
|
592
|
+
cwd: worktreePath,
|
|
593
|
+
stdio: 'pipe'
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (userEmail) {
|
|
597
|
+
execSync(`git config user.email "${userEmail}"`, {
|
|
598
|
+
cwd: worktreePath,
|
|
599
|
+
stdio: 'pipe'
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
catch (configError) {
|
|
604
|
+
console.log(chalk.yellow(`⚠️ Could not copy git config to worktree: ${configError}`));
|
|
605
|
+
}
|
|
606
|
+
try {
|
|
607
|
+
const packageJsonPath = path.join(worktreePath, 'package.json');
|
|
608
|
+
await fs.access(packageJsonPath);
|
|
609
|
+
console.log(chalk.cyan('📦 Found package.json, installing dependencies...'));
|
|
610
|
+
execSync('npm install', {
|
|
611
|
+
cwd: worktreePath,
|
|
612
|
+
stdio: 'inherit'
|
|
613
|
+
});
|
|
614
|
+
console.log(chalk.green('✅ Dependencies installed successfully'));
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
console.log(chalk.gray('ℹ️ No package.json found or npm install not needed'));
|
|
618
|
+
}
|
|
619
|
+
console.log(chalk.green(`✅ Created worktree at: ${worktreePath}`));
|
|
620
|
+
console.log(chalk.gray('You can continue working in your main repository while Ivan works here'));
|
|
621
|
+
return worktreePath;
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
throw new Error(`Failed to create worktree for branch ${branchName}: ${error}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
async removeWorktree(branchName) {
|
|
628
|
+
try {
|
|
629
|
+
const repoName = path.basename(this.originalWorkingDir);
|
|
630
|
+
const worktreeBasePath = path.join(path.dirname(this.originalWorkingDir), `.${repoName}-ivan-worktrees`);
|
|
631
|
+
const worktreePath = path.join(worktreeBasePath, branchName);
|
|
632
|
+
const escapedPath = worktreePath.replace(/"/g, '\\"');
|
|
633
|
+
execSync(`git worktree remove --force "${escapedPath}"`, {
|
|
634
|
+
cwd: this.originalWorkingDir,
|
|
635
|
+
stdio: 'pipe'
|
|
636
|
+
});
|
|
637
|
+
execSync('git worktree prune', {
|
|
638
|
+
cwd: this.originalWorkingDir,
|
|
639
|
+
stdio: 'pipe'
|
|
640
|
+
});
|
|
641
|
+
try {
|
|
642
|
+
const files = await fs.readdir(worktreeBasePath);
|
|
643
|
+
if (files.length === 0) {
|
|
644
|
+
await fs.rmdir(worktreeBasePath);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
// Ignore errors
|
|
649
|
+
}
|
|
650
|
+
console.log(chalk.green(`✅ Removed worktree for branch: ${branchName}`));
|
|
651
|
+
}
|
|
652
|
+
catch (error) {
|
|
653
|
+
console.log(chalk.yellow(`⚠️ Could not remove worktree: ${error}`));
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
switchToWorktree(worktreePath) {
|
|
657
|
+
this.workingDir = worktreePath;
|
|
658
|
+
}
|
|
659
|
+
switchToOriginalDir() {
|
|
660
|
+
this.workingDir = this.originalWorkingDir;
|
|
661
|
+
}
|
|
662
|
+
getWorktreePath(branchName) {
|
|
663
|
+
const repoName = path.basename(this.originalWorkingDir);
|
|
664
|
+
return path.join(path.dirname(this.originalWorkingDir), `.${repoName}-ivan-worktrees`, branchName);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
//# sourceMappingURL=git-manager-pat.js.map
|