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.
@@ -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
 
@@ -491,7 +491,7 @@ class CloudManager {
491
491
  }
492
492
 
493
493
  /**
494
- * Upload a single backup as zip
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
- const zipPath = path.join(tempDir, zipFileName);
510
+ let totalSize = 0;
511
+ let uploaded = 0;
511
512
 
512
- this.logger.info(`Compressing ${backupId}...`);
513
+ // Upload sessions zip
514
+ this.logger.info(`Compressing sessions/${backupId}...`);
515
+ const sessionsZipPath = path.join(tempDir, `sessions-${zipFileName}`);
513
516
 
514
- // Create a combined backup folder
515
- const combinedDir = path.join(tempDir, backupId);
516
- this.platform.ensureDir(combinedDir);
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
- // Copy sessions
519
- const sessionsDestDir = path.join(combinedDir, 'sessions');
520
- this.platform.ensureDir(sessionsDestDir);
521
- try {
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 });
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
- // Copy settings if exists and has content
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
- 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
- }
543
+ this.logger.info(`Compressing settings/${backupId}...`);
544
+ const settingsZipPath = path.join(tempDir, `settings-${zipFileName}`);
541
545
 
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
- }
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
- const zipSize = fs.statSync(zipPath).size;
550
- this.logger.info(`Zip size: ${this.formatSize(zipSize)}`);
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
- // Upload zip
553
- const destPath = `${remote}:${this.cloudPath}/${zipFileName}`;
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
- try {
556
- this.logger.info(`Uploading ${zipFileName}...`);
557
- const cmd = `rclone copy "${zipPath}" "${remote}:${this.cloudPath}/" --progress`;
558
- this.platform.exec(cmd, { silent: false, stdio: 'inherit' });
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
- // 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
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
- // Download zip
665
- this.logger.info(`Downloading ${zipFileName}...`);
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 (!fs.existsSync(zipPath)) {
670
- this.logger.error(`Backup not found: ${backupId}`);
671
- return false;
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
- // 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'))) {
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(path.dirname(sessionsDir));
686
- if (fs.existsSync(sessionsDir)) {
687
- fs.rmSync(sessionsDir, { recursive: true, force: true });
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
- fs.renameSync(path.join(extractedBackup, 'sessions'), sessionsDir);
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(path.join(extractedBackup, 'settings'))) {
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(path.dirname(settingsDir));
695
- if (fs.existsSync(settingsDir)) {
696
- fs.rmSync(settingsDir, { recursive: true, force: true });
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
- // Cleanup
702
- try {
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
- try {
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
- if (!output || !output.trim()) {
747
- this.logger.error(`Backup not found on cloud: ${backupId}`);
748
- return false;
749
- }
750
-
751
- this.logger.step(`Deleting ${zipFileName} from ${remote}...`);
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
- // Delete the backup
754
- const deleteCmd = `rclone delete "${remote}:${this.cloudPath}/${zipFileName}"`;
755
- this.platform.exec(deleteCmd, { silent: true });
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 (zip files)
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
@@ -204,6 +204,9 @@ class Installer {
204
204
  commands: cmdsInstalled
205
205
  });
206
206
 
207
+ // Close readline interface
208
+ this.prompts.close();
209
+
207
210
  return true;
208
211
  }
209
212
 
@@ -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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuppaclaude",
3
- "version": "1.3.6",
3
+ "version": "1.3.8",
4
4
  "description": "Claude Code power-up installer - SuperClaude + Spec Kit + Claude-Z + Claude HUD",
5
5
  "main": "lib/index.js",
6
6
  "bin": {