@ariso-ai/ivan 1.0.0
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 +412 -0
- package/dist/agent.d.ts +11 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +48 -0
- package/dist/agent.js.map +1 -0
- package/dist/config/config.d.ts +20 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +187 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config.d.ts +46 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +414 -0
- package/dist/config.js.map +1 -0
- package/dist/database/database.d.ts +12 -0
- package/dist/database/database.d.ts.map +1 -0
- package/dist/database/database.js +45 -0
- package/dist/database/database.js.map +1 -0
- package/dist/database/migration.d.ts +11 -0
- package/dist/database/migration.d.ts.map +1 -0
- package/dist/database/migration.js +64 -0
- package/dist/database/migration.js.map +1 -0
- package/dist/database/migrations/001_create_jobs_table.d.ts +3 -0
- package/dist/database/migrations/001_create_jobs_table.d.ts.map +1 -0
- package/dist/database/migrations/001_create_jobs_table.js +14 -0
- package/dist/database/migrations/001_create_jobs_table.js.map +1 -0
- package/dist/database/migrations/001_initial_schema.d.ts +3 -0
- package/dist/database/migrations/001_initial_schema.d.ts.map +1 -0
- package/dist/database/migrations/001_initial_schema.js +66 -0
- package/dist/database/migrations/001_initial_schema.js.map +1 -0
- package/dist/database/migrations/002_create_tasks_table.d.ts +3 -0
- package/dist/database/migrations/002_create_tasks_table.d.ts.map +1 -0
- package/dist/database/migrations/002_create_tasks_table.js +16 -0
- package/dist/database/migrations/002_create_tasks_table.js.map +1 -0
- package/dist/database/migrations/003_add_log_to_tasks.d.ts +3 -0
- package/dist/database/migrations/003_add_log_to_tasks.d.ts.map +1 -0
- package/dist/database/migrations/003_add_log_to_tasks.js +11 -0
- package/dist/database/migrations/003_add_log_to_tasks.js.map +1 -0
- package/dist/database/migrations/004_add_branch_to_tasks.d.ts +3 -0
- package/dist/database/migrations/004_add_branch_to_tasks.d.ts.map +1 -0
- package/dist/database/migrations/004_add_branch_to_tasks.js +11 -0
- package/dist/database/migrations/004_add_branch_to_tasks.js.map +1 -0
- package/dist/database/migrations/005_add_type_to_tasks.d.ts +3 -0
- package/dist/database/migrations/005_add_type_to_tasks.d.ts.map +1 -0
- package/dist/database/migrations/005_add_type_to_tasks.js +11 -0
- package/dist/database/migrations/005_add_type_to_tasks.js.map +1 -0
- package/dist/database/migrations/006_add_comment_url_and_commit_to_tasks.d.ts +3 -0
- package/dist/database/migrations/006_add_comment_url_and_commit_to_tasks.d.ts.map +1 -0
- package/dist/database/migrations/006_add_comment_url_and_commit_to_tasks.js +15 -0
- package/dist/database/migrations/006_add_comment_url_and_commit_to_tasks.js.map +1 -0
- package/dist/database/migrations/006_add_comment_url_to_tasks.d.ts +3 -0
- package/dist/database/migrations/006_add_comment_url_to_tasks.d.ts.map +1 -0
- package/dist/database/migrations/006_add_comment_url_to_tasks.js +14 -0
- package/dist/database/migrations/006_add_comment_url_to_tasks.js.map +1 -0
- package/dist/database/migrations/007_add_commit_to_tasks.d.ts +3 -0
- package/dist/database/migrations/007_add_commit_to_tasks.d.ts.map +1 -0
- package/dist/database/migrations/007_add_commit_to_tasks.js +13 -0
- package/dist/database/migrations/007_add_commit_to_tasks.js.map +1 -0
- package/dist/database/migrations/008_add_lint_and_test_task_type.d.ts +3 -0
- package/dist/database/migrations/008_add_lint_and_test_task_type.d.ts.map +1 -0
- package/dist/database/migrations/008_add_lint_and_test_task_type.js +42 -0
- package/dist/database/migrations/008_add_lint_and_test_task_type.js.map +1 -0
- package/dist/database/migrations/index.d.ts +3 -0
- package/dist/database/migrations/index.d.ts.map +1 -0
- package/dist/database/migrations/index.js +19 -0
- package/dist/database/migrations/index.js.map +1 -0
- package/dist/database/types.d.ts +34 -0
- package/dist/database/types.d.ts.map +1 -0
- package/dist/database/types.js +2 -0
- package/dist/database/types.js.map +1 -0
- package/dist/database.d.ts +13 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +32 -0
- package/dist/database.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +285 -0
- package/dist/index.js.map +1 -0
- package/dist/scripts/task-executor.d.ts +3 -0
- package/dist/scripts/task-executor.d.ts.map +1 -0
- package/dist/scripts/task-executor.js +139 -0
- package/dist/scripts/task-executor.js.map +1 -0
- package/dist/scripts/task-planner.d.ts +3 -0
- package/dist/scripts/task-planner.d.ts.map +1 -0
- package/dist/scripts/task-planner.js +81 -0
- package/dist/scripts/task-planner.js.map +1 -0
- package/dist/services/address-executor.d.ts +13 -0
- package/dist/services/address-executor.d.ts.map +1 -0
- package/dist/services/address-executor.js +202 -0
- package/dist/services/address-executor.js.map +1 -0
- package/dist/services/address-task-executor.d.ts +19 -0
- package/dist/services/address-task-executor.d.ts.map +1 -0
- package/dist/services/address-task-executor.js +736 -0
- package/dist/services/address-task-executor.js.map +1 -0
- package/dist/services/claude-cli-executor.d.ts +14 -0
- package/dist/services/claude-cli-executor.d.ts.map +1 -0
- package/dist/services/claude-cli-executor.js +241 -0
- package/dist/services/claude-cli-executor.js.map +1 -0
- package/dist/services/claude-executor.d.ts +14 -0
- package/dist/services/claude-executor.d.ts.map +1 -0
- package/dist/services/claude-executor.js +274 -0
- package/dist/services/claude-executor.js.map +1 -0
- package/dist/services/claude-planner.d.ts +15 -0
- package/dist/services/claude-planner.d.ts.map +1 -0
- package/dist/services/claude-planner.js +107 -0
- package/dist/services/claude-planner.js.map +1 -0
- package/dist/services/docker-orchestrator.d.ts +11 -0
- package/dist/services/docker-orchestrator.d.ts.map +1 -0
- package/dist/services/docker-orchestrator.js +85 -0
- package/dist/services/docker-orchestrator.js.map +1 -0
- package/dist/services/executor-factory.d.ts +14 -0
- package/dist/services/executor-factory.d.ts.map +1 -0
- package/dist/services/executor-factory.js +14 -0
- package/dist/services/executor-factory.js.map +1 -0
- package/dist/services/git-manager.d.ts +36 -0
- package/dist/services/git-manager.d.ts.map +1 -0
- package/dist/services/git-manager.js +728 -0
- package/dist/services/git-manager.js.map +1 -0
- package/dist/services/index.d.ts +9 -0
- package/dist/services/index.d.ts.map +1 -0
- package/dist/services/index.js +9 -0
- package/dist/services/index.js.map +1 -0
- package/dist/services/job-manager.d.ts +30 -0
- package/dist/services/job-manager.d.ts.map +1 -0
- package/dist/services/job-manager.js +337 -0
- package/dist/services/job-manager.js.map +1 -0
- package/dist/services/openai-service.d.ts +14 -0
- package/dist/services/openai-service.d.ts.map +1 -0
- package/dist/services/openai-service.js +186 -0
- package/dist/services/openai-service.js.map +1 -0
- package/dist/services/pr-service.d.ts +31 -0
- package/dist/services/pr-service.d.ts.map +1 -0
- package/dist/services/pr-service.js +291 -0
- package/dist/services/pr-service.js.map +1 -0
- package/dist/services/repository-manager.d.ts +12 -0
- package/dist/services/repository-manager.d.ts.map +1 -0
- package/dist/services/repository-manager.js +101 -0
- package/dist/services/repository-manager.js.map +1 -0
- package/dist/services/task-executor.d.ts +20 -0
- package/dist/services/task-executor.d.ts.map +1 -0
- package/dist/services/task-executor.js +717 -0
- package/dist/services/task-executor.js.map +1 -0
- package/dist/types/non-interactive-config.d.ts +28 -0
- package/dist/types/non-interactive-config.d.ts.map +1 -0
- package/dist/types/non-interactive-config.js +2 -0
- package/dist/types/non-interactive-config.js.map +1 -0
- package/dist/web-server.d.ts +16 -0
- package/dist/web-server.d.ts.map +1 -0
- package/dist/web-server.js +488 -0
- package/dist/web-server.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { OpenAIService } from './openai-service.js';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { promises as fs, writeFileSync, unlinkSync } from 'fs';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
export class GitManager {
|
|
8
|
+
workingDir;
|
|
9
|
+
openaiService = null;
|
|
10
|
+
originalWorkingDir;
|
|
11
|
+
constructor(workingDir) {
|
|
12
|
+
this.workingDir = workingDir;
|
|
13
|
+
this.originalWorkingDir = workingDir;
|
|
14
|
+
}
|
|
15
|
+
getOpenAIService() {
|
|
16
|
+
if (!this.openaiService) {
|
|
17
|
+
this.openaiService = new OpenAIService();
|
|
18
|
+
}
|
|
19
|
+
return this.openaiService;
|
|
20
|
+
}
|
|
21
|
+
ensureGitRepo() {
|
|
22
|
+
try {
|
|
23
|
+
execSync('git rev-parse --git-dir', {
|
|
24
|
+
cwd: this.workingDir,
|
|
25
|
+
stdio: 'ignore'
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
throw new Error(`Directory is not a git repository: ${this.workingDir}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
isGitHubCliInstalled() {
|
|
33
|
+
try {
|
|
34
|
+
execSync('gh --version', { stdio: 'ignore' });
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
validateGitHubCliInstallation() {
|
|
42
|
+
if (!this.isGitHubCliInstalled()) {
|
|
43
|
+
throw new Error('GitHub CLI (gh) is not installed or not in PATH. Please install it first:\n' +
|
|
44
|
+
' macOS: brew install gh\n' +
|
|
45
|
+
' Ubuntu/Debian: sudo apt install gh\n' +
|
|
46
|
+
' Or visit: https://cli.github.com/');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
validateGitHubCliAuthentication() {
|
|
50
|
+
try {
|
|
51
|
+
execSync('gh auth status', { stdio: 'ignore' });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throw new Error('GitHub CLI is not authenticated. Please run "gh auth login" first.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async createBranch(branchName) {
|
|
58
|
+
this.ensureGitRepo();
|
|
59
|
+
try {
|
|
60
|
+
const escapedBranchName = branchName.replace(/"/g, '\\"');
|
|
61
|
+
execSync(`git checkout -b "${escapedBranchName}"`, {
|
|
62
|
+
cwd: this.workingDir,
|
|
63
|
+
stdio: 'pipe'
|
|
64
|
+
});
|
|
65
|
+
console.log(chalk.green(`✅ Created and switched to branch: ${branchName}`));
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
throw new Error(`Failed to create branch ${branchName}: ${error}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async commitChanges(message) {
|
|
72
|
+
this.ensureGitRepo();
|
|
73
|
+
try {
|
|
74
|
+
const status = execSync('git status --porcelain', {
|
|
75
|
+
cwd: this.workingDir,
|
|
76
|
+
encoding: 'utf8'
|
|
77
|
+
});
|
|
78
|
+
if (!status.trim()) {
|
|
79
|
+
console.log(chalk.yellow('⚠️ No changes to commit'));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
execSync('git add --all', {
|
|
83
|
+
cwd: this.workingDir,
|
|
84
|
+
stdio: 'pipe'
|
|
85
|
+
});
|
|
86
|
+
// Escape all shell special characters including backticks, quotes, and dollar signs
|
|
87
|
+
const escapedMessage = message
|
|
88
|
+
.replace(/\\/g, '\\\\') // Escape backslashes first
|
|
89
|
+
.replace(/"/g, '\\"') // Escape double quotes
|
|
90
|
+
.replace(/`/g, '\\`') // Escape backticks
|
|
91
|
+
.replace(/\$/g, '\\$') // Escape dollar signs
|
|
92
|
+
.replace(/!/g, '\\!'); // Escape exclamation marks
|
|
93
|
+
const commitMessage = `${escapedMessage}\n\nCo-authored-by: ivan-agent <ivan-agent@users.noreply.github.com>`;
|
|
94
|
+
execSync(`git commit -m "${commitMessage}"`, {
|
|
95
|
+
cwd: this.workingDir,
|
|
96
|
+
stdio: 'pipe'
|
|
97
|
+
});
|
|
98
|
+
console.log(chalk.green(`✅ Committed changes: ${message}`));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
throw new Error(`Failed to commit changes: ${error}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async createEmptyCommit(message) {
|
|
105
|
+
this.ensureGitRepo();
|
|
106
|
+
try {
|
|
107
|
+
// Escape all shell special characters including backticks, quotes, and dollar signs
|
|
108
|
+
const escapedMessage = message
|
|
109
|
+
.replace(/\\/g, '\\\\') // Escape backslashes first
|
|
110
|
+
.replace(/"/g, '\\"') // Escape double quotes
|
|
111
|
+
.replace(/`/g, '\\`') // Escape backticks
|
|
112
|
+
.replace(/\$/g, '\\$') // Escape dollar signs
|
|
113
|
+
.replace(/!/g, '\\!'); // Escape exclamation marks
|
|
114
|
+
const commitMessage = `${escapedMessage}\n\nCo-authored-by: ivan-agent <ivan-agent@users.noreply.github.com>`;
|
|
115
|
+
execSync(`git commit --allow-empty -m "${commitMessage}"`, {
|
|
116
|
+
cwd: this.workingDir,
|
|
117
|
+
stdio: 'pipe'
|
|
118
|
+
});
|
|
119
|
+
console.log(chalk.green(`✅ Created empty commit: ${message}`));
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
throw new Error(`Failed to create empty commit: ${error}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async pushBranch(branchName) {
|
|
126
|
+
this.ensureGitRepo();
|
|
127
|
+
try {
|
|
128
|
+
const escapedBranchName = branchName.replace(/"/g, '\\"');
|
|
129
|
+
execSync(`git push -u origin "${escapedBranchName}"`, {
|
|
130
|
+
cwd: this.workingDir,
|
|
131
|
+
stdio: 'pipe'
|
|
132
|
+
});
|
|
133
|
+
console.log(chalk.green(`✅ Pushed branch to origin: ${branchName}`));
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
throw new Error(`Failed to push branch ${branchName}: ${error}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async createPullRequest(title, body) {
|
|
140
|
+
this.ensureGitRepo();
|
|
141
|
+
// Add attribution to @ivan-agent in the PR body
|
|
142
|
+
const bodyWithAttribution = `${body}\n\n---\n*Co-authored with @ivan-agent*`;
|
|
143
|
+
// Ensure the final body doesn't exceed GitHub's limit (65536 characters)
|
|
144
|
+
const MAX_BODY_LENGTH = 65536;
|
|
145
|
+
let finalBody = bodyWithAttribution;
|
|
146
|
+
if (finalBody.length > MAX_BODY_LENGTH) {
|
|
147
|
+
// Truncate the original body to fit within the limit, accounting for the attribution
|
|
148
|
+
const attributionText = '\n\n---\n*Co-authored with @ivan-agent*';
|
|
149
|
+
const truncationText = '\n\n... (description truncated to fit GitHub limits)';
|
|
150
|
+
const maxOriginalBodyLength = MAX_BODY_LENGTH - attributionText.length - truncationText.length;
|
|
151
|
+
finalBody = body.substring(0, maxOriginalBodyLength) + truncationText + attributionText;
|
|
152
|
+
}
|
|
153
|
+
// Write PR body to a temporary file to avoid shell escaping issues
|
|
154
|
+
const tmpDir = os.tmpdir();
|
|
155
|
+
const tmpFile = path.join(tmpDir, `pr-body-${Date.now()}.md`);
|
|
156
|
+
writeFileSync(tmpFile, finalBody, 'utf8');
|
|
157
|
+
try {
|
|
158
|
+
const escapedTitle = title.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
159
|
+
// Create PR and optionally assign to ivan-agent (will fail silently if user doesn't have permissions)
|
|
160
|
+
const result = execSync(`gh pr create --draft --title "${escapedTitle}" --body-file "${tmpFile}" --assignee ivan-agent`, {
|
|
161
|
+
cwd: this.workingDir,
|
|
162
|
+
encoding: 'utf8',
|
|
163
|
+
stdio: 'pipe'
|
|
164
|
+
});
|
|
165
|
+
const prUrl = result.trim();
|
|
166
|
+
console.log(chalk.green(`✅ Created pull request: ${prUrl}`));
|
|
167
|
+
// Clean up temp file
|
|
168
|
+
try {
|
|
169
|
+
unlinkSync(tmpFile);
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// Ignore file deletion errors
|
|
173
|
+
}
|
|
174
|
+
// Generate and add specific review comment
|
|
175
|
+
await this.addReviewComment(prUrl);
|
|
176
|
+
return prUrl;
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// If assignee fails, try without it
|
|
180
|
+
try {
|
|
181
|
+
const escapedTitle = title.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
182
|
+
const result = execSync(`gh pr create --draft --title "${escapedTitle}" --body-file "${tmpFile}"`, {
|
|
183
|
+
cwd: this.workingDir,
|
|
184
|
+
encoding: 'utf8',
|
|
185
|
+
stdio: 'pipe'
|
|
186
|
+
});
|
|
187
|
+
const prUrl = result.trim();
|
|
188
|
+
console.log(chalk.green(`✅ Created pull request: ${prUrl}`));
|
|
189
|
+
// Generate and add specific review comment
|
|
190
|
+
await this.addReviewComment(prUrl);
|
|
191
|
+
return prUrl;
|
|
192
|
+
}
|
|
193
|
+
catch (fallbackError) {
|
|
194
|
+
throw new Error(`Failed to create pull request: ${fallbackError}`);
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
// Clean up temp file
|
|
198
|
+
try {
|
|
199
|
+
unlinkSync(tmpFile);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Ignore file deletion errors
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
getChangedFiles(from, to) {
|
|
208
|
+
this.ensureGitRepo();
|
|
209
|
+
try {
|
|
210
|
+
if (from && to) {
|
|
211
|
+
// Get files changed between two refs
|
|
212
|
+
const files = execSync(`git diff --name-only ${from} ${to}`, {
|
|
213
|
+
cwd: this.workingDir,
|
|
214
|
+
encoding: 'utf8'
|
|
215
|
+
});
|
|
216
|
+
return files.split('\n').filter(line => line.trim());
|
|
217
|
+
}
|
|
218
|
+
else if (from) {
|
|
219
|
+
// Get files changed from a specific ref to current
|
|
220
|
+
const files = execSync(`git diff --name-only ${from}`, {
|
|
221
|
+
cwd: this.workingDir,
|
|
222
|
+
encoding: 'utf8'
|
|
223
|
+
});
|
|
224
|
+
return files.split('\n').filter(line => line.trim());
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
// Default: Check both staged and unstaged changes, plus untracked files
|
|
228
|
+
const status = execSync('git status --porcelain', {
|
|
229
|
+
cwd: this.workingDir,
|
|
230
|
+
encoding: 'utf8'
|
|
231
|
+
});
|
|
232
|
+
if (!status.trim()) {
|
|
233
|
+
return [];
|
|
234
|
+
}
|
|
235
|
+
// Parse git status output to get file names
|
|
236
|
+
const files = status.trim().split('\n').map(line => {
|
|
237
|
+
// Remove status codes and get the file path
|
|
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
|
+
// Get diff between two refs
|
|
252
|
+
const diff = execSync(`git diff ${from} ${to}`, {
|
|
253
|
+
cwd: this.workingDir,
|
|
254
|
+
encoding: 'utf8'
|
|
255
|
+
});
|
|
256
|
+
return diff;
|
|
257
|
+
}
|
|
258
|
+
else if (from) {
|
|
259
|
+
// Get diff from a specific ref to current
|
|
260
|
+
const diff = execSync(`git diff ${from}`, {
|
|
261
|
+
cwd: this.workingDir,
|
|
262
|
+
encoding: 'utf8'
|
|
263
|
+
});
|
|
264
|
+
return diff;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
// Default: First, add all changes to staging area (without committing)
|
|
268
|
+
// This allows us to see all changes including untracked files
|
|
269
|
+
execSync('git add -A', {
|
|
270
|
+
cwd: this.workingDir,
|
|
271
|
+
stdio: 'pipe'
|
|
272
|
+
});
|
|
273
|
+
// Get diff of all staged changes
|
|
274
|
+
const diff = execSync('git diff --cached', {
|
|
275
|
+
cwd: this.workingDir,
|
|
276
|
+
encoding: 'utf8'
|
|
277
|
+
});
|
|
278
|
+
// Reset the staging area to leave files as they were
|
|
279
|
+
execSync('git reset', {
|
|
280
|
+
cwd: this.workingDir,
|
|
281
|
+
stdio: 'pipe'
|
|
282
|
+
});
|
|
283
|
+
return diff;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return '';
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
getCurrentBranch() {
|
|
291
|
+
this.ensureGitRepo();
|
|
292
|
+
try {
|
|
293
|
+
return execSync('git branch --show-current', {
|
|
294
|
+
cwd: this.workingDir,
|
|
295
|
+
encoding: 'utf8'
|
|
296
|
+
}).trim();
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return '';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
getMainBranch() {
|
|
303
|
+
this.ensureGitRepo();
|
|
304
|
+
try {
|
|
305
|
+
// Try to get the default branch from GitHub
|
|
306
|
+
const remoteInfo = execSync('gh repo view --json defaultBranchRef', {
|
|
307
|
+
cwd: this.workingDir,
|
|
308
|
+
encoding: 'utf8',
|
|
309
|
+
stdio: 'pipe'
|
|
310
|
+
});
|
|
311
|
+
const parsed = JSON.parse(remoteInfo);
|
|
312
|
+
if (parsed?.defaultBranchRef?.name) {
|
|
313
|
+
return parsed.defaultBranchRef.name;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// Fallback to checking common branch names
|
|
318
|
+
}
|
|
319
|
+
// Check if main exists
|
|
320
|
+
try {
|
|
321
|
+
execSync('git rev-parse --verify main', {
|
|
322
|
+
cwd: this.workingDir,
|
|
323
|
+
stdio: 'ignore'
|
|
324
|
+
});
|
|
325
|
+
return 'main';
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// Try master
|
|
329
|
+
try {
|
|
330
|
+
execSync('git rev-parse --verify master', {
|
|
331
|
+
cwd: this.workingDir,
|
|
332
|
+
stdio: 'ignore'
|
|
333
|
+
});
|
|
334
|
+
return 'master';
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return 'main'; // Default to main
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async cleanupAndSyncMain() {
|
|
342
|
+
// Always operate on the original directory for main branch operations
|
|
343
|
+
const workDir = this.originalWorkingDir;
|
|
344
|
+
try {
|
|
345
|
+
// Ensure git repo in original directory
|
|
346
|
+
execSync('git rev-parse --git-dir', {
|
|
347
|
+
cwd: workDir,
|
|
348
|
+
stdio: 'ignore'
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
throw new Error(`Directory is not a git repository: ${workDir}`);
|
|
353
|
+
}
|
|
354
|
+
try {
|
|
355
|
+
// Stash any uncommitted changes
|
|
356
|
+
const status = execSync('git status --porcelain', {
|
|
357
|
+
cwd: workDir,
|
|
358
|
+
encoding: 'utf8'
|
|
359
|
+
});
|
|
360
|
+
if (status.trim()) {
|
|
361
|
+
console.log(chalk.yellow('⚠️ Stashing uncommitted changes'));
|
|
362
|
+
execSync('git stash push -u -m "Ivan: stashing before cleanup"', {
|
|
363
|
+
cwd: workDir,
|
|
364
|
+
stdio: 'pipe'
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
// Remove any untracked files and directories
|
|
368
|
+
execSync('git clean -fd', {
|
|
369
|
+
cwd: workDir,
|
|
370
|
+
stdio: 'pipe'
|
|
371
|
+
});
|
|
372
|
+
// Switch to main branch
|
|
373
|
+
execSync('git checkout main', {
|
|
374
|
+
cwd: workDir,
|
|
375
|
+
stdio: 'pipe'
|
|
376
|
+
});
|
|
377
|
+
// Pull latest changes
|
|
378
|
+
execSync('git pull origin main', {
|
|
379
|
+
cwd: workDir,
|
|
380
|
+
stdio: 'pipe'
|
|
381
|
+
});
|
|
382
|
+
console.log(chalk.green('✅ Cleaned up and synced with main branch'));
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
throw new Error(`Failed to cleanup and sync main: ${error}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
generateBranchName(taskDescription) {
|
|
389
|
+
const sanitized = taskDescription
|
|
390
|
+
.toLowerCase()
|
|
391
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
392
|
+
.replace(/\s+/g, '-')
|
|
393
|
+
.substring(0, 50);
|
|
394
|
+
const timestamp = Date.now().toString().slice(-6);
|
|
395
|
+
return `ivan/${sanitized}-${timestamp}`;
|
|
396
|
+
}
|
|
397
|
+
async getPRInfo(prNumber) {
|
|
398
|
+
try {
|
|
399
|
+
const prJson = execSync(`gh pr view ${prNumber} --json headRefName,number,title,url`, {
|
|
400
|
+
cwd: this.workingDir,
|
|
401
|
+
encoding: 'utf8'
|
|
402
|
+
});
|
|
403
|
+
return JSON.parse(prJson);
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
throw new Error(`Failed to get PR info for #${prNumber}: ${error}`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
async addReviewComment(prUrl) {
|
|
410
|
+
try {
|
|
411
|
+
// Get the diff between main branch and current branch for the PR
|
|
412
|
+
const currentBranch = this.getCurrentBranch();
|
|
413
|
+
const mainBranch = this.getMainBranch();
|
|
414
|
+
// Get diff between main and current branch
|
|
415
|
+
const diff = execSync(`git diff ${mainBranch}...${currentBranch}`, {
|
|
416
|
+
cwd: this.workingDir,
|
|
417
|
+
encoding: 'utf8',
|
|
418
|
+
maxBuffer: 10 * 1024 * 1024 // 10MB buffer for large diffs
|
|
419
|
+
});
|
|
420
|
+
// Get list of changed files between main and current branch
|
|
421
|
+
const changedFiles = execSync(`git diff --name-only ${mainBranch}...${currentBranch}`, {
|
|
422
|
+
cwd: this.workingDir,
|
|
423
|
+
encoding: 'utf8'
|
|
424
|
+
}).trim().split('\n').filter(f => f.trim());
|
|
425
|
+
// Generate specific review instructions using OpenAI
|
|
426
|
+
const reviewInstructions = await this.generateReviewInstructions(diff, changedFiles);
|
|
427
|
+
// Add the review comment
|
|
428
|
+
const reviewComment = `@codex ${reviewInstructions}`;
|
|
429
|
+
execSync(`gh pr comment ${prUrl} --body "${reviewComment.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/`/g, '\\`').replace(/\$/g, '\\$')}"`, {
|
|
430
|
+
cwd: this.workingDir,
|
|
431
|
+
stdio: 'pipe'
|
|
432
|
+
});
|
|
433
|
+
console.log(chalk.green('✅ Added specific review request for @codex'));
|
|
434
|
+
}
|
|
435
|
+
catch (error) {
|
|
436
|
+
console.log(chalk.yellow(`⚠️ Could not add review comment: ${error}`));
|
|
437
|
+
// Fallback to generic review request
|
|
438
|
+
try {
|
|
439
|
+
execSync(`gh pr comment ${prUrl} --body "@codex please review the changes and verify the implementation meets requirements"`, {
|
|
440
|
+
cwd: this.workingDir,
|
|
441
|
+
stdio: 'pipe'
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
catch (fallbackError) {
|
|
445
|
+
console.log(chalk.yellow(`⚠️ Could not add fallback review comment: ${fallbackError}`));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async generateReviewInstructions(diff, changedFiles) {
|
|
450
|
+
try {
|
|
451
|
+
// Check if we have actual diff content
|
|
452
|
+
if (!diff || diff.trim().length === 0) {
|
|
453
|
+
console.log(chalk.yellow('⚠️ No diff found between branches for review instructions'));
|
|
454
|
+
return 'please review the changes in this PR and verify the implementation meets requirements';
|
|
455
|
+
}
|
|
456
|
+
const openaiService = this.getOpenAIService();
|
|
457
|
+
const client = await openaiService.getClient();
|
|
458
|
+
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.
|
|
459
|
+
|
|
460
|
+
Changed files:
|
|
461
|
+
${changedFiles.join('\n')}
|
|
462
|
+
|
|
463
|
+
Diff:
|
|
464
|
+
${diff.substring(0, 8000)}${diff.length > 8000 ? '\n... (diff truncated)' : ''}
|
|
465
|
+
|
|
466
|
+
Generate a brief (1-2 sentences) review request that:
|
|
467
|
+
1. Mentions the key changes or features implemented
|
|
468
|
+
2. Asks the reviewer to verify specific aspects of the implementation
|
|
469
|
+
3. Is conversational and clear
|
|
470
|
+
|
|
471
|
+
Example format: "please review the new task executor implementation and verify that error handling properly captures all edge cases"
|
|
472
|
+
|
|
473
|
+
Return ONLY the review request text, without any prefix like "Please review" since @codex will already be prepended.`;
|
|
474
|
+
const completion = await client.chat.completions.create({
|
|
475
|
+
model: 'gpt-4o-mini',
|
|
476
|
+
messages: [
|
|
477
|
+
{
|
|
478
|
+
role: 'system',
|
|
479
|
+
content: 'You are a helpful assistant that generates specific code review requests for new pull requests.'
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
role: 'user',
|
|
483
|
+
content: prompt
|
|
484
|
+
}
|
|
485
|
+
],
|
|
486
|
+
temperature: 0.3,
|
|
487
|
+
max_tokens: 150
|
|
488
|
+
});
|
|
489
|
+
const reviewRequest = completion.choices[0]?.message?.content?.trim();
|
|
490
|
+
if (!reviewRequest) {
|
|
491
|
+
return 'please review the changes and verify the implementation meets requirements';
|
|
492
|
+
}
|
|
493
|
+
return reviewRequest;
|
|
494
|
+
}
|
|
495
|
+
catch (error) {
|
|
496
|
+
console.error('Error generating review instructions:', error);
|
|
497
|
+
// Fallback to a generic message if OpenAI fails
|
|
498
|
+
return 'please review the changes and verify the implementation meets requirements';
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async createWorktree(branchName) {
|
|
502
|
+
this.ensureGitRepo();
|
|
503
|
+
try {
|
|
504
|
+
// Create worktree directory inside the repo's parent directory
|
|
505
|
+
// This ensures it has the same permissions as the main repo
|
|
506
|
+
const repoName = path.basename(this.originalWorkingDir);
|
|
507
|
+
const worktreeBasePath = path.join(path.dirname(this.originalWorkingDir), `.${repoName}-ivan-worktrees`);
|
|
508
|
+
const worktreePath = path.join(worktreeBasePath, branchName);
|
|
509
|
+
// Ensure the parent directory exists with proper permissions
|
|
510
|
+
await fs.mkdir(worktreeBasePath, { recursive: true, mode: 0o755 });
|
|
511
|
+
// Remove any existing worktree at this path (in case of previous failure)
|
|
512
|
+
try {
|
|
513
|
+
execSync(`git worktree remove --force "${worktreePath}"`, {
|
|
514
|
+
cwd: this.originalWorkingDir,
|
|
515
|
+
stdio: 'ignore'
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
// Ignore if worktree doesn't exist
|
|
520
|
+
}
|
|
521
|
+
// Clean up any stale worktree entries
|
|
522
|
+
execSync('git worktree prune', {
|
|
523
|
+
cwd: this.originalWorkingDir,
|
|
524
|
+
stdio: 'pipe'
|
|
525
|
+
});
|
|
526
|
+
// Create the worktree
|
|
527
|
+
const escapedBranchName = branchName.replace(/"/g, '\\"');
|
|
528
|
+
const escapedPath = worktreePath.replace(/"/g, '\\"');
|
|
529
|
+
// Check if branch already exists
|
|
530
|
+
let branchExists = false;
|
|
531
|
+
try {
|
|
532
|
+
execSync(`git rev-parse --verify "${escapedBranchName}"`, {
|
|
533
|
+
cwd: this.originalWorkingDir,
|
|
534
|
+
stdio: 'ignore'
|
|
535
|
+
});
|
|
536
|
+
branchExists = true;
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
branchExists = false;
|
|
540
|
+
}
|
|
541
|
+
// Try to create the worktree
|
|
542
|
+
try {
|
|
543
|
+
if (branchExists) {
|
|
544
|
+
// Branch exists, create worktree from it
|
|
545
|
+
console.log(chalk.gray(`Creating worktree from existing branch: ${branchName}`));
|
|
546
|
+
execSync(`git worktree add "${escapedPath}" "${escapedBranchName}"`, {
|
|
547
|
+
cwd: this.originalWorkingDir,
|
|
548
|
+
stdio: 'pipe'
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
// Branch doesn't exist, create new branch in worktree
|
|
553
|
+
console.log(chalk.gray(`Creating new branch in worktree: ${branchName}`));
|
|
554
|
+
execSync(`git worktree add -b "${escapedBranchName}" "${escapedPath}"`, {
|
|
555
|
+
cwd: this.originalWorkingDir,
|
|
556
|
+
stdio: 'pipe'
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
catch (worktreeError) {
|
|
561
|
+
// If worktree creation fails because it already exists, remove it and retry
|
|
562
|
+
const errorMessage = worktreeError instanceof Error ? worktreeError.message : String(worktreeError);
|
|
563
|
+
if (errorMessage.includes('already exists')) {
|
|
564
|
+
console.log(chalk.yellow('⚠️ Worktree already exists. Removing and recreating...'));
|
|
565
|
+
// Force remove the existing worktree
|
|
566
|
+
try {
|
|
567
|
+
execSync(`git worktree remove --force "${escapedPath}"`, {
|
|
568
|
+
cwd: this.originalWorkingDir,
|
|
569
|
+
stdio: 'pipe'
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
// Ignore removal errors
|
|
574
|
+
}
|
|
575
|
+
// Clean up any stale worktree entries
|
|
576
|
+
execSync('git worktree prune', {
|
|
577
|
+
cwd: this.originalWorkingDir,
|
|
578
|
+
stdio: 'pipe'
|
|
579
|
+
});
|
|
580
|
+
// Retry creating the worktree
|
|
581
|
+
if (branchExists) {
|
|
582
|
+
console.log(chalk.gray(`Recreating worktree from existing branch: ${branchName}`));
|
|
583
|
+
execSync(`git worktree add "${escapedPath}" "${escapedBranchName}"`, {
|
|
584
|
+
cwd: this.originalWorkingDir,
|
|
585
|
+
stdio: 'pipe'
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
console.log(chalk.gray(`Creating new branch in worktree: ${branchName}`));
|
|
590
|
+
execSync(`git worktree add -b "${escapedBranchName}" "${escapedPath}"`, {
|
|
591
|
+
cwd: this.originalWorkingDir,
|
|
592
|
+
stdio: 'pipe'
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
throw worktreeError;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
// Verify the worktree was created successfully
|
|
601
|
+
try {
|
|
602
|
+
await fs.access(worktreePath);
|
|
603
|
+
}
|
|
604
|
+
catch {
|
|
605
|
+
throw new Error(`Worktree was not created successfully at ${worktreePath}`);
|
|
606
|
+
}
|
|
607
|
+
// Set proper permissions - make sure the current user owns all files
|
|
608
|
+
if (process.platform !== 'win32') {
|
|
609
|
+
try {
|
|
610
|
+
// Use the same permissions as the original repo
|
|
611
|
+
const stats = await fs.stat(this.originalWorkingDir);
|
|
612
|
+
await fs.chmod(worktreePath, stats.mode);
|
|
613
|
+
// Make sure all files are readable and writable by the owner
|
|
614
|
+
execSync(`find "${escapedPath}" -type f -exec chmod u+rw {} \\;`, {
|
|
615
|
+
cwd: path.dirname(worktreePath),
|
|
616
|
+
stdio: 'ignore'
|
|
617
|
+
});
|
|
618
|
+
execSync(`find "${escapedPath}" -type d -exec chmod u+rwx {} \\;`, {
|
|
619
|
+
cwd: path.dirname(worktreePath),
|
|
620
|
+
stdio: 'ignore'
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
catch (permError) {
|
|
624
|
+
console.log(chalk.yellow(`⚠️ Could not set optimal permissions on worktree: ${permError}`));
|
|
625
|
+
// Try simpler chmod as fallback
|
|
626
|
+
try {
|
|
627
|
+
execSync(`chmod -R 755 "${escapedPath}"`, {
|
|
628
|
+
stdio: 'ignore'
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
// Ignore permission errors on systems where chmod doesn't work
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
// Copy git config from main repo to ensure commits work
|
|
637
|
+
try {
|
|
638
|
+
const userName = execSync('git config user.name || true', {
|
|
639
|
+
cwd: this.originalWorkingDir,
|
|
640
|
+
encoding: 'utf8'
|
|
641
|
+
}).trim();
|
|
642
|
+
const userEmail = execSync('git config user.email || true', {
|
|
643
|
+
cwd: this.originalWorkingDir,
|
|
644
|
+
encoding: 'utf8'
|
|
645
|
+
}).trim();
|
|
646
|
+
if (userName) {
|
|
647
|
+
execSync(`git config user.name "${userName}"`, {
|
|
648
|
+
cwd: worktreePath,
|
|
649
|
+
stdio: 'pipe'
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
if (userEmail) {
|
|
653
|
+
execSync(`git config user.email "${userEmail}"`, {
|
|
654
|
+
cwd: worktreePath,
|
|
655
|
+
stdio: 'pipe'
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
catch (configError) {
|
|
660
|
+
console.log(chalk.yellow(`⚠️ Could not copy git config to worktree: ${configError}`));
|
|
661
|
+
}
|
|
662
|
+
// Check if package.json exists and run npm install if it does
|
|
663
|
+
try {
|
|
664
|
+
const packageJsonPath = path.join(worktreePath, 'package.json');
|
|
665
|
+
await fs.access(packageJsonPath);
|
|
666
|
+
console.log(chalk.cyan('📦 Found package.json, installing dependencies...'));
|
|
667
|
+
execSync('npm install', {
|
|
668
|
+
cwd: worktreePath,
|
|
669
|
+
stdio: 'inherit'
|
|
670
|
+
});
|
|
671
|
+
console.log(chalk.green('✅ Dependencies installed successfully'));
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
// No package.json or npm install failed, continue without error
|
|
675
|
+
console.log(chalk.gray('ℹ️ No package.json found or npm install not needed'));
|
|
676
|
+
}
|
|
677
|
+
console.log(chalk.green(`✅ Created worktree at: ${worktreePath}`));
|
|
678
|
+
console.log(chalk.gray('You can continue working in your main repository while Ivan works here'));
|
|
679
|
+
return worktreePath;
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
throw new Error(`Failed to create worktree for branch ${branchName}: ${error}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async removeWorktree(branchName) {
|
|
686
|
+
try {
|
|
687
|
+
const repoName = path.basename(this.originalWorkingDir);
|
|
688
|
+
const worktreeBasePath = path.join(path.dirname(this.originalWorkingDir), `.${repoName}-ivan-worktrees`);
|
|
689
|
+
const worktreePath = path.join(worktreeBasePath, branchName);
|
|
690
|
+
const escapedPath = worktreePath.replace(/"/g, '\\"');
|
|
691
|
+
// Remove the worktree
|
|
692
|
+
execSync(`git worktree remove --force "${escapedPath}"`, {
|
|
693
|
+
cwd: this.originalWorkingDir,
|
|
694
|
+
stdio: 'pipe'
|
|
695
|
+
});
|
|
696
|
+
// Prune worktree list
|
|
697
|
+
execSync('git worktree prune', {
|
|
698
|
+
cwd: this.originalWorkingDir,
|
|
699
|
+
stdio: 'pipe'
|
|
700
|
+
});
|
|
701
|
+
// Try to remove the base directory if it's empty
|
|
702
|
+
try {
|
|
703
|
+
const files = await fs.readdir(worktreeBasePath);
|
|
704
|
+
if (files.length === 0) {
|
|
705
|
+
await fs.rmdir(worktreeBasePath);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
catch {
|
|
709
|
+
// Ignore errors when cleaning up directories
|
|
710
|
+
}
|
|
711
|
+
console.log(chalk.green(`✅ Removed worktree for branch: ${branchName}`));
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
console.log(chalk.yellow(`⚠️ Could not remove worktree: ${error}`));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
switchToWorktree(worktreePath) {
|
|
718
|
+
this.workingDir = worktreePath;
|
|
719
|
+
}
|
|
720
|
+
switchToOriginalDir() {
|
|
721
|
+
this.workingDir = this.originalWorkingDir;
|
|
722
|
+
}
|
|
723
|
+
getWorktreePath(branchName) {
|
|
724
|
+
const repoName = path.basename(this.originalWorkingDir);
|
|
725
|
+
return path.join(path.dirname(this.originalWorkingDir), `.${repoName}-ivan-worktrees`, branchName);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
//# sourceMappingURL=git-manager.js.map
|