zuppaclaude 1.3.5 → 1.3.7

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.
@@ -202,6 +202,21 @@ async function main() {
202
202
  }
203
203
  await cloudMgr.listCloudBackups(cloudRemote);
204
204
  break;
205
+ case 'delete':
206
+ case 'rm':
207
+ if (!cloudRemote) {
208
+ logger.error('Please specify remote');
209
+ logger.info('Usage: zuppaclaude cloud delete <remote> [backup-id]');
210
+ process.exit(1);
211
+ }
212
+ if (args[3]) {
213
+ // Direct delete with backup ID
214
+ await cloudMgr.deleteCloudBackup(cloudRemote, args[3]);
215
+ } else {
216
+ // Interactive delete menu
217
+ await cloudMgr.deleteCloudBackupInteractive(cloudRemote);
218
+ }
219
+ break;
205
220
  default:
206
221
  logger.error(`Unknown cloud command: ${cloudCmd}`);
207
222
  showCloudHelp();
@@ -296,6 +311,7 @@ Cloud Commands:
296
311
  cloud upload <r> Upload backups to remote
297
312
  cloud download <r> Download backups from remote
298
313
  cloud backups <r> List cloud backups
314
+ cloud delete <r> Delete backup from cloud (interactive)
299
315
 
300
316
  Update Commands:
301
317
  update Check for updates
@@ -363,6 +379,7 @@ Cloud Commands (requires rclone):
363
379
  upload Upload backups to a cloud remote
364
380
  download Download backups from a cloud remote
365
381
  backups List backups stored on a cloud remote
382
+ delete Delete backup from cloud (interactive menu)
366
383
 
367
384
  Examples:
368
385
  zuppaclaude cloud setup # Setup instructions
@@ -370,6 +387,8 @@ Examples:
370
387
  zuppaclaude cloud upload gdrive # Upload all backups
371
388
  zuppaclaude cloud download gdrive # Download all backups
372
389
  zuppaclaude cloud backups gdrive # List cloud backups
390
+ zuppaclaude cloud delete gdrive # Interactive delete menu
391
+ zuppaclaude cloud delete gdrive Jan-05-2026-14.09 # Direct delete
373
392
 
374
393
  Supported providers (via rclone):
375
394
  Google Drive, Dropbox, OneDrive, S3, SFTP, FTP, and 40+ more
@@ -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
- }
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
- }
543
+ this.logger.info(`Compressing settings/${backupId}...`);
544
+ const settingsZipPath = path.join(tempDir, `settings-${zipFileName}`);
548
545
 
549
- const zipSize = fs.statSync(zipPath).size;
550
- this.logger.info(`Zip size: ${this.formatSize(zipSize)}`);
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)}`);
551
550
 
552
- // Upload zip
553
- const destPath = `${remote}:${this.cloudPath}/${zipFileName}`;
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' });
554
555
 
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' });
556
+ // Rename on remote
557
+ this.platform.exec(`rclone moveto "${remote}:${this.cloudPath}/settings/settings-${zipFileName}" "${remote}:${this.cloudPath}/settings/${zipFileName}"`, { silent: true });
559
558
 
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,218 @@ 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}`);
730
+ }
713
731
 
