zuppaclaude 1.3.3 → 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.
@@ -8,6 +8,7 @@
8
8
  const { Installer } = require('../lib/installer');
9
9
  const { Settings } = require('../lib/settings');
10
10
  const { Logger } = require('../lib/utils/logger');
11
+ const { UpdateManager } = require('../lib/components/updater');
11
12
 
12
13
  const args = process.argv.slice(2);
13
14
  const command = args[0] || 'install';
@@ -23,11 +24,26 @@ function getCloudArg(args) {
23
24
  return null;
24
25
  }
25
26
 
27
+ /**
28
+ * Check for updates at startup (non-blocking for some commands)
29
+ */
30
+ async function checkUpdates(skipCommands = ['version', 'v', '-v', '--version', 'help', 'h', '-h', '--help', 'update']) {
31
+ if (skipCommands.includes(command)) {
32
+ return;
33
+ }
34
+
35
+ const updater = new UpdateManager();
36
+ await updater.checkAndUpdate();
37
+ }
38
+
26
39
  async function main() {
27
40
  const logger = new Logger();
28
41
 
29
42
  logger.banner();
30
43
 
44
+ // Check for updates at startup
45
+ await checkUpdates();
46
+
31
47
  try {
32
48
  switch (command) {
33
49
  case 'install':
@@ -192,6 +208,37 @@ async function main() {
192
208
  }
193
209
  break;
194
210
 
211
+ case 'update':
212
+ const updateMgr = new UpdateManager();
213
+ const updateCmd = args[1] || 'check';
214
+
215
+ switch (updateCmd) {
216
+ case 'check':
217
+ await updateMgr.status();
218
+ break;
219
+ case 'now':
220
+ case 'install':
221
+ const result = await updateMgr.checkForUpdates();
222
+ if (result.hasUpdate) {
223
+ await updateMgr.update();
224
+ } else {
225
+ logger.success('You are already on the latest version');
226
+ }
227
+ break;
228
+ case 'enable':
229
+ case 'on':
230
+ updateMgr.enableAutoUpdate();
231
+ break;
232
+ case 'disable':
233
+ case 'off':
234
+ updateMgr.disableAutoUpdate();
235
+ break;
236
+ default:
237
+ logger.error(`Unknown update command: ${updateCmd}`);
238
+ showUpdateHelp();
239
+ }
240
+ break;
241
+
195
242
  case 'version':
196
243
  case 'v':
197
244
  case '-v':
@@ -233,6 +280,7 @@ Commands:
233
280
  settings, s Manage settings
234
281
  session Manage Claude Code sessions
235
282
  cloud Manage cloud remotes (rclone)
283
+ update Check for updates and manage auto-update
236
284
  version, v Show version
237
285
  help, h Show this help
238
286
 
@@ -249,6 +297,12 @@ Cloud Commands:
249
297
  cloud download <r> Download backups from remote
250
298
  cloud backups <r> List cloud backups
251
299
 
300
+ Update Commands:
301
+ update Check for updates
302
+ update now Update to latest version
303
+ update enable Enable auto-update (default)
304
+ update disable Disable auto-update
305
+
252
306
  Session Commands:
253
307
  session list List all sessions
254
308
  session backup Backup sessions only
@@ -262,6 +316,7 @@ Examples:
262
316
  npx zuppaclaude backup --cloud gdrive # Backup to Google Drive
263
317
  npx zuppaclaude restore 2026-01-05T12-00-00 # Restore from backup
264
318
  npx zuppaclaude cloud setup # Configure cloud
319
+ npx zuppaclaude update # Check for updates
265
320
  `);
266
321
  }
267
322
 
@@ -321,4 +376,20 @@ Supported providers (via rclone):
321
376
  `);
322
377
  }
323
378
 
379
+ function showUpdateHelp() {
380
+ console.log(`
381
+ Update Commands:
382
+ check Check for updates (default)
383
+ now Update to latest version immediately
384
+ enable Enable auto-update at startup
385
+ disable Disable auto-update
386
+
387
+ Examples:
388
+ zuppaclaude update # Check for updates
389
+ zuppaclaude update now # Update immediately
390
+ zuppaclaude update enable # Enable auto-update
391
+ zuppaclaude update disable # Disable auto-update
392
+ `);
393
+ }
394
+
324
395
  main();
@@ -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);
427
521
  try {
428
- // Use rclone sync for efficiency
429
- const cmd = `rclone sync "${sourcePath}" "${destPath}" --progress`;
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
- 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,7 +652,78 @@ class CloudManager {
483
652
  }
484
653
 
485
654
  /**
486
- * List cloud backups
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 lsd "${remote}:${this.cloudPath}" 2>/dev/null`;
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
- this.logger.warning('No cloud backups found');
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(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/));
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');
@@ -11,6 +11,7 @@ const { SessionManager } = require('./session');
11
11
  const { CloudManager } = require('./cloud');
12
12
  const { BackupManager } = require('./backup');
13
13
  const { CommandsInstaller } = require('./commands');
14
+ const { UpdateManager } = require('./updater');
14
15
 
15
16
  module.exports = {
16
17
  SuperClaudeInstaller,
@@ -21,5 +22,6 @@ module.exports = {
21
22
  SessionManager,
22
23
  CloudManager,
23
24
  BackupManager,
24
- CommandsInstaller
25
+ CommandsInstaller,
26
+ UpdateManager
25
27
  };
@@ -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;
@@ -0,0 +1,216 @@
1
+ /**
2
+ * Auto-Update Manager
3
+ * Checks for updates and auto-updates if enabled
4
+ */
5
+
6
+ const { execSync } = require('child_process');
7
+ const https = require('https');
8
+ const { Logger } = require('../utils/logger');
9
+ const { Settings } = require('../settings');
10
+
11
+ class UpdateManager {
12
+ constructor() {
13
+ this.logger = new Logger();
14
+ this.settings = new Settings();
15
+ this.packageName = 'zuppaclaude';
16
+ this.currentVersion = require('../../package.json').version;
17
+ }
18
+
19
+ /**
20
+ * Get latest version from npm
21
+ */
22
+ async getLatestVersion() {
23
+ return new Promise((resolve, reject) => {
24
+ const url = `https://registry.npmjs.org/${this.packageName}/latest`;
25
+
26
+ https.get(url, (res) => {
27
+ let data = '';
28
+
29
+ res.on('data', (chunk) => {
30
+ data += chunk;
31
+ });
32
+
33
+ res.on('end', () => {
34
+ try {
35
+ const json = JSON.parse(data);
36
+ resolve(json.version);
37
+ } catch (e) {
38
+ reject(e);
39
+ }
40
+ });
41
+ }).on('error', (err) => {
42
+ reject(err);
43
+ });
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Compare versions (returns true if latest > current)
49
+ */
50
+ isNewerVersion(latest, current) {
51
+ const latestParts = latest.split('.').map(Number);
52
+ const currentParts = current.split('.').map(Number);
53
+
54
+ for (let i = 0; i < 3; i++) {
55
+ if (latestParts[i] > currentParts[i]) return true;
56
+ if (latestParts[i] < currentParts[i]) return false;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ /**
62
+ * Check for updates
63
+ */
64
+ async checkForUpdates(silent = false) {
65
+ try {
66
+ const latestVersion = await this.getLatestVersion();
67
+ const hasUpdate = this.isNewerVersion(latestVersion, this.currentVersion);
68
+
69
+ if (hasUpdate && !silent) {
70
+ console.log('');
71
+ console.log('\x1b[33m╔═══════════════════════════════════════════════════════════════════╗\x1b[0m');
72
+ console.log('\x1b[33m║ Update Available! ║\x1b[0m');
73
+ console.log('\x1b[33m╚═══════════════════════════════════════════════════════════════════╝\x1b[0m');
74
+ console.log('');
75
+ console.log(` Current version: \x1b[31m${this.currentVersion}\x1b[0m`);
76
+ console.log(` Latest version: \x1b[32m${latestVersion}\x1b[0m`);
77
+ console.log('');
78
+ }
79
+
80
+ return {
81
+ hasUpdate,
82
+ currentVersion: this.currentVersion,
83
+ latestVersion
84
+ };
85
+ } catch (error) {
86
+ // Silently fail - don't interrupt user workflow
87
+ return {
88
+ hasUpdate: false,
89
+ currentVersion: this.currentVersion,
90
+ latestVersion: null,
91
+ error: error.message
92
+ };
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Update to latest version
98
+ */
99
+ async update() {
100
+ try {
101
+ this.logger.step('Updating ZuppaClaude...');
102
+
103
+ // Use npm to update globally
104
+ execSync(`npm install -g ${this.packageName}@latest`, {
105
+ stdio: 'inherit'
106
+ });
107
+
108
+ // Verify update
109
+ const newVersion = await this.getLatestVersion();
110
+ this.logger.success(`Updated to v${newVersion}`);
111
+
112
+ return true;
113
+ } catch (error) {
114
+ this.logger.error(`Update failed: ${error.message}`);
115
+ this.logger.info(`Manual update: npm install -g ${this.packageName}@latest`);
116
+ return false;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Check and auto-update if enabled
122
+ * Called at startup
123
+ */
124
+ async checkAndUpdate() {
125
+ const userSettings = this.settings.load() || {};
126
+ const autoUpdate = userSettings.autoUpdate !== false; // Default true
127
+
128
+ const result = await this.checkForUpdates(true);
129
+
130
+ if (result.hasUpdate) {
131
+ if (autoUpdate) {
132
+ console.log('');
133
+ console.log(`\x1b[36m[i]\x1b[0m New version available: v${result.latestVersion}`);
134
+ console.log('\x1b[36m[i]\x1b[0m Auto-updating...');
135
+ console.log('');
136
+
137
+ const updated = await this.update();
138
+
139
+ if (updated) {
140
+ console.log('');
141
+ console.log('\x1b[32m[✓]\x1b[0m Update complete! Please restart to use the new version.');
142
+ console.log('');
143
+ return { updated: true, version: result.latestVersion };
144
+ }
145
+ } else {
146
+ // Just notify
147
+ console.log('');
148
+ console.log(`\x1b[33m[!]\x1b[0m Update available: v${this.currentVersion} → v${result.latestVersion}`);
149
+ console.log(`\x1b[33m[!]\x1b[0m Run: npm install -g ${this.packageName}@latest`);
150
+ console.log('');
151
+ return { updated: false, version: result.latestVersion };
152
+ }
153
+ }
154
+
155
+ return { updated: false, version: this.currentVersion };
156
+ }
157
+
158
+ /**
159
+ * Enable auto-update
160
+ */
161
+ enableAutoUpdate() {
162
+ const userSettings = this.settings.load() || {};
163
+ userSettings.autoUpdate = true;
164
+ this.settings.save(userSettings);
165
+ this.logger.success('Auto-update enabled');
166
+ }
167
+
168
+ /**
169
+ * Disable auto-update
170
+ */
171
+ disableAutoUpdate() {
172
+ const userSettings = this.settings.load() || {};
173
+ userSettings.autoUpdate = false;
174
+ this.settings.save(userSettings);
175
+ this.logger.success('Auto-update disabled');
176
+ }
177
+
178
+ /**
179
+ * Show update status
180
+ */
181
+ async status() {
182
+ const userSettings = this.settings.load() || {};
183
+ const autoUpdate = userSettings.autoUpdate !== false;
184
+
185
+ console.log('');
186
+ console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
187
+ console.log('\x1b[35m Update Status\x1b[0m');
188
+ console.log('\x1b[35m═══════════════════════════════════════════════════════════════════\x1b[0m');
189
+ console.log('');
190
+ console.log(` Current version: \x1b[36m${this.currentVersion}\x1b[0m`);
191
+ console.log(` Auto-update: ${autoUpdate ? '\x1b[32menabled\x1b[0m' : '\x1b[31mdisabled\x1b[0m'}`);
192
+ console.log('');
193
+
194
+ this.logger.info('Checking for updates...');
195
+ const result = await this.checkForUpdates(true);
196
+
197
+ if (result.latestVersion) {
198
+ console.log(` Latest version: \x1b[36m${result.latestVersion}\x1b[0m`);
199
+
200
+ if (result.hasUpdate) {
201
+ console.log('');
202
+ console.log(' \x1b[33mUpdate available!\x1b[0m');
203
+ console.log(` Run: \x1b[36mnpm install -g ${this.packageName}@latest\x1b[0m`);
204
+ } else {
205
+ console.log('');
206
+ console.log(' \x1b[32mYou are up to date!\x1b[0m');
207
+ }
208
+ } else {
209
+ console.log(' \x1b[31mCould not check for updates\x1b[0m');
210
+ }
211
+
212
+ console.log('');
213
+ }
214
+ }
215
+
216
+ module.exports = { UpdateManager };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zuppaclaude",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "Claude Code power-up installer - SuperClaude + Spec Kit + Claude-Z + Claude HUD",
5
5
  "main": "lib/index.js",
6
6
  "bin": {