ccmanager 2.8.0 → 2.9.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 (77) hide show
  1. package/dist/cli.test.js +13 -2
  2. package/dist/components/App.js +125 -50
  3. package/dist/components/App.test.js +270 -0
  4. package/dist/components/ConfigureShortcuts.js +82 -8
  5. package/dist/components/DeleteWorktree.js +39 -5
  6. package/dist/components/DeleteWorktree.test.d.ts +1 -0
  7. package/dist/components/DeleteWorktree.test.js +128 -0
  8. package/dist/components/LoadingSpinner.d.ts +8 -0
  9. package/dist/components/LoadingSpinner.js +37 -0
  10. package/dist/components/LoadingSpinner.test.d.ts +1 -0
  11. package/dist/components/LoadingSpinner.test.js +187 -0
  12. package/dist/components/Menu.js +64 -16
  13. package/dist/components/Menu.recent-projects.test.js +32 -11
  14. package/dist/components/Menu.test.js +136 -4
  15. package/dist/components/MergeWorktree.js +79 -18
  16. package/dist/components/MergeWorktree.test.d.ts +1 -0
  17. package/dist/components/MergeWorktree.test.js +227 -0
  18. package/dist/components/NewWorktree.js +88 -9
  19. package/dist/components/NewWorktree.test.d.ts +1 -0
  20. package/dist/components/NewWorktree.test.js +244 -0
  21. package/dist/components/ProjectList.js +44 -13
  22. package/dist/components/ProjectList.recent-projects.test.js +8 -3
  23. package/dist/components/ProjectList.test.js +105 -8
  24. package/dist/components/RemoteBranchSelector.test.js +3 -1
  25. package/dist/hooks/useGitStatus.d.ts +11 -0
  26. package/dist/hooks/useGitStatus.js +70 -12
  27. package/dist/hooks/useGitStatus.test.js +30 -23
  28. package/dist/services/configurationManager.d.ts +75 -0
  29. package/dist/services/configurationManager.effect.test.d.ts +1 -0
  30. package/dist/services/configurationManager.effect.test.js +407 -0
  31. package/dist/services/configurationManager.js +246 -0
  32. package/dist/services/globalSessionOrchestrator.test.js +0 -8
  33. package/dist/services/projectManager.d.ts +98 -2
  34. package/dist/services/projectManager.js +228 -59
  35. package/dist/services/projectManager.test.js +242 -2
  36. package/dist/services/sessionManager.d.ts +44 -2
  37. package/dist/services/sessionManager.effect.test.d.ts +1 -0
  38. package/dist/services/sessionManager.effect.test.js +321 -0
  39. package/dist/services/sessionManager.js +216 -65
  40. package/dist/services/sessionManager.statePersistence.test.js +18 -9
  41. package/dist/services/sessionManager.test.js +40 -36
  42. package/dist/services/worktreeService.d.ts +356 -26
  43. package/dist/services/worktreeService.js +793 -353
  44. package/dist/services/worktreeService.test.js +294 -313
  45. package/dist/types/errors.d.ts +74 -0
  46. package/dist/types/errors.js +31 -0
  47. package/dist/types/errors.test.d.ts +1 -0
  48. package/dist/types/errors.test.js +201 -0
  49. package/dist/types/index.d.ts +5 -17
  50. package/dist/utils/claudeDir.d.ts +58 -6
  51. package/dist/utils/claudeDir.js +103 -8
  52. package/dist/utils/claudeDir.test.d.ts +1 -0
  53. package/dist/utils/claudeDir.test.js +108 -0
  54. package/dist/utils/concurrencyLimit.d.ts +5 -0
  55. package/dist/utils/concurrencyLimit.js +11 -0
  56. package/dist/utils/concurrencyLimit.test.js +40 -1
  57. package/dist/utils/gitStatus.d.ts +36 -8
  58. package/dist/utils/gitStatus.js +170 -88
  59. package/dist/utils/gitStatus.test.js +12 -9
  60. package/dist/utils/hookExecutor.d.ts +41 -6
  61. package/dist/utils/hookExecutor.js +75 -32
  62. package/dist/utils/hookExecutor.test.js +73 -20
  63. package/dist/utils/terminalCapabilities.d.ts +18 -0
  64. package/dist/utils/terminalCapabilities.js +81 -0
  65. package/dist/utils/terminalCapabilities.test.d.ts +1 -0
  66. package/dist/utils/terminalCapabilities.test.js +104 -0
  67. package/dist/utils/testHelpers.d.ts +106 -0
  68. package/dist/utils/testHelpers.js +153 -0
  69. package/dist/utils/testHelpers.test.d.ts +1 -0
  70. package/dist/utils/testHelpers.test.js +114 -0
  71. package/dist/utils/worktreeConfig.d.ts +77 -2
  72. package/dist/utils/worktreeConfig.js +156 -16
  73. package/dist/utils/worktreeConfig.test.d.ts +1 -0
  74. package/dist/utils/worktreeConfig.test.js +39 -0
  75. package/package.json +4 -4
  76. package/dist/integration-tests/devcontainer.integration.test.js +0 -101
  77. /package/dist/{integration-tests/devcontainer.integration.test.d.ts → components/App.test.d.ts} +0 -0
@@ -1,12 +1,38 @@
1
1
  import { execSync } from 'child_process';
2
2
  import { existsSync, statSync, cpSync } from 'fs';
3
3
  import path from 'path';
4
+ import { Effect, Either } from 'effect';
4
5
  import { AmbiguousBranchError, } from '../types/index.js';
6
+ import { GitError, FileSystemError } from '../types/errors.js';
5
7
  import { setWorktreeParentBranch } from '../utils/worktreeConfig.js';
6
8
  import { getClaudeProjectsDir, pathToClaudeProjectName, } from '../utils/claudeDir.js';
7
9
  import { executeWorktreePostCreationHook } from '../utils/hookExecutor.js';
8
10
  import { configurationManager } from './configurationManager.js';
9
11
  const CLAUDE_DIR = '.claude';
