ccmanager 2.1.0 → 2.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.
- package/README.md +18 -0
- package/dist/components/App.js +1 -1
- package/dist/components/Configuration.js +21 -7
- package/dist/components/ConfigureStatusHooks.d.ts +6 -0
- package/dist/components/{ConfigureHooks.js → ConfigureStatusHooks.js} +16 -18
- package/dist/components/ConfigureStatusHooks.test.d.ts +1 -0
- package/dist/components/ConfigureStatusHooks.test.js +62 -0
- package/dist/components/ConfigureWorktreeHooks.d.ts +6 -0
- package/dist/components/ConfigureWorktreeHooks.js +114 -0
- package/dist/components/ConfigureWorktreeHooks.test.d.ts +1 -0
- package/dist/components/ConfigureWorktreeHooks.test.js +60 -0
- package/dist/services/configurationManager.d.ts +3 -1
- package/dist/services/configurationManager.js +10 -0
- package/dist/services/projectManager.test.js +8 -9
- package/dist/services/sessionManager.d.ts +0 -1
- package/dist/services/sessionManager.js +3 -33
- package/dist/services/worktreeService.d.ts +2 -2
- package/dist/services/worktreeService.js +18 -1
- package/dist/services/worktreeService.test.js +162 -7
- package/dist/types/index.d.ts +10 -2
- package/dist/utils/hookExecutor.d.ts +20 -0
- package/dist/utils/hookExecutor.js +96 -0
- package/dist/utils/hookExecutor.test.d.ts +1 -0
- package/dist/utils/hookExecutor.test.js +387 -0
- package/package.json +1 -1
- package/dist/components/ConfigureHooks.d.ts +0 -6
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
1
|
+
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
2
|
import { WorktreeService } from './worktreeService.js';
|
|
3
3
|
import { execSync } from 'child_process';
|
|
4
4
|
import { existsSync, statSync } from 'fs';
|
|
5
|
+
import { configurationManager } from './configurationManager.js';
|
|
6
|
+
import { executeWorktreePostCreationHook } from '../utils/hookExecutor.js';
|
|
5
7
|
// Mock child_process module
|
|
6
8
|
vi.mock('child_process');
|
|
7
9
|
// Mock fs module
|
|
@@ -14,10 +16,22 @@ vi.mock('./worktreeConfigManager.js', () => ({
|
|
|
14
16
|
reset: vi.fn(),
|
|
15
17
|
},
|
|
16
18
|
}));
|
|
19
|
+
// Mock configurationManager
|
|
20
|
+
vi.mock('./configurationManager.js', () => ({
|
|
21
|
+
configurationManager: {
|
|
22
|
+
getWorktreeHooks: vi.fn(),
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
// Mock HookExecutor
|
|
26
|
+
vi.mock('../utils/hookExecutor.js', () => ({
|
|
27
|
+
executeWorktreePostCreationHook: vi.fn(),
|
|
28
|
+
}));
|
|
17
29
|
// Get the mocked function with proper typing
|
|
18
30
|
const mockedExecSync = vi.mocked(execSync);
|
|
19
31
|
const mockedExistsSync = vi.mocked(existsSync);
|
|
20
32
|
const mockedStatSync = vi.mocked(statSync);
|
|
33
|
+
const mockedGetWorktreeHooks = vi.mocked(configurationManager.getWorktreeHooks);
|
|
34
|
+
const mockedExecuteHook = vi.mocked(executeWorktreePostCreationHook);
|
|
21
35
|
describe('WorktreeService', () => {
|
|
22
36
|
let service;
|
|
23
37
|
beforeEach(() => {
|
|
@@ -29,6 +43,8 @@ describe('WorktreeService', () => {
|
|
|
29
43
|
}
|
|
30
44
|
throw new Error('Command not mocked: ' + cmd);
|
|
31
45
|
});
|
|
46
|
+
// Default mock for getWorktreeHooks to return empty config
|
|
47
|
+
mockedGetWorktreeHooks.mockReturnValue({});
|
|
32
48
|
service = new WorktreeService('/fake/path');
|
|
33
49
|
});
|
|
34
50
|
describe('getGitRootPath', () => {
|
|
@@ -177,7 +193,7 @@ origin/feature/test
|
|
|
177
193
|
});
|
|
178
194
|
});
|
|
179
195
|
describe('createWorktree', () => {
|
|
180
|
-
it('should create worktree with base branch when branch does not exist', () => {
|
|
196
|
+
it('should create worktree with base branch when branch does not exist', async () => {
|
|
181
197
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
182
198
|
if (typeof cmd === 'string') {
|
|
183
199
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -190,11 +206,11 @@ origin/feature/test
|
|
|
190
206
|
}
|
|
191
207
|
throw new Error('Unexpected command');
|
|
192
208
|
});
|
|
193
|
-
const result = service.createWorktree('/path/to/worktree', 'new-feature', 'develop');
|
|
209
|
+
const result = await service.createWorktree('/path/to/worktree', 'new-feature', 'develop');
|
|
194
210
|
expect(result).toEqual({ success: true });
|
|
195
211
|
expect(execSync).toHaveBeenCalledWith('git worktree add -b "new-feature" "/path/to/worktree" "develop"', expect.any(Object));
|
|
196
212
|
});
|
|
197
|
-
it('should create worktree without base branch when branch exists', () => {
|
|
213
|
+
it('should create worktree without base branch when branch exists', async () => {
|
|
198
214
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
199
215
|
if (typeof cmd === 'string') {
|
|
200
216
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -207,11 +223,11 @@ origin/feature/test
|
|
|
207
223
|
}
|
|
208
224
|
throw new Error('Unexpected command');
|
|
209
225
|
});
|
|
210
|
-
const result = service.createWorktree('/path/to/worktree', 'existing-feature', 'main');
|
|
226
|
+
const result = await service.createWorktree('/path/to/worktree', 'existing-feature', 'main');
|
|
211
227
|
expect(result).toEqual({ success: true });
|
|
212
228
|
expect(execSync).toHaveBeenCalledWith('git worktree add "/path/to/worktree" "existing-feature"', expect.any(Object));
|
|
213
229
|
});
|
|
214
|
-
it('should create worktree from specified base branch when branch does not exist', () => {
|
|
230
|
+
it('should create worktree from specified base branch when branch does not exist', async () => {
|
|
215
231
|
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
216
232
|
if (typeof cmd === 'string') {
|
|
217
233
|
if (cmd === 'git rev-parse --git-common-dir') {
|
|
@@ -224,7 +240,7 @@ origin/feature/test
|
|
|
224
240
|
}
|
|
225
241
|
throw new Error('Unexpected command');
|
|
226
242
|
});
|
|
227
|
-
const result = service.createWorktree('/path/to/worktree', 'new-feature', 'main');
|
|
243
|
+
const result = await service.createWorktree('/path/to/worktree', 'new-feature', 'main');
|
|
228
244
|
expect(result).toEqual({ success: true });
|
|
229
245
|
expect(execSync).toHaveBeenCalledWith('git worktree add -b "new-feature" "/path/to/worktree" "main"', expect.any(Object));
|
|
230
246
|
});
|
|
@@ -389,4 +405,143 @@ branch refs/heads/other-branch
|
|
|
389
405
|
expect(existsSync).toHaveBeenCalledWith('/fake/path/.claude');
|
|
390
406
|
});
|
|
391
407
|
});
|
|
408
|
+
describe('Worktree Hook Execution', () => {
|
|
409
|
+
afterEach(() => {
|
|
410
|
+
vi.clearAllMocks();
|
|
411
|
+
});
|
|
412
|
+
it('should execute post-creation hook when worktree is created', async () => {
|
|
413
|
+
// Arrange
|
|
414
|
+
const hookCommand = 'echo "Worktree created: $CCMANAGER_WORKTREE_PATH"';
|
|
415
|
+
mockedGetWorktreeHooks.mockReturnValue({
|
|
416
|
+
post_creation: {
|
|
417
|
+
command: hookCommand,
|
|
418
|
+
enabled: true,
|
|
419
|
+
},
|
|
420
|
+
});
|
|
421
|
+
mockedExecuteHook.mockResolvedValue(undefined);
|
|
422
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
423
|
+
if (typeof cmd === 'string') {
|
|
424
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
425
|
+
return '/fake/path/.git\n';
|
|
426
|
+
}
|
|
427
|
+
if (cmd.includes('git worktree list')) {
|
|
428
|
+
return 'worktree /fake/path\nHEAD abc123\nbranch refs/heads/main\n';
|
|
429
|
+
}
|
|
430
|
+
if (cmd.includes('git worktree add')) {
|
|
431
|
+
return '';
|
|
432
|
+
}
|
|
433
|
+
if (cmd.includes('git rev-parse --verify')) {
|
|
434
|
+
throw new Error('Branch not found');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return '';
|
|
438
|
+
});
|
|
439
|
+
// Act
|
|
440
|
+
const result = await service.createWorktree('feature-branch-dir', 'feature-branch', 'main', false, false);
|
|
441
|
+
// Assert
|
|
442
|
+
expect(result.success).toBe(true);
|
|
443
|
+
expect(mockedGetWorktreeHooks).toHaveBeenCalled();
|
|
444
|
+
expect(mockedExecuteHook).toHaveBeenCalledWith(hookCommand, expect.objectContaining({
|
|
445
|
+
path: '/fake/path/feature-branch-dir',
|
|
446
|
+
branch: 'feature-branch',
|
|
447
|
+
isMainWorktree: false,
|
|
448
|
+
hasSession: false,
|
|
449
|
+
}), '/fake/path', 'main');
|
|
450
|
+
});
|
|
451
|
+
it('should not execute hook when disabled', async () => {
|
|
452
|
+
// Arrange
|
|
453
|
+
mockedGetWorktreeHooks.mockReturnValue({
|
|
454
|
+
post_creation: {
|
|
455
|
+
command: 'echo "Should not run"',
|
|
456
|
+
enabled: false,
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
460
|
+
if (typeof cmd === 'string') {
|
|
461
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
462
|
+
return '/fake/path/.git\n';
|
|
463
|
+
}
|
|
464
|
+
if (cmd.includes('git worktree list')) {
|
|
465
|
+
return 'worktree /fake/path\nHEAD abc123\nbranch refs/heads/main\n';
|
|
466
|
+
}
|
|
467
|
+
if (cmd.includes('git worktree add')) {
|
|
468
|
+
return '';
|
|
469
|
+
}
|
|
470
|
+
if (cmd.includes('git rev-parse --verify')) {
|
|
471
|
+
throw new Error('Branch not found');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return '';
|
|
475
|
+
});
|
|
476
|
+
// Act
|
|
477
|
+
const result = await service.createWorktree('feature-branch-dir', 'feature-branch', 'main', false, false);
|
|
478
|
+
// Assert
|
|
479
|
+
expect(result.success).toBe(true);
|
|
480
|
+
expect(mockedGetWorktreeHooks).toHaveBeenCalled();
|
|
481
|
+
expect(mockedExecuteHook).not.toHaveBeenCalled();
|
|
482
|
+
});
|
|
483
|
+
it('should not execute hook when not configured', async () => {
|
|
484
|
+
// Arrange
|
|
485
|
+
mockedGetWorktreeHooks.mockReturnValue({});
|
|
486
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
487
|
+
if (typeof cmd === 'string') {
|
|
488
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
489
|
+
return '/fake/path/.git\n';
|
|
490
|
+
}
|
|
491
|
+
if (cmd.includes('git worktree list')) {
|
|
492
|
+
return 'worktree /fake/path\nHEAD abc123\nbranch refs/heads/main\n';
|
|
493
|
+
}
|
|
494
|
+
if (cmd.includes('git worktree add')) {
|
|
495
|
+
return '';
|
|
496
|
+
}
|
|
497
|
+
if (cmd.includes('git rev-parse --verify')) {
|
|
498
|
+
throw new Error('Branch not found');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return '';
|
|
502
|
+
});
|
|
503
|
+
// Act
|
|
504
|
+
const result = await service.createWorktree('feature-branch-dir', 'feature-branch', 'main', false, false);
|
|
505
|
+
// Assert
|
|
506
|
+
expect(result.success).toBe(true);
|
|
507
|
+
expect(mockedGetWorktreeHooks).toHaveBeenCalled();
|
|
508
|
+
expect(mockedExecuteHook).not.toHaveBeenCalled();
|
|
509
|
+
});
|
|
510
|
+
it('should not fail worktree creation if hook execution fails', async () => {
|
|
511
|
+
// Arrange
|
|
512
|
+
mockedGetWorktreeHooks.mockReturnValue({
|
|
513
|
+
post_creation: {
|
|
514
|
+
command: 'failing-command',
|
|
515
|
+
enabled: true,
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
// The real executeWorktreePostCreationHook doesn't throw, it catches errors internally
|
|
519
|
+
// So the mock should resolve, not reject
|
|
520
|
+
mockedExecuteHook.mockResolvedValue(undefined);
|
|
521
|
+
mockedExecSync.mockImplementation((cmd, _options) => {
|
|
522
|
+
if (typeof cmd === 'string') {
|
|
523
|
+
if (cmd === 'git rev-parse --git-common-dir') {
|
|
524
|
+
return '/fake/path/.git\n';
|
|
525
|
+
}
|
|
526
|
+
if (cmd.includes('git worktree list')) {
|
|
527
|
+
return 'worktree /fake/path\nHEAD abc123\nbranch refs/heads/main\n';
|
|
528
|
+
}
|
|
529
|
+
if (cmd.includes('git worktree add')) {
|
|
530
|
+
return '';
|
|
531
|
+
}
|
|
532
|
+
if (cmd.includes('git rev-parse --verify')) {
|
|
533
|
+
throw new Error('Branch not found');
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return '';
|
|
537
|
+
});
|
|
538
|
+
// Act
|
|
539
|
+
const result = await service.createWorktree('feature-branch-dir', 'feature-branch', 'main', false, false);
|
|
540
|
+
// Allow async operations to complete
|
|
541
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
542
|
+
// Assert
|
|
543
|
+
expect(result.success).toBe(true);
|
|
544
|
+
expect(mockedExecuteHook).toHaveBeenCalled();
|
|
545
|
+
});
|
|
546
|
+
});
|
|
392
547
|
});
|
package/dist/types/index.d.ts
CHANGED
|
@@ -54,6 +54,13 @@ export interface StatusHookConfig {
|
|
|
54
54
|
busy?: StatusHook;
|
|
55
55
|
waiting_input?: StatusHook;
|
|
56
56
|
}
|
|
57
|
+
export interface WorktreeHook {
|
|
58
|
+
command: string;
|
|
59
|
+
enabled: boolean;
|
|
60
|
+
}
|
|
61
|
+
export interface WorktreeHookConfig {
|
|
62
|
+
post_creation?: WorktreeHook;
|
|
63
|
+
}
|
|
57
64
|
export interface WorktreeConfig {
|
|
58
65
|
autoDirectory: boolean;
|
|
59
66
|
autoDirectoryPattern?: string;
|
|
@@ -84,6 +91,7 @@ export interface DevcontainerConfig {
|
|
|
84
91
|
export interface ConfigurationData {
|
|
85
92
|
shortcuts?: ShortcutConfig;
|
|
86
93
|
statusHooks?: StatusHookConfig;
|
|
94
|
+
worktreeHooks?: WorktreeHookConfig;
|
|
87
95
|
worktree?: WorktreeConfig;
|
|
88
96
|
command?: CommandConfig;
|
|
89
97
|
commandPresets?: CommandPresetsConfig;
|
|
@@ -126,10 +134,10 @@ export interface IProjectManager {
|
|
|
126
134
|
export interface IWorktreeService {
|
|
127
135
|
getWorktrees(): Worktree[];
|
|
128
136
|
getGitRootPath(): string;
|
|
129
|
-
createWorktree(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): {
|
|
137
|
+
createWorktree(worktreePath: string, branch: string, baseBranch: string, copySessionData?: boolean, copyClaudeDirectory?: boolean): Promise<{
|
|
130
138
|
success: boolean;
|
|
131
139
|
error?: string;
|
|
132
|
-
}
|
|
140
|
+
}>;
|
|
133
141
|
deleteWorktree(worktreePath: string, options?: {
|
|
134
142
|
deleteBranch?: boolean;
|
|
135
143
|
}): {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Worktree, Session, SessionState } from '../types/index.js';
|
|
2
|
+
export interface HookEnvironment {
|
|
3
|
+
CCMANAGER_WORKTREE_PATH: string;
|
|
4
|
+
CCMANAGER_WORKTREE_BRANCH: string;
|
|
5
|
+
CCMANAGER_GIT_ROOT: string;
|
|
6
|
+
CCMANAGER_BASE_BRANCH?: string;
|
|
7
|
+
[key: string]: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Execute a hook command with the provided environment variables
|
|
11
|
+
*/
|
|
12
|
+
export declare function executeHook(command: string, cwd: string, environment: HookEnvironment): Promise<void>;
|
|
13
|
+
/**
|
|
14
|
+
* Execute a worktree post-creation hook
|
|
15
|
+
*/
|
|
16
|
+
export declare function executeWorktreePostCreationHook(command: string, worktree: Worktree, gitRoot: string, baseBranch?: string): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Execute a session status change hook
|
|
19
|
+
*/
|
|
20
|
+
export declare function executeStatusHook(oldState: SessionState, newState: SessionState, session: Session): Promise<void>;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { WorktreeService } from '../services/worktreeService.js';
|
|
3
|
+
import { configurationManager } from '../services/configurationManager.js';
|
|
4
|
+
/**
|
|
5
|
+
* Execute a hook command with the provided environment variables
|
|
6
|
+
*/
|
|
7
|
+
export function executeHook(command, cwd, environment) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
// Use spawn with shell to execute the command and wait for all child processes
|
|
10
|
+
const child = spawn(command, [], {
|
|
11
|
+
cwd,
|
|
12
|
+
env: {
|
|
13
|
+
...process.env,
|
|
14
|
+
...environment,
|
|
15
|
+
},
|
|
16
|
+
shell: true,
|
|
17
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
18
|
+
});
|
|
19
|
+
let stderr = '';
|
|
20
|
+
// Collect stderr for logging
|
|
21
|
+
child.stderr?.on('data', data => {
|
|
22
|
+
stderr += data.toString();
|
|
23
|
+
});
|
|
24
|
+
// Wait for the process and all its children to exit
|
|
25
|
+
child.on('exit', (code, signal) => {
|
|
26
|
+
if (code !== 0 || signal) {
|
|
27
|
+
let errorMessage = signal
|
|
28
|
+
? `Hook terminated by signal ${signal}`
|
|
29
|
+
: `Hook exited with code ${code}`;
|
|
30
|
+
// Include stderr in the error message when exit code is not 0
|
|
31
|
+
if (stderr) {
|
|
32
|
+
errorMessage += `\nStderr: ${stderr}`;
|
|
33
|
+
}
|
|
34
|
+
reject(new Error(errorMessage));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// When exit code is 0, ignore stderr and resolve successfully
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
// Handle errors in spawning the process
|
|
41
|
+
child.on('error', error => {
|
|
42
|
+
reject(error);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Execute a worktree post-creation hook
|
|
48
|
+
*/
|
|
49
|
+
export async function executeWorktreePostCreationHook(command, worktree, gitRoot, baseBranch) {
|
|
50
|
+
const environment = {
|
|
51
|
+
CCMANAGER_WORKTREE_PATH: worktree.path,
|
|
52
|
+
CCMANAGER_WORKTREE_BRANCH: worktree.branch || 'unknown',
|
|
53
|
+
CCMANAGER_GIT_ROOT: gitRoot,
|
|
54
|
+
};
|
|
55
|
+
if (baseBranch) {
|
|
56
|
+
environment.CCMANAGER_BASE_BRANCH = baseBranch;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await executeHook(command, worktree.path, environment);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
// Log error but don't throw - hooks should not break the main flow
|
|
63
|
+
console.error(`Failed to execute post-creation hook: ${error instanceof Error ? error.message : String(error)}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Execute a session status change hook
|
|
68
|
+
*/
|
|
69
|
+
export async function executeStatusHook(oldState, newState, session) {
|
|
70
|
+
const statusHooks = configurationManager.getStatusHooks();
|
|
71
|
+
const hook = statusHooks[newState];
|
|
72
|
+
if (hook && hook.enabled && hook.command) {
|
|
73
|
+
// Get branch information
|
|
74
|
+
const worktreeService = new WorktreeService();
|
|
75
|
+
const worktrees = worktreeService.getWorktrees();
|
|
76
|
+
const worktree = worktrees.find(wt => wt.path === session.worktreePath);
|
|
77
|
+
const branch = worktree?.branch || 'unknown';
|
|
78
|
+
// Build environment for status hook
|
|
79
|
+
const environment = {
|
|
80
|
+
CCMANAGER_WORKTREE_PATH: session.worktreePath,
|
|
81
|
+
CCMANAGER_WORKTREE_BRANCH: branch,
|
|
82
|
+
CCMANAGER_GIT_ROOT: session.worktreePath, // For status hooks, we use worktree path as cwd
|
|
83
|
+
CCMANAGER_OLD_STATE: oldState,
|
|
84
|
+
CCMANAGER_NEW_STATE: newState,
|
|
85
|
+
CCMANAGER_SESSION_ID: session.id,
|
|
86
|
+
};
|
|
87
|
+
// Execute the hook command in the session's worktree directory
|
|
88
|
+
try {
|
|
89
|
+
await executeHook(hook.command, session.worktreePath, environment);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
// Log error but don't throw - hooks should not break the main flow
|
|
93
|
+
console.error(`Failed to execute ${newState} hook: ${error instanceof Error ? error.message : String(error)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|