claude-nomad 0.66.0 → 0.67.0
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.
- package/CHANGELOG.md +33 -0
- package/dist/nomad.mjs +197 -129
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.67.0](https://github.com/funkadelic/claude-nomad/compare/v0.66.0...v0.67.0) (2026-08-18)
|
|
4
|
+
|
|
5
|
+
### What's new
|
|
6
|
+
|
|
7
|
+
**On all supported platforms, i.e. \*nix, Mac, WSL2, and native Windows.**
|
|
8
|
+
|
|
9
|
+
- **Your backup cache no longer collects empty folders.** Every sync saves a copy of anything it is
|
|
10
|
+
about to overwrite into a dated folder under `~/.cache/claude-nomad/backup/`, and it creates that
|
|
11
|
+
folder before it touches your files so a full disk or a permissions problem stops the sync before
|
|
12
|
+
it has changed anything. A sync that turns out to have nothing to save now clears its own folder
|
|
13
|
+
instead of leaving an empty one behind, and so does one that stops early.
|
|
14
|
+
- **`nomad clean --backups` removes empty backup folders whatever their age.** They hold nothing to
|
|
15
|
+
recover, so neither the 14-day default nor `--keep <N>` keeps one alive, and they no longer use up
|
|
16
|
+
the slots `--keep <N>` reserves: ask for the five newest and you get the five newest that actually
|
|
17
|
+
hold something. Backups holding anything at all, a file or even a link, are left exactly as
|
|
18
|
+
before. `nomad clean --backups --dry-run` always prints its preview, since it changes nothing and
|
|
19
|
+
never waits on another command.
|
|
20
|
+
- **`nomad adopt` no longer runs at the same time as a pull, push, or sync.** It waits its turn the
|
|
21
|
+
way those commands already wait for each other, so a cleanup or a sync running in another terminal
|
|
22
|
+
cannot remove or overwrite a backup that adopt is still writing. If another one is already
|
|
23
|
+
running, adopt says so and does nothing rather than starting a move it cannot finish safely.
|
|
24
|
+
Previewing with `nomad adopt <name> --dry-run` never waits, since it only reads and prints.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
* clean up the empty folders in your backup cache ([#525](https://github.com/funkadelic/claude-nomad/issues/525)) ([3f5ae92](https://github.com/funkadelic/claude-nomad/commit/3f5ae92e46173364f5e9c66e4963bcc716c813e6))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
* **ci:** base the drift branch on main on a dispatch run ([#524](https://github.com/funkadelic/claude-nomad/issues/524)) ([3e94c1e](https://github.com/funkadelic/claude-nomad/commit/3e94c1e2df9296a539c71eac873058cb69225d78))
|
|
35
|
+
|
|
3
36
|
## [0.66.0](https://github.com/funkadelic/claude-nomad/compare/v0.65.0...v0.66.0) (2026-08-17)
|
|
4
37
|
|
|
5
38
|
### What's new
|
package/dist/nomad.mjs
CHANGED
|
@@ -807,6 +807,7 @@ import {
|
|
|
807
807
|
mkdirSync,
|
|
808
808
|
openSync,
|
|
809
809
|
renameSync,
|
|
810
|
+
rmdirSync,
|
|
810
811
|
statSync,
|
|
811
812
|
symlinkSync,
|
|
812
813
|
writeFileSync
|
|
@@ -867,6 +868,12 @@ function freshBackupTs(backupRoot) {
|
|
|
867
868
|
while (existsSync3(join5(backupRoot, `${base}-${n}`))) n++;
|
|
868
869
|
return `${base}-${n}`;
|
|
869
870
|
}
|
|
871
|
+
function discardEmptyBackupDir(backupRoot) {
|
|
872
|
+
try {
|
|
873
|
+
rmdirSync(backupRoot);
|
|
874
|
+
} catch {
|
|
875
|
+
}
|
|
876
|
+
}
|
|
870
877
|
function ensureSymlink(linkPath, target) {
|
|
871
878
|
if (existsSync3(linkPath)) {
|
|
872
879
|
if (lstatSync4(linkPath).isSymbolicLink()) return;
|
|
@@ -1549,7 +1556,7 @@ var init_push_leak_verdict = __esm({
|
|
|
1549
1556
|
|
|
1550
1557
|
// src/commands.adopt.ts
|
|
1551
1558
|
import { existsSync as existsSync5, lstatSync as lstatSync7 } from "node:fs";
|
|
1552
|
-
import { join as
|
|
1559
|
+
import { join as join9 } from "node:path";
|
|
1553
1560
|
|
|
1554
1561
|
// src/commands.adopt.recover.ts
|
|
1555
1562
|
import { lstatSync as lstatSync6, rmSync as rmSync4 } from "node:fs";
|
|
@@ -2460,10 +2467,125 @@ init_config_sharedDirs_guard();
|
|
|
2460
2467
|
init_exit_codes();
|
|
2461
2468
|
init_utils();
|
|
2462
2469
|
init_utils_fs();
|
|
2470
|
+
|
|
2471
|
+
// src/utils.lockfile.ts
|
|
2472
|
+
init_config();
|
|
2473
|
+
init_utils();
|
|
2474
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2475
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
2476
|
+
function lockFilePath() {
|
|
2477
|
+
return join8(home(), ".cache", "claude-nomad", "nomad.lock");
|
|
2478
|
+
}
|
|
2479
|
+
function acquireLock(verb) {
|
|
2480
|
+
const lp = lockFilePath();
|
|
2481
|
+
mkdirSync2(dirname3(lp), { recursive: true });
|
|
2482
|
+
try {
|
|
2483
|
+
const fd = openSync2(lp, "wx");
|
|
2484
|
+
try {
|
|
2485
|
+
writeFileSync2(fd, String(process.pid));
|
|
2486
|
+
} catch (writeErr) {
|
|
2487
|
+
try {
|
|
2488
|
+
closeSync2(fd);
|
|
2489
|
+
} catch {
|
|
2490
|
+
}
|
|
2491
|
+
try {
|
|
2492
|
+
unlinkSync(lp);
|
|
2493
|
+
} catch {
|
|
2494
|
+
}
|
|
2495
|
+
throw writeErr;
|
|
2496
|
+
}
|
|
2497
|
+
return { fd, path: lp };
|
|
2498
|
+
} catch (err) {
|
|
2499
|
+
const code = err.code;
|
|
2500
|
+
if (code !== "EEXIST") throw err;
|
|
2501
|
+
return checkStaleAndRetry(verb, lp);
|
|
2502
|
+
}
|
|
2503
|
+
}
|
|
2504
|
+
function releaseLock(handle) {
|
|
2505
|
+
if (handle === null) return;
|
|
2506
|
+
const lp = handle.path;
|
|
2507
|
+
try {
|
|
2508
|
+
closeSync2(handle.fd);
|
|
2509
|
+
} catch {
|
|
2510
|
+
}
|
|
2511
|
+
try {
|
|
2512
|
+
unlinkSync(lp);
|
|
2513
|
+
} catch (err) {
|
|
2514
|
+
if (err.code !== "ENOENT") throw err;
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
function unlinkIfSamePid(expectedPidStr, lp) {
|
|
2518
|
+
let current;
|
|
2519
|
+
try {
|
|
2520
|
+
current = readFileSync3(lp, "utf8").trim();
|
|
2521
|
+
} catch {
|
|
2522
|
+
return false;
|
|
2523
|
+
}
|
|
2524
|
+
if (current !== expectedPidStr) return false;
|
|
2525
|
+
try {
|
|
2526
|
+
unlinkSync(lp);
|
|
2527
|
+
return true;
|
|
2528
|
+
} catch {
|
|
2529
|
+
return false;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
function checkStaleAndRetry(verb, lp) {
|
|
2533
|
+
let pidStr;
|
|
2534
|
+
try {
|
|
2535
|
+
pidStr = readFileSync3(lp, "utf8").trim();
|
|
2536
|
+
} catch {
|
|
2537
|
+
pidStr = "";
|
|
2538
|
+
}
|
|
2539
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
2540
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
2541
|
+
if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
|
|
2542
|
+
warn(`another nomad ${verb} running, skipping`);
|
|
2543
|
+
return null;
|
|
2544
|
+
}
|
|
2545
|
+
try {
|
|
2546
|
+
process.kill(pid, 0);
|
|
2547
|
+
warn(`another nomad ${verb} running, skipping`);
|
|
2548
|
+
return null;
|
|
2549
|
+
} catch (err) {
|
|
2550
|
+
const code = err.code;
|
|
2551
|
+
if (code === "ESRCH") {
|
|
2552
|
+
if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
|
|
2553
|
+
warn(`another nomad ${verb} running, skipping`);
|
|
2554
|
+
return null;
|
|
2555
|
+
}
|
|
2556
|
+
warn(`another nomad ${verb} running, skipping`);
|
|
2557
|
+
return null;
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
function retryOnce(verb, lp) {
|
|
2561
|
+
try {
|
|
2562
|
+
const fd = openSync2(lp, "wx");
|
|
2563
|
+
try {
|
|
2564
|
+
writeFileSync2(fd, String(process.pid));
|
|
2565
|
+
} catch {
|
|
2566
|
+
try {
|
|
2567
|
+
closeSync2(fd);
|
|
2568
|
+
} catch {
|
|
2569
|
+
}
|
|
2570
|
+
try {
|
|
2571
|
+
unlinkSync(lp);
|
|
2572
|
+
} catch {
|
|
2573
|
+
}
|
|
2574
|
+
warn(`another nomad ${verb} running, skipping`);
|
|
2575
|
+
return null;
|
|
2576
|
+
}
|
|
2577
|
+
return { fd, path: lp };
|
|
2578
|
+
} catch {
|
|
2579
|
+
warn(`another nomad ${verb} running, skipping`);
|
|
2580
|
+
return null;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// src/commands.adopt.ts
|
|
2463
2585
|
init_utils_json();
|
|
2464
2586
|
var ADOPT_PUSH_HINT = "run `nomad push` to share with other hosts";
|
|
2465
2587
|
function readMapIfPresent(repoHome2) {
|
|
2466
|
-
const mapPath =
|
|
2588
|
+
const mapPath = join9(repoHome2, "path-map.json");
|
|
2467
2589
|
return existsSync5(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
2468
2590
|
}
|
|
2469
2591
|
function isConfiguredTarget(name, map) {
|
|
@@ -2498,7 +2620,7 @@ function adoptStopsEarly(name, linkPath, sharedTarget) {
|
|
|
2498
2620
|
function performAdoptMove(name, linkPath, sharedTarget, repo, backup) {
|
|
2499
2621
|
const ts = freshBackupTs(backup);
|
|
2500
2622
|
const snapshotted = backupBeforeWrite(linkPath, ts);
|
|
2501
|
-
const rel =
|
|
2623
|
+
const rel = join9("shared", name);
|
|
2502
2624
|
const stage = () => gitOrFatal(["add", "--", rel], `git add shared/${name}`, repo);
|
|
2503
2625
|
copyIntoSharedOrFatal(name, linkPath, sharedTarget, repo);
|
|
2504
2626
|
refuseLateDeniedEntries(name, linkPath, sharedTarget, repo, { snapshotted, ts });
|
|
@@ -2538,8 +2660,21 @@ function cmdAdopt(name, opts = {}) {
|
|
|
2538
2660
|
);
|
|
2539
2661
|
process.exit(1);
|
|
2540
2662
|
}
|
|
2541
|
-
const linkPath =
|
|
2542
|
-
const sharedTarget =
|
|
2663
|
+
const linkPath = join9(claude, name);
|
|
2664
|
+
const sharedTarget = join9(repo, "shared", name);
|
|
2665
|
+
if (dryRun) {
|
|
2666
|
+
adoptPreflightAndMove(name, linkPath, sharedTarget, repo, backup, true);
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
const handle = acquireLock("adopt");
|
|
2670
|
+
if (handle === null) process.exit(EXIT.SUCCESS);
|
|
2671
|
+
try {
|
|
2672
|
+
adoptPreflightAndMove(name, linkPath, sharedTarget, repo, backup, false);
|
|
2673
|
+
} finally {
|
|
2674
|
+
releaseLock(handle);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
function adoptPreflightAndMove(name, linkPath, sharedTarget, repo, backup, dryRun) {
|
|
2543
2678
|
if (adoptStopsEarly(name, linkPath, sharedTarget)) return;
|
|
2544
2679
|
refuseDeniedEntries(name, linkPath);
|
|
2545
2680
|
if (dryRun) {
|
|
@@ -2566,8 +2701,8 @@ init_config();
|
|
|
2566
2701
|
import { existsSync as existsSync6 } from "node:fs";
|
|
2567
2702
|
|
|
2568
2703
|
// src/commands.redact.core.ts
|
|
2569
|
-
import { appendFileSync, readFileSync as
|
|
2570
|
-
import { join as
|
|
2704
|
+
import { appendFileSync, readFileSync as readFileSync4 } from "node:fs";
|
|
2705
|
+
import { join as join10 } from "node:path";
|
|
2571
2706
|
function collectMatchIntervals(content, findings) {
|
|
2572
2707
|
const intervals = [];
|
|
2573
2708
|
for (const f of findings) {
|
|
@@ -2625,10 +2760,10 @@ function isAlreadyPresent(line, lines) {
|
|
|
2625
2760
|
function appendGitleaksIgnore(fingerprint, repo) {
|
|
2626
2761
|
const sanitized = fingerprint.replace(/[\r\n]/g, "").trim();
|
|
2627
2762
|
if (sanitized.length === 0) return;
|
|
2628
|
-
const ignPath =
|
|
2763
|
+
const ignPath = join10(repo, ".gitleaksignore");
|
|
2629
2764
|
let raw;
|
|
2630
2765
|
try {
|
|
2631
|
-
raw =
|
|
2766
|
+
raw = readFileSync4(ignPath, "utf8");
|
|
2632
2767
|
} catch {
|
|
2633
2768
|
raw = "";
|
|
2634
2769
|
}
|
|
@@ -2666,121 +2801,6 @@ import { join as join11 } from "node:path";
|
|
|
2666
2801
|
import { createInterface } from "node:readline/promises";
|
|
2667
2802
|
init_utils_fs();
|
|
2668
2803
|
init_utils_json();
|
|
2669
|
-
|
|
2670
|
-
// src/utils.lockfile.ts
|
|
2671
|
-
init_config();
|
|
2672
|
-
init_utils();
|
|
2673
|
-
import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2674
|
-
import { dirname as dirname3, join as join10 } from "node:path";
|
|
2675
|
-
function lockFilePath() {
|
|
2676
|
-
return join10(home(), ".cache", "claude-nomad", "nomad.lock");
|
|
2677
|
-
}
|
|
2678
|
-
function acquireLock(verb) {
|
|
2679
|
-
const lp = lockFilePath();
|
|
2680
|
-
mkdirSync2(dirname3(lp), { recursive: true });
|
|
2681
|
-
try {
|
|
2682
|
-
const fd = openSync2(lp, "wx");
|
|
2683
|
-
try {
|
|
2684
|
-
writeFileSync2(fd, String(process.pid));
|
|
2685
|
-
} catch (writeErr) {
|
|
2686
|
-
try {
|
|
2687
|
-
closeSync2(fd);
|
|
2688
|
-
} catch {
|
|
2689
|
-
}
|
|
2690
|
-
try {
|
|
2691
|
-
unlinkSync(lp);
|
|
2692
|
-
} catch {
|
|
2693
|
-
}
|
|
2694
|
-
throw writeErr;
|
|
2695
|
-
}
|
|
2696
|
-
return { fd, path: lp };
|
|
2697
|
-
} catch (err) {
|
|
2698
|
-
const code = err.code;
|
|
2699
|
-
if (code !== "EEXIST") throw err;
|
|
2700
|
-
return checkStaleAndRetry(verb, lp);
|
|
2701
|
-
}
|
|
2702
|
-
}
|
|
2703
|
-
function releaseLock(handle) {
|
|
2704
|
-
if (handle === null) return;
|
|
2705
|
-
const lp = handle.path;
|
|
2706
|
-
try {
|
|
2707
|
-
closeSync2(handle.fd);
|
|
2708
|
-
} catch {
|
|
2709
|
-
}
|
|
2710
|
-
try {
|
|
2711
|
-
unlinkSync(lp);
|
|
2712
|
-
} catch (err) {
|
|
2713
|
-
if (err.code !== "ENOENT") throw err;
|
|
2714
|
-
}
|
|
2715
|
-
}
|
|
2716
|
-
function unlinkIfSamePid(expectedPidStr, lp) {
|
|
2717
|
-
let current;
|
|
2718
|
-
try {
|
|
2719
|
-
current = readFileSync4(lp, "utf8").trim();
|
|
2720
|
-
} catch {
|
|
2721
|
-
return false;
|
|
2722
|
-
}
|
|
2723
|
-
if (current !== expectedPidStr) return false;
|
|
2724
|
-
try {
|
|
2725
|
-
unlinkSync(lp);
|
|
2726
|
-
return true;
|
|
2727
|
-
} catch {
|
|
2728
|
-
return false;
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
function checkStaleAndRetry(verb, lp) {
|
|
2732
|
-
let pidStr;
|
|
2733
|
-
try {
|
|
2734
|
-
pidStr = readFileSync4(lp, "utf8").trim();
|
|
2735
|
-
} catch {
|
|
2736
|
-
pidStr = "";
|
|
2737
|
-
}
|
|
2738
|
-
const pid = Number.parseInt(pidStr, 10);
|
|
2739
|
-
if (!Number.isFinite(pid) || pid <= 0) {
|
|
2740
|
-
if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
|
|
2741
|
-
warn(`another nomad ${verb} running, skipping`);
|
|
2742
|
-
return null;
|
|
2743
|
-
}
|
|
2744
|
-
try {
|
|
2745
|
-
process.kill(pid, 0);
|
|
2746
|
-
warn(`another nomad ${verb} running, skipping`);
|
|
2747
|
-
return null;
|
|
2748
|
-
} catch (err) {
|
|
2749
|
-
const code = err.code;
|
|
2750
|
-
if (code === "ESRCH") {
|
|
2751
|
-
if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
|
|
2752
|
-
warn(`another nomad ${verb} running, skipping`);
|
|
2753
|
-
return null;
|
|
2754
|
-
}
|
|
2755
|
-
warn(`another nomad ${verb} running, skipping`);
|
|
2756
|
-
return null;
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
function retryOnce(verb, lp) {
|
|
2760
|
-
try {
|
|
2761
|
-
const fd = openSync2(lp, "wx");
|
|
2762
|
-
try {
|
|
2763
|
-
writeFileSync2(fd, String(process.pid));
|
|
2764
|
-
} catch {
|
|
2765
|
-
try {
|
|
2766
|
-
closeSync2(fd);
|
|
2767
|
-
} catch {
|
|
2768
|
-
}
|
|
2769
|
-
try {
|
|
2770
|
-
unlinkSync(lp);
|
|
2771
|
-
} catch {
|
|
2772
|
-
}
|
|
2773
|
-
warn(`another nomad ${verb} running, skipping`);
|
|
2774
|
-
return null;
|
|
2775
|
-
}
|
|
2776
|
-
return { fd, path: lp };
|
|
2777
|
-
} catch {
|
|
2778
|
-
warn(`another nomad ${verb} running, skipping`);
|
|
2779
|
-
return null;
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
2782
|
-
|
|
2783
|
-
// src/commands.capture-settings.ts
|
|
2784
2804
|
init_utils();
|
|
2785
2805
|
async function confirmCapture(destLabel, keys) {
|
|
2786
2806
|
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
@@ -2866,6 +2886,9 @@ var TS_SHAPE = /^\d{8}-\d{6}(-\d+)?$/;
|
|
|
2866
2886
|
var DURATION_RE = /^(\d+)([dhm])$/;
|
|
2867
2887
|
var UNIT_MS = { d: 864e5, h: 36e5, m: 6e4 };
|
|
2868
2888
|
var CLEAN_DEFAULT_OLDER_THAN_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
2889
|
+
function nameOf(d) {
|
|
2890
|
+
return d.name;
|
|
2891
|
+
}
|
|
2869
2892
|
function isTsDir(name) {
|
|
2870
2893
|
return TS_SHAPE.test(name);
|
|
2871
2894
|
}
|
|
@@ -2878,6 +2901,19 @@ function listBackupDirs(backupBase2) {
|
|
|
2878
2901
|
if (!existsSync8(backupBase2)) return [];
|
|
2879
2902
|
return readdirSync4(backupBase2).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join12(backupBase2, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2880
2903
|
}
|
|
2904
|
+
function holdsNoContent(dir) {
|
|
2905
|
+
let entries;
|
|
2906
|
+
try {
|
|
2907
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
2908
|
+
} catch {
|
|
2909
|
+
return false;
|
|
2910
|
+
}
|
|
2911
|
+
for (const entry of entries) {
|
|
2912
|
+
if (!entry.isDirectory()) return false;
|
|
2913
|
+
if (!holdsNoContent(join12(dir, entry.name))) return false;
|
|
2914
|
+
}
|
|
2915
|
+
return true;
|
|
2916
|
+
}
|
|
2881
2917
|
function prunableByAge(dirs, olderThanMs, nowMs) {
|
|
2882
2918
|
return dirs.filter((d) => nowMs - d.mtimeMs > olderThanMs).map((d) => d.name);
|
|
2883
2919
|
}
|
|
@@ -2910,8 +2946,30 @@ function cmdClean(opts, backupBase2 = backupBase()) {
|
|
|
2910
2946
|
}
|
|
2911
2947
|
olderThanMs = parsed;
|
|
2912
2948
|
}
|
|
2949
|
+
if (dryRun) {
|
|
2950
|
+
pruneBackups({ dryRun, olderThanMs, keep }, backupBase2);
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
const handle = acquireLock("clean");
|
|
2954
|
+
if (handle === null) process.exit(EXIT.SUCCESS);
|
|
2955
|
+
try {
|
|
2956
|
+
pruneBackups({ dryRun, olderThanMs, keep }, backupBase2);
|
|
2957
|
+
} finally {
|
|
2958
|
+
releaseLock(handle);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2961
|
+
function pruneBackups(opts, backupBase2) {
|
|
2962
|
+
const { dryRun, olderThanMs, keep } = opts;
|
|
2913
2963
|
const dirs = listBackupDirs(backupBase2);
|
|
2914
|
-
const
|
|
2964
|
+
const empty = new Set(dirs.filter((d) => holdsNoContent(join12(backupBase2, d.name))).map(nameOf));
|
|
2965
|
+
const aged = new Set(
|
|
2966
|
+
resolveTargets(
|
|
2967
|
+
dirs.filter((d) => !empty.has(d.name)),
|
|
2968
|
+
olderThanMs,
|
|
2969
|
+
keep
|
|
2970
|
+
)
|
|
2971
|
+
);
|
|
2972
|
+
const targets = dirs.filter((d) => empty.has(d.name) || aged.has(d.name)).map(nameOf);
|
|
2915
2973
|
if (dryRun) {
|
|
2916
2974
|
for (const name of targets) item(name);
|
|
2917
2975
|
log(`dry-run: ${targets.length} backup(s) would be removed`);
|
|
@@ -3964,7 +4022,7 @@ function reportBackupsCheck(section2, backupBase2 = backupBase()) {
|
|
|
3964
4022
|
if (count > DOCTOR_BACKUP_COUNT_WARN || sizeMb > DOCTOR_BACKUP_SIZE_WARN_MB) {
|
|
3965
4023
|
addItem(
|
|
3966
4024
|
section2,
|
|
3967
|
-
`${yellow(warnGlyph)} backups: ${count} dirs / ${sizeMb.toFixed(1)} MB (run 'nomad clean --backups --keep <N>'; bare --backups
|
|
4025
|
+
`${yellow(warnGlyph)} backups: ${count} dirs / ${sizeMb.toFixed(1)} MB (run 'nomad clean --backups --keep <N>'; bare --backups prunes empty dirs plus those older than 14d)`
|
|
3968
4026
|
);
|
|
3969
4027
|
}
|
|
3970
4028
|
}
|
|
@@ -9001,12 +9059,20 @@ function handleWedge(repo, forceRemote) {
|
|
|
9001
9059
|
return die(wedgeMarkerRunbookText(state), { code: EXIT.CONFLICT });
|
|
9002
9060
|
}
|
|
9003
9061
|
function runPullCore(opts = {}) {
|
|
9062
|
+
const backup = backupBase();
|
|
9063
|
+
const ts = freshBackupTs(backup);
|
|
9064
|
+
try {
|
|
9065
|
+
return runPullWithBackupTs(ts, opts);
|
|
9066
|
+
} finally {
|
|
9067
|
+
discardEmptyBackupDir(join61(backup, ts));
|
|
9068
|
+
}
|
|
9069
|
+
}
|
|
9070
|
+
function runPullWithBackupTs(ts, opts) {
|
|
9004
9071
|
const dryRun = opts.dryRun === true;
|
|
9005
9072
|
const forceRemote = opts.forceRemote === true;
|
|
9006
9073
|
const compose = opts.compose === true;
|
|
9007
9074
|
const repo = repoHome();
|
|
9008
9075
|
const backup = backupBase();
|
|
9009
|
-
const ts = freshBackupTs(backup);
|
|
9010
9076
|
const recovered = handleWedge(repo, forceRemote);
|
|
9011
9077
|
if (!dryRun) {
|
|
9012
9078
|
const backupRoot = join61(backup, ts);
|
|
@@ -10638,7 +10704,7 @@ function parseSyncArgs(argv) {
|
|
|
10638
10704
|
// package.json
|
|
10639
10705
|
var package_default = {
|
|
10640
10706
|
name: "claude-nomad",
|
|
10641
|
-
version: "0.
|
|
10707
|
+
version: "0.67.0",
|
|
10642
10708
|
type: "module",
|
|
10643
10709
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
10644
10710
|
keywords: [
|
|
@@ -10867,7 +10933,9 @@ var DEFAULT_HELP = [
|
|
|
10867
10933
|
row(" --backups", "Required: confirm backup pruning is the intended target."),
|
|
10868
10934
|
row(" --dry-run", "List the snapshots that would be removed without deleting."),
|
|
10869
10935
|
row(" --older-than <dur>", "Delete snapshots older than a duration (e.g. 14d, 24h, 30m)."),
|
|
10870
|
-
cont("Default when no retention flag is given: 14d."),
|
|
10936
|
+
cont("Default when no retention flag is given: 14d. A snapshot holding"),
|
|
10937
|
+
cont("nothing is removed in every mode, whatever its age, and never"),
|
|
10938
|
+
cont("uses up one of the --keep slots."),
|
|
10871
10939
|
row(" --keep <N>", "Keep the N most-recent snapshots, delete the rest. Mutually"),
|
|
10872
10940
|
cont("exclusive with --older-than."),
|
|
10873
10941
|
"",
|
package/package.json
CHANGED