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,4 +1,6 @@
1
1
  import { GitProject, IProjectManager, IWorktreeService, MenuMode, RecentProject } from '../types/index.js';
2
+ import { Effect } from 'effect';
3
+ import { FileSystemError, ConfigError } from '../types/errors.js';
2
4
  export declare class ProjectManager implements IProjectManager {
3
5
  currentMode: MenuMode;
4
6
  currentProject?: GitProject;
@@ -15,7 +17,6 @@ export declare class ProjectManager implements IProjectManager {
15
17
  setMode(mode: MenuMode): void;
16
18
  selectProject(project: GitProject): void;
17
19
  getWorktreeService(projectPath?: string): IWorktreeService;
18
- refreshProjects(): Promise<void>;
19
20
  isMultiProjectEnabled(): boolean;
20
21
  getProjectsDir(): string | undefined;
21
22
  getCurrentProjectPath(): string;
@@ -26,7 +27,6 @@ export declare class ProjectManager implements IProjectManager {
26
27
  getRecentProjects(limit?: number): RecentProject[];
27
28
  addRecentProject(project: GitProject): void;
28
29
  clearRecentProjects(): void;
29
- discoverProjects(projectsDir: string): Promise<GitProject[]>;
30
30
  /**
31
31
  * Fast directory discovery - similar to ghq's approach
32
32
  */
@@ -50,6 +50,102 @@ export declare class ProjectManager implements IProjectManager {
50
50
  validateGitRepository(projectPath: string): Promise<boolean>;
51
51
  getCachedProject(projectPath: string): GitProject | undefined;
52
52
  refreshProject(projectPath: string): Promise<GitProject | null>;
53
+ /**
54
+ * Discover Git projects in the specified directory using Effect
55
+ *
56
+ * Recursively scans the directory for Git repositories with parallel processing.
57
+ * Caches results for improved performance.
58
+ *
59
+ * @param {string} projectsDir - Root directory to search for Git projects
60
+ * @returns {Effect.Effect<GitProject[], FileSystemError, never>} Effect containing discovered projects or FileSystemError
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * import {Effect} from 'effect';
65
+ * import {projectManager} from './services/projectManager.js';
66
+ *
67
+ * // Discover projects with error handling
68
+ * const projects = await Effect.runPromise(
69
+ * Effect.catchAll(
70
+ * projectManager.instance.discoverProjectsEffect('/home/user/projects'),
71
+ * (error) => {
72
+ * console.error(`Discovery failed: ${error.cause}`);
73
+ * return Effect.succeed([]); // Return empty array on error
74
+ * }
75
+ * )
76
+ * );
77
+ *
78
+ * console.log(`Found ${projects.length} git repositories`);
79
+ * ```
80
+ */
81
+ discoverProjectsEffect(projectsDir: string): Effect.Effect<GitProject[], FileSystemError, never>;
82
+ /**
83
+ * Load recent projects from cache using Effect
84
+ *
85
+ * Reads and parses the recent projects JSON file. Returns empty array if file doesn't exist.
86
+ *
87
+ * @returns {Effect.Effect<RecentProject[], FileSystemError | ConfigError, never>} Effect containing recent projects or error
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * import {Effect} from 'effect';
92
+ * import {projectManager} from './services/projectManager.js';
93
+ *
94
+ * // Load recent projects with error handling
95
+ * const recent = await Effect.runPromise(
96
+ * Effect.match(
97
+ * projectManager.instance.loadRecentProjectsEffect(),
98
+ * {
99
+ * onFailure: (error) => {
100
+ * if (error._tag === 'ConfigError') {
101
+ * console.error(`Parse error: ${error.details}`);
102
+ * } else {
103
+ * console.error(`File error: ${error.cause}`);
104
+ * }
105
+ * return [];
106
+ * },
107
+ * onSuccess: (projects) => projects
108
+ * }
109
+ * )
110
+ * );
111
+ * ```
112
+ */
113
+ loadRecentProjectsEffect(): Effect.Effect<RecentProject[], FileSystemError | ConfigError, never>;
114
+ /**
115
+ * Save recent projects to cache using Effect
116
+ *
117
+ * Writes the recent projects array to JSON file.
118
+ *
119
+ * @param {RecentProject[]} projects - Recent projects to save
120
+ * @returns {Effect.Effect<void, FileSystemError, never>} Effect that succeeds or fails with FileSystemError
121
+ *
122
+ * @example
123
+ * ```typescript
124
+ * import {Effect} from 'effect';
125
+ * import {projectManager} from './services/projectManager.js';
126
+ *
127
+ * const recentProjects = [
128
+ * { path: '/home/user/project1', name: 'project1', lastAccessed: Date.now() }
129
+ * ];
130
+ *
131
+ * // Save with error recovery
132
+ * await Effect.runPromise(
133
+ * Effect.catchAll(
134
+ * projectManager.instance.saveRecentProjectsEffect(recentProjects),
135
+ * (error) => {
136
+ * console.error(`Failed to save: ${error.cause}`);
137
+ * return Effect.void; // Continue despite error
138
+ * }
139
+ * )
140
+ * );
141
+ * ```
142
+ */
143
+ saveRecentProjectsEffect(projects: RecentProject[]): Effect.Effect<void, FileSystemError, never>;
144
+ /**
145
+ * Refresh projects list (Effect version)
146
+ * @returns Effect with void or FileSystemError
147
+ */
148
+ refreshProjectsEffect(): Effect.Effect<void, FileSystemError, never>;
53
149
  }
54
150
  export declare const projectManager: {
55
151
  readonly instance: ProjectManager;
@@ -4,6 +4,8 @@ import { promises as fs } from 'fs';
4
4
  import path from 'path';
5
5
  import { homedir } from 'os';
6
6
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
7
+ import { Effect } from 'effect';
8
+ import { FileSystemError, ConfigError } from '../types/errors.js';
7
9
  export class ProjectManager {
8
10
  constructor() {
9
11
  Object.defineProperty(this, "currentMode", {
@@ -111,24 +113,6 @@ export class ProjectManager {
111
113
  this.worktreeServiceCache.set(path, service);
112
114
  return service;
113
115
  }
114
- async refreshProjects() {
115
- if (!this.projectsDir) {
116
- throw new Error('Projects directory not configured');
117
- }
118
- // Discover projects
119
- this.projects = await this.discoverProjects(this.projectsDir);
120
- // Update current project if it still exists
121
- if (this.currentProject) {
122
- const updatedProject = this.projects.find(p => p.path === this.currentProject.path);
123
- if (updatedProject) {
124
- this.currentProject = updatedProject;
125
- }
126
- else {
127
- // Current project no longer exists
128
- this.currentProject = undefined;
129
- }
130
- }
131
- }
132
116
  // Helper methods
133
117
  isMultiProjectEnabled() {
134
118
  return !!process.env[ENV_VARS.MULTI_PROJECT_ROOT];
@@ -209,47 +193,6 @@ export class ProjectManager {
209
193
  this.saveRecentProjects();
210
194
  }
211
195
  // Multi-project discovery methods
212
- async discoverProjects(projectsDir) {
213
- const projects = [];
214
- const projectMap = new Map();
215
- try {
216
- // Verify the directory exists
217
- await fs.access(projectsDir);
218
- // Step 1: Fast concurrent directory discovery
219
- const directories = await this.discoverDirectories(projectsDir);
220
- // Step 2: Process directories in parallel to check if they're git repos
221
- const results = await this.processDirectoriesInParallel(directories, projectsDir);
222
- // Step 3: Create project objects (all results are valid git repos)
223
- for (const result of results) {
224
- // Handle name conflicts
225
- let displayName = result.name;
226
- if (projectMap.has(result.name)) {
227
- displayName = result.relativePath.replace(/[\\/\\\\]/g, '/');
228
- }
229
- const project = {
230
- name: displayName,
231
- path: result.path,
232
- relativePath: result.relativePath,
233
- isValid: true,
234
- error: result.error,
235
- };
236
- projectMap.set(displayName, project);
237
- }
238
- // Convert to array and sort
239
- projects.push(...projectMap.values());
240
- projects.sort((a, b) => a.name.localeCompare(b.name));
241
- // Cache results
242
- this.projectCache.clear();
243
- projects.forEach(p => this.projectCache.set(p.path, p));
244
- return projects;
245
- }
246
- catch (error) {
247
- if (error.code === 'ENOENT') {
248
- throw new Error(`Projects directory does not exist: ${projectsDir}`);
249
- }
250
- throw error;
251
- }
252
- }
253
196
  /**
254
197
  * Fast directory discovery - similar to ghq's approach
255
198
  */
@@ -384,6 +327,232 @@ export class ProjectManager {
384
327
  this.projectCache.set(projectPath, project);
385
328
  return project;
386
329
  }
330
+ // Effect-based API methods
331
+ /**
332
+ * Discover Git projects in the specified directory using Effect
333
+ *
334
+ * Recursively scans the directory for Git repositories with parallel processing.
335
+ * Caches results for improved performance.
336
+ *
337
+ * @param {string} projectsDir - Root directory to search for Git projects
338
+ * @returns {Effect.Effect<GitProject[], FileSystemError, never>} Effect containing discovered projects or FileSystemError
339
+ *
340
+ * @example
341
+ * ```typescript
342
+ * import {Effect} from 'effect';
343
+ * import {projectManager} from './services/projectManager.js';
344
+ *
345
+ * // Discover projects with error handling
346
+ * const projects = await Effect.runPromise(
347
+ * Effect.catchAll(
348
+ * projectManager.instance.discoverProjectsEffect('/home/user/projects'),
349
+ * (error) => {
350
+ * console.error(`Discovery failed: ${error.cause}`);
351
+ * return Effect.succeed([]); // Return empty array on error
352
+ * }
353
+ * )
354
+ * );
355
+ *
356
+ * console.log(`Found ${projects.length} git repositories`);
357
+ * ```
358
+ */
359
+ discoverProjectsEffect(projectsDir) {
360
+ return Effect.tryPromise({
361
+ try: async () => {
362
+ // Verify the directory exists
363
+ await fs.access(projectsDir);
364
+ // Step 1: Fast concurrent directory discovery
365
+ const directories = await this.discoverDirectories(projectsDir);
366
+ // Step 2: Process directories in parallel to check if they're git repos
367
+ const results = await this.processDirectoriesInParallel(directories, projectsDir);
368
+ // Step 3: Create project objects
369
+ const projects = [];
370
+ const projectMap = new Map();
371
+ for (const result of results) {
372
+ // Handle name conflicts
373
+ let displayName = result.name;
374
+ if (projectMap.has(result.name)) {
375
+ displayName = result.relativePath.replace(/[\\/\\\\]/g, '/');
376
+ }
377
+ const project = {
378
+ name: displayName,
379
+ path: result.path,
380
+ relativePath: result.relativePath,
381
+ isValid: true,
382
+ error: result.error,
383
+ };
384
+ projectMap.set(displayName, project);
385
+ }
386
+ // Convert to array and sort
387
+ projects.push(...projectMap.values());
388
+ projects.sort((a, b) => a.name.localeCompare(b.name));
389
+ // Cache results
390
+ this.projectCache.clear();
391
+ projects.forEach(p => this.projectCache.set(p.path, p));
392
+ return projects;
393
+ },
394
+ catch: error => {
395
+ if (error instanceof FileSystemError) {
396
+ return error;
397
+ }
398
+ const nodeError = error;
399
+ const cause = nodeError.code === 'ENOENT'
400
+ ? `Projects directory does not exist: ${projectsDir}`
401
+ : String(error);
402
+ return new FileSystemError({
403
+ operation: 'read',
404
+ path: projectsDir,
405
+ cause,
406
+ });
407
+ },
408
+ });
409
+ }
410
+ /**
411
+ * Load recent projects from cache using Effect
412
+ *
413
+ * Reads and parses the recent projects JSON file. Returns empty array if file doesn't exist.
414
+ *
415
+ * @returns {Effect.Effect<RecentProject[], FileSystemError | ConfigError, never>} Effect containing recent projects or error
416
+ *
417
+ * @example
418
+ * ```typescript
419
+ * import {Effect} from 'effect';
420
+ * import {projectManager} from './services/projectManager.js';
421
+ *
422
+ * // Load recent projects with error handling
423
+ * const recent = await Effect.runPromise(
424
+ * Effect.match(
425
+ * projectManager.instance.loadRecentProjectsEffect(),
426
+ * {
427
+ * onFailure: (error) => {
428
+ * if (error._tag === 'ConfigError') {
429
+ * console.error(`Parse error: ${error.details}`);
430
+ * } else {
431
+ * console.error(`File error: ${error.cause}`);
432
+ * }
433
+ * return [];
434
+ * },
435
+ * onSuccess: (projects) => projects
436
+ * }
437
+ * )
438
+ * );
439
+ * ```
440
+ */
441
+ loadRecentProjectsEffect() {
442
+ return Effect.try({
443
+ try: () => {
444
+ if (existsSync(this.dataPath)) {
445
+ const data = readFileSync(this.dataPath, 'utf-8');
446
+ try {
447
+ const parsed = JSON.parse(data);
448
+ return parsed || [];
449
+ }
450
+ catch (parseError) {
451
+ throw new ConfigError({
452
+ configPath: this.dataPath,
453
+ reason: 'parse',
454
+ details: String(parseError),
455
+ });
456
+ }
457
+ }
458
+ return [];
459
+ },
460
+ catch: error => {
461
+ if (error instanceof ConfigError) {
462
+ return error;
463
+ }
464
+ return new FileSystemError({
465
+ operation: 'read',
466
+ path: this.dataPath,
467
+ cause: String(error),
468
+ });
469
+ },
470
+ });
471
+ }
472
+ /**
473
+ * Save recent projects to cache using Effect
474
+ *
475
+ * Writes the recent projects array to JSON file.
476
+ *
477
+ * @param {RecentProject[]} projects - Recent projects to save
478
+ * @returns {Effect.Effect<void, FileSystemError, never>} Effect that succeeds or fails with FileSystemError
479
+ *
480
+ * @example
481
+ * ```typescript
482
+ * import {Effect} from 'effect';
483
+ * import {projectManager} from './services/projectManager.js';
484
+ *
485
+ * const recentProjects = [
486
+ * { path: '/home/user/project1', name: 'project1', lastAccessed: Date.now() }
487
+ * ];
488
+ *
489
+ * // Save with error recovery
490
+ * await Effect.runPromise(
491
+ * Effect.catchAll(
492
+ * projectManager.instance.saveRecentProjectsEffect(recentProjects),
493
+ * (error) => {
494
+ * console.error(`Failed to save: ${error.cause}`);
495
+ * return Effect.void; // Continue despite error
496
+ * }
497
+ * )
498
+ * );
499
+ * ```
500
+ */
501
+ saveRecentProjectsEffect(projects) {
502
+ return Effect.try({
503
+ try: () => {
504
+ writeFileSync(this.dataPath, JSON.stringify(projects, null, 2));
505
+ },
506
+ catch: error => {
507
+ return new FileSystemError({
508
+ operation: 'write',
509
+ path: this.dataPath,
510
+ cause: String(error),
511
+ });
512
+ },
513
+ });
514
+ }
515
+ /**
516
+ * Refresh projects list (Effect version)
517
+ * @returns Effect with void or FileSystemError
518
+ */
519
+ refreshProjectsEffect() {
520
+ return Effect.flatMap(Effect.try({
521
+ try: () => {
522
+ if (!this.projectsDir) {
523
+ throw new FileSystemError({
524
+ operation: 'read',
525
+ path: '',
526
+ cause: 'Projects directory not configured',
527
+ });
528
+ }
529
+ return this.projectsDir;
530
+ },
531
+ catch: error => {
532
+ if (error instanceof FileSystemError) {
533
+ return error;
534
+ }
535
+ return new FileSystemError({
536
+ operation: 'read',
537
+ path: '',
538
+ cause: String(error),
539
+ });
540
+ },
541
+ }), projectsDir => Effect.flatMap(this.discoverProjectsEffect(projectsDir), projects => Effect.sync(() => {
542
+ this.projects = projects;
543
+ // Update current project if it still exists
544
+ if (this.currentProject) {
545
+ const updatedProject = this.projects.find(p => p.path === this.currentProject.path);
546
+ if (updatedProject) {
547
+ this.currentProject = updatedProject;
548
+ }
549
+ else {
550
+ // Current project no longer exists
551
+ this.currentProject = undefined;
552
+ }
553
+ }
554
+ })));
555
+ }
387
556
  }
388
557
  // Recent projects
389
558
  Object.defineProperty(ProjectManager, "MAX_RECENT_PROJECTS", {