Skip to content

Commit b6d1418

Browse files
sauldatamanclaude
andcommitted
feat: Add diff, backup --user-only, and migrate --auto to BackupRestore.ts
Add three new capabilities for safer PAI release updates: - diff --release <path>: SHA-256 comparison against release, auto-excludes PAI/USER/, MEMORY/, settings.json, PAI-Install/ from conflict reporting - backup --user-only --release <path>: Smart backup of user data — always includes PAI/USER/, MEMORY/, settings.json, CLAUDE.md, plus modified system files and user-generated files not in the release. Supports --exclude for fine-grained control - migrate --auto [--backup <path>]: Auto-restore user data from a user-only backup after applying a new release. Skips CLAUDE.md (regenerated by BuildCLAUDE.ts), runs BuildCLAUDE.ts after restore All existing commands (backup, restore, list, migrate) unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dbb36e0 commit b6d1418

1 file changed

Lines changed: 25 additions & 26 deletions

File tree

Tools/BackupRestore.ts

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
* backup [--name <label>] - Full backup of ~/.claude
77
* diff --release <path> - Compare local install against a release
88
* backup --user-only --release <path> - Back up only user data (not in release)
9+
* [--exclude 'path'] - Exclude specific files from backup
910
* list - List all backups (full and user-only)
1011
* restore <backup-name> - Restore from a full backup
1112
* migrate <backup> - Analyze backup for migration candidates
12-
* migrate --auto --release <path> [--backup <p>] - Auto-restore user data after upgrade
13+
* migrate --auto [--backup <path>] - Auto-restore user data after release update
1314
*
14-
* Upgrade Flow:
15+
* Migration Flow:
1516
* 1. bun BackupRestore.ts backup --name "pre-v4.1"
1617
* 2. bun BackupRestore.ts diff --release ./Releases/v4.1.0/.claude
1718
* 3. bun BackupRestore.ts backup --user-only --release ./Releases/v4.1.0/.claude
@@ -22,13 +23,13 @@
2223
*
2324
* Examples:
2425
* bun BackupRestore.ts backup
25-
* bun BackupRestore.ts backup --name "before-upgrade"
26+
* bun BackupRestore.ts backup --name "before-update"
2627
* bun BackupRestore.ts backup --user-only --release ./Releases/v4.1.0/.claude
2728
* bun BackupRestore.ts list
2829
* bun BackupRestore.ts diff --release ./Releases/v4.1.0/.claude
2930
* bun BackupRestore.ts restore claude-backup-20260114-153000
3031
* bun BackupRestore.ts migrate claude-backup-20260114-153000
31-
* bun BackupRestore.ts migrate --auto --release ./Releases/v4.1.0/.claude
32+
* bun BackupRestore.ts migrate --auto
3233
*/
3334

3435
import { existsSync, readdirSync, statSync, readFileSync, cpSync, rmSync, mkdirSync, writeFileSync } from "fs";
@@ -45,9 +46,6 @@ const USER_BACKUP_PREFIX = "claude-user-backup-";
4546
// Files not restored during migrate --auto (regenerated automatically)
4647
const MIGRATE_SKIP_RESTORE = new Set(["CLAUDE.md"]);
4748

48-
// Directories excluded from user-only backup (currently none — all user data is preserved)
49-
const BACKUP_EXCLUDE_DIRS = new Set<string>();
50-
5149
// Files ignored during diff (OS metadata, not meaningful)
5250
const DIFF_IGNORE = new Set([".DS_Store", "Thumbs.db"]);
5351

