zuppaclaude 1.3.6 → 1.3.8
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 +1 -1
- package/lib/components/cloud.js +144 -124
- package/lib/installer.js +3 -0
- package/lib/uninstaller.js +4 -0
- package/package.json +1 -1
package/lib/components/backup.js
CHANGED
|
@@ -212,7 +212,7 @@ class BackupManager {
|
|
|
212
212
|
console.log(` 📍 Location: ${result.path}`);
|
|
213
213
|
|
|
214
214
|
if (cloud) {
|
|
215
|
-
console.log(` ☁️ Cloud: ${cloud}:zuppaclaude-backups/${result.timestamp}`);
|
|
215
|
+
console.log(` ☁️ Cloud: ${cloud}:zuppaclaude-backups/sessions/${result.timestamp}.zip`);
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
package/lib/components/cloud.js
CHANGED
|
@@ -491,7 +491,7 @@ class CloudManager {
|
|
|
491
491
|
}
|
|
492
492
|
|
|
493
493
|
/**
|
|
494
|
-
* Upload a single backup as
|
|
494
|
+
* Upload a single backup as separate zips (sessions + settings)
|
|
495
495
|
*/
|
|
496
496
|
async uploadSingleBackup(remote, backupId) {
|
|
497
497
|
const sessionsPath = path.join(this.backupDir, 'sessions', backupId);
|
|
@@ -507,80 +507,72 @@ class CloudManager {
|
|
|
507
507
|
this.platform.ensureDir(tempDir);
|
|
508
508
|
|
|
509
509
|
const zipFileName = `${backupId}.zip`;
|
|
510
|
-
|
|
510
|
+
let totalSize = 0;
|
|
511
|
+
let uploaded = 0;
|
|
511
512
|
|
|
512
|
-
|
|
513
|
+
// Upload sessions zip
|
|
514
|
+
this.logger.info(`Compressing sessions/${backupId}...`);
|
|
515
|
+
const sessionsZipPath = path.join(tempDir, `sessions-${zipFileName}`);
|
|
513
516
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
+
if (this.createZip(sessionsPath, sessionsZipPath)) {
|
|
518
|
+
const zipSize = fs.statSync(sessionsZipPath).size;
|
|
519
|
+
totalSize += zipSize;
|
|
520
|
+
this.logger.info(`Sessions zip: ${this.formatSize(zipSize)}`);
|
|
517
521
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
522
|
+
try {
|
|
523
|
+
this.logger.info(`Uploading to sessions/${zipFileName}...`);
|
|
524
|
+
const cmd = `rclone copy "${sessionsZipPath}" "${remote}:${this.cloudPath}/sessions/" --progress`;
|
|
525
|
+
this.platform.exec(cmd, { silent: false, stdio: 'inherit' });
|
|
526
|
+
|
|
527
|
+
// Rename on remote to remove 'sessions-' prefix
|
|
528
|
+
this.platform.exec(`rclone moveto "${remote}:${this.cloudPath}/sessions/sessions-${zipFileName}" "${remote}:${this.cloudPath}/sessions/${zipFileName}"`, { silent: true });
|
|
529
|
+
|
|
530
|
+
uploaded++;
|
|
531
|
+
} catch (error) {
|
|
532
|
+
this.logger.error(`Failed to upload sessions: ${error.message}`);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Cleanup
|
|
536
|
+
try { fs.unlinkSync(sessionsZipPath); } catch (e) {}
|
|
526
537
|
}
|
|
527
538
|
|
|
528
|
-
//
|
|
539
|
+
// Upload settings zip if exists and has content
|
|
529
540
|
if (fs.existsSync(settingsPath)) {
|
|
530
541
|
const settingsFiles = fs.readdirSync(settingsPath);
|
|
531
542
|
if (settingsFiles.length > 0) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
try {
|
|
535
|
-
this.platform.exec(`cp -r "${settingsPath}"/* "${settingsDestDir}/"`, { silent: true });
|
|
536
|
-
} catch (e) {
|
|
537
|
-
// Ignore if empty
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
}
|
|
543
|
+
this.logger.info(`Compressing settings/${backupId}...`);
|
|
544
|
+
const settingsZipPath = path.join(tempDir, `settings-${zipFileName}`);
|
|
541
545
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
return false;
|
|
547
|
-
}
|
|
546
|
+
if (this.createZip(settingsPath, settingsZipPath)) {
|
|
547
|
+
const zipSize = fs.statSync(settingsZipPath).size;
|
|
548
|
+
totalSize += zipSize;
|
|
549
|
+
this.logger.info(`Settings zip: ${this.formatSize(zipSize)}`);
|
|
548
550
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
+
try {
|
|
552
|
+
this.logger.info(`Uploading to settings/${zipFileName}...`);
|
|
553
|
+
const cmd = `rclone copy "${settingsZipPath}" "${remote}:${this.cloudPath}/settings/" --progress`;
|
|
554
|
+
this.platform.exec(cmd, { silent: false, stdio: 'inherit' });
|
|
551
555
|
|
|
552
|
-
|
|
553
|
-
|
|
556
|
+
// Rename on remote
|
|
557
|
+
this.platform.exec(`rclone moveto "${remote}:${this.cloudPath}/settings/settings-${zipFileName}" "${remote}:${this.cloudPath}/settings/${zipFileName}"`, { silent: true });
|
|
554
558
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
this.logger.success(`Uploaded ${zipFileName}`);
|
|
559
|
+
uploaded++;
|
|
560
|
+
} catch (error) {
|
|
561
|
+
this.logger.error(`Failed to upload settings: ${error.message}`);
|
|
562
|
+
}
|
|
561
563
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
fs.rmSync(combinedDir, { recursive: true, force: true });
|
|
566
|
-
} catch (e) {
|
|
567
|
-
// Ignore cleanup errors
|
|
564
|
+
// Cleanup
|
|
565
|
+
try { fs.unlinkSync(settingsZipPath); } catch (e) {}
|
|
566
|
+
}
|
|
568
567
|
}
|
|
568
|
+
}
|
|
569
569
|
|
|
570
|
+
if (uploaded > 0) {
|
|
571
|
+
this.logger.success(`Uploaded ${backupId} (${this.formatSize(totalSize)})`);
|
|
570
572
|
return true;
|
|
571
|
-
} catch (error) {
|
|
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
|
-
|
|
582
|
-
return false;
|
|
583
573
|
}
|
|
574
|
+
|
|
575
|
+
return false;
|
|
584
576
|
}
|
|
585
577
|
|
|
586
578
|
/**
|
|
@@ -652,78 +644,97 @@ class CloudManager {
|
|
|
652
644
|
}
|
|
653
645
|
|
|
654
646
|
/**
|
|
655
|
-
* Download and extract a single backup
|
|
647
|
+
* Download and extract a single backup (from sessions/ and settings/ folders)
|
|
656
648
|
*/
|
|
657
649
|
async downloadSingleBackup(remote, backupId) {
|
|
658
650
|
const zipFileName = `${backupId}.zip`;
|
|
659
651
|
const tempDir = path.join(this.backupDir, '.temp');
|
|
660
652
|
this.platform.ensureDir(tempDir);
|
|
661
|
-
const zipPath = path.join(tempDir, zipFileName);
|
|
662
653
|
|
|
654
|
+
let downloaded = 0;
|
|
655
|
+
|
|
656
|
+
// Download sessions zip
|
|
657
|
+
const sessionsZipPath = path.join(tempDir, `sessions-${zipFileName}`);
|
|
663
658
|
try {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const cmd = `rclone copy "${remote}:${this.cloudPath}/${zipFileName}" "${tempDir}/" --progress`;
|
|
659
|
+
this.logger.info(`Downloading sessions/${zipFileName}...`);
|
|
660
|
+
const cmd = `rclone copy "${remote}:${this.cloudPath}/sessions/${zipFileName}" "${tempDir}/" --progress`;
|
|
667
661
|
this.platform.exec(cmd, { silent: false, stdio: 'inherit' });
|
|
668
662
|
|
|
669
|
-
if (
|
|
670
|
-
|
|
671
|
-
|
|
663
|
+
// Check if downloaded (rclone names it as the original)
|
|
664
|
+
const downloadedPath = path.join(tempDir, zipFileName);
|
|
665
|
+
if (fs.existsSync(downloadedPath)) {
|
|
666
|
+
fs.renameSync(downloadedPath, sessionsZipPath);
|
|
672
667
|
}
|
|
673
668
|
|
|
674
|
-
|
|
675
|
-
|
|
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'))) {
|
|
669
|
+
if (fs.existsSync(sessionsZipPath)) {
|
|
670
|
+
this.logger.info(`Extracting sessions...`);
|
|
684
671
|
const sessionsDir = path.join(this.backupDir, 'sessions', backupId);
|
|
685
|
-
this.platform.ensureDir(
|
|
686
|
-
|
|
687
|
-
|
|
672
|
+
this.platform.ensureDir(sessionsDir);
|
|
673
|
+
this.extractZip(sessionsZipPath, sessionsDir);
|
|
674
|
+
|
|
675
|
+
// Move contents up if nested
|
|
676
|
+
const nestedDir = path.join(sessionsDir, backupId);
|
|
677
|
+
if (fs.existsSync(nestedDir)) {
|
|
678
|
+
const files = fs.readdirSync(nestedDir);
|
|
679
|
+
for (const file of files) {
|
|
680
|
+
fs.renameSync(path.join(nestedDir, file), path.join(sessionsDir, file));
|
|
681
|
+
}
|
|
682
|
+
fs.rmdirSync(nestedDir);
|
|
688
683
|
}
|
|
689
|
-
|
|
684
|
+
|
|
685
|
+
downloaded++;
|
|
686
|
+
try { fs.unlinkSync(sessionsZipPath); } catch (e) {}
|
|
687
|
+
}
|
|
688
|
+
} catch (error) {
|
|
689
|
+
this.logger.warning(`Sessions not found or failed: ${error.message}`);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Download settings zip
|
|
693
|
+
const settingsZipPath = path.join(tempDir, `settings-${zipFileName}`);
|
|
694
|
+
try {
|
|
695
|
+
this.logger.info(`Downloading settings/${zipFileName}...`);
|
|
696
|
+
const cmd = `rclone copy "${remote}:${this.cloudPath}/settings/${zipFileName}" "${tempDir}/" --progress`;
|
|
697
|
+
this.platform.exec(cmd, { silent: false, stdio: 'inherit' });
|
|
698
|
+
|
|
699
|
+
const downloadedPath = path.join(tempDir, zipFileName);
|
|
700
|
+
if (fs.existsSync(downloadedPath)) {
|
|
701
|
+
fs.renameSync(downloadedPath, settingsZipPath);
|
|
690
702
|
}
|
|
691
703
|
|
|
692
|
-
if (fs.existsSync(
|
|
704
|
+
if (fs.existsSync(settingsZipPath)) {
|
|
705
|
+
this.logger.info(`Extracting settings...`);
|
|
693
706
|
const settingsDir = path.join(this.backupDir, 'settings', backupId);
|
|
694
|
-
this.platform.ensureDir(
|
|
695
|
-
|
|
696
|
-
|
|
707
|
+
this.platform.ensureDir(settingsDir);
|
|
708
|
+
this.extractZip(settingsZipPath, settingsDir);
|
|
709
|
+
|
|
710
|
+
// Move contents up if nested
|
|
711
|
+
const nestedDir = path.join(settingsDir, backupId);
|
|
712
|
+
if (fs.existsSync(nestedDir)) {
|
|
713
|
+
const files = fs.readdirSync(nestedDir);
|
|
714
|
+
for (const file of files) {
|
|
715
|
+
fs.renameSync(path.join(nestedDir, file), path.join(settingsDir, file));
|
|
716
|
+
}
|
|
717
|
+
fs.rmdirSync(nestedDir);
|
|
697
718
|
}
|
|
698
|
-
fs.renameSync(path.join(extractedBackup, 'settings'), settingsDir);
|
|
699
|
-
}
|
|
700
719
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
fs.unlinkSync(zipPath);
|
|
704
|
-
fs.rmSync(extractDir, { recursive: true, force: true });
|
|
705
|
-
} catch (e) {
|
|
706
|
-
// Ignore cleanup errors
|
|
720
|
+
downloaded++;
|
|
721
|
+
try { fs.unlinkSync(settingsZipPath); } catch (e) {}
|
|
707
722
|
}
|
|
723
|
+
} catch (error) {
|
|
724
|
+
// Settings might not exist, that's ok
|
|
725
|
+
}
|
|
708
726
|
|
|
727
|
+
if (downloaded > 0) {
|
|
709
728
|
this.logger.success(`Downloaded ${backupId}`);
|
|
710
729
|
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
730
|
}
|
|
731
|
+
|
|
732
|
+
this.logger.error(`Backup not found: ${backupId}`);
|
|
733
|
+
return false;
|
|
723
734
|
}
|
|
724
735
|
|
|
725
736
|
/**
|
|
726
|
-
* Delete a backup from cloud
|
|
737
|
+
* Delete a backup from cloud (both sessions and settings)
|
|
727
738
|
*/
|
|
728
739
|
async deleteCloudBackup(remote, backupId) {
|
|
729
740
|
if (!this.isRcloneInstalled()) {
|
|
@@ -737,29 +748,35 @@ class CloudManager {
|
|
|
737
748
|
}
|
|
738
749
|
|
|
739
750
|
const zipFileName = `${backupId}.zip`;
|
|
751
|
+
let deleted = 0;
|
|
740
752
|
|
|
741
|
-
|
|
742
|
-
// Check if backup exists
|
|
743
|
-
const cmd = `rclone lsf "${remote}:${this.cloudPath}/${zipFileName}" 2>/dev/null`;
|
|
744
|
-
const output = this.platform.exec(cmd, { silent: true });
|
|
753
|
+
this.logger.step(`Deleting ${backupId} from ${remote}...`);
|
|
745
754
|
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
|
|
755
|
+
// Delete sessions zip
|
|
756
|
+
try {
|
|
757
|
+
const sessionsPath = `${remote}:${this.cloudPath}/sessions/${zipFileName}`;
|
|
758
|
+
this.platform.exec(`rclone delete "${sessionsPath}"`, { silent: true });
|
|
759
|
+
deleted++;
|
|
760
|
+
} catch (e) {
|
|
761
|
+
// Ignore
|
|
762
|
+
}
|
|
752
763
|
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
764
|
+
// Delete settings zip
|
|
765
|
+
try {
|
|
766
|
+
const settingsPath = `${remote}:${this.cloudPath}/settings/${zipFileName}`;
|
|
767
|
+
this.platform.exec(`rclone delete "${settingsPath}"`, { silent: true });
|
|
768
|
+
deleted++;
|
|
769
|
+
} catch (e) {
|
|
770
|
+
// Ignore
|
|
771
|
+
}
|
|
756
772
|
|
|
773
|
+
if (deleted > 0) {
|
|
757
774
|
this.logger.success(`Deleted ${backupId} from ${remote}`);
|
|
758
775
|
return true;
|
|
759
|
-
} catch (error) {
|
|
760
|
-
this.logger.error(`Failed to delete: ${error.message}`);
|
|
761
|
-
return false;
|
|
762
776
|
}
|
|
777
|
+
|
|
778
|
+
this.logger.error(`Backup not found on cloud: ${backupId}`);
|
|
779
|
+
return false;
|
|
763
780
|
}
|
|
764
781
|
|
|
765
782
|
/**
|
|
@@ -819,11 +836,11 @@ class CloudManager {
|
|
|
819
836
|
}
|
|
820
837
|
|
|
821
838
|
/**
|
|
822
|
-
* Get list of cloud backups (without printing)
|
|
839
|
+
* Get list of cloud backups (without printing) - looks in sessions folder
|
|
823
840
|
*/
|
|
824
841
|
async getCloudBackupList(remote) {
|
|
825
842
|
try {
|
|
826
|
-
const cmd = `rclone lsf "${remote}:${this.cloudPath}/" --files-only 2>/dev/null`;
|
|
843
|
+
const cmd = `rclone lsf "${remote}:${this.cloudPath}/sessions/" --files-only 2>/dev/null`;
|
|
827
844
|
const output = this.platform.exec(cmd, { silent: true });
|
|
828
845
|
|
|
829
846
|
if (!output) return [];
|
|
@@ -838,7 +855,7 @@ class CloudManager {
|
|
|
838
855
|
}
|
|
839
856
|
|
|
840
857
|
/**
|
|
841
|
-
* List cloud backups (
|
|
858
|
+
* List cloud backups (from sessions folder)
|
|
842
859
|
*/
|
|
843
860
|
async listCloudBackups(remote) {
|
|
844
861
|
if (!this.isRcloneInstalled()) {
|
|
@@ -852,7 +869,7 @@ class CloudManager {
|
|
|
852
869
|
}
|
|
853
870
|
|
|
854
871
|
try {
|
|
855
|
-
const cmd = `rclone lsf "${remote}:${this.cloudPath}/" --files-only 2>/dev/null`;
|
|
872
|
+
const cmd = `rclone lsf "${remote}:${this.cloudPath}/sessions/" --files-only 2>/dev/null`;
|
|
856
873
|
const output = this.platform.exec(cmd, { silent: true });
|
|
857
874
|
|
|
858
875
|
console.log('');
|
|
@@ -860,6 +877,8 @@ class CloudManager {
|
|
|
860
877
|
console.log(`\x1b[35m Cloud Backups (${remote})\x1b[0m`);
|
|
861
878
|
console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
|
|
862
879
|
console.log('');
|
|
880
|
+
console.log(` Path: ${remote}:${this.cloudPath}/`);
|
|
881
|
+
console.log('');
|
|
863
882
|
|
|
864
883
|
if (!output) {
|
|
865
884
|
console.log(' No backups found');
|
|
@@ -877,6 +896,7 @@ class CloudManager {
|
|
|
877
896
|
} else {
|
|
878
897
|
for (const backup of backups) {
|
|
879
898
|
console.log(` 📦 ${backup}`);
|
|
899
|
+
console.log(` └── sessions/${backup}.zip`);
|
|
880
900
|
}
|
|
881
901
|
}
|
|
882
902
|
|
package/lib/installer.js
CHANGED
package/lib/uninstaller.js
CHANGED
|
@@ -43,6 +43,7 @@ class Uninstaller {
|
|
|
43
43
|
const confirm = await this.prompts.confirm('Are you sure you want to uninstall ZuppaClaude?', false);
|
|
44
44
|
if (!confirm) {
|
|
45
45
|
this.logger.info('Uninstallation cancelled');
|
|
46
|
+
this.prompts.close();
|
|
46
47
|
return false;
|
|
47
48
|
}
|
|
48
49
|
|
|
@@ -74,6 +75,9 @@ class Uninstaller {
|
|
|
74
75
|
// Print summary
|
|
75
76
|
this.printSummary(results, preserveSettings);
|
|
76
77
|
|
|
78
|
+
// Close readline interface
|
|
79
|
+
this.prompts.close();
|
|
80
|
+
|
|
77
81
|
return true;
|
|
78
82
|
}
|
|
79
83
|
|