claude-nomad 0.39.0 → 0.40.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 +12 -0
- package/README.md +5 -0
- package/dist/nomad.mjs +289 -178
- package/dist/nomad.worker.mjs +24 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.40.0](https://github.com/funkadelic/claude-nomad/compare/v0.39.0...v0.40.0) (2026-06-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
* **spinner:** animate long-running operations ([#233](https://github.com/funkadelic/claude-nomad/issues/233)) ([e6755c3](https://github.com/funkadelic/claude-nomad/commit/e6755c3e5e86b52cbc5f996e9c5aee9d79da4574))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
* **docs-site:** build gate, link validation, and page metadata ([#231](https://github.com/funkadelic/claude-nomad/issues/231)) ([d4ad8ec](https://github.com/funkadelic/claude-nomad/commit/d4ad8ec0c414a91153a584156ccee2b32d824360))
|
|
14
|
+
|
|
3
15
|
## [0.39.0](https://github.com/funkadelic/claude-nomad/compare/v0.38.1...v0.39.0) (2026-06-02)
|
|
4
16
|
|
|
5
17
|
|
package/README.md
CHANGED
|
@@ -57,6 +57,11 @@ $ nomad pull # apply config to ~/.claude/
|
|
|
57
57
|
$ nomad push # publish local changes (sessions, settings)
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
During `nomad push` and `nomad pull`, long-running steps (rebase, secret scan, git push, session
|
|
61
|
+
sync) show an animated progress indicator on an interactive terminal so the CLI does not look hung.
|
|
62
|
+
In CI and when output is piped, only plain text lines are printed, with no ANSI control codes, so
|
|
63
|
+
log output remains grep-stable.
|
|
64
|
+
|
|
60
65
|
When `nomad push` detects a potential secret, it drops into an interactive menu (TTY) or aborts with
|
|
61
66
|
a recovery hint (non-TTY/CI). Three non-interactive recovery paths are available without the menu:
|
|
62
67
|
|
package/dist/nomad.mjs
CHANGED
|
@@ -2956,8 +2956,8 @@ ${lines}`);
|
|
|
2956
2956
|
}
|
|
2957
2957
|
|
|
2958
2958
|
// src/commands.pull.ts
|
|
2959
|
-
import { existsSync as
|
|
2960
|
-
import { join as
|
|
2959
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync8 } from "node:fs";
|
|
2960
|
+
import { join as join36 } from "node:path";
|
|
2961
2961
|
|
|
2962
2962
|
// src/commands.push.sections.ts
|
|
2963
2963
|
init_color();
|
|
@@ -3633,156 +3633,23 @@ function computePreview(ts, map) {
|
|
|
3633
3633
|
return { unmapped: remapResult.unmapped, collisions: 0 };
|
|
3634
3634
|
}
|
|
3635
3635
|
|
|
3636
|
-
// src/
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
applySharedLinks(ts, map);
|
|
3642
|
-
const { label } = regenerateSettings(ts);
|
|
3643
|
-
const remapResult = remapPull(ts);
|
|
3644
|
-
const extrasResult = remapExtrasPull(ts);
|
|
3645
|
-
const summary = section("Summary");
|
|
3646
|
-
addItem(
|
|
3647
|
-
summary,
|
|
3648
|
-
summaryRow("pull", remapResult.unmapped + extrasResult.unmapped, 0, extrasResult.skipped)
|
|
3649
|
-
);
|
|
3650
|
-
renderTree([
|
|
3651
|
-
buildSettingsSection(label),
|
|
3652
|
-
buildSessionsSection(remapResult.pulled, remapResult.unmapped),
|
|
3653
|
-
buildExtrasSection(extrasResult.pulled, extrasResult.skipped),
|
|
3654
|
-
summary
|
|
3655
|
-
]);
|
|
3656
|
-
}
|
|
3657
|
-
function cmdPull(opts = {}) {
|
|
3658
|
-
const dryRun = opts.dryRun === true;
|
|
3659
|
-
if (!existsSync28(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
3660
|
-
if (!existsSync28(join33(REPO_HOME, "shared", "settings.base.json"))) {
|
|
3661
|
-
die("repo not initialized; run 'nomad init' to scaffold");
|
|
3662
|
-
}
|
|
3663
|
-
const handle = acquireLock("pull");
|
|
3664
|
-
if (handle === null) process.exit(0);
|
|
3665
|
-
try {
|
|
3666
|
-
const ts = freshBackupTs(BACKUP_BASE);
|
|
3667
|
-
if (!dryRun) {
|
|
3668
|
-
const backupRoot = join33(BACKUP_BASE, ts);
|
|
3669
|
-
try {
|
|
3670
|
-
mkdirSync7(backupRoot, { recursive: true });
|
|
3671
|
-
} catch (err) {
|
|
3672
|
-
die(`could not create backup dir: ${err.message}`);
|
|
3673
|
-
}
|
|
3674
|
-
}
|
|
3675
|
-
log(
|
|
3676
|
-
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
3677
|
-
);
|
|
3678
|
-
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", REPO_HOME);
|
|
3679
|
-
const mapPath = join33(REPO_HOME, "path-map.json");
|
|
3680
|
-
const map = existsSync28(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
3681
|
-
divergenceCheckExtras(ts);
|
|
3682
|
-
if (dryRun) {
|
|
3683
|
-
const previewResult = computePreview(ts, map);
|
|
3684
|
-
log("dry-run complete; no mutation");
|
|
3685
|
-
emitSummary("pull", previewResult.unmapped);
|
|
3686
|
-
} else {
|
|
3687
|
-
applyWetPull(ts, map);
|
|
3688
|
-
}
|
|
3689
|
-
} catch (err) {
|
|
3690
|
-
if (err instanceof NomadFatal) {
|
|
3691
|
-
fail(err.message);
|
|
3692
|
-
process.exitCode = 1;
|
|
3693
|
-
} else {
|
|
3694
|
-
throw err;
|
|
3695
|
-
}
|
|
3696
|
-
} finally {
|
|
3697
|
-
releaseLock(handle);
|
|
3698
|
-
}
|
|
3699
|
-
}
|
|
3700
|
-
|
|
3701
|
-
// src/commands.push.ts
|
|
3702
|
-
init_config();
|
|
3703
|
-
import { existsSync as existsSync31 } from "node:fs";
|
|
3704
|
-
import { join as join38, relative as relative5 } from "node:path";
|
|
3705
|
-
|
|
3706
|
-
// src/commands.push.allowlist.ts
|
|
3707
|
-
init_config();
|
|
3708
|
-
init_config_sharedDirs_guard();
|
|
3709
|
-
init_utils();
|
|
3710
|
-
function isAllowed(path, allowed) {
|
|
3711
|
-
for (const entry of allowed) {
|
|
3712
|
-
if (path === entry) return true;
|
|
3713
|
-
if (entry === "hosts/") {
|
|
3714
|
-
if (/^hosts\/[^/]+\.json$/.test(path)) return true;
|
|
3715
|
-
continue;
|
|
3716
|
-
}
|
|
3717
|
-
if (entry.endsWith("/") && path.startsWith(entry)) return true;
|
|
3718
|
-
}
|
|
3719
|
-
return false;
|
|
3720
|
-
}
|
|
3721
|
-
function isNeverSync(path) {
|
|
3722
|
-
const blockSet = path.startsWith("shared/extras/") ? ALWAYS_NEVER_SYNC : NEVER_SYNC;
|
|
3723
|
-
for (const segment of path.split("/")) {
|
|
3724
|
-
if (blockSet.has(segment)) return true;
|
|
3725
|
-
}
|
|
3726
|
-
return false;
|
|
3727
|
-
}
|
|
3728
|
-
function parsePorcelainZ(statusPorcelain) {
|
|
3729
|
-
const records = statusPorcelain.split("\0");
|
|
3730
|
-
const paths = [];
|
|
3731
|
-
for (let i = 0; i < records.length; i++) {
|
|
3732
|
-
const rec = records[i];
|
|
3733
|
-
if (rec === void 0 || rec === "") continue;
|
|
3734
|
-
if (rec.length < 4) continue;
|
|
3735
|
-
const xy = rec.slice(0, 2);
|
|
3736
|
-
const newPath = rec.slice(3);
|
|
3737
|
-
paths.push(newPath);
|
|
3738
|
-
if (/[RC]/.test(xy)) {
|
|
3739
|
-
const oldPath = records[i + 1];
|
|
3740
|
-
if (oldPath !== void 0 && oldPath !== "") paths.push(oldPath);
|
|
3741
|
-
i++;
|
|
3742
|
-
}
|
|
3743
|
-
}
|
|
3744
|
-
return paths;
|
|
3745
|
-
}
|
|
3746
|
-
function enforceAllowList(statusPorcelain, map) {
|
|
3747
|
-
const extrasWhitelist = SUPPORTED_EXTRAS;
|
|
3748
|
-
const allowed = [
|
|
3749
|
-
...PUSH_ALLOWED_STATIC,
|
|
3750
|
-
...Object.keys(map.projects).map((l) => `shared/projects/${l}/`),
|
|
3751
|
-
...Object.entries(map.extras ?? {}).flatMap(
|
|
3752
|
-
([l, names]) => names.filter((n) => extrasWhitelist.includes(n)).flatMap((n) => [`shared/extras/${l}/${n}`, `shared/extras/${l}/${n}/`])
|
|
3753
|
-
),
|
|
3754
|
-
...(map.sharedDirs ?? []).filter((d) => isValidSharedDir(d)).map((d) => `shared/${d}/`)
|
|
3755
|
-
];
|
|
3756
|
-
const neverSyncHits = [];
|
|
3757
|
-
const violations = [];
|
|
3758
|
-
for (const path of parsePorcelainZ(statusPorcelain)) {
|
|
3759
|
-
if (isNeverSync(path)) {
|
|
3760
|
-
neverSyncHits.push(path);
|
|
3761
|
-
} else if (!isAllowed(path, allowed)) {
|
|
3762
|
-
violations.push(path);
|
|
3763
|
-
}
|
|
3764
|
-
}
|
|
3765
|
-
if (neverSyncHits.length === 0 && violations.length === 0) return;
|
|
3766
|
-
for (const p of neverSyncHits) {
|
|
3767
|
-
fail(`${p} is in NEVER_SYNC and must never be pushed`);
|
|
3768
|
-
}
|
|
3769
|
-
for (const p of violations) {
|
|
3770
|
-
fail(`to sync ${p}, add to PUSH_ALLOWED in src/config.ts`);
|
|
3771
|
-
}
|
|
3772
|
-
throw new NomadFatal("push allow-list violations");
|
|
3773
|
-
}
|
|
3636
|
+
// src/spinner.ts
|
|
3637
|
+
init_color();
|
|
3638
|
+
import { existsSync as existsSync29 } from "node:fs";
|
|
3639
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3640
|
+
import { Worker } from "node:worker_threads";
|
|
3774
3641
|
|
|
3775
3642
|
// src/commands.push.recovery.ts
|
|
3776
3643
|
init_config();
|
|
3777
3644
|
import { readFileSync as readFileSync10, rmSync as rmSync10, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3778
|
-
import { join as
|
|
3645
|
+
import { join as join35 } from "node:path";
|
|
3779
3646
|
import { createInterface } from "node:readline/promises";
|
|
3780
3647
|
|
|
3781
3648
|
// src/commands.push.recovery.redact.ts
|
|
3782
3649
|
init_config();
|
|
3783
3650
|
init_config_sharedDirs_guard();
|
|
3784
|
-
import { cpSync as cpSync5, existsSync as
|
|
3785
|
-
import { dirname as dirname5, join as
|
|
3651
|
+
import { cpSync as cpSync5, existsSync as existsSync28, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
|
|
3652
|
+
import { dirname as dirname5, join as join33, sep as sep2 } from "node:path";
|
|
3786
3653
|
init_push_gitleaks_scan();
|
|
3787
3654
|
init_utils_json();
|
|
3788
3655
|
init_utils();
|
|
@@ -3813,8 +3680,8 @@ function resolveStagedDir(localPath, map) {
|
|
|
3813
3680
|
assertSafeLogical(logical);
|
|
3814
3681
|
const abs = hostMap[HOST];
|
|
3815
3682
|
if (abs === void 0) continue;
|
|
3816
|
-
if (localPath.startsWith(
|
|
3817
|
-
return
|
|
3683
|
+
if (localPath.startsWith(join33(CLAUDE_HOME, "projects", encodePath(abs)) + sep2)) {
|
|
3684
|
+
return join33(REPO_HOME, "shared", "projects", logical);
|
|
3818
3685
|
}
|
|
3819
3686
|
}
|
|
3820
3687
|
return null;
|
|
@@ -3836,7 +3703,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
3836
3703
|
`could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
3837
3704
|
);
|
|
3838
3705
|
}
|
|
3839
|
-
const sessionDir =
|
|
3706
|
+
const sessionDir = join33(dirname5(localPath), sid);
|
|
3840
3707
|
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
3841
3708
|
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
|
|
3842
3709
|
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
@@ -3869,10 +3736,10 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
3869
3736
|
`nothing to redact in the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
3870
3737
|
);
|
|
3871
3738
|
}
|
|
3872
|
-
|
|
3873
|
-
cpSync5(localPath,
|
|
3874
|
-
if (
|
|
3875
|
-
cpSync5(sessionDir,
|
|
3739
|
+
mkdirSync7(stagedProjectDir, { recursive: true });
|
|
3740
|
+
cpSync5(localPath, join33(stagedProjectDir, `${sid}.jsonl`), { force: true });
|
|
3741
|
+
if (existsSync28(sessionDir)) {
|
|
3742
|
+
cpSync5(sessionDir, join33(stagedProjectDir, sid), { force: true, recursive: true });
|
|
3876
3743
|
}
|
|
3877
3744
|
return true;
|
|
3878
3745
|
}
|
|
@@ -3880,13 +3747,13 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
3880
3747
|
// src/commands.push.recovery.drop.ts
|
|
3881
3748
|
init_config();
|
|
3882
3749
|
import { rmSync as rmSync9 } from "node:fs";
|
|
3883
|
-
import { join as
|
|
3750
|
+
import { join as join34 } from "node:path";
|
|
3884
3751
|
function dropSessionFromStaged(sid, map) {
|
|
3885
3752
|
const logicals = Object.keys(map.projects);
|
|
3886
3753
|
if (logicals.length === 0) return false;
|
|
3887
3754
|
for (const logical of logicals) {
|
|
3888
|
-
const jsonl =
|
|
3889
|
-
const dir =
|
|
3755
|
+
const jsonl = join34(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`);
|
|
3756
|
+
const dir = join34(REPO_HOME, "shared", "projects", logical, sid);
|
|
3890
3757
|
rmSync9(jsonl, { force: true });
|
|
3891
3758
|
rmSync9(dir, { recursive: true, force: true });
|
|
3892
3759
|
}
|
|
@@ -4004,7 +3871,7 @@ function applyThenRescan(scanVerdict, repoHome) {
|
|
|
4004
3871
|
return next;
|
|
4005
3872
|
}
|
|
4006
3873
|
function allowThenRescan(append, scanVerdict, repoHome) {
|
|
4007
|
-
const ignPath =
|
|
3874
|
+
const ignPath = join35(repoHome, ".gitleaksignore");
|
|
4008
3875
|
let before;
|
|
4009
3876
|
try {
|
|
4010
3877
|
before = readFileSync10(ignPath, "utf8");
|
|
@@ -4083,6 +3950,225 @@ async function resolveLeakFindings(verdict, ts, map, deps = {}) {
|
|
|
4083
3950
|
return current;
|
|
4084
3951
|
}
|
|
4085
3952
|
|
|
3953
|
+
// src/spinner.ts
|
|
3954
|
+
function formatElapsed(ms) {
|
|
3955
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
3956
|
+
}
|
|
3957
|
+
function writePlainStart(out, label) {
|
|
3958
|
+
out.write(`${label}...
|
|
3959
|
+
`);
|
|
3960
|
+
}
|
|
3961
|
+
function writePlainDone(out, label, ms) {
|
|
3962
|
+
out.write(`${label} done (${formatElapsed(ms)})
|
|
3963
|
+
`);
|
|
3964
|
+
}
|
|
3965
|
+
function writeAnimatedDone(out, label, ms, useTTY) {
|
|
3966
|
+
out.write("\r\x1B[K");
|
|
3967
|
+
const glyph = useTTY ? green(okGlyph) : okGlyph;
|
|
3968
|
+
out.write(`${glyph} ${label} (${formatElapsed(ms)})
|
|
3969
|
+
`);
|
|
3970
|
+
}
|
|
3971
|
+
function resolveWorkerPath(deps = {}) {
|
|
3972
|
+
const check = deps.existsSyncFn ?? existsSync29;
|
|
3973
|
+
const base = deps.baseUrl ?? import.meta.url;
|
|
3974
|
+
const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
|
|
3975
|
+
if (check(mjs)) return mjs;
|
|
3976
|
+
return fileURLToPath4(new URL("./spinner.worker.ts", base));
|
|
3977
|
+
}
|
|
3978
|
+
function makeRealWorker() {
|
|
3979
|
+
return new Worker(resolveWorkerPath());
|
|
3980
|
+
}
|
|
3981
|
+
function startSpinner(label, deps = {}) {
|
|
3982
|
+
const ttyCheck = deps.isTTYCheck ?? (() => isTTY());
|
|
3983
|
+
const env = deps.env ?? process.env;
|
|
3984
|
+
const out = deps.out ?? process.stderr;
|
|
3985
|
+
const now = deps.now ?? Date.now;
|
|
3986
|
+
const startMs = now();
|
|
3987
|
+
const animate = ttyCheck() && !env.CI;
|
|
3988
|
+
let worker = null;
|
|
3989
|
+
let degraded = false;
|
|
3990
|
+
let finalized = false;
|
|
3991
|
+
if (animate) {
|
|
3992
|
+
const factory = deps.makeWorker ?? makeRealWorker;
|
|
3993
|
+
try {
|
|
3994
|
+
worker = factory();
|
|
3995
|
+
worker.unref?.();
|
|
3996
|
+
worker.postMessage({ type: "start", label });
|
|
3997
|
+
} catch {
|
|
3998
|
+
degraded = true;
|
|
3999
|
+
worker = null;
|
|
4000
|
+
writePlainStart(out, label);
|
|
4001
|
+
}
|
|
4002
|
+
} else {
|
|
4003
|
+
writePlainStart(out, label);
|
|
4004
|
+
}
|
|
4005
|
+
function finalize(success, doneLabel) {
|
|
4006
|
+
if (finalized) return;
|
|
4007
|
+
finalized = true;
|
|
4008
|
+
const dl = doneLabel ?? label;
|
|
4009
|
+
const elapsed = now() - startMs;
|
|
4010
|
+
if (animate && !degraded && worker !== null) {
|
|
4011
|
+
worker.postMessage({ type: "pause" });
|
|
4012
|
+
if (success) writeAnimatedDone(out, dl, elapsed, ttyCheck());
|
|
4013
|
+
else out.write("\r\x1B[K");
|
|
4014
|
+
worker.terminate();
|
|
4015
|
+
worker = null;
|
|
4016
|
+
} else if (success) {
|
|
4017
|
+
writePlainDone(out, dl, elapsed);
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
return {
|
|
4021
|
+
succeed: (doneLabel) => finalize(true, doneLabel),
|
|
4022
|
+
stop: () => finalize(false)
|
|
4023
|
+
};
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
// src/commands.pull.ts
|
|
4027
|
+
init_utils();
|
|
4028
|
+
init_utils_fs();
|
|
4029
|
+
init_utils_json();
|
|
4030
|
+
function applyWetPull(ts, map) {
|
|
4031
|
+
applySharedLinks(ts, map);
|
|
4032
|
+
const { label } = regenerateSettings(ts);
|
|
4033
|
+
const syncSp = startSpinner("Syncing sessions");
|
|
4034
|
+
let remapResult;
|
|
4035
|
+
try {
|
|
4036
|
+
remapResult = remapPull(ts);
|
|
4037
|
+
syncSp.succeed();
|
|
4038
|
+
} finally {
|
|
4039
|
+
syncSp.stop();
|
|
4040
|
+
}
|
|
4041
|
+
const extrasResult = remapExtrasPull(ts);
|
|
4042
|
+
const summary = section("Summary");
|
|
4043
|
+
addItem(
|
|
4044
|
+
summary,
|
|
4045
|
+
summaryRow("pull", remapResult.unmapped + extrasResult.unmapped, 0, extrasResult.skipped)
|
|
4046
|
+
);
|
|
4047
|
+
renderTree([
|
|
4048
|
+
buildSettingsSection(label),
|
|
4049
|
+
buildSessionsSection(remapResult.pulled, remapResult.unmapped),
|
|
4050
|
+
buildExtrasSection(extrasResult.pulled, extrasResult.skipped),
|
|
4051
|
+
summary
|
|
4052
|
+
]);
|
|
4053
|
+
}
|
|
4054
|
+
function cmdPull(opts = {}) {
|
|
4055
|
+
const dryRun = opts.dryRun === true;
|
|
4056
|
+
if (!existsSync30(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
4057
|
+
if (!existsSync30(join36(REPO_HOME, "shared", "settings.base.json"))) {
|
|
4058
|
+
die("repo not initialized; run 'nomad init' to scaffold");
|
|
4059
|
+
}
|
|
4060
|
+
const handle = acquireLock("pull");
|
|
4061
|
+
if (handle === null) process.exit(0);
|
|
4062
|
+
try {
|
|
4063
|
+
const ts = freshBackupTs(BACKUP_BASE);
|
|
4064
|
+
if (!dryRun) {
|
|
4065
|
+
const backupRoot = join36(BACKUP_BASE, ts);
|
|
4066
|
+
try {
|
|
4067
|
+
mkdirSync8(backupRoot, { recursive: true });
|
|
4068
|
+
} catch (err) {
|
|
4069
|
+
die(`could not create backup dir: ${err.message}`);
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
log(
|
|
4073
|
+
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
4074
|
+
);
|
|
4075
|
+
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", REPO_HOME);
|
|
4076
|
+
const mapPath = join36(REPO_HOME, "path-map.json");
|
|
4077
|
+
const map = existsSync30(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
4078
|
+
divergenceCheckExtras(ts);
|
|
4079
|
+
if (dryRun) {
|
|
4080
|
+
const previewResult = computePreview(ts, map);
|
|
4081
|
+
log("dry-run complete; no mutation");
|
|
4082
|
+
emitSummary("pull", previewResult.unmapped);
|
|
4083
|
+
} else {
|
|
4084
|
+
applyWetPull(ts, map);
|
|
4085
|
+
}
|
|
4086
|
+
} catch (err) {
|
|
4087
|
+
if (err instanceof NomadFatal) {
|
|
4088
|
+
fail(err.message);
|
|
4089
|
+
process.exitCode = 1;
|
|
4090
|
+
} else {
|
|
4091
|
+
throw err;
|
|
4092
|
+
}
|
|
4093
|
+
} finally {
|
|
4094
|
+
releaseLock(handle);
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
// src/commands.push.ts
|
|
4099
|
+
init_config();
|
|
4100
|
+
import { existsSync as existsSync32 } from "node:fs";
|
|
4101
|
+
import { join as join38, relative as relative5 } from "node:path";
|
|
4102
|
+
|
|
4103
|
+
// src/commands.push.allowlist.ts
|
|
4104
|
+
init_config();
|
|
4105
|
+
init_config_sharedDirs_guard();
|
|
4106
|
+
init_utils();
|
|
4107
|
+
function isAllowed(path, allowed) {
|
|
4108
|
+
for (const entry of allowed) {
|
|
4109
|
+
if (path === entry) return true;
|
|
4110
|
+
if (entry === "hosts/") {
|
|
4111
|
+
if (/^hosts\/[^/]+\.json$/.test(path)) return true;
|
|
4112
|
+
continue;
|
|
4113
|
+
}
|
|
4114
|
+
if (entry.endsWith("/") && path.startsWith(entry)) return true;
|
|
4115
|
+
}
|
|
4116
|
+
return false;
|
|
4117
|
+
}
|
|
4118
|
+
function isNeverSync(path) {
|
|
4119
|
+
const blockSet = path.startsWith("shared/extras/") ? ALWAYS_NEVER_SYNC : NEVER_SYNC;
|
|
4120
|
+
for (const segment of path.split("/")) {
|
|
4121
|
+
if (blockSet.has(segment)) return true;
|
|
4122
|
+
}
|
|
4123
|
+
return false;
|
|
4124
|
+
}
|
|
4125
|
+
function parsePorcelainZ(statusPorcelain) {
|
|
4126
|
+
const records = statusPorcelain.split("\0");
|
|
4127
|
+
const paths = [];
|
|
4128
|
+
for (let i = 0; i < records.length; i++) {
|
|
4129
|
+
const rec = records[i];
|
|
4130
|
+
if (rec === void 0 || rec === "") continue;
|
|
4131
|
+
if (rec.length < 4) continue;
|
|
4132
|
+
const xy = rec.slice(0, 2);
|
|
4133
|
+
const newPath = rec.slice(3);
|
|
4134
|
+
paths.push(newPath);
|
|
4135
|
+
if (/[RC]/.test(xy)) {
|
|
4136
|
+
const oldPath = records[i + 1];
|
|
4137
|
+
if (oldPath !== void 0 && oldPath !== "") paths.push(oldPath);
|
|
4138
|
+
i++;
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
return paths;
|
|
4142
|
+
}
|
|
4143
|
+
function enforceAllowList(statusPorcelain, map) {
|
|
4144
|
+
const extrasWhitelist = SUPPORTED_EXTRAS;
|
|
4145
|
+
const allowed = [
|
|
4146
|
+
...PUSH_ALLOWED_STATIC,
|
|
4147
|
+
...Object.keys(map.projects).map((l) => `shared/projects/${l}/`),
|
|
4148
|
+
...Object.entries(map.extras ?? {}).flatMap(
|
|
4149
|
+
([l, names]) => names.filter((n) => extrasWhitelist.includes(n)).flatMap((n) => [`shared/extras/${l}/${n}`, `shared/extras/${l}/${n}/`])
|
|
4150
|
+
),
|
|
4151
|
+
...(map.sharedDirs ?? []).filter((d) => isValidSharedDir(d)).map((d) => `shared/${d}/`)
|
|
4152
|
+
];
|
|
4153
|
+
const neverSyncHits = [];
|
|
4154
|
+
const violations = [];
|
|
4155
|
+
for (const path of parsePorcelainZ(statusPorcelain)) {
|
|
4156
|
+
if (isNeverSync(path)) {
|
|
4157
|
+
neverSyncHits.push(path);
|
|
4158
|
+
} else if (!isAllowed(path, allowed)) {
|
|
4159
|
+
violations.push(path);
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
if (neverSyncHits.length === 0 && violations.length === 0) return;
|
|
4163
|
+
for (const p of neverSyncHits) {
|
|
4164
|
+
fail(`${p} is in NEVER_SYNC and must never be pushed`);
|
|
4165
|
+
}
|
|
4166
|
+
for (const p of violations) {
|
|
4167
|
+
fail(`to sync ${p}, add to PUSH_ALLOWED in src/config.ts`);
|
|
4168
|
+
}
|
|
4169
|
+
throw new NomadFatal("push allow-list violations");
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4086
4172
|
// src/commands.push.ts
|
|
4087
4173
|
init_push_leak_verdict();
|
|
4088
4174
|
init_push_checks();
|
|
@@ -4092,7 +4178,7 @@ init_color();
|
|
|
4092
4178
|
init_config();
|
|
4093
4179
|
init_config_sharedDirs_guard();
|
|
4094
4180
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
4095
|
-
import { copyFileSync, existsSync as
|
|
4181
|
+
import { copyFileSync, existsSync as existsSync31, mkdirSync as mkdirSync9, readdirSync as readdirSync10, rmSync as rmSync11 } from "node:fs";
|
|
4096
4182
|
import { homedir as homedir5 } from "node:os";
|
|
4097
4183
|
import { join as join37 } from "node:path";
|
|
4098
4184
|
init_push_leak_verdict();
|
|
@@ -4110,7 +4196,7 @@ function stageSessions(tmpRoot, map) {
|
|
|
4110
4196
|
reverse.set(encodePath(p), logical);
|
|
4111
4197
|
}
|
|
4112
4198
|
const localProjects = join37(CLAUDE_HOME, "projects");
|
|
4113
|
-
if (!
|
|
4199
|
+
if (!existsSync31(localProjects)) return 0;
|
|
4114
4200
|
let staged = 0;
|
|
4115
4201
|
for (const dir of readdirSync10(localProjects)) {
|
|
4116
4202
|
const logical = reverse.get(dir);
|
|
@@ -4132,7 +4218,7 @@ function stageExtras(tmpRoot, map) {
|
|
|
4132
4218
|
for (const dirname6 of dirnames) {
|
|
4133
4219
|
if (!whitelist.includes(dirname6)) continue;
|
|
4134
4220
|
const src = join37(localRoot, dirname6);
|
|
4135
|
-
if (!
|
|
4221
|
+
if (!existsSync31(src)) continue;
|
|
4136
4222
|
const dst = join37(tmpRoot, "shared", "extras", logical, dirname6);
|
|
4137
4223
|
copyExtras(src, dst);
|
|
4138
4224
|
staged++;
|
|
@@ -4152,7 +4238,7 @@ function previewPushLeaks(map) {
|
|
|
4152
4238
|
return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
|
|
4153
4239
|
}
|
|
4154
4240
|
const ignoreFile = join37(REPO_HOME, ".gitleaksignore");
|
|
4155
|
-
if (
|
|
4241
|
+
if (existsSync31(ignoreFile)) {
|
|
4156
4242
|
copyFileSync(ignoreFile, join37(tmpRoot, ".gitleaksignore"));
|
|
4157
4243
|
}
|
|
4158
4244
|
let findings;
|
|
@@ -4186,15 +4272,34 @@ function guardGitlinks() {
|
|
|
4186
4272
|
`gitlink trap: ${gitlinks.length} nested .git ${noun} in shared/; remove before retry`
|
|
4187
4273
|
);
|
|
4188
4274
|
}
|
|
4275
|
+
function runScan() {
|
|
4276
|
+
const sp = startSpinner("Scanning for secrets");
|
|
4277
|
+
try {
|
|
4278
|
+
const verdict = scanPushVerdict();
|
|
4279
|
+
sp.succeed();
|
|
4280
|
+
return verdict;
|
|
4281
|
+
} finally {
|
|
4282
|
+
sp.stop();
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
function runPush() {
|
|
4286
|
+
const sp = startSpinner("Pushing");
|
|
4287
|
+
try {
|
|
4288
|
+
gitOrFatal(["push"], "git push", REPO_HOME);
|
|
4289
|
+
sp.succeed();
|
|
4290
|
+
} finally {
|
|
4291
|
+
sp.stop();
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4189
4294
|
async function commitAndPush(st, ts, map, redactAll, allowAll, allowRule) {
|
|
4190
4295
|
gitOrFatal(["add", "-A"], "git add", REPO_HOME);
|
|
4191
|
-
let verdict =
|
|
4296
|
+
let verdict = runScan();
|
|
4192
4297
|
if (verdict.leak) {
|
|
4193
4298
|
renderPushTree(st, verdict);
|
|
4194
4299
|
verdict = await resolveLeakFindings(verdict, ts, map, { redactAll, allowAll, allowRule });
|
|
4195
4300
|
}
|
|
4196
4301
|
gitOrFatal(["commit", "-m", `chore: sync from ${HOST}`], "git commit", REPO_HOME);
|
|
4197
|
-
|
|
4302
|
+
runPush();
|
|
4198
4303
|
renderPushTree(st, verdict);
|
|
4199
4304
|
}
|
|
4200
4305
|
function runDryRunPreview(st, map) {
|
|
@@ -4227,13 +4332,19 @@ async function cmdPush(opts = {}) {
|
|
|
4227
4332
|
const allowAll = opts.allowAll === true;
|
|
4228
4333
|
const allowRule = opts.allowRule;
|
|
4229
4334
|
guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
|
|
4230
|
-
if (!
|
|
4335
|
+
if (!existsSync32(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
4231
4336
|
const handle = acquireLock("push");
|
|
4232
4337
|
if (handle === null) process.exit(0);
|
|
4233
4338
|
try {
|
|
4234
4339
|
console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
|
|
4235
4340
|
probeGitleaks();
|
|
4236
|
-
|
|
4341
|
+
const rebaseSp = startSpinner("Rebasing onto origin");
|
|
4342
|
+
try {
|
|
4343
|
+
rebaseBeforePush();
|
|
4344
|
+
rebaseSp.succeed();
|
|
4345
|
+
} finally {
|
|
4346
|
+
rebaseSp.stop();
|
|
4347
|
+
}
|
|
4237
4348
|
const ts = freshBackupTs(BACKUP_BASE);
|
|
4238
4349
|
const remap = remapPush(ts, { dryRun });
|
|
4239
4350
|
const extras = remapExtrasPush(ts, { dryRun });
|
|
@@ -4246,7 +4357,7 @@ async function cmdPush(opts = {}) {
|
|
|
4246
4357
|
return;
|
|
4247
4358
|
}
|
|
4248
4359
|
const mapPath = join38(REPO_HOME, "path-map.json");
|
|
4249
|
-
if (!
|
|
4360
|
+
if (!existsSync32(mapPath)) {
|
|
4250
4361
|
if (dryRun) return runDryRunPreview(st, null);
|
|
4251
4362
|
die("path-map.json missing, cannot enforce push allow-list");
|
|
4252
4363
|
}
|
|
@@ -4286,17 +4397,17 @@ init_config();
|
|
|
4286
4397
|
|
|
4287
4398
|
// src/diff.ts
|
|
4288
4399
|
init_config();
|
|
4289
|
-
import { existsSync as
|
|
4400
|
+
import { existsSync as existsSync33 } from "node:fs";
|
|
4290
4401
|
import { join as join39 } from "node:path";
|
|
4291
4402
|
init_utils();
|
|
4292
4403
|
init_utils_fs();
|
|
4293
4404
|
init_utils_json();
|
|
4294
4405
|
function cmdDiff() {
|
|
4295
4406
|
try {
|
|
4296
|
-
if (!
|
|
4407
|
+
if (!existsSync33(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
4297
4408
|
const ts = freshBackupTs(BACKUP_BASE);
|
|
4298
4409
|
const mapPath = join39(REPO_HOME, "path-map.json");
|
|
4299
|
-
const map =
|
|
4410
|
+
const map = existsSync33(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
4300
4411
|
const result = computePreview(ts, map);
|
|
4301
4412
|
emitSummary("diff", result.unmapped);
|
|
4302
4413
|
} catch (err) {
|
|
@@ -4311,7 +4422,7 @@ function cmdDiff() {
|
|
|
4311
4422
|
|
|
4312
4423
|
// src/init.ts
|
|
4313
4424
|
init_config();
|
|
4314
|
-
import { existsSync as
|
|
4425
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4315
4426
|
import { join as join41 } from "node:path";
|
|
4316
4427
|
|
|
4317
4428
|
// src/init.gh-onboard.ts
|
|
@@ -4393,16 +4504,16 @@ init_config();
|
|
|
4393
4504
|
init_utils();
|
|
4394
4505
|
init_utils_fs();
|
|
4395
4506
|
init_utils_json();
|
|
4396
|
-
import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as
|
|
4507
|
+
import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as existsSync34, rmSync as rmSync12, statSync as statSync9 } from "node:fs";
|
|
4397
4508
|
import { join as join40 } from "node:path";
|
|
4398
4509
|
function snapshotIntoShared(map) {
|
|
4399
4510
|
for (const name of allSharedLinks(map)) {
|
|
4400
4511
|
const src = join40(CLAUDE_HOME, name);
|
|
4401
|
-
if (!
|
|
4512
|
+
if (!existsSync34(src)) continue;
|
|
4402
4513
|
const dst = join40(REPO_HOME, "shared", name);
|
|
4403
4514
|
if (statSync9(src).isDirectory()) {
|
|
4404
4515
|
const gk = join40(dst, ".gitkeep");
|
|
4405
|
-
if (
|
|
4516
|
+
if (existsSync34(gk)) rmSync12(gk);
|
|
4406
4517
|
cpSync6(src, dst, { recursive: true, force: false, errorOnExist: true });
|
|
4407
4518
|
} else {
|
|
4408
4519
|
copyFileSync2(src, dst);
|
|
@@ -4410,7 +4521,7 @@ function snapshotIntoShared(map) {
|
|
|
4410
4521
|
log(`snapshotted shared/${name} from ${src}`);
|
|
4411
4522
|
}
|
|
4412
4523
|
const userSettings = join40(CLAUDE_HOME, "settings.json");
|
|
4413
|
-
if (
|
|
4524
|
+
if (existsSync34(userSettings)) {
|
|
4414
4525
|
let parsed;
|
|
4415
4526
|
try {
|
|
4416
4527
|
parsed = readJson(userSettings);
|
|
@@ -4437,7 +4548,7 @@ function preflightConflict(repoHome) {
|
|
|
4437
4548
|
join41(repoHome, "shared")
|
|
4438
4549
|
];
|
|
4439
4550
|
for (const c of candidates) {
|
|
4440
|
-
if (
|
|
4551
|
+
if (existsSync35(c)) return c;
|
|
4441
4552
|
}
|
|
4442
4553
|
return null;
|
|
4443
4554
|
}
|
|
@@ -4456,7 +4567,7 @@ function cmdInit(opts = {}) {
|
|
|
4456
4567
|
mkdirSync10(join41(REPO_HOME, "shared", name), { recursive: true });
|
|
4457
4568
|
}
|
|
4458
4569
|
const userClaudeMd = join41(CLAUDE_HOME, "CLAUDE.md");
|
|
4459
|
-
if (!snapshot || !
|
|
4570
|
+
if (!snapshot || !existsSync35(userClaudeMd)) {
|
|
4460
4571
|
writeFileSync6(join41(REPO_HOME, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
|
|
4461
4572
|
log("created shared/CLAUDE.md");
|
|
4462
4573
|
}
|
|
@@ -4730,7 +4841,7 @@ function parsePushArgs(argv) {
|
|
|
4730
4841
|
// package.json
|
|
4731
4842
|
var package_default = {
|
|
4732
4843
|
name: "claude-nomad",
|
|
4733
|
-
version: "0.
|
|
4844
|
+
version: "0.40.0",
|
|
4734
4845
|
type: "module",
|
|
4735
4846
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
4736
4847
|
keywords: [
|
|
@@ -4920,7 +5031,7 @@ var DEFAULT_HELP = [
|
|
|
4920
5031
|
init_config();
|
|
4921
5032
|
init_utils();
|
|
4922
5033
|
init_utils_json();
|
|
4923
|
-
import { existsSync as
|
|
5034
|
+
import { existsSync as existsSync36, readFileSync as readFileSync11, readdirSync as readdirSync11 } from "node:fs";
|
|
4924
5035
|
import { join as join42 } from "node:path";
|
|
4925
5036
|
function resumeCmd(sessionId) {
|
|
4926
5037
|
if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
|
|
@@ -4928,7 +5039,7 @@ function resumeCmd(sessionId) {
|
|
|
4928
5039
|
process.exit(1);
|
|
4929
5040
|
}
|
|
4930
5041
|
const projectsRoot = join42(CLAUDE_HOME, "projects");
|
|
4931
|
-
if (!
|
|
5042
|
+
if (!existsSync36(projectsRoot)) {
|
|
4932
5043
|
fail(`${projectsRoot} does not exist`);
|
|
4933
5044
|
process.exit(1);
|
|
4934
5045
|
}
|
|
@@ -4943,7 +5054,7 @@ function resumeCmd(sessionId) {
|
|
|
4943
5054
|
process.exit(1);
|
|
4944
5055
|
}
|
|
4945
5056
|
const mapPath = join42(REPO_HOME, "path-map.json");
|
|
4946
|
-
if (!
|
|
5057
|
+
if (!existsSync36(mapPath)) {
|
|
4947
5058
|
fail("path-map.json missing");
|
|
4948
5059
|
process.exit(1);
|
|
4949
5060
|
}
|
|
@@ -4967,7 +5078,7 @@ function resumeCmd(sessionId) {
|
|
|
4967
5078
|
function findTranscriptPath(projectsRoot, sessionId) {
|
|
4968
5079
|
for (const dir of readdirSync11(projectsRoot)) {
|
|
4969
5080
|
const candidate = join42(projectsRoot, dir, `${sessionId}.jsonl`);
|
|
4970
|
-
if (
|
|
5081
|
+
if (existsSync36(candidate)) return candidate;
|
|
4971
5082
|
}
|
|
4972
5083
|
return null;
|
|
4973
5084
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/spinner.worker.ts
|
|
2
|
+
import { parentPort } from "node:worker_threads";
|
|
3
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
4
|
+
var INTERVAL_MS = 80;
|
|
5
|
+
var frame = 0;
|
|
6
|
+
var timer = null;
|
|
7
|
+
if (parentPort !== null) {
|
|
8
|
+
parentPort.on("message", (msg) => {
|
|
9
|
+
if (msg.type === "start" && msg.label !== void 0) {
|
|
10
|
+
const label = msg.label;
|
|
11
|
+
frame = 0;
|
|
12
|
+
if (timer !== null) clearInterval(timer);
|
|
13
|
+
timer = setInterval(() => {
|
|
14
|
+
process.stderr.write(`${FRAMES[frame % FRAMES.length]} ${label}\r`);
|
|
15
|
+
frame++;
|
|
16
|
+
}, INTERVAL_MS);
|
|
17
|
+
} else if (msg.type === "pause") {
|
|
18
|
+
if (timer !== null) {
|
|
19
|
+
clearInterval(timer);
|
|
20
|
+
timer = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
package/package.json
CHANGED