episoda 0.2.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 (111) hide show
  1. package/dist/commands/auth.d.ts +22 -0
  2. package/dist/commands/auth.d.ts.map +1 -0
  3. package/dist/commands/auth.js +384 -0
  4. package/dist/commands/auth.js.map +1 -0
  5. package/dist/commands/dev.d.ts +20 -0
  6. package/dist/commands/dev.d.ts.map +1 -0
  7. package/dist/commands/dev.js +305 -0
  8. package/dist/commands/dev.js.map +1 -0
  9. package/dist/commands/status.d.ts +9 -0
  10. package/dist/commands/status.d.ts.map +1 -0
  11. package/dist/commands/status.js +75 -0
  12. package/dist/commands/status.js.map +1 -0
  13. package/dist/commands/stop.d.ts +17 -0
  14. package/dist/commands/stop.d.ts.map +1 -0
  15. package/dist/commands/stop.js +81 -0
  16. package/dist/commands/stop.js.map +1 -0
  17. package/dist/core/auth.d.ts +26 -0
  18. package/dist/core/auth.d.ts.map +1 -0
  19. package/dist/core/auth.js +113 -0
  20. package/dist/core/auth.js.map +1 -0
  21. package/dist/core/command-protocol.d.ts +262 -0
  22. package/dist/core/command-protocol.d.ts.map +1 -0
  23. package/dist/core/command-protocol.js +13 -0
  24. package/dist/core/command-protocol.js.map +1 -0
  25. package/dist/core/connection-manager.d.ts +58 -0
  26. package/dist/core/connection-manager.d.ts.map +1 -0
  27. package/dist/core/connection-manager.js +215 -0
  28. package/dist/core/connection-manager.js.map +1 -0
  29. package/dist/core/errors.d.ts +18 -0
  30. package/dist/core/errors.d.ts.map +1 -0
  31. package/dist/core/errors.js +55 -0
  32. package/dist/core/errors.js.map +1 -0
  33. package/dist/core/git-executor.d.ts +157 -0
  34. package/dist/core/git-executor.d.ts.map +1 -0
  35. package/dist/core/git-executor.js +1605 -0
  36. package/dist/core/git-executor.js.map +1 -0
  37. package/dist/core/git-parser.d.ts +40 -0
  38. package/dist/core/git-parser.d.ts.map +1 -0
  39. package/dist/core/git-parser.js +194 -0
  40. package/dist/core/git-parser.js.map +1 -0
  41. package/dist/core/git-validator.d.ts +42 -0
  42. package/dist/core/git-validator.d.ts.map +1 -0
  43. package/dist/core/git-validator.js +102 -0
  44. package/dist/core/git-validator.js.map +1 -0
  45. package/dist/core/index.d.ts +17 -0
  46. package/dist/core/index.d.ts.map +1 -0
  47. package/dist/core/index.js +41 -0
  48. package/dist/core/index.js.map +1 -0
  49. package/dist/core/version.d.ts +9 -0
  50. package/dist/core/version.d.ts.map +1 -0
  51. package/dist/core/version.js +19 -0
  52. package/dist/core/version.js.map +1 -0
  53. package/dist/core/websocket-client.d.ts +122 -0
  54. package/dist/core/websocket-client.d.ts.map +1 -0
  55. package/dist/core/websocket-client.js +438 -0
  56. package/dist/core/websocket-client.js.map +1 -0
  57. package/dist/daemon/daemon-manager.d.ts +71 -0
  58. package/dist/daemon/daemon-manager.d.ts.map +1 -0
  59. package/dist/daemon/daemon-manager.js +289 -0
  60. package/dist/daemon/daemon-manager.js.map +1 -0
  61. package/dist/daemon/daemon-process.d.ts +13 -0
  62. package/dist/daemon/daemon-process.d.ts.map +1 -0
  63. package/dist/daemon/daemon-process.js +608 -0
  64. package/dist/daemon/daemon-process.js.map +1 -0
  65. package/dist/daemon/machine-id.d.ts +36 -0
  66. package/dist/daemon/machine-id.d.ts.map +1 -0
  67. package/dist/daemon/machine-id.js +195 -0
  68. package/dist/daemon/machine-id.js.map +1 -0
  69. package/dist/daemon/project-tracker.d.ts +92 -0
  70. package/dist/daemon/project-tracker.d.ts.map +1 -0
  71. package/dist/daemon/project-tracker.js +259 -0
  72. package/dist/daemon/project-tracker.js.map +1 -0
  73. package/dist/dev-wrapper.d.ts +88 -0
  74. package/dist/dev-wrapper.d.ts.map +1 -0
  75. package/dist/dev-wrapper.js +288 -0
  76. package/dist/dev-wrapper.js.map +1 -0
  77. package/dist/framework-detector.d.ts +29 -0
  78. package/dist/framework-detector.d.ts.map +1 -0
  79. package/dist/framework-detector.js +276 -0
  80. package/dist/framework-detector.js.map +1 -0
  81. package/dist/git-helpers/git-credential-helper.d.ts +29 -0
  82. package/dist/git-helpers/git-credential-helper.d.ts.map +1 -0
  83. package/dist/git-helpers/git-credential-helper.js +349 -0
  84. package/dist/git-helpers/git-credential-helper.js.map +1 -0
  85. package/dist/hooks/post-checkout +296 -0
  86. package/dist/hooks/pre-commit +139 -0
  87. package/dist/index.d.ts +8 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +102 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/ipc/ipc-client.d.ts +95 -0
  92. package/dist/ipc/ipc-client.d.ts.map +1 -0
  93. package/dist/ipc/ipc-client.js +204 -0
  94. package/dist/ipc/ipc-client.js.map +1 -0
  95. package/dist/ipc/ipc-server.d.ts +55 -0
  96. package/dist/ipc/ipc-server.d.ts.map +1 -0
  97. package/dist/ipc/ipc-server.js +177 -0
  98. package/dist/ipc/ipc-server.js.map +1 -0
  99. package/dist/output.d.ts +48 -0
  100. package/dist/output.d.ts.map +1 -0
  101. package/dist/output.js +129 -0
  102. package/dist/output.js.map +1 -0
  103. package/dist/utils/port-check.d.ts +15 -0
  104. package/dist/utils/port-check.d.ts.map +1 -0
  105. package/dist/utils/port-check.js +79 -0
  106. package/dist/utils/port-check.js.map +1 -0
  107. package/dist/utils/update-checker.d.ts +23 -0
  108. package/dist/utils/update-checker.d.ts.map +1 -0
  109. package/dist/utils/update-checker.js +95 -0
  110. package/dist/utils/update-checker.js.map +1 -0
  111. package/package.json +51 -0
