@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.
Files changed (150) hide show
  1. package/README.md +412 -0
  2. package/dist/agent.d.ts +11 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/agent.js +48 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/config/config.d.ts +20 -0
  7. package/dist/config/config.d.ts.map +1 -0
  8. package/dist/config/config.js +187 -0
  9. package/dist/config/config.js.map +1 -0
  10. package/dist/config.d.ts +46 -0
  11. package/dist/config.d.ts.map +1 -0
  12. package/dist/config.js +414 -0
  13. package/dist/config.js.map +1 -0
  14. package/dist/database/database.d.ts +12 -0
  15. package/dist/database/database.d.ts.map +1 -0
  16. package/dist/database/database.js +45 -0
  17. package/dist/database/database.js.map +1 -0
  18. package/dist/database/migration.d.ts +11 -0
  19. package/dist/database/migration.d.ts.map +1 -0
  20. package/dist/database/migration.js +64 -0
  21. package/dist/database/migration.js.map +1 -0
  22. package/dist/database/migrations/001_create_jobs_table.d.ts +3 -0
  23. package/dist/database/migrations/001_create_jobs_table.d.ts.map +1 -0
  24. package/dist/database/migrations/001_create_jobs_table.js +14 -0
  25. package/dist/database/migrations/001_create_jobs_table.js.map +1 -0
  26. package/dist/database/migrations/001_initial_schema.d.ts +3 -0
  27. package/dist/database/migrations/001_initial_schema.d.ts.map +1 -0
  28. package/dist/database/migrations/001_initial_schema.js +66 -0
  29. package/dist/database/migrations/001_initial_schema.js.map +1 -0
  30. package/dist/database/migrations/002_create_tasks_table.d.ts +3 -0
  31. package/dist/database/migrations/002_create_tasks_table.d.ts.map +1 -0
  32. package/dist/database/migrations/002_create_tasks_table.js +16 -0
  33. package/dist/database/migrations/002_create_tasks_table.js.map +1 -0
  34. package/dist/database/migrations/003_add_log_to_tasks.d.ts +3 -0
  35. package/dist/database/migrations/003_add_log_to_tasks.d.ts.map +1 -0
  36. package/dist/database/migrations/003_add_log_to_tasks.js +11 -0
  37. package/dist/database/migrations/003_add_log_to_tasks.js.map +1 -0
  38. package/dist/database/migrations/004_add_branch_to_tasks.d.ts +3 -0
  39. package/dist/database/migrations/004_add_branch_to_tasks.d.ts.map +1 -0
  40. package/dist/database/migrations/004_add_branch_to_tasks.js +11 -0
  41. package/dist/database/migrations/004_add_branch_to_tasks.js.map +1 -0
  42. package/dist/database/migrations/005_add_type_to_tasks.d.ts +3 -0
  43. package/dist/database/migrations/005_add_type_to_tasks.d.ts.map +1 -0
  44. package/dist/database/migrations/005_add_type_to_tasks.js +11 -0
  45. package/dist/database/migrations/005_add_type_to_tasks.js.map +1 -0
  46. package/dist/database/migrations/006_add_comment_url_and_commit_to_tasks.d.ts +3 -0
  47. package/dist/database/migrations/006_add_comment_url_and_commit_to_tasks.d.ts.map +1 -0
  48. package/dist/database/migrations/006_add_comment_url_and_commit_to_tasks.js +15 -0
  49. package/dist/database/migrations/006_add_comment_url_and_commit_to_tasks.js.map +1 -0
  50. package/dist/database/migrations/006_add_comment_url_to_tasks.d.ts +3 -0
  51. package/dist/database/migrations/006_add_comment_url_to_tasks.d.ts.map +1 -0
  52. package/dist/database/migrations/006_add_comment_url_to_tasks.js +14 -0
  53. package/dist/database/migrations/006_add_comment_url_to_tasks.js.map +1 -0
  54. package/dist/database/migrations/007_add_commit_to_tasks.d.ts +3 -0
  55. package/dist/database/migrations/007_add_commit_to_tasks.d.ts.map +1 -0
  56. package/dist/database/migrations/007_add_commit_to_tasks.js +13 -0
  57. package/dist/database/migrations/007_add_commit_to_tasks.js.map +1 -0
  58. package/dist/database/migrations/008_add_lint_and_test_task_type.d.ts +3 -0
  59. package/dist/database/migrations/008_add_lint_and_test_task_type.d.ts.map +1 -0
  60. package/dist/database/migrations/008_add_lint_and_test_task_type.js +42 -0
  61. package/dist/database/migrations/008_add_lint_and_test_task_type.js.map +1 -0
  62. package/dist/database/migrations/index.d.ts +3 -0
  63. package/dist/database/migrations/index.d.ts.map +1 -0
  64. package/dist/database/migrations/index.js +19 -0
  65. package/dist/database/migrations/index.js.map +1 -0
  66. package/dist/database/types.d.ts +34 -0
  67. package/dist/database/types.d.ts.map +1 -0
  68. package/dist/database/types.js +2 -0
  69. package/dist/database/types.js.map +1 -0
  70. package/dist/database.d.ts +13 -0
  71. package/dist/database.d.ts.map +1 -0
  72. package/dist/database.js +32 -0
  73. package/dist/database.js.map +1 -0
  74. package/dist/index.d.ts +3 -0
  75. package/dist/index.d.ts.map +1 -0
  76. package/dist/index.js +285 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/scripts/task-executor.d.ts +3 -0
  79. package/dist/scripts/task-executor.d.ts.map +1 -0
  80. package/dist/scripts/task-executor.js +139 -0
  81. package/dist/scripts/task-executor.js.map +1 -0
  82. package/dist/scripts/task-planner.d.ts +3 -0
  83. package/dist/scripts/task-planner.d.ts.map +1 -0
  84. package/dist/scripts/task-planner.js +81 -0
  85. package/dist/scripts/task-planner.js.map +1 -0
  86. package/dist/services/address-executor.d.ts +13 -0
  87. package/dist/services/address-executor.d.ts.map +1 -0
  88. package/dist/services/address-executor.js +202 -0
  89. package/dist/services/address-executor.js.map +1 -0
  90. package/dist/services/address-task-executor.d.ts +19 -0
  91. package/dist/services/address-task-executor.d.ts.map +1 -0
  92. package/dist/services/address-task-executor.js +736 -0
  93. package/dist/services/address-task-executor.js.map +1 -0
  94. package/dist/services/claude-cli-executor.d.ts +14 -0
  95. package/dist/services/claude-cli-executor.d.ts.map +1 -0
  96. package/dist/services/claude-cli-executor.js +241 -0
  97. package/dist/services/claude-cli-executor.js.map +1 -0
  98. package/dist/services/claude-executor.d.ts +14 -0
  99. package/dist/services/claude-executor.d.ts.map +1 -0
  100. package/dist/services/claude-executor.js +274 -0
  101. package/dist/services/claude-executor.js.map +1 -0
  102. package/dist/services/claude-planner.d.ts +15 -0
  103. package/dist/services/claude-planner.d.ts.map +1 -0
  104. package/dist/services/claude-planner.js +107 -0
  105. package/dist/services/claude-planner.js.map +1 -0
  106. package/dist/services/docker-orchestrator.d.ts +11 -0
  107. package/dist/services/docker-orchestrator.d.ts.map +1 -0
  108. package/dist/services/docker-orchestrator.js +85 -0
  109. package/dist/services/docker-orchestrator.js.map +1 -0
  110. package/dist/services/executor-factory.d.ts +14 -0
  111. package/dist/services/executor-factory.d.ts.map +1 -0
  112. package/dist/services/executor-factory.js +14 -0
  113. package/dist/services/executor-factory.js.map +1 -0
  114. package/dist/services/git-manager.d.ts +36 -0
  115. package/dist/services/git-manager.d.ts.map +1 -0
  116. package/dist/services/git-manager.js +728 -0
  117. package/dist/services/git-manager.js.map +1 -0
  118. package/dist/services/index.d.ts +9 -0
  119. package/dist/services/index.d.ts.map +1 -0
  120. package/dist/services/index.js +9 -0
  121. package/dist/services/index.js.map +1 -0
  122. package/dist/services/job-manager.d.ts +30 -0
  123. package/dist/services/job-manager.d.ts.map +1 -0
  124. package/dist/services/job-manager.js +337 -0
  125. package/dist/services/job-manager.js.map +1 -0
  126. package/dist/services/openai-service.d.ts +14 -0
  127. package/dist/services/openai-service.d.ts.map +1 -0
  128. package/dist/services/openai-service.js +186 -0
  129. package/dist/services/openai-service.js.map +1 -0
  130. package/dist/services/pr-service.d.ts +31 -0
  131. package/dist/services/pr-service.d.ts.map +1 -0
  132. package/dist/services/pr-service.js +291 -0
  133. package/dist/services/pr-service.js.map +1 -0
  134. package/dist/services/repository-manager.d.ts +12 -0
  135. package/dist/services/repository-manager.d.ts.map +1 -0
  136. package/dist/services/repository-manager.js +101 -0
  137. package/dist/services/repository-manager.js.map +1 -0
  138. package/dist/services/task-executor.d.ts +20 -0
  139. package/dist/services/task-executor.d.ts.map +1 -0
  140. package/dist/services/task-executor.js +717 -0
  141. package/dist/services/task-executor.js.map +1 -0
  142. package/dist/types/non-interactive-config.d.ts +28 -0
  143. package/dist/types/non-interactive-config.d.ts.map +1 -0
  144. package/dist/types/non-interactive-config.js +2 -0
  145. package/dist/types/non-interactive-config.js.map +1 -0
  146. package/dist/web-server.d.ts +16 -0
  147. package/dist/web-server.d.ts.map +1 -0
  148. package/dist/web-server.js +488 -0
  149. package/dist/web-server.js.map +1 -0
  150. 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