zuppaclaude 1.3.4 → 1.3.5
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/lib/components/backup.js +13 -8
- package/lib/components/cloud.js +280 -44
- package/lib/components/session.js +33 -16
- package/package.json +1 -1
package/lib/components/backup.js
CHANGED
|
@@ -40,26 +40,30 @@ class BackupManager {
|
|
|
40
40
|
this.logger.warning('No sessions to backup');
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
// Step 2: Backup settings to
|
|
43
|
+
// Step 2: Backup settings to settings/{timestamp} directory
|
|
44
44
|
this.logger.step('Step 2/3: Backing up ZuppaClaude settings...');
|
|
45
45
|
|
|
46
|
+
let settingsBackupPath = null;
|
|
46
47
|
if (sessionResult) {
|
|
47
|
-
const
|
|
48
|
+
const settingsDir = path.join(this.backupDir, 'settings', sessionResult.timestamp);
|
|
49
|
+
this.platform.ensureDir(settingsDir);
|
|
50
|
+
settingsBackupPath = path.join(settingsDir, 'zc-settings.json');
|
|
48
51
|
const currentSettings = this.settings.load();
|
|
49
52
|
|
|
50
53
|
if (currentSettings) {
|
|
51
54
|
fs.writeFileSync(settingsBackupPath, JSON.stringify(currentSettings, null, 2));
|
|
52
|
-
this.logger.success(
|
|
55
|
+
this.logger.success(`Settings backed up to settings/${sessionResult.timestamp}/`);
|
|
53
56
|
} else {
|
|
54
57
|
this.logger.info('No settings to backup');
|
|
55
58
|
}
|
|
56
59
|
|
|
57
|
-
// Update manifest with settings info
|
|
60
|
+
// Update manifest in sessions backup with settings info
|
|
58
61
|
const manifestPath = path.join(sessionResult.path, 'manifest.json');
|
|
59
62
|
if (fs.existsSync(manifestPath)) {
|
|
60
63
|
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
61
64
|
manifest.settings = currentSettings ? true : false;
|
|
62
65
|
manifest.backupType = 'full';
|
|
66
|
+
manifest.settingsPath = `settings/${sessionResult.timestamp}`;
|
|
63
67
|
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
64
68
|
}
|
|
65
69
|
}
|
|
@@ -119,9 +123,10 @@ class BackupManager {
|
|
|
119
123
|
this.logger.info('Step 1/3: Using local backup');
|
|
120
124
|
}
|
|
121
125
|
|
|
122
|
-
const
|
|
126
|
+
const sessionsBackupPath = path.join(this.backupDir, 'sessions', backupId);
|
|
127
|
+
const settingsBackupDir = path.join(this.backupDir, 'settings', backupId);
|
|
123
128
|
|
|
124
|
-
if (!fs.existsSync(
|
|
129
|
+
if (!fs.existsSync(sessionsBackupPath)) {
|
|
125
130
|
this.logger.error(`Backup not found: ${backupId}`);
|
|
126
131
|
this.logger.info('Run "npx zuppaclaude backup list" to see available backups');
|
|
127
132
|
return false;
|
|
@@ -138,11 +143,11 @@ class BackupManager {
|
|
|
138
143
|
this.logger.info('Step 2/3: Sessions restore skipped');
|
|
139
144
|
}
|
|
140
145
|
|
|
141
|
-
// Step 3: Restore settings
|
|
146
|
+
// Step 3: Restore settings from settings/{timestamp}/
|
|
142
147
|
if (!sessionsOnly) {
|
|
143
148
|
this.logger.step('Step 3/3: Restoring settings...');
|
|
144
149
|
|
|
145
|
-
const settingsBackupPath = path.join(
|
|
150
|
+
const settingsBackupPath = path.join(settingsBackupDir, 'zc-settings.json');
|
|
146
151
|
|
|
147
152
|
if (fs.existsSync(settingsBackupPath)) {
|
|
148
153
|
try {
|
package/lib/components/cloud.js
CHANGED
|
@@ -386,7 +386,57 @@ class CloudManager {
|
|
|
386
386
|
}
|
|
387
387
|
|
|
388
388
|
/**
|
|
389
|
-
*
|
|
389
|
+
* Create zip file from directory
|
|
390
|
+
*/
|
|
391
|
+
createZip(sourceDir, zipPath) {
|
|
392
|
+
try {
|
|
393
|
+
const parentDir = path.dirname(sourceDir);
|
|
394
|
+
const folderName = path.basename(sourceDir);
|
|
395
|
+
|
|
396
|
+
if (this.platform.isWindows) {
|
|
397
|
+
// Use PowerShell on Windows
|
|
398
|
+
this.platform.exec(
|
|
399
|
+
`powershell -command "Compress-Archive -Path '${sourceDir}' -DestinationPath '${zipPath}' -Force"`,
|
|
400
|
+
{ silent: true }
|
|
401
|
+
);
|
|
402
|
+
} else {
|
|
403
|
+
// Use zip on macOS/Linux
|
|
404
|
+
this.platform.exec(
|
|
405
|
+
`cd "${parentDir}" && zip -r "${zipPath}" "${folderName}"`,
|
|
406
|
+
{ silent: true }
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return true;
|
|
410
|
+
} catch (error) {
|
|
411
|
+
this.logger.warning(`Failed to create zip: ${error.message}`);
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Extract zip file
|
|
418
|
+
*/
|
|
419
|
+
extractZip(zipPath, destDir) {
|
|
420
|
+
try {
|
|
421
|
+
this.platform.ensureDir(destDir);
|
|
422
|
+
|
|
423
|
+
if (this.platform.isWindows) {
|
|
424
|
+
this.platform.exec(
|
|
425
|
+
`powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`,
|
|
426
|
+
{ silent: true }
|
|
427
|
+
);
|
|
428
|
+
} else {
|
|
429
|
+
this.platform.exec(`unzip -o "${zipPath}" -d "${destDir}"`, { silent: true });
|
|
430
|
+
}
|
|
431
|
+
return true;
|
|
432
|
+
} catch (error) {
|
|
433
|
+
this.logger.warning(`Failed to extract zip: ${error.message}`);
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Upload backup to cloud (as zip)
|
|
390
440
|
*/
|
|
391
441
|
async upload(remote, backupId = null) {
|
|
392
442
|
if (!this.isRcloneInstalled()) {
|
|
@@ -401,20 +451,16 @@ class CloudManager {
|
|
|
401
451
|
return false;
|
|
402
452
|
}
|
|
403
453
|
|
|
404
|
-
//
|
|
405
|
-
let sourcePath = this.backupDir;
|
|
406
|
-
let destPath = `${remote}:${this.cloudPath}`;
|
|
407
|
-
|
|
454
|
+
// Upload specific backup or all backups
|
|
408
455
|
if (backupId) {
|
|
409
|
-
|
|
410
|
-
if (!fs.existsSync(sourcePath)) {
|
|
411
|
-
this.logger.error(`Backup not found: ${backupId}`);
|
|
412
|
-
return false;
|
|
413
|
-
}
|
|
414
|
-
destPath = `${remote}:${this.cloudPath}/${backupId}`;
|
|
456
|
+
return await this.uploadSingleBackup(remote, backupId);
|
|
415
457
|
}
|
|
416
458
|
|
|
417
|
-
|
|
459
|
+
// Upload all backups (sessions and settings folders)
|
|
460
|
+
const sessionsDir = path.join(this.backupDir, 'sessions');
|
|
461
|
+
const settingsDir = path.join(this.backupDir, 'settings');
|
|
462
|
+
|
|
463
|
+
if (!fs.existsSync(sessionsDir) && !fs.existsSync(settingsDir)) {
|
|
418
464
|
this.logger.error('No backups to upload');
|
|
419
465
|
this.logger.info('Run "npx zuppaclaude backup" first');
|
|
420
466
|
return false;
|
|
@@ -424,23 +470,130 @@ class CloudManager {
|
|
|
424
470
|
this.logger.step(`Uploading to ${remote}...`);
|
|
425
471
|
console.log('');
|
|
426
472
|
|
|
473
|
+
let uploaded = 0;
|
|
474
|
+
|
|
475
|
+
// Get all backup timestamps from sessions folder
|
|
476
|
+
if (fs.existsSync(sessionsDir)) {
|
|
477
|
+
const backupFolders = fs.readdirSync(sessionsDir).filter(name => {
|
|
478
|
+
return fs.statSync(path.join(sessionsDir, name)).isDirectory();
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
for (const folder of backupFolders) {
|
|
482
|
+
const success = await this.uploadSingleBackup(remote, folder);
|
|
483
|
+
if (success) uploaded++;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
console.log('');
|
|
488
|
+
this.logger.success(`Uploaded ${uploaded} backup(s) to ${remote}:${this.cloudPath}/`);
|
|
489
|
+
console.log('');
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Upload a single backup as zip
|
|
495
|
+
*/
|
|
496
|
+
async uploadSingleBackup(remote, backupId) {
|
|
497
|
+
const sessionsPath = path.join(this.backupDir, 'sessions', backupId);
|
|
498
|
+
const settingsPath = path.join(this.backupDir, 'settings', backupId);
|
|
499
|
+
|
|
500
|
+
if (!fs.existsSync(sessionsPath)) {
|
|
501
|
+
this.logger.error(`Backup not found: ${backupId}`);
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Create temp directory for zip
|
|
506
|
+
const tempDir = path.join(this.backupDir, '.temp');
|
|
507
|
+
this.platform.ensureDir(tempDir);
|
|
508
|
+
|
|
509
|
+
const zipFileName = `${backupId}.zip`;
|
|
510
|
+
const zipPath = path.join(tempDir, zipFileName);
|
|
511
|
+
|
|
512
|
+
this.logger.info(`Compressing ${backupId}...`);
|
|
513
|
+
|
|
514
|
+
// Create a combined backup folder
|
|
515
|
+
const combinedDir = path.join(tempDir, backupId);
|
|
516
|
+
this.platform.ensureDir(combinedDir);
|
|
517
|
+
|
|
518
|
+
// Copy sessions
|
|
519
|
+
const sessionsDestDir = path.join(combinedDir, 'sessions');
|
|
520
|
+
this.platform.ensureDir(sessionsDestDir);
|
|
427
521
|
try {
|
|
428
|
-
|
|
429
|
-
|
|
522
|
+
this.platform.exec(`cp -r "${sessionsPath}"/* "${sessionsDestDir}/"`, { silent: true });
|
|
523
|
+
} catch (e) {
|
|
524
|
+
// Fallback: copy the directory itself
|
|
525
|
+
this.platform.exec(`cp -r "${sessionsPath}" "${combinedDir}/"`, { silent: true });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Copy settings if exists and has content
|
|
529
|
+
if (fs.existsSync(settingsPath)) {
|
|
530
|
+
const settingsFiles = fs.readdirSync(settingsPath);
|
|
531
|
+
if (settingsFiles.length > 0) {
|
|
532
|
+
const settingsDestDir = path.join(combinedDir, 'settings');
|
|
533
|
+
this.platform.ensureDir(settingsDestDir);
|
|
534
|
+
try {
|
|
535
|
+
this.platform.exec(`cp -r "${settingsPath}"/* "${settingsDestDir}/"`, { silent: true });
|
|
536
|
+
} catch (e) {
|
|
537
|
+
// Ignore if empty
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Create zip
|
|
543
|
+
const zipCreated = this.createZip(combinedDir, zipPath);
|
|
544
|
+
if (!zipCreated) {
|
|
545
|
+
this.logger.error('Failed to create zip');
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const zipSize = fs.statSync(zipPath).size;
|
|
550
|
+
this.logger.info(`Zip size: ${this.formatSize(zipSize)}`);
|
|
551
|
+
|
|
552
|
+
// Upload zip
|
|
553
|
+
const destPath = `${remote}:${this.cloudPath}/${zipFileName}`;
|
|
554
|
+
|
|
555
|
+
try {
|
|
556
|
+
this.logger.info(`Uploading ${zipFileName}...`);
|
|
557
|
+
const cmd = `rclone copy "${zipPath}" "${remote}:${this.cloudPath}/" --progress`;
|
|
430
558
|
this.platform.exec(cmd, { silent: false, stdio: 'inherit' });
|
|
431
559
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
560
|
+
this.logger.success(`Uploaded ${zipFileName}`);
|
|
561
|
+
|
|
562
|
+
// Cleanup: delete temp files
|
|
563
|
+
try {
|
|
564
|
+
fs.unlinkSync(zipPath);
|
|
565
|
+
fs.rmSync(combinedDir, { recursive: true, force: true });
|
|
566
|
+
} catch (e) {
|
|
567
|
+
// Ignore cleanup errors
|
|
568
|
+
}
|
|
569
|
+
|
|
435
570
|
return true;
|
|
436
571
|
} catch (error) {
|
|
437
572
|
this.logger.error(`Upload failed: ${error.message}`);
|
|
573
|
+
|
|
574
|
+
// Cleanup on error
|
|
575
|
+
try {
|
|
576
|
+
fs.unlinkSync(zipPath);
|
|
577
|
+
fs.rmSync(combinedDir, { recursive: true, force: true });
|
|
578
|
+
} catch (e) {
|
|
579
|
+
// Ignore
|
|
580
|
+
}
|
|
581
|
+
|
|
438
582
|
return false;
|
|
439
583
|
}
|
|
440
584
|
}
|
|
441
585
|
|
|
442
586
|
/**
|
|
443
|
-
*
|
|
587
|
+
* Format file size
|
|
588
|
+
*/
|
|
589
|
+
formatSize(bytes) {
|
|
590
|
+
if (bytes < 1024) return bytes + ' B';
|
|
591
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
592
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Download backup from cloud (downloads and extracts zip)
|
|
444
597
|
*/
|
|
445
598
|
async download(remote, backupId = null) {
|
|
446
599
|
if (!this.isRcloneInstalled()) {
|
|
@@ -454,26 +607,42 @@ class CloudManager {
|
|
|
454
607
|
return false;
|
|
455
608
|
}
|
|
456
609
|
|
|
457
|
-
|
|
458
|
-
let destPath = this.backupDir;
|
|
459
|
-
|
|
610
|
+
// Download specific backup or all backups
|
|
460
611
|
if (backupId) {
|
|
461
|
-
|
|
462
|
-
destPath = path.join(this.backupDir, backupId);
|
|
612
|
+
return await this.downloadSingleBackup(remote, backupId);
|
|
463
613
|
}
|
|
464
614
|
|
|
465
|
-
|
|
466
|
-
|
|
615
|
+
// Download all backups
|
|
467
616
|
console.log('');
|
|
468
617
|
this.logger.step(`Downloading from ${remote}...`);
|
|
469
618
|
console.log('');
|
|
470
619
|
|
|
471
620
|
try {
|
|
472
|
-
|
|
473
|
-
|
|
621
|
+
// List all zip files in cloud
|
|
622
|
+
const cmd = `rclone lsf "${remote}:${this.cloudPath}/" --files-only 2>/dev/null`;
|
|
623
|
+
const output = this.platform.exec(cmd, { silent: true });
|
|
624
|
+
|
|
625
|
+
if (!output) {
|
|
626
|
+
this.logger.warning('No backups found on cloud');
|
|
627
|
+
return false;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const zipFiles = output.split('\n').filter(f => f.endsWith('.zip'));
|
|
631
|
+
|
|
632
|
+
if (zipFiles.length === 0) {
|
|
633
|
+
this.logger.warning('No backup archives found on cloud');
|
|
634
|
+
return false;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
let downloaded = 0;
|
|
638
|
+
for (const zipFile of zipFiles) {
|
|
639
|
+
const backupName = zipFile.replace('.zip', '');
|
|
640
|
+
const success = await this.downloadSingleBackup(remote, backupName);
|
|
641
|
+
if (success) downloaded++;
|
|
642
|
+
}
|
|
474
643
|
|
|
475
644
|
console.log('');
|
|
476
|
-
this.logger.success(`
|
|
645
|
+
this.logger.success(`Downloaded ${downloaded} backup(s) from ${remote}`);
|
|
477
646
|
console.log('');
|
|
478
647
|
return true;
|
|
479
648
|
} catch (error) {
|
|
@@ -483,7 +652,78 @@ class CloudManager {
|
|
|
483
652
|
}
|
|
484
653
|
|
|
485
654
|
/**
|
|
486
|
-
*
|
|
655
|
+
* Download and extract a single backup
|
|
656
|
+
*/
|
|
657
|
+
async downloadSingleBackup(remote, backupId) {
|
|
658
|
+
const zipFileName = `${backupId}.zip`;
|
|
659
|
+
const tempDir = path.join(this.backupDir, '.temp');
|
|
660
|
+
this.platform.ensureDir(tempDir);
|
|
661
|
+
const zipPath = path.join(tempDir, zipFileName);
|
|
662
|
+
|
|
663
|
+
try {
|
|
664
|
+
// Download zip
|
|
665
|
+
this.logger.info(`Downloading ${zipFileName}...`);
|
|
666
|
+
const cmd = `rclone copy "${remote}:${this.cloudPath}/${zipFileName}" "${tempDir}/" --progress`;
|
|
667
|
+
this.platform.exec(cmd, { silent: false, stdio: 'inherit' });
|
|
668
|
+
|
|
669
|
+
if (!fs.existsSync(zipPath)) {
|
|
670
|
+
this.logger.error(`Backup not found: ${backupId}`);
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Extract zip
|
|
675
|
+
this.logger.info(`Extracting ${zipFileName}...`);
|
|
676
|
+
const extractDir = path.join(tempDir, 'extract');
|
|
677
|
+
this.platform.ensureDir(extractDir);
|
|
678
|
+
this.extractZip(zipPath, extractDir);
|
|
679
|
+
|
|
680
|
+
// Move sessions and settings to proper locations
|
|
681
|
+
const extractedBackup = path.join(extractDir, backupId);
|
|
682
|
+
|
|
683
|
+
if (fs.existsSync(path.join(extractedBackup, 'sessions'))) {
|
|
684
|
+
const sessionsDir = path.join(this.backupDir, 'sessions', backupId);
|
|
685
|
+
this.platform.ensureDir(path.dirname(sessionsDir));
|
|
686
|
+
if (fs.existsSync(sessionsDir)) {
|
|
687
|
+
fs.rmSync(sessionsDir, { recursive: true, force: true });
|
|
688
|
+
}
|
|
689
|
+
fs.renameSync(path.join(extractedBackup, 'sessions'), sessionsDir);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (fs.existsSync(path.join(extractedBackup, 'settings'))) {
|
|
693
|
+
const settingsDir = path.join(this.backupDir, 'settings', backupId);
|
|
694
|
+
this.platform.ensureDir(path.dirname(settingsDir));
|
|
695
|
+
if (fs.existsSync(settingsDir)) {
|
|
696
|
+
fs.rmSync(settingsDir, { recursive: true, force: true });
|
|
697
|
+
}
|
|
698
|
+
fs.renameSync(path.join(extractedBackup, 'settings'), settingsDir);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Cleanup
|
|
702
|
+
try {
|
|
703
|
+
fs.unlinkSync(zipPath);
|
|
704
|
+
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
705
|
+
} catch (e) {
|
|
706
|
+
// Ignore cleanup errors
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
this.logger.success(`Downloaded ${backupId}`);
|
|
710
|
+
return true;
|
|
711
|
+
} catch (error) {
|
|
712
|
+
this.logger.error(`Failed to download ${backupId}: ${error.message}`);
|
|
713
|
+
|
|
714
|
+
// Cleanup on error
|
|
715
|
+
try {
|
|
716
|
+
if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
|
|
717
|
+
} catch (e) {
|
|
718
|
+
// Ignore
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* List cloud backups (zip files)
|
|
487
727
|
*/
|
|
488
728
|
async listCloudBackups(remote) {
|
|
489
729
|
if (!this.isRcloneInstalled()) {
|
|
@@ -497,29 +737,25 @@ class CloudManager {
|
|
|
497
737
|
}
|
|
498
738
|
|
|
499
739
|
try {
|
|
500
|
-
const cmd = `rclone
|
|
740
|
+
const cmd = `rclone lsf "${remote}:${this.cloudPath}/" --files-only 2>/dev/null`;
|
|
501
741
|
const output = this.platform.exec(cmd, { silent: true });
|
|
502
742
|
|
|
743
|
+
console.log('');
|
|
744
|
+
console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
|
|
745
|
+
console.log(`\x1b[35m Cloud Backups (${remote})\x1b[0m`);
|
|
746
|
+
console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
|
|
747
|
+
console.log('');
|
|
748
|
+
|
|
503
749
|
if (!output) {
|
|
504
|
-
|
|
750
|
+
console.log(' No backups found');
|
|
751
|
+
console.log('');
|
|
505
752
|
return [];
|
|
506
753
|
}
|
|
507
754
|
|
|
508
755
|
const backups = output
|
|
509
756
|
.split('\n')
|
|
510
|
-
.filter(
|
|
511
|
-
.map(
|
|
512
|
-
const parts = line.trim().split(/\s+/);
|
|
513
|
-
const name = parts[parts.length - 1];
|
|
514
|
-
return name;
|
|
515
|
-
})
|
|
516
|
-
.filter(name => name && name.match(/^\d{4}-\d{2}-\d{2}T/));
|
|
517
|
-
|
|
518
|
-
console.log('');
|
|
519
|
-
console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
|
|
520
|
-
console.log(`\x1b[35m Cloud Backups (${remote})\x1b[0m`);
|
|
521
|
-
console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
|
|
522
|
-
console.log('');
|
|
757
|
+
.filter(f => f.endsWith('.zip'))
|
|
758
|
+
.map(f => f.replace('.zip', ''));
|
|
523
759
|
|
|
524
760
|
if (backups.length === 0) {
|
|
525
761
|
console.log(' No backups found');
|
|
@@ -119,6 +119,21 @@ class SessionManager {
|
|
|
119
119
|
return projects;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Format timestamp as Jan-05-2026-13.56
|
|
124
|
+
*/
|
|
125
|
+
formatTimestamp(date = new Date()) {
|
|
126
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
127
|
+
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
128
|
+
const month = months[date.getMonth()];
|
|
129
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
130
|
+
const year = date.getFullYear();
|
|
131
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
132
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
133
|
+
|
|
134
|
+
return `${month}-${day}-${year}-${hours}.${minutes}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
122
137
|
/**
|
|
123
138
|
* Backup all sessions
|
|
124
139
|
*/
|
|
@@ -130,10 +145,9 @@ class SessionManager {
|
|
|
130
145
|
return null;
|
|
131
146
|
}
|
|
132
147
|
|
|
133
|
-
// Create backup directory with timestamp
|
|
134
|
-
const timestamp =
|
|
135
|
-
const
|
|
136
|
-
const sessionsBackupPath = path.join(backupPath, 'sessions');
|
|
148
|
+
// Create backup directory with timestamp (Jan-05-2026-13.56 format)
|
|
149
|
+
const timestamp = this.formatTimestamp();
|
|
150
|
+
const sessionsBackupPath = path.join(this.backupDir, 'sessions', timestamp);
|
|
137
151
|
|
|
138
152
|
this.platform.ensureDir(sessionsBackupPath);
|
|
139
153
|
|
|
@@ -144,7 +158,8 @@ class SessionManager {
|
|
|
144
158
|
let backedUp = 0;
|
|
145
159
|
let totalSize = 0;
|
|
146
160
|
const manifest = {
|
|
147
|
-
timestamp:
|
|
161
|
+
timestamp: timestamp,
|
|
162
|
+
timestampISO: new Date().toISOString(),
|
|
148
163
|
version: require('../../package.json').version,
|
|
149
164
|
projects: []
|
|
150
165
|
};
|
|
@@ -186,7 +201,7 @@ class SessionManager {
|
|
|
186
201
|
const historyPath = path.join(this.claudeDir, 'history.jsonl');
|
|
187
202
|
if (fs.existsSync(historyPath)) {
|
|
188
203
|
try {
|
|
189
|
-
fs.copyFileSync(historyPath, path.join(
|
|
204
|
+
fs.copyFileSync(historyPath, path.join(sessionsBackupPath, 'history.jsonl'));
|
|
190
205
|
const historyStats = fs.statSync(historyPath);
|
|
191
206
|
manifest.history = {
|
|
192
207
|
size: historyStats.size,
|
|
@@ -201,17 +216,17 @@ class SessionManager {
|
|
|
201
216
|
|
|
202
217
|
// Save manifest
|
|
203
218
|
fs.writeFileSync(
|
|
204
|
-
path.join(
|
|
219
|
+
path.join(sessionsBackupPath, 'manifest.json'),
|
|
205
220
|
JSON.stringify(manifest, null, 2)
|
|
206
221
|
);
|
|
207
222
|
|
|
208
223
|
console.log('');
|
|
209
224
|
this.logger.success(`Backup complete: ${backedUp} sessions, ${this.formatSize(totalSize)}`);
|
|
210
|
-
this.logger.info(`Location: ${
|
|
225
|
+
this.logger.info(`Location: ${sessionsBackupPath}`);
|
|
211
226
|
console.log('');
|
|
212
227
|
|
|
213
228
|
return {
|
|
214
|
-
path:
|
|
229
|
+
path: sessionsBackupPath,
|
|
215
230
|
sessions: backedUp,
|
|
216
231
|
size: totalSize,
|
|
217
232
|
timestamp: timestamp
|
|
@@ -222,18 +237,20 @@ class SessionManager {
|
|
|
222
237
|
* List available backups
|
|
223
238
|
*/
|
|
224
239
|
listBackups() {
|
|
225
|
-
|
|
240
|
+
const sessionsDir = path.join(this.backupDir, 'sessions');
|
|
241
|
+
|
|
242
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
226
243
|
this.logger.warning('No backups found');
|
|
227
244
|
return [];
|
|
228
245
|
}
|
|
229
246
|
|
|
230
|
-
const backups = fs.readdirSync(
|
|
247
|
+
const backups = fs.readdirSync(sessionsDir)
|
|
231
248
|
.filter(name => {
|
|
232
|
-
const backupPath = path.join(
|
|
249
|
+
const backupPath = path.join(sessionsDir, name);
|
|
233
250
|
return fs.statSync(backupPath).isDirectory();
|
|
234
251
|
})
|
|
235
252
|
.map(name => {
|
|
236
|
-
const backupPath = path.join(
|
|
253
|
+
const backupPath = path.join(sessionsDir, name);
|
|
237
254
|
const manifestPath = path.join(backupPath, 'manifest.json');
|
|
238
255
|
|
|
239
256
|
let manifest = null;
|
|
@@ -271,7 +288,6 @@ class SessionManager {
|
|
|
271
288
|
console.log('');
|
|
272
289
|
|
|
273
290
|
for (const backup of backups) {
|
|
274
|
-
const dateStr = backup.id.replace('T', ' ').replace(/-/g, ':').slice(0, 16).replace(/:/g, '-').slice(0,10) + ' ' + backup.id.slice(11, 16).replace(/-/g, ':');
|
|
275
291
|
console.log(` 📦 ${backup.id}`);
|
|
276
292
|
console.log(` ${backup.projects} projects, ${backup.sessions} sessions`);
|
|
277
293
|
console.log('');
|
|
@@ -284,7 +300,7 @@ class SessionManager {
|
|
|
284
300
|
* Restore from backup
|
|
285
301
|
*/
|
|
286
302
|
restore(backupId) {
|
|
287
|
-
const backupPath = path.join(this.backupDir, backupId);
|
|
303
|
+
const backupPath = path.join(this.backupDir, 'sessions', backupId);
|
|
288
304
|
|
|
289
305
|
if (!fs.existsSync(backupPath)) {
|
|
290
306
|
this.logger.error(`Backup not found: ${backupId}`);
|
|
@@ -306,7 +322,8 @@ class SessionManager {
|
|
|
306
322
|
let restored = 0;
|
|
307
323
|
|
|
308
324
|
for (const project of manifest.projects) {
|
|
309
|
-
|
|
325
|
+
// Sessions are directly under the backup path (not in a nested sessions folder)
|
|
326
|
+
const projectBackupPath = path.join(backupPath, project.id);
|
|
310
327
|
const projectDestPath = path.join(this.projectsDir, project.id);
|
|
311
328
|
|
|
312
329
|
if (!fs.existsSync(projectBackupPath)) continue;
|