ccmanager 1.4.4 → 2.0.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 +34 -1
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +30 -2
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +67 -0
- package/dist/components/App.d.ts +1 -0
- package/dist/components/App.js +107 -37
- package/dist/components/Menu.d.ts +6 -1
- package/dist/components/Menu.js +227 -50
- package/dist/components/Menu.recent-projects.test.d.ts +1 -0
- package/dist/components/Menu.recent-projects.test.js +159 -0
- package/dist/components/Menu.test.d.ts +1 -0
- package/dist/components/Menu.test.js +196 -0
- package/dist/components/ProjectList.d.ts +10 -0
- package/dist/components/ProjectList.js +231 -0
- package/dist/components/ProjectList.recent-projects.test.d.ts +1 -0
- package/dist/components/ProjectList.recent-projects.test.js +186 -0
- package/dist/components/ProjectList.test.d.ts +1 -0
- package/dist/components/ProjectList.test.js +501 -0
- package/dist/components/Session.js +4 -14
- package/dist/constants/env.d.ts +3 -0
- package/dist/constants/env.js +4 -0
- package/dist/constants/error.d.ts +6 -0
- package/dist/constants/error.js +7 -0
- package/dist/hooks/useSearchMode.d.ts +15 -0
- package/dist/hooks/useSearchMode.js +67 -0
- package/dist/services/configurationManager.d.ts +1 -0
- package/dist/services/configurationManager.js +14 -7
- package/dist/services/globalSessionOrchestrator.d.ts +16 -0
- package/dist/services/globalSessionOrchestrator.js +73 -0
- package/dist/services/globalSessionOrchestrator.test.d.ts +1 -0
- package/dist/services/globalSessionOrchestrator.test.js +180 -0
- package/dist/services/projectManager.d.ts +60 -0
- package/dist/services/projectManager.js +418 -0
- package/dist/services/projectManager.test.d.ts +1 -0
- package/dist/services/projectManager.test.js +342 -0
- package/dist/services/sessionManager.d.ts +8 -0
- package/dist/services/sessionManager.js +41 -7
- package/dist/services/sessionManager.test.js +79 -0
- package/dist/services/worktreeService.d.ts +1 -0
- package/dist/services/worktreeService.js +20 -5
- package/dist/services/worktreeService.test.js +72 -0
- package/dist/types/index.d.ts +55 -0
- package/package.json +1 -1
|
@@ -16,6 +16,12 @@ export class ConfigurationManager {
|
|
|
16
16
|
writable: true,
|
|
17
17
|
value: void 0
|
|
18
18
|
});
|
|
19
|
+
Object.defineProperty(this, "configDir", {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
configurable: true,
|
|
22
|
+
writable: true,
|
|
23
|
+
value: void 0
|
|
24
|
+
});
|
|
19
25
|
Object.defineProperty(this, "config", {
|
|
20
26
|
enumerable: true,
|
|
21
27
|
configurable: true,
|
|
@@ -24,15 +30,16 @@ export class ConfigurationManager {
|
|
|
24
30
|
});
|
|
25
31
|
// Determine config directory based on platform
|
|
26
32
|
const homeDir = homedir();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
this.configDir =
|
|
34
|
+
process.platform === 'win32'
|
|
35
|
+
? join(process.env['APPDATA'] || join(homeDir, 'AppData', 'Roaming'), 'ccmanager')
|
|
36
|
+
: join(homeDir, '.config', 'ccmanager');
|
|
30
37
|
// Ensure config directory exists
|
|
31
|
-
if (!existsSync(configDir)) {
|
|
32
|
-
mkdirSync(configDir, { recursive: true });
|
|
38
|
+
if (!existsSync(this.configDir)) {
|
|
39
|
+
mkdirSync(this.configDir, { recursive: true });
|
|
33
40
|
}
|
|
34
|
-
this.configPath = join(configDir, 'config.json');
|
|
35
|
-
this.legacyShortcutsPath = join(configDir, 'shortcuts.json');
|
|
41
|
+
this.configPath = join(this.configDir, 'config.json');
|
|
42
|
+
this.legacyShortcutsPath = join(this.configDir, 'shortcuts.json');
|
|
36
43
|
this.loadConfig();
|
|
37
44
|
}
|
|
38
45
|
loadConfig() {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { SessionManager } from './sessionManager.js';
|
|
2
|
+
import { Session } from '../types/index.js';
|
|
3
|
+
declare class GlobalSessionOrchestrator {
|
|
4
|
+
private static instance;
|
|
5
|
+
private projectManagers;
|
|
6
|
+
private globalManager;
|
|
7
|
+
private constructor();
|
|
8
|
+
static getInstance(): GlobalSessionOrchestrator;
|
|
9
|
+
getManagerForProject(projectPath?: string): SessionManager;
|
|
10
|
+
getAllActiveSessions(): Session[];
|
|
11
|
+
destroyAllSessions(): void;
|
|
12
|
+
destroyProjectSessions(projectPath: string): void;
|
|
13
|
+
getProjectSessions(projectPath: string): Session[];
|
|
14
|
+
}
|
|
15
|
+
export declare const globalSessionOrchestrator: GlobalSessionOrchestrator;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { SessionManager } from './sessionManager.js';
|
|
2
|
+
class GlobalSessionOrchestrator {
|
|
3
|
+
constructor() {
|
|
4
|
+
Object.defineProperty(this, "projectManagers", {
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true,
|
|
8
|
+
value: new Map()
|
|
9
|
+
});
|
|
10
|
+
Object.defineProperty(this, "globalManager", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: void 0
|
|
15
|
+
});
|
|
16
|
+
// Create a global session manager for single-project mode
|
|
17
|
+
this.globalManager = new SessionManager();
|
|
18
|
+
}
|
|
19
|
+
static getInstance() {
|
|
20
|
+
if (!GlobalSessionOrchestrator.instance) {
|
|
21
|
+
GlobalSessionOrchestrator.instance = new GlobalSessionOrchestrator();
|
|
22
|
+
}
|
|
23
|
+
return GlobalSessionOrchestrator.instance;
|
|
24
|
+
}
|
|
25
|
+
getManagerForProject(projectPath) {
|
|
26
|
+
// If no project path, return the global manager (single-project mode)
|
|
27
|
+
if (!projectPath) {
|
|
28
|
+
return this.globalManager;
|
|
29
|
+
}
|
|
30
|
+
// Get or create a session manager for this project
|
|
31
|
+
let manager = this.projectManagers.get(projectPath);
|
|
32
|
+
if (!manager) {
|
|
33
|
+
manager = new SessionManager();
|
|
34
|
+
this.projectManagers.set(projectPath, manager);
|
|
35
|
+
}
|
|
36
|
+
return manager;
|
|
37
|
+
}
|
|
38
|
+
getAllActiveSessions() {
|
|
39
|
+
const sessions = [];
|
|
40
|
+
// Get sessions from global manager
|
|
41
|
+
sessions.push(...this.globalManager.getAllSessions());
|
|
42
|
+
// Get sessions from all project managers
|
|
43
|
+
for (const manager of this.projectManagers.values()) {
|
|
44
|
+
sessions.push(...manager.getAllSessions());
|
|
45
|
+
}
|
|
46
|
+
return sessions;
|
|
47
|
+
}
|
|
48
|
+
destroyAllSessions() {
|
|
49
|
+
// Destroy sessions in global manager
|
|
50
|
+
this.globalManager.destroy();
|
|
51
|
+
// Destroy sessions in all project managers
|
|
52
|
+
for (const manager of this.projectManagers.values()) {
|
|
53
|
+
manager.destroy();
|
|
54
|
+
}
|
|
55
|
+
// Clear the project managers map
|
|
56
|
+
this.projectManagers.clear();
|
|
57
|
+
}
|
|
58
|
+
destroyProjectSessions(projectPath) {
|
|
59
|
+
const manager = this.projectManagers.get(projectPath);
|
|
60
|
+
if (manager) {
|
|
61
|
+
manager.destroy();
|
|
62
|
+
this.projectManagers.delete(projectPath);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
getProjectSessions(projectPath) {
|
|
66
|
+
const manager = this.projectManagers.get(projectPath);
|
|
67
|
+
if (manager) {
|
|
68
|
+
return manager.getAllSessions();
|
|
69
|
+
}
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export const globalSessionOrchestrator = GlobalSessionOrchestrator.getInstance();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { globalSessionOrchestrator } from './globalSessionOrchestrator.js';
|
|
3
|
+
// Mock SessionManager
|
|
4
|
+
vi.mock('./sessionManager.js', () => {
|
|
5
|
+
class MockSessionManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
Object.defineProperty(this, "sessions", {
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
writable: true,
|
|
11
|
+
value: new Map()
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
getAllSessions() {
|
|
15
|
+
return Array.from(this.sessions.values());
|
|
16
|
+
}
|
|
17
|
+
destroy() {
|
|
18
|
+
this.sessions.clear();
|
|
19
|
+
}
|
|
20
|
+
getSession(worktreePath) {
|
|
21
|
+
return this.sessions.get(worktreePath);
|
|
22
|
+
}
|
|
23
|
+
setSessionActive(_worktreePath, _active) {
|
|
24
|
+
// Mock implementation
|
|
25
|
+
}
|
|
26
|
+
destroySession(worktreePath) {
|
|
27
|
+
this.sessions.delete(worktreePath);
|
|
28
|
+
}
|
|
29
|
+
on() {
|
|
30
|
+
// Mock implementation
|
|
31
|
+
}
|
|
32
|
+
off() {
|
|
33
|
+
// Mock implementation
|
|
34
|
+
}
|
|
35
|
+
emit() {
|
|
36
|
+
// Mock implementation
|
|
37
|
+
}
|
|
38
|
+
async createSessionWithPreset(_worktreePath, _presetId) {
|
|
39
|
+
// Mock implementation
|
|
40
|
+
return {};
|
|
41
|
+
}
|
|
42
|
+
async createSessionWithDevcontainer(_worktreePath, _config, _presetId) {
|
|
43
|
+
// Mock implementation
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { SessionManager: MockSessionManager };
|
|
48
|
+
});
|
|
49
|
+
describe('GlobalSessionManager', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
// Clear any existing sessions
|
|
52
|
+
globalSessionOrchestrator.destroyAllSessions();
|
|
53
|
+
});
|
|
54
|
+
it('should return the same manager instance for the same project', () => {
|
|
55
|
+
const manager1 = globalSessionOrchestrator.getManagerForProject('/project1');
|
|
56
|
+
const manager2 = globalSessionOrchestrator.getManagerForProject('/project1');
|
|
57
|
+
expect(manager1).toBe(manager2);
|
|
58
|
+
});
|
|
59
|
+
it('should return different managers for different projects', () => {
|
|
60
|
+
const manager1 = globalSessionOrchestrator.getManagerForProject('/project1');
|
|
61
|
+
const manager2 = globalSessionOrchestrator.getManagerForProject('/project2');
|
|
62
|
+
expect(manager1).not.toBe(manager2);
|
|
63
|
+
});
|
|
64
|
+
it('should return global manager when no project path is provided', () => {
|
|
65
|
+
const manager1 = globalSessionOrchestrator.getManagerForProject();
|
|
66
|
+
const manager2 = globalSessionOrchestrator.getManagerForProject();
|
|
67
|
+
expect(manager1).toBe(manager2);
|
|
68
|
+
});
|
|
69
|
+
it('should get all active sessions from all managers', () => {
|
|
70
|
+
const globalManager = globalSessionOrchestrator.getManagerForProject();
|
|
71
|
+
const project1Manager = globalSessionOrchestrator.getManagerForProject('/project1');
|
|
72
|
+
const project2Manager = globalSessionOrchestrator.getManagerForProject('/project2');
|
|
73
|
+
// Add mock sessions
|
|
74
|
+
globalManager.sessions.set('global-session', {
|
|
75
|
+
id: 'global-session',
|
|
76
|
+
});
|
|
77
|
+
project1Manager.sessions.set('project1-session', {
|
|
78
|
+
id: 'project1-session',
|
|
79
|
+
});
|
|
80
|
+
project2Manager.sessions.set('project2-session', {
|
|
81
|
+
id: 'project2-session',
|
|
82
|
+
});
|
|
83
|
+
const allSessions = globalSessionOrchestrator.getAllActiveSessions();
|
|
84
|
+
expect(allSessions).toHaveLength(3);
|
|
85
|
+
const sessionIds = allSessions.map(s => s.id);
|
|
86
|
+
expect(sessionIds).toContain('global-session');
|
|
87
|
+
expect(sessionIds).toContain('project1-session');
|
|
88
|
+
expect(sessionIds).toContain('project2-session');
|
|
89
|
+
});
|
|
90
|
+
it('should destroy all sessions when destroyAllSessions is called', () => {
|
|
91
|
+
const globalManager = globalSessionOrchestrator.getManagerForProject();
|
|
92
|
+
const project1Manager = globalSessionOrchestrator.getManagerForProject('/project1');
|
|
93
|
+
// Add mock sessions
|
|
94
|
+
globalManager.sessions.set('global-session', {
|
|
95
|
+
id: 'global-session',
|
|
96
|
+
});
|
|
97
|
+
project1Manager.sessions.set('project1-session', {
|
|
98
|
+
id: 'project1-session',
|
|
99
|
+
});
|
|
100
|
+
globalSessionOrchestrator.destroyAllSessions();
|
|
101
|
+
expect(globalManager.sessions.size).toBe(0);
|
|
102
|
+
expect(project1Manager.sessions.size).toBe(0);
|
|
103
|
+
});
|
|
104
|
+
it('should destroy only project sessions when destroyProjectSessions is called', () => {
|
|
105
|
+
const globalManager = globalSessionOrchestrator.getManagerForProject();
|
|
106
|
+
const project1Manager = globalSessionOrchestrator.getManagerForProject('/project1');
|
|
107
|
+
const project2Manager = globalSessionOrchestrator.getManagerForProject('/project2');
|
|
108
|
+
// Add mock sessions
|
|
109
|
+
globalManager.sessions.set('global-session', {
|
|
110
|
+
id: 'global-session',
|
|
111
|
+
});
|
|
112
|
+
project1Manager.sessions.set('project1-session', {
|
|
113
|
+
id: 'project1-session',
|
|
114
|
+
});
|
|
115
|
+
project2Manager.sessions.set('project2-session', {
|
|
116
|
+
id: 'project2-session',
|
|
117
|
+
});
|
|
118
|
+
globalSessionOrchestrator.destroyProjectSessions('/project1');
|
|
119
|
+
// Global and project2 sessions should remain
|
|
120
|
+
expect(globalManager.sessions.size).toBe(1);
|
|
121
|
+
expect(project2Manager.sessions.size).toBe(1);
|
|
122
|
+
// project1 sessions should be cleared
|
|
123
|
+
const newProject1Manager = globalSessionOrchestrator.getManagerForProject('/project1');
|
|
124
|
+
expect(newProject1Manager).not.toBe(project1Manager); // Should be a new instance
|
|
125
|
+
expect(newProject1Manager.sessions.size).toBe(0);
|
|
126
|
+
});
|
|
127
|
+
it('should persist sessions when switching between projects', () => {
|
|
128
|
+
const project1Manager = globalSessionOrchestrator.getManagerForProject('/project1');
|
|
129
|
+
project1Manager.sessions.set('session1', {
|
|
130
|
+
id: 'session1',
|
|
131
|
+
worktreePath: '/project1/main',
|
|
132
|
+
});
|
|
133
|
+
// Switch to project2
|
|
134
|
+
const project2Manager = globalSessionOrchestrator.getManagerForProject('/project2');
|
|
135
|
+
project2Manager.sessions.set('session2', {
|
|
136
|
+
id: 'session2',
|
|
137
|
+
worktreePath: '/project2/main',
|
|
138
|
+
});
|
|
139
|
+
// Switch back to project1
|
|
140
|
+
const project1ManagerAgain = globalSessionOrchestrator.getManagerForProject('/project1');
|
|
141
|
+
// Session should still exist
|
|
142
|
+
expect(project1ManagerAgain).toBe(project1Manager);
|
|
143
|
+
expect(project1ManagerAgain.sessions.get('session1')).toEqual({
|
|
144
|
+
id: 'session1',
|
|
145
|
+
worktreePath: '/project1/main',
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
it('should get sessions for a specific project', () => {
|
|
149
|
+
const project1Manager = globalSessionOrchestrator.getManagerForProject('/project1');
|
|
150
|
+
const project2Manager = globalSessionOrchestrator.getManagerForProject('/project2');
|
|
151
|
+
// Add mock sessions with different states
|
|
152
|
+
project1Manager.sessions.set('session1', {
|
|
153
|
+
id: 'session1',
|
|
154
|
+
state: 'idle',
|
|
155
|
+
});
|
|
156
|
+
project1Manager.sessions.set('session2', {
|
|
157
|
+
id: 'session2',
|
|
158
|
+
state: 'busy',
|
|
159
|
+
});
|
|
160
|
+
project1Manager.sessions.set('session3', {
|
|
161
|
+
id: 'session3',
|
|
162
|
+
state: 'waiting',
|
|
163
|
+
});
|
|
164
|
+
project2Manager.sessions.set('session4', {
|
|
165
|
+
id: 'session4',
|
|
166
|
+
state: 'idle',
|
|
167
|
+
});
|
|
168
|
+
const project1Sessions = globalSessionOrchestrator.getProjectSessions('/project1');
|
|
169
|
+
const project2Sessions = globalSessionOrchestrator.getProjectSessions('/project2');
|
|
170
|
+
const project3Sessions = globalSessionOrchestrator.getProjectSessions('/project3'); // non-existent
|
|
171
|
+
expect(project1Sessions).toHaveLength(3);
|
|
172
|
+
expect(project2Sessions).toHaveLength(1);
|
|
173
|
+
expect(project3Sessions).toHaveLength(0);
|
|
174
|
+
// Verify the sessions are correct
|
|
175
|
+
const project1Ids = project1Sessions.map(s => s.id);
|
|
176
|
+
expect(project1Ids).toContain('session1');
|
|
177
|
+
expect(project1Ids).toContain('session2');
|
|
178
|
+
expect(project1Ids).toContain('session3');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { GitProject, IProjectManager, IWorktreeService, MenuMode, RecentProject } from '../types/index.js';
|
|
2
|
+
export declare class ProjectManager implements IProjectManager {
|
|
3
|
+
currentMode: MenuMode;
|
|
4
|
+
currentProject?: GitProject;
|
|
5
|
+
projects: GitProject[];
|
|
6
|
+
private worktreeServiceCache;
|
|
7
|
+
private projectsDir?;
|
|
8
|
+
private projectCache;
|
|
9
|
+
private discoveryWorkers;
|
|
10
|
+
private static readonly MAX_RECENT_PROJECTS;
|
|
11
|
+
private recentProjects;
|
|
12
|
+
private dataPath;
|
|
13
|
+
private configDir;
|
|
14
|
+
constructor();
|
|
15
|
+
setMode(mode: MenuMode): void;
|
|
16
|
+
selectProject(project: GitProject): void;
|
|
17
|
+
getWorktreeService(projectPath?: string): IWorktreeService;
|
|
18
|
+
refreshProjects(): Promise<void>;
|
|
19
|
+
isMultiProjectEnabled(): boolean;
|
|
20
|
+
getProjectsDir(): string | undefined;
|
|
21
|
+
getCurrentProjectPath(): string;
|
|
22
|
+
clearWorktreeServiceCache(projectPath?: string): void;
|
|
23
|
+
getCachedServices(): Map<string, IWorktreeService>;
|
|
24
|
+
private loadRecentProjects;
|
|
25
|
+
private saveRecentProjects;
|
|
26
|
+
getRecentProjects(limit?: number): RecentProject[];
|
|
27
|
+
addRecentProject(project: GitProject): void;
|
|
28
|
+
clearRecentProjects(): void;
|
|
29
|
+
discoverProjects(projectsDir: string): Promise<GitProject[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Fast directory discovery - similar to ghq's approach
|
|
32
|
+
*/
|
|
33
|
+
private discoverDirectories;
|
|
34
|
+
/**
|
|
35
|
+
* Quick check for .git directory without running git commands
|
|
36
|
+
*/
|
|
37
|
+
private hasGitDirectory;
|
|
38
|
+
/**
|
|
39
|
+
* Process directories in parallel using worker pool pattern
|
|
40
|
+
*/
|
|
41
|
+
private processDirectoriesInParallel;
|
|
42
|
+
/**
|
|
43
|
+
* Process a single directory to check if it's a valid git repo
|
|
44
|
+
* @param task - The discovery task containing path information
|
|
45
|
+
* @param _rootDir - The root directory (unused)
|
|
46
|
+
* @returns A DiscoveryResult object if the directory is a valid git repository,
|
|
47
|
+
* or null if it's not a valid git repository (will be filtered out)
|
|
48
|
+
*/
|
|
49
|
+
private processDirectory;
|
|
50
|
+
validateGitRepository(projectPath: string): Promise<boolean>;
|
|
51
|
+
getCachedProject(projectPath: string): GitProject | undefined;
|
|
52
|
+
refreshProject(projectPath: string): Promise<GitProject | null>;
|
|
53
|
+
}
|
|
54
|
+
export declare const projectManager: {
|
|
55
|
+
readonly instance: ProjectManager;
|
|
56
|
+
getRecentProjects(limit?: number): RecentProject[];
|
|
57
|
+
addRecentProject(project: GitProject): void;
|
|
58
|
+
clearRecentProjects(): void;
|
|
59
|
+
_resetForTesting(): void;
|
|
60
|
+
};
|