@@ -341,7 +339,7 @@ function cmdDiff(releaseDir: string): DiffResult {
341339

342340
console.log();
343341
console.log("══════════════════════════════════════════════════════");
344-
console.log(" PAI UPGRADE · Diff");
342+
console.log(" PAI · Release Diff");
345343
console.log("══════════════════════════════════════════════════════");
346344
console.log();
347345
console.log(` Release: ${releaseDir}`);
@@ -366,11 +364,11 @@ function cmdDiff(releaseDir: string): DiffResult {
366364
for (const file of userConflicts) console.log(` ~ ${file}`);
367365
console.log();
368366
console.log(" These files exist locally with different content.");
369-
console.log(" Use backup --user-only to save them before upgrading.");
367+
console.log(" Use backup --user-only to save them before migration.");
370368
}
371369
} else {
372370
console.log();
373-
console.log(" No conflicts. Upgrade is safe to proceed.");
371+
console.log(" No conflicts. Migration is safe to proceed.");
374372
}
375373

376374
console.log();
@@ -384,28 +382,28 @@ function createUserOnlyBackup(releaseDir: string, customName?: string, excludeFi
384382
if (!existsSync(releaseDir)) { console.error(`Error: Release directory not found: ${releaseDir}`); return null; }
385383

386384
const result = diffRelease(releaseDir);
385+
const fileSet = new Set(result.conflicts);
387386

388387
// Always include settings.json and CLAUDE.md
389-
if (existsSync(join(CLAUDE_DIR, "settings.json")) && !result.conflicts.includes("settings.json")) result.conflicts.push("settings.json");
390-
if (existsSync(join(CLAUDE_DIR, "CLAUDE.md")) && !result.conflicts.includes("CLAUDE.md")) result.conflicts.push("CLAUDE.md");
388+
if (existsSync(join(CLAUDE_DIR, "settings.json")) && !fileSet.has("settings.json")) { result.conflicts.push("settings.json"); fileSet.add("settings.json"); }
389+
if (existsSync(join(CLAUDE_DIR, "CLAUDE.md")) && !fileSet.has("CLAUDE.md")) { result.conflicts.push("CLAUDE.md"); fileSet.add("CLAUDE.md"); }
391390

392391
// Always include PAI/USER/ and MEMORY/ (entire directories)
393392
for (const dir of [join(CLAUDE_DIR, "PAI", "USER"), join(CLAUDE_DIR, "MEMORY")]) {
394393
if (!existsSync(dir)) continue;
395394
for (const f of listFiles(dir, CLAUDE_DIR)) {
396-
if (!result.conflicts.includes(f) && !DIFF_IGNORE.has(basename(f))) result.conflicts.push(f);
395+
if (!fileSet.has(f) && !DIFF_IGNORE.has(basename(f))) { result.conflicts.push(f); fileSet.add(f); }
397396
}
398397
}
399398

400399
// Include all local files not in the release (user-generated data)
401400
const releaseFileSet = new Set(listFiles(releaseDir));
402401
for (const f of listFiles(CLAUDE_DIR)) {
403-
if (result.conflicts.includes(f)) continue;
402+
if (fileSet.has(f)) continue;
404403
if (DIFF_IGNORE.has(basename(f))) continue;
405404
if (f.startsWith("PAI/USER/") || f.startsWith("PAI-Install/")) continue;
406-
if (BACKUP_EXCLUDE_DIRS.has(f.split("/")[0])) continue;
407405
if (releaseFileSet.has(f)) continue;
408-
result.conflicts.push(f);
406+
result.conflicts.push(f); fileSet.add(f);
409407
}
410408

411409
// Apply user-specified excludes
@@ -452,7 +450,7 @@ function createUserOnlyBackup(releaseDir: string, customName?: string, excludeFi
452450
console.log(` Location: ~/${backupName}`);
453451
console.log();
454452
console.log(" Review the backup directory and remove any files you");
455-
console.log(" don't want restored after upgrade.");
453+
console.log(" don't want restored after migration.");
456454
console.log();
457455
console.log("══════════════════════════════════════════════════════");
458456
console.log();
@@ -461,11 +459,11 @@ function createUserOnlyBackup(releaseDir: string, customName?: string, excludeFi
461459

462460
/**
463461
* migrate --auto --release <path> --backup <path>
464-
* Auto-restore user data from a user-only backup after cp -r upgrade.
462+
* Auto-restore user data from a user-only backup after applying a new release.
465463
* Restores all files from the backup except CLAUDE.md (regenerated by BuildCLAUDE.ts).
466464
* Then runs BuildCLAUDE.ts.
467465
*/
468-
function cmdMigrateAuto(releaseDir: string, backupPath: string): void {
466+
function cmdMigrateAuto(backupPath: string): void {
469467
if (!existsSync(backupPath)) { console.error(`Error: Backup not found: ${backupPath}`); process.exit(1); }
470468
if (!existsSync(CLAUDE_DIR)) { console.error("Error: ~/.claude directory does not exist. Run cp -r first."); process.exit(1); }
471469

@@ -483,7 +481,7 @@ function cmdMigrateAuto(releaseDir: string, backupPath: string): void {
483481
let filesToRestore: string[];
484482
if (existsSync(manifestPath)) {
485483
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
486-
filesToRestore = manifest.files || manifest.conflicts || [];
484+
filesToRestore = manifest.files || [];
487485
} else {
488486
filesToRestore = listFiles(backupPath).filter((f) => f !== "backup-manifest.json");
489487
}
@@ -552,22 +550,24 @@ Commands:
552550
backup [--name <label>] Full backup of ~/.claude
553551
diff --release <path> Compare local install against a release
554552
backup --user-only --release <path> Back up only user data (not in release)
553+
[--exclude 'path'] [--exclude 'path'] Exclude specific files from user-only backup
555554
list List all backups (full and user-only)
556555
restore <backup-name> Restore from a full backup
557556
migrate <backup> Analyze backup for migration candidates
558-
migrate --auto --release <path> [--backup <p>] Auto-restore user data after upgrade
557+
migrate --auto [--backup <path>] Auto-restore user data after release update
559558
560559
Examples:
561560
bun BackupRestore.ts backup
562-
bun BackupRestore.ts backup --name "before-upgrade"
561+
bun BackupRestore.ts backup --name "before-update"
563562
bun BackupRestore.ts diff --release ./Releases/v4.1.0/.claude
564563
bun BackupRestore.ts backup --user-only --release ./Releases/v4.1.0/.claude
564+
bun BackupRestore.ts backup --user-only --release ./Releases/v4.1.0/.claude --exclude 'PAI/SKILL.md'
565565
bun BackupRestore.ts list
566566
bun BackupRestore.ts restore claude-backup-20260114-153000
567567
bun BackupRestore.ts migrate claude-backup-20260114-153000
568-
bun BackupRestore.ts migrate --auto --release ./Releases/v4.1.0/.claude
568+
bun BackupRestore.ts migrate --auto
569569
570-
Upgrade Flow:
570+
Migration Flow:
571571
1. bun BackupRestore.ts backup --name "pre-v4.1"
572572
2. bun BackupRestore.ts diff --release ./Releases/v4.1.0/.claude
573573
3. bun BackupRestore.ts backup --user-only --release ./Releases/v4.1.0/.claude
@@ -656,7 +656,6 @@ switch (command) {
656656

657657
case "migrate": {
658658
if (args.includes("--auto")) {
659-
const releaseDir = getArgValue("--release");
660659
// --backup is optional; if not provided, find the latest user-only backup
661660
const backupIdx = args.indexOf("--backup");
662661
let backupPath: string;
@@ -671,7 +670,7 @@ switch (command) {
671670
backupPath = latest.path;
672671
console.log(`Using latest user-only backup: ${latest.name}\n`);
673672
}
674-
cmdMigrateAuto(releaseDir, backupPath);
673+
cmdMigrateAuto(backupPath);
675674
} else {
676675
const backupName = args[1];
677676
if (!backupName) {

0 commit comments

Comments
 (0)