claude-nomad 0.59.0 → 0.60.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 +22 -0
- package/README.md +7 -6
- package/dist/nomad.mjs +306 -84
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.60.0](https://github.com/funkadelic/claude-nomad/compare/v0.59.0...v0.60.0) (2026-07-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
* **links:** preserve live gsd hook entries across regenerateSettings ([#421](https://github.com/funkadelic/claude-nomad/issues/421)) ([10a18ca](https://github.com/funkadelic/claude-nomad/commit/10a18ca7ed6c1851cdc43e46ad616527d41a1f92))
|
|
9
|
+
* **sync:** compact output with merged tree and no-op collapse ([#415](https://github.com/funkadelic/claude-nomad/issues/415)) ([9bb9cdd](https://github.com/funkadelic/claude-nomad/commit/9bb9cdd7e901ea463d5c1fb5b66ffc39b2864f8a))
|
|
10
|
+
* **sync:** compact summary-only output by default, add --verbose ([#420](https://github.com/funkadelic/claude-nomad/issues/420)) ([b5b95b3](https://github.com/funkadelic/claude-nomad/commit/b5b95b372634a0b372023b399e7f16bb0063ff74))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
* **lint:** enable sonarjs recommended ruleset with triaged exclusions ([#422](https://github.com/funkadelic/claude-nomad/issues/422)) ([f92fce8](https://github.com/funkadelic/claude-nomad/commit/f92fce8533e06814d5e3f58c1f120adf3e00ded7))
|
|
16
|
+
* **tests:** make Defender exclusion step non-fatal ([#419](https://github.com/funkadelic/claude-nomad/issues/419)) ([413a906](https://github.com/funkadelic/claude-nomad/commit/413a9065ef152dfaedd76b325de749070e2165ab))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Testing
|
|
20
|
+
|
|
21
|
+
* **integration:** cover extras deny-set, host overrides, and skills sync in round-trip ([#418](https://github.com/funkadelic/claude-nomad/issues/418)) ([70074b5](https://github.com/funkadelic/claude-nomad/commit/70074b5c5a8831a93726576e346e7e89ac511ffa))
|
|
22
|
+
* **pull:** fix flaky incomingChanges tests via shared mock instance ([#417](https://github.com/funkadelic/claude-nomad/issues/417)) ([6589887](https://github.com/funkadelic/claude-nomad/commit/6589887d0ac1c77f7696b89a1a518e469e913dae))
|
|
23
|
+
* **vitest:** raise subprocess project maxWorkers to 2 ([#423](https://github.com/funkadelic/claude-nomad/issues/423)) ([8fa28b5](https://github.com/funkadelic/claude-nomad/commit/8fa28b5a2ef4cae4816f5b6ad23bf90189a611d1))
|
|
24
|
+
|
|
3
25
|
## [0.59.0](https://github.com/funkadelic/claude-nomad/compare/v0.58.1...v0.59.0) (2026-07-14)
|
|
4
26
|
|
|
5
27
|
|
package/README.md
CHANGED
|
@@ -174,12 +174,13 @@ $ nomad sync # pull config, then publish local changes, in one step
|
|
|
174
174
|
|
|
175
175
|
`nomad sync` is the command to reach for day to day: it always pulls first (so changes from your
|
|
176
176
|
other machines land before anything is pushed, and work that exists only on this machine is kept,
|
|
177
|
-
not deleted) and then pushes, under one lock, so there is no ordering to remember.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
[
|
|
177
|
+
not deleted) and then pushes, under one lock, so there is no ordering to remember. Output is compact
|
|
178
|
+
by default: a run prints only a short Sync summary, not the full status tree; pass
|
|
179
|
+
`nomad sync --verbose` (or `--all` / `-v`) to see the full tree. `nomad pull` and `nomad push` are
|
|
180
|
+
still available as lower-level commands for cases `sync` does not cover: recovering a wedged repo
|
|
181
|
+
with `nomad pull --force-remote`, or resolving a detected secret without the interactive menu via
|
|
182
|
+
`nomad push --redact-all` / `--allow` / `--allow-all` (see [Changing settings](#changing-settings)
|
|
183
|
+
and [Recovery flows](https://funkadelic.github.io/claude-nomad/recovery/)). The
|
|
183
184
|
[FAQ](https://funkadelic.github.io/claude-nomad/faq/) covers what `sync` does under the hood and the
|
|
184
185
|
push/pull order it enforces.
|
|
185
186
|
|
package/dist/nomad.mjs
CHANGED
|
@@ -1353,6 +1353,75 @@ function stripGsdHookEntries(settings) {
|
|
|
1353
1353
|
}
|
|
1354
1354
|
return out;
|
|
1355
1355
|
}
|
|
1356
|
+
function keepMatcherEntry(entry) {
|
|
1357
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
1358
|
+
const entryObj = entry;
|
|
1359
|
+
if (!Array.isArray(entryObj.hooks)) return null;
|
|
1360
|
+
const innerHooks = entryObj.hooks;
|
|
1361
|
+
const kept = innerHooks.filter((h2) => {
|
|
1362
|
+
if (h2 === null || typeof h2 !== "object" || Array.isArray(h2)) return false;
|
|
1363
|
+
const hookObj = h2;
|
|
1364
|
+
const cmd = hookObj.command;
|
|
1365
|
+
return isGsdHookEntry(typeof cmd === "string" ? cmd : "");
|
|
1366
|
+
});
|
|
1367
|
+
if (kept.length === 0) return null;
|
|
1368
|
+
return { ...entryObj, hooks: kept };
|
|
1369
|
+
}
|
|
1370
|
+
function keepEventMatchers(matchers) {
|
|
1371
|
+
if (!Array.isArray(matchers)) return null;
|
|
1372
|
+
const kept = [];
|
|
1373
|
+
for (const entry of matchers) {
|
|
1374
|
+
const result = keepMatcherEntry(entry);
|
|
1375
|
+
if (result !== null) kept.push(result);
|
|
1376
|
+
}
|
|
1377
|
+
return kept.length === 0 ? null : kept;
|
|
1378
|
+
}
|
|
1379
|
+
function keepGsdHookEntries(settings) {
|
|
1380
|
+
const hooksVal = settings.hooks;
|
|
1381
|
+
if (hooksVal === null || typeof hooksVal !== "object" || Array.isArray(hooksVal)) return {};
|
|
1382
|
+
const hooksObj = hooksVal;
|
|
1383
|
+
const keptHooks = {};
|
|
1384
|
+
for (const [event, matchers] of Object.entries(hooksObj)) {
|
|
1385
|
+
const kept = keepEventMatchers(matchers);
|
|
1386
|
+
if (kept !== null) keptHooks[event] = kept;
|
|
1387
|
+
}
|
|
1388
|
+
return Object.keys(keptHooks).length === 0 ? {} : { hooks: keptHooks };
|
|
1389
|
+
}
|
|
1390
|
+
function asPlainObject(value) {
|
|
1391
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1392
|
+
return value;
|
|
1393
|
+
}
|
|
1394
|
+
function canonicalMatcherKey(m) {
|
|
1395
|
+
const obj = asPlainObject(m);
|
|
1396
|
+
if (obj === null) return JSON.stringify(m);
|
|
1397
|
+
return JSON.stringify(
|
|
1398
|
+
Object.keys(obj).sort((a, b) => a.localeCompare(b)).map((k) => [k, obj[k]])
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
function unionMatcherArrays(baseMatchers, gsdMatchers) {
|
|
1402
|
+
const seen = new Set(baseMatchers.map(canonicalMatcherKey));
|
|
1403
|
+
const merged = [...baseMatchers];
|
|
1404
|
+
for (const m of gsdMatchers) {
|
|
1405
|
+
const key = canonicalMatcherKey(m);
|
|
1406
|
+
if (!seen.has(key)) {
|
|
1407
|
+
merged.push(m);
|
|
1408
|
+
seen.add(key);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return merged;
|
|
1412
|
+
}
|
|
1413
|
+
function graftGsdHookEntries(base, gsdOnly) {
|
|
1414
|
+
const gsdHooks = asPlainObject(gsdOnly.hooks);
|
|
1415
|
+
if (gsdHooks === null || Object.keys(gsdHooks).length === 0) return base;
|
|
1416
|
+
const baseHooks = asPlainObject(base.hooks);
|
|
1417
|
+
const mergedHooks = baseHooks ? { ...baseHooks } : {};
|
|
1418
|
+
for (const [event, gsdMatchers] of Object.entries(gsdHooks)) {
|
|
1419
|
+
if (!Array.isArray(gsdMatchers)) continue;
|
|
1420
|
+
const baseMatchers = mergedHooks[event];
|
|
1421
|
+
mergedHooks[event] = Array.isArray(baseMatchers) ? unionMatcherArrays(baseMatchers, gsdMatchers) : gsdMatchers;
|
|
1422
|
+
}
|
|
1423
|
+
return { ...base, hooks: mergedHooks };
|
|
1424
|
+
}
|
|
1356
1425
|
function matcherHasGsdEntry(entry) {
|
|
1357
1426
|
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return false;
|
|
1358
1427
|
const entryObj = entry;
|
|
@@ -1804,6 +1873,34 @@ function syncSharedLinksPush(map) {
|
|
|
1804
1873
|
copyExtrasFiltered(localPath, join5(repo, "shared", name), ALWAYS_NEVER_SYNC);
|
|
1805
1874
|
}
|
|
1806
1875
|
}
|
|
1876
|
+
function readExistingSettings(settingsPath) {
|
|
1877
|
+
if (!existsSync4(settingsPath)) return { existing: {}, present: false, malformed: false };
|
|
1878
|
+
try {
|
|
1879
|
+
const parsed = readJson(settingsPath);
|
|
1880
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1881
|
+
return { existing: {}, present: true, malformed: true };
|
|
1882
|
+
}
|
|
1883
|
+
return { existing: parsed, present: true, malformed: false };
|
|
1884
|
+
} catch {
|
|
1885
|
+
return { existing: {}, present: true, malformed: true };
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
function emitDriftWarnings(merged, existing) {
|
|
1889
|
+
const drift = classifySettingsDrift(merged, existing);
|
|
1890
|
+
if (drift.behind.length > 0) {
|
|
1891
|
+
const { phrase, pronoun } = describeSettings(drift.behind);
|
|
1892
|
+
warn(
|
|
1893
|
+
`your settings.json is missing ${phrase} that the synced copy has; run 'nomad pull' to restore ${pronoun}.`
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
const { promotable } = partitionByCaptureExclusion(drift.ahead);
|
|
1897
|
+
if (promotable.length > 0) {
|
|
1898
|
+
const { phrase, pronoun, verb } = describeSettings(promotable);
|
|
1899
|
+
warn(
|
|
1900
|
+
`your settings.json has ${phrase} that ${verb} not yet synced; run 'nomad capture-settings' to save ${pronoun} to the repo before the next pull overwrites ${pronoun}.`
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1807
1904
|
function regenerateSettings(ts, opts = {}) {
|
|
1808
1905
|
const dryRun = opts.dryRun === true;
|
|
1809
1906
|
const suppressDriftWarn = opts.suppressDriftWarn === true;
|
|
@@ -1819,25 +1916,12 @@ function regenerateSettings(ts, opts = {}) {
|
|
|
1819
1916
|
const overrides = hasOverrides ? readJson(hostPath) : {};
|
|
1820
1917
|
const merged = deepMerge(base, overrides);
|
|
1821
1918
|
const settingsPath = join5(claude, "settings.json");
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
const drift = classifySettingsDrift(merged, existing);
|
|
1826
|
-
if (drift.behind.length > 0) {
|
|
1827
|
-
const { phrase, pronoun } = describeSettings(drift.behind);
|
|
1828
|
-
warn(
|
|
1829
|
-
`your settings.json is missing ${phrase} that the synced copy has; run 'nomad pull' to restore ${pronoun}.`
|
|
1830
|
-
);
|
|
1831
|
-
}
|
|
1832
|
-
const { promotable } = partitionByCaptureExclusion(drift.ahead);
|
|
1833
|
-
if (promotable.length > 0) {
|
|
1834
|
-
const { phrase, pronoun, verb } = describeSettings(promotable);
|
|
1835
|
-
warn(
|
|
1836
|
-
`your settings.json has ${phrase} that ${verb} not yet synced; run 'nomad capture-settings' to save ${pronoun} to the repo before the next pull overwrites ${pronoun}.`
|
|
1837
|
-
);
|
|
1838
|
-
}
|
|
1839
|
-
} catch {
|
|
1919
|
+
const { existing, present, malformed } = readExistingSettings(settingsPath);
|
|
1920
|
+
if (!suppressDriftWarn && present) {
|
|
1921
|
+
if (malformed) {
|
|
1840
1922
|
warn("existing settings.json is malformed; skipping drift-check and regenerating.");
|
|
1923
|
+
} else {
|
|
1924
|
+
emitDriftWarnings(merged, existing);
|
|
1841
1925
|
}
|
|
1842
1926
|
}
|
|
1843
1927
|
const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
|
|
@@ -1846,7 +1930,10 @@ function regenerateSettings(ts, opts = {}) {
|
|
|
1846
1930
|
return { label: overrideLabel };
|
|
1847
1931
|
}
|
|
1848
1932
|
backupBeforeWrite(settingsPath, ts);
|
|
1849
|
-
writeJsonAtomic(
|
|
1933
|
+
writeJsonAtomic(
|
|
1934
|
+
settingsPath,
|
|
1935
|
+
graftGsdHookEntries(stripGsdHookEntries(merged), keepGsdHookEntries(existing))
|
|
1936
|
+
);
|
|
1850
1937
|
return { label: overrideLabel };
|
|
1851
1938
|
}
|
|
1852
1939
|
|
|
@@ -5818,19 +5905,25 @@ function summarySection(st) {
|
|
|
5818
5905
|
addItem(s, summaryRow("push", unmapped, st.remap.collisions, st.extras.skipped));
|
|
5819
5906
|
return s;
|
|
5820
5907
|
}
|
|
5821
|
-
function
|
|
5908
|
+
function buildPushTreeSections(st, verdict) {
|
|
5822
5909
|
const leakScan = section("Leak scan");
|
|
5823
5910
|
addItem(leakScan, verdict.verdictRow);
|
|
5824
|
-
|
|
5911
|
+
return [...syncedSections(st), leakScan, summarySection(st)];
|
|
5825
5912
|
}
|
|
5826
|
-
function
|
|
5913
|
+
function renderPushTree(st, verdict) {
|
|
5914
|
+
renderTree(buildPushTreeSections(st, verdict));
|
|
5915
|
+
}
|
|
5916
|
+
function buildNoScanSections(st, opts = {}) {
|
|
5827
5917
|
const sections = [];
|
|
5828
5918
|
if (opts.noMapHint === true) {
|
|
5829
5919
|
const pathMap = section("Path map");
|
|
5830
5920
|
addItem(pathMap, `${dim(infoGlyph)} no path-map.json (nothing to preview)`);
|
|
5831
5921
|
sections.push(pathMap);
|
|
5832
5922
|
}
|
|
5833
|
-
|
|
5923
|
+
return [...sections, ...syncedSections(st), summarySection(st)];
|
|
5924
|
+
}
|
|
5925
|
+
function renderNoScanTree(st, opts = {}) {
|
|
5926
|
+
renderTree(buildNoScanSections(st, opts));
|
|
5834
5927
|
}
|
|
5835
5928
|
|
|
5836
5929
|
// src/commands.pull.ts
|
|
@@ -6746,17 +6839,9 @@ function buildWetPullSections(ts, map, prePostHeads) {
|
|
|
6746
6839
|
const remapResult = withSpinner("Syncing sessions", () => remapPull(ts));
|
|
6747
6840
|
const extrasResult = remapExtrasPull(ts, { prePostHeads });
|
|
6748
6841
|
const localOnly = scanLocalOnly();
|
|
6842
|
+
const unmapped = remapResult.unmapped + extrasResult.unmapped;
|
|
6749
6843
|
const summary = section(PULL_SUMMARY_HEADER);
|
|
6750
|
-
addItem(
|
|
6751
|
-
summary,
|
|
6752
|
-
summaryRow(
|
|
6753
|
-
"pull",
|
|
6754
|
-
remapResult.unmapped + extrasResult.unmapped,
|
|
6755
|
-
0,
|
|
6756
|
-
extrasResult.skipped,
|
|
6757
|
-
localOnly
|
|
6758
|
-
)
|
|
6759
|
-
);
|
|
6844
|
+
addItem(summary, summaryRow("pull", unmapped, 0, extrasResult.skipped, localOnly));
|
|
6760
6845
|
return {
|
|
6761
6846
|
sections: [
|
|
6762
6847
|
buildSettingsSection(label),
|
|
@@ -6764,7 +6849,10 @@ function buildWetPullSections(ts, map, prePostHeads) {
|
|
|
6764
6849
|
buildExtrasSection(extrasResult.pulled, extrasResult.skipped),
|
|
6765
6850
|
summary
|
|
6766
6851
|
],
|
|
6767
|
-
localOnly
|
|
6852
|
+
localOnly,
|
|
6853
|
+
settingsLabel: label,
|
|
6854
|
+
unmapped,
|
|
6855
|
+
extrasSkipped: extrasResult.skipped
|
|
6768
6856
|
};
|
|
6769
6857
|
}
|
|
6770
6858
|
function handleWedge(repo, forceRemote) {
|
|
@@ -6788,6 +6876,7 @@ function handleWedge(repo, forceRemote) {
|
|
|
6788
6876
|
function runPullCore(opts = {}) {
|
|
6789
6877
|
const dryRun = opts.dryRun === true;
|
|
6790
6878
|
const forceRemote = opts.forceRemote === true;
|
|
6879
|
+
const compose = opts.compose === true;
|
|
6791
6880
|
const repo = repoHome();
|
|
6792
6881
|
const backup = backupBase();
|
|
6793
6882
|
const ts = freshBackupTs(backup);
|
|
@@ -6800,9 +6889,11 @@ function runPullCore(opts = {}) {
|
|
|
6800
6889
|
die(`could not create backup dir: ${err.message}`);
|
|
6801
6890
|
}
|
|
6802
6891
|
}
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6892
|
+
if (!compose) {
|
|
6893
|
+
log(
|
|
6894
|
+
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
6895
|
+
);
|
|
6896
|
+
}
|
|
6806
6897
|
const prePostHeads = capturePrePostHeads(repo, () => {
|
|
6807
6898
|
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
|
|
6808
6899
|
});
|
|
@@ -6814,8 +6905,22 @@ function runPullCore(opts = {}) {
|
|
|
6814
6905
|
log("dry-run complete; no mutation");
|
|
6815
6906
|
return { tag: "dry" };
|
|
6816
6907
|
}
|
|
6817
|
-
const { sections, localOnly } = buildWetPullSections(
|
|
6818
|
-
|
|
6908
|
+
const { sections, localOnly, settingsLabel, unmapped, extrasSkipped } = buildWetPullSections(
|
|
6909
|
+
ts,
|
|
6910
|
+
map,
|
|
6911
|
+
prePostHeads
|
|
6912
|
+
);
|
|
6913
|
+
const incomingChanges = prePostHeads === void 0 ? true : prePostHeads.pre !== prePostHeads.post;
|
|
6914
|
+
return {
|
|
6915
|
+
tag: "wet",
|
|
6916
|
+
sections,
|
|
6917
|
+
localOnly,
|
|
6918
|
+
divergedKeptLocal,
|
|
6919
|
+
incomingChanges,
|
|
6920
|
+
settingsLabel,
|
|
6921
|
+
unmapped,
|
|
6922
|
+
extrasSkipped
|
|
6923
|
+
};
|
|
6819
6924
|
}
|
|
6820
6925
|
function cmdPull(opts = {}) {
|
|
6821
6926
|
const repo = repoHome();
|
|
@@ -7242,7 +7347,7 @@ function previewPushLeaks(map, opts = {}) {
|
|
|
7242
7347
|
|
|
7243
7348
|
// src/commands.push.steps.ts
|
|
7244
7349
|
init_utils();
|
|
7245
|
-
async function commitAndPush(st, ts, map, resolution, repo, newManifest) {
|
|
7350
|
+
async function commitAndPush(st, ts, map, resolution, repo, newManifest, render) {
|
|
7246
7351
|
gitOrFatal(["add", "-A"], "git add", repo);
|
|
7247
7352
|
const staged = parsePorcelainZ2(gitStatusPorcelainZ(repo));
|
|
7248
7353
|
const toDrop = staged.filter((p) => isGsdDropped(p));
|
|
@@ -7250,13 +7355,18 @@ async function commitAndPush(st, ts, map, resolution, repo, newManifest) {
|
|
|
7250
7355
|
gitOrFatal(["restore", "--staged", "--", ...toDrop], "git restore --staged", repo);
|
|
7251
7356
|
}
|
|
7252
7357
|
if (staged.length === toDrop.length) {
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7358
|
+
const sections2 = buildNoScanSections(st);
|
|
7359
|
+
if (render) {
|
|
7360
|
+
log("nothing to commit");
|
|
7361
|
+
renderTree(sections2);
|
|
7362
|
+
}
|
|
7363
|
+
return { outcome: "nothing", sections: sections2 };
|
|
7256
7364
|
}
|
|
7257
7365
|
st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: true });
|
|
7258
7366
|
let verdict = withSpinner("Scanning for secrets", () => scanPushVerdict(repo));
|
|
7367
|
+
const hadLeak = verdict.leak;
|
|
7259
7368
|
if (verdict.leak) {
|
|
7369
|
+
if (!render) log("push (leak recovery)");
|
|
7260
7370
|
renderPushTree(st, verdict);
|
|
7261
7371
|
verdict = await resolveLeakFindings(verdict, ts, map, resolution);
|
|
7262
7372
|
}
|
|
@@ -7267,8 +7377,13 @@ async function commitAndPush(st, ts, map, resolution, repo, newManifest) {
|
|
|
7267
7377
|
} catch (err) {
|
|
7268
7378
|
warn(`could not write push manifest (next push will full-rescan): ${String(err)}`);
|
|
7269
7379
|
}
|
|
7270
|
-
|
|
7271
|
-
|
|
7380
|
+
if (hadLeak) {
|
|
7381
|
+
renderPushTree(st, verdict);
|
|
7382
|
+
return { outcome: "pushed", sections: [] };
|
|
7383
|
+
}
|
|
7384
|
+
const sections = buildPushTreeSections(st, verdict);
|
|
7385
|
+
if (render) renderTree(sections);
|
|
7386
|
+
return { outcome: "pushed", sections };
|
|
7272
7387
|
}
|
|
7273
7388
|
function runDryRunPreview(st, map, repo, selection) {
|
|
7274
7389
|
st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: false });
|
|
@@ -7285,15 +7400,56 @@ function runDryRunPreview(st, map, repo, selection) {
|
|
|
7285
7400
|
init_push_checks();
|
|
7286
7401
|
init_utils();
|
|
7287
7402
|
init_utils_fs();
|
|
7403
|
+
function aheadOfUpstream(repo) {
|
|
7404
|
+
try {
|
|
7405
|
+
const raw = gitCaptureRaw(["rev-list", "--count", "@{u}..HEAD"], repo);
|
|
7406
|
+
return Number.parseInt(raw.trim(), 10) > 0;
|
|
7407
|
+
} catch {
|
|
7408
|
+
return false;
|
|
7409
|
+
}
|
|
7410
|
+
}
|
|
7411
|
+
function emptyStatusResult(st, compose, repo) {
|
|
7412
|
+
if (compose) {
|
|
7413
|
+
return {
|
|
7414
|
+
tag: "nothing",
|
|
7415
|
+
sections: buildNoScanSections(st),
|
|
7416
|
+
aheadOfOrigin: aheadOfUpstream(repo),
|
|
7417
|
+
globalConfigCount: st.globalConfig.length,
|
|
7418
|
+
collisions: st.remap.collisions
|
|
7419
|
+
};
|
|
7420
|
+
}
|
|
7421
|
+
log("nothing to commit");
|
|
7422
|
+
renderNoScanTree(st);
|
|
7423
|
+
return { tag: "nothing" };
|
|
7424
|
+
}
|
|
7425
|
+
function toPushCoreResult(outcome, sections, compose, repo, st) {
|
|
7426
|
+
if (!compose) return { tag: outcome };
|
|
7427
|
+
const globalConfigCount = st.globalConfig.length;
|
|
7428
|
+
const collisions = st.remap.collisions;
|
|
7429
|
+
if (outcome === "nothing") {
|
|
7430
|
+
return {
|
|
7431
|
+
tag: "nothing",
|
|
7432
|
+
sections,
|
|
7433
|
+
aheadOfOrigin: aheadOfUpstream(repo),
|
|
7434
|
+
globalConfigCount,
|
|
7435
|
+
collisions
|
|
7436
|
+
};
|
|
7437
|
+
}
|
|
7438
|
+
return { tag: "pushed", sections, globalConfigCount, collisions };
|
|
7439
|
+
}
|
|
7288
7440
|
async function runPushCore(opts = {}) {
|
|
7289
7441
|
const dryRun = opts.dryRun === true;
|
|
7290
7442
|
const redactAll = opts.redactAll === true;
|
|
7291
7443
|
const allowAll = opts.allowAll === true;
|
|
7292
7444
|
const allowRule = opts.allowRule;
|
|
7293
7445
|
const fullScan = opts.fullScan === true;
|
|
7446
|
+
const compose = opts.compose === true;
|
|
7447
|
+
const render = !compose;
|
|
7294
7448
|
const repo = repoHome();
|
|
7295
7449
|
const backup = backupBase();
|
|
7296
|
-
|
|
7450
|
+
if (!compose) {
|
|
7451
|
+
console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
|
|
7452
|
+
}
|
|
7297
7453
|
reportSettingsAheadDrift(repo);
|
|
7298
7454
|
const scannerVersion = probeGitleaks();
|
|
7299
7455
|
const configHash = computeConfigHash();
|
|
@@ -7318,11 +7474,7 @@ async function runPushCore(opts = {}) {
|
|
|
7318
7474
|
const st = { dryRun, remap, extras, globalConfig: [] };
|
|
7319
7475
|
guardGitlinks(repo);
|
|
7320
7476
|
const status = gitStatusPorcelainZ(repo, { untrackedAll: true });
|
|
7321
|
-
if (!dryRun && !status)
|
|
7322
|
-
log("nothing to commit");
|
|
7323
|
-
renderNoScanTree(st);
|
|
7324
|
-
return { tag: "nothing" };
|
|
7325
|
-
}
|
|
7477
|
+
if (!dryRun && !status) return emptyStatusResult(st, compose, repo);
|
|
7326
7478
|
if (map === null) {
|
|
7327
7479
|
if (dryRun) {
|
|
7328
7480
|
runDryRunPreview(st, null, repo, selection);
|
|
@@ -7335,15 +7487,16 @@ async function runPushCore(opts = {}) {
|
|
|
7335
7487
|
runDryRunPreview(st, map, repo, selection);
|
|
7336
7488
|
return { tag: "dry" };
|
|
7337
7489
|
}
|
|
7338
|
-
const outcome = await commitAndPush(
|
|
7490
|
+
const { outcome, sections } = await commitAndPush(
|
|
7339
7491
|
st,
|
|
7340
7492
|
ts,
|
|
7341
7493
|
map,
|
|
7342
7494
|
{ redactAll, allowAll, allowRule },
|
|
7343
7495
|
repo,
|
|
7344
|
-
newManifest
|
|
7496
|
+
newManifest,
|
|
7497
|
+
render
|
|
7345
7498
|
);
|
|
7346
|
-
return outcome
|
|
7499
|
+
return toPushCoreResult(outcome, sections, compose, repo, st);
|
|
7347
7500
|
}
|
|
7348
7501
|
async function cmdPush(opts = {}) {
|
|
7349
7502
|
const dryRun = opts.dryRun === true;
|
|
@@ -7373,33 +7526,56 @@ async function cmdPush(opts = {}) {
|
|
|
7373
7526
|
import { existsSync as existsSync46 } from "node:fs";
|
|
7374
7527
|
import { join as join54 } from "node:path";
|
|
7375
7528
|
init_config();
|
|
7529
|
+
init_color();
|
|
7376
7530
|
init_utils();
|
|
7377
7531
|
init_utils_fs();
|
|
7378
7532
|
init_utils_json();
|
|
7379
|
-
function pullHasNoSyncedItems(sections) {
|
|
7380
|
-
return sections.every(
|
|
7381
|
-
(s) => s.header === "Settings" || s.header === PULL_SUMMARY_HEADER || s.items.length === 0
|
|
7382
|
-
);
|
|
7383
|
-
}
|
|
7384
7533
|
function isNoopSync(pull, pushOutcome) {
|
|
7385
7534
|
if (!pushOutcome.ok) return false;
|
|
7386
7535
|
if (pull.localOnly !== 0 || pull.divergedKeptLocal !== 0) return false;
|
|
7387
7536
|
if (pushOutcome.result.tag !== "nothing") return false;
|
|
7388
|
-
|
|
7537
|
+
if (pushOutcome.result.aheadOfOrigin === true) return false;
|
|
7538
|
+
return !pull.incomingChanges;
|
|
7389
7539
|
}
|
|
7390
|
-
function
|
|
7391
|
-
const
|
|
7392
|
-
return
|
|
7540
|
+
function buildPullSummaryRow(pull) {
|
|
7541
|
+
const applied = pull.incomingChanges ? "upstream changes applied" : "no upstream changes";
|
|
7542
|
+
return `pull: ${applied}; settings regenerated (base + ${pull.settingsLabel})`;
|
|
7393
7543
|
}
|
|
7394
|
-
function
|
|
7395
|
-
|
|
7544
|
+
function countNoun(n, singular, plural) {
|
|
7545
|
+
return `${n} ${n === 1 ? singular : plural}`;
|
|
7546
|
+
}
|
|
7547
|
+
function pushedParenParts(pull, globalConfigCount) {
|
|
7548
|
+
const parts = [];
|
|
7549
|
+
if (pull.localOnly > 0) {
|
|
7550
|
+
parts.push(countNoun(pull.localOnly, "local-only session", "local-only sessions"));
|
|
7551
|
+
}
|
|
7396
7552
|
if (pull.divergedKeptLocal > 0) {
|
|
7397
|
-
|
|
7553
|
+
parts.push(countNoun(pull.divergedKeptLocal, "diverged extras file", "diverged extras files"));
|
|
7398
7554
|
}
|
|
7399
|
-
if (
|
|
7400
|
-
|
|
7555
|
+
if (globalConfigCount > 0) {
|
|
7556
|
+
parts.push(countNoun(globalConfigCount, "config file", "config files"));
|
|
7557
|
+
}
|
|
7558
|
+
return parts;
|
|
7559
|
+
}
|
|
7560
|
+
function buildPushSummaryRow(pull, result) {
|
|
7561
|
+
if (result.tag !== "pushed") return "push: nothing to push";
|
|
7562
|
+
const parts = pushedParenParts(pull, result.globalConfigCount ?? 0);
|
|
7563
|
+
return parts.length > 0 ? `push: pushed (${parts.join(", ")})` : "push: pushed";
|
|
7564
|
+
}
|
|
7565
|
+
function buildSkipAndCollisionRows(pull, result) {
|
|
7566
|
+
const rows = [];
|
|
7567
|
+
if (pull.unmapped > 0) {
|
|
7568
|
+
rows.push(`${dim(infoGlyph)} ${pull.unmapped} not in path-map (run nomad doctor to list)`);
|
|
7569
|
+
}
|
|
7570
|
+
if (pull.extrasSkipped > 0) {
|
|
7571
|
+
rows.push(`${dim(infoGlyph)} ${pull.extrasSkipped} extras skipped`);
|
|
7572
|
+
}
|
|
7573
|
+
const collisions = result.tag === "dry" ? void 0 : result.collisions;
|
|
7574
|
+
if (collisions !== void 0 && collisions > 0) {
|
|
7575
|
+
const phrase = countNoun(collisions, "collision", "collisions");
|
|
7576
|
+
rows.push(`${yellow(warnGlyph)} ${phrase} (run nomad doctor to list)`);
|
|
7401
7577
|
}
|
|
7402
|
-
return
|
|
7578
|
+
return rows;
|
|
7403
7579
|
}
|
|
7404
7580
|
function buildSyncSummarySection(pull, pushOutcome) {
|
|
7405
7581
|
const s = section("Sync summary");
|
|
@@ -7407,22 +7583,59 @@ function buildSyncSummarySection(pull, pushOutcome) {
|
|
|
7407
7583
|
addItem(s, `pull: applied, push: failed (${pushOutcome.message})`);
|
|
7408
7584
|
return s;
|
|
7409
7585
|
}
|
|
7410
|
-
|
|
7411
|
-
addItem(s,
|
|
7412
|
-
|
|
7586
|
+
const { result } = pushOutcome;
|
|
7587
|
+
addItem(s, buildPullSummaryRow(pull));
|
|
7588
|
+
addItem(s, buildPushSummaryRow(pull, result));
|
|
7589
|
+
if (result.tag === "nothing" && result.aheadOfOrigin === true) {
|
|
7590
|
+
addItem(s, "sync repo has unpushed commits");
|
|
7591
|
+
}
|
|
7592
|
+
for (const row2 of buildSkipAndCollisionRows(pull, result)) addItem(s, row2);
|
|
7413
7593
|
return s;
|
|
7414
7594
|
}
|
|
7415
|
-
|
|
7595
|
+
var SYNC_SECTION_ORDER = ["Settings", "Global config", "Sessions", "Extras", "Leak scan"];
|
|
7596
|
+
function isDroppedSummaryHeader(header) {
|
|
7597
|
+
return header === PULL_SUMMARY_HEADER || header === "Push summary";
|
|
7598
|
+
}
|
|
7599
|
+
function mergeSyncSections(pullSections, pushSections) {
|
|
7600
|
+
const byHeader = new Map(
|
|
7601
|
+
SYNC_SECTION_ORDER.map((header) => [header, section(header)])
|
|
7602
|
+
);
|
|
7603
|
+
for (const s of [...pullSections, ...pushSections]) {
|
|
7604
|
+
if (isDroppedSummaryHeader(s.header)) continue;
|
|
7605
|
+
let target = byHeader.get(s.header);
|
|
7606
|
+
if (target === void 0) {
|
|
7607
|
+
target = section(s.header);
|
|
7608
|
+
byHeader.set(s.header, target);
|
|
7609
|
+
}
|
|
7610
|
+
for (const item2 of s.items) {
|
|
7611
|
+
if (!target.items.includes(item2)) addItem(target, item2);
|
|
7612
|
+
}
|
|
7613
|
+
}
|
|
7614
|
+
return [...byHeader.values()].filter((s) => s.items.length > 0);
|
|
7615
|
+
}
|
|
7616
|
+
function pushSectionsOf(pushOutcome) {
|
|
7617
|
+
if (!pushOutcome.ok) return [];
|
|
7618
|
+
const result = pushOutcome.result;
|
|
7619
|
+
if (result.tag === "dry") return [];
|
|
7620
|
+
return result.sections ?? [];
|
|
7621
|
+
}
|
|
7622
|
+
function renderWetSync(pull, pushOutcome, verbose) {
|
|
7416
7623
|
if (isNoopSync(pull, pushOutcome)) {
|
|
7417
7624
|
ok("already in sync");
|
|
7418
7625
|
return;
|
|
7419
7626
|
}
|
|
7420
|
-
|
|
7421
|
-
|
|
7627
|
+
log(`sync on host=${HOST}`);
|
|
7628
|
+
const summary = buildSyncSummarySection(pull, pushOutcome);
|
|
7629
|
+
if (!verbose) {
|
|
7630
|
+
renderTree([summary]);
|
|
7631
|
+
return;
|
|
7632
|
+
}
|
|
7633
|
+
const merged = mergeSyncSections(pull.sections, pushSectionsOf(pushOutcome));
|
|
7634
|
+
renderTree([...merged, summary]);
|
|
7422
7635
|
}
|
|
7423
7636
|
async function runSyncPushHalf() {
|
|
7424
7637
|
try {
|
|
7425
|
-
const result = await runPushCore();
|
|
7638
|
+
const result = await runPushCore({ compose: true });
|
|
7426
7639
|
return { ok: true, result };
|
|
7427
7640
|
} catch (err) {
|
|
7428
7641
|
if (err instanceof NomadFatal) {
|
|
@@ -7432,10 +7645,10 @@ async function runSyncPushHalf() {
|
|
|
7432
7645
|
throw err;
|
|
7433
7646
|
}
|
|
7434
7647
|
}
|
|
7435
|
-
async function runSyncWet() {
|
|
7436
|
-
const pull = runPullCore();
|
|
7648
|
+
async function runSyncWet(verbose) {
|
|
7649
|
+
const pull = runPullCore({ compose: true });
|
|
7437
7650
|
const pushOutcome = await runSyncPushHalf();
|
|
7438
|
-
renderWetSync(pull, pushOutcome);
|
|
7651
|
+
renderWetSync(pull, pushOutcome, verbose);
|
|
7439
7652
|
}
|
|
7440
7653
|
async function runSyncDryRun(repo, backup) {
|
|
7441
7654
|
const ts = freshBackupTs(backup);
|
|
@@ -7447,6 +7660,7 @@ async function runSyncDryRun(repo, backup) {
|
|
|
7447
7660
|
}
|
|
7448
7661
|
async function cmdSync(opts = {}) {
|
|
7449
7662
|
const dryRun = opts.dryRun === true;
|
|
7663
|
+
const verbose = opts.verbose === true;
|
|
7450
7664
|
const repo = repoHome();
|
|
7451
7665
|
const backup = backupBase();
|
|
7452
7666
|
if (!existsSync46(repo)) die(`repo not cloned at ${repo}`);
|
|
@@ -7459,7 +7673,7 @@ async function cmdSync(opts = {}) {
|
|
|
7459
7673
|
if (dryRun) {
|
|
7460
7674
|
await runSyncDryRun(repo, backup);
|
|
7461
7675
|
} else {
|
|
7462
|
-
await runSyncWet();
|
|
7676
|
+
await runSyncWet(verbose);
|
|
7463
7677
|
}
|
|
7464
7678
|
} catch (err) {
|
|
7465
7679
|
if (err instanceof NomadFatal) {
|
|
@@ -8084,24 +8298,27 @@ function parsePushArgs(argv) {
|
|
|
8084
8298
|
// src/nomad.dispatch.sync.ts
|
|
8085
8299
|
function parseSyncArgs(argv) {
|
|
8086
8300
|
let dryRun = false;
|
|
8301
|
+
let verbose = false;
|
|
8087
8302
|
let i = 3;
|
|
8088
8303
|
while (i < argv.length) {
|
|
8089
8304
|
const token = argv[i];
|
|
8090
8305
|
if (token === "--dry-run") {
|
|
8091
8306
|
if (dryRun) return null;
|
|
8092
8307
|
dryRun = true;
|
|
8308
|
+
} else if (token === "--verbose" || token === "--all" || token === "-v") {
|
|
8309
|
+
verbose = true;
|
|
8093
8310
|
} else {
|
|
8094
8311
|
return null;
|
|
8095
8312
|
}
|
|
8096
8313
|
i++;
|
|
8097
8314
|
}
|
|
8098
|
-
return { dryRun };
|
|
8315
|
+
return { dryRun, verbose };
|
|
8099
8316
|
}
|
|
8100
8317
|
|
|
8101
8318
|
// package.json
|
|
8102
8319
|
var package_default = {
|
|
8103
8320
|
name: "claude-nomad",
|
|
8104
|
-
version: "0.
|
|
8321
|
+
version: "0.60.0",
|
|
8105
8322
|
type: "module",
|
|
8106
8323
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
8107
8324
|
keywords: [
|
|
@@ -8236,10 +8453,15 @@ var DEFAULT_HELP = [
|
|
|
8236
8453
|
cont("Mutually exclusive with --redact-all/--allow. Cannot combine with --dry-run."),
|
|
8237
8454
|
"",
|
|
8238
8455
|
row(" sync", "Pull (retain-merge), then push, under one lock. One-shot; hides ordering."),
|
|
8456
|
+
cont("Compact by default: prints only the Sync summary."),
|
|
8239
8457
|
row(" --dry-run", "Stack the pull preview then the push preview; mutates nothing."),
|
|
8240
8458
|
cont("Fails fast on a pull-half error; a wedged repo hints at nomad pull --force-remote"),
|
|
8241
8459
|
cont("(sync itself takes no --force-remote). Mid-push leak recovery behaves exactly"),
|
|
8242
8460
|
cont("like nomad push."),
|
|
8461
|
+
row(
|
|
8462
|
+
" --verbose, --all, -v",
|
|
8463
|
+
"Show the full merged tree; default output is the Sync summary only."
|
|
8464
|
+
),
|
|
8243
8465
|
"",
|
|
8244
8466
|
row(" diff", "Offline preview of what `pull` would change against local repo state."),
|
|
8245
8467
|
cont("No git pull, no lock acquired."),
|
|
@@ -8465,10 +8687,10 @@ try {
|
|
|
8465
8687
|
case "sync": {
|
|
8466
8688
|
const syncArgs = parseSyncArgs(process.argv);
|
|
8467
8689
|
if (syncArgs === null) {
|
|
8468
|
-
console.error("usage: nomad sync [--dry-run]");
|
|
8690
|
+
console.error("usage: nomad sync [--dry-run] [--verbose|--all|-v]");
|
|
8469
8691
|
process.exit(1);
|
|
8470
8692
|
}
|
|
8471
|
-
await cmdSync({ dryRun: syncArgs.dryRun });
|
|
8693
|
+
await cmdSync({ dryRun: syncArgs.dryRun, verbose: syncArgs.verbose });
|
|
8472
8694
|
break;
|
|
8473
8695
|
}
|
|
8474
8696
|
case "init": {
|
package/package.json
CHANGED