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.
Files changed (82) 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/components/Session.js +11 -6
  26. package/dist/hooks/useGitStatus.d.ts +11 -0
  27. package/dist/hooks/useGitStatus.js +70 -12
  28. package/dist/hooks/useGitStatus.test.js +30 -23
  29. package/dist/services/configurationManager.d.ts +75 -0
  30. package/dist/services/configurationManager.effect.test.d.ts +1 -0
  31. package/dist/services/configurationManager.effect.test.js +407 -0
  32. package/dist/services/configurationManager.js +246 -0
  33. package/dist/services/globalSessionOrchestrator.test.js +0 -8
  34. package/dist/services/projectManager.d.ts +98 -2
  35. package/dist/services/projectManager.js +228 -59
  36. package/dist/services/projectManager.test.js +242 -2
  37. package/dist/services/sessionManager.d.ts +44 -2
  38. package/dist/services/sessionManager.effect.test.d.ts +1 -0
  39. package/dist/services/sessionManager.effect.test.js +321 -0
  40. package/dist/services/sessionManager.js +216 -65
  41. package/dist/services/sessionManager.statePersistence.test.js +18 -9
  42. package/dist/services/sessionManager.test.js +40 -36
  43. package/dist/services/shortcutManager.d.ts +2 -0
  44. package/dist/services/shortcutManager.js +53 -0
  45. package/dist/services/shortcutManager.test.d.ts +1 -0
  46. package/dist/services/shortcutManager.test.js +30 -0
  47. package/dist/services/worktreeService.d.ts +356 -26
  48. package/dist/services/worktreeService.js +793 -353
  49. package/dist/services/worktreeService.test.js +294 -313
  50. package/dist/types/errors.d.ts +74 -0
  51. package/dist/types/errors.js +31 -0
  52. package/dist/types/errors.test.d.ts +1 -0
  53. package/dist/types/errors.test.js +201 -0
  54. package/dist/types/index.d.ts +5 -17
  55. package/dist/utils/claudeDir.d.ts +58 -6
  56. package/dist/utils/claudeDir.js +103 -8
  57. package/dist/utils/claudeDir.test.d.ts +1 -0
  58. package/dist/utils/claudeDir.test.js +108 -0
  59. package/dist/utils/concurrencyLimit.d.ts +5 -0
  60. package/dist/utils/concurrencyLimit.js +11 -0
  61. package/dist/utils/concurrencyLimit.test.js +40 -1
  62. package/dist/utils/gitStatus.d.ts +36 -8
  63. package/dist/utils/gitStatus.js +170 -88
  64. package/dist/utils/gitStatus.test.js +12 -9
  65. package/dist/utils/hookExecutor.d.ts +41 -6
  66. package/dist/utils/hookExecutor.js +75 -32
  67. package/dist/utils/hookExecutor.test.js +73 -20
  68. package/dist/utils/terminalCapabilities.d.ts +18 -0
  69. package/dist/utils/terminalCapabilities.js +81 -0
  70. package/dist/utils/terminalCapabilities.test.d.ts +1 -0
  71. package/dist/utils/terminalCapabilities.test.js +104 -0
  72. package/dist/utils/testHelpers.d.ts +106 -0
  73. package/dist/utils/testHelpers.js +153 -0
  74. package/dist/utils/testHelpers.test.d.ts +1 -0
  75. package/dist/utils/testHelpers.test.js +114 -0
  76. package/dist/utils/worktreeConfig.d.ts +77 -2
  77. package/dist/utils/worktreeConfig.js +156 -16
  78. package/dist/utils/worktreeConfig.test.d.ts +1 -0
  79. package/dist/utils/worktreeConfig.test.js +39 -0
  80. package/package.json +4 -4
  81. package/dist/integration-tests/devcontainer.integration.test.js +0 -101
  82. /package/dist/{integration-tests/devcontainer.integration.test.d.ts → components/App.test.d.ts} +0 -0
@@ -1,7 +1,9 @@
1
1
  import { homedir } from 'os';
2
2
  import { join } from 'path';
3
3
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
4
+ import { Effect, Either } from 'effect';
4
5
  import { DEFAULT_SHORTCUTS, } from '../types/index.js';
