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,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File writer module for installing Claude setup files
|
|
3
|
+
* Handles file writing, conflict resolution, backup, and rollback
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs').promises;
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { EventEmitter } = require('events');
|
|
9
|
+
const {
|
|
10
|
+
ValidationError,
|
|
11
|
+
ErrorFactory
|
|
12
|
+
} = require('../utils/errors');
|
|
13
|
+
const platformUtils = require('../utils/platform');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* File Writer class implementing WriterAPI interface
|
|
17
|
+
*/
|
|
18
|
+
class FileWriter extends EventEmitter {
|
|
19
|
+
constructor(options = {}) {
|
|
20
|
+
super();
|
|
21
|
+
|
|
22
|
+
this.options = {
|
|
23
|
+
backupDir: options.backupDir || null, // null = same directory as original
|
|
24
|
+
maxRetries: options.maxRetries || 3,
|
|
25
|
+
retryDelay: options.retryDelay || 1000,
|
|
26
|
+
batchSize: options.batchSize || 50,
|
|
27
|
+
streaming: options.streaming || false,
|
|
28
|
+
progressReporter: options.progressReporter || null,
|
|
29
|
+
platformUtils: options.platformUtils || platformUtils,
|
|
30
|
+
...options
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
this.rollbackStack = [];
|
|
34
|
+
this.activeOperations = new Map();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Write a single file with options for backup, permissions, etc.
|
|
39
|
+
* Implements WriterAPI.writeFile interface
|
|
40
|
+
*/
|
|
41
|
+
async writeFile(filePath, content, options = {}) {
|
|
42
|
+
const opts = { ...this.options, ...options };
|
|
43
|
+
|
|
44
|
+
// Validate inputs
|
|
45
|
+
this.validatePath(filePath, opts.targetDir);
|
|
46
|
+
|
|
47
|
+
if (opts.dryRun) {
|
|
48
|
+
return { path: filePath, dryRun: true, size: Buffer.byteLength(content) };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Emit progress start
|
|
52
|
+
if (opts.progressReporter) {
|
|
53
|
+
opts.progressReporter.start('Writing file', 1);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const absolutePath = this.resolvePath(filePath, opts.targetDir);
|
|
58
|
+
const dirPath = path.dirname(absolutePath);
|
|
59
|
+
|
|
60
|
+
// Ensure directory exists
|
|
61
|
+
await this.ensureDirectory(dirPath);
|
|
62
|
+
|
|
63
|
+
// Check if file exists and handle backup
|
|
64
|
+
let backupPath = null;
|
|
65
|
+
const fileExists = await this.fileExists(absolutePath);
|
|
66
|
+
|
|
67
|
+
if (fileExists && opts.backup) {
|
|
68
|
+
backupPath = await this.backup(absolutePath);
|
|
69
|
+
this.rollbackStack.push({ type: 'restore', backupPath, originalPath: absolutePath });
|
|
70
|
+
} else if (fileExists && !opts.overwrite) {
|
|
71
|
+
if (opts.progressReporter) {
|
|
72
|
+
opts.progressReporter.complete('File skipped (already exists)');
|
|
73
|
+
}
|
|
74
|
+
return { path: filePath, skipped: true, reason: 'exists' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Write file atomically
|
|
78
|
+
await this.writeFileAtomic(absolutePath, content, opts);
|
|
79
|
+
|
|
80
|
+
// Track for potential rollback
|
|
81
|
+
if (!fileExists) {
|
|
82
|
+
this.rollbackStack.push({ type: 'delete', path: absolutePath });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (opts.progressReporter) {
|
|
86
|
+
opts.progressReporter.update(1, `Wrote ${path.basename(absolutePath)}`);
|
|
87
|
+
opts.progressReporter.complete('File written successfully');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
path: filePath,
|
|
92
|
+
absolutePath,
|
|
93
|
+
size: Buffer.byteLength(content),
|
|
94
|
+
backup: backupPath
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (opts.progressReporter) {
|
|
99
|
+
opts.progressReporter.fail(error);
|
|
100
|
+
}
|
|
101
|
+
throw ErrorFactory.fromCaught(error, `writing file ${filePath}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Write multiple files to a directory
|
|
107
|
+
* Implements WriterAPI.writeDirectory interface
|
|
108
|
+
*/
|
|
109
|
+
async writeDirectory(files, options = {}) {
|
|
110
|
+
const opts = { ...this.options, ...options };
|
|
111
|
+
const targetDir = opts.targetDir;
|
|
112
|
+
|
|
113
|
+
if (!targetDir) {
|
|
114
|
+
throw new ValidationError('targetDir is required for writeDirectory', 'targetDir');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Validate files array
|
|
118
|
+
if (!Array.isArray(files)) {
|
|
119
|
+
throw new ValidationError('files must be an array', 'files');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = {
|
|
123
|
+
written: [],
|
|
124
|
+
skipped: [],
|
|
125
|
+
backed_up: new Map(),
|
|
126
|
+
errors: [],
|
|
127
|
+
directoriesCreated: []
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Emit progress start
|
|
131
|
+
if (opts.progressReporter) {
|
|
132
|
+
opts.progressReporter.start('Writing files', files.length);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Pre-flight validation
|
|
136
|
+
if (opts.atomic) {
|
|
137
|
+
this.createCheckpoint();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Process files in batches to manage memory
|
|
142
|
+
const batchSize = opts.batchSize || 50;
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < files.length; i += batchSize) {
|
|
145
|
+
const batch = files.slice(i, i + batchSize);
|
|
146
|
+
|
|
147
|
+
if (opts.atomic) {
|
|
148
|
+
// For atomic operations, process sequentially
|
|
149
|
+
for (const file of batch) {
|
|
150
|
+
await this.processFileForDirectory(file, targetDir, opts, result, i + batch.indexOf(file) + 1);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
// For non-atomic, can process in parallel
|
|
154
|
+
await Promise.all(
|
|
155
|
+
batch.map((file, idx) =>
|
|
156
|
+
this.processFileForDirectory(file, targetDir, opts, result, i + idx + 1)
|
|
157
|
+
.catch(error => {
|
|
158
|
+
result.errors.push({ path: file.path, error });
|
|
159
|
+
})
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (opts.progressReporter) {
|
|
166
|
+
opts.progressReporter.complete(`Wrote ${result.written.length} files successfully`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Log installation summary for debugging
|
|
170
|
+
this.emit('installationSummary', {
|
|
171
|
+
filesWritten: result.written.length,
|
|
172
|
+
filesSkipped: result.skipped.length,
|
|
173
|
+
directoriesCreated: result.directoriesCreated.length,
|
|
174
|
+
errors: result.errors.length
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
return result;
|
|
178
|
+
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (opts.atomic) {
|
|
181
|
+
// Rollback all changes
|
|
182
|
+
await this.rollbackToCheckpoint();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (opts.progressReporter) {
|
|
186
|
+
opts.progressReporter.fail(error);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
throw ErrorFactory.fromCaught(error, 'writing directory');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create a backup of a file or directory
|
|
195
|
+
* Implements WriterAPI.backup interface
|
|
196
|
+
*/
|
|
197
|
+
async backup(sourcePath, options = {}) {
|
|
198
|
+
const opts = { ...this.options, ...options };
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const absoluteSource = this.resolvePath(sourcePath);
|
|
202
|
+
|
|
203
|
+
// Check if source exists
|
|
204
|
+
const stats = await fs.stat(absoluteSource);
|
|
205
|
+
|
|
206
|
+
// Generate backup path with timestamp
|
|
207
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
208
|
+
const sourceName = path.basename(absoluteSource);
|
|
209
|
+
const backupName = `${sourceName}.backup.${timestamp}`;
|
|
210
|
+
|
|
211
|
+
const backupDir = opts.backupDir || path.dirname(absoluteSource);
|
|
212
|
+
const backupPath = path.join(backupDir, backupName);
|
|
213
|
+
|
|
214
|
+
// Ensure backup directory exists
|
|
215
|
+
await this.ensureDirectory(path.dirname(backupPath));
|
|
216
|
+
|
|
217
|
+
if (stats.isDirectory()) {
|
|
218
|
+
// Recursive directory copy
|
|
219
|
+
await this.copyDirectory(absoluteSource, backupPath);
|
|
220
|
+
} else {
|
|
221
|
+
// File copy with permission preservation
|
|
222
|
+
await fs.copyFile(absoluteSource, backupPath);
|
|
223
|
+
await fs.chmod(backupPath, stats.mode);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return backupPath;
|
|
227
|
+
|
|
228
|
+
} catch (error) {
|
|
229
|
+
throw ErrorFactory.fromCaught(error, `backing up ${sourcePath}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Restore from backup
|
|
235
|
+
* Implements WriterAPI.rollback interface
|
|
236
|
+
*/
|
|
237
|
+
async rollback(backupPath, _options = {}) {
|
|
238
|
+
try {
|
|
239
|
+
const absoluteBackup = this.resolvePath(backupPath);
|
|
240
|
+
|
|
241
|
+
// Extract original path from backup name
|
|
242
|
+
const backupName = path.basename(absoluteBackup);
|
|
243
|
+
const match = backupName.match(/(.+)\.backup\.([\d-T]+.*?)$/);
|
|
244
|
+
|
|
245
|
+
if (!match) {
|
|
246
|
+
throw new ValidationError('Invalid backup file name format', 'backupPath');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const originalName = match[1];
|
|
250
|
+
const originalPath = path.join(path.dirname(absoluteBackup), originalName);
|
|
251
|
+
|
|
252
|
+
const backupStats = await fs.stat(absoluteBackup);
|
|
253
|
+
|
|
254
|
+
if (backupStats.isDirectory()) {
|
|
255
|
+
// Remove current directory and restore backup
|
|
256
|
+
try {
|
|
257
|
+
await fs.rm(originalPath, { recursive: true, force: true });
|
|
258
|
+
} catch (error) {
|
|
259
|
+
// Directory might not exist, which is OK
|
|
260
|
+
}
|
|
261
|
+
await this.copyDirectory(absoluteBackup, originalPath);
|
|
262
|
+
} else {
|
|
263
|
+
// Restore file
|
|
264
|
+
await fs.copyFile(absoluteBackup, originalPath);
|
|
265
|
+
await fs.chmod(originalPath, backupStats.mode);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Clean up backup file
|
|
269
|
+
await fs.rm(absoluteBackup, { recursive: true, force: true });
|
|
270
|
+
|
|
271
|
+
return originalPath;
|
|
272
|
+
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw ErrorFactory.fromCaught(error, `rolling back from ${backupPath}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Validate target directory and permissions
|
|
280
|
+
*/
|
|
281
|
+
async validateTarget(targetDir) {
|
|
282
|
+
try {
|
|
283
|
+
const absoluteTarget = this.resolvePath(targetDir);
|
|
284
|
+
|
|
285
|
+
// Check if target exists
|
|
286
|
+
const exists = await this.fileExists(absoluteTarget);
|
|
287
|
+
|
|
288
|
+
if (exists) {
|
|
289
|
+
const stats = await fs.stat(absoluteTarget);
|
|
290
|
+
if (!stats.isDirectory()) {
|
|
291
|
+
return false; // Target exists but is not a directory
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Test write permissions
|
|
296
|
+
return await this.options.platformUtils.isWritable(absoluteTarget);
|
|
297
|
+
|
|
298
|
+
} catch (error) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Validate that installation was complete by checking expected structure
|
|
305
|
+
*/
|
|
306
|
+
async validateInstallationCompleteness(targetDir, expectedFiles = []) {
|
|
307
|
+
const issues = [];
|
|
308
|
+
|
|
309
|
+
// Check for essential .claude structure
|
|
310
|
+
const essentialPaths = [
|
|
311
|
+
'.claude',
|
|
312
|
+
'.claude/settings.json',
|
|
313
|
+
'.claude/hooks',
|
|
314
|
+
'.claude/hooks/comms',
|
|
315
|
+
'.claude/agents',
|
|
316
|
+
'.claude/agents/core'
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
for (const relativePath of essentialPaths) {
|
|
320
|
+
const fullPath = path.join(targetDir, relativePath);
|
|
321
|
+
const exists = await this.fileExists(fullPath);
|
|
322
|
+
if (!exists) {
|
|
323
|
+
issues.push({
|
|
324
|
+
type: 'missing_essential',
|
|
325
|
+
path: relativePath,
|
|
326
|
+
message: `Essential path missing: ${relativePath}`
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Check expected files if provided
|
|
332
|
+
for (const expectedFile of expectedFiles) {
|
|
333
|
+
const fullPath = path.join(targetDir, expectedFile);
|
|
334
|
+
const exists = await this.fileExists(fullPath);
|
|
335
|
+
if (!exists) {
|
|
336
|
+
issues.push({
|
|
337
|
+
type: 'missing_expected',
|
|
338
|
+
path: expectedFile,
|
|
339
|
+
message: `Expected file missing: ${expectedFile}`
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
complete: issues.length === 0,
|
|
346
|
+
issues
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Check available disk space
|
|
352
|
+
*/
|
|
353
|
+
async checkDiskSpace(targetDir, requiredBytes) {
|
|
354
|
+
try {
|
|
355
|
+
const spaceInfo = await this.options.platformUtils.getDiskSpace(targetDir);
|
|
356
|
+
if (!spaceInfo) {
|
|
357
|
+
return true; // Can't determine, assume OK
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return spaceInfo.free >= requiredBytes;
|
|
361
|
+
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return true; // Can't determine, assume OK
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Validate file path for security
|
|
369
|
+
*/
|
|
370
|
+
validatePath(filePath, basePath) {
|
|
371
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
372
|
+
throw new ValidationError('File path must be a non-empty string', 'filePath');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (basePath && !this.options.platformUtils.isSafePath(filePath, basePath)) {
|
|
376
|
+
throw new ValidationError('File path escapes target directory', 'filePath');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Check for dangerous patterns
|
|
380
|
+
const dangerousPatterns = [
|
|
381
|
+
/\.\.[\\/]/, // Directory traversal
|
|
382
|
+
/^[\\/]/, // Absolute paths when basePath is provided
|
|
383
|
+
/[<>:"|*?]/ // Invalid filename characters on Windows
|
|
384
|
+
];
|
|
385
|
+
|
|
386
|
+
if (basePath) {
|
|
387
|
+
for (const pattern of dangerousPatterns) {
|
|
388
|
+
if (pattern.test(filePath)) {
|
|
389
|
+
throw new ValidationError('File path contains invalid characters', 'filePath');
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// PRIVATE HELPER METHODS
|
|
396
|
+
|
|
397
|
+
async processFileForDirectory(file, targetDir, opts, result, currentIndex) {
|
|
398
|
+
try {
|
|
399
|
+
const fileOptions = {
|
|
400
|
+
...opts,
|
|
401
|
+
targetDir,
|
|
402
|
+
permissions: file.permissions,
|
|
403
|
+
encoding: file.encoding
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
// Track directory creation for reporting
|
|
407
|
+
const dirPath = path.dirname(path.join(targetDir, file.path));
|
|
408
|
+
if (!result.directoriesCreated.includes(dirPath)) {
|
|
409
|
+
result.directoriesCreated.push(dirPath);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const writeResult = await this.writeFile(file.path, file.content, fileOptions);
|
|
413
|
+
|
|
414
|
+
if (writeResult.skipped) {
|
|
415
|
+
result.skipped.push(file.path);
|
|
416
|
+
} else {
|
|
417
|
+
result.written.push(file.path);
|
|
418
|
+
if (writeResult.backup) {
|
|
419
|
+
result.backed_up.set(writeResult.absolutePath, writeResult.backup);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (opts.progressReporter) {
|
|
424
|
+
opts.progressReporter.update(currentIndex, `Processed ${file.path}`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
} catch (error) {
|
|
428
|
+
result.errors.push({ path: file.path, error });
|
|
429
|
+
|
|
430
|
+
if (opts.atomic) {
|
|
431
|
+
throw error; // Stop processing in atomic mode
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async writeFileAtomic(filePath, content, options) {
|
|
437
|
+
const tempPath = `${filePath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2)}`;
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
// Write to temporary file first
|
|
441
|
+
if (options.encoding === 'binary' || Buffer.isBuffer(content)) {
|
|
442
|
+
await fs.writeFile(tempPath, content);
|
|
443
|
+
} else {
|
|
444
|
+
const processedContent = options.normalizeLineEndings
|
|
445
|
+
? this.options.platformUtils.convertFileContent(content)
|
|
446
|
+
: content;
|
|
447
|
+
await fs.writeFile(tempPath, processedContent, 'utf8');
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Set permissions if specified
|
|
451
|
+
if (options.permissions) {
|
|
452
|
+
await fs.chmod(tempPath, options.permissions);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Atomic rename
|
|
456
|
+
await fs.rename(tempPath, filePath);
|
|
457
|
+
|
|
458
|
+
} catch (error) {
|
|
459
|
+
// Clean up temp file on failure
|
|
460
|
+
try {
|
|
461
|
+
await fs.unlink(tempPath);
|
|
462
|
+
} catch (cleanupError) {
|
|
463
|
+
// Ignore cleanup errors
|
|
464
|
+
}
|
|
465
|
+
throw error;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async ensureDirectory(dirPath) {
|
|
470
|
+
try {
|
|
471
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
472
|
+
return dirPath;
|
|
473
|
+
} catch (error) {
|
|
474
|
+
if (error.code !== 'EEXIST') {
|
|
475
|
+
throw error;
|
|
476
|
+
}
|
|
477
|
+
return dirPath;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async fileExists(filePath) {
|
|
482
|
+
try {
|
|
483
|
+
await fs.access(filePath);
|
|
484
|
+
return true;
|
|
485
|
+
} catch {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
resolvePath(filePath, basePath = null) {
|
|
491
|
+
if (basePath) {
|
|
492
|
+
return this.options.platformUtils.getAbsolutePath(filePath, basePath);
|
|
493
|
+
}
|
|
494
|
+
return this.options.platformUtils.getAbsolutePath(filePath);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async copyDirectory(src, dest) {
|
|
498
|
+
await this.ensureDirectory(dest);
|
|
499
|
+
const entries = await fs.readdir(src, { withFileTypes: true });
|
|
500
|
+
|
|
501
|
+
for (const entry of entries) {
|
|
502
|
+
const srcPath = path.join(src, entry.name);
|
|
503
|
+
const destPath = path.join(dest, entry.name);
|
|
504
|
+
|
|
505
|
+
if (entry.isDirectory()) {
|
|
506
|
+
await this.copyDirectory(srcPath, destPath);
|
|
507
|
+
} else {
|
|
508
|
+
await fs.copyFile(srcPath, destPath);
|
|
509
|
+
const stats = await fs.stat(srcPath);
|
|
510
|
+
await fs.chmod(destPath, stats.mode);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
createCheckpoint() {
|
|
516
|
+
this.rollbackStack = [];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
async rollbackToCheckpoint() {
|
|
520
|
+
// Rollback in reverse order
|
|
521
|
+
for (let i = this.rollbackStack.length - 1; i >= 0; i--) {
|
|
522
|
+
const operation = this.rollbackStack[i];
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
if (operation.type === 'delete') {
|
|
526
|
+
await fs.unlink(operation.path);
|
|
527
|
+
} else if (operation.type === 'restore') {
|
|
528
|
+
await this.rollback(operation.backupPath);
|
|
529
|
+
}
|
|
530
|
+
} catch (error) {
|
|
531
|
+
// Log but continue rollback
|
|
532
|
+
console.warn(`Rollback operation failed: ${error.message}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
this.rollbackStack = [];
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
async createRollbackPoint(targetDir) {
|
|
540
|
+
const timestamp = Date.now();
|
|
541
|
+
const rollbackId = `rollback-${timestamp}`;
|
|
542
|
+
|
|
543
|
+
// Create snapshot of current state
|
|
544
|
+
const snapshot = {
|
|
545
|
+
id: rollbackId,
|
|
546
|
+
timestamp,
|
|
547
|
+
targetDir,
|
|
548
|
+
files: await this.snapshotDirectory(targetDir)
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
return snapshot;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
async rollbackToPoint(rollbackInfo) {
|
|
555
|
+
// Restore directory to snapshot state
|
|
556
|
+
await this.restoreFromSnapshot(rollbackInfo);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async snapshotDirectory(dirPath) {
|
|
560
|
+
const snapshot = new Map();
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
564
|
+
|
|
565
|
+
for (const entry of entries) {
|
|
566
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
567
|
+
|
|
568
|
+
if (entry.isFile()) {
|
|
569
|
+
const content = await fs.readFile(fullPath);
|
|
570
|
+
const stats = await fs.stat(fullPath);
|
|
571
|
+
snapshot.set(fullPath, { content, mode: stats.mode, type: 'file' });
|
|
572
|
+
} else if (entry.isDirectory()) {
|
|
573
|
+
snapshot.set(fullPath, { type: 'directory' });
|
|
574
|
+
const subSnapshot = await this.snapshotDirectory(fullPath);
|
|
575
|
+
for (const [subPath, subData] of subSnapshot) {
|
|
576
|
+
snapshot.set(subPath, subData);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
} catch (error) {
|
|
581
|
+
if (error.code !== 'ENOENT') {
|
|
582
|
+
throw error;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return snapshot;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async restoreFromSnapshot(rollbackInfo) {
|
|
590
|
+
const { targetDir, files } = rollbackInfo;
|
|
591
|
+
|
|
592
|
+
// Remove all current files
|
|
593
|
+
try {
|
|
594
|
+
await fs.rm(targetDir, { recursive: true, force: true });
|
|
595
|
+
} catch (error) {
|
|
596
|
+
// Directory might not exist
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Restore from snapshot
|
|
600
|
+
for (const [filePath, fileData] of files) {
|
|
601
|
+
if (fileData.type === 'directory') {
|
|
602
|
+
await this.ensureDirectory(filePath);
|
|
603
|
+
} else if (fileData.type === 'file') {
|
|
604
|
+
await this.ensureDirectory(path.dirname(filePath));
|
|
605
|
+
await fs.writeFile(filePath, fileData.content);
|
|
606
|
+
await fs.chmod(filePath, fileData.mode);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// Legacy function exports for backward compatibility
|
|
613
|
+
async function writeSetupFiles(setupFiles, targetDir, options = {}) {
|
|
614
|
+
const writer = new FileWriter(options);
|
|
615
|
+
const files = setupFiles.map(file => ({
|
|
616
|
+
path: file.path || file.name,
|
|
617
|
+
content: file.content,
|
|
618
|
+
encoding: file.encoding,
|
|
619
|
+
permissions: file.permissions
|
|
620
|
+
}));
|
|
621
|
+
|
|
622
|
+
return writer.writeDirectory(files, { targetDir, ...options });
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async function backupExistingFiles(targetDir, files) {
|
|
626
|
+
const writer = new FileWriter();
|
|
627
|
+
const backups = new Map();
|
|
628
|
+
|
|
629
|
+
for (const file of files) {
|
|
630
|
+
const filePath = path.join(targetDir, file.path || file.name);
|
|
631
|
+
try {
|
|
632
|
+
if (await writer.fileExists(filePath)) {
|
|
633
|
+
const backupPath = await writer.backup(filePath);
|
|
634
|
+
backups.set(filePath, backupPath);
|
|
635
|
+
}
|
|
636
|
+
} catch (error) {
|
|
637
|
+
// Continue with other files
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return backups;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
module.exports = {
|
|
645
|
+
FileWriter,
|
|
646
|
+
writeSetupFiles,
|
|
647
|
+
backupExistingFiles
|
|
648
|
+
};
|