ccmanager 2.8.0 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.test.js +13 -2
- package/dist/components/App.js +125 -50
- package/dist/components/App.test.js +270 -0
- package/dist/components/ConfigureShortcuts.js +82 -8
- package/dist/components/DeleteWorktree.js +39 -5
- package/dist/components/DeleteWorktree.test.d.ts +1 -0
- package/dist/components/DeleteWorktree.test.js +128 -0
- package/dist/components/LoadingSpinner.d.ts +8 -0
- package/dist/components/LoadingSpinner.js +37 -0
- package/dist/components/LoadingSpinner.test.d.ts +1 -0
- package/dist/components/LoadingSpinner.test.js +187 -0
- package/dist/components/Menu.js +64 -16
- package/dist/components/Menu.recent-projects.test.js +32 -11
- package/dist/components/Menu.test.js +136 -4
- package/dist/components/MergeWorktree.js +79 -18
- package/dist/components/MergeWorktree.test.d.ts +1 -0
- package/dist/components/MergeWorktree.test.js +227 -0
- package/dist/components/NewWorktree.js +88 -9
- package/dist/components/NewWorktree.test.d.ts +1 -0
- package/dist/components/NewWorktree.test.js +244 -0
- package/dist/components/ProjectList.js +44 -13
- package/dist/components/ProjectList.recent-projects.test.js +8 -3
- package/dist/components/ProjectList.test.js +105 -8
- package/dist/components/RemoteBranchSelector.test.js +3 -1
- package/dist/components/Session.js +11 -6
- package/dist/hooks/useGitStatus.d.ts +11 -0
- package/dist/hooks/useGitStatus.js +70 -12
- package/dist/hooks/useGitStatus.test.js +30 -23
- package/dist/services/configurationManager.d.ts +75 -0
- package/dist/services/configurationManager.effect.test.d.ts +1 -0
- package/dist/services/configurationManager.effect.test.js +407 -0
- package/dist/services/configurationManager.js +246 -0
- package/dist/services/globalSessionOrchestrator.test.js +0 -8
- package/dist/services/projectManager.d.ts +98 -2
- package/dist/services/projectManager.js +228 -59
- package/dist/services/projectManager.test.js +242 -2
- package/dist/services/sessionManager.d.ts +44 -2
- package/dist/services/sessionManager.effect.test.d.ts +1 -0
- package/dist/services/sessionManager.effect.test.js +321 -0
- package/dist/services/sessionManager.js +216 -65
- package/dist/services/sessionManager.statePersistence.test.js +18 -9
- package/dist/services/sessionManager.test.js +40 -36
- package/dist/services/shortcutManager.d.ts +2 -0
- package/dist/services/shortcutManager.js +53 -0
- package/dist/services/shortcutManager.test.d.ts +1 -0
- package/dist/services/shortcutManager.test.js +30 -0
- package/dist/services/worktreeService.d.ts +356 -26
- package/dist/services/worktreeService.js +793 -353
- package/dist/services/worktreeService.test.js +294 -313
- package/dist/types/errors.d.ts +74 -0
- package/dist/types/errors.js +31 -0
- package/dist/types/errors.test.d.ts +1 -0
- package/dist/types/errors.test.js +201 -0
- package/dist/types/index.d.ts +5 -17
- package/dist/utils/claudeDir.d.ts +58 -6
- package/dist/utils/claudeDir.js +103 -8
- package/dist/utils/claudeDir.test.d.ts +1 -0
- package/dist/utils/claudeDir.test.js +108 -0
- package/dist/utils/concurrencyLimit.d.ts +5 -0
- package/dist/utils/concurrencyLimit.js +11 -0
- package/dist/utils/concurrencyLimit.test.js +40 -1
- package/dist/utils/gitStatus.d.ts +36 -8
- package/dist/utils/gitStatus.js +170 -88
- package/dist/utils/gitStatus.test.js +12 -9
- package/dist/utils/hookExecutor.d.ts +41 -6
- package/dist/utils/hookExecutor.js +75 -32
- package/dist/utils/hookExecutor.test.js +73 -20
- package/dist/utils/terminalCapabilities.d.ts +18 -0
- package/dist/utils/terminalCapabilities.js +81 -0
- package/dist/utils/terminalCapabilities.test.d.ts +1 -0
- package/dist/utils/terminalCapabilities.test.js +104 -0
- package/dist/utils/testHelpers.d.ts +106 -0
- package/dist/utils/testHelpers.js +153 -0
- package/dist/utils/testHelpers.test.d.ts +1 -0
- package/dist/utils/testHelpers.test.js +114 -0
- package/dist/utils/worktreeConfig.d.ts +77 -2
- package/dist/utils/worktreeConfig.js +156 -16
- package/dist/utils/worktreeConfig.test.d.ts +1 -0
- package/dist/utils/worktreeConfig.test.js +39 -0
- package/package.json +4 -4
- package/dist/integration-tests/devcontainer.integration.test.js +0 -101
- /package/dist/{integration-tests/devcontainer.integration.test.d.ts → components/App.test.d.ts} +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helper utilities for Effect-ts based testing
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities to simplify testing Effect and Either types:
|
|
5
|
+
* - Synchronous and asynchronous Effect execution
|
|
6
|
+
* - Assertions for success and failure cases
|
|
7
|
+
* - Pattern matching for specific error types
|
|
8
|
+
*
|
|
9
|
+
* These utilities follow Effect-ts best practices and are designed to work
|
|
10
|
+
* seamlessly with Vitest test framework.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // Test successful Effect
|
|
15
|
+
* const result = expectEffectSuccess(myService.getData());
|
|
16
|
+
* expect(result).toEqual(expectedData);
|
|
17
|
+
*
|
|
18
|
+
* // Test failed Effect
|
|
19
|
+
* const error = expectEffectFailure(myService.failingOp());
|
|
20
|
+
* expect(error._tag).toBe('GitError');
|
|
21
|
+
*
|
|
22
|
+
* // Pattern match on specific error type
|
|
23
|
+
* const command = matchEffectError(effect, {
|
|
24
|
+
* GitError: (err) => err.command
|
|
25
|
+
* });
|
|
26
|
+
* expect(command).toBe('git status');
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @module testHelpers
|
|
30
|
+
*/
|
|
31
|
+
import { Effect, Either } from 'effect';
|
|
32
|
+
import type { AppError } from '../types/errors.js';
|
|
33
|
+
/**
|
|
34
|
+
* Run an Effect synchronously and return the success value
|
|
35
|
+
* Throws if the Effect fails
|
|
36
|
+
*
|
|
37
|
+
* @param effect - The Effect to run
|
|
38
|
+
* @returns The success value
|
|
39
|
+
* @throws Error if the Effect fails
|
|
40
|
+
*/
|
|
41
|
+
export declare function runEffectSync<A, E>(effect: Effect.Effect<A, E, never>): A;
|
|
42
|
+
/**
|
|
43
|
+
* Run an Effect as a Promise
|
|
44
|
+
*
|
|
45
|
+
* @param effect - The Effect to run
|
|
46
|
+
* @returns Promise that resolves with success value or rejects with error
|
|
47
|
+
*/
|
|
48
|
+
export declare function runEffectPromise<A, E>(effect: Effect.Effect<A, E, never>): Promise<A>;
|
|
49
|
+
/**
|
|
50
|
+
* Assert that an Effect succeeds and return the success value
|
|
51
|
+
* Throws an error if the Effect fails
|
|
52
|
+
*
|
|
53
|
+
* @param effect - The Effect to test
|
|
54
|
+
* @returns The success value
|
|
55
|
+
* @throws Error if the Effect fails
|
|
56
|
+
*/
|
|
57
|
+
export declare function expectEffectSuccess<A, E>(effect: Effect.Effect<A, E, never>): A;
|
|
58
|
+
/**
|
|
59
|
+
* Assert that an Effect fails and return the error
|
|
60
|
+
* Throws an error if the Effect succeeds
|
|
61
|
+
*
|
|
62
|
+
* @param effect - The Effect to test
|
|
63
|
+
* @returns The error value
|
|
64
|
+
* @throws Error if the Effect succeeds
|
|
65
|
+
*/
|
|
66
|
+
export declare function expectEffectFailure<A, E>(effect: Effect.Effect<A, E, never>): E;
|
|
67
|
+
/**
|
|
68
|
+
* Assert that an Either is Right and return the right value
|
|
69
|
+
* Throws an error if the Either is Left
|
|
70
|
+
*
|
|
71
|
+
* @param either - The Either to test
|
|
72
|
+
* @returns The right value
|
|
73
|
+
* @throws Error if the Either is Left
|
|
74
|
+
*/
|
|
75
|
+
export declare function expectEitherRight<A, E>(either: Either.Either<A, E>): A;
|
|
76
|
+
/**
|
|
77
|
+
* Assert that an Either is Left and return the left value
|
|
78
|
+
* Throws an error if the Either is Right
|
|
79
|
+
*
|
|
80
|
+
* @param either - The Either to test
|
|
81
|
+
* @returns The left value
|
|
82
|
+
* @throws Error if the Either is Right
|
|
83
|
+
*/
|
|
84
|
+
export declare function expectEitherLeft<A, E>(either: Either.Either<A, E>): E;
|
|
85
|
+
/**
|
|
86
|
+
* Pattern match on Effect error and extract specific error type
|
|
87
|
+
* Useful for testing specific error scenarios
|
|
88
|
+
*
|
|
89
|
+
* @param effect - The Effect that should fail
|
|
90
|
+
* @param matchers - Object mapping error tags to extraction functions
|
|
91
|
+
* @returns The extracted value from the matched error
|
|
92
|
+
* @throws Error if the Effect succeeds or error doesn't match
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* const result = matchEffectError(effect, {
|
|
97
|
+
* GitError: (err) => err.command,
|
|
98
|
+
* ValidationError: (err) => err.field
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export declare function matchEffectError<R>(effect: Effect.Effect<unknown, AppError, never>, matchers: {
|
|
103
|
+
[K in AppError['_tag']]?: (error: Extract<AppError, {
|
|
104
|
+
_tag: K;
|
|
105
|
+
}>) => R;
|
|
106
|
+
}): R;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test helper utilities for Effect-ts based testing
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities to simplify testing Effect and Either types:
|
|
5
|
+
* - Synchronous and asynchronous Effect execution
|
|
6
|
+
* - Assertions for success and failure cases
|
|
7
|
+
* - Pattern matching for specific error types
|
|
8
|
+
*
|
|
9
|
+
* These utilities follow Effect-ts best practices and are designed to work
|
|
10
|
+
* seamlessly with Vitest test framework.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* // Test successful Effect
|
|
15
|
+
* const result = expectEffectSuccess(myService.getData());
|
|
16
|
+
* expect(result).toEqual(expectedData);
|
|
17
|
+
*
|
|
18
|
+
* // Test failed Effect
|
|
19
|
+
* const error = expectEffectFailure(myService.failingOp());
|
|
20
|
+
* expect(error._tag).toBe('GitError');
|
|
21
|
+
*
|
|
22
|
+
* // Pattern match on specific error type
|
|
23
|
+
* const command = matchEffectError(effect, {
|
|
24
|
+
* GitError: (err) => err.command
|
|
25
|
+
* });
|
|
26
|
+
* expect(command).toBe('git status');
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @module testHelpers
|
|
30
|
+
*/
|
|
31
|
+
import { Effect, Either, Exit } from 'effect';
|
|
32
|
+
/**
|
|
33
|
+
* Run an Effect synchronously and return the success value
|
|
34
|
+
* Throws if the Effect fails
|
|
35
|
+
*
|
|
36
|
+
* @param effect - The Effect to run
|
|
37
|
+
* @returns The success value
|
|
38
|
+
* @throws Error if the Effect fails
|
|
39
|
+
*/
|
|
40
|
+
export function runEffectSync(effect) {
|
|
41
|
+
const exit = Effect.runSync(Effect.exit(effect));
|
|
42
|
+
if (Exit.isFailure(exit)) {
|
|
43
|
+
// Extract the error from the Cause
|
|
44
|
+
const cause = exit.cause;
|
|
45
|
+
if (cause._tag === 'Fail') {
|
|
46
|
+
throw cause.error;
|
|
47
|
+
}
|
|
48
|
+
throw new Error(`Unexpected cause type: ${cause._tag}`);
|
|
49
|
+
}
|
|
50
|
+
return exit.value;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Run an Effect as a Promise
|
|
54
|
+
*
|
|
55
|
+
* @param effect - The Effect to run
|
|
56
|
+
* @returns Promise that resolves with success value or rejects with error
|
|
57
|
+
*/
|
|
58
|
+
export function runEffectPromise(effect) {
|
|
59
|
+
return Effect.runPromise(effect);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Assert that an Effect succeeds and return the success value
|
|
63
|
+
* Throws an error if the Effect fails
|
|
64
|
+
*
|
|
65
|
+
* @param effect - The Effect to test
|
|
66
|
+
* @returns The success value
|
|
67
|
+
* @throws Error if the Effect fails
|
|
68
|
+
*/
|
|
69
|
+
export function expectEffectSuccess(effect) {
|
|
70
|
+
const exit = Effect.runSync(Effect.exit(effect));
|
|
71
|
+
if (Exit.isFailure(exit)) {
|
|
72
|
+
throw new Error(`Expected Effect to succeed, but it failed with: ${JSON.stringify(exit.cause)}`);
|
|
73
|
+
}
|
|
74
|
+
return exit.value;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Assert that an Effect fails and return the error
|
|
78
|
+
* Throws an error if the Effect succeeds
|
|
79
|
+
*
|
|
80
|
+
* @param effect - The Effect to test
|
|
81
|
+
* @returns The error value
|
|
82
|
+
* @throws Error if the Effect succeeds
|
|
83
|
+
*/
|
|
84
|
+
export function expectEffectFailure(effect) {
|
|
85
|
+
const exit = Effect.runSync(Effect.exit(effect));
|
|
86
|
+
if (Exit.isSuccess(exit)) {
|
|
87
|
+
throw new Error(`Expected Effect to fail, but it succeeded with: ${JSON.stringify(exit.value)}`);
|
|
88
|
+
}
|
|
89
|
+
// Extract the error from the Cause
|
|
90
|
+
const failure = exit.cause;
|
|
91
|
+
if (failure._tag === 'Fail') {
|
|
92
|
+
return failure.error;
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`Expected Fail cause, got: ${failure._tag}`);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Assert that an Either is Right and return the right value
|
|
98
|
+
* Throws an error if the Either is Left
|
|
99
|
+
*
|
|
100
|
+
* @param either - The Either to test
|
|
101
|
+
* @returns The right value
|
|
102
|
+
* @throws Error if the Either is Left
|
|
103
|
+
*/
|
|
104
|
+
export function expectEitherRight(either) {
|
|
105
|
+
if (Either.isLeft(either)) {
|
|
106
|
+
throw new Error(`Expected Either to be Right, but it was Left with: ${JSON.stringify(either.left)}`);
|
|
107
|
+
}
|
|
108
|
+
return either.right;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Assert that an Either is Left and return the left value
|
|
112
|
+
* Throws an error if the Either is Right
|
|
113
|
+
*
|
|
114
|
+
* @param either - The Either to test
|
|
115
|
+
* @returns The left value
|
|
116
|
+
* @throws Error if the Either is Right
|
|
117
|
+
*/
|
|
118
|
+
export function expectEitherLeft(either) {
|
|
119
|
+
if (Either.isRight(either)) {
|
|
120
|
+
throw new Error(`Expected Either to be Left, but it was Right with: ${JSON.stringify(either.right)}`);
|
|
121
|
+
}
|
|
122
|
+
return either.left;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Pattern match on Effect error and extract specific error type
|
|
126
|
+
* Useful for testing specific error scenarios
|
|
127
|
+
*
|
|
128
|
+
* @param effect - The Effect that should fail
|
|
129
|
+
* @param matchers - Object mapping error tags to extraction functions
|
|
130
|
+
* @returns The extracted value from the matched error
|
|
131
|
+
* @throws Error if the Effect succeeds or error doesn't match
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* const result = matchEffectError(effect, {
|
|
136
|
+
* GitError: (err) => err.command,
|
|
137
|
+
* ValidationError: (err) => err.field
|
|
138
|
+
* });
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export function matchEffectError(effect, matchers) {
|
|
142
|
+
const error = expectEffectFailure(effect);
|
|
143
|
+
const tag = error._tag;
|
|
144
|
+
const matcher = matchers[tag];
|
|
145
|
+
if (!matcher) {
|
|
146
|
+
throw new Error(`No matcher found for error tag: ${error._tag}. Available matchers: ${Object.keys(matchers).join(', ')}`);
|
|
147
|
+
}
|
|
148
|
+
// Type assertion is safe here because we've verified the tag matches
|
|
149
|
+
// We use 'as any' because TypeScript cannot express the constraint that
|
|
150
|
+
// the error type matches the matcher's expected parameter type
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
152
|
+
return matcher(error);
|
|
153
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Effect, Either } from 'effect';
|
|
3
|
+
import { runEffectSync, runEffectPromise, expectEffectSuccess, expectEffectFailure, expectEitherRight, expectEitherLeft, matchEffectError, } from './testHelpers.js';
|
|
4
|
+
import { GitError, ValidationError } from '../types/errors.js';
|
|
5
|
+
describe('testHelpers', () => {
|
|
6
|
+
describe('runEffectSync', () => {
|
|
7
|
+
it('should run Effect synchronously and return success value', () => {
|
|
8
|
+
const effect = Effect.succeed(42);
|
|
9
|
+
const result = runEffectSync(effect);
|
|
10
|
+
expect(result).toBe(42);
|
|
11
|
+
});
|
|
12
|
+
it('should throw error when Effect fails', () => {
|
|
13
|
+
const effect = Effect.fail(new Error('test error'));
|
|
14
|
+
expect(() => runEffectSync(effect)).toThrow('test error');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
describe('runEffectPromise', () => {
|
|
18
|
+
it('should run Effect as Promise and resolve with success value', async () => {
|
|
19
|
+
const effect = Effect.succeed(42);
|
|
20
|
+
const result = await runEffectPromise(effect);
|
|
21
|
+
expect(result).toBe(42);
|
|
22
|
+
});
|
|
23
|
+
it('should reject when Effect fails', async () => {
|
|
24
|
+
const effect = Effect.fail(new Error('async error'));
|
|
25
|
+
await expect(runEffectPromise(effect)).rejects.toThrow('async error');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
describe('expectEffectSuccess', () => {
|
|
29
|
+
it('should return success value when Effect succeeds', () => {
|
|
30
|
+
const effect = Effect.succeed('success');
|
|
31
|
+
const result = expectEffectSuccess(effect);
|
|
32
|
+
expect(result).toBe('success');
|
|
33
|
+
});
|
|
34
|
+
it('should throw when Effect fails', () => {
|
|
35
|
+
const effect = Effect.fail(new GitError({ command: 'git test', exitCode: 1, stderr: 'error' }));
|
|
36
|
+
expect(() => expectEffectSuccess(effect)).toThrow();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe('expectEffectFailure', () => {
|
|
40
|
+
it('should return error when Effect fails', () => {
|
|
41
|
+
const gitError = new GitError({
|
|
42
|
+
command: 'git test',
|
|
43
|
+
exitCode: 1,
|
|
44
|
+
stderr: 'error',
|
|
45
|
+
});
|
|
46
|
+
const effect = Effect.fail(gitError);
|
|
47
|
+
const error = expectEffectFailure(effect);
|
|
48
|
+
expect(error).toBe(gitError);
|
|
49
|
+
});
|
|
50
|
+
it('should throw when Effect succeeds', () => {
|
|
51
|
+
const effect = Effect.succeed(42);
|
|
52
|
+
expect(() => expectEffectFailure(effect)).toThrow();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('expectEitherRight', () => {
|
|
56
|
+
it('should return right value when Either is Right', () => {
|
|
57
|
+
const either = Either.right('success');
|
|
58
|
+
const result = expectEitherRight(either);
|
|
59
|
+
expect(result).toBe('success');
|
|
60
|
+
});
|
|
61
|
+
it('should throw when Either is Left', () => {
|
|
62
|
+
const either = Either.left('error');
|
|
63
|
+
expect(() => expectEitherRight(either)).toThrow();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('expectEitherLeft', () => {
|
|
67
|
+
it('should return left value when Either is Left', () => {
|
|
68
|
+
const either = Either.left('error');
|
|
69
|
+
const result = expectEitherLeft(either);
|
|
70
|
+
expect(result).toBe('error');
|
|
71
|
+
});
|
|
72
|
+
it('should throw when Either is Right', () => {
|
|
73
|
+
const either = Either.right('success');
|
|
74
|
+
expect(() => expectEitherLeft(either)).toThrow();
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
describe('matchEffectError', () => {
|
|
78
|
+
it('should match GitError and return extracted value', () => {
|
|
79
|
+
const gitError = new GitError({
|
|
80
|
+
command: 'git test',
|
|
81
|
+
exitCode: 1,
|
|
82
|
+
stderr: 'error',
|
|
83
|
+
});
|
|
84
|
+
const effect = Effect.fail(gitError);
|
|
85
|
+
const result = matchEffectError(effect, {
|
|
86
|
+
GitError: err => err.command,
|
|
87
|
+
});
|
|
88
|
+
expect(result).toBe('git test');
|
|
89
|
+
});
|
|
90
|
+
it('should match ValidationError and return extracted value', () => {
|
|
91
|
+
const validationError = new ValidationError({
|
|
92
|
+
field: 'test',
|
|
93
|
+
constraint: 'required',
|
|
94
|
+
receivedValue: null,
|
|
95
|
+
});
|
|
96
|
+
const effect = Effect.fail(validationError);
|
|
97
|
+
const result = matchEffectError(effect, {
|
|
98
|
+
ValidationError: err => err.field,
|
|
99
|
+
});
|
|
100
|
+
expect(result).toBe('test');
|
|
101
|
+
});
|
|
102
|
+
it('should throw when error type does not match', () => {
|
|
103
|
+
const gitError = new GitError({
|
|
104
|
+
command: 'git test',
|
|
105
|
+
exitCode: 1,
|
|
106
|
+
stderr: 'error',
|
|
107
|
+
});
|
|
108
|
+
const effect = Effect.fail(gitError);
|
|
109
|
+
expect(() => matchEffectError(effect, {
|
|
110
|
+
ValidationError: err => err.field,
|
|
111
|
+
})).toThrow();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -1,3 +1,78 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
import { GitError } from '../types/errors.js';
|
|
1
3
|
export declare function isWorktreeConfigEnabled(gitPath?: string): boolean;
|
|
2
|
-
|
|
3
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Get parent branch for worktree using Effect
|
|
6
|
+
* Returns null if config doesn't exist or worktree config is not available
|
|
7
|
+
*
|
|
8
|
+
* @param {string} worktreePath - Path to the worktree directory
|
|
9
|
+
* @returns {Effect.Effect<string | null, never>} Effect containing parent branch name or null
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* import {Effect} from 'effect';
|
|
14
|
+
* import {getWorktreeParentBranch} from './utils/worktreeConfig.js';
|
|
15
|
+
*
|
|
16
|
+
* // This function never fails - returns null on error
|
|
17
|
+
* const parentBranch = await Effect.runPromise(
|
|
18
|
+
* getWorktreeParentBranch('/path/to/worktree')
|
|
19
|
+
* );
|
|
20
|
+
*
|
|
21
|
+
* if (parentBranch) {
|
|
22
|
+
* console.log(`Parent branch: ${parentBranch}`);
|
|
23
|
+
* } else {
|
|
24
|
+
* console.log('No parent branch configured');
|
|
25
|
+
* }
|
|
26
|
+
*
|
|
27
|
+
* // Use with Effect.flatMap for chaining
|
|
28
|
+
* const status = await Effect.runPromise(
|
|
29
|
+
* Effect.flatMap(
|
|
30
|
+
* getWorktreeParentBranch('/path/to/worktree'),
|
|
31
|
+
* (branch) => branch
|
|
32
|
+
* ? Effect.succeed(`Tracking ${branch}`)
|
|
33
|
+
* : Effect.succeed('No tracking')
|
|
34
|
+
* )
|
|
35
|
+
* );
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function getWorktreeParentBranch(worktreePath: string): Effect.Effect<string | null, never>;
|
|
39
|
+
/**
|
|
40
|
+
* Set parent branch for worktree using Effect
|
|
41
|
+
* Succeeds silently if worktree config is not available
|
|
42
|
+
*
|
|
43
|
+
* @param {string} worktreePath - Path to the worktree directory
|
|
44
|
+
* @param {string} parentBranch - Name of the parent branch to track
|
|
45
|
+
* @returns {Effect.Effect<void, GitError>} Effect that succeeds or fails with GitError
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* import {Effect} from 'effect';
|
|
50
|
+
* import {setWorktreeParentBranch} from './utils/worktreeConfig.js';
|
|
51
|
+
*
|
|
52
|
+
* // Set parent branch with error handling
|
|
53
|
+
* await Effect.runPromise(
|
|
54
|
+
* Effect.catchTag(
|
|
55
|
+
* setWorktreeParentBranch('/path/to/worktree', 'main'),
|
|
56
|
+
* 'GitError',
|
|
57
|
+
* (error) => {
|
|
58
|
+
* console.error(`Failed to set parent branch: ${error.stderr}`);
|
|
59
|
+
* return Effect.void; // Continue despite error
|
|
60
|
+
* }
|
|
61
|
+
* )
|
|
62
|
+
* );
|
|
63
|
+
*
|
|
64
|
+
* // Or use Effect.orElse for fallback
|
|
65
|
+
* await Effect.runPromise(
|
|
66
|
+
* Effect.orElse(
|
|
67
|
+
* setWorktreeParentBranch('/path/to/worktree', 'develop'),
|
|
68
|
+
* () => {
|
|
69
|
+
* console.log('Using fallback - no parent tracking');
|
|
70
|
+
* return Effect.void;
|
|
71
|
+
* }
|
|
72
|
+
* )
|
|
73
|
+
* );
|
|
74
|
+
* ```
|
|
75
|
+
*
|
|
76
|
+
* @throws {GitError} When git config command fails
|
|
77
|
+
*/
|
|
78
|
+
export declare function setWorktreeParentBranch(worktreePath: string, parentBranch: string): Effect.Effect<void, GitError>;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { promisify } from 'util';
|
|
2
|
-
import {
|
|
2
|
+
import { execSync, execFile } from 'child_process';
|
|
3
|
+
import { Effect } from 'effect';
|
|
4
|
+
import { GitError } from '../types/errors.js';
|
|
3
5
|
import { worktreeConfigManager } from '../services/worktreeConfigManager.js';
|
|
4
|
-
const
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
5
7
|
export function isWorktreeConfigEnabled(gitPath) {
|
|
6
8
|
try {
|
|
7
9
|
const result = execSync('git config extensions.worktreeConfig', {
|
|
@@ -14,30 +16,168 @@ export function isWorktreeConfigEnabled(gitPath) {
|
|
|
14
16
|
return false;
|
|
15
17
|
}
|
|
16
18
|
}
|
|
17
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Get parent branch for worktree using Effect
|
|
21
|
+
* Returns null if config doesn't exist or worktree config is not available
|
|
22
|
+
*
|
|
23
|
+
* @param {string} worktreePath - Path to the worktree directory
|
|
24
|
+
* @returns {Effect.Effect<string | null, never>} Effect containing parent branch name or null
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import {Effect} from 'effect';
|
|
29
|
+
* import {getWorktreeParentBranch} from './utils/worktreeConfig.js';
|
|
30
|
+
*
|
|
31
|
+
* // This function never fails - returns null on error
|
|
32
|
+
* const parentBranch = await Effect.runPromise(
|
|
33
|
+
* getWorktreeParentBranch('/path/to/worktree')
|
|
34
|
+
* );
|
|
35
|
+
*
|
|
36
|
+
* if (parentBranch) {
|
|
37
|
+
* console.log(`Parent branch: ${parentBranch}`);
|
|
38
|
+
* } else {
|
|
39
|
+
* console.log('No parent branch configured');
|
|
40
|
+
* }
|
|
41
|
+
*
|
|
42
|
+
* // Use with Effect.flatMap for chaining
|
|
43
|
+
* const status = await Effect.runPromise(
|
|
44
|
+
* Effect.flatMap(
|
|
45
|
+
* getWorktreeParentBranch('/path/to/worktree'),
|
|
46
|
+
* (branch) => branch
|
|
47
|
+
* ? Effect.succeed(`Tracking ${branch}`)
|
|
48
|
+
* : Effect.succeed('No tracking')
|
|
49
|
+
* )
|
|
50
|
+
* );
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function getWorktreeParentBranch(worktreePath) {
|
|
18
54
|
// Return null if worktree config extension is not available
|
|
19
55
|
if (!worktreeConfigManager.isAvailable()) {
|
|
20
|
-
return null;
|
|
56
|
+
return Effect.succeed(null);
|
|
21
57
|
}
|
|
22
|
-
|
|
23
|
-
|
|
58
|
+
return Effect.catchAll(Effect.tryPromise({
|
|
59
|
+
try: signal => execFileAsync('git', ['config', '--worktree', 'ccmanager.parentBranch'], {
|
|
24
60
|
cwd: worktreePath,
|
|
25
61
|
encoding: 'utf8',
|
|
26
62
|
signal,
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
63
|
+
}).then(result => result.stdout.trim() || null),
|
|
64
|
+
catch: error => error,
|
|
65
|
+
}), error => {
|
|
66
|
+
// Abort errors should interrupt
|
|
67
|
+
if (isAbortError(error)) {
|
|
68
|
+
return Effect.interrupt;
|
|
69
|
+
}
|
|
70
|
+
// Config not existing is not an error, return null
|
|
71
|
+
return Effect.succeed(null);
|
|
72
|
+
});
|
|
33
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Set parent branch for worktree using Effect
|
|
76
|
+
* Succeeds silently if worktree config is not available
|
|
77
|
+
*
|
|
78
|
+
* @param {string} worktreePath - Path to the worktree directory
|
|
79
|
+
* @param {string} parentBranch - Name of the parent branch to track
|
|
80
|
+
* @returns {Effect.Effect<void, GitError>} Effect that succeeds or fails with GitError
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```typescript
|
|
84
|
+
* import {Effect} from 'effect';
|
|
85
|
+
* import {setWorktreeParentBranch} from './utils/worktreeConfig.js';
|
|
86
|
+
*
|
|
87
|
+
* // Set parent branch with error handling
|
|
88
|
+
* await Effect.runPromise(
|
|
89
|
+
* Effect.catchTag(
|
|
90
|
+
* setWorktreeParentBranch('/path/to/worktree', 'main'),
|
|
91
|
+
* 'GitError',
|
|
92
|
+
* (error) => {
|
|
93
|
+
* console.error(`Failed to set parent branch: ${error.stderr}`);
|
|
94
|
+
* return Effect.void; // Continue despite error
|
|
95
|
+
* }
|
|
96
|
+
* )
|
|
97
|
+
* );
|
|
98
|
+
*
|
|
99
|
+
* // Or use Effect.orElse for fallback
|
|
100
|
+
* await Effect.runPromise(
|
|
101
|
+
* Effect.orElse(
|
|
102
|
+
* setWorktreeParentBranch('/path/to/worktree', 'develop'),
|
|
103
|
+
* () => {
|
|
104
|
+
* console.log('Using fallback - no parent tracking');
|
|
105
|
+
* return Effect.void;
|
|
106
|
+
* }
|
|
107
|
+
* )
|
|
108
|
+
* );
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* @throws {GitError} When git config command fails
|
|
112
|
+
*/
|
|
34
113
|
export function setWorktreeParentBranch(worktreePath, parentBranch) {
|
|
35
114
|
// Skip if worktree config extension is not available
|
|
36
115
|
if (!worktreeConfigManager.isAvailable()) {
|
|
37
|
-
return;
|
|
116
|
+
return Effect.void;
|
|
117
|
+
}
|
|
118
|
+
const command = `git config --worktree ccmanager.parentBranch ${parentBranch}`;
|
|
119
|
+
return Effect.catchAll(Effect.tryPromise({
|
|
120
|
+
try: signal => execFileAsync('git', ['config', '--worktree', 'ccmanager.parentBranch', parentBranch], {
|
|
121
|
+
cwd: worktreePath,
|
|
122
|
+
encoding: 'utf8',
|
|
123
|
+
signal,
|
|
124
|
+
}).then(() => undefined),
|
|
125
|
+
catch: error => error,
|
|
126
|
+
}), error => {
|
|
127
|
+
// Abort errors should interrupt
|
|
128
|
+
if (isAbortError(error)) {
|
|
129
|
+
return Effect.interrupt;
|
|
130
|
+
}
|
|
131
|
+
// Other errors are git failures
|
|
132
|
+
return Effect.fail(toGitError(command, error));
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
function isAbortError(error) {
|
|
136
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
if (typeof error === 'object' &&
|
|
140
|
+
error !== null &&
|
|
141
|
+
'code' in error &&
|
|
142
|
+
error.code === 'ABORT_ERR') {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
function toGitError(command, error) {
|
|
148
|
+
if (error instanceof GitError) {
|
|
149
|
+
return error;
|
|
150
|
+
}
|
|
151
|
+
if (typeof error === 'object' &&
|
|
152
|
+
error !== null &&
|
|
153
|
+
'code' in error &&
|
|
154
|
+
'stderr' in error) {
|
|
155
|
+
const execError = error;
|
|
156
|
+
const exitCode = typeof execError.code === 'number'
|
|
157
|
+
? execError.code
|
|
158
|
+
: Number.parseInt(String(execError.code ?? '-1'), 10) || -1;
|
|
159
|
+
const stderr = typeof execError.stderr === 'string'
|
|
160
|
+
? execError.stderr
|
|
161
|
+
: (execError.message ?? '');
|
|
162
|
+
return new GitError({
|
|
163
|
+
command,
|
|
164
|
+
exitCode,
|
|
165
|
+
stderr,
|
|
166
|
+
stdout: typeof execError.stdout === 'string' && execError.stdout.length > 0
|
|
167
|
+
? execError.stdout
|
|
168
|
+
: undefined,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
if (error instanceof Error) {
|
|
172
|
+
return new GitError({
|
|
173
|
+
command,
|
|
174
|
+
exitCode: -1,
|
|
175
|
+
stderr: error.message,
|
|
176
|
+
});
|
|
38
177
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
178
|
+
return new GitError({
|
|
179
|
+
command,
|
|
180
|
+
exitCode: -1,
|
|
181
|
+
stderr: String(error),
|
|
42
182
|
});
|
|
43
183
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { isWorktreeConfigEnabled } from './worktreeConfig.js';
|
|
3
|
+
import * as cp from 'child_process';
|
|
4
|
+
vi.mock('child_process');
|
|
5
|
+
vi.mock('../services/worktreeConfigManager.js', () => ({
|
|
6
|
+
worktreeConfigManager: {
|
|
7
|
+
isAvailable: vi.fn(() => true),
|
|
8
|
+
},
|
|
9
|
+
}));
|
|
10
|
+
describe('worktreeConfig', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.clearAllMocks();
|
|
13
|
+
});
|
|
14
|
+
describe('isWorktreeConfigEnabled', () => {
|
|
15
|
+
it('should return true when worktree config is enabled', () => {
|
|
16
|
+
vi.mocked(cp.execSync).mockReturnValue('true\n');
|
|
17
|
+
const result = isWorktreeConfigEnabled('/test/path');
|
|
18
|
+
expect(result).toBe(true);
|
|
19
|
+
expect(cp.execSync).toHaveBeenCalledWith('git config extensions.worktreeConfig', {
|
|
20
|
+
cwd: '/test/path',
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
it('should return false when worktree config is disabled', () => {
|
|
25
|
+
vi.mocked(cp.execSync).mockReturnValue('false\n');
|
|
26
|
+
const result = isWorktreeConfigEnabled('/test/path');
|
|
27
|
+
expect(result).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
it('should return false when git config command fails', () => {
|
|
30
|
+
vi.mocked(cp.execSync).mockImplementation(() => {
|
|
31
|
+
throw new Error('Command failed');
|
|
32
|
+
});
|
|
33
|
+
const result = isWorktreeConfigEnabled('/test/path');
|
|
34
|
+
expect(result).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
// Note: getWorktreeParentBranch and setWorktreeParentBranch are tested
|
|
38
|
+
// in integration tests since they use Effect.tryPromise with real execFile
|
|
39
|
+
});
|