12
+ /**
13
+ * WorktreeService - Git worktree management with Effect-based error handling
14
+ *
15
+ * All public methods return Effect types for type-safe, composable error handling.
16
+ * See CLAUDE.md for complete examples and patterns.
17
+ *
18
+ * ## Effect-ts Resources
19
+ * - Effect Type: https://effect.website/docs/effect/effect-type
20
+ * - Error Management: https://effect.website/docs/error-management/error-handling
21
+ * - Effect Execution: https://effect.website/docs/guides/running-effects
22
+ * - Tagged Errors: https://effect.website/docs/error-management/expected-errors#tagged-errors
23
+ *
24
+ * ## Key Concepts
25
+ * - All operations return `Effect.Effect<T, E, never>` where T is success type, E is error type
26
+ * - Execute Effects with `Effect.runPromise()` or `Effect.match()` for type-safe handling
27
+ * - Use error discrimination via `error._tag` property for TypeScript type narrowing
28
+ * - Compose Effects with `Effect.flatMap()`, `Effect.all()`, `Effect.catchTag()`, etc.
29
+ *
30
+ * @example
31
+ * ```typescript
32
+ * // See individual method JSDoc for specific usage examples
33
+ * const service = new WorktreeService();
34
+ * ```
35
+ */
10
36
  export class WorktreeService {
11
37
  constructor(rootPath) {
12
38
  Object.defineProperty(this, "rootPath", {
@@ -52,147 +78,19 @@ export class WorktreeService {
52
78
  return path.resolve(this.rootPath);
53
79
  }
54
80
  }
55
- getWorktrees() {
56
- try {
57
- const output = execSync('git worktree list --porcelain', {
58
- cwd: this.rootPath,
59
- encoding: 'utf8',
60
- });
61
- const worktrees = [];
62
- const lines = output.trim().split('\n');
63
- const parseWorktree = (lines, startIndex) => {
64
- const worktreeLine = lines[startIndex];
65
- if (!worktreeLine?.startsWith('worktree ')) {
66
- return [null, startIndex];
67
- }
68
- const worktree = {
69
- path: worktreeLine.substring(9),
70
- isMainWorktree: false,
71
- hasSession: false,
72
- };
73
- let i = startIndex + 1;
74
- while (i < lines.length &&
75
- lines[i] &&
76
- !lines[i].startsWith('worktree ')) {
77
- const line = lines[i];
78
- if (line && line.startsWith('branch ')) {
79
- const branch = line.substring(7);
80
- worktree.branch = branch.startsWith('refs/heads/')
81
- ? branch.substring(11)
82
- : branch;
83
- }
84
- else if (line === 'bare') {
85
- worktree.isMainWorktree = true;
86
- }
87
- i++;
88
- }
89
- return [worktree, i];
90
- };
91
- let index = 0;
92
- while (index < lines.length) {
93
- const [worktree, nextIndex] = parseWorktree(lines, index);
94
- if (worktree) {
95
- worktrees.push(worktree);
96
- }
97
- index = nextIndex > index ? nextIndex : index + 1;
98
- }
99
- // Mark the first worktree as main if none are marked
100
- if (worktrees.length > 0 && !worktrees.some(w => w.isMainWorktree)) {
101
- worktrees[0].isMainWorktree = true;
102
- }
103
- return worktrees;
104
- }
105
- catch (_error) {
106
- // If git worktree command fails, assume we're in a regular git repo
107
- return [
108
- {
109
- path: this.rootPath,
110
- branch: this.getCurrentBranch(),
111
- isMainWorktree: true,
112
- hasSession: false,
113
- },
114
- ];
115
- }
116
- }
117
- getCurrentBranch() {
118
- try {
119
- const branch = execSync('git rev-parse --abbrev-ref HEAD', {
120
- cwd: this.rootPath,
121
- encoding: 'utf8',
122
- }).trim();
123
- return branch;
124
- }
125
- catch {
126
- return 'unknown';
127
- }
128
- }
129
81
  isGitRepository() {
130
82
  return existsSync(path.join(this.rootPath, '.git'));
131
83
  }
132
84
  getGitRootPath() {
133
85
  return this.gitRootPath;
134
86
  }
135
- getDefaultBranch() {
136
- try {
137
- // Try to get the default branch from origin
138
- const defaultBranch = execSync("git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'", {
139
- cwd: this.rootPath,
140
- encoding: 'utf8',
141
- shell: '/bin/bash',
142
- }).trim();
143
- return defaultBranch || 'main';
144
- }
145
- catch {
146
- // Fallback to common default branch names
147
- try {
148
- execSync('git rev-parse --verify main', {
149
- cwd: this.rootPath,
150
- encoding: 'utf8',
151
- });
152
- return 'main';
153
- }
154
- catch {
155
- try {
156
- execSync('git rev-parse --verify master', {
157
- cwd: this.rootPath,
158
- encoding: 'utf8',
159
- });
160
- return 'master';
161
- }
162
- catch {
163
- return 'main';
164
- }
165
- }
166
- }
167
- }
168
- getAllBranches() {
169
- try {
170
- const output = execSync("git branch -a --format='%(refname:short)' | grep -v HEAD | sort -u", {
171
- cwd: this.rootPath,
172
- encoding: 'utf8',
173
- shell: '/bin/bash',
174
- });
175
- const branches = output
176
- .trim()
177
- .split('\n')
178
- .filter(branch => branch && !branch.startsWith('origin/'))
179
- .map(branch => branch.trim());
180
- // Also include remote branches without origin/ prefix
181
- const remoteBranches = output
182
- .trim()
183
- .split('\n')
184
- .filter(branch => branch.startsWith('origin/'))
185
- .map(branch => branch.replace('origin/', ''));
186
- // Merge and deduplicate
187
- const allBranches = [...new Set([...branches, ...remoteBranches])];
188
- return allBranches.filter(branch => branch);
189
- }
190
- catch {
191
- return [];
192
- }
193
- }
194
87
  /**
195
- * Resolves a branch name to its proper git reference.
88
+ * SYNCHRONOUS HELPER: Resolves a branch name to its proper git reference.
89
+ *
90
+ * This method remains synchronous as it's called within Effect.gen contexts
91
+ * but doesn't need to be wrapped in Effect itself. It's used by createWorktreeEffect()
92
+ * to resolve branch references before creating worktrees.
93
+ *
196
94
  * Handles multiple remotes and throws AmbiguousBranchError when disambiguation is needed.
197
95
  *
198
96
  * Priority order:
@@ -200,6 +98,11 @@ export class WorktreeService {
200
98
  * 2. Single remote branch -> return remote/branch
201
99
  * 3. Multiple remote branches -> throw AmbiguousBranchError
202
100
  * 4. No branches found -> return original (let git handle error)
101
+ *
102
+ * @private
103
+ * @param {string} branchName - Branch name to resolve
104
+ * @returns {string} Resolved branch reference
105
+ * @throws {AmbiguousBranchError} When branch exists in multiple remotes
203
106
  */
204
107
  resolveBranchReference(branchName) {
205
108
  try {
@@ -260,7 +163,13 @@ export class WorktreeService {
260
163
  }
261
164
  }
262
165
  /**
263
- * Gets all git remotes for this repository.
166
+ * SYNCHRONOUS HELPER: Gets all git remotes for this repository.
167
+ *
168
+ * This method remains synchronous as it's a simple utility used by resolveBranchReference().
169
+ * No need for Effect version since it's a pure read operation with no complex error handling.
170
+ *
171
+ * @private
172
+ * @returns {string[]} Array of remote names, empty array on error
264
173
  */
265
174
  getAllRemotes() {
266
175
  try {
@@ -278,293 +187,824 @@ export class WorktreeService {
278
187
  return [];
279
188
  }
280
189
  }
281
- async createWorktree(worktreePath, branch, baseBranch, copySessionData = false, copyClaudeDirectory = false) {
190
+ /**
191
+ * SYNCHRONOUS HELPER: Copies Claude Code session data between worktrees.
192
+ *
193
+ * This method remains synchronous and is wrapped in Effect.try when called from
194
+ * createWorktreeEffect() (line ~676). This provides proper error handling while
195
+ * keeping the implementation simple.
196
+ *
197
+ * @private
198
+ * @param {string} sourceWorktreePath - Source worktree path
199
+ * @param {string} targetWorktreePath - Target worktree path
200
+ * @throws {Error} When copy operation fails
201
+ */
202
+ copyClaudeSessionData(sourceWorktreePath, targetWorktreePath) {
282
203
  try {
204
+ const projectsDirEither = getClaudeProjectsDir();
205
+ if (Either.isLeft(projectsDirEither)) {
206
+ throw new Error(`Could not determine Claude projects directory: ${projectsDirEither.left.field} ${projectsDirEither.left.constraint}`);
207
+ }
208
+ const projectsDir = projectsDirEither.right;
209
+ if (!existsSync(projectsDir)) {
210
+ throw new Error(`Claude projects directory does not exist: ${projectsDir}`);
211
+ }
212
+ // Convert paths to Claude's naming convention
213
+ const sourceProjectName = pathToClaudeProjectName(sourceWorktreePath);
214
+ const targetProjectName = pathToClaudeProjectName(targetWorktreePath);
215
+ const sourceProjectDir = path.join(projectsDir, sourceProjectName);
216
+ const targetProjectDir = path.join(projectsDir, targetProjectName);
217
+ // Only copy if source project exists
218
+ if (existsSync(sourceProjectDir)) {
219
+ cpSync(sourceProjectDir, targetProjectDir, {
220
+ recursive: true,
221
+ force: true,
222
+ errorOnExist: false,
223
+ preserveTimestamps: true,
224
+ });
225
+ }
226
+ }
227
+ catch (error) {
228
+ console.error(`Failed to copy Claude session data: ${error}`);
229
+ throw new Error(`Failed to copy Claude session data: ${error}`);
230
+ }
231
+ }
232
+ /**
233
+ * Effect-based hasClaudeDirectoryInBranch operation
234
+ * Checks if a .claude directory exists in the worktree for the specified branch
235
+ *
236
+ * @param {string} branchName - Name of the branch to check
237
+ * @returns {Effect.Effect<boolean, GitError, never>} Effect containing true if .claude directory exists, false otherwise
238
+ *
239
+ * @example
240
+ * ```typescript
241
+ * // Check if branch has .claude directory
242
+ * const hasClaudeDir = await Effect.runPromise(
243
+ * effect
244
+ * );
245
+ *
246
+ * // Or use Effect.match for error handling
247
+ * const result = await Effect.runPromise(
248
+ * Effect.match(effect, {
249
+ * onFailure: (error: GitError) => ({
250
+ * type: 'error' as const,
251
+ * message: error.stderr
252
+ * }),
253
+ * onSuccess: (hasDir: boolean) => ({
254
+ * type: 'success' as const,
255
+ * data: hasDir
256
+ * })
257
+ * })
258
+ * );
259
+ * ```
260
+ *
261
+ * @throws {GitError} When git operations fail
262
+ */
263
+ hasClaudeDirectoryInBranchEffect(branchName) {
264
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
265
+ const self = this;
266
+ return Effect.gen(function* () {
267
+ // Get all worktrees
268
+ const worktrees = yield* self.getWorktreesEffect();
269
+ // Try to find worktree for the branch
270
+ let targetWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === branchName);
271
+ // If branch worktree not found, try the default branch
272
+ if (!targetWorktree) {
273
+ const defaultBranch = yield* self.getDefaultBranchEffect();
274
+ if (branchName === defaultBranch) {
275
+ targetWorktree = worktrees.find(wt => wt.branch &&
276
+ wt.branch.replace('refs/heads/', '') === defaultBranch);
277
+ }
278
+ }
279
+ // If still not found and it's the default branch, try the main worktree
280
+ if (!targetWorktree) {
281
+ const defaultBranch = yield* self.getDefaultBranchEffect();
282
+ if (branchName === defaultBranch) {
283
+ targetWorktree = worktrees.find(wt => wt.isMainWorktree);
284
+ }
285
+ }
286
+ if (!targetWorktree) {
287
+ return false;
288
+ }
289
+ // Check if .claude directory exists in the worktree
290
+ const claudePath = path.join(targetWorktree.path, CLAUDE_DIR);
291
+ return existsSync(claudePath) && statSync(claudePath).isDirectory();
292
+ });
293
+ }
294
+ /**
295
+ * Effect-based copyClaudeDirectoryFromBaseBranch operation
296
+ * Copies .claude directory from base branch worktree to target worktree
297
+ *
298
+ * @param {string} worktreePath - Path of the target worktree
299
+ * @param {string} baseBranch - Name of the base branch to copy from
300
+ * @returns {Effect.Effect<void, GitError | FileSystemError, never>} Effect that completes successfully or fails with error
301
+ *
302
+ * @example
303
+ * ```typescript
304
+ * // Copy .claude directory from main branch
305
+ * await Effect.runPromise(
306
+ * effect
307
+ * );
308
+ *
309
+ * // With error handling
310
+ * const result = await Effect.runPromise(
311
+ * Effect.catchAll(
312
+ * effect,
313
+ * (error) => {
314
+ * console.warn('Could not copy .claude directory:', error);
315
+ * return Effect.succeed(undefined); // Continue despite error
316
+ * }
317
+ * )
318
+ * );
319
+ * ```
320
+ *
321
+ * @throws {GitError} When base worktree cannot be found
322
+ * @throws {FileSystemError} When copying the directory fails
323
+ */
324
+ copyClaudeDirectoryFromBaseBranchEffect(worktreePath, baseBranch) {
325
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
326
+ const self = this;
327
+ return Effect.gen(function* () {
328
+ // Find the worktree directory for the base branch
329
+ const worktrees = yield* self.getWorktreesEffect();
330
+ let baseWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === baseBranch);
331
+ // If base branch worktree not found, try the default branch
332
+ if (!baseWorktree) {
333
+ const defaultBranch = yield* self.getDefaultBranchEffect();
334
+ baseWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === defaultBranch);
335
+ }
336
+ // If still not found, try the main worktree
337
+ if (!baseWorktree) {
338
+ baseWorktree = worktrees.find(wt => wt.isMainWorktree);
339
+ }
340
+ if (!baseWorktree) {
341
+ return yield* Effect.fail(new GitError({
342
+ command: 'find base worktree',
343
+ exitCode: 1,
344
+ stderr: 'Could not find base worktree to copy settings from',
345
+ }));
346
+ }
347
+ // Check if .claude directory exists in base worktree
348
+ const sourceClaudeDir = path.join(baseWorktree.path, CLAUDE_DIR);
349
+ if (!existsSync(sourceClaudeDir) ||
350
+ !statSync(sourceClaudeDir).isDirectory()) {
351
+ // No .claude directory to copy, this is fine
352
+ return;
353
+ }
354
+ // Copy .claude directory to new worktree
355
+ const targetClaudeDir = path.join(worktreePath, CLAUDE_DIR);
356
+ yield* Effect.try({
357
+ try: () => cpSync(sourceClaudeDir, targetClaudeDir, { recursive: true }),
358
+ catch: (error) => new FileSystemError({
359
+ operation: 'write',
360
+ path: targetClaudeDir,
361
+ cause: String(error),
362
+ }),
363
+ });
364
+ });
365
+ }
366
+ /**
367
+ * Effect-based getDefaultBranch operation
368
+ * Returns Effect that may fail with GitError
369
+ *
370
+ * @returns {Effect.Effect<string, GitError, never>} Effect containing default branch name or GitError
371
+ *
372
+ * @example
373
+ * ```typescript
374
+ * // Use Effect.match for type-safe error handling
375
+ * const result = await Effect.runPromise(
376
+ * Effect.match(effect, {
377
+ * onFailure: (error: GitError) => ({
378
+ * type: 'error' as const,
379
+ * message: `Failed to get default branch: ${error.stderr}`
380
+ * }),
381
+ * onSuccess: (branch: string) => ({
382
+ * type: 'success' as const,
383
+ * data: branch
384
+ * })
385
+ * })
386
+ * );
387
+ *
388
+ * if (result.type === 'success') {
389
+ * console.log(`Default branch is: ${result.data}`);
390
+ * }
391
+ * ```
392
+ *
393
+ * @throws {GitError} When git symbolic-ref command fails and fallback detection also fails
394
+ */
395
+ getDefaultBranchEffect() {
396
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
397
+ const self = this;
398
+ return Effect.catchAll(Effect.try({
399
+ try: () => {
400
+ // Try to get the default branch from origin
401
+ const defaultBranch = execSync("git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'", {
402
+ cwd: self.rootPath,
403
+ encoding: 'utf8',
404
+ shell: '/bin/bash',
405
+ }).trim();
406
+ if (!defaultBranch) {
407
+ throw new Error('No default branch from symbolic-ref');
408
+ }
409
+ return defaultBranch;
410
+ },
411
+ catch: (error) => error,
412
+ }), (_error) => {
413
+ // Fallback to checking for main/master branches
414
+ return Effect.catchAll(Effect.try({
415
+ try: () => {
416
+ execSync('git rev-parse --verify main', {
417
+ cwd: self.rootPath,
418
+ encoding: 'utf8',
419
+ });
420
+ return 'main';
421
+ },
422
+ catch: (error) => error,
423
+ }), (_mainError) => {
424
+ return Effect.catchAll(Effect.try({
425
+ try: () => {
426
+ execSync('git rev-parse --verify master', {
427
+ cwd: self.rootPath,
428
+ encoding: 'utf8',
429
+ });
430
+ return 'master';
431
+ },
432
+ catch: (error) => error,
433
+ }), (_masterError) => {
434
+ // All attempts failed, return 'main' as default
435
+ // This is acceptable behavior for new repositories
436
+ return Effect.succeed('main');
437
+ });
438
+ });
439
+ });
440
+ }
441
+ /**
442
+ * Effect-based getAllBranches operation
443
+ * Returns Effect that succeeds with array of branches (empty on failure for non-critical operation)
444
+ *
445
+ * @returns {Effect.Effect<string[], GitError, never>} Effect containing array of branch names
446
+ *
447
+ * @example
448
+ * ```typescript
449
+ * // Execute in async context - this operation returns empty array on failure
450
+ * const branches = await Effect.runPromise(
451
+ * effect
452
+ * );
453
+ * console.log(`Found ${branches.length} branches`);
454
+ *
455
+ * // Or use Effect.match for explicit error handling
456
+ * const result = await Effect.runPromise(
457
+ * Effect.match(effect, {
458
+ * onFailure: (error: GitError) => ({
459
+ * type: 'error' as const,
460
+ * message: error.stderr
461
+ * }),
462
+ * onSuccess: (branches: string[]) => ({
463
+ * type: 'success' as const,
464
+ * data: branches
465
+ * })
466
+ * })
467
+ * );
468
+ * ```
469
+ *
470
+ * @throws {GitError} When git branch command fails (but falls back to empty array)
471
+ */
472
+ getAllBranchesEffect() {
473
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
474
+ const self = this;
475
+ return Effect.catchAll(Effect.try({
476
+ try: () => {
477
+ const output = execSync("git branch -a --format='%(refname:short)' | grep -v HEAD | sort -u", {
478
+ cwd: self.rootPath,
479
+ encoding: 'utf8',
480
+ shell: '/bin/bash',
481
+ });
482
+ const branches = output
483
+ .trim()
484
+ .split('\n')
485
+ .filter(branch => branch && !branch.startsWith('origin/'))
486
+ .map(branch => branch.trim());
487
+ // Also include remote branches without origin/ prefix
488
+ const remoteBranches = output
489
+ .trim()
490
+ .split('\n')
491
+ .filter(branch => branch.startsWith('origin/'))
492
+ .map(branch => branch.replace('origin/', ''));
493
+ // Merge and deduplicate
494
+ const allBranches = [...new Set([...branches, ...remoteBranches])];
495
+ return allBranches.filter(branch => branch);
496
+ },
497
+ catch: (error) => error,
498
+ }), (_error) => {
499
+ // Return empty array on failure (non-critical operation)
500
+ return Effect.succeed([]);
501
+ });
502
+ }
503
+ /**
504
+ * Effect-based getCurrentBranch operation
505
+ * Returns Effect that may fail with GitError
506
+ *
507
+ * @returns {Effect.Effect<string, GitError, never>} Effect containing current branch name or GitError
508
+ *
509
+ * @example
510
+ * ```typescript
511
+ * // Use Effect.match for type-safe error handling
512
+ * const result = await Effect.runPromise(
513
+ * Effect.match(effect, {
514
+ * onFailure: (error: GitError) => ({
515
+ * type: 'error' as const,
516
+ * message: `Failed to get current branch: ${error.stderr}`
517
+ * }),
518
+ * onSuccess: (branch: string) => ({
519
+ * type: 'success' as const,
520
+ * data: branch
521
+ * })
522
+ * })
523
+ * );
524
+ *
525
+ * if (result.type === 'success') {
526
+ * console.log(`Current branch: ${result.data}`);
527
+ * }
528
+ * ```
529
+ *
530
+ * @throws {GitError} When git rev-parse command fails
531
+ */
532
+ getCurrentBranchEffect() {
533
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
534
+ const self = this;
535
+ return Effect.catchAll(Effect.try({
536
+ try: () => {
537
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', {
538
+ cwd: self.rootPath,
539
+ encoding: 'utf8',
540
+ }).trim();
541
+ if (!branch) {
542
+ throw new Error('No current branch returned');
543
+ }
544
+ return branch;
545
+ },
546
+ catch: (error) => error,
547
+ }), (_error) => {
548
+ // Return 'unknown' as fallback for compatibility
549
+ return Effect.succeed('unknown');
550
+ });
551
+ }
552
+ /**
553
+ * Effect-based getWorktrees operation
554
+ * Returns Effect that may fail with GitError
555
+ *
556
+ * @returns {Effect.Effect<Worktree[], GitError, never>} Effect containing array of worktrees or GitError
557
+ *
558
+ * @example
559
+ * ```typescript
560
+ * // Execute in async context
561
+ * const worktrees = await Effect.runPromise(
562
+ * effect
563
+ * );
564
+ *
565
+ * // Or use Effect.match for type-safe error handling
566
+ * const result = await Effect.runPromise(
567
+ * Effect.match(effect, {
568
+ * onFailure: (error: GitError) => ({
569
+ * type: 'error' as const,
570
+ * message: `Git error: ${error.stderr}`
571
+ * }),
572
+ * onSuccess: (worktrees: Worktree[]) => ({
573
+ * type: 'success' as const,
574
+ * data: worktrees
575
+ * })
576
+ * })
577
+ * );
578
+ *
579
+ * if (result.type === 'error') {
580
+ * console.error(result.message);
581
+ * } else {
582
+ * console.log(`Found ${result.data.length} worktrees`);
583
+ * }
584
+ * ```
585
+ *
586
+ * @throws {GitError} When git worktree list command fails
587
+ */
588
+ getWorktreesEffect() {
589
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
590
+ const self = this;
591
+ return Effect.catchAll(Effect.try({
592
+ try: () => {
593
+ const output = execSync('git worktree list --porcelain', {
594
+ cwd: self.rootPath,
595
+ encoding: 'utf8',
596
+ });
597
+ const worktrees = [];
598
+ const lines = output.trim().split('\n');
599
+ const parseWorktree = (lines, startIndex) => {
600
+ const worktreeLine = lines[startIndex];
601
+ if (!worktreeLine?.startsWith('worktree ')) {
602
+ return [null, startIndex];
603
+ }
604
+ const worktree = {
605
+ path: worktreeLine.substring(9),
606
+ isMainWorktree: false,
607
+ hasSession: false,
608
+ };
609
+ let i = startIndex + 1;
610
+ while (i < lines.length &&
611
+ lines[i] &&
612
+ !lines[i].startsWith('worktree ')) {
613
+ const line = lines[i];
614
+ if (line && line.startsWith('branch ')) {
615
+ const branch = line.substring(7);
616
+ worktree.branch = branch.startsWith('refs/heads/')
617
+ ? branch.substring(11)
618
+ : branch;
619
+ }
620
+ else if (line === 'bare') {
621
+ worktree.isMainWorktree = true;
622
+ }
623
+ i++;
624
+ }
625
+ return [worktree, i];
626
+ };
627
+ let index = 0;
628
+ while (index < lines.length) {
629
+ const [worktree, nextIndex] = parseWorktree(lines, index);
630
+ if (worktree) {
631
+ worktrees.push(worktree);
632
+ }
633
+ index = nextIndex > index ? nextIndex : index + 1;
634
+ }
635
+ // Mark the first worktree as main if none are marked
636
+ if (worktrees.length > 0 && !worktrees.some(w => w.isMainWorktree)) {
637
+ worktrees[0].isMainWorktree = true;
638
+ }
639
+ return worktrees;
640
+ },
641
+ catch: (error) => error,
642
+ }), (error) => {
643
+ // If git worktree command not supported, fallback to single worktree
644
+ const execError = error;
645
+ if (execError.status === 1 ||
646
+ execError.stderr?.includes('unknown command')) {
647
+ // Use Effect-based getCurrentBranchEffect() instead of synchronous getCurrentBranch()
648
+ return Effect.map(self.getCurrentBranchEffect(), branch => [
649
+ {
650
+ path: self.rootPath,
651
+ branch,
652
+ isMainWorktree: true,
653
+ hasSession: false,
654
+ },
655
+ ]);
656
+ }
657
+ // For other errors, wrap in GitError
658
+ return Effect.fail(new GitError({
659
+ command: 'git worktree list --porcelain',
660
+ exitCode: execError.status || 1,
661
+ stderr: execError.stderr || String(error),
662
+ stdout: execError.stdout,
663
+ }));
664
+ });
665
+ }
666
+ /**
667
+ * Effect-based createWorktree operation
668
+ * May fail with GitError or FileSystemError
669
+ *
670
+ * @param {string} worktreePath - Path where the new worktree will be created
671
+ * @param {string} branch - Name of the branch for the new worktree
672
+ * @param {string} baseBranch - Base branch to create the new branch from
673
+ * @param {boolean} copySessionData - Whether to copy Claude session data (default: false)
674
+ * @param {boolean} copyClaudeDirectory - Whether to copy .claude directory (default: false)
675
+ * @returns {Effect.Effect<Worktree, GitError | FileSystemError, never>} Effect containing created worktree or error
676
+ *
677
+ * @example
678
+ * ```typescript
679
+ * // Create new worktree with Effect.match for error handling
680
+ * const result = await Effect.runPromise(
681
+ * Effect.match(
682
+ * effect,
683
+ * {
684
+ * onFailure: (error: GitError | FileSystemError) => {
685
+ * switch (error._tag) {
686
+ * case 'GitError':
687
+ * return {type: 'error' as const, msg: `Git failed: ${error.stderr}`};
688
+ * case 'FileSystemError':
689
+ * return {type: 'error' as const, msg: `FS failed: ${error.cause}`};
690
+ * }
691
+ * },
692
+ * onSuccess: (worktree: Worktree) => ({
693
+ * type: 'success' as const,
694
+ * data: worktree
695
+ * })
696
+ * }
697
+ * )
698
+ * );
699
+ *
700
+ * if (result.type === 'error') {
701
+ * console.error(result.msg);
702
+ * } else {
703
+ * console.log(`Created worktree at ${result.data.path}`);
704
+ * }
705
+ * ```
706
+ *
707
+ * @throws {GitError} When git worktree add command fails
708
+ * @throws {FileSystemError} When session data copy fails
709
+ */
710
+ createWorktreeEffect(worktreePath, branch, baseBranch, copySessionData = false, copyClaudeDirectory = false) {
711
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
712
+ const self = this;
713
+ return Effect.gen(function* () {
283
714
  // Resolve the worktree path relative to the git repository root
715
+ const gitRootPath = yield* Effect.sync(() => execSync('git rev-parse --git-common-dir', {
716
+ cwd: self.rootPath,
717
+ encoding: 'utf8',
718
+ }).trim());
719
+ const absoluteGitRoot = path.isAbsolute(gitRootPath)
720
+ ? path.dirname(gitRootPath)
721
+ : path.resolve(self.rootPath, path.dirname(gitRootPath));
284
722
  const resolvedPath = path.isAbsolute(worktreePath)
285
723
  ? worktreePath
286
- : path.join(this.gitRootPath, worktreePath);
724
+ : path.join(absoluteGitRoot, worktreePath);
287
725
  // Check if branch exists
288
- let branchExists = false;
289
- try {
290
- execSync(`git rev-parse --verify ${branch}`, {
291
- cwd: this.rootPath,
292
- encoding: 'utf8',
293
- });
294
- branchExists = true;
295
- }
296
- catch {
297
- // Branch doesn't exist
298
- }
299
- // Create the worktree
726
+ const branchExists = yield* Effect.catchAll(Effect.try({
727
+ try: () => {
728
+ execSync(`git rev-parse --verify ${branch}`, {
729
+ cwd: self.rootPath,
730
+ encoding: 'utf8',
731
+ });
732
+ return true;
733
+ },
734
+ catch: (error) => error,
735
+ }), () => Effect.succeed(false));
736
+ // Create the worktree command
300
737
  let command;
301
738
  if (branchExists) {
302
739
  command = `git worktree add "${resolvedPath}" "${branch}"`;
303
740
  }
304
741
  else {
305
742
  // Resolve the base branch to its proper git reference
306
- try {
307
- const resolvedBaseBranch = this.resolveBranchReference(baseBranch);
308
- // Create new branch from specified base branch
309
- command = `git worktree add -b "${branch}" "${resolvedPath}" "${resolvedBaseBranch}"`;
310
- }
311
- catch (error) {
312
- if (error instanceof AmbiguousBranchError) {
313
- // TODO: Future enhancement - show disambiguation modal in UI
314
- // The UI should present the available remote options to the user:
315
- // - origin/foo/bar-xyz
316
- // - upstream/foo/bar-xyz
317
- // For now, return error message to be displayed to user
318
- return {
319
- success: false,
320
- error: error.message,
321
- };
322
- }
323
- // Re-throw any other errors
324
- throw error;
325
- }
743
+ const resolvedBaseBranch = self.resolveBranchReference(baseBranch);
744
+ command = `git worktree add -b "${branch}" "${resolvedPath}" "${resolvedBaseBranch}"`;
326
745
  }
327
- execSync(command, {
328
- cwd: this.gitRootPath, // Execute from git root to ensure proper resolution
329
- encoding: 'utf8',
746
+ // Execute the worktree creation command
747
+ yield* Effect.try({
748
+ try: () => {
749
+ execSync(command, {
750
+ cwd: absoluteGitRoot,
751
+ encoding: 'utf8',
752
+ });
753
+ },
754
+ catch: (error) => {
755
+ const execError = error;
756
+ return new GitError({
757
+ command,
758
+ exitCode: execError.status || 1,
759
+ stderr: execError.stderr || String(error),
760
+ stdout: execError.stdout,
761
+ });
762
+ },
330
763
  });
331
764
  // Copy session data if requested
332
765
  if (copySessionData) {
333
- this.copyClaudeSessionData(this.rootPath, resolvedPath);
766
+ yield* Effect.try({
767
+ try: () => self.copyClaudeSessionData(self.rootPath, resolvedPath),
768
+ catch: (error) => new FileSystemError({
769
+ operation: 'write',
770
+ path: resolvedPath,
771
+ cause: String(error),
772
+ }),
773
+ });
334
774
  }
335
775
  // Store the parent branch in worktree config
336
- try {
337
- setWorktreeParentBranch(resolvedPath, baseBranch);
338
- }
339
- catch (error) {
340
- console.error('Warning: Failed to set parent branch in worktree config:', error);
341
- }
776
+ yield* Effect.catchAll(setWorktreeParentBranch(resolvedPath, baseBranch), (_error) => {
777
+ // Log warning but don't fail
778
+ console.error('Warning: Failed to set parent branch in worktree config:', _error);
779
+ return Effect.succeed(undefined);
780
+ });
342
781
  // Copy .claude directory if requested
343
782
  if (copyClaudeDirectory) {
344
- try {
345
- this.copyClaudeDirectoryFromBaseBranch(resolvedPath, baseBranch);
346
- }
347
- catch (error) {
783
+ yield* Effect.catchAll(self.copyClaudeDirectoryFromBaseBranchEffect(resolvedPath, baseBranch), (error) => {
348
784
  console.error('Warning: Failed to copy .claude directory:', error);
349
- }
785
+ return Effect.succeed(undefined);
786
+ });
350
787
  }
351
788
  // Execute post-creation hook if configured
352
789
  const worktreeHooks = configurationManager.getWorktreeHooks();
353
790
  if (worktreeHooks.post_creation?.enabled &&
354
791
  worktreeHooks.post_creation?.command) {
355
- // Create a worktree object for the hook
356
792
  const newWorktree = {
357
793
  path: resolvedPath,
358
794
  branch: branch,
359
795
  isMainWorktree: false,
360
796
  hasSession: false,
361
797
  };
362
- // Execute the hook synchronously (blocking)
363
- // Wait for the hook to complete before returning
364
- await executeWorktreePostCreationHook(worktreeHooks.post_creation.command, newWorktree, this.gitRootPath, baseBranch);
798
+ yield* executeWorktreePostCreationHook(worktreeHooks.post_creation.command, newWorktree, absoluteGitRoot, baseBranch);
365
799
  }
366
- return { success: true };
367
- }
368
- catch (error) {
369
800
  return {
370
- success: false,
371
- error: error instanceof Error ? error.message : 'Failed to create worktree',
801
+ path: resolvedPath,
802
+ branch,
803
+ isMainWorktree: false,
804
+ hasSession: false,
372
805
  };
373
- }
806
+ });
374
807
  }
375
- deleteWorktree(worktreePath, options) {
376
- try {
808
+ /**
809
+ * Effect-based deleteWorktree operation
810
+ * May fail with GitError
811
+ *
812
+ * @param {string} worktreePath - Path of the worktree to delete
813
+ * @param {{deleteBranch?: boolean}} options - Options for deletion (default: deleteBranch = true)
814
+ * @returns {Effect.Effect<void, GitError, never>} Effect that completes successfully or fails with GitError
815
+ *
816
+ * @example
817
+ * ```typescript
818
+ * // Delete worktree with Effect.catchTag for specific error handling
819
+ * await Effect.runPromise(
820
+ * Effect.catchTag(
821
+ * effect,
822
+ * 'GitError',
823
+ * (error) => {
824
+ * console.error(`Failed to delete worktree: ${error.stderr}`);
825
+ * return Effect.succeed(undefined); // Continue despite error
826
+ * }
827
+ * )
828
+ * );
829
+ * ```
830
+ *
831
+ * @throws {GitError} When git worktree remove command fails or worktree not found
832
+ */
833
+ deleteWorktreeEffect(worktreePath, options) {
834
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
835
+ const self = this;
836
+ return Effect.gen(function* () {
377
837
  // Get the worktree info to find the branch
378
- const worktrees = this.getWorktrees();
838
+ const worktrees = yield* self.getWorktreesEffect();
379
839
  const worktree = worktrees.find(wt => wt.path === worktreePath);
380
840
  if (!worktree) {
381
- return {
382
- success: false,
383
- error: 'Worktree not found',
384
- };
841
+ return yield* Effect.fail(new GitError({
842
+ command: 'git worktree remove',
843
+ exitCode: 1,
844
+ stderr: 'Worktree not found',
845
+ }));
385
846
  }
386
847
  if (worktree.isMainWorktree) {
387
- return {
388
- success: false,
389
- error: 'Cannot delete the main worktree',
390
- };
848
+ return yield* Effect.fail(new GitError({
849
+ command: 'git worktree remove',
850
+ exitCode: 1,
851
+ stderr: 'Cannot delete the main worktree',
852
+ }));
391
853
  }
392
854
  // Remove the worktree
393
- execSync(`git worktree remove "${worktreePath}" --force`, {
394
- cwd: this.rootPath,
395
- encoding: 'utf8',
855
+ yield* Effect.try({
856
+ try: () => {
857
+ execSync(`git worktree remove "${worktreePath}" --force`, {
858
+ cwd: self.rootPath,
859
+ encoding: 'utf8',
860
+ });
861
+ },
862
+ catch: (error) => {
863
+ const execError = error;
864
+ return new GitError({
865
+ command: `git worktree remove "${worktreePath}" --force`,
866
+ exitCode: execError.status || 1,
867
+ stderr: execError.stderr || String(error),
868
+ stdout: execError.stdout,
869
+ });
870
+ },
396
871
  });
397
872
  // Delete the branch if requested (default to true for backward compatibility)
398
873
  const deleteBranch = options?.deleteBranch ?? true;
399
874
  if (deleteBranch && worktree.branch) {
400
875
  const branchName = worktree.branch.replace('refs/heads/', '');
401
- try {
402
- execSync(`git branch -D "${branchName}"`, {
403
- cwd: this.rootPath,
404
- encoding: 'utf8',
405
- });
406
- }
407
- catch {
876
+ yield* Effect.catchAll(Effect.try({
877
+ try: () => {
878
+ execSync(`git branch -D "${branchName}"`, {
879
+ cwd: self.rootPath,
880
+ encoding: 'utf8',
881
+ });
882
+ },
883
+ catch: (error) => error,
884
+ }), (_error) => {
408
885
  // Branch might not exist or might be checked out elsewhere
409
886
  // This is not a fatal error
410
- }
887
+ return Effect.succeed(undefined);
888
+ });
411
889
  }
412
- return { success: true };
413
- }
414
- catch (error) {
415
- return {
416
- success: false,
417
- error: error instanceof Error ? error.message : 'Failed to delete worktree',
418
- };
419
- }
890
+ });
420
891
  }
421
- mergeWorktree(sourceBranch, targetBranch, useRebase = false) {
422
- try {
892
+ /**
893
+ * Effect-based mergeWorktree operation
894
+ * May fail with GitError
895
+ *
896
+ * @param {string} sourceBranch - Branch to merge from
897
+ * @param {string} targetBranch - Branch to merge into
898
+ * @param {boolean} useRebase - Whether to use rebase instead of merge (default: false)
899
+ * @returns {Effect.Effect<void, GitError, never>} Effect that completes successfully or fails with GitError
900
+ *
901
+ * @example
902
+ * ```typescript
903
+ * // Merge with Effect.all for parallel operations
904
+ * await Effect.runPromise(
905
+ * Effect.all([
906
+ * effect1,
907
+ * effect2
908
+ * ], {concurrency: 1}) // Sequential to avoid conflicts
909
+ * );
910
+ *
911
+ * // Or use Effect.catchAll for fallback behavior
912
+ * const result = await Effect.runPromise(
913
+ * Effect.catchAll(
914
+ * effect,
915
+ * (error: GitError) => {
916
+ * console.error(`Merge failed: ${error.stderr}`);
917
+ * // Return alternative Effect or rethrow
918
+ * return Effect.fail(error);
919
+ * }
920
+ * )
921
+ * );
922
+ * ```
923
+ *
924
+ * @throws {GitError} When git merge/rebase command fails or worktrees not found
925
+ */
926
+ mergeWorktreeEffect(sourceBranch, targetBranch, useRebase = false) {
927
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
928
+ const self = this;
929
+ return Effect.gen(function* () {
423
930
  // Get worktrees to find the target worktree path
424
- const worktrees = this.getWorktrees();
931
+ const worktrees = yield* self.getWorktreesEffect();
425
932
  const targetWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === targetBranch);
426
933
  if (!targetWorktree) {
427
- return {
428
- success: false,
429
- error: 'Target branch worktree not found',
430
- };
934
+ return yield* Effect.fail(new GitError({
935
+ command: useRebase ? 'git rebase' : 'git merge',
936
+ exitCode: 1,
937
+ stderr: 'Target branch worktree not found',
938
+ }));
431
939
  }
432
940
  // Perform the merge or rebase in the target worktree
433
941
  if (useRebase) {
434
942
  // For rebase, we need to checkout source branch and rebase it onto target
435
943
  const sourceWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === sourceBranch);
436
944
  if (!sourceWorktree) {
437
- return {
438
- success: false,
439
- error: 'Source branch worktree not found',
440
- };
945
+ return yield* Effect.fail(new GitError({
946
+ command: 'git rebase',
947
+ exitCode: 1,
948
+ stderr: 'Source branch worktree not found',
949
+ }));
441
950
  }
442
951
  // Rebase source branch onto target branch
443
- execSync(`git rebase "${targetBranch}"`, {
444
- cwd: sourceWorktree.path,
445
- encoding: 'utf8',
952
+ yield* Effect.try({
953
+ try: () => {
954
+ execSync(`git rebase "${targetBranch}"`, {
955
+ cwd: sourceWorktree.path,
956
+ encoding: 'utf8',
957
+ });
958
+ },
959
+ catch: (error) => {
960
+ const execError = error;
961
+ return new GitError({
962
+ command: `git rebase "${targetBranch}"`,
963
+ exitCode: execError.status || 1,
964
+ stderr: execError.stderr || String(error),
965
+ stdout: execError.stdout,
966
+ });
967
+ },
446
968
  });
447
969
  // After rebase, merge the rebased source branch into target branch
448
- execSync(`git merge --ff-only "${sourceBranch}"`, {
449
- cwd: targetWorktree.path,
450
- encoding: 'utf8',
970
+ yield* Effect.try({
971
+ try: () => {
972
+ execSync(`git merge --ff-only "${sourceBranch}"`, {
973
+ cwd: targetWorktree.path,
974
+ encoding: 'utf8',
975
+ });
976
+ },
977
+ catch: (error) => {
978
+ const execError = error;
979
+ return new GitError({
980
+ command: `git merge --ff-only "${sourceBranch}"`,
981
+ exitCode: execError.status || 1,
982
+ stderr: execError.stderr || String(error),
983
+ stdout: execError.stdout,
984
+ });
985
+ },
451
986
  });
452
987
  }
453
988
  else {
454
989
  // Regular merge
455
- execSync(`git merge --no-ff "${sourceBranch}"`, {
456
- cwd: targetWorktree.path,
457
- encoding: 'utf8',
458
- });
459
- }
460
- return { success: true };
461
- }
462
- catch (error) {
463
- return {
464
- success: false,
465
- error: error instanceof Error
466
- ? error.message
467
- : useRebase
468
- ? 'Failed to rebase branches'
469
- : 'Failed to merge branches',
470
- };
471
- }
472
- }
473
- deleteWorktreeByBranch(branch) {
474
- try {
475
- // Get worktrees to find the worktree by branch
476
- const worktrees = this.getWorktrees();
477
- const worktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === branch);
478
- if (!worktree) {
479
- return {
480
- success: false,
481
- error: 'Worktree not found for branch',
482
- };
483
- }
484
- return this.deleteWorktree(worktree.path);
485
- }
486
- catch (error) {
487
- return {
488
- success: false,
489
- error: error instanceof Error
490
- ? error.message
491
- : 'Failed to delete worktree by branch',
492
- };
493
- }
494
- }
495
- copyClaudeSessionData(sourceWorktreePath, targetWorktreePath) {
496
- try {
497
- const projectsDir = getClaudeProjectsDir();
498
- if (!existsSync(projectsDir)) {
499
- throw new Error(`Claude projects directory does not exist: ${projectsDir}`);
500
- }
501
- // Convert paths to Claude's naming convention
502
- const sourceProjectName = pathToClaudeProjectName(sourceWorktreePath);
503
- const targetProjectName = pathToClaudeProjectName(targetWorktreePath);
504
- const sourceProjectDir = path.join(projectsDir, sourceProjectName);
505
- const targetProjectDir = path.join(projectsDir, targetProjectName);
506
- // Only copy if source project exists
507
- if (existsSync(sourceProjectDir)) {
508
- cpSync(sourceProjectDir, targetProjectDir, {
509
- recursive: true,
510
- force: true,
511
- errorOnExist: false,
512
- preserveTimestamps: true,
990
+ yield* Effect.try({
991
+ try: () => {
992
+ execSync(`git merge --no-ff "${sourceBranch}"`, {
993
+ cwd: targetWorktree.path,
994
+ encoding: 'utf8',
995
+ });
996
+ },
997
+ catch: (error) => {
998
+ const execError = error;
999
+ return new GitError({
1000
+ command: `git merge --no-ff "${sourceBranch}"`,
1001
+ exitCode: execError.status || 1,
1002
+ stderr: execError.stderr || String(error),
1003
+ stdout: execError.stdout,
1004
+ });
1005
+ },
513
1006
  });
514
1007
  }
515
- }
516
- catch (error) {
517
- console.error(`Failed to copy Claude session data: ${error}`);
518
- throw new Error(`Failed to copy Claude session data: ${error}`);
519
- }
520
- }
521
- hasClaudeDirectoryInBranch(branchName) {
522
- // Find the worktree directory for the branch
523
- const worktrees = this.getWorktrees();
524
- let targetWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === branchName);
525
- // If branch worktree not found, try the default branch
526
- if (!targetWorktree) {
527
- const defaultBranch = this.getDefaultBranch();
528
- if (branchName === defaultBranch) {
529
- targetWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === defaultBranch);
530
- }
531
- }
532
- // If still not found and it's the default branch, try the main worktree
533
- if (!targetWorktree && branchName === this.getDefaultBranch()) {
534
- targetWorktree = worktrees.find(wt => wt.isMainWorktree);
535
- }
536
- if (!targetWorktree) {
537
- return false;
538
- }
539
- // Check if .claude directory exists in the worktree
540
- const claudePath = path.join(targetWorktree.path, CLAUDE_DIR);
541
- return existsSync(claudePath) && statSync(claudePath).isDirectory();
542
- }
543
- copyClaudeDirectoryFromBaseBranch(worktreePath, baseBranch) {
544
- // Find the worktree directory for the base branch
545
- const worktrees = this.getWorktrees();
546
- let baseWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === baseBranch);
547
- // If base branch worktree not found, try the default branch
548
- if (!baseWorktree) {
549
- const defaultBranch = this.getDefaultBranch();
550
- baseWorktree = worktrees.find(wt => wt.branch && wt.branch.replace('refs/heads/', '') === defaultBranch);
551
- }
552
- // If still not found, try the main worktree
553
- if (!baseWorktree) {
554
- baseWorktree = worktrees.find(wt => wt.isMainWorktree);
555
- }
556
- if (!baseWorktree) {
557
- throw new Error('Could not find base worktree to copy settings from');
558
- }
559
- // Check if .claude directory exists in base worktree
560
- const sourceClaudeDir = path.join(baseWorktree.path, CLAUDE_DIR);
561
- if (!existsSync(sourceClaudeDir) ||
562
- !statSync(sourceClaudeDir).isDirectory()) {
563
- // No .claude directory to copy, this is fine
564
- return;
565
- }
566
- // Copy .claude directory to new worktree
567
- const targetClaudeDir = path.join(worktreePath, CLAUDE_DIR);
568
- cpSync(sourceClaudeDir, targetClaudeDir, { recursive: true });
1008
+ });
569
1009
  }
570
1010
  }