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