claude-nomad 0.56.2 → 0.57.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 +19 -0
- package/README.md +18 -12
- package/dist/nomad.mjs +293 -121
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.57.0](https://github.com/funkadelic/claude-nomad/compare/v0.56.2...v0.57.0) (2026-07-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
* **docs-site:** add starlight-llms-txt plugin ([#378](https://github.com/funkadelic/claude-nomad/issues/378)) ([0604327](https://github.com/funkadelic/claude-nomad/commit/0604327ffa69abbee0b70183a49416ec14a3306c))
|
|
9
|
+
* **sync:** add unified nomad sync command ([#380](https://github.com/funkadelic/claude-nomad/issues/380)) ([e60a05e](https://github.com/funkadelic/claude-nomad/commit/e60a05e62179751652d70e54ba3deb8b6b7c4859))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
* **plugin:** tolerate a user-supplied --dry-run in push and sync wrappers ([#381](https://github.com/funkadelic/claude-nomad/issues/381)) ([7ce0a99](https://github.com/funkadelic/claude-nomad/commit/7ce0a99ae53cd8737b5e03bb0f5f730447b1108d))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
* remove vestigial private-repo guards from workflows ([#383](https://github.com/funkadelic/claude-nomad/issues/383)) ([31e52b8](https://github.com/funkadelic/claude-nomad/commit/31e52b834ca55204dac29fcec6ceee35605f818b))
|
|
20
|
+
* scrub internal planning IDs from source and test comments ([#382](https://github.com/funkadelic/claude-nomad/issues/382)) ([b024b7b](https://github.com/funkadelic/claude-nomad/commit/b024b7b1b877af347db7ca1754751ede73b5879f))
|
|
21
|
+
|
|
3
22
|
## [0.56.2](https://github.com/funkadelic/claude-nomad/compare/v0.56.1...v0.56.2) (2026-07-01)
|
|
4
23
|
|
|
5
24
|
|
package/README.md
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
|
|
14
14
|
Open Claude Code on a second machine and it is a blank slate: none of your custom skills, slash
|
|
15
15
|
commands, tuned settings, or past conversations. **claude-nomad** keeps all of it in sync through a
|
|
16
|
-
private Git repo you control. `nomad
|
|
17
|
-
|
|
16
|
+
private Git repo you control. Run `nomad sync` on any machine and everything is there, conversations
|
|
17
|
+
included; it pulls in your latest config first, then publishes your local changes, so you never have
|
|
18
|
+
to remember which one to run first.
|
|
18
19
|
|
|
19
20
|
Not dotfiles, not rsync. **claude-nomad** understands Claude Code's state, so your session history
|
|
20
21
|
survives different file paths and your secrets never ride along.
|
|
@@ -117,14 +118,19 @@ Everyday loop on any host:
|
|
|
117
118
|
|
|
118
119
|
```bash
|
|
119
120
|
$ nomad doctor # confirm setup
|
|
120
|
-
$ nomad
|
|
121
|
-
$ nomad push # publish local changes (sessions, settings)
|
|
121
|
+
$ nomad sync # pull config, then publish local changes, in one step
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
`nomad sync` is the command to reach for day to day: it always pulls first (so changes from your
|
|
125
|
+
other machines land before anything is pushed, and work that exists only on this machine is kept,
|
|
126
|
+
not deleted) and then pushes, under one lock, so there is no ordering to remember. `nomad pull` and
|
|
127
|
+
`nomad push` are still available as lower-level commands for cases `sync` does not cover: recovering
|
|
128
|
+
a wedged repo with `nomad pull --force-remote`, or resolving a detected secret without the
|
|
129
|
+
interactive menu via `nomad push --redact-all` / `--allow` / `--allow-all` (see
|
|
130
|
+
[Changing settings](#changing-settings) and
|
|
131
|
+
[Recovery flows](https://funkadelic.github.io/claude-nomad/recovery/)). The
|
|
132
|
+
[FAQ](https://funkadelic.github.io/claude-nomad/faq/) covers what `sync` does under the hood and the
|
|
133
|
+
push/pull order it enforces.
|
|
128
134
|
|
|
129
135
|
### Make your sessions follow you
|
|
130
136
|
|
|
@@ -216,10 +222,10 @@ first (minimum version `>= 0.35.0`), then add the plugin.
|
|
|
216
222
|
/plugin install nomad@claude-nomad
|
|
217
223
|
```
|
|
218
224
|
|
|
219
|
-
It adds `/nomad:pull`, `/nomad:diff`, `/nomad:push` (preview only),
|
|
220
|
-
`/nomad:clean`, plus a session-start drift check. The plugin versions
|
|
221
|
-
but requires nomad `>= 0.35.0` because it calls recent subcommands
|
|
222
|
-
`nomad clean --backups`) and reads the doctor command's status output. See the
|
|
225
|
+
It adds `/nomad:sync` (preview only), `/nomad:pull`, `/nomad:diff`, `/nomad:push` (preview only),
|
|
226
|
+
`/nomad:doctor`, and `/nomad:clean`, plus a session-start drift check. The plugin versions
|
|
227
|
+
independently from the CLI, but requires nomad `>= 0.35.0` because it calls recent subcommands
|
|
228
|
+
(`nomad diff`, `nomad clean --backups`) and reads the doctor command's status output. See the
|
|
223
229
|
[plugin guide](https://funkadelic.github.io/claude-nomad/plugin/) for details.
|
|
224
230
|
|
|
225
231
|
## Requirements
|
package/dist/nomad.mjs
CHANGED
|
@@ -214,12 +214,13 @@ function gitOrFatal(args, context, cwd) {
|
|
|
214
214
|
throw new NomadFatal(`${context} failed`);
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
-
var log, warn, fail, item, NomadFatal, die, gitStatusPorcelainZ;
|
|
217
|
+
var log, ok, warn, fail, item, NomadFatal, die, gitStatusPorcelainZ;
|
|
218
218
|
var init_utils = __esm({
|
|
219
219
|
"src/utils.ts"() {
|
|
220
220
|
"use strict";
|
|
221
221
|
init_color();
|
|
222
222
|
log = (msg) => console.log(`${dim(infoGlyph)} ${msg}`);
|
|
223
|
+
ok = (msg) => console.log(`${green(okGlyph)} ${msg}`);
|
|
223
224
|
warn = (msg) => {
|
|
224
225
|
console.error(`${yellow(warnGlyph)} ${msg}`);
|
|
225
226
|
};
|
|
@@ -468,7 +469,7 @@ var init_config = __esm({
|
|
|
468
469
|
"hosts/",
|
|
469
470
|
"path-map.json",
|
|
470
471
|
".gitleaksignore",
|
|
471
|
-
// written by nomad push Allow action
|
|
472
|
+
// written by nomad push Allow action
|
|
472
473
|
".gitleaks.overlay.toml"
|
|
473
474
|
// user-owned gitleaks allowlist overlay layered on the bundled base
|
|
474
475
|
];
|
|
@@ -5804,7 +5805,7 @@ function remapExtrasPush(ts, opts = {}) {
|
|
|
5804
5805
|
// Repo-only files survive; local edits propagate (overlay overwrites).
|
|
5805
5806
|
// The filter prevents ALWAYS_NEVER_SYNC files from landing in the repo
|
|
5806
5807
|
// working tree before the allow-list gate fires, eliminating the
|
|
5807
|
-
// "residue wedges repeat push" regression
|
|
5808
|
+
// "residue wedges repeat push" regression. The allow-list gate
|
|
5808
5809
|
// (enforceAllowList / blockSetFor in commands.push.allowlist.ts)
|
|
5809
5810
|
// remains the hard security boundary.
|
|
5810
5811
|
// All others: copyExtrasFiltered with per-extra denylist.
|
|
@@ -5871,16 +5872,18 @@ function divergenceWarnLine(o) {
|
|
|
5871
5872
|
}
|
|
5872
5873
|
function divergenceCheckExtras(ts, prePostHeads) {
|
|
5873
5874
|
const v = loadValidatedExtras({});
|
|
5874
|
-
if (v === null) return;
|
|
5875
|
+
if (v === null) return 0;
|
|
5875
5876
|
const counts = { unmapped: 0, skipped: 0 };
|
|
5876
5877
|
const backupRoot = join43(backupBase(), ts, "extras");
|
|
5877
5878
|
const repo = repoHome();
|
|
5879
|
+
let divergedCount = 0;
|
|
5878
5880
|
for (const { logical, localRoot, dirname: dirname10 } of eachExtrasTarget(v, counts)) {
|
|
5879
5881
|
const local = join43(localRoot, dirname10);
|
|
5880
5882
|
const repoEntry = join43(repo, "shared", "extras", logical, dirname10);
|
|
5881
5883
|
if (!existsSync36(local) || !existsSync36(repoEntry)) continue;
|
|
5882
5884
|
const modified = listDivergingModified(local, repoEntry);
|
|
5883
5885
|
if (modified.length === 0) continue;
|
|
5886
|
+
divergedCount += modified.length;
|
|
5884
5887
|
const projectBackupRoot = join43(backupRoot, encodePath(localRoot));
|
|
5885
5888
|
warn(
|
|
5886
5889
|
divergenceWarnLine({
|
|
@@ -5898,6 +5901,7 @@ function divergenceCheckExtras(ts, prePostHeads) {
|
|
|
5898
5901
|
warn(keptDeleteWarnLine(logical, relToLocal));
|
|
5899
5902
|
}
|
|
5900
5903
|
}
|
|
5904
|
+
return divergedCount;
|
|
5901
5905
|
}
|
|
5902
5906
|
|
|
5903
5907
|
// src/skills-sync.ts
|
|
@@ -6458,7 +6462,7 @@ function capturePrePostHeads(repo, rebase) {
|
|
|
6458
6462
|
if (pre === void 0 || post === void 0) return void 0;
|
|
6459
6463
|
return { pre, post };
|
|
6460
6464
|
}
|
|
6461
|
-
function
|
|
6465
|
+
function buildWetPullSections(ts, map, prePostHeads) {
|
|
6462
6466
|
applySharedLinks(ts, map);
|
|
6463
6467
|
const { label } = regenerateSettings(ts);
|
|
6464
6468
|
syncSkillsPull(ts);
|
|
@@ -6476,12 +6480,15 @@ function applyWetPull(ts, map, prePostHeads) {
|
|
|
6476
6480
|
localOnly
|
|
6477
6481
|
)
|
|
6478
6482
|
);
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6483
|
+
return {
|
|
6484
|
+
sections: [
|
|
6485
|
+
buildSettingsSection(label),
|
|
6486
|
+
buildSessionsSection(remapResult.pulled, remapResult.unmapped, localOnly),
|
|
6487
|
+
buildExtrasSection(extrasResult.pulled, extrasResult.skipped),
|
|
6488
|
+
summary
|
|
6489
|
+
],
|
|
6490
|
+
localOnly
|
|
6491
|
+
};
|
|
6485
6492
|
}
|
|
6486
6493
|
function handleWedge(repo, forceRemote) {
|
|
6487
6494
|
const wedge = classifyWedge(repo);
|
|
@@ -6501,11 +6508,40 @@ function handleWedge(repo, forceRemote) {
|
|
|
6501
6508
|
const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
|
|
6502
6509
|
die(wedgeMarkerRunbookText(state));
|
|
6503
6510
|
}
|
|
6504
|
-
function
|
|
6511
|
+
function runPullCore(opts = {}) {
|
|
6505
6512
|
const dryRun = opts.dryRun === true;
|
|
6506
6513
|
const forceRemote = opts.forceRemote === true;
|
|
6507
6514
|
const repo = repoHome();
|
|
6508
6515
|
const backup = backupBase();
|
|
6516
|
+
const ts = freshBackupTs(backup);
|
|
6517
|
+
handleWedge(repo, forceRemote);
|
|
6518
|
+
if (!dryRun) {
|
|
6519
|
+
const backupRoot = join46(backup, ts);
|
|
6520
|
+
try {
|
|
6521
|
+
mkdirSync10(backupRoot, { recursive: true });
|
|
6522
|
+
} catch (err) {
|
|
6523
|
+
die(`could not create backup dir: ${err.message}`);
|
|
6524
|
+
}
|
|
6525
|
+
}
|
|
6526
|
+
log(
|
|
6527
|
+
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
6528
|
+
);
|
|
6529
|
+
const prePostHeads = capturePrePostHeads(repo, () => {
|
|
6530
|
+
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
|
|
6531
|
+
});
|
|
6532
|
+
const mapPath = join46(repo, "path-map.json");
|
|
6533
|
+
const map = existsSync39(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
6534
|
+
const divergedKeptLocal = divergenceCheckExtras(ts, dryRun ? prePostHeads : void 0);
|
|
6535
|
+
if (dryRun) {
|
|
6536
|
+
computePreview(ts, map, "pull");
|
|
6537
|
+
log("dry-run complete; no mutation");
|
|
6538
|
+
return { tag: "dry" };
|
|
6539
|
+
}
|
|
6540
|
+
const { sections, localOnly } = buildWetPullSections(ts, map, prePostHeads);
|
|
6541
|
+
return { tag: "wet", sections, localOnly, divergedKeptLocal };
|
|
6542
|
+
}
|
|
6543
|
+
function cmdPull(opts = {}) {
|
|
6544
|
+
const repo = repoHome();
|
|
6509
6545
|
if (!existsSync39(repo)) die(`repo not cloned at ${repo}`);
|
|
6510
6546
|
if (!existsSync39(join46(repo, "shared", "settings.base.json"))) {
|
|
6511
6547
|
die("repo not initialized; run 'nomad init' to scaffold");
|
|
@@ -6513,31 +6549,8 @@ function cmdPull(opts = {}) {
|
|
|
6513
6549
|
const handle = acquireLock("pull");
|
|
6514
6550
|
if (handle === null) process.exit(0);
|
|
6515
6551
|
try {
|
|
6516
|
-
const
|
|
6517
|
-
|
|
6518
|
-
if (!dryRun) {
|
|
6519
|
-
const backupRoot = join46(backup, ts);
|
|
6520
|
-
try {
|
|
6521
|
-
mkdirSync10(backupRoot, { recursive: true });
|
|
6522
|
-
} catch (err) {
|
|
6523
|
-
die(`could not create backup dir: ${err.message}`);
|
|
6524
|
-
}
|
|
6525
|
-
}
|
|
6526
|
-
log(
|
|
6527
|
-
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
6528
|
-
);
|
|
6529
|
-
const prePostHeads = capturePrePostHeads(repo, () => {
|
|
6530
|
-
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
|
|
6531
|
-
});
|
|
6532
|
-
const mapPath = join46(repo, "path-map.json");
|
|
6533
|
-
const map = existsSync39(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
6534
|
-
divergenceCheckExtras(ts, dryRun ? prePostHeads : void 0);
|
|
6535
|
-
if (dryRun) {
|
|
6536
|
-
computePreview(ts, map, "pull");
|
|
6537
|
-
log("dry-run complete; no mutation");
|
|
6538
|
-
} else {
|
|
6539
|
-
applyWetPull(ts, map, prePostHeads);
|
|
6540
|
-
}
|
|
6552
|
+
const result = runPullCore(opts);
|
|
6553
|
+
if (result.tag === "wet") renderTree(result.sections);
|
|
6541
6554
|
} catch (err) {
|
|
6542
6555
|
if (err instanceof NomadFatal) {
|
|
6543
6556
|
fail(err.message);
|
|
@@ -6962,7 +6975,7 @@ async function commitAndPush(st, ts, map, resolution, repo, newManifest) {
|
|
|
6962
6975
|
if (staged.length === toDrop.length) {
|
|
6963
6976
|
log("nothing to commit");
|
|
6964
6977
|
renderNoScanTree(st);
|
|
6965
|
-
return;
|
|
6978
|
+
return "nothing";
|
|
6966
6979
|
}
|
|
6967
6980
|
st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: true });
|
|
6968
6981
|
let verdict = withSpinner("Scanning for secrets", () => scanPushVerdict(repo));
|
|
@@ -6978,6 +6991,7 @@ async function commitAndPush(st, ts, map, resolution, repo, newManifest) {
|
|
|
6978
6991
|
warn(`could not write push manifest (next push will full-rescan): ${String(err)}`);
|
|
6979
6992
|
}
|
|
6980
6993
|
renderPushTree(st, verdict);
|
|
6994
|
+
return "pushed";
|
|
6981
6995
|
}
|
|
6982
6996
|
function runDryRunPreview(st, map, repo, selection) {
|
|
6983
6997
|
st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: false });
|
|
@@ -6994,55 +7008,181 @@ function runDryRunPreview(st, map, repo, selection) {
|
|
|
6994
7008
|
init_push_checks();
|
|
6995
7009
|
init_utils();
|
|
6996
7010
|
init_utils_fs();
|
|
6997
|
-
async function
|
|
7011
|
+
async function runPushCore(opts = {}) {
|
|
6998
7012
|
const dryRun = opts.dryRun === true;
|
|
6999
7013
|
const redactAll = opts.redactAll === true;
|
|
7000
7014
|
const allowAll = opts.allowAll === true;
|
|
7001
7015
|
const allowRule = opts.allowRule;
|
|
7002
7016
|
const fullScan = opts.fullScan === true;
|
|
7003
|
-
guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
|
|
7004
7017
|
const repo = repoHome();
|
|
7005
7018
|
const backup = backupBase();
|
|
7019
|
+
console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
|
|
7020
|
+
reportSettingsAheadDrift(repo);
|
|
7021
|
+
const scannerVersion = probeGitleaks();
|
|
7022
|
+
const configHash = computeConfigHash();
|
|
7023
|
+
const old = readManifest(manifestPath());
|
|
7024
|
+
const mapPath = join51(repo, "path-map.json");
|
|
7025
|
+
const { map, selection, newManifest } = loadSelectionForPush(
|
|
7026
|
+
mapPath,
|
|
7027
|
+
old,
|
|
7028
|
+
scannerVersion,
|
|
7029
|
+
configHash,
|
|
7030
|
+
fullScan
|
|
7031
|
+
);
|
|
7032
|
+
withSpinner("Rebasing onto origin", () => rebaseBeforePush(repo));
|
|
7033
|
+
const ts = freshBackupTs(backup);
|
|
7034
|
+
const remap = withSpinner("Syncing sessions", () => remapPush(ts, { dryRun, selection }));
|
|
7035
|
+
const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
|
|
7036
|
+
if (!dryRun) {
|
|
7037
|
+
syncSkillsPush();
|
|
7038
|
+
stripGsdHooksFromBase(repo, backup);
|
|
7039
|
+
}
|
|
7040
|
+
const st = { dryRun, remap, extras, globalConfig: [] };
|
|
7041
|
+
guardGitlinks(repo);
|
|
7042
|
+
const status = gitStatusPorcelainZ(repo, { untrackedAll: true });
|
|
7043
|
+
if (!dryRun && !status) {
|
|
7044
|
+
log("nothing to commit");
|
|
7045
|
+
renderNoScanTree(st);
|
|
7046
|
+
return { tag: "nothing" };
|
|
7047
|
+
}
|
|
7048
|
+
if (map === null) {
|
|
7049
|
+
if (dryRun) {
|
|
7050
|
+
runDryRunPreview(st, null, repo, selection);
|
|
7051
|
+
return { tag: "dry" };
|
|
7052
|
+
}
|
|
7053
|
+
return die("path-map.json missing, cannot enforce push allow-list");
|
|
7054
|
+
}
|
|
7055
|
+
if (status) enforceAllowList(status, map);
|
|
7056
|
+
if (dryRun) {
|
|
7057
|
+
runDryRunPreview(st, map, repo, selection);
|
|
7058
|
+
return { tag: "dry" };
|
|
7059
|
+
}
|
|
7060
|
+
const outcome = await commitAndPush(
|
|
7061
|
+
st,
|
|
7062
|
+
ts,
|
|
7063
|
+
map,
|
|
7064
|
+
{ redactAll, allowAll, allowRule },
|
|
7065
|
+
repo,
|
|
7066
|
+
newManifest
|
|
7067
|
+
);
|
|
7068
|
+
return outcome === "nothing" ? { tag: "nothing" } : { tag: "pushed" };
|
|
7069
|
+
}
|
|
7070
|
+
async function cmdPush(opts = {}) {
|
|
7071
|
+
const dryRun = opts.dryRun === true;
|
|
7072
|
+
const redactAll = opts.redactAll === true;
|
|
7073
|
+
const allowAll = opts.allowAll === true;
|
|
7074
|
+
const allowRule = opts.allowRule;
|
|
7075
|
+
guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
|
|
7076
|
+
const repo = repoHome();
|
|
7006
7077
|
if (!existsSync43(repo)) die(`repo not cloned at ${repo}`);
|
|
7007
7078
|
const handle = acquireLock("push");
|
|
7008
7079
|
if (handle === null) process.exit(0);
|
|
7009
7080
|
try {
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
mapPath,
|
|
7018
|
-
old,
|
|
7019
|
-
scannerVersion,
|
|
7020
|
-
configHash,
|
|
7021
|
-
fullScan
|
|
7022
|
-
);
|
|
7023
|
-
withSpinner("Rebasing onto origin", () => rebaseBeforePush(repo));
|
|
7024
|
-
const ts = freshBackupTs(backup);
|
|
7025
|
-
const remap = withSpinner("Syncing sessions", () => remapPush(ts, { dryRun, selection }));
|
|
7026
|
-
const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
|
|
7027
|
-
if (!dryRun) {
|
|
7028
|
-
syncSkillsPush();
|
|
7029
|
-
stripGsdHooksFromBase(repo, backup);
|
|
7030
|
-
}
|
|
7031
|
-
const st = { dryRun, remap, extras, globalConfig: [] };
|
|
7032
|
-
guardGitlinks(repo);
|
|
7033
|
-
const status = gitStatusPorcelainZ(repo, { untrackedAll: true });
|
|
7034
|
-
if (!dryRun && !status) {
|
|
7035
|
-
log("nothing to commit");
|
|
7036
|
-
renderNoScanTree(st);
|
|
7037
|
-
return;
|
|
7081
|
+
await runPushCore(opts);
|
|
7082
|
+
} catch (err) {
|
|
7083
|
+
if (err instanceof NomadFatal) {
|
|
7084
|
+
fail(err.message);
|
|
7085
|
+
process.exitCode = 1;
|
|
7086
|
+
} else {
|
|
7087
|
+
throw err;
|
|
7038
7088
|
}
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7089
|
+
} finally {
|
|
7090
|
+
releaseLock(handle);
|
|
7091
|
+
}
|
|
7092
|
+
}
|
|
7093
|
+
|
|
7094
|
+
// src/commands.sync.ts
|
|
7095
|
+
import { existsSync as existsSync44 } from "node:fs";
|
|
7096
|
+
import { join as join52 } from "node:path";
|
|
7097
|
+
init_config();
|
|
7098
|
+
init_utils();
|
|
7099
|
+
init_utils_fs();
|
|
7100
|
+
init_utils_json();
|
|
7101
|
+
function pullHasNoSyncedItems(sections) {
|
|
7102
|
+
return sections.every(
|
|
7103
|
+
(s) => s.header === "Settings" || s.header === "Summary" || s.items.length === 0
|
|
7104
|
+
);
|
|
7105
|
+
}
|
|
7106
|
+
function isNoopSync(pull, pushOutcome) {
|
|
7107
|
+
if (!pushOutcome.ok) return false;
|
|
7108
|
+
if (pull.localOnly !== 0 || pull.divergedKeptLocal !== 0) return false;
|
|
7109
|
+
if (pushOutcome.result.tag !== "nothing") return false;
|
|
7110
|
+
return pullHasNoSyncedItems(pull.sections);
|
|
7111
|
+
}
|
|
7112
|
+
function pullPhrase(pull) {
|
|
7113
|
+
const summary = pull.sections.find((s) => s.header === "Summary");
|
|
7114
|
+
return summary?.items[0] ?? "applied";
|
|
7115
|
+
}
|
|
7116
|
+
function reconciledNotes(pull) {
|
|
7117
|
+
const notes = [];
|
|
7118
|
+
if (pull.divergedKeptLocal > 0) {
|
|
7119
|
+
notes.push(`${pull.divergedKeptLocal} diverged files kept local and pushed`);
|
|
7120
|
+
}
|
|
7121
|
+
if (pull.localOnly > 0) {
|
|
7122
|
+
notes.push(`${pull.localOnly} local-only sessions pushed`);
|
|
7123
|
+
}
|
|
7124
|
+
return notes;
|
|
7125
|
+
}
|
|
7126
|
+
function buildSyncSummarySection(pull, pushOutcome) {
|
|
7127
|
+
const s = section("Summary");
|
|
7128
|
+
if (!pushOutcome.ok) {
|
|
7129
|
+
addItem(s, `pull: applied, push: failed (${pushOutcome.message})`);
|
|
7130
|
+
return s;
|
|
7131
|
+
}
|
|
7132
|
+
addItem(s, `pull: ${pullPhrase(pull)}`);
|
|
7133
|
+
addItem(s, `push: ${pushOutcome.result.tag === "pushed" ? "pushed" : "nothing to push"}`);
|
|
7134
|
+
for (const note of reconciledNotes(pull)) addItem(s, note);
|
|
7135
|
+
return s;
|
|
7136
|
+
}
|
|
7137
|
+
function renderWetSync(pull, pushOutcome) {
|
|
7138
|
+
if (isNoopSync(pull, pushOutcome)) {
|
|
7139
|
+
ok("already in sync");
|
|
7140
|
+
return;
|
|
7141
|
+
}
|
|
7142
|
+
renderTree(pull.sections);
|
|
7143
|
+
renderTree([buildSyncSummarySection(pull, pushOutcome)]);
|
|
7144
|
+
}
|
|
7145
|
+
async function runSyncPushHalf() {
|
|
7146
|
+
try {
|
|
7147
|
+
const result = await runPushCore();
|
|
7148
|
+
return { ok: true, result };
|
|
7149
|
+
} catch (err) {
|
|
7150
|
+
if (err instanceof NomadFatal) {
|
|
7151
|
+
process.exitCode = 1;
|
|
7152
|
+
return { ok: false, message: err.message };
|
|
7153
|
+
}
|
|
7154
|
+
throw err;
|
|
7155
|
+
}
|
|
7156
|
+
}
|
|
7157
|
+
async function runSyncWet() {
|
|
7158
|
+
const pull = runPullCore();
|
|
7159
|
+
const pushOutcome = await runSyncPushHalf();
|
|
7160
|
+
renderWetSync(pull, pushOutcome);
|
|
7161
|
+
}
|
|
7162
|
+
async function runSyncDryRun(repo, backup) {
|
|
7163
|
+
const ts = freshBackupTs(backup);
|
|
7164
|
+
const mapPath = join52(repo, "path-map.json");
|
|
7165
|
+
const map = existsSync44(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
7166
|
+
computePreview(ts, map, "pull");
|
|
7167
|
+
log("push preview below is computed against pre-pull state (a real sync pushes after pull)");
|
|
7168
|
+
await runPushCore({ dryRun: true });
|
|
7169
|
+
}
|
|
7170
|
+
async function cmdSync(opts = {}) {
|
|
7171
|
+
const dryRun = opts.dryRun === true;
|
|
7172
|
+
const repo = repoHome();
|
|
7173
|
+
const backup = backupBase();
|
|
7174
|
+
if (!existsSync44(repo)) die(`repo not cloned at ${repo}`);
|
|
7175
|
+
if (!existsSync44(join52(repo, "shared", "settings.base.json"))) {
|
|
7176
|
+
die("repo not initialized; run 'nomad init' to scaffold");
|
|
7177
|
+
}
|
|
7178
|
+
const handle = acquireLock("sync");
|
|
7179
|
+
if (handle === null) process.exit(0);
|
|
7180
|
+
try {
|
|
7181
|
+
if (dryRun) {
|
|
7182
|
+
await runSyncDryRun(repo, backup);
|
|
7183
|
+
} else {
|
|
7184
|
+
await runSyncWet();
|
|
7042
7185
|
}
|
|
7043
|
-
if (status) enforceAllowList(status, map);
|
|
7044
|
-
if (dryRun) return runDryRunPreview(st, map, repo, selection);
|
|
7045
|
-
await commitAndPush(st, ts, map, { redactAll, allowAll, allowRule }, repo, newManifest);
|
|
7046
7186
|
} catch (err) {
|
|
7047
7187
|
if (err instanceof NomadFatal) {
|
|
7048
7188
|
fail(err.message);
|
|
@@ -7099,18 +7239,18 @@ init_config();
|
|
|
7099
7239
|
|
|
7100
7240
|
// src/diff.ts
|
|
7101
7241
|
init_config();
|
|
7102
|
-
import { existsSync as
|
|
7103
|
-
import { join as
|
|
7242
|
+
import { existsSync as existsSync45 } from "node:fs";
|
|
7243
|
+
import { join as join53 } from "node:path";
|
|
7104
7244
|
init_utils();
|
|
7105
7245
|
init_utils_fs();
|
|
7106
7246
|
init_utils_json();
|
|
7107
7247
|
function cmdDiff() {
|
|
7108
7248
|
try {
|
|
7109
7249
|
const repo = repoHome();
|
|
7110
|
-
if (!
|
|
7250
|
+
if (!existsSync45(repo)) die(`repo not cloned at ${repo}`);
|
|
7111
7251
|
const ts = freshBackupTs(backupBase());
|
|
7112
|
-
const mapPath =
|
|
7113
|
-
const map =
|
|
7252
|
+
const mapPath = join53(repo, "path-map.json");
|
|
7253
|
+
const map = existsSync45(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
7114
7254
|
divergenceCheckExtras(ts);
|
|
7115
7255
|
computePreview(ts, map, "diff");
|
|
7116
7256
|
} catch (err) {
|
|
@@ -7125,8 +7265,8 @@ function cmdDiff() {
|
|
|
7125
7265
|
|
|
7126
7266
|
// src/init.ts
|
|
7127
7267
|
init_config();
|
|
7128
|
-
import { existsSync as
|
|
7129
|
-
import { join as
|
|
7268
|
+
import { existsSync as existsSync47, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6 } from "node:fs";
|
|
7269
|
+
import { join as join55 } from "node:path";
|
|
7130
7270
|
|
|
7131
7271
|
// src/init.gh-onboard.ts
|
|
7132
7272
|
init_config();
|
|
@@ -7208,33 +7348,33 @@ init_config();
|
|
|
7208
7348
|
init_utils();
|
|
7209
7349
|
init_utils_fs();
|
|
7210
7350
|
init_utils_json();
|
|
7211
|
-
import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as
|
|
7212
|
-
import { join as
|
|
7351
|
+
import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync46, rmSync as rmSync15, statSync as statSync12 } from "node:fs";
|
|
7352
|
+
import { join as join54 } from "node:path";
|
|
7213
7353
|
function snapshotIntoShared(map) {
|
|
7214
7354
|
const repo = repoHome();
|
|
7215
7355
|
const claude = claudeHome();
|
|
7216
7356
|
for (const name of allSharedLinks(map)) {
|
|
7217
|
-
const src =
|
|
7218
|
-
if (!
|
|
7219
|
-
const dst =
|
|
7357
|
+
const src = join54(claude, name);
|
|
7358
|
+
if (!existsSync46(src)) continue;
|
|
7359
|
+
const dst = join54(repo, "shared", name);
|
|
7220
7360
|
if (statSync12(src).isDirectory()) {
|
|
7221
|
-
const gk =
|
|
7222
|
-
if (
|
|
7361
|
+
const gk = join54(dst, ".gitkeep");
|
|
7362
|
+
if (existsSync46(gk)) rmSync15(gk);
|
|
7223
7363
|
cpSync7(src, dst, { recursive: true, force: false, errorOnExist: true });
|
|
7224
7364
|
} else {
|
|
7225
7365
|
copyFileSync2(src, dst);
|
|
7226
7366
|
}
|
|
7227
7367
|
log(`snapshotted shared/${name} from ${src}`);
|
|
7228
7368
|
}
|
|
7229
|
-
const userSettings =
|
|
7230
|
-
if (
|
|
7369
|
+
const userSettings = join54(claude, "settings.json");
|
|
7370
|
+
if (existsSync46(userSettings)) {
|
|
7231
7371
|
let parsed;
|
|
7232
7372
|
try {
|
|
7233
7373
|
parsed = readJson(userSettings);
|
|
7234
7374
|
} catch (err) {
|
|
7235
7375
|
return die(`malformed ${userSettings}: ${err.message}`);
|
|
7236
7376
|
}
|
|
7237
|
-
const hostFile =
|
|
7377
|
+
const hostFile = join54(repo, "hosts", `${HOST}.json`);
|
|
7238
7378
|
writeJsonAtomic(hostFile, parsed);
|
|
7239
7379
|
log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
|
|
7240
7380
|
}
|
|
@@ -7247,14 +7387,14 @@ var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.cl
|
|
|
7247
7387
|
var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
|
|
7248
7388
|
function preflightConflict(repoHome2) {
|
|
7249
7389
|
const candidates = [
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7390
|
+
join55(repoHome2, "shared", "settings.base.json"),
|
|
7391
|
+
join55(repoHome2, "shared", "CLAUDE.md"),
|
|
7392
|
+
join55(repoHome2, "path-map.json"),
|
|
7393
|
+
join55(repoHome2, "hosts"),
|
|
7394
|
+
join55(repoHome2, "shared")
|
|
7255
7395
|
];
|
|
7256
7396
|
for (const c of candidates) {
|
|
7257
|
-
if (
|
|
7397
|
+
if (existsSync47(c)) return c;
|
|
7258
7398
|
}
|
|
7259
7399
|
return null;
|
|
7260
7400
|
}
|
|
@@ -7272,25 +7412,25 @@ function cmdInit(opts = {}) {
|
|
|
7272
7412
|
die(`already initialized; refusing to clobber ${conflict}`);
|
|
7273
7413
|
}
|
|
7274
7414
|
ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
|
|
7275
|
-
mkdirSync12(
|
|
7276
|
-
mkdirSync12(
|
|
7415
|
+
mkdirSync12(join55(repo, "shared"), { recursive: true });
|
|
7416
|
+
mkdirSync12(join55(repo, "hosts"), { recursive: true });
|
|
7277
7417
|
for (const name of SHARED_KEEP_DIRS) {
|
|
7278
|
-
mkdirSync12(
|
|
7418
|
+
mkdirSync12(join55(repo, "shared", name), { recursive: true });
|
|
7279
7419
|
}
|
|
7280
|
-
const userClaudeMd =
|
|
7281
|
-
if (!snapshot || !
|
|
7282
|
-
writeFileSync6(
|
|
7420
|
+
const userClaudeMd = join55(claude, "CLAUDE.md");
|
|
7421
|
+
if (!snapshot || !existsSync47(userClaudeMd)) {
|
|
7422
|
+
writeFileSync6(join55(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
|
|
7283
7423
|
item("created shared/CLAUDE.md");
|
|
7284
7424
|
}
|
|
7285
7425
|
for (const name of SHARED_KEEP_DIRS) {
|
|
7286
|
-
writeFileSync6(
|
|
7426
|
+
writeFileSync6(join55(repo, "shared", name, ".gitkeep"), "");
|
|
7287
7427
|
item(`created shared/${name}/.gitkeep`);
|
|
7288
7428
|
}
|
|
7289
|
-
writeFileSync6(
|
|
7429
|
+
writeFileSync6(join55(repo, "hosts", ".gitkeep"), "");
|
|
7290
7430
|
item("created hosts/.gitkeep");
|
|
7291
|
-
writeJsonAtomic(
|
|
7431
|
+
writeJsonAtomic(join55(repo, "shared", "settings.base.json"), {});
|
|
7292
7432
|
item("created shared/settings.base.json");
|
|
7293
|
-
writeJsonAtomic(
|
|
7433
|
+
writeJsonAtomic(join55(repo, "path-map.json"), { projects: {} });
|
|
7294
7434
|
item("created path-map.json");
|
|
7295
7435
|
if (snapshot) {
|
|
7296
7436
|
snapshotIntoShared({ projects: {} });
|
|
@@ -7356,11 +7496,11 @@ function maybeDisableRepoActions(repoHome2, run) {
|
|
|
7356
7496
|
// src/init.prompt.ts
|
|
7357
7497
|
init_config();
|
|
7358
7498
|
init_utils();
|
|
7359
|
-
import { existsSync as
|
|
7360
|
-
import { join as
|
|
7499
|
+
import { existsSync as existsSync48, readdirSync as readdirSync17, statSync as statSync13 } from "node:fs";
|
|
7500
|
+
import { join as join56 } from "node:path";
|
|
7361
7501
|
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
7362
7502
|
function nonEmptyExists(path) {
|
|
7363
|
-
if (!
|
|
7503
|
+
if (!existsSync48(path)) return false;
|
|
7364
7504
|
try {
|
|
7365
7505
|
if (statSync13(path).isDirectory()) return readdirSync17(path).length > 0;
|
|
7366
7506
|
return true;
|
|
@@ -7369,8 +7509,8 @@ function nonEmptyExists(path) {
|
|
|
7369
7509
|
}
|
|
7370
7510
|
}
|
|
7371
7511
|
function hasExistingClaudeConfig(claudeHome2) {
|
|
7372
|
-
if (
|
|
7373
|
-
return SHARED_LINKS.some((name) => nonEmptyExists(
|
|
7512
|
+
if (existsSync48(join56(claudeHome2, "settings.json"))) return true;
|
|
7513
|
+
return SHARED_LINKS.some((name) => nonEmptyExists(join56(claudeHome2, name)));
|
|
7374
7514
|
}
|
|
7375
7515
|
async function confirmSnapshotDefault(claudeHome2) {
|
|
7376
7516
|
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
@@ -7654,10 +7794,27 @@ function parsePushArgs(argv) {
|
|
|
7654
7794
|
};
|
|
7655
7795
|
}
|
|
7656
7796
|
|
|
7797
|
+
// src/nomad.dispatch.sync.ts
|
|
7798
|
+
function parseSyncArgs(argv) {
|
|
7799
|
+
let dryRun = false;
|
|
7800
|
+
let i = 3;
|
|
7801
|
+
while (i < argv.length) {
|
|
7802
|
+
const token = argv[i];
|
|
7803
|
+
if (token === "--dry-run") {
|
|
7804
|
+
if (dryRun) return null;
|
|
7805
|
+
dryRun = true;
|
|
7806
|
+
} else {
|
|
7807
|
+
return null;
|
|
7808
|
+
}
|
|
7809
|
+
i++;
|
|
7810
|
+
}
|
|
7811
|
+
return { dryRun };
|
|
7812
|
+
}
|
|
7813
|
+
|
|
7657
7814
|
// package.json
|
|
7658
7815
|
var package_default = {
|
|
7659
7816
|
name: "claude-nomad",
|
|
7660
|
-
version: "0.
|
|
7817
|
+
version: "0.57.0",
|
|
7661
7818
|
type: "module",
|
|
7662
7819
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
7663
7820
|
keywords: [
|
|
@@ -7786,6 +7943,12 @@ var DEFAULT_HELP = [
|
|
|
7786
7943
|
cont("when no finding survives. No TTY required. Never skips the scan."),
|
|
7787
7944
|
cont("Mutually exclusive with --redact-all/--allow. Cannot combine with --dry-run."),
|
|
7788
7945
|
"",
|
|
7946
|
+
row(" sync", "Pull (retain-merge), then push, under one lock. One-shot; hides ordering."),
|
|
7947
|
+
row(" --dry-run", "Stack the pull preview then the push preview; mutates nothing."),
|
|
7948
|
+
cont("Fails fast on a pull-half error; a wedged repo hints at nomad pull --force-remote"),
|
|
7949
|
+
cont("(sync itself takes no --force-remote). Mid-push leak recovery behaves exactly"),
|
|
7950
|
+
cont("like nomad push."),
|
|
7951
|
+
"",
|
|
7789
7952
|
row(" diff", "Offline preview of what `pull` would change against local repo state."),
|
|
7790
7953
|
cont("No git pull, no lock acquired."),
|
|
7791
7954
|
"",
|
|
@@ -7882,15 +8045,15 @@ var DEFAULT_HELP = [
|
|
|
7882
8045
|
init_config();
|
|
7883
8046
|
init_utils();
|
|
7884
8047
|
init_utils_json();
|
|
7885
|
-
import { existsSync as
|
|
7886
|
-
import { join as
|
|
8048
|
+
import { existsSync as existsSync49, readFileSync as readFileSync17, readdirSync as readdirSync18 } from "node:fs";
|
|
8049
|
+
import { join as join57 } from "node:path";
|
|
7887
8050
|
function resumeCmd(sessionId) {
|
|
7888
8051
|
if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
|
|
7889
8052
|
fail(`invalid session id: ${sessionId}`);
|
|
7890
8053
|
process.exit(1);
|
|
7891
8054
|
}
|
|
7892
|
-
const projectsRoot =
|
|
7893
|
-
if (!
|
|
8055
|
+
const projectsRoot = join57(claudeHome(), "projects");
|
|
8056
|
+
if (!existsSync49(projectsRoot)) {
|
|
7894
8057
|
fail(`${projectsRoot} does not exist`);
|
|
7895
8058
|
process.exit(1);
|
|
7896
8059
|
}
|
|
@@ -7904,8 +8067,8 @@ function resumeCmd(sessionId) {
|
|
|
7904
8067
|
fail(`no cwd field found in ${jsonlPath}`);
|
|
7905
8068
|
process.exit(1);
|
|
7906
8069
|
}
|
|
7907
|
-
const mapPath =
|
|
7908
|
-
if (!
|
|
8070
|
+
const mapPath = join57(repoHome(), "path-map.json");
|
|
8071
|
+
if (!existsSync49(mapPath)) {
|
|
7909
8072
|
fail("path-map.json missing");
|
|
7910
8073
|
process.exit(1);
|
|
7911
8074
|
}
|
|
@@ -7928,8 +8091,8 @@ function resumeCmd(sessionId) {
|
|
|
7928
8091
|
}
|
|
7929
8092
|
function findTranscriptPath(projectsRoot, sessionId) {
|
|
7930
8093
|
for (const dir of readdirSync18(projectsRoot)) {
|
|
7931
|
-
const candidate =
|
|
7932
|
-
if (
|
|
8094
|
+
const candidate = join57(projectsRoot, dir, `${sessionId}.jsonl`);
|
|
8095
|
+
if (existsSync49(candidate)) return candidate;
|
|
7933
8096
|
}
|
|
7934
8097
|
return null;
|
|
7935
8098
|
}
|
|
@@ -8007,6 +8170,15 @@ try {
|
|
|
8007
8170
|
});
|
|
8008
8171
|
break;
|
|
8009
8172
|
}
|
|
8173
|
+
case "sync": {
|
|
8174
|
+
const syncArgs = parseSyncArgs(process.argv);
|
|
8175
|
+
if (syncArgs === null) {
|
|
8176
|
+
console.error("usage: nomad sync [--dry-run]");
|
|
8177
|
+
process.exit(1);
|
|
8178
|
+
}
|
|
8179
|
+
await cmdSync({ dryRun: syncArgs.dryRun });
|
|
8180
|
+
break;
|
|
8181
|
+
}
|
|
8010
8182
|
case "init": {
|
|
8011
8183
|
const initArgs = parseInitArgs(process.argv);
|
|
8012
8184
|
if (initArgs === null) {
|
package/package.json
CHANGED