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.
Files changed (44) hide show
  1. package/README.md +34 -1
  2. package/dist/cli.d.ts +4 -0
  3. package/dist/cli.js +30 -2
  4. package/dist/cli.test.d.ts +1 -0
  5. package/dist/cli.test.js +67 -0
  6. package/dist/components/App.d.ts +1 -0
  7. package/dist/components/App.js +107 -37
  8. package/dist/components/Menu.d.ts +6 -1
  9. package/dist/components/Menu.js +227 -50
  10. package/dist/components/Menu.recent-projects.test.d.ts +1 -0
  11. package/dist/components/Menu.recent-projects.test.js +159 -0
  12. package/dist/components/Menu.test.d.ts +1 -0
  13. package/dist/components/Menu.test.js +196 -0
  14. package/dist/components/ProjectList.d.ts +10 -0
  15. package/dist/components/ProjectList.js +231 -0
  16. package/dist/components/ProjectList.recent-projects.test.d.ts +1 -0
  17. package/dist/components/ProjectList.recent-projects.test.js +186 -0
  18. package/dist/components/ProjectList.test.d.ts +1 -0
  19. package/dist/components/ProjectList.test.js +501 -0
  20. package/dist/components/Session.js +4 -14
  21. package/dist/constants/env.d.ts +3 -0
  22. package/dist/constants/env.js +4 -0
  23. package/dist/constants/error.d.ts +6 -0
  24. package/dist/constants/error.js +7 -0
  25. package/dist/hooks/useSearchMode.d.ts +15 -0
  26. package/dist/hooks/useSearchMode.js +67 -0
  27. package/dist/services/configurationManager.d.ts +1 -0
  28. package/dist/services/configurationManager.js +14 -7
  29. package/dist/services/globalSessionOrchestrator.d.ts +16 -0
  30. package/dist/services/globalSessionOrchestrator.js +73 -0
  31. package/dist/services/globalSessionOrchestrator.test.d.ts +1 -0
  32. package/dist/services/globalSessionOrchestrator.test.js +180 -0
  33. package/dist/services/projectManager.d.ts +60 -0
  34. package/dist/services/projectManager.js +418 -0
  35. package/dist/services/projectManager.test.d.ts +1 -0
  36. package/dist/services/projectManager.test.js +342 -0
  37. package/dist/services/sessionManager.d.ts +8 -0
  38. package/dist/services/sessionManager.js +41 -7
  39. package/dist/services/sessionManager.test.js +79 -0
  40. package/dist/services/worktreeService.d.ts +1 -0
  41. package/dist/services/worktreeService.js +20 -5
  42. package/dist/services/worktreeService.test.js +72 -0
  43. package/dist/types/index.d.ts +55 -0
  44. 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
- const configDir = process.platform === 'win32'
28
- ? join(process.env['APPDATA'] || join(homeDir, 'AppData', 'Roaming'), 'ccmanager')
29
- : join(homeDir, '.config', 'ccmanager');
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
+ };