@@ -0,0 +1,1605 @@
1
+ "use strict";
2
+ /**
3
+ * Git Command Executor
4
+ *
5
+ * Executes git commands with error handling and returns structured results.
6
+ * Interface-agnostic: Returns structured data, not formatted strings.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.GitExecutor = void 0;
43
+ const child_process_1 = require("child_process");
44
+ const util_1 = require("util");
45
+ const git_validator_1 = require("./git-validator");
46
+ const git_parser_1 = require("./git-parser");
47
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
48
+ /**
49
+ * Executes git commands with error handling
50
+ *
51
+ * DESIGN PRINCIPLES:
52
+ * - Interface-agnostic: Returns structured data, not formatted strings
53
+ * - No console.log: Let the interface handle output
54
+ * - Error codes: Return error codes, not messages
55
+ * - Reusable: Can be used by CLI, MCP, or any other interface
56
+ */
57
+ class GitExecutor {
58
+ /**
59
+ * Execute a git command
60
+ * @param command - The git command to execute
61
+ * @param options - Execution options (timeout, cwd, etc.)
62
+ * @returns Structured result with success/error details
63
+ */
64
+ async execute(command, options) {
65
+ try {
66
+ // Validate git is installed
67
+ const gitInstalled = await this.validateGitInstalled();
68
+ if (!gitInstalled) {
69
+ return {
70
+ success: false,
71
+ error: 'GIT_NOT_INSTALLED'
72
+ };
73
+ }
74
+ // Determine working directory
75
+ const cwd = options?.cwd || process.cwd();
76
+ // Validate this is a git repository (except for init-like commands)
77
+ const isGitRepo = await this.isGitRepository(cwd);
78
+ if (!isGitRepo) {
79
+ return {
80
+ success: false,
81
+ error: 'NOT_GIT_REPO'
82
+ };
83
+ }
84
+ // Route to appropriate handler based on action
85
+ switch (command.action) {
86
+ case 'checkout':
87
+ return await this.executeCheckout(command, cwd, options);
88
+ case 'create_branch':
89
+ return await this.executeCreateBranch(command, cwd, options);
90
+ case 'commit':
91
+ return await this.executeCommit(command, cwd, options);
92
+ case 'push':
93
+ return await this.executePush(command, cwd, options);
94
+ case 'status':
95
+ return await this.executeStatus(cwd, options);
96
+ case 'pull':
97
+ return await this.executePull(command, cwd, options);
98
+ case 'delete_branch':
99
+ return await this.executeDeleteBranch(command, cwd, options);
100
+ // EP597: Read operations for production local dev mode
101
+ case 'branch_exists':
102
+ return await this.executeBranchExists(command, cwd, options);
103
+ case 'branch_has_commits':
104
+ return await this.executeBranchHasCommits(command, cwd, options);
105
+ // EP598: Main branch check for production
106
+ case 'main_branch_check':
107
+ return await this.executeMainBranchCheck(cwd, options);
108
+ // EP599: Get commits for branch
109
+ case 'get_commits':
110
+ return await this.executeGetCommits(command, cwd, options);
111
+ // EP599: Advanced operations for move-to-module and discard-main-changes
112
+ case 'stash':
113
+ return await this.executeStash(command, cwd, options);
114
+ case 'reset':
115
+ return await this.executeReset(command, cwd, options);
116
+ case 'merge':
117
+ return await this.executeMerge(command, cwd, options);
118
+ case 'cherry_pick':
119
+ return await this.executeCherryPick(command, cwd, options);
120
+ case 'clean':
121
+ return await this.executeClean(command, cwd, options);
122
+ case 'add':
123
+ return await this.executeAdd(command, cwd, options);
124
+ case 'fetch':
125
+ return await this.executeFetch(command, cwd, options);
126
+ // EP599-3: Composite operations
127
+ case 'move_to_module':
128
+ return await this.executeMoveToModule(command, cwd, options);
129
+ case 'discard_main_changes':
130
+ return await this.executeDiscardMainChanges(cwd, options);
131
+ // EP523: Branch sync operations
132
+ case 'sync_status':
133
+ return await this.executeSyncStatus(command, cwd, options);
134
+ case 'sync_main':
135
+ return await this.executeSyncMain(cwd, options);
136
+ case 'rebase_branch':
137
+ return await this.executeRebaseBranch(command, cwd, options);
138
+ case 'rebase_abort':
139
+ return await this.executeRebaseAbort(cwd, options);
140
+ case 'rebase_continue':
141
+ return await this.executeRebaseContinue(cwd, options);
142
+ case 'rebase_status':
143
+ return await this.executeRebaseStatus(cwd, options);
144
+ default:
145
+ return {
146
+ success: false,
147
+ error: 'UNKNOWN_ERROR',
148
+ output: 'Unknown command action'
149
+ };
150
+ }
151
+ }
152
+ catch (error) {
153
+ return {
154
+ success: false,
155
+ error: 'UNKNOWN_ERROR',
156
+ output: error instanceof Error ? error.message : 'Unknown error occurred'
157
+ };
158
+ }
159
+ }
160
+ /**
161
+ * Execute checkout command
162
+ */
163
+ async executeCheckout(command, cwd, options) {
164
+ // Validate branch name
165
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
166
+ if (!validation.valid) {
167
+ return {
168
+ success: false,
169
+ error: validation.error || 'UNKNOWN_ERROR'
170
+ };
171
+ }
172
+ // Check for uncommitted changes first
173
+ const statusResult = await this.executeStatus(cwd, options);
174
+ if (statusResult.success && statusResult.details?.uncommittedFiles?.length) {
175
+ return {
176
+ success: false,
177
+ error: 'UNCOMMITTED_CHANGES',
178
+ details: {
179
+ uncommittedFiles: statusResult.details.uncommittedFiles
180
+ }
181
+ };
182
+ }
183
+ // Build command
184
+ const args = ['checkout'];
185
+ if (command.create) {
186
+ args.push('-b');
187
+ }
188
+ args.push(command.branch);
189
+ return await this.runGitCommand(args, cwd, options);
190
+ }
191
+ /**
192
+ * Execute create_branch command
193
+ */
194
+ async executeCreateBranch(command, cwd, options) {
195
+ // Validate branch name
196
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
197
+ if (!validation.valid) {
198
+ return {
199
+ success: false,
200
+ error: validation.error || 'UNKNOWN_ERROR'
201
+ };
202
+ }
203
+ // Validate source branch if provided
204
+ if (command.from) {
205
+ const fromValidation = (0, git_validator_1.validateBranchName)(command.from);
206
+ if (!fromValidation.valid) {
207
+ return {
208
+ success: false,
209
+ error: fromValidation.error || 'UNKNOWN_ERROR'
210
+ };
211
+ }
212
+ }
213
+ // Build command - use checkout -b to create AND checkout the branch
214
+ // This ensures the user is on the new branch immediately
215
+ const args = ['checkout', '-b', command.branch];
216
+ if (command.from) {
217
+ args.push(command.from);
218
+ }
219
+ return await this.runGitCommand(args, cwd, options);
220
+ }
221
+ /**
222
+ * Execute commit command
223
+ */
224
+ async executeCommit(command, cwd, options) {
225
+ // Validate commit message
226
+ const validation = (0, git_validator_1.validateCommitMessage)(command.message);
227
+ if (!validation.valid) {
228
+ return {
229
+ success: false,
230
+ error: validation.error || 'UNKNOWN_ERROR'
231
+ };
232
+ }
233
+ // Validate file paths if provided
234
+ if (command.files) {
235
+ const fileValidation = (0, git_validator_1.validateFilePaths)(command.files);
236
+ if (!fileValidation.valid) {
237
+ return {
238
+ success: false,
239
+ error: fileValidation.error || 'UNKNOWN_ERROR'
240
+ };
241
+ }
242
+ // Stage specific files first
243
+ for (const file of command.files) {
244
+ const addResult = await this.runGitCommand(['add', file], cwd, options);
245
+ if (!addResult.success) {
246
+ return addResult;
247
+ }
248
+ }
249
+ }
250
+ else {
251
+ // Stage all changes
252
+ const addResult = await this.runGitCommand(['add', '-A'], cwd, options);
253
+ if (!addResult.success) {
254
+ return addResult;
255
+ }
256
+ }
257
+ // Execute commit
258
+ const args = ['commit', '-m', command.message];
259
+ return await this.runGitCommand(args, cwd, options);
260
+ }
261
+ /**
262
+ * Execute push command
263
+ * EP769: Added force parameter for pushing rebased branches
264
+ */
265
+ async executePush(command, cwd, options) {
266
+ // Validate branch name
267
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
268
+ if (!validation.valid) {
269
+ return {
270
+ success: false,
271
+ error: validation.error || 'UNKNOWN_ERROR'
272
+ };
273
+ }
274
+ // Build command
275
+ const args = ['push'];
276
+ // EP769: Add --force flag for rebased branches
277
+ if (command.force) {
278
+ args.push('--force');
279
+ }
280
+ if (command.setUpstream) {
281
+ args.push('-u', 'origin', command.branch);
282
+ }
283
+ else {
284
+ args.push('origin', command.branch);
285
+ }
286
+ // Configure git credential helper for GitHub token if provided
287
+ const env = { ...process.env };
288
+ if (options?.githubToken) {
289
+ env.GIT_ASKPASS = 'echo';
290
+ env.GIT_USERNAME = 'x-access-token';
291
+ env.GIT_PASSWORD = options.githubToken;
292
+ }
293
+ return await this.runGitCommand(args, cwd, { ...options, env });
294
+ }
295
+ /**
296
+ * Execute status command
297
+ */
298
+ async executeStatus(cwd, options) {
299
+ const result = await this.runGitCommand(['status', '--porcelain', '-b'], cwd, options);
300
+ if (result.success && result.output) {
301
+ const statusInfo = (0, git_parser_1.parseGitStatus)(result.output);
302
+ return {
303
+ success: true,
304
+ output: result.output,
305
+ details: {
306
+ uncommittedFiles: statusInfo.uncommittedFiles,
307
+ branchName: statusInfo.currentBranch
308
+ }
309
+ };
310
+ }
311
+ return result;
312
+ }
313
+ /**
314
+ * Execute pull command
315
+ */
316
+ async executePull(command, cwd, options) {
317
+ // Validate branch name if provided
318
+ if (command.branch) {
319
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
320
+ if (!validation.valid) {
321
+ return {
322
+ success: false,
323
+ error: validation.error || 'UNKNOWN_ERROR'
324
+ };
325
+ }
326
+ }
327
+ // Check for uncommitted changes first
328
+ const statusResult = await this.executeStatus(cwd, options);
329
+ if (statusResult.success && statusResult.details?.uncommittedFiles?.length) {
330
+ return {
331
+ success: false,
332
+ error: 'UNCOMMITTED_CHANGES',
333
+ details: {
334
+ uncommittedFiles: statusResult.details.uncommittedFiles
335
+ }
336
+ };
337
+ }
338
+ // Build command
339
+ const args = ['pull'];
340
+ if (command.branch) {
341
+ args.push('origin', command.branch);
342
+ }
343
+ const result = await this.runGitCommand(args, cwd, options);
344
+ // Check for merge conflicts
345
+ if (!result.success && result.output) {
346
+ const conflicts = (0, git_parser_1.parseMergeConflicts)(result.output);
347
+ if (conflicts.length > 0) {
348
+ return {
349
+ success: false,
350
+ error: 'MERGE_CONFLICT',
351
+ output: result.output,
352
+ details: {
353
+ conflictingFiles: conflicts
354
+ }
355
+ };
356
+ }
357
+ }
358
+ return result;
359
+ }
360
+ /**
361
+ * Execute delete_branch command
362
+ */
363
+ async executeDeleteBranch(command, cwd, options) {
364
+ // Validate branch name
365
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
366
+ if (!validation.valid) {
367
+ return {
368
+ success: false,
369
+ error: validation.error || 'UNKNOWN_ERROR'
370
+ };
371
+ }
372
+ // Build command
373
+ const args = ['branch'];
374
+ args.push(command.force ? '-D' : '-d');
375
+ args.push(command.branch);
376
+ return await this.runGitCommand(args, cwd, options);
377
+ }
378
+ /**
379
+ * EP597: Execute branch_exists command
380
+ * Checks if a branch exists locally and/or remotely
381
+ */
382
+ async executeBranchExists(command, cwd, options) {
383
+ // Validate branch name
384
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
385
+ if (!validation.valid) {
386
+ return {
387
+ success: false,
388
+ error: validation.error || 'UNKNOWN_ERROR'
389
+ };
390
+ }
391
+ try {
392
+ let isLocal = false;
393
+ let isRemote = false;
394
+ // Check local branches
395
+ try {
396
+ const { stdout: localBranches } = await execAsync('git branch --list', { cwd, timeout: options?.timeout || 10000 });
397
+ isLocal = localBranches.split('\n').some(line => {
398
+ const branchName = line.replace(/^\*?\s*/, '').trim();
399
+ return branchName === command.branch;
400
+ });
401
+ }
402
+ catch {
403
+ // Ignore errors - branch doesn't exist locally
404
+ }
405
+ // Check remote branches
406
+ try {
407
+ const { stdout: remoteBranches } = await execAsync(`git ls-remote --heads origin ${command.branch}`, { cwd, timeout: options?.timeout || 10000 });
408
+ isRemote = remoteBranches.trim().length > 0;
409
+ }
410
+ catch {
411
+ // Ignore errors - can't check remote (might be network issue)
412
+ }
413
+ const branchExists = isLocal || isRemote;
414
+ return {
415
+ success: true,
416
+ output: branchExists ? `Branch ${command.branch} exists` : `Branch ${command.branch} does not exist`,
417
+ details: {
418
+ branchName: command.branch,
419
+ branchExists,
420
+ isLocal,
421
+ isRemote
422
+ }
423
+ };
424
+ }
425
+ catch (error) {
426
+ return {
427
+ success: false,
428
+ error: 'UNKNOWN_ERROR',
429
+ output: error.message || 'Failed to check branch existence'
430
+ };
431
+ }
432
+ }
433
+ /**
434
+ * EP597: Execute branch_has_commits command
435
+ * Checks if a branch has commits ahead of the base branch (default: main)
436
+ */
437
+ async executeBranchHasCommits(command, cwd, options) {
438
+ // Validate branch name
439
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
440
+ if (!validation.valid) {
441
+ return {
442
+ success: false,
443
+ error: validation.error || 'UNKNOWN_ERROR'
444
+ };
445
+ }
446
+ const baseBranch = command.baseBranch || 'main';
447
+ try {
448
+ // Use git cherry to find commits unique to the branch
449
+ // This shows commits on branch that aren't on base
450
+ const { stdout } = await execAsync(`git cherry origin/${baseBranch} ${command.branch}`, { cwd, timeout: options?.timeout || 10000 });
451
+ // git cherry shows lines starting with + for unique commits
452
+ const uniqueCommits = stdout.trim().split('\n').filter(line => line.startsWith('+'));
453
+ const hasCommits = uniqueCommits.length > 0;
454
+ return {
455
+ success: true,
456
+ output: hasCommits
457
+ ? `Branch ${command.branch} has ${uniqueCommits.length} commits ahead of ${baseBranch}`
458
+ : `Branch ${command.branch} has no commits ahead of ${baseBranch}`,
459
+ details: {
460
+ branchName: command.branch,
461
+ hasCommits
462
+ }
463
+ };
464
+ }
465
+ catch (error) {
466
+ // If git cherry fails (branch not found, etc.), try alternative method
467
+ try {
468
+ // Alternative: count commits with rev-list
469
+ const { stdout } = await execAsync(`git rev-list --count origin/${baseBranch}..${command.branch}`, { cwd, timeout: options?.timeout || 10000 });
470
+ const commitCount = parseInt(stdout.trim(), 10);
471
+ const hasCommits = commitCount > 0;
472
+ return {
473
+ success: true,
474
+ output: hasCommits
475
+ ? `Branch ${command.branch} has ${commitCount} commits ahead of ${baseBranch}`
476
+ : `Branch ${command.branch} has no commits ahead of ${baseBranch}`,
477
+ details: {
478
+ branchName: command.branch,
479
+ hasCommits
480
+ }
481
+ };
482
+ }
483
+ catch {
484
+ // Both methods failed - branch likely doesn't exist or isn't tracked
485
+ return {
486
+ success: false,
487
+ error: 'BRANCH_NOT_FOUND',
488
+ output: error.message || `Failed to check commits for branch ${command.branch}`
489
+ };
490
+ }
491
+ }
492
+ }
493
+ /**
494
+ * EP598: Execute main branch check - returns current branch, uncommitted files, and unpushed commits
495
+ */
496
+ async executeMainBranchCheck(cwd, options) {
497
+ try {
498
+ // Get current branch
499
+ let currentBranch = '';
500
+ try {
501
+ const { stdout } = await execAsync('git branch --show-current', { cwd, timeout: options?.timeout || 10000 });
502
+ currentBranch = stdout.trim();
503
+ }
504
+ catch (error) {
505
+ return {
506
+ success: false,
507
+ error: 'UNKNOWN_ERROR',
508
+ output: error.message || 'Failed to get current branch'
509
+ };
510
+ }
511
+ // Get uncommitted files
512
+ let uncommittedFiles = [];
513
+ try {
514
+ const { stdout } = await execAsync('git status --porcelain', { cwd, timeout: options?.timeout || 10000 });
515
+ if (stdout) {
516
+ uncommittedFiles = stdout.split('\n').filter(line => line.trim()).map(line => {
517
+ const parts = line.trim().split(/\s+/);
518
+ return parts.slice(1).join(' ');
519
+ });
520
+ }
521
+ }
522
+ catch {
523
+ // Ignore errors - just means no uncommitted changes
524
+ }
525
+ // Get unpushed commits (only if on main branch)
526
+ let localCommits = [];
527
+ if (currentBranch === 'main') {
528
+ try {
529
+ const { stdout } = await execAsync('git log origin/main..HEAD --format="%H|%s|%an"', { cwd, timeout: options?.timeout || 10000 });
530
+ if (stdout) {
531
+ localCommits = stdout.split('\n').filter(line => line.trim()).map(line => {
532
+ const [sha, message, author] = line.split('|');
533
+ return {
534
+ sha: sha ? sha.substring(0, 8) : '',
535
+ message: message || '',
536
+ author: author || ''
537
+ };
538
+ });
539
+ }
540
+ }
541
+ catch {
542
+ // Ignore errors - might not have origin/main or no remote
543
+ }
544
+ }
545
+ return {
546
+ success: true,
547
+ output: `Branch: ${currentBranch}, Uncommitted: ${uncommittedFiles.length}, Unpushed: ${localCommits.length}`,
548
+ details: {
549
+ currentBranch,
550
+ uncommittedFiles,
551
+ localCommits
552
+ }
553
+ };
554
+ }
555
+ catch (error) {
556
+ return {
557
+ success: false,
558
+ error: 'UNKNOWN_ERROR',
559
+ output: error.message || 'Failed to check main branch'
560
+ };
561
+ }
562
+ }
563
+ /**
564
+ * EP599: Execute get_commits command
565
+ * Returns commits for a branch with pushed/unpushed status
566
+ */
567
+ async executeGetCommits(command, cwd, options) {
568
+ // Validate branch name
569
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
570
+ if (!validation.valid) {
571
+ return {
572
+ success: false,
573
+ error: validation.error || 'UNKNOWN_ERROR'
574
+ };
575
+ }
576
+ const limit = command.limit || 10;
577
+ const baseBranch = command.baseBranch || 'main';
578
+ try {
579
+ // Get commits unique to this branch (not in main)
580
+ let stdout;
581
+ try {
582
+ const result = await execAsync(`git log ${baseBranch}.."${command.branch}" --pretty=format:"%H|%an|%ae|%aI|%s" -n ${limit} --`, { cwd, timeout: options?.timeout || 10000 });
583
+ stdout = result.stdout;
584
+ }
585
+ catch (error) {
586
+ // Fallback: if comparison fails, show all commits on this branch
587
+ try {
588
+ const result = await execAsync(`git log "${command.branch}" --pretty=format:"%H|%an|%ae|%aI|%s" -n ${limit} --`, { cwd, timeout: options?.timeout || 10000 });
589
+ stdout = result.stdout;
590
+ }
591
+ catch (branchError) {
592
+ // Branch doesn't exist locally
593
+ return {
594
+ success: false,
595
+ error: 'BRANCH_NOT_FOUND',
596
+ output: `Branch ${command.branch} not found locally`
597
+ };
598
+ }
599
+ }
600
+ if (!stdout.trim()) {
601
+ return {
602
+ success: true,
603
+ output: 'No commits found',
604
+ details: {
605
+ commits: []
606
+ }
607
+ };
608
+ }
609
+ // Parse commits
610
+ const commitLines = stdout.trim().split('\n');
611
+ // Check which commits have been pushed to remote
612
+ let remoteShas = new Set();
613
+ try {
614
+ const { stdout: remoteCommits } = await execAsync(`git log "origin/${command.branch}" --pretty=format:"%H" -n ${limit} --`, { cwd, timeout: options?.timeout || 10000 });
615
+ remoteShas = new Set(remoteCommits.trim().split('\n').filter(Boolean));
616
+ }
617
+ catch {
618
+ // Remote branch doesn't exist - all commits are local/unpushed
619
+ }
620
+ const commits = commitLines.map((line) => {
621
+ const [sha, authorName, authorEmail, date, ...messageParts] = line.split('|');
622
+ const message = messageParts.join('|'); // Handle pipes in commit messages
623
+ const isPushed = remoteShas.has(sha);
624
+ return {
625
+ sha,
626
+ message,
627
+ authorName,
628
+ authorEmail,
629
+ date,
630
+ isPushed
631
+ };
632
+ });
633
+ return {
634
+ success: true,
635
+ output: `Found ${commits.length} commits`,
636
+ details: {
637
+ commits
638
+ }
639
+ };
640
+ }
641
+ catch (error) {
642
+ return {
643
+ success: false,
644
+ error: 'UNKNOWN_ERROR',
645
+ output: error.message || 'Failed to get commits'
646
+ };
647
+ }
648
+ }
649
+ // ========================================
650
+ // EP599: Advanced operations for move-to-module and discard-main-changes
651
+ // ========================================
652
+ /**
653
+ * Execute git stash operations
654
+ */
655
+ async executeStash(command, cwd, options) {
656
+ try {
657
+ const args = ['stash'];
658
+ switch (command.operation) {
659
+ case 'push':
660
+ args.push('push');
661
+ if (command.includeUntracked) {
662
+ args.push('--include-untracked');
663
+ }
664
+ if (command.message) {
665
+ args.push('-m', command.message);
666
+ }
667
+ break;
668
+ case 'pop':
669
+ args.push('pop');
670
+ break;
671
+ case 'drop':
672
+ args.push('drop');
673
+ break;
674
+ case 'list':
675
+ args.push('list');
676
+ break;
677
+ }
678
+ return await this.runGitCommand(args, cwd, options);
679
+ }
680
+ catch (error) {
681
+ return {
682
+ success: false,
683
+ error: 'UNKNOWN_ERROR',
684
+ output: error.message || 'Stash operation failed'
685
+ };
686
+ }
687
+ }
688
+ /**
689
+ * Execute git reset
690
+ */
691
+ async executeReset(command, cwd, options) {
692
+ try {
693
+ const args = ['reset', `--${command.mode}`];
694
+ if (command.target) {
695
+ args.push(command.target);
696
+ }
697
+ return await this.runGitCommand(args, cwd, options);
698
+ }
699
+ catch (error) {
700
+ return {
701
+ success: false,
702
+ error: 'UNKNOWN_ERROR',
703
+ output: error.message || 'Reset failed'
704
+ };
705
+ }
706
+ }
707
+ /**
708
+ * Execute git merge
709
+ */
710
+ async executeMerge(command, cwd, options) {
711
+ try {
712
+ if (command.abort) {
713
+ return await this.runGitCommand(['merge', '--abort'], cwd, options);
714
+ }
715
+ // Validate branch name
716
+ const branchValidation = (0, git_validator_1.validateBranchName)(command.branch);
717
+ if (!branchValidation.valid) {
718
+ return {
719
+ success: false,
720
+ error: 'BRANCH_NOT_FOUND',
721
+ output: branchValidation.error || 'Invalid branch name'
722
+ };
723
+ }
724
+ const args = ['merge', command.branch];
725
+ if (command.strategy === 'ours') {
726
+ args.push('--strategy=ours');
727
+ }
728
+ else if (command.strategy === 'theirs') {
729
+ args.push('--strategy-option=theirs');
730
+ }
731
+ if (command.noEdit) {
732
+ args.push('--no-edit');
733
+ }
734
+ const result = await this.runGitCommand(args, cwd, options);
735
+ // Check for merge conflicts
736
+ if (!result.success && result.output?.includes('CONFLICT')) {
737
+ result.details = result.details || {};
738
+ result.details.mergeConflicts = true;
739
+ }
740
+ return result;
741
+ }
742
+ catch (error) {
743
+ return {
744
+ success: false,
745
+ error: 'MERGE_CONFLICT',
746
+ output: error.message || 'Merge failed'
747
+ };
748
+ }
749
+ }
750
+ /**
751
+ * Execute git cherry-pick
752
+ */
753
+ async executeCherryPick(command, cwd, options) {
754
+ try {
755
+ if (command.abort) {
756
+ return await this.runGitCommand(['cherry-pick', '--abort'], cwd, options);
757
+ }
758
+ const result = await this.runGitCommand(['cherry-pick', command.sha], cwd, options);
759
+ // Check for cherry-pick conflicts
760
+ if (!result.success && result.output?.includes('CONFLICT')) {
761
+ result.details = result.details || {};
762
+ result.details.cherryPickConflicts = true;
763
+ }
764
+ return result;
765
+ }
766
+ catch (error) {
767
+ return {
768
+ success: false,
769
+ error: 'MERGE_CONFLICT',
770
+ output: error.message || 'Cherry-pick failed'
771
+ };
772
+ }
773
+ }
774
+ /**
775
+ * Execute git clean
776
+ */
777
+ async executeClean(command, cwd, options) {
778
+ try {
779
+ const args = ['clean'];
780
+ if (command.force) {
781
+ args.push('-f');
782
+ }
783
+ if (command.directories) {
784
+ args.push('-d');
785
+ }
786
+ return await this.runGitCommand(args, cwd, options);
787
+ }
788
+ catch (error) {
789
+ return {
790
+ success: false,
791
+ error: 'UNKNOWN_ERROR',
792
+ output: error.message || 'Clean failed'
793
+ };
794
+ }
795
+ }
796
+ /**
797
+ * Execute git add
798
+ */
799
+ async executeAdd(command, cwd, options) {
800
+ try {
801
+ const args = ['add'];
802
+ if (command.all) {
803
+ args.push('-A');
804
+ }
805
+ else if (command.files && command.files.length > 0) {
806
+ // Validate file paths
807
+ const validation = (0, git_validator_1.validateFilePaths)(command.files);
808
+ if (!validation.valid) {
809
+ return {
810
+ success: false,
811
+ error: 'UNKNOWN_ERROR',
812
+ output: validation.error || 'Invalid file paths'
813
+ };
814
+ }
815
+ args.push(...command.files);
816
+ }
817
+ else {
818
+ args.push('-A'); // Default to all
819
+ }
820
+ return await this.runGitCommand(args, cwd, options);
821
+ }
822
+ catch (error) {
823
+ return {
824
+ success: false,
825
+ error: 'UNKNOWN_ERROR',
826
+ output: error.message || 'Add failed'
827
+ };
828
+ }
829
+ }
830
+ /**
831
+ * Execute git fetch
832
+ */
833
+ async executeFetch(command, cwd, options) {
834
+ try {
835
+ const args = ['fetch'];
836
+ if (command.remote) {
837
+ args.push(command.remote);
838
+ if (command.branch) {
839
+ args.push(command.branch);
840
+ }
841
+ }
842
+ else {
843
+ args.push('origin');
844
+ }
845
+ return await this.runGitCommand(args, cwd, options);
846
+ }
847
+ catch (error) {
848
+ return {
849
+ success: false,
850
+ error: 'NETWORK_ERROR',
851
+ output: error.message || 'Fetch failed'
852
+ };
853
+ }
854
+ }
855
+ // ========================================
856
+ // EP599-3: Composite operations
857
+ // ========================================
858
+ /**
859
+ * Execute move_to_module - composite operation
860
+ * Moves commits/changes to a module branch with conflict handling
861
+ */
862
+ async executeMoveToModule(command, cwd, options) {
863
+ const { targetBranch, commitShas, conflictResolution } = command;
864
+ let hasStash = false;
865
+ const cherryPickedCommits = [];
866
+ try {
867
+ // Step 1: Get current branch
868
+ const { stdout: currentBranchOut } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd });
869
+ const currentBranch = currentBranchOut.trim();
870
+ // Step 2: Stash uncommitted changes
871
+ const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd });
872
+ if (statusOutput.trim()) {
873
+ try {
874
+ await execAsync('git add -A', { cwd });
875
+ const { stdout: stashHash } = await execAsync('git stash create -m "episoda-move-to-module"', { cwd });
876
+ if (stashHash && stashHash.trim()) {
877
+ await execAsync(`git stash store -m "episoda-move-to-module" ${stashHash.trim()}`, { cwd });
878
+ await execAsync('git reset --hard HEAD', { cwd });
879
+ hasStash = true;
880
+ }
881
+ }
882
+ catch (stashError) {
883
+ // Continue without stashing
884
+ }
885
+ }
886
+ // Step 3: Switch to main if we have commits to move
887
+ if (commitShas && commitShas.length > 0 && currentBranch !== 'main' && currentBranch !== 'master') {
888
+ await execAsync('git checkout main', { cwd });
889
+ }
890
+ // Step 4: Create or checkout target branch
891
+ let branchExists = false;
892
+ try {
893
+ await execAsync(`git rev-parse --verify ${targetBranch}`, { cwd });
894
+ branchExists = true;
895
+ }
896
+ catch {
897
+ branchExists = false;
898
+ }
899
+ if (!branchExists) {
900
+ await execAsync(`git checkout -b ${targetBranch}`, { cwd });
901
+ }
902
+ else {
903
+ await execAsync(`git checkout ${targetBranch}`, { cwd });
904
+ // Try to merge changes from main
905
+ if (currentBranch === 'main' || currentBranch === 'master') {
906
+ try {
907
+ const mergeStrategy = conflictResolution === 'ours' ? '--strategy=ours' :
908
+ conflictResolution === 'theirs' ? '--strategy-option=theirs' : '';
909
+ await execAsync(`git merge ${currentBranch} ${mergeStrategy} --no-edit`, { cwd });
910
+ }
911
+ catch (mergeError) {
912
+ // Check for conflicts
913
+ const { stdout: conflictStatus } = await execAsync('git status --porcelain', { cwd });
914
+ if (conflictStatus.includes('UU ') || conflictStatus.includes('AA ') || conflictStatus.includes('DD ')) {
915
+ const { stdout: conflictFiles } = await execAsync('git diff --name-only --diff-filter=U', { cwd });
916
+ const conflictedFiles = conflictFiles.trim().split('\n').filter(Boolean);
917
+ if (conflictResolution) {
918
+ // Auto-resolve conflicts
919
+ for (const file of conflictedFiles) {
920
+ await execAsync(`git checkout --${conflictResolution} "${file}"`, { cwd });
921
+ await execAsync(`git add "${file}"`, { cwd });
922
+ }
923
+ await execAsync('git commit --no-edit', { cwd });
924
+ }
925
+ else {
926
+ // Abort merge and return conflict error
927
+ await execAsync('git merge --abort', { cwd });
928
+ return {
929
+ success: false,
930
+ error: 'MERGE_CONFLICT',
931
+ output: 'Merge conflicts detected',
932
+ details: {
933
+ hasConflicts: true,
934
+ conflictedFiles,
935
+ movedToBranch: targetBranch
936
+ }
937
+ };
938
+ }
939
+ }
940
+ }
941
+ }
942
+ }
943
+ // Step 5: Cherry-pick commits if provided
944
+ if (commitShas && commitShas.length > 0 && (currentBranch === 'main' || currentBranch === 'master')) {
945
+ for (const sha of commitShas) {
946
+ try {
947
+ // Check if commit already exists in branch
948
+ const { stdout: logOutput } = await execAsync(`git log --format=%H ${targetBranch} | grep ${sha}`, { cwd }).catch(() => ({ stdout: '' }));
949
+ if (!logOutput.trim()) {
950
+ await execAsync(`git cherry-pick ${sha}`, { cwd });
951
+ cherryPickedCommits.push(sha);
952
+ }
953
+ }
954
+ catch (err) {
955
+ await execAsync('git cherry-pick --abort', { cwd }).catch(() => { });
956
+ }
957
+ }
958
+ // Reset main to origin/main
959
+ await execAsync('git checkout main', { cwd });
960
+ await execAsync('git reset --hard origin/main', { cwd });
961
+ await execAsync(`git checkout ${targetBranch}`, { cwd });
962
+ }
963
+ // Step 6: Apply stashed changes
964
+ if (hasStash) {
965
+ try {
966
+ await execAsync('git stash pop', { cwd });
967
+ }
968
+ catch (stashError) {
969
+ // Check for stash conflicts
970
+ const { stdout: conflictStatus } = await execAsync('git status --porcelain', { cwd });
971
+ if (conflictStatus.includes('UU ') || conflictStatus.includes('AA ')) {
972
+ if (conflictResolution) {
973
+ const { stdout: conflictFiles } = await execAsync('git diff --name-only --diff-filter=U', { cwd });
974
+ const conflictedFiles = conflictFiles.trim().split('\n').filter(Boolean);
975
+ for (const file of conflictedFiles) {
976
+ await execAsync(`git checkout --${conflictResolution} "${file}"`, { cwd });
977
+ await execAsync(`git add "${file}"`, { cwd });
978
+ }
979
+ }
980
+ }
981
+ }
982
+ }
983
+ return {
984
+ success: true,
985
+ output: `Successfully moved to branch ${targetBranch}`,
986
+ details: {
987
+ movedToBranch: targetBranch,
988
+ cherryPickedCommits,
989
+ currentBranch: targetBranch
990
+ }
991
+ };
992
+ }
993
+ catch (error) {
994
+ // Try to restore stash if something went wrong
995
+ if (hasStash) {
996
+ try {
997
+ const { stdout: stashList } = await execAsync('git stash list', { cwd });
998
+ if (stashList.includes('episoda-move-to-module')) {
999
+ await execAsync('git stash pop', { cwd });
1000
+ }
1001
+ }
1002
+ catch (e) {
1003
+ // Ignore errors restoring stash
1004
+ }
1005
+ }
1006
+ return {
1007
+ success: false,
1008
+ error: 'UNKNOWN_ERROR',
1009
+ output: error.message || 'Move to module failed'
1010
+ };
1011
+ }
1012
+ }
1013
+ /**
1014
+ * Execute discard_main_changes - composite operation
1015
+ * Discards all uncommitted files and local commits on main branch
1016
+ */
1017
+ async executeDiscardMainChanges(cwd, options) {
1018
+ try {
1019
+ // Step 1: Verify we're on main/master
1020
+ const { stdout: currentBranchOut } = await execAsync('git rev-parse --abbrev-ref HEAD', { cwd });
1021
+ const branch = currentBranchOut.trim();
1022
+ if (branch !== 'main' && branch !== 'master') {
1023
+ return {
1024
+ success: false,
1025
+ error: 'BRANCH_NOT_FOUND',
1026
+ output: `Cannot discard changes - not on main branch. Current branch: ${branch}`
1027
+ };
1028
+ }
1029
+ let discardedFiles = 0;
1030
+ // Step 2: Stash uncommitted changes (will be dropped)
1031
+ const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd });
1032
+ if (statusOutput.trim()) {
1033
+ try {
1034
+ await execAsync('git stash --include-untracked', { cwd });
1035
+ discardedFiles = statusOutput.trim().split('\n').length;
1036
+ }
1037
+ catch (stashError) {
1038
+ // Continue - might be nothing to stash
1039
+ }
1040
+ }
1041
+ // Step 3: Fetch and reset to origin
1042
+ await execAsync('git fetch origin', { cwd });
1043
+ await execAsync(`git reset --hard origin/${branch}`, { cwd });
1044
+ // Step 4: Clean untracked files
1045
+ try {
1046
+ await execAsync('git clean -fd', { cwd });
1047
+ }
1048
+ catch (cleanError) {
1049
+ // Non-critical
1050
+ }
1051
+ // Step 5: Drop the stash
1052
+ try {
1053
+ await execAsync('git stash drop', { cwd });
1054
+ }
1055
+ catch (dropError) {
1056
+ // Stash might not exist
1057
+ }
1058
+ return {
1059
+ success: true,
1060
+ output: `Successfully discarded all changes and reset to origin/${branch}`,
1061
+ details: {
1062
+ currentBranch: branch,
1063
+ discardedFiles
1064
+ }
1065
+ };
1066
+ }
1067
+ catch (error) {
1068
+ return {
1069
+ success: false,
1070
+ error: 'UNKNOWN_ERROR',
1071
+ output: error.message || 'Discard main changes failed'
1072
+ };
1073
+ }
1074
+ }
1075
+ // ========================================
1076
+ // EP523: Branch sync operations
1077
+ // ========================================
1078
+ /**
1079
+ * EP523: Get sync status of a branch relative to main
1080
+ * Returns how many commits behind/ahead the branch is
1081
+ */
1082
+ async executeSyncStatus(command, cwd, options) {
1083
+ try {
1084
+ // Validate branch name
1085
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
1086
+ if (!validation.valid) {
1087
+ return {
1088
+ success: false,
1089
+ error: validation.error || 'UNKNOWN_ERROR'
1090
+ };
1091
+ }
1092
+ // Fetch latest from remote to get accurate counts
1093
+ try {
1094
+ await execAsync('git fetch origin', { cwd, timeout: options?.timeout || 30000 });
1095
+ }
1096
+ catch (fetchError) {
1097
+ // Network error - return what we can determine locally
1098
+ return {
1099
+ success: false,
1100
+ error: 'NETWORK_ERROR',
1101
+ output: 'Unable to fetch from remote. Check your network connection.'
1102
+ };
1103
+ }
1104
+ let commitsBehind = 0;
1105
+ let commitsAhead = 0;
1106
+ // Count commits the branch is BEHIND main (main has commits branch doesn't)
1107
+ try {
1108
+ const { stdout: behindOutput } = await execAsync(`git rev-list --count ${command.branch}..origin/main`, { cwd, timeout: options?.timeout || 10000 });
1109
+ commitsBehind = parseInt(behindOutput.trim(), 10) || 0;
1110
+ }
1111
+ catch {
1112
+ // Branch might not exist or no common ancestor
1113
+ commitsBehind = 0;
1114
+ }
1115
+ // Count commits the branch is AHEAD of main (branch has commits main doesn't)
1116
+ try {
1117
+ const { stdout: aheadOutput } = await execAsync(`git rev-list --count origin/main..${command.branch}`, { cwd, timeout: options?.timeout || 10000 });
1118
+ commitsAhead = parseInt(aheadOutput.trim(), 10) || 0;
1119
+ }
1120
+ catch {
1121
+ // Branch might not exist or no common ancestor
1122
+ commitsAhead = 0;
1123
+ }
1124
+ const isBehind = commitsBehind > 0;
1125
+ const isAhead = commitsAhead > 0;
1126
+ const needsSync = isBehind;
1127
+ return {
1128
+ success: true,
1129
+ output: isBehind
1130
+ ? `Branch ${command.branch} is ${commitsBehind} commit(s) behind main`
1131
+ : `Branch ${command.branch} is up to date with main`,
1132
+ details: {
1133
+ branchName: command.branch,
1134
+ commitsBehind,
1135
+ commitsAhead,
1136
+ isBehind,
1137
+ isAhead,
1138
+ needsSync
1139
+ }
1140
+ };
1141
+ }
1142
+ catch (error) {
1143
+ return {
1144
+ success: false,
1145
+ error: 'UNKNOWN_ERROR',
1146
+ output: error.message || 'Failed to check sync status'
1147
+ };
1148
+ }
1149
+ }
1150
+ /**
1151
+ * EP523: Sync local main branch with remote
1152
+ * Used before creating new branches to ensure we branch from latest main
1153
+ */
1154
+ async executeSyncMain(cwd, options) {
1155
+ try {
1156
+ // Get current branch to restore later if needed
1157
+ let currentBranch = '';
1158
+ try {
1159
+ const { stdout } = await execAsync('git branch --show-current', { cwd, timeout: 5000 });
1160
+ currentBranch = stdout.trim();
1161
+ }
1162
+ catch {
1163
+ // Ignore - might be detached HEAD
1164
+ }
1165
+ // Fetch latest from remote
1166
+ try {
1167
+ await execAsync('git fetch origin main', { cwd, timeout: options?.timeout || 30000 });
1168
+ }
1169
+ catch (fetchError) {
1170
+ return {
1171
+ success: false,
1172
+ error: 'NETWORK_ERROR',
1173
+ output: 'Unable to fetch from remote. Check your network connection.'
1174
+ };
1175
+ }
1176
+ // Check if we need to switch to main
1177
+ const needsSwitch = currentBranch !== 'main' && currentBranch !== '';
1178
+ if (needsSwitch) {
1179
+ // Check for uncommitted changes first
1180
+ const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd, timeout: 5000 });
1181
+ if (statusOutput.trim()) {
1182
+ return {
1183
+ success: false,
1184
+ error: 'UNCOMMITTED_CHANGES',
1185
+ output: 'Cannot sync main: you have uncommitted changes. Commit or stash them first.',
1186
+ details: {
1187
+ uncommittedFiles: statusOutput.trim().split('\n').map(line => line.slice(3))
1188
+ }
1189
+ };
1190
+ }
1191
+ // Switch to main
1192
+ await execAsync('git checkout main', { cwd, timeout: options?.timeout || 10000 });
1193
+ }
1194
+ // Pull latest main
1195
+ try {
1196
+ await execAsync('git pull origin main', { cwd, timeout: options?.timeout || 30000 });
1197
+ }
1198
+ catch (pullError) {
1199
+ // Check for conflicts
1200
+ if (pullError.message?.includes('CONFLICT') || pullError.stderr?.includes('CONFLICT')) {
1201
+ // Abort the merge
1202
+ await execAsync('git merge --abort', { cwd, timeout: 5000 }).catch(() => { });
1203
+ return {
1204
+ success: false,
1205
+ error: 'MERGE_CONFLICT',
1206
+ output: 'Conflict while syncing main. This is unexpected - main should not have local commits.'
1207
+ };
1208
+ }
1209
+ throw pullError;
1210
+ }
1211
+ // Switch back to original branch if we switched
1212
+ if (needsSwitch && currentBranch) {
1213
+ await execAsync(`git checkout "${currentBranch}"`, { cwd, timeout: options?.timeout || 10000 });
1214
+ }
1215
+ return {
1216
+ success: true,
1217
+ output: 'Successfully synced main with remote',
1218
+ details: {
1219
+ currentBranch: needsSwitch ? currentBranch : 'main'
1220
+ }
1221
+ };
1222
+ }
1223
+ catch (error) {
1224
+ return {
1225
+ success: false,
1226
+ error: 'UNKNOWN_ERROR',
1227
+ output: error.message || 'Failed to sync main'
1228
+ };
1229
+ }
1230
+ }
1231
+ /**
1232
+ * EP523: Rebase a branch onto main
1233
+ * Used when resuming work on a branch that's behind main
1234
+ */
1235
+ async executeRebaseBranch(command, cwd, options) {
1236
+ try {
1237
+ // Validate branch name
1238
+ const validation = (0, git_validator_1.validateBranchName)(command.branch);
1239
+ if (!validation.valid) {
1240
+ return {
1241
+ success: false,
1242
+ error: validation.error || 'UNKNOWN_ERROR'
1243
+ };
1244
+ }
1245
+ // Check for uncommitted changes
1246
+ const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd, timeout: 5000 });
1247
+ if (statusOutput.trim()) {
1248
+ return {
1249
+ success: false,
1250
+ error: 'UNCOMMITTED_CHANGES',
1251
+ output: 'Cannot rebase: you have uncommitted changes. Commit or stash them first.',
1252
+ details: {
1253
+ uncommittedFiles: statusOutput.trim().split('\n').map(line => line.slice(3))
1254
+ }
1255
+ };
1256
+ }
1257
+ // Get current branch
1258
+ const { stdout: currentBranchOut } = await execAsync('git branch --show-current', { cwd, timeout: 5000 });
1259
+ const currentBranch = currentBranchOut.trim();
1260
+ // Ensure we're on the target branch
1261
+ if (currentBranch !== command.branch) {
1262
+ await execAsync(`git checkout "${command.branch}"`, { cwd, timeout: options?.timeout || 10000 });
1263
+ }
1264
+ // Fetch latest main
1265
+ await execAsync('git fetch origin main', { cwd, timeout: options?.timeout || 30000 });
1266
+ // Perform rebase
1267
+ try {
1268
+ await execAsync('git rebase origin/main', { cwd, timeout: options?.timeout || 60000 });
1269
+ }
1270
+ catch (rebaseError) {
1271
+ const errorOutput = (rebaseError.stderr || '') + (rebaseError.stdout || '');
1272
+ // Check for conflicts
1273
+ if (errorOutput.includes('CONFLICT') || errorOutput.includes('could not apply')) {
1274
+ // Get conflicting files
1275
+ let conflictFiles = [];
1276
+ try {
1277
+ const { stdout: conflictOutput } = await execAsync('git diff --name-only --diff-filter=U', { cwd, timeout: 5000 });
1278
+ conflictFiles = conflictOutput.trim().split('\n').filter(Boolean);
1279
+ }
1280
+ catch {
1281
+ // Ignore - try alternate method
1282
+ try {
1283
+ const { stdout: statusOut } = await execAsync('git status --porcelain', { cwd, timeout: 5000 });
1284
+ conflictFiles = statusOut
1285
+ .trim()
1286
+ .split('\n')
1287
+ .filter(line => line.startsWith('UU ') || line.startsWith('AA ') || line.startsWith('DD '))
1288
+ .map(line => line.slice(3));
1289
+ }
1290
+ catch {
1291
+ // Couldn't get conflict files
1292
+ }
1293
+ }
1294
+ return {
1295
+ success: false,
1296
+ error: 'REBASE_CONFLICT',
1297
+ output: `Rebase conflict in ${conflictFiles.length} file(s). Resolve conflicts then use rebase_continue, or use rebase_abort to cancel.`,
1298
+ details: {
1299
+ inRebase: true,
1300
+ rebaseConflicts: conflictFiles,
1301
+ hasConflicts: true,
1302
+ conflictedFiles: conflictFiles
1303
+ }
1304
+ };
1305
+ }
1306
+ throw rebaseError;
1307
+ }
1308
+ return {
1309
+ success: true,
1310
+ output: `Successfully rebased ${command.branch} onto main`,
1311
+ details: {
1312
+ branchName: command.branch,
1313
+ inRebase: false
1314
+ }
1315
+ };
1316
+ }
1317
+ catch (error) {
1318
+ return {
1319
+ success: false,
1320
+ error: 'UNKNOWN_ERROR',
1321
+ output: error.message || 'Rebase failed'
1322
+ };
1323
+ }
1324
+ }
1325
+ /**
1326
+ * EP523: Abort an in-progress rebase
1327
+ * Returns to the state before rebase was started
1328
+ */
1329
+ async executeRebaseAbort(cwd, options) {
1330
+ try {
1331
+ await execAsync('git rebase --abort', { cwd, timeout: options?.timeout || 10000 });
1332
+ return {
1333
+ success: true,
1334
+ output: 'Rebase aborted. Your branch has been restored to its previous state.',
1335
+ details: {
1336
+ inRebase: false
1337
+ }
1338
+ };
1339
+ }
1340
+ catch (error) {
1341
+ // Check if there's no rebase in progress
1342
+ if (error.message?.includes('No rebase in progress') || error.stderr?.includes('No rebase in progress')) {
1343
+ return {
1344
+ success: true,
1345
+ output: 'No rebase in progress.',
1346
+ details: {
1347
+ inRebase: false
1348
+ }
1349
+ };
1350
+ }
1351
+ return {
1352
+ success: false,
1353
+ error: 'UNKNOWN_ERROR',
1354
+ output: error.message || 'Failed to abort rebase'
1355
+ };
1356
+ }
1357
+ }
1358
+ /**
1359
+ * EP523: Continue a paused rebase after conflicts are resolved
1360
+ */
1361
+ async executeRebaseContinue(cwd, options) {
1362
+ try {
1363
+ // Stage all resolved files
1364
+ await execAsync('git add -A', { cwd, timeout: 5000 });
1365
+ // Continue the rebase
1366
+ await execAsync('git rebase --continue', { cwd, timeout: options?.timeout || 60000 });
1367
+ return {
1368
+ success: true,
1369
+ output: 'Rebase continued successfully.',
1370
+ details: {
1371
+ inRebase: false
1372
+ }
1373
+ };
1374
+ }
1375
+ catch (error) {
1376
+ const errorOutput = (error.stderr || '') + (error.stdout || '');
1377
+ // Check if there are still conflicts
1378
+ if (errorOutput.includes('CONFLICT') || errorOutput.includes('could not apply')) {
1379
+ // Get remaining conflict files
1380
+ let conflictFiles = [];
1381
+ try {
1382
+ const { stdout: conflictOutput } = await execAsync('git diff --name-only --diff-filter=U', { cwd, timeout: 5000 });
1383
+ conflictFiles = conflictOutput.trim().split('\n').filter(Boolean);
1384
+ }
1385
+ catch {
1386
+ // Ignore
1387
+ }
1388
+ return {
1389
+ success: false,
1390
+ error: 'REBASE_CONFLICT',
1391
+ output: 'More conflicts encountered. Resolve them and try again.',
1392
+ details: {
1393
+ inRebase: true,
1394
+ rebaseConflicts: conflictFiles,
1395
+ hasConflicts: true,
1396
+ conflictedFiles: conflictFiles
1397
+ }
1398
+ };
1399
+ }
1400
+ // Check if there's no rebase in progress
1401
+ if (errorOutput.includes('No rebase in progress')) {
1402
+ return {
1403
+ success: true,
1404
+ output: 'No rebase in progress.',
1405
+ details: {
1406
+ inRebase: false
1407
+ }
1408
+ };
1409
+ }
1410
+ return {
1411
+ success: false,
1412
+ error: 'UNKNOWN_ERROR',
1413
+ output: error.message || 'Failed to continue rebase'
1414
+ };
1415
+ }
1416
+ }
1417
+ /**
1418
+ * EP523: Check if a rebase is currently in progress
1419
+ */
1420
+ async executeRebaseStatus(cwd, options) {
1421
+ try {
1422
+ // Check for rebase-merge directory (indicates rebase in progress)
1423
+ let inRebase = false;
1424
+ let rebaseConflicts = [];
1425
+ try {
1426
+ const { stdout: gitDir } = await execAsync('git rev-parse --git-dir', { cwd, timeout: 5000 });
1427
+ const gitDirPath = gitDir.trim();
1428
+ // Check for rebase directories
1429
+ const fs = await Promise.resolve().then(() => __importStar(require('fs'))).then(m => m.promises);
1430
+ const rebaseMergePath = `${gitDirPath}/rebase-merge`;
1431
+ const rebaseApplyPath = `${gitDirPath}/rebase-apply`;
1432
+ try {
1433
+ await fs.access(rebaseMergePath);
1434
+ inRebase = true;
1435
+ }
1436
+ catch {
1437
+ try {
1438
+ await fs.access(rebaseApplyPath);
1439
+ inRebase = true;
1440
+ }
1441
+ catch {
1442
+ inRebase = false;
1443
+ }
1444
+ }
1445
+ }
1446
+ catch {
1447
+ // If we can't determine, check via status
1448
+ try {
1449
+ const { stdout: statusOutput } = await execAsync('git status', { cwd, timeout: 5000 });
1450
+ inRebase = statusOutput.includes('rebase in progress') ||
1451
+ statusOutput.includes('interactive rebase in progress') ||
1452
+ statusOutput.includes('You are currently rebasing');
1453
+ }
1454
+ catch {
1455
+ inRebase = false;
1456
+ }
1457
+ }
1458
+ // If in rebase, get conflict files
1459
+ if (inRebase) {
1460
+ try {
1461
+ const { stdout: conflictOutput } = await execAsync('git diff --name-only --diff-filter=U', { cwd, timeout: 5000 });
1462
+ rebaseConflicts = conflictOutput.trim().split('\n').filter(Boolean);
1463
+ }
1464
+ catch {
1465
+ // No conflicts or couldn't get them
1466
+ }
1467
+ }
1468
+ return {
1469
+ success: true,
1470
+ output: inRebase
1471
+ ? `Rebase in progress with ${rebaseConflicts.length} conflicting file(s)`
1472
+ : 'No rebase in progress',
1473
+ details: {
1474
+ inRebase,
1475
+ rebaseConflicts,
1476
+ hasConflicts: rebaseConflicts.length > 0
1477
+ }
1478
+ };
1479
+ }
1480
+ catch (error) {
1481
+ return {
1482
+ success: false,
1483
+ error: 'UNKNOWN_ERROR',
1484
+ output: error.message || 'Failed to check rebase status'
1485
+ };
1486
+ }
1487
+ }
1488
+ /**
1489
+ * Run a git command and return structured result
1490
+ */
1491
+ async runGitCommand(args, cwd, options) {
1492
+ try {
1493
+ // Sanitize arguments
1494
+ const sanitizedArgs = (0, git_validator_1.sanitizeArgs)(args);
1495
+ // Build command
1496
+ const command = ['git', ...sanitizedArgs].join(' ');
1497
+ // Execute with timeout
1498
+ const timeout = options?.timeout || 30000; // 30 second default
1499
+ const execOptions = {
1500
+ cwd,
1501
+ timeout,
1502
+ env: options?.env || process.env,
1503
+ maxBuffer: 1024 * 1024 * 10 // 10MB buffer
1504
+ };
1505
+ const { stdout, stderr } = await execAsync(command, execOptions);
1506
+ // Combine output
1507
+ const output = (stdout + stderr).trim();
1508
+ // Extract additional details
1509
+ const details = {};
1510
+ // Try to extract branch name
1511
+ const branchName = (0, git_parser_1.extractBranchName)(output);
1512
+ if (branchName) {
1513
+ details.branchName = branchName;
1514
+ }
1515
+ // Check for detached HEAD
1516
+ if ((0, git_parser_1.isDetachedHead)(output)) {
1517
+ details.branchName = 'HEAD (detached)';
1518
+ }
1519
+ return {
1520
+ success: true,
1521
+ output,
1522
+ details: Object.keys(details).length > 0 ? details : undefined
1523
+ };
1524
+ }
1525
+ catch (error) {
1526
+ // Parse error
1527
+ const stderr = error.stderr || '';
1528
+ const stdout = error.stdout || '';
1529
+ const exitCode = error.code || 1;
1530
+ // Determine error code
1531
+ const errorCode = (0, git_parser_1.parseGitError)(stderr, stdout, exitCode);
1532
+ // Extract additional details based on error type
1533
+ const details = {
1534
+ exitCode
1535
+ };
1536
+ // Parse conflicts if merge conflict
1537
+ if (errorCode === 'MERGE_CONFLICT') {
1538
+ const conflicts = (0, git_parser_1.parseMergeConflicts)(stdout + stderr);
1539
+ if (conflicts.length > 0) {
1540
+ details.conflictingFiles = conflicts;
1541
+ }
1542
+ }
1543
+ // Parse status for uncommitted changes
1544
+ if (errorCode === 'UNCOMMITTED_CHANGES') {
1545
+ try {
1546
+ const statusResult = await this.executeStatus(cwd, options);
1547
+ if (statusResult.details?.uncommittedFiles) {
1548
+ details.uncommittedFiles = statusResult.details.uncommittedFiles;
1549
+ }
1550
+ }
1551
+ catch {
1552
+ // Ignore errors when getting status
1553
+ }
1554
+ }
1555
+ return {
1556
+ success: false,
1557
+ error: errorCode,
1558
+ output: (stdout + stderr).trim(),
1559
+ details
1560
+ };
1561
+ }
1562
+ }
1563
+ /**
1564
+ * Validate that git is installed
1565
+ */
1566
+ async validateGitInstalled() {
1567
+ try {
1568
+ await execAsync('git --version', { timeout: 5000 });
1569
+ return true;
1570
+ }
1571
+ catch {
1572
+ return false;
1573
+ }
1574
+ }
1575
+ /**
1576
+ * Check if directory is a git repository
1577
+ */
1578
+ async isGitRepository(cwd) {
1579
+ try {
1580
+ await execAsync('git rev-parse --git-dir', { cwd, timeout: 5000 });
1581
+ return true;
1582
+ }
1583
+ catch {
1584
+ return false;
1585
+ }
1586
+ }
1587
+ /**
1588
+ * Detect the git working directory (repository root)
1589
+ * @returns Path to the git repository root
1590
+ */
1591
+ async detectWorkingDirectory(startPath) {
1592
+ try {
1593
+ const { stdout } = await execAsync('git rev-parse --show-toplevel', {
1594
+ cwd: startPath || process.cwd(),
1595
+ timeout: 5000
1596
+ });
1597
+ return stdout.trim();
1598
+ }
1599
+ catch {
1600
+ return null;
1601
+ }
1602
+ }
1603
+ }
1604
+ exports.GitExecutor = GitExecutor;
1605
+ //# sourceMappingURL=git-executor.js.map