zuppaclaude 1.3.4 → 1.3.6

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
@@ -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 the same directory
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 settingsBackupPath = path.join(sessionResult.path, 'zc-settings.json');
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('Settings backed up');
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 backupPath = path.join(this.backupDir, backupId);
126
+ const sessionsBackupPath = path.join(this.backupDir, 'sessions', backupId);
127
+ const settingsBackupDir = path.join(this.backupDir, 'settings', backupId);
123
128
 
124
- if (!fs.existsSync(backupPath)) {
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(backupPath, 'zc-settings.json');
150
+ const settingsBackupPath = path.join(settingsBackupDir, 'zc-settings.json');
146
151
 
147
152
  if (fs.existsSync(settingsBackupPath)) {
148
153
  try {
@@ -386,7 +386,57 @@ class CloudManager {
386
386
  }
387
387
 
388
388
  /**
389
- * Upload backup to cloud
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
- // Determine what to upload
405
- let sourcePath = this.backupDir;
406
- let destPath = `${remote}:${this.cloudPath}`;
407
-
454
+ // Upload specific backup or all backups
408
455
  if (backupId) {
409
- sourcePath = path.join(this.backupDir, backupId);
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
- if (!fs.existsSync(sourcePath)) {
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);
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 });
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
+
427
555
  try {
428
- // Use rclone sync for efficiency
429
- const cmd = `rclone sync "${sourcePath}" "${destPath}" --progress`;
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
- console.log('');
433
- this.logger.success(`Backup uploaded to ${remote}:${this.cloudPath}`);
434
- console.log('');
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
- * Download backup from cloud
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
- let sourcePath = `${remote}:${this.cloudPath}`;
458
- let destPath = this.backupDir;
459
-
610
+ // Download specific backup or all backups
460
611
  if (backupId) {
461
- sourcePath = `${remote}:${this.cloudPath}/${backupId}`;
462
- destPath = path.join(this.backupDir, backupId);
612
+ return await this.downloadSingleBackup(remote, backupId);
463
613
  }
464
614
 
465
- this.platform.ensureDir(destPath);
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
- const cmd = `rclone sync "${sourcePath}" "${destPath}" --progress`;
473
- this.platform.exec(cmd, { silent: false, stdio: 'inherit' });
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(`Backup downloaded from ${remote}`);
645
+ this.logger.success(`Downloaded ${downloaded} backup(s) from ${remote}`);
477
646
  console.log('');
478
647
  return true;
479
648
  } catch (error) {
@@ -483,37 +652,208 @@ class CloudManager {
483
652
  }
484
653
 
485
654
  /**
486
- * List cloud backups
655
+ * Download and extract a single backup
487
656
  */
488
- async listCloudBackups(remote) {
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
+ * Delete a backup from cloud
727
+ */
728
+ async deleteCloudBackup(remote, backupId) {
489
729
  if (!this.isRcloneInstalled()) {
490
730
  this.logger.error('rclone is not installed');
491
- return [];
731
+ return false;
492
732
  }
493
733
 
494
734
  if (!this.remoteExists(remote)) {
495
735
  this.logger.error(`Remote not found: ${remote}`);
496
- return [];
736
+ return false;
497
737
  }
498
738
 
739
+ const zipFileName = `${backupId}.zip`;
740
+
499
741
  try {
500
- const cmd = `rclone lsd "${remote}:${this.cloudPath}" 2>/dev/null`;
742
+ // Check if backup exists
743
+ const cmd = `rclone lsf "${remote}:${this.cloudPath}/${zipFileName}" 2>/dev/null`;
501
744
  const output = this.platform.exec(cmd, { silent: true });
502
745
 
503
- if (!output) {
504
- this.logger.warning('No cloud backups found');
505
- return [];
746
+ if (!output || !output.trim()) {
747
+ this.logger.error(`Backup not found on cloud: ${backupId}`);
748
+ return false;
506
749
  }
507
750
 
508
- const backups = output
751
+ this.logger.step(`Deleting ${zipFileName} from ${remote}...`);
752
+
753
+ // Delete the backup
754
+ const deleteCmd = `rclone delete "${remote}:${this.cloudPath}/${zipFileName}"`;
755
+ this.platform.exec(deleteCmd, { silent: true });
756
+
757
+ this.logger.success(`Deleted ${backupId} from ${remote}`);
758
+ return true;
759
+ } catch (error) {
760
+ this.logger.error(`Failed to delete: ${error.message}`);
761
+ return false;
762
+ }
763
+ }
764
+
765
+ /**
766
+ * Interactive delete - show list and let user choose
767
+ */
768
+ async deleteCloudBackupInteractive(remote) {
769
+ if (!this.isRcloneInstalled()) {
770
+ this.logger.error('rclone is not installed');
771
+ return false;
772
+ }
773
+
774
+ if (!this.remoteExists(remote)) {
775
+ this.logger.error(`Remote not found: ${remote}`);
776
+ return false;
777
+ }
778
+
779
+ // Get list of backups
780
+ const backups = await this.getCloudBackupList(remote);
781
+
782
+ if (backups.length === 0) {
783
+ this.logger.warning('No backups found on cloud');
784
+ return false;
785
+ }
786
+
787
+ console.log('');
788
+ console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
789
+ console.log(`\x1b[35m Delete Cloud Backup (${remote})\x1b[0m`);
790
+ console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
791
+ console.log('');
792
+
793
+ // Show numbered list
794
+ for (let i = 0; i < backups.length; i++) {
795
+ console.log(` ${i + 1}. 📦 ${backups[i]}`);
796
+ }
797
+ console.log(` ${backups.length + 1}. ❌ Cancel`);
798
+ console.log('');
799
+
800
+ // Get user choice
801
+ const choice = await this.prompts.number(`Select backup to delete (1-${backups.length + 1}):`, 1, backups.length + 1);
802
+
803
+ if (choice === backups.length + 1 || choice === null) {
804
+ this.logger.info('Cancelled');
805
+ return false;
806
+ }
807
+
808
+ const selectedBackup = backups[choice - 1];
809
+
810
+ // Confirm deletion
811
+ const confirm = await this.prompts.confirm(`Delete "${selectedBackup}" from ${remote}?`, false);
812
+
813
+ if (!confirm) {
814
+ this.logger.info('Cancelled');
815
+ return false;
816
+ }
817
+
818
+ return await this.deleteCloudBackup(remote, selectedBackup);
819
+ }
820
+
821
+ /**
822
+ * Get list of cloud backups (without printing)
823
+ */
824
+ async getCloudBackupList(remote) {
825
+ try {
826
+ const cmd = `rclone lsf "${remote}:${this.cloudPath}/" --files-only 2>/dev/null`;
827
+ const output = this.platform.exec(cmd, { silent: true });
828
+
829
+ if (!output) return [];
830
+
831
+ return output
509
832
  .split('\n')
510
- .filter(line => line.trim())
511
- .map(line => {
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/));
833
+ .filter(f => f.endsWith('.zip'))
834
+ .map(f => f.replace('.zip', ''));
835
+ } catch (error) {
836
+ return [];
837
+ }
838
+ }
839
+
840
+ /**
841
+ * List cloud backups (zip files)
842
+ */
843
+ async listCloudBackups(remote) {
844
+ if (!this.isRcloneInstalled()) {
845
+ this.logger.error('rclone is not installed');
846
+ return [];
847
+ }
848
+
849
+ if (!this.remoteExists(remote)) {
850
+ this.logger.error(`Remote not found: ${remote}`);
851
+ return [];
852
+ }
853
+
854
+ try {
855
+ const cmd = `rclone lsf "${remote}:${this.cloudPath}/" --files-only 2>/dev/null`;
856
+ const output = this.platform.exec(cmd, { silent: true });
517
857
 
518
858
  console.log('');
519
859
  console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
@@ -521,6 +861,17 @@ class CloudManager {
521
861
  console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
522
862
  console.log('');
523
863
 
864
+ if (!output) {
865
+ console.log(' No backups found');
866
+ console.log('');
867
+ return [];
868
+ }
869
+
870
+ const backups = output
871
+ .split('\n')
872
+ .filter(f => f.endsWith('.zip'))
873
+ .map(f => f.replace('.zip', ''));
874
+
524
875
  if (backups.length === 0) {
525
876
  console.log(' No backups found');
526
877
  } else {
@@ -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 = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
135
- const backupPath = path.join(this.backupDir, timestamp);
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: new Date().toISOString(),
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(backupPath, 'history.jsonl'));
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(backupPath, 'manifest.json'),
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: ${backupPath}`);
225
+ this.logger.info(`Location: ${sessionsBackupPath}`);
211
226
  console.log('');
212
227
 
213
228
  return {
214
- path: backupPath,
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
- if (!fs.existsSync(this.backupDir)) {
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(this.backupDir)
247
+ const backups = fs.readdirSync(sessionsDir)
231
248
  .filter(name => {
232
- const backupPath = path.join(this.backupDir, name);
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(this.backupDir, name);
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
- const projectBackupPath = path.join(backupPath, 'sessions', project.id);
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;
@@ -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.4",
3
+ "version": "1.3.6",
4
4
  "description": "Claude Code power-up installer - SuperClaude + Spec Kit + Claude-Z + Claude HUD",
5
5
  "main": "lib/index.js",
6
6
  "bin": {