claude-comms 1.0.1 → 1.0.3
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/CHANGELOG.md +76 -0
- package/README.md +64 -23
- package/bin/claude-setup.js +118 -38
- package/package.json +67 -19
- package/src/cli/index.js +30 -0
- package/src/cli/interfaces.js +386 -0
- package/src/cli/parser.js +158 -0
- package/src/cli/prompts.js +317 -0
- package/src/cli/runner.js +436 -0
- package/src/fetcher/github.js +1092 -0
- package/src/index.js +221 -0
- package/src/installer/dependency-check.js +19 -0
- package/src/installer/file-writer.js +648 -0
- package/src/orchestrator/installer.js +875 -0
- package/src/orchestrator/main.js +267 -0
- package/src/orchestrator/mocks/index.js +388 -0
- package/src/utils/config.js +430 -0
- package/src/utils/constants.js +397 -0
- package/src/utils/errors.js +361 -0
- package/src/utils/index.js +46 -0
- package/src/utils/logger.js +177 -0
- package/src/utils/platform.js +362 -0
- package/src/utils/retry.js +358 -0
- package/templates/settings.local.json +1 -0
- package/.claude/hooks/comms/messages_UmaMatrix.json +0 -10
- package/.claude/hooks/comms/messages_VictorSpiral.json +0 -10
- package/.claude/hooks/comms/messages_WendyQuantum.json +0 -10
- package/.claude/hooks/comms/messages_XavierPrism.json +0 -10
- package/templates/get_unread_messages.py +0 -54
- package/templates/send_message.py +0 -75
|
@@ -0,0 +1,875 @@
|
|
|
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
|
+
// Try tarball approach first if fetcher supports it
|
|
245
|
+
if (this.fetcher.fetchAsTarball) {
|
|
246
|
+
this.logger.info('Using optimized tarball fetch strategy');
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const fetchedFiles = await this.fetcher.fetchAsTarball({
|
|
250
|
+
version: this.options.version,
|
|
251
|
+
useCache: this.options.cache,
|
|
252
|
+
retryCount: 3
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Calculate total files for progress tracking
|
|
256
|
+
this.progress.total = this._countFiles(fetchedFiles);
|
|
257
|
+
|
|
258
|
+
this.logger.info(`Fetched ${this.progress.total} files successfully via tarball`);
|
|
259
|
+
return fetchedFiles;
|
|
260
|
+
|
|
261
|
+
} catch (tarballError) {
|
|
262
|
+
this.logger.warn(`Tarball fetch failed: ${tarballError.message}`);
|
|
263
|
+
this.logger.info('Falling back to individual file fetch strategy');
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Fallback to individual file fetching
|
|
268
|
+
this.logger.info('Using individual file fetch strategy');
|
|
269
|
+
|
|
270
|
+
// Fetch .claude directory structure
|
|
271
|
+
const claudeFiles = await this.fetcher.fetchDirectory('.claude', {
|
|
272
|
+
version: this.options.version,
|
|
273
|
+
useCache: this.options.cache,
|
|
274
|
+
retryCount: 3
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Fetch CLAUDE.md file
|
|
278
|
+
const claudeMd = await this.fetcher.fetchFile('CLAUDE.md', {
|
|
279
|
+
version: this.options.version,
|
|
280
|
+
useCache: this.options.cache
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const fetchedFiles = {
|
|
284
|
+
'.claude': claudeFiles,
|
|
285
|
+
'CLAUDE.md': claudeMd
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Calculate total files for progress tracking
|
|
289
|
+
this.progress.total = this._countFiles(fetchedFiles);
|
|
290
|
+
|
|
291
|
+
this.logger.info(`Fetched ${this.progress.total} files successfully via individual fetch`);
|
|
292
|
+
return fetchedFiles;
|
|
293
|
+
|
|
294
|
+
} catch (error) {
|
|
295
|
+
throw new InstallerError(
|
|
296
|
+
`Failed to fetch files from GitHub: ${error.message}`,
|
|
297
|
+
ErrorCode.GITHUB_API_ERROR,
|
|
298
|
+
{ version: this.options.version }
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Install files with transaction-like behavior
|
|
305
|
+
*/
|
|
306
|
+
async _installFilesWithTransaction(fetchedFiles) {
|
|
307
|
+
this.state = InstallationState.WRITING;
|
|
308
|
+
this._emitProgress('Installing files...', 50);
|
|
309
|
+
|
|
310
|
+
if (this.options.dryRun) {
|
|
311
|
+
this.logger.info('DRY RUN: Would install files to:', this.options.targetDir);
|
|
312
|
+
this._simulateInstallation(fetchedFiles);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
// Validate fetched files structure
|
|
318
|
+
const validationResult = this._validateFetchedFiles(fetchedFiles);
|
|
319
|
+
if (!validationResult.complete) {
|
|
320
|
+
this.logger.warn('Incomplete file structure detected:');
|
|
321
|
+
validationResult.issues.forEach(issue => {
|
|
322
|
+
this.logger.warn(` - ${issue.message}`);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Create backups if files exist
|
|
327
|
+
await this._createBackupsIfNeeded(fetchedFiles);
|
|
328
|
+
|
|
329
|
+
// Write files using injected writer
|
|
330
|
+
const writeResult = await this.writer.writeDirectory(
|
|
331
|
+
this._flattenFiles(fetchedFiles),
|
|
332
|
+
{
|
|
333
|
+
targetDir: this.options.targetDir,
|
|
334
|
+
overwrite: this.options.force,
|
|
335
|
+
backup: true,
|
|
336
|
+
preservePermissions: true,
|
|
337
|
+
dryRun: this.options.dryRun
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
// Track installed files for potential rollback
|
|
342
|
+
this.installedFiles = writeResult.written;
|
|
343
|
+
|
|
344
|
+
// Store backup information
|
|
345
|
+
writeResult.backed_up.forEach((backupPath, originalPath) => {
|
|
346
|
+
this.backups.set(originalPath, { backupPath, timestamp: Date.now() });
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
this.logger.info(`Installed ${writeResult.written.length} files`);
|
|
350
|
+
this.logger.info(`Created ${writeResult.directoriesCreated?.length || 0} directories`);
|
|
351
|
+
|
|
352
|
+
if (writeResult.errors.length > 0) {
|
|
353
|
+
const errorSummary = writeResult.errors.map(err => `${err.path}: ${err.error.message}`).join('; ');
|
|
354
|
+
throw new InstallerError(
|
|
355
|
+
`Some files failed to install: ${errorSummary}`,
|
|
356
|
+
ErrorCode.FILE_WRITE_ERROR
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
} catch (error) {
|
|
361
|
+
throw new InstallerError(
|
|
362
|
+
`File installation failed: ${error.message}`,
|
|
363
|
+
ErrorCode.FILE_WRITE_ERROR
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Run post-installation configuration tasks
|
|
370
|
+
*/
|
|
371
|
+
async _runPostInstallationTasks() {
|
|
372
|
+
this.state = InstallationState.CONFIGURING;
|
|
373
|
+
this._emitProgress('Configuring installation...', 80);
|
|
374
|
+
|
|
375
|
+
if (this.options.dryRun) {
|
|
376
|
+
this.logger.info('DRY RUN: Would configure local settings');
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
// Generate settings.local.json with comprehensive defaults
|
|
382
|
+
const settingsPath = path.join(this.options.targetDir, '.claude', 'settings.local.json');
|
|
383
|
+
const defaultSettings = {
|
|
384
|
+
observability: {
|
|
385
|
+
server_url: 'http://localhost:3001',
|
|
386
|
+
enabled: true,
|
|
387
|
+
auto_start: false
|
|
388
|
+
},
|
|
389
|
+
hooks: {
|
|
390
|
+
enabled: true,
|
|
391
|
+
python_path: 'python3',
|
|
392
|
+
uv_path: 'uv',
|
|
393
|
+
timeout: 30
|
|
394
|
+
},
|
|
395
|
+
agents: {
|
|
396
|
+
max_concurrent: 10,
|
|
397
|
+
timeout: 300,
|
|
398
|
+
retry_attempts: 3
|
|
399
|
+
},
|
|
400
|
+
communication: {
|
|
401
|
+
message_retention_hours: 24,
|
|
402
|
+
broadcast_enabled: true
|
|
403
|
+
},
|
|
404
|
+
development: {
|
|
405
|
+
verbose_logging: false,
|
|
406
|
+
debug_mode: false
|
|
407
|
+
},
|
|
408
|
+
installation: {
|
|
409
|
+
date: new Date().toISOString(),
|
|
410
|
+
version: this.options.version,
|
|
411
|
+
installer_version: '1.0.0',
|
|
412
|
+
installed_via: 'npx-installer'
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// Only create settings.local.json if it doesn't exist
|
|
417
|
+
const settingsExists = await this.validator.pathExists(settingsPath);
|
|
418
|
+
if (!settingsExists) {
|
|
419
|
+
await this.writer.writeFile(settingsPath, JSON.stringify(defaultSettings, null, 2), {
|
|
420
|
+
overwrite: false
|
|
421
|
+
});
|
|
422
|
+
this.installedFiles.push(settingsPath);
|
|
423
|
+
this.logger.info('Created settings.local.json with default configuration');
|
|
424
|
+
} else {
|
|
425
|
+
this.logger.info('settings.local.json already exists, skipping creation');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this.logger.info('Post-installation configuration completed');
|
|
429
|
+
|
|
430
|
+
} catch (error) {
|
|
431
|
+
throw new InstallerError(
|
|
432
|
+
`Post-installation configuration failed: ${error.message}`,
|
|
433
|
+
ErrorCode.CONFIGURATION_FAILED
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Verify installation was successful
|
|
440
|
+
*/
|
|
441
|
+
async _verifyInstallation() {
|
|
442
|
+
this.state = InstallationState.VERIFYING;
|
|
443
|
+
this._emitProgress('Verifying installation...', 90);
|
|
444
|
+
|
|
445
|
+
const requiredPaths = [
|
|
446
|
+
path.join(this.options.targetDir, '.claude'),
|
|
447
|
+
path.join(this.options.targetDir, 'CLAUDE.md'),
|
|
448
|
+
path.join(this.options.targetDir, '.claude', 'settings.local.json')
|
|
449
|
+
];
|
|
450
|
+
|
|
451
|
+
const missingPaths = [];
|
|
452
|
+
for (const requiredPath of requiredPaths) {
|
|
453
|
+
const exists = await this.validator.pathExists(requiredPath);
|
|
454
|
+
if (!exists) {
|
|
455
|
+
missingPaths.push(requiredPath);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Use the file writer's validation method for completeness check
|
|
460
|
+
if (this.writer.validateInstallationCompleteness) {
|
|
461
|
+
const completenessResult = await this.writer.validateInstallationCompleteness(
|
|
462
|
+
this.options.targetDir,
|
|
463
|
+
['CLAUDE.md']
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
if (!completenessResult.complete) {
|
|
467
|
+
this.logger.warn('Installation completeness issues detected:');
|
|
468
|
+
completenessResult.issues.forEach(issue => {
|
|
469
|
+
this.logger.warn(` - ${issue.message}`);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (missingPaths.length > 0) {
|
|
475
|
+
throw new InstallerError(
|
|
476
|
+
`Installation verification failed: missing ${missingPaths.join(', ')}`,
|
|
477
|
+
ErrorCode.VERIFICATION_FAILED
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
this.logger.info('Installation verification completed');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Rollback changes if installation fails
|
|
486
|
+
*/
|
|
487
|
+
async _rollbackChanges() {
|
|
488
|
+
this.state = InstallationState.ROLLING_BACK;
|
|
489
|
+
this.logger.warn('Rolling back installation changes...');
|
|
490
|
+
|
|
491
|
+
try {
|
|
492
|
+
// Remove newly installed files
|
|
493
|
+
for (const filePath of this.installedFiles) {
|
|
494
|
+
try {
|
|
495
|
+
await this.writer.removeFile(filePath);
|
|
496
|
+
} catch (error) {
|
|
497
|
+
this.logger.warn(`Failed to remove file during rollback: ${filePath}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Restore backed up files
|
|
502
|
+
for (const [originalPath, backupInfo] of this.backups) {
|
|
503
|
+
try {
|
|
504
|
+
await this.writer.rollback(backupInfo.backupPath, originalPath);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
this.logger.warn(`Failed to restore backup during rollback: ${originalPath}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
this.logger.info('Rollback completed');
|
|
511
|
+
|
|
512
|
+
} catch (error) {
|
|
513
|
+
this.logger.error('Rollback failed:', error.message);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ============================================================================
|
|
518
|
+
// PRIVATE METHODS - Utilities
|
|
519
|
+
// ============================================================================
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Validate and normalize installation options
|
|
523
|
+
*/
|
|
524
|
+
_validateOptions(options) {
|
|
525
|
+
if (!options || typeof options !== 'object') {
|
|
526
|
+
throw new InstallerError('Invalid options provided', ErrorCode.INVALID_INPUT);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return {
|
|
530
|
+
targetDir: path.resolve(options.targetDir || process.cwd()),
|
|
531
|
+
version: options.version || 'main',
|
|
532
|
+
force: Boolean(options.force),
|
|
533
|
+
skipPythonCheck: Boolean(options.skipPythonCheck),
|
|
534
|
+
verbose: Boolean(options.verbose),
|
|
535
|
+
cache: Boolean(options.cache),
|
|
536
|
+
dryRun: Boolean(options.dryRun)
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Emit progress update event
|
|
542
|
+
*/
|
|
543
|
+
_emitProgress(message, current) {
|
|
544
|
+
this.progress = {
|
|
545
|
+
...this.progress,
|
|
546
|
+
current,
|
|
547
|
+
message,
|
|
548
|
+
stage: this.state
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
this.emit('progress', {
|
|
552
|
+
task: 'installation',
|
|
553
|
+
current,
|
|
554
|
+
total: 100,
|
|
555
|
+
percentage: current,
|
|
556
|
+
message,
|
|
557
|
+
status: this.state === InstallationState.FAILED ? 'failed' : 'running'
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Build final installation result
|
|
563
|
+
*/
|
|
564
|
+
_buildInstallResult(success) {
|
|
565
|
+
return {
|
|
566
|
+
success,
|
|
567
|
+
filesInstalled: this.installedFiles,
|
|
568
|
+
warnings: [], // TODO: Collect warnings during installation
|
|
569
|
+
errors: this.errors,
|
|
570
|
+
backups: Array.from(this.backups.entries()).map(([path, info]) => ({
|
|
571
|
+
originalPath: path,
|
|
572
|
+
backupPath: info.backupPath,
|
|
573
|
+
timestamp: info.timestamp
|
|
574
|
+
}))
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Count total number of files in fetch result
|
|
580
|
+
*/
|
|
581
|
+
_countFiles(fetchedFiles) {
|
|
582
|
+
let count = 0;
|
|
583
|
+
|
|
584
|
+
function countRecursive(item) {
|
|
585
|
+
if (item.type === 'file') {
|
|
586
|
+
count++;
|
|
587
|
+
} else if (item.children) {
|
|
588
|
+
item.children.forEach(countRecursive);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
Object.values(fetchedFiles).forEach(item => {
|
|
593
|
+
if (typeof item === 'object' && item.type) {
|
|
594
|
+
countRecursive(item);
|
|
595
|
+
} else {
|
|
596
|
+
count++; // Single file
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
return count;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Flatten file tree structure for writer
|
|
605
|
+
*/
|
|
606
|
+
_flattenFiles(fetchedFiles) {
|
|
607
|
+
const flattened = [];
|
|
608
|
+
|
|
609
|
+
function flattenRecursive(item, basePath = '') {
|
|
610
|
+
if (item.type === 'file') {
|
|
611
|
+
// Construct proper path: if basePath exists and item.path doesn't already include it
|
|
612
|
+
let filePath;
|
|
613
|
+
if (basePath && !item.path.startsWith(basePath)) {
|
|
614
|
+
filePath = path.join(basePath, item.path);
|
|
615
|
+
} else {
|
|
616
|
+
filePath = item.path;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
flattened.push({
|
|
620
|
+
path: filePath,
|
|
621
|
+
content: item.content,
|
|
622
|
+
encoding: item.encoding || 'utf-8',
|
|
623
|
+
permissions: item.permissions
|
|
624
|
+
});
|
|
625
|
+
} else if (item.type === 'dir' && item.children) {
|
|
626
|
+
// For directory recursion, construct the new basePath from current basePath and item's path
|
|
627
|
+
const newBasePath = basePath && !item.path.startsWith(basePath) ?
|
|
628
|
+
path.join(basePath, item.path) : item.path;
|
|
629
|
+
|
|
630
|
+
item.children.forEach(child => {
|
|
631
|
+
flattenRecursive(child, newBasePath);
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
Object.entries(fetchedFiles).forEach(([rootPath, item]) => {
|
|
637
|
+
if (typeof item === 'object' && item.type) {
|
|
638
|
+
// For directory trees like .claude, start recursion with the rootPath as basePath
|
|
639
|
+
if (item.type === 'dir' && item.children) {
|
|
640
|
+
item.children.forEach(child => {
|
|
641
|
+
flattenRecursive(child, rootPath);
|
|
642
|
+
});
|
|
643
|
+
} else if (item.type === 'file') {
|
|
644
|
+
// Single file - use item.path directly if it's already properly prefixed, otherwise use rootPath
|
|
645
|
+
const filePath = item.path.startsWith(rootPath) ? item.path : rootPath;
|
|
646
|
+
flattened.push({
|
|
647
|
+
path: filePath,
|
|
648
|
+
content: item.content,
|
|
649
|
+
encoding: item.encoding || 'utf-8',
|
|
650
|
+
permissions: item.permissions
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
} else if (typeof item === 'object' && item.content) {
|
|
654
|
+
// Single file like CLAUDE.md - use rootPath as the target path
|
|
655
|
+
flattened.push({
|
|
656
|
+
path: rootPath,
|
|
657
|
+
content: item.content,
|
|
658
|
+
encoding: item.encoding || 'utf-8'
|
|
659
|
+
});
|
|
660
|
+
} else if (typeof item === 'string') {
|
|
661
|
+
// Direct content string
|
|
662
|
+
flattened.push({
|
|
663
|
+
path: rootPath,
|
|
664
|
+
content: item,
|
|
665
|
+
encoding: 'utf-8'
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
return flattened;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Simulate installation for dry-run mode
|
|
675
|
+
*/
|
|
676
|
+
_simulateInstallation(fetchedFiles) {
|
|
677
|
+
this.logger.info('DRY RUN: Files that would be installed:');
|
|
678
|
+
|
|
679
|
+
const flattened = this._flattenFiles(fetchedFiles);
|
|
680
|
+
flattened.forEach(file => {
|
|
681
|
+
const targetPath = path.join(this.options.targetDir, file.path);
|
|
682
|
+
this.logger.info(` • ${targetPath}`);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
this.logger.info(`DRY RUN: Would install ${flattened.length} files`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Create backups of existing files if needed
|
|
690
|
+
*/
|
|
691
|
+
async _createBackupsIfNeeded(_fetchedFiles) {
|
|
692
|
+
if (!this.options.force) {
|
|
693
|
+
// Check for existing .claude directory
|
|
694
|
+
const claudeDir = path.join(this.options.targetDir, '.claude');
|
|
695
|
+
const claudeMd = path.join(this.options.targetDir, 'CLAUDE.md');
|
|
696
|
+
|
|
697
|
+
const existingPaths = [];
|
|
698
|
+
if (await this.validator.pathExists(claudeDir)) {
|
|
699
|
+
existingPaths.push('.claude/');
|
|
700
|
+
}
|
|
701
|
+
if (await this.validator.pathExists(claudeMd)) {
|
|
702
|
+
existingPaths.push('CLAUDE.md');
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (existingPaths.length > 0) {
|
|
706
|
+
this.logger.warn(`Existing files will be backed up: ${existingPaths.join(', ')}`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Validate fetched files structure for completeness
|
|
713
|
+
*/
|
|
714
|
+
_validateFetchedFiles(fetchedFiles) {
|
|
715
|
+
const issues = [];
|
|
716
|
+
|
|
717
|
+
// Check for essential components
|
|
718
|
+
if (!fetchedFiles['.claude']) {
|
|
719
|
+
issues.push({
|
|
720
|
+
type: 'missing_component',
|
|
721
|
+
component: '.claude',
|
|
722
|
+
message: 'Missing .claude directory structure'
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (!fetchedFiles['CLAUDE.md']) {
|
|
727
|
+
issues.push({
|
|
728
|
+
type: 'missing_component',
|
|
729
|
+
component: 'CLAUDE.md',
|
|
730
|
+
message: 'Missing CLAUDE.md file'
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Check .claude structure if present
|
|
735
|
+
if (fetchedFiles['.claude'] && fetchedFiles['.claude'].children) {
|
|
736
|
+
const claudeChildren = fetchedFiles['.claude'].children;
|
|
737
|
+
const expectedDirs = ['hooks', 'agents', 'commands'];
|
|
738
|
+
|
|
739
|
+
for (const expectedDir of expectedDirs) {
|
|
740
|
+
const hasDir = claudeChildren.some(child =>
|
|
741
|
+
child.path === expectedDir && child.type === 'dir'
|
|
742
|
+
);
|
|
743
|
+
if (!hasDir) {
|
|
744
|
+
issues.push({
|
|
745
|
+
type: 'missing_directory',
|
|
746
|
+
directory: expectedDir,
|
|
747
|
+
message: `Missing ${expectedDir} directory in .claude structure`
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return {
|
|
754
|
+
complete: issues.length === 0,
|
|
755
|
+
issues
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ============================================================================
|
|
760
|
+
// MOCK IMPLEMENTATIONS - To be replaced by real modules from WP03/04
|
|
761
|
+
// ============================================================================
|
|
762
|
+
|
|
763
|
+
_createMockFetcher() {
|
|
764
|
+
return {
|
|
765
|
+
async fetchDirectory(path, _options) {
|
|
766
|
+
return {
|
|
767
|
+
path,
|
|
768
|
+
type: 'dir',
|
|
769
|
+
children: [
|
|
770
|
+
{
|
|
771
|
+
path: 'settings.json',
|
|
772
|
+
type: 'file',
|
|
773
|
+
content: '{"version": "1.0.0"}',
|
|
774
|
+
encoding: 'utf-8'
|
|
775
|
+
}
|
|
776
|
+
]
|
|
777
|
+
};
|
|
778
|
+
},
|
|
779
|
+
|
|
780
|
+
async fetchFile(path, _options) {
|
|
781
|
+
return {
|
|
782
|
+
path,
|
|
783
|
+
content: `# Claude Code Setup\n\nInstalled from ${this.options?.version || 'main'}`,
|
|
784
|
+
encoding: 'utf-8'
|
|
785
|
+
};
|
|
786
|
+
},
|
|
787
|
+
|
|
788
|
+
async validateConnection() {
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
_createMockWriter() {
|
|
795
|
+
return {
|
|
796
|
+
async writeFile(path, content, options = {}) {
|
|
797
|
+
if (options.dryRun) return;
|
|
798
|
+
// Mock implementation - real writer will handle actual file operations
|
|
799
|
+
},
|
|
800
|
+
|
|
801
|
+
async writeDirectory(files, options = {}) {
|
|
802
|
+
return {
|
|
803
|
+
written: files.map(f => path.join(options.targetDir || '', f.path)),
|
|
804
|
+
skipped: [],
|
|
805
|
+
backed_up: new Map(),
|
|
806
|
+
errors: [],
|
|
807
|
+
directoriesCreated: files.map(f => path.dirname(path.join(options.targetDir || '', f.path))).filter((v, i, a) => a.indexOf(v) === i)
|
|
808
|
+
};
|
|
809
|
+
},
|
|
810
|
+
|
|
811
|
+
async validateInstallationCompleteness(_targetDir, _expectedFiles = []) {
|
|
812
|
+
return {
|
|
813
|
+
complete: true,
|
|
814
|
+
issues: []
|
|
815
|
+
};
|
|
816
|
+
},
|
|
817
|
+
|
|
818
|
+
async backup(path) {
|
|
819
|
+
return `${path}.backup-${Date.now()}`;
|
|
820
|
+
},
|
|
821
|
+
|
|
822
|
+
async rollback(_backupPath, _originalPath) {
|
|
823
|
+
// Mock implementation
|
|
824
|
+
},
|
|
825
|
+
|
|
826
|
+
async removeFile(_path) {
|
|
827
|
+
// Mock implementation
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
_createMockLogger() {
|
|
833
|
+
return {
|
|
834
|
+
info: (msg, ...args) => console.log(`[INFO] ${msg}`, ...args),
|
|
835
|
+
warn: (msg, ...args) => console.warn(`[WARN] ${msg}`, ...args),
|
|
836
|
+
error: (msg, ...args) => console.error(`[ERROR] ${msg}`, ...args),
|
|
837
|
+
debug: (msg, ...args) => console.log(`[DEBUG] ${msg}`, ...args)
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
_createMockValidator() {
|
|
842
|
+
return {
|
|
843
|
+
async validateTargetDirectory(_targetDir) {
|
|
844
|
+
// Mock validation - always pass
|
|
845
|
+
return null;
|
|
846
|
+
},
|
|
847
|
+
|
|
848
|
+
async validatePythonEnvironment() {
|
|
849
|
+
// Mock validation - always pass
|
|
850
|
+
return null;
|
|
851
|
+
},
|
|
852
|
+
|
|
853
|
+
async checkExistingFiles(_targetDir) {
|
|
854
|
+
// Mock validation - no existing files
|
|
855
|
+
return null;
|
|
856
|
+
},
|
|
857
|
+
|
|
858
|
+
async validateNetworkAccess() {
|
|
859
|
+
// Mock validation - always pass
|
|
860
|
+
return null;
|
|
861
|
+
},
|
|
862
|
+
|
|
863
|
+
async pathExists(_path) {
|
|
864
|
+
// Mock - assume path exists for verification
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
module.exports = {
|
|
872
|
+
Installer,
|
|
873
|
+
InstallationState,
|
|
874
|
+
ErrorCode
|
|
875
|
+
};
|