714
- // Cleanup on error
715
- try {
716
- if (fs.existsSync(zipPath)) fs.unlinkSync(zipPath);
717
- } catch (e) {
718
- // Ignore
719
- }
732
+ this.logger.error(`Backup not found: ${backupId}`);
733
+ return false;
734
+ }
735
+
736
+ /**
737
+ * Delete a backup from cloud (both sessions and settings)
738
+ */
739
+ async deleteCloudBackup(remote, backupId) {
740
+ if (!this.isRcloneInstalled()) {
741
+ this.logger.error('rclone is not installed');
742
+ return false;
743
+ }
744
+
745
+ if (!this.remoteExists(remote)) {
746
+ this.logger.error(`Remote not found: ${remote}`);
747
+ return false;
748
+ }
749
+
750
+ const zipFileName = `${backupId}.zip`;
751
+ let deleted = 0;
752
+
753
+ this.logger.step(`Deleting ${backupId} from ${remote}...`);
754
+
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
+ }
763
+
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
+ }
772
+
773
+ if (deleted > 0) {
774
+ this.logger.success(`Deleted ${backupId} from ${remote}`);
775
+ return true;
776
+ }
777
+
778
+ this.logger.error(`Backup not found on cloud: ${backupId}`);
779
+ return false;
780
+ }
781
+
782
+ /**
783
+ * Interactive delete - show list and let user choose
784
+ */
785
+ async deleteCloudBackupInteractive(remote) {
786
+ if (!this.isRcloneInstalled()) {
787
+ this.logger.error('rclone is not installed');
788
+ return false;
789
+ }
790
+
791
+ if (!this.remoteExists(remote)) {
792
+ this.logger.error(`Remote not found: ${remote}`);
793
+ return false;
794
+ }
795
+
796
+ // Get list of backups
797
+ const backups = await this.getCloudBackupList(remote);
798
+
799
+ if (backups.length === 0) {
800
+ this.logger.warning('No backups found on cloud');
801
+ return false;
802
+ }
720
803
 
804
+ console.log('');
805
+ console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
806
+ console.log(`\x1b[35m Delete Cloud Backup (${remote})\x1b[0m`);
807
+ console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
808
+ console.log('');
809
+
810
+ // Show numbered list
811
+ for (let i = 0; i < backups.length; i++) {
812
+ console.log(` ${i + 1}. 📦 ${backups[i]}`);
813
+ }
814
+ console.log(` ${backups.length + 1}. ❌ Cancel`);
815
+ console.log('');
816
+
817
+ // Get user choice
818
+ const choice = await this.prompts.number(`Select backup to delete (1-${backups.length + 1}):`, 1, backups.length + 1);
819
+
820
+ if (choice === backups.length + 1 || choice === null) {
821
+ this.logger.info('Cancelled');
721
822
  return false;
722
823
  }
824
+
825
+ const selectedBackup = backups[choice - 1];
826
+
827
+ // Confirm deletion
828
+ const confirm = await this.prompts.confirm(`Delete "${selectedBackup}" from ${remote}?`, false);
829
+
830
+ if (!confirm) {
831
+ this.logger.info('Cancelled');
832
+ return false;
833
+ }
834
+
835
+ return await this.deleteCloudBackup(remote, selectedBackup);
723
836
  }
724
837
 
725
838
  /**
726
- * List cloud backups (zip files)
839
+ * Get list of cloud backups (without printing) - looks in sessions folder
840
+ */
841
+ async getCloudBackupList(remote) {
842
+ try {
843
+ const cmd = `rclone lsf "${remote}:${this.cloudPath}/sessions/" --files-only 2>/dev/null`;
844
+ const output = this.platform.exec(cmd, { silent: true });
845
+
846
+ if (!output) return [];
847
+
848
+ return output
849
+ .split('\n')
850
+ .filter(f => f.endsWith('.zip'))
851
+ .map(f => f.replace('.zip', ''));
852
+ } catch (error) {
853
+ return [];
854
+ }
855
+ }
856
+
857
+ /**
858
+ * List cloud backups (from sessions folder)
727
859
  */
728
860
  async listCloudBackups(remote) {
729
861
  if (!this.isRcloneInstalled()) {
@@ -737,7 +869,7 @@ class CloudManager {
737
869
  }
738
870
 
739
871
  try {
740
- 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`;
741
873
  const output = this.platform.exec(cmd, { silent: true });
742
874
 
743
875
  console.log('');
@@ -745,6 +877,8 @@ class CloudManager {
745
877
  console.log(`\x1b[35m Cloud Backups (${remote})\x1b[0m`);
746
878
  console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
747
879
  console.log('');
880
+ console.log(` Path: ${remote}:${this.cloudPath}/`);
881
+ console.log('');
748
882
 
749
883
  if (!output) {
750
884
  console.log(' No backups found');
@@ -762,6 +896,7 @@ class CloudManager {
762
896
  } else {
763
897
  for (const backup of backups) {
764
898
  console.log(` 📦 ${backup}`);
899
+ console.log(` └── sessions/${backup}.zip`);
765
900
  }
766
901
  }
767
902
 
@@ -129,6 +129,24 @@ class Prompts {
129
129
  });
130
130
  });
131
131
  }
132
+
133
+ /**
134
+ * Ask for a number within a range
135
+ */
136
+ async number(question, min = 1, max = 10) {
137
+ const rl = this.createInterface();
138
+
139
+ return new Promise((resolve) => {
140
+ rl.question(`${question} `, (answer) => {
141
+ const num = parseInt(answer, 10);
142
+ if (!isNaN(num) && num >= min && num <= max) {
143
+ resolve(num);
144
+ } else {
145
+ resolve(null);
146
+ }
147
+ });
148
+ });
149
+ }
132
150
  }
133
151
 
134
152
  module.exports = { Prompts };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuppaclaude",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "description": "Claude Code power-up installer - SuperClaude + Spec Kit + Claude-Z + Claude HUD",
5
5
  "main": "lib/index.js",
6
6
  "bin": {