6
+ import { FileSystemError, ConfigError, ValidationError, } from '../types/errors.js';
5
7
  export class ConfigurationManager {
6
8
  constructor() {
7
9
  Object.defineProperty(this, "configPath", {
@@ -268,5 +270,249 @@ export class ConfigurationManager {
268
270
  presets.selectPresetOnStart = enabled;
269
271
  this.setCommandPresets(presets);
270
272
  }
273
+ // Effect-based methods for type-safe error handling
274
+ /**
275
+ * Load configuration from file with Effect-based error handling
276
+ *
277
+ * @returns {Effect.Effect<ConfigurationData, FileSystemError | ConfigError, never>} Configuration data on success, errors on failure
278
+ *
279
+ * @example
280
+ * ```typescript
281
+ * const result = await Effect.runPromise(
282
+ * configManager.loadConfigEffect()
283
+ * );
284
+ * ```
285
+ */
286
+ loadConfigEffect() {
287
+ return Effect.try({
288
+ try: () => {
289
+ // Try to load the new config file
290
+ if (existsSync(this.configPath)) {
291
+ const configData = readFileSync(this.configPath, 'utf-8');
292
+ const parsedConfig = JSON.parse(configData);
293
+ return this.applyDefaults(parsedConfig);
294
+ }
295
+ else {
296
+ // If new config doesn't exist, check for legacy shortcuts.json
297
+ const migratedConfig = this.migrateLegacyShortcutsSync();
298
+ return this.applyDefaults(migratedConfig || {});
299
+ }
300
+ },
301
+ catch: (error) => {
302
+ // Determine error type
303
+ if (error instanceof SyntaxError) {
304
+ return new ConfigError({
305
+ configPath: this.configPath,
306
+ reason: 'parse',
307
+ details: String(error),
308
+ });
309
+ }
310
+ return new FileSystemError({
311
+ operation: 'read',
312
+ path: this.configPath,
313
+ cause: String(error),
314
+ });
315
+ },
316
+ });
317
+ }
318
+ /**
319
+ * Save configuration to file with Effect-based error handling
320
+ *
321
+ * @returns {Effect.Effect<void, FileSystemError, never>} Void on success, FileSystemError on write failure
322
+ *
323
+ * @example
324
+ * ```typescript
325
+ * await Effect.runPromise(
326
+ * configManager.saveConfigEffect(config)
327
+ * );
328
+ * ```
329
+ */
330
+ saveConfigEffect(config) {
331
+ return Effect.try({
332
+ try: () => {
333
+ this.config = config;
334
+ writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
335
+ },
336
+ catch: (error) => {
337
+ return new FileSystemError({
338
+ operation: 'write',
339
+ path: this.configPath,
340
+ cause: String(error),
341
+ });
342
+ },
343
+ });
344
+ }
345
+ /**
346
+ * Validate configuration structure
347
+ * Synchronous validation using Either
348
+ */
349
+ validateConfig(config) {
350
+ if (!config || typeof config !== 'object') {
351
+ return Either.left(new ValidationError({
352
+ field: 'config',
353
+ constraint: 'must be a valid configuration object',
354
+ receivedValue: config,
355
+ }));
356
+ }
357
+ // Validate shortcuts field if present
358
+ const configObj = config;
359
+ if (configObj['shortcuts'] !== undefined &&
360
+ (typeof configObj['shortcuts'] !== 'object' ||
361
+ configObj['shortcuts'] === null)) {
362
+ return Either.left(new ValidationError({
363
+ field: 'config',
364
+ constraint: 'shortcuts must be a valid object',
365
+ receivedValue: config,
366
+ }));
367
+ }
368
+ // Additional validation could go here
369
+ return Either.right(config);
370
+ }
371
+ /**
372
+ * Get preset by ID with Either-based error handling
373
+ * Synchronous lookup using Either
374
+ */
375
+ getPresetByIdEffect(id) {
376
+ const presets = this.getCommandPresets();
377
+ const preset = presets.presets.find(p => p.id === id);
378
+ if (!preset) {
379
+ return Either.left(new ValidationError({
380
+ field: 'presetId',
381
+ constraint: 'Preset not found',
382
+ receivedValue: id,
383
+ }));
384
+ }
385
+ return Either.right(preset);
386
+ }
387
+ /**
388
+ * Set shortcuts with Effect-based error handling
389
+ *
390
+ * @returns {Effect.Effect<void, FileSystemError, never>} Void on success, FileSystemError on save failure
391
+ *
392
+ * @example
393
+ * ```typescript
394
+ * await Effect.runPromise(
395
+ * configManager.setShortcutsEffect(shortcuts)
396
+ * );
397
+ * ```
398
+ */
399
+ setShortcutsEffect(shortcuts) {
400
+ this.config.shortcuts = shortcuts;
401
+ return this.saveConfigEffect(this.config);
402
+ }
403
+ /**
404
+ * Set command presets with Effect-based error handling
405
+ */
406
+ setCommandPresetsEffect(presets) {
407
+ this.config.commandPresets = presets;
408
+ return this.saveConfigEffect(this.config);
409
+ }
410
+ /**
411
+ * Add or update preset with Effect-based error handling
412
+ */
413
+ addPresetEffect(preset) {
414
+ const presets = this.getCommandPresets();
415
+ // Replace if exists, otherwise add
416
+ const existingIndex = presets.presets.findIndex(p => p.id === preset.id);
417
+ if (existingIndex >= 0) {
418
+ presets.presets[existingIndex] = preset;
419
+ }
420
+ else {
421
+ presets.presets.push(preset);
422
+ }
423
+ return this.setCommandPresetsEffect(presets);
424
+ }
425
+ /**
426
+ * Delete preset with Effect-based error handling
427
+ */
428
+ deletePresetEffect(id) {
429
+ const presets = this.getCommandPresets();
430
+ // Don't delete if it's the last preset
431
+ if (presets.presets.length <= 1) {
432
+ return Effect.fail(new ValidationError({
433
+ field: 'presetId',
434
+ constraint: 'Cannot delete last preset',
435
+ receivedValue: id,
436
+ }));
437
+ }
438
+ // Remove the preset
439
+ presets.presets = presets.presets.filter(p => p.id !== id);
440
+ // Update default if needed
441
+ if (presets.defaultPresetId === id && presets.presets.length > 0) {
442
+ presets.defaultPresetId = presets.presets[0].id;
443
+ }
444
+ return this.setCommandPresetsEffect(presets);
445
+ }
446
+ /**
447
+ * Set default preset with Effect-based error handling
448
+ */
449
+ setDefaultPresetEffect(id) {
450
+ const presets = this.getCommandPresets();
451
+ // Only update if preset exists
452
+ if (!presets.presets.some(p => p.id === id)) {
453
+ return Effect.fail(new ValidationError({
454
+ field: 'presetId',
455
+ constraint: 'Preset not found',
456
+ receivedValue: id,
457
+ }));
458
+ }
459
+ presets.defaultPresetId = id;
460
+ return this.setCommandPresetsEffect(presets);
461
+ }
462
+ // Helper methods
463
+ /**
464
+ * Apply default values to configuration
465
+ */
466
+ applyDefaults(config) {
467
+ // Ensure default values
468
+ if (!config.shortcuts) {
469
+ config.shortcuts = DEFAULT_SHORTCUTS;
470
+ }
471
+ if (!config.statusHooks) {
472
+ config.statusHooks = {};
473
+ }
474
+ if (!config.worktreeHooks) {
475
+ config.worktreeHooks = {};
476
+ }
477
+ if (!config.worktree) {
478
+ config.worktree = {
479
+ autoDirectory: false,
480
+ copySessionData: true,
481
+ };
482
+ }
483
+ if (!Object.prototype.hasOwnProperty.call(config.worktree, 'copySessionData')) {
484
+ config.worktree.copySessionData = true;
485
+ }
486
+ if (!config.command) {
487
+ config.command = {
488
+ command: 'claude',
489
+ };
490
+ }
491
+ return config;
492
+ }
493
+ /**
494
+ * Synchronous legacy shortcuts migration helper
495
+ */
496
+ migrateLegacyShortcutsSync() {
497
+ if (existsSync(this.legacyShortcutsPath)) {
498
+ try {
499
+ const shortcutsData = readFileSync(this.legacyShortcutsPath, 'utf-8');
500
+ const shortcuts = JSON.parse(shortcutsData);
501
+ // Validate that it's a valid shortcuts config
502
+ if (shortcuts && typeof shortcuts === 'object') {
503
+ const config = { shortcuts };
504
+ // Save to new config format
505
+ this.config = config;
506
+ writeFileSync(this.configPath, JSON.stringify(this.config, null, 2));
507
+ console.log('Migrated shortcuts from legacy shortcuts.json to config.json');
508
+ return config;
509
+ }
510
+ }
511
+ catch (error) {
512
+ console.error('Failed to migrate legacy shortcuts:', error);
513
+ }
514
+ }
515
+ return null;
516
+ }
271
517
  }
272
518
  export const configurationManager = new ConfigurationManager();
@@ -35,14 +35,6 @@ vi.mock('./sessionManager.js', () => {
35
35
  emit() {
36
36
  // Mock implementation
37
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
38
  }
47
39
  return { SessionManager: MockSessionManager };
48
40
  });
@@ -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;