claude-comms 1.0.1 → 1.0.2

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.
@@ -0,0 +1,849 @@
1
+ /**
2
+ * Installation Orchestrator - WP06
3
+ *
4
+ * Core orchestrator class that coordinates the complete installation flow:
5
+ * - Pre-flight validation and checks
6
+ * - GitHub fetching via injected FetcherAPI
7
+ * - File writing via injected WriterAPI
8
+ * - Progress aggregation and state management
9
+ * - Transaction-like behavior with rollback support
10
+ * - Error handling and recovery
11
+ *
12
+ * Uses dependency injection for testing and modularity.
13
+ */
14
+
15
+ const EventEmitter = require('events');
16
+ const path = require('path');
17
+ const { InstallerError } = require('../utils/errors');
18
+
19
+ // Error codes enum - matches interface contracts
20
+ const ErrorCode = {
21
+ // Network errors (E1xx)
22
+ NETWORK_ERROR: 'E101',
23
+ GITHUB_RATE_LIMIT: 'E102',
24
+ GITHUB_API_ERROR: 'E103',
25
+
26
+ // File system errors (E2xx)
27
+ FILE_WRITE_ERROR: 'E201',
28
+ FILE_READ_ERROR: 'E202',
29
+ PERMISSION_DENIED: 'E203',
30
+ DISK_FULL: 'E204',
31
+
32
+ // Validation errors (E3xx)
33
+ INVALID_TARGET_DIR: 'E301',
34
+ DEPENDENCY_MISSING: 'E302',
35
+ INVALID_VERSION: 'E303',
36
+
37
+ // User errors (E4xx)
38
+ USER_CANCELLED: 'E401',
39
+ INVALID_INPUT: 'E402',
40
+
41
+ // Installation errors (E5xx)
42
+ VALIDATION_ERROR: 'E501',
43
+ CONFIGURATION_FAILED: 'E502',
44
+ VERIFICATION_FAILED: 'E503'
45
+ };
46
+
47
+ /**
48
+ * Installation state constants
49
+ */
50
+ const InstallationState = {
51
+ IDLE: 'idle',
52
+ VALIDATING: 'validating',
53
+ FETCHING: 'fetching',
54
+ WRITING: 'writing',
55
+ CONFIGURING: 'configuring',
56
+ VERIFYING: 'verifying',
57
+ COMPLETED: 'completed',
58
+ FAILED: 'failed',
59
+ ROLLING_BACK: 'rolling_back'
60
+ };
61
+
62
+ /**
63
+ * Main Installation Orchestrator
64
+ *
65
+ * Coordinates the complete installation flow with dependency injection
66
+ * for fetcher, writer, and logger modules.
67
+ */
68
+ class Installer extends EventEmitter {
69
+ constructor(dependencies = {}) {
70
+ super();
71
+
72
+ // Injected dependencies (replaced with real implementations by WP03/04)
73
+ this.fetcher = dependencies.fetcher || this._createMockFetcher();
74
+ this.writer = dependencies.writer || this._createMockWriter();
75
+ this.logger = dependencies.logger || this._createMockLogger();
76
+ this.validator = dependencies.validator || this._createMockValidator();
77
+
78
+ // Installation state
79
+ this.state = InstallationState.IDLE;
80
+ this.options = null;
81
+ this.backups = new Map(); // path -> backup info
82
+ this.installedFiles = [];
83
+ this.errors = [];
84
+
85
+ // Progress tracking
86
+ this.progress = {
87
+ current: 0,
88
+ total: 0,
89
+ stage: '',
90
+ message: ''
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Main installation entry point
96
+ * @param {InstallOptions} options - Installation configuration
97
+ * @returns {Promise<InstallResult>}
98
+ */
99
+ async install(options) {
100
+ this.options = this._validateOptions(options);
101
+ this.state = InstallationState.VALIDATING;
102
+
103
+ try {
104
+ this.logger.info('Starting Claude Code installation...');
105
+ this._emitProgress('Starting installation...', 0);
106
+
107
+ // Phase 1: Pre-flight validation
108
+ await this._runPreFlightChecks();
109
+
110
+ // Phase 2: Fetch files from GitHub
111
+ const fetchedFiles = await this._fetchInstallationFiles();
112
+
113
+ // Phase 3: Install files with transaction support
114
+ await this._installFilesWithTransaction(fetchedFiles);
115
+
116
+ // Phase 4: Post-installation configuration
117
+ await this._runPostInstallationTasks();
118
+
119
+ // Phase 5: Final verification
120
+ await this._verifyInstallation();
121
+
122
+ this.state = InstallationState.COMPLETED;
123
+ this._emitProgress('Installation completed successfully', 100);
124
+
125
+ return this._buildInstallResult(true);
126
+
127
+ } catch (error) {
128
+ this.state = InstallationState.FAILED;
129
+ this.errors.push(error);
130
+
131
+ this.logger.error('Installation failed:', error.message);
132
+
133
+ // Attempt rollback if we wrote any files
134
+ if (this.installedFiles.length > 0 || this.backups.size > 0) {
135
+ await this._rollbackChanges();
136
+ }
137
+
138
+ return this._buildInstallResult(false);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Validate installation environment without making changes
144
+ * @param {InstallOptions} options - Installation configuration
145
+ * @returns {Promise<ValidationResult>}
146
+ */
147
+ async validateEnvironment(options) {
148
+ this.options = this._validateOptions(options);
149
+
150
+ try {
151
+ this.logger.info('Validating environment...');
152
+
153
+ const issues = [];
154
+
155
+ // Check target directory
156
+ const targetDirIssue = await this.validator.validateTargetDirectory(this.options.targetDir);
157
+ if (targetDirIssue) issues.push(targetDirIssue);
158
+
159
+ // Check dependencies if requested
160
+ if (!this.options.skipPythonCheck) {
161
+ const pythonIssue = await this.validator.validatePythonEnvironment();
162
+ if (pythonIssue) issues.push(pythonIssue);
163
+ }
164
+
165
+ // Check for existing files
166
+ const existingIssue = await this.validator.checkExistingFiles(this.options.targetDir);
167
+ if (existingIssue) issues.push(existingIssue);
168
+
169
+ // Check network connectivity
170
+ const networkIssue = await this.validator.validateNetworkAccess();
171
+ if (networkIssue) issues.push(networkIssue);
172
+
173
+ return {
174
+ valid: issues.length === 0,
175
+ issues
176
+ };
177
+
178
+ } catch (error) {
179
+ return {
180
+ valid: false,
181
+ issues: [{
182
+ code: ErrorCode.VALIDATION_ERROR,
183
+ message: `Validation failed: ${error.message}`,
184
+ severity: 'error'
185
+ }]
186
+ };
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Get current installation progress
192
+ * @returns {ProgressEvent}
193
+ */
194
+ getProgress() {
195
+ return {
196
+ ...this.progress,
197
+ state: this.state,
198
+ hasErrors: this.errors.length > 0
199
+ };
200
+ }
201
+
202
+ // ============================================================================
203
+ // PRIVATE METHODS - Installation Flow
204
+ // ============================================================================
205
+
206
+ /**
207
+ * Run pre-flight validation checks
208
+ */
209
+ async _runPreFlightChecks() {
210
+ this.state = InstallationState.VALIDATING;
211
+ this._emitProgress('Running pre-flight checks...', 10);
212
+
213
+ this.logger.info('Running pre-flight validation...');
214
+
215
+ const validation = await this.validateEnvironment(this.options);
216
+ if (!validation.valid) {
217
+ const errorIssues = validation.issues.filter(issue => issue.severity === 'error');
218
+ if (errorIssues.length > 0) {
219
+ const errorMessage = errorIssues.map(issue => issue.message).join('; ');
220
+ throw new InstallerError(
221
+ `Pre-flight validation failed: ${errorMessage}`,
222
+ ErrorCode.VALIDATION_ERROR
223
+ );
224
+ }
225
+ }
226
+
227
+ // Log warnings but continue
228
+ const warnings = validation.issues.filter(issue => issue.severity === 'warning');
229
+ warnings.forEach(warning => this.logger.warn(warning.message));
230
+
231
+ this.logger.info('Pre-flight checks completed');
232
+ }
233
+
234
+ /**
235
+ * Fetch installation files from GitHub
236
+ */
237
+ async _fetchInstallationFiles() {
238
+ this.state = InstallationState.FETCHING;
239
+ this._emitProgress('Fetching files from GitHub...', 30);
240
+
241
+ this.logger.info(`Fetching files from version: ${this.options.version}`);
242
+
243
+ try {
244
+ // Fetch .claude directory structure
245
+ const claudeFiles = await this.fetcher.fetchDirectory('.claude', {
246
+ version: this.options.version,
247
+ useCache: this.options.cache,
248
+ retryCount: 3
249
+ });
250
+
251
+ // Fetch CLAUDE.md file
252
+ const claudeMd = await this.fetcher.fetchFile('CLAUDE.md', {
253
+ version: this.options.version,
254
+ useCache: this.options.cache
255
+ });
256
+
257
+ const fetchedFiles = {
258
+ '.claude': claudeFiles,
259
+ 'CLAUDE.md': claudeMd
260
+ };
261
+
262
+ // Calculate total files for progress tracking
263
+ this.progress.total = this._countFiles(fetchedFiles);
264
+
265
+ this.logger.info(`Fetched ${this.progress.total} files successfully`);
266
+ return fetchedFiles;
267
+
268
+ } catch (error) {
269
+ throw new InstallerError(
270
+ `Failed to fetch files from GitHub: ${error.message}`,
271
+ ErrorCode.GITHUB_API_ERROR,
272
+ { version: this.options.version }
273
+ );
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Install files with transaction-like behavior
279
+ */
280
+ async _installFilesWithTransaction(fetchedFiles) {
281
+ this.state = InstallationState.WRITING;
282
+ this._emitProgress('Installing files...', 50);
283
+
284
+ if (this.options.dryRun) {
285
+ this.logger.info('DRY RUN: Would install files to:', this.options.targetDir);
286
+ this._simulateInstallation(fetchedFiles);
287
+ return;
288
+ }
289
+
290
+ try {
291
+ // Validate fetched files structure
292
+ const validationResult = this._validateFetchedFiles(fetchedFiles);
293
+ if (!validationResult.complete) {
294
+ this.logger.warn('Incomplete file structure detected:');
295
+ validationResult.issues.forEach(issue => {
296
+ this.logger.warn(` - ${issue.message}`);
297
+ });
298
+ }
299
+
300
+ // Create backups if files exist
301
+ await this._createBackupsIfNeeded(fetchedFiles);
302
+
303
+ // Write files using injected writer
304
+ const writeResult = await this.writer.writeDirectory(
305
+ this._flattenFiles(fetchedFiles),
306
+ {
307
+ targetDir: this.options.targetDir,
308
+ overwrite: this.options.force,
309
+ backup: true,
310
+ preservePermissions: true,
311
+ dryRun: this.options.dryRun
312
+ }
313
+ );
314
+
315
+ // Track installed files for potential rollback
316
+ this.installedFiles = writeResult.written;
317
+
318
+ // Store backup information
319
+ writeResult.backed_up.forEach((backupPath, originalPath) => {
320
+ this.backups.set(originalPath, { backupPath, timestamp: Date.now() });
321
+ });
322
+
323
+ this.logger.info(`Installed ${writeResult.written.length} files`);
324
+ this.logger.info(`Created ${writeResult.directoriesCreated?.length || 0} directories`);
325
+
326
+ if (writeResult.errors.length > 0) {
327
+ const errorSummary = writeResult.errors.map(err => `${err.path}: ${err.error.message}`).join('; ');
328
+ throw new InstallerError(
329
+ `Some files failed to install: ${errorSummary}`,
330
+ ErrorCode.FILE_WRITE_ERROR
331
+ );
332
+ }
333
+
334
+ } catch (error) {
335
+ throw new InstallerError(
336
+ `File installation failed: ${error.message}`,
337
+ ErrorCode.FILE_WRITE_ERROR
338
+ );
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Run post-installation configuration tasks
344
+ */
345
+ async _runPostInstallationTasks() {
346
+ this.state = InstallationState.CONFIGURING;
347
+ this._emitProgress('Configuring installation...', 80);
348
+
349
+ if (this.options.dryRun) {
350
+ this.logger.info('DRY RUN: Would configure local settings');
351
+ return;
352
+ }
353
+
354
+ try {
355
+ // Generate settings.local.json with comprehensive defaults
356
+ const settingsPath = path.join(this.options.targetDir, '.claude', 'settings.local.json');
357
+ const defaultSettings = {
358
+ observability: {
359
+ server_url: 'http://localhost:3001',
360
+ enabled: true,
361
+ auto_start: false
362
+ },
363
+ hooks: {
364
+ enabled: true,
365
+ python_path: 'python3',
366
+ uv_path: 'uv',
367
+ timeout: 30
368
+ },
369
+ agents: {
370
+ max_concurrent: 10,
371
+ timeout: 300,
372
+ retry_attempts: 3
373
+ },
374
+ communication: {
375
+ message_retention_hours: 24,
376
+ broadcast_enabled: true
377
+ },
378
+ development: {
379
+ verbose_logging: false,
380
+ debug_mode: false
381
+ },
382
+ installation: {
383
+ date: new Date().toISOString(),
384
+ version: this.options.version,
385
+ installer_version: '1.0.0',
386
+ installed_via: 'npx-installer'
387
+ }
388
+ };
389
+
390
+ // Only create settings.local.json if it doesn't exist
391
+ const settingsExists = await this.validator.pathExists(settingsPath);
392
+ if (!settingsExists) {
393
+ await this.writer.writeFile(settingsPath, JSON.stringify(defaultSettings, null, 2), {
394
+ overwrite: false
395
+ });
396
+ this.installedFiles.push(settingsPath);
397
+ this.logger.info('Created settings.local.json with default configuration');
398
+ } else {
399
+ this.logger.info('settings.local.json already exists, skipping creation');
400
+ }
401
+
402
+ this.logger.info('Post-installation configuration completed');
403
+
404
+ } catch (error) {
405
+ throw new InstallerError(
406
+ `Post-installation configuration failed: ${error.message}`,
407
+ ErrorCode.CONFIGURATION_FAILED
408
+ );
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Verify installation was successful
414
+ */
415
+ async _verifyInstallation() {
416
+ this.state = InstallationState.VERIFYING;
417
+ this._emitProgress('Verifying installation...', 90);
418
+
419
+ const requiredPaths = [
420
+ path.join(this.options.targetDir, '.claude'),
421
+ path.join(this.options.targetDir, 'CLAUDE.md'),
422
+ path.join(this.options.targetDir, '.claude', 'settings.local.json')
423
+ ];
424
+
425
+ const missingPaths = [];
426
+ for (const requiredPath of requiredPaths) {
427
+ const exists = await this.validator.pathExists(requiredPath);
428
+ if (!exists) {
429
+ missingPaths.push(requiredPath);
430
+ }
431
+ }
432
+
433
+ // Use the file writer's validation method for completeness check
434
+ if (this.writer.validateInstallationCompleteness) {
435
+ const completenessResult = await this.writer.validateInstallationCompleteness(
436
+ this.options.targetDir,
437
+ ['CLAUDE.md']
438
+ );
439
+
440
+ if (!completenessResult.complete) {
441
+ this.logger.warn('Installation completeness issues detected:');
442
+ completenessResult.issues.forEach(issue => {
443
+ this.logger.warn(` - ${issue.message}`);
444
+ });
445
+ }
446
+ }
447
+
448
+ if (missingPaths.length > 0) {
449
+ throw new InstallerError(
450
+ `Installation verification failed: missing ${missingPaths.join(', ')}`,
451
+ ErrorCode.VERIFICATION_FAILED
452
+ );
453
+ }
454
+
455
+ this.logger.info('Installation verification completed');
456
+ }
457
+
458
+ /**
459
+ * Rollback changes if installation fails
460
+ */
461
+ async _rollbackChanges() {
462
+ this.state = InstallationState.ROLLING_BACK;
463
+ this.logger.warn('Rolling back installation changes...');
464
+
465
+ try {
466
+ // Remove newly installed files
467
+ for (const filePath of this.installedFiles) {
468
+ try {
469
+ await this.writer.removeFile(filePath);
470
+ } catch (error) {
471
+ this.logger.warn(`Failed to remove file during rollback: ${filePath}`);
472
+ }
473
+ }
474
+
475
+ // Restore backed up files
476
+ for (const [originalPath, backupInfo] of this.backups) {
477
+ try {
478
+ await this.writer.rollback(backupInfo.backupPath, originalPath);
479
+ } catch (error) {
480
+ this.logger.warn(`Failed to restore backup during rollback: ${originalPath}`);
481
+ }
482
+ }
483
+
484
+ this.logger.info('Rollback completed');
485
+
486
+ } catch (error) {
487
+ this.logger.error('Rollback failed:', error.message);
488
+ }
489
+ }
490
+
491
+ // ============================================================================
492
+ // PRIVATE METHODS - Utilities
493
+ // ============================================================================
494
+
495
+ /**
496
+ * Validate and normalize installation options
497
+ */
498
+ _validateOptions(options) {
499
+ if (!options || typeof options !== 'object') {
500
+ throw new InstallerError('Invalid options provided', ErrorCode.INVALID_INPUT);
501
+ }
502
+
503
+ return {
504
+ targetDir: path.resolve(options.targetDir || process.cwd()),
505
+ version: options.version || 'main',
506
+ force: Boolean(options.force),
507
+ skipPythonCheck: Boolean(options.skipPythonCheck),
508
+ verbose: Boolean(options.verbose),
509
+ cache: Boolean(options.cache),
510
+ dryRun: Boolean(options.dryRun)
511
+ };
512
+ }
513
+
514
+ /**
515
+ * Emit progress update event
516
+ */
517
+ _emitProgress(message, current) {
518
+ this.progress = {
519
+ ...this.progress,
520
+ current,
521
+ message,
522
+ stage: this.state
523
+ };
524
+
525
+ this.emit('progress', {
526
+ task: 'installation',
527
+ current,
528
+ total: 100,
529
+ percentage: current,
530
+ message,
531
+ status: this.state === InstallationState.FAILED ? 'failed' : 'running'
532
+ });
533
+ }
534
+
535
+ /**
536
+ * Build final installation result
537
+ */
538
+ _buildInstallResult(success) {
539
+ return {
540
+ success,
541
+ filesInstalled: this.installedFiles,
542
+ warnings: [], // TODO: Collect warnings during installation
543
+ errors: this.errors,
544
+ backups: Array.from(this.backups.entries()).map(([path, info]) => ({
545
+ originalPath: path,
546
+ backupPath: info.backupPath,
547
+ timestamp: info.timestamp
548
+ }))
549
+ };
550
+ }
551
+
552
+ /**
553
+ * Count total number of files in fetch result
554
+ */
555
+ _countFiles(fetchedFiles) {
556
+ let count = 0;
557
+
558
+ function countRecursive(item) {
559
+ if (item.type === 'file') {
560
+ count++;
561
+ } else if (item.children) {
562
+ item.children.forEach(countRecursive);
563
+ }
564
+ }
565
+
566
+ Object.values(fetchedFiles).forEach(item => {
567
+ if (typeof item === 'object' && item.type) {
568
+ countRecursive(item);
569
+ } else {
570
+ count++; // Single file
571
+ }
572
+ });
573
+
574
+ return count;
575
+ }
576
+
577
+ /**
578
+ * Flatten file tree structure for writer
579
+ */
580
+ _flattenFiles(fetchedFiles) {
581
+ const flattened = [];
582
+
583
+ function flattenRecursive(item, basePath = '') {
584
+ if (item.type === 'file') {
585
+ // Construct proper path: if basePath exists and item.path doesn't already include it
586
+ let filePath;
587
+ if (basePath && !item.path.startsWith(basePath)) {
588
+ filePath = path.join(basePath, item.path);
589
+ } else {
590
+ filePath = item.path;
591
+ }
592
+
593
+ flattened.push({
594
+ path: filePath,
595
+ content: item.content,
596
+ encoding: item.encoding || 'utf-8',
597
+ permissions: item.permissions
598
+ });
599
+ } else if (item.type === 'dir' && item.children) {
600
+ // For directory recursion, construct the new basePath from current basePath and item's path
601
+ const newBasePath = basePath && !item.path.startsWith(basePath) ?
602
+ path.join(basePath, item.path) : item.path;
603
+
604
+ item.children.forEach(child => {
605
+ flattenRecursive(child, newBasePath);
606
+ });
607
+ }
608
+ }
609
+
610
+ Object.entries(fetchedFiles).forEach(([rootPath, item]) => {
611
+ if (typeof item === 'object' && item.type) {
612
+ // For directory trees like .claude, start recursion with the rootPath as basePath
613
+ if (item.type === 'dir' && item.children) {
614
+ item.children.forEach(child => {
615
+ flattenRecursive(child, rootPath);
616
+ });
617
+ } else if (item.type === 'file') {
618
+ // Single file - use item.path directly if it's already properly prefixed, otherwise use rootPath
619
+ const filePath = item.path.startsWith(rootPath) ? item.path : rootPath;
620
+ flattened.push({
621
+ path: filePath,
622
+ content: item.content,
623
+ encoding: item.encoding || 'utf-8',
624
+ permissions: item.permissions
625
+ });
626
+ }
627
+ } else if (typeof item === 'object' && item.content) {
628
+ // Single file like CLAUDE.md - use rootPath as the target path
629
+ flattened.push({
630
+ path: rootPath,
631
+ content: item.content,
632
+ encoding: item.encoding || 'utf-8'
633
+ });
634
+ } else if (typeof item === 'string') {
635
+ // Direct content string
636
+ flattened.push({
637
+ path: rootPath,
638
+ content: item,
639
+ encoding: 'utf-8'
640
+ });
641
+ }
642
+ });
643
+
644
+ return flattened;
645
+ }
646
+
647
+ /**
648
+ * Simulate installation for dry-run mode
649
+ */
650
+ _simulateInstallation(fetchedFiles) {
651
+ this.logger.info('DRY RUN: Files that would be installed:');
652
+
653
+ const flattened = this._flattenFiles(fetchedFiles);
654
+ flattened.forEach(file => {
655
+ const targetPath = path.join(this.options.targetDir, file.path);
656
+ this.logger.info(` • ${targetPath}`);
657
+ });
658
+
659
+ this.logger.info(`DRY RUN: Would install ${flattened.length} files`);
660
+ }
661
+
662
+ /**
663
+ * Create backups of existing files if needed
664
+ */
665
+ async _createBackupsIfNeeded(_fetchedFiles) {
666
+ if (!this.options.force) {
667
+ // Check for existing .claude directory
668
+ const claudeDir = path.join(this.options.targetDir, '.claude');
669
+ const claudeMd = path.join(this.options.targetDir, 'CLAUDE.md');
670
+
671
+ const existingPaths = [];
672
+ if (await this.validator.pathExists(claudeDir)) {
673
+ existingPaths.push('.claude/');
674
+ }
675
+ if (await this.validator.pathExists(claudeMd)) {
676
+ existingPaths.push('CLAUDE.md');
677
+ }
678
+
679
+ if (existingPaths.length > 0) {
680
+ this.logger.warn(`Existing files will be backed up: ${existingPaths.join(', ')}`);
681
+ }
682
+ }
683
+ }
684
+
685
+ /**
686
+ * Validate fetched files structure for completeness
687
+ */
688
+ _validateFetchedFiles(fetchedFiles) {
689
+ const issues = [];
690
+
691
+ // Check for essential components
692
+ if (!fetchedFiles['.claude']) {
693
+ issues.push({
694
+ type: 'missing_component',
695
+ component: '.claude',
696
+ message: 'Missing .claude directory structure'
697
+ });
698
+ }
699
+
700
+ if (!fetchedFiles['CLAUDE.md']) {
701
+ issues.push({
702
+ type: 'missing_component',
703
+ component: 'CLAUDE.md',
704
+ message: 'Missing CLAUDE.md file'
705
+ });
706
+ }
707
+
708
+ // Check .claude structure if present
709
+ if (fetchedFiles['.claude'] && fetchedFiles['.claude'].children) {
710
+ const claudeChildren = fetchedFiles['.claude'].children;
711
+ const expectedDirs = ['hooks', 'agents', 'commands'];
712
+
713
+ for (const expectedDir of expectedDirs) {
714
+ const hasDir = claudeChildren.some(child =>
715
+ child.path === expectedDir && child.type === 'dir'
716
+ );
717
+ if (!hasDir) {
718
+ issues.push({
719
+ type: 'missing_directory',
720
+ directory: expectedDir,
721
+ message: `Missing ${expectedDir} directory in .claude structure`
722
+ });
723
+ }
724
+ }
725
+ }
726
+
727
+ return {
728
+ complete: issues.length === 0,
729
+ issues
730
+ };
731
+ }
732
+
733
+ // ============================================================================
734
+ // MOCK IMPLEMENTATIONS - To be replaced by real modules from WP03/04
735
+ // ============================================================================
736
+
737
+ _createMockFetcher() {
738
+ return {
739
+ async fetchDirectory(path, _options) {
740
+ return {
741
+ path,
742
+ type: 'dir',
743
+ children: [
744
+ {
745
+ path: 'settings.json',
746
+ type: 'file',
747
+ content: '{"version": "1.0.0"}',
748
+ encoding: 'utf-8'
749
+ }
750
+ ]
751
+ };
752
+ },
753
+
754
+ async fetchFile(path, _options) {
755
+ return {
756
+ path,
757
+ content: `# Claude Code Setup\n\nInstalled from ${this.options?.version || 'main'}`,
758
+ encoding: 'utf-8'
759
+ };
760
+ },
761
+
762
+ async validateConnection() {
763
+ return true;
764
+ }
765
+ };
766
+ }
767
+
768
+ _createMockWriter() {
769
+ return {
770
+ async writeFile(path, content, options = {}) {
771
+ if (options.dryRun) return;
772
+ // Mock implementation - real writer will handle actual file operations
773
+ },
774
+
775
+ async writeDirectory(files, options = {}) {
776
+ return {
777
+ written: files.map(f => path.join(options.targetDir || '', f.path)),
778
+ skipped: [],
779
+ backed_up: new Map(),
780
+ errors: [],
781
+ directoriesCreated: files.map(f => path.dirname(path.join(options.targetDir || '', f.path))).filter((v, i, a) => a.indexOf(v) === i)
782
+ };
783
+ },
784
+
785
+ async validateInstallationCompleteness(_targetDir, _expectedFiles = []) {
786
+ return {
787
+ complete: true,
788
+ issues: []
789
+ };
790
+ },
791
+
792
+ async backup(path) {
793
+ return `${path}.backup-${Date.now()}`;
794
+ },
795
+
796
+ async rollback(_backupPath, _originalPath) {
797
+ // Mock implementation
798
+ },
799
+
800
+ async removeFile(_path) {
801
+ // Mock implementation
802
+ }
803
+ };
804
+ }
805
+
806
+ _createMockLogger() {
807
+ return {
808
+ info: (msg, ...args) => console.log(`[INFO] ${msg}`, ...args),
809
+ warn: (msg, ...args) => console.warn(`[WARN] ${msg}`, ...args),
810
+ error: (msg, ...args) => console.error(`[ERROR] ${msg}`, ...args),
811
+ debug: (msg, ...args) => console.log(`[DEBUG] ${msg}`, ...args)
812
+ };
813
+ }
814
+
815
+ _createMockValidator() {
816
+ return {
817
+ async validateTargetDirectory(_targetDir) {
818
+ // Mock validation - always pass
819
+ return null;
820
+ },
821
+
822
+ async validatePythonEnvironment() {
823
+ // Mock validation - always pass
824
+ return null;
825
+ },
826
+
827
+ async checkExistingFiles(_targetDir) {
828
+ // Mock validation - no existing files
829
+ return null;
830
+ },
831
+
832
+ async validateNetworkAccess() {
833
+ // Mock validation - always pass
834
+ return null;
835
+ },
836
+
837
+ async pathExists(_path) {
838
+ // Mock - assume path exists for verification
839
+ return true;
840
+ }
841
+ };
842
+ }
843
+ }
844
+
845
+ module.exports = {
846
+ Installer,
847
+ InstallationState,
848
+ ErrorCode
849
+ };