teleportation-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/config-loader.mjs +93 -0
- package/.claude/hooks/heartbeat.mjs +331 -0
- package/.claude/hooks/notification.mjs +35 -0
- package/.claude/hooks/permission_request.mjs +307 -0
- package/.claude/hooks/post_tool_use.mjs +137 -0
- package/.claude/hooks/pre_tool_use.mjs +451 -0
- package/.claude/hooks/session-register.mjs +274 -0
- package/.claude/hooks/session_end.mjs +256 -0
- package/.claude/hooks/session_start.mjs +308 -0
- package/.claude/hooks/stop.mjs +277 -0
- package/.claude/hooks/user_prompt_submit.mjs +91 -0
- package/LICENSE +21 -0
- package/README.md +243 -0
- package/lib/auth/api-key.js +110 -0
- package/lib/auth/credentials.js +341 -0
- package/lib/backup/manager.js +461 -0
- package/lib/cli/daemon-commands.js +299 -0
- package/lib/cli/index.js +303 -0
- package/lib/cli/session-commands.js +294 -0
- package/lib/cli/snapshot-commands.js +223 -0
- package/lib/cli/worktree-commands.js +291 -0
- package/lib/config/manager.js +306 -0
- package/lib/daemon/lifecycle.js +336 -0
- package/lib/daemon/pid-manager.js +160 -0
- package/lib/daemon/teleportation-daemon.js +2009 -0
- package/lib/handoff/config.js +102 -0
- package/lib/handoff/example.js +152 -0
- package/lib/handoff/git-handoff.js +351 -0
- package/lib/handoff/handoff.js +277 -0
- package/lib/handoff/index.js +25 -0
- package/lib/handoff/session-state.js +238 -0
- package/lib/install/installer.js +555 -0
- package/lib/machine-coders/claude-code-adapter.js +329 -0
- package/lib/machine-coders/example.js +239 -0
- package/lib/machine-coders/gemini-cli-adapter.js +406 -0
- package/lib/machine-coders/index.js +103 -0
- package/lib/machine-coders/interface.js +168 -0
- package/lib/router/classifier.js +251 -0
- package/lib/router/example.js +92 -0
- package/lib/router/index.js +69 -0
- package/lib/router/mech-llms-client.js +277 -0
- package/lib/router/models.js +188 -0
- package/lib/router/router.js +382 -0
- package/lib/session/cleanup.js +100 -0
- package/lib/session/metadata.js +258 -0
- package/lib/session/mute-checker.js +114 -0
- package/lib/session-registry/manager.js +302 -0
- package/lib/snapshot/manager.js +390 -0
- package/lib/utils/errors.js +166 -0
- package/lib/utils/logger.js +148 -0
- package/lib/utils/retry.js +155 -0
- package/lib/worktree/manager.js +301 -0
- package/package.json +66 -0
- package/teleportation-cli.cjs +2987 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup Manager for Teleportation
|
|
3
|
+
*
|
|
4
|
+
* Handles backup and rollback of configuration files before setup/changes.
|
|
5
|
+
* Ensures users can safely restore previous state if something goes wrong.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import crypto from 'crypto';
|
|
11
|
+
import os from 'os';
|
|
12
|
+
|
|
13
|
+
const HOME_DIR = os.homedir();
|
|
14
|
+
const BACKUP_DIR = path.join(HOME_DIR, '.teleportation', 'backups');
|
|
15
|
+
const MAX_BACKUPS = 10;
|
|
16
|
+
const MIN_BACKUP_AGE_DAYS = 7;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Files and directories that may be modified during setup
|
|
20
|
+
*/
|
|
21
|
+
const BACKUP_TARGETS = [
|
|
22
|
+
{
|
|
23
|
+
name: 'claude-settings',
|
|
24
|
+
path: path.join(HOME_DIR, '.claude', 'settings.json'),
|
|
25
|
+
type: 'file',
|
|
26
|
+
description: 'Claude Code settings'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'claude-hooks',
|
|
30
|
+
path: path.join(HOME_DIR, '.claude', 'hooks'),
|
|
31
|
+
type: 'directory',
|
|
32
|
+
description: 'Claude Code hooks'
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'teleportation-config',
|
|
36
|
+
path: path.join(HOME_DIR, '.teleportation', 'config.json'),
|
|
37
|
+
type: 'file',
|
|
38
|
+
description: 'Teleportation configuration'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'teleportation-credentials',
|
|
42
|
+
path: path.join(HOME_DIR, '.teleportation', 'credentials'),
|
|
43
|
+
type: 'file',
|
|
44
|
+
description: 'Teleportation credentials'
|
|
45
|
+
}
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
export class BackupManager {
|
|
49
|
+
constructor() {
|
|
50
|
+
this.backupDir = BACKUP_DIR;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Create a backup of all relevant files before making changes
|
|
55
|
+
* @param {string} reason - Why this backup is being created (e.g., "teleportation setup")
|
|
56
|
+
* @returns {Promise<{backupPath: string, manifest: object}>}
|
|
57
|
+
*/
|
|
58
|
+
async createBackup(reason) {
|
|
59
|
+
// Add random suffix to prevent collisions if multiple backups in same millisecond
|
|
60
|
+
const randomSuffix = crypto.randomBytes(3).toString('hex');
|
|
61
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-') + '-' + randomSuffix;
|
|
62
|
+
const backupPath = path.join(this.backupDir, timestamp);
|
|
63
|
+
|
|
64
|
+
// Ensure backup directory exists
|
|
65
|
+
fs.mkdirSync(backupPath, { recursive: true });
|
|
66
|
+
|
|
67
|
+
const manifest = {
|
|
68
|
+
timestamp: new Date().toISOString(),
|
|
69
|
+
reason,
|
|
70
|
+
version: '1.0.0',
|
|
71
|
+
files: []
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Backup each target
|
|
75
|
+
for (const target of BACKUP_TARGETS) {
|
|
76
|
+
if (!fs.existsSync(target.path)) {
|
|
77
|
+
continue; // Skip if doesn't exist
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const backupName = target.type === 'directory'
|
|
81
|
+
? target.name
|
|
82
|
+
: `${target.name}.json`;
|
|
83
|
+
const backupTarget = path.join(backupPath, backupName);
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
if (target.type === 'directory') {
|
|
87
|
+
await this._copyDirectoryWithPermissions(target.path, backupTarget);
|
|
88
|
+
const files = fs.readdirSync(target.path);
|
|
89
|
+
manifest.files.push({
|
|
90
|
+
original: target.path,
|
|
91
|
+
backup: backupName,
|
|
92
|
+
type: 'directory',
|
|
93
|
+
existed: true,
|
|
94
|
+
fileCount: files.length,
|
|
95
|
+
files: files,
|
|
96
|
+
description: target.description
|
|
97
|
+
});
|
|
98
|
+
} else {
|
|
99
|
+
// Copy file and preserve permissions
|
|
100
|
+
fs.copyFileSync(target.path, backupTarget);
|
|
101
|
+
const srcStat = fs.statSync(target.path);
|
|
102
|
+
fs.chmodSync(backupTarget, srcStat.mode);
|
|
103
|
+
const hash = await this._hashFile(target.path);
|
|
104
|
+
manifest.files.push({
|
|
105
|
+
original: target.path,
|
|
106
|
+
backup: backupName,
|
|
107
|
+
type: 'file',
|
|
108
|
+
existed: true,
|
|
109
|
+
hash: target.name.includes('credentials') ? undefined : hash,
|
|
110
|
+
description: target.description
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error(`Warning: Failed to backup ${target.path}: ${err.message}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Save manifest
|
|
119
|
+
manifest.canRollback = manifest.files.length > 0;
|
|
120
|
+
fs.writeFileSync(
|
|
121
|
+
path.join(backupPath, 'manifest.json'),
|
|
122
|
+
JSON.stringify(manifest, null, 2)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Update 'latest' symlink
|
|
126
|
+
await this._updateLatestLink(timestamp);
|
|
127
|
+
|
|
128
|
+
// Cleanup old backups
|
|
129
|
+
await this._cleanupOldBackups();
|
|
130
|
+
|
|
131
|
+
return { backupPath, manifest };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Restore from a backup
|
|
136
|
+
* @param {string|null} backupId - Specific backup ID or null for latest
|
|
137
|
+
* @returns {Promise<{manifest: object, restoredFiles: string[]}>}
|
|
138
|
+
*/
|
|
139
|
+
async restore(backupId = null) {
|
|
140
|
+
let backupPath;
|
|
141
|
+
|
|
142
|
+
if (backupId) {
|
|
143
|
+
backupPath = path.join(this.backupDir, backupId);
|
|
144
|
+
} else {
|
|
145
|
+
// Use latest (with symlink fallback support)
|
|
146
|
+
backupPath = this._resolveLatest();
|
|
147
|
+
if (!backupPath) {
|
|
148
|
+
throw new Error('No backups found');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const manifestPath = path.join(backupPath, 'manifest.json');
|
|
153
|
+
if (!fs.existsSync(manifestPath)) {
|
|
154
|
+
throw new Error(`No backup manifest found at ${backupPath}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
158
|
+
const restoredFiles = [];
|
|
159
|
+
const failedFiles = [];
|
|
160
|
+
|
|
161
|
+
for (const file of manifest.files) {
|
|
162
|
+
const backupFile = path.join(backupPath, file.backup);
|
|
163
|
+
|
|
164
|
+
if (!fs.existsSync(backupFile)) {
|
|
165
|
+
console.error(`Warning: Backup file not found: ${backupFile}`);
|
|
166
|
+
failedFiles.push({ path: file.original, reason: 'Backup file not found' });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
if (file.type === 'directory') {
|
|
172
|
+
// Remove existing directory first
|
|
173
|
+
if (fs.existsSync(file.original)) {
|
|
174
|
+
fs.rmSync(file.original, { recursive: true, force: true });
|
|
175
|
+
}
|
|
176
|
+
await this._copyDirectoryWithPermissions(backupFile, file.original);
|
|
177
|
+
} else {
|
|
178
|
+
// Ensure parent directory exists
|
|
179
|
+
const parentDir = path.dirname(file.original);
|
|
180
|
+
if (!fs.existsSync(parentDir)) {
|
|
181
|
+
fs.mkdirSync(parentDir, { recursive: true });
|
|
182
|
+
}
|
|
183
|
+
// Copy file and preserve permissions
|
|
184
|
+
fs.copyFileSync(backupFile, file.original);
|
|
185
|
+
const backupStat = fs.statSync(backupFile);
|
|
186
|
+
fs.chmodSync(file.original, backupStat.mode);
|
|
187
|
+
}
|
|
188
|
+
restoredFiles.push(file.original);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
console.error(`Warning: Failed to restore ${file.original}: ${err.message}`);
|
|
191
|
+
failedFiles.push({ path: file.original, reason: err.message });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return { manifest, restoredFiles, failedFiles };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* List all available backups
|
|
200
|
+
* @returns {Array<{id: string, timestamp: string, reason: string, fileCount: number}>}
|
|
201
|
+
*/
|
|
202
|
+
listBackups() {
|
|
203
|
+
if (!fs.existsSync(this.backupDir)) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const backups = [];
|
|
208
|
+
const entries = fs.readdirSync(this.backupDir);
|
|
209
|
+
|
|
210
|
+
for (const entry of entries) {
|
|
211
|
+
if (entry === 'latest') continue;
|
|
212
|
+
|
|
213
|
+
const entryPath = path.join(this.backupDir, entry);
|
|
214
|
+
const stat = fs.statSync(entryPath);
|
|
215
|
+
|
|
216
|
+
if (!stat.isDirectory()) continue;
|
|
217
|
+
|
|
218
|
+
const manifestPath = path.join(entryPath, 'manifest.json');
|
|
219
|
+
if (!fs.existsSync(manifestPath)) continue;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
223
|
+
backups.push({
|
|
224
|
+
id: entry,
|
|
225
|
+
timestamp: manifest.timestamp,
|
|
226
|
+
reason: manifest.reason,
|
|
227
|
+
fileCount: manifest.files.length,
|
|
228
|
+
canRollback: manifest.canRollback
|
|
229
|
+
});
|
|
230
|
+
} catch (err) {
|
|
231
|
+
// Skip invalid manifests
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Sort by timestamp descending (newest first)
|
|
236
|
+
return backups.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get details of a specific backup
|
|
241
|
+
* @param {string} backupId
|
|
242
|
+
* @returns {object|null}
|
|
243
|
+
*/
|
|
244
|
+
getBackupDetails(backupId) {
|
|
245
|
+
const manifestPath = path.join(this.backupDir, backupId, 'manifest.json');
|
|
246
|
+
if (!fs.existsSync(manifestPath)) {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
return JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if there are any files that would be modified
|
|
254
|
+
* @returns {Array<{path: string, description: string, exists: boolean}>}
|
|
255
|
+
*/
|
|
256
|
+
checkExistingFiles() {
|
|
257
|
+
const existing = [];
|
|
258
|
+
|
|
259
|
+
for (const target of BACKUP_TARGETS) {
|
|
260
|
+
if (fs.existsSync(target.path)) {
|
|
261
|
+
const info = {
|
|
262
|
+
path: target.path,
|
|
263
|
+
description: target.description,
|
|
264
|
+
exists: true
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
if (target.type === 'directory') {
|
|
268
|
+
const files = fs.readdirSync(target.path);
|
|
269
|
+
info.fileCount = files.length;
|
|
270
|
+
info.files = files;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
existing.push(info);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return existing;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Delete a specific backup
|
|
282
|
+
* @param {string} backupId
|
|
283
|
+
*/
|
|
284
|
+
deleteBackup(backupId) {
|
|
285
|
+
const backupPath = path.join(this.backupDir, backupId);
|
|
286
|
+
if (fs.existsSync(backupPath)) {
|
|
287
|
+
fs.rmSync(backupPath, { recursive: true, force: true });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Update latest link if needed (handles both symlink and fallback file)
|
|
291
|
+
const latestLink = path.join(this.backupDir, 'latest');
|
|
292
|
+
if (fs.existsSync(latestLink)) {
|
|
293
|
+
try {
|
|
294
|
+
const stat = fs.lstatSync(latestLink);
|
|
295
|
+
let latestTarget;
|
|
296
|
+
|
|
297
|
+
if (stat.isSymbolicLink()) {
|
|
298
|
+
// It's a symlink - read target directly
|
|
299
|
+
latestTarget = fs.readlinkSync(latestLink);
|
|
300
|
+
} else {
|
|
301
|
+
// It's a fallback file (Windows compat) - read content as target ID
|
|
302
|
+
latestTarget = fs.readFileSync(latestLink, 'utf8').trim();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (latestTarget === backupId) {
|
|
306
|
+
fs.unlinkSync(latestLink);
|
|
307
|
+
// Point to next most recent
|
|
308
|
+
const backups = this.listBackups();
|
|
309
|
+
if (backups.length > 0) {
|
|
310
|
+
// Use _updateLatestLink to handle symlink/fallback consistently
|
|
311
|
+
this._updateLatestLink(backups[0].id);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} catch (err) {
|
|
315
|
+
// If we can't read the latest link, just remove it
|
|
316
|
+
try {
|
|
317
|
+
fs.unlinkSync(latestLink);
|
|
318
|
+
} catch {
|
|
319
|
+
// Ignore errors removing latest link
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Clean up old backups beyond retention limit
|
|
327
|
+
*/
|
|
328
|
+
async _cleanupOldBackups() {
|
|
329
|
+
const backups = this.listBackups();
|
|
330
|
+
|
|
331
|
+
if (backups.length <= MAX_BACKUPS) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const now = Date.now();
|
|
336
|
+
const minAgeMs = MIN_BACKUP_AGE_DAYS * 24 * 60 * 60 * 1000;
|
|
337
|
+
|
|
338
|
+
// Keep MAX_BACKUPS, but never delete backups less than MIN_BACKUP_AGE_DAYS old
|
|
339
|
+
const toDelete = backups.slice(MAX_BACKUPS).filter(backup => {
|
|
340
|
+
const backupTime = new Date(backup.timestamp).getTime();
|
|
341
|
+
return (now - backupTime) > minAgeMs;
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
for (const backup of toDelete) {
|
|
345
|
+
this.deleteBackup(backup.id);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Update the 'latest' symlink (with fallback to regular file for Windows compatibility)
|
|
351
|
+
*/
|
|
352
|
+
async _updateLatestLink(targetId) {
|
|
353
|
+
const latestLink = path.join(this.backupDir, 'latest');
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
// Remove existing link/file
|
|
357
|
+
if (fs.existsSync(latestLink)) {
|
|
358
|
+
fs.unlinkSync(latestLink);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Try to create symlink first
|
|
362
|
+
try {
|
|
363
|
+
fs.symlinkSync(targetId, latestLink);
|
|
364
|
+
} catch (symlinkErr) {
|
|
365
|
+
// Fallback: create a regular file containing the target ID (Windows compatibility)
|
|
366
|
+
fs.writeFileSync(latestLink, targetId, 'utf8');
|
|
367
|
+
}
|
|
368
|
+
} catch (err) {
|
|
369
|
+
// Symlinks might fail on some systems, that's ok
|
|
370
|
+
console.error(`Warning: Could not create latest symlink: ${err.message}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Resolve 'latest' to actual backup path (supports both symlink and fallback file)
|
|
376
|
+
*/
|
|
377
|
+
_resolveLatest() {
|
|
378
|
+
const latestLink = path.join(this.backupDir, 'latest');
|
|
379
|
+
|
|
380
|
+
if (!fs.existsSync(latestLink)) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
// Try symlink first
|
|
386
|
+
const stat = fs.lstatSync(latestLink);
|
|
387
|
+
if (stat.isSymbolicLink()) {
|
|
388
|
+
return fs.realpathSync(latestLink);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Fallback: read as file containing target ID
|
|
392
|
+
const targetId = fs.readFileSync(latestLink, 'utf8').trim();
|
|
393
|
+
const targetPath = path.join(this.backupDir, targetId);
|
|
394
|
+
if (fs.existsSync(targetPath)) {
|
|
395
|
+
return targetPath;
|
|
396
|
+
}
|
|
397
|
+
} catch (err) {
|
|
398
|
+
// Try direct realpath as last resort
|
|
399
|
+
try {
|
|
400
|
+
return fs.realpathSync(latestLink);
|
|
401
|
+
} catch {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Copy a directory recursively (basic, without permission preservation)
|
|
411
|
+
*/
|
|
412
|
+
async _copyDirectory(src, dest) {
|
|
413
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
414
|
+
|
|
415
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
416
|
+
|
|
417
|
+
for (const entry of entries) {
|
|
418
|
+
const srcPath = path.join(src, entry.name);
|
|
419
|
+
const destPath = path.join(dest, entry.name);
|
|
420
|
+
|
|
421
|
+
if (entry.isDirectory()) {
|
|
422
|
+
await this._copyDirectory(srcPath, destPath);
|
|
423
|
+
} else {
|
|
424
|
+
fs.copyFileSync(srcPath, destPath);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Copy a directory recursively with permission preservation
|
|
431
|
+
*/
|
|
432
|
+
async _copyDirectoryWithPermissions(src, dest) {
|
|
433
|
+
const srcStat = fs.statSync(src);
|
|
434
|
+
fs.mkdirSync(dest, { recursive: true, mode: srcStat.mode });
|
|
435
|
+
|
|
436
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
437
|
+
|
|
438
|
+
for (const entry of entries) {
|
|
439
|
+
const srcPath = path.join(src, entry.name);
|
|
440
|
+
const destPath = path.join(dest, entry.name);
|
|
441
|
+
|
|
442
|
+
if (entry.isDirectory()) {
|
|
443
|
+
await this._copyDirectoryWithPermissions(srcPath, destPath);
|
|
444
|
+
} else {
|
|
445
|
+
fs.copyFileSync(srcPath, destPath);
|
|
446
|
+
const fileStat = fs.statSync(srcPath);
|
|
447
|
+
fs.chmodSync(destPath, fileStat.mode);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Calculate SHA256 hash of a file
|
|
454
|
+
*/
|
|
455
|
+
async _hashFile(filePath) {
|
|
456
|
+
const content = fs.readFileSync(filePath);
|
|
457
|
+
return 'sha256:' + crypto.createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export default BackupManager;
|