claude-nomad 0.62.2 → 0.62.4
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 +28 -0
- package/README.md +4 -3
- package/dist/nomad.mjs +265 -173
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.62.4](https://github.com/funkadelic/claude-nomad/compare/v0.62.3...v0.62.4) (2026-07-21)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Fixed
|
|
7
|
+
|
|
8
|
+
* **sync:** compute the dry-run pull preview after the fetch ([#451](https://github.com/funkadelic/claude-nomad/issues/451)) ([8118625](https://github.com/funkadelic/claude-nomad/commit/81186258896081a7b2a2585f40eed67bd3e5c6b6))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
* **pull:** split commands.pull.recovery.ts on the force-remote seam ([#456](https://github.com/funkadelic/claude-nomad/issues/456)) ([1c1875b](https://github.com/funkadelic/claude-nomad/commit/1c1875b40e67512329e9a2d1215afa7667c29d1c))
|
|
14
|
+
* **push:** split redact-all batch from recovery actions ([#457](https://github.com/funkadelic/claude-nomad/issues/457)) ([b104472](https://github.com/funkadelic/claude-nomad/commit/b104472b87d3f847546ee938900bcaa4ea1abdd8))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Testing
|
|
18
|
+
|
|
19
|
+
* precompile the CLI once per vitest run ([#453](https://github.com/funkadelic/claude-nomad/issues/453)) ([e0b043b](https://github.com/funkadelic/claude-nomad/commit/e0b043b851ae9bd89da026199f70d723e213aed9))
|
|
20
|
+
* **pull:** reset process.exitCode in beforeEach as well as afterEach ([#450](https://github.com/funkadelic/claude-nomad/issues/450)) ([78943e7](https://github.com/funkadelic/claude-nomad/commit/78943e72a41d4a542a4d6528d995ae9f8a8452cc))
|
|
21
|
+
|
|
22
|
+
## [0.62.3](https://github.com/funkadelic/claude-nomad/compare/v0.62.2...v0.62.3) (2026-07-21)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
* abort when an autostash pop conflicts during rebase ([#448](https://github.com/funkadelic/claude-nomad/issues/448)) ([a22b3df](https://github.com/funkadelic/claude-nomad/commit/a22b3dfdc95478ba09a6cf6bf9994c9d8d9ac416))
|
|
28
|
+
* **pull:** stop --force-remote applying conflict-markered config ([#452](https://github.com/funkadelic/claude-nomad/issues/452)) ([5612ac9](https://github.com/funkadelic/claude-nomad/commit/5612ac9c25e25118a73aaa465d2ccd9b16604e8a))
|
|
29
|
+
* retain never-pushed local skills on pull ([#449](https://github.com/funkadelic/claude-nomad/issues/449)) ([0faf4f7](https://github.com/funkadelic/claude-nomad/commit/0faf4f726d2982ba9e4b6da9be58b17b838bcd37))
|
|
30
|
+
|
|
3
31
|
## [0.62.2](https://github.com/funkadelic/claude-nomad/compare/v0.62.1...v0.62.2) (2026-07-21)
|
|
4
32
|
|
|
5
33
|
|
package/README.md
CHANGED
|
@@ -253,9 +253,10 @@ operation, parks stranded commits on a `nomad/stranded-<ts>` branch, resets to `
|
|
|
253
253
|
re-pulls; refuses if stranded or dirty tracked changes touch synced config), and a repo where the
|
|
254
254
|
rebase was torn down but the git index still has unmerged entries with no active rebase or merge in
|
|
255
255
|
progress (clears the stuck index via `git reset --mixed HEAD`, preserving working-tree edits,
|
|
256
|
-
surfaces any orphaned autostash entry, then re-pulls
|
|
257
|
-
|
|
258
|
-
|
|
256
|
+
surfaces any orphaned autostash entry, then re-pulls; stops after the index repair if any conflicted
|
|
257
|
+
file still carries conflict markers, so they are never copied into your live config). Run
|
|
258
|
+
`nomad doctor` first if you are unsure which state you are in; the Repository section names the
|
|
259
|
+
specific problem and points at the right fix.
|
|
259
260
|
|
|
260
261
|
If an external tool (such as Claude Code or GSD) wrote new keys into your `~/.claude/settings.json`
|
|
261
262
|
that are not yet in your shared repo, run `nomad capture-settings` to promote them before the next
|
package/dist/nomad.mjs
CHANGED
|
@@ -779,7 +779,7 @@ function classifyWedge(repo) {
|
|
|
779
779
|
return unmergedIndexPresent(repo) ? "unmerged-index" : null;
|
|
780
780
|
}
|
|
781
781
|
function unmergedIndexRunbookText(resumeCmd2) {
|
|
782
|
-
return `repo has an unmerged index with no active rebase or merge in progress (torn-down rebase left stage-2/3 entries behind).
|
|
782
|
+
return `repo has an unmerged index with no active rebase or merge in progress (torn-down rebase or merge left stage-2/3 entries behind).
|
|
783
783
|
|
|
784
784
|
Manual recovery:
|
|
785
785
|
1. git reset --mixed HEAD (clears the stuck index; preserves working-tree files)
|
|
@@ -915,6 +915,43 @@ var init_push_gitleaks_config = __esm({
|
|
|
915
915
|
}
|
|
916
916
|
});
|
|
917
917
|
|
|
918
|
+
// src/autostash-guard.ts
|
|
919
|
+
function autostashConflictRunbookText(resumeCmd2, stashRetained) {
|
|
920
|
+
const head = `${resumeCmd2} reported success, but the autostash pop it ran internally conflicted. The working tree now holds conflict markers and the index is unmerged. Nothing has been copied to ~/.claude/ yet, and there is no rebase or merge in progress to abort.`;
|
|
921
|
+
if (stashRetained) {
|
|
922
|
+
return `${head}
|
|
923
|
+
|
|
924
|
+
Recovery (the pre-conflict content is retained in the stash; nothing is lost):
|
|
925
|
+
1. git stash list (confirm stash@{0}: autostash is present)
|
|
926
|
+
2. git show 'stash@{0}:<path>' (view the pre-conflict content; does not re-trigger the conflict)
|
|
927
|
+
3. git reset --hard HEAD (discard the markered working tree)
|
|
928
|
+
4. re-apply the content from step 2 into <path>, as needed
|
|
929
|
+
5. git stash drop (once you are done with the stash entry)
|
|
930
|
+
6. ${resumeCmd2}`;
|
|
931
|
+
}
|
|
932
|
+
return `${head}
|
|
933
|
+
|
|
934
|
+
No autostash entry was found to recover from, so the working tree is the only copy of this edit. Resolve the conflict markers by hand in the affected file(s), then:
|
|
935
|
+
1. git add <path> (mark resolved)
|
|
936
|
+
2. ${resumeCmd2}
|
|
937
|
+
|
|
938
|
+
Do not run "git reset --hard": with no stash entry to fall back on, that would discard your only copy of the edit.`;
|
|
939
|
+
}
|
|
940
|
+
function assertNoAutostashConflict(repo, resumeCmd2) {
|
|
941
|
+
if (!unmergedIndexPresent(repo)) return;
|
|
942
|
+
throw new NomadFatal(autostashConflictRunbookText(resumeCmd2, orphanedAutostashPresent(repo)), {
|
|
943
|
+
code: EXIT.CONFLICT
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
var init_autostash_guard = __esm({
|
|
947
|
+
"src/autostash-guard.ts"() {
|
|
948
|
+
"use strict";
|
|
949
|
+
init_commands_pull_wedge();
|
|
950
|
+
init_exit_codes();
|
|
951
|
+
init_utils();
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
|
|
918
955
|
// src/push-checks.ts
|
|
919
956
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
920
957
|
import { readdirSync as readdirSync7, rmSync as rmSync7 } from "node:fs";
|
|
@@ -1014,11 +1051,13 @@ function rebaseBeforePush(repo) {
|
|
|
1014
1051
|
`rebase failed; if a conflict was reported, resolve it in ${repo} and run "git rebase --continue" (or "git rebase --abort" to give up). Re-run nomad push after resolution.`
|
|
1015
1052
|
);
|
|
1016
1053
|
}
|
|
1054
|
+
assertNoAutostashConflict(repo, "nomad push");
|
|
1017
1055
|
}
|
|
1018
1056
|
var init_push_checks = __esm({
|
|
1019
1057
|
"src/push-checks.ts"() {
|
|
1020
1058
|
"use strict";
|
|
1021
1059
|
init_push_gitleaks_config();
|
|
1060
|
+
init_autostash_guard();
|
|
1022
1061
|
init_commands_pull_wedge();
|
|
1023
1062
|
init_exit_codes();
|
|
1024
1063
|
init_utils();
|
|
@@ -1644,9 +1683,9 @@ function cpSyncGuarded(src, dst, filter, label) {
|
|
|
1644
1683
|
throw err;
|
|
1645
1684
|
}
|
|
1646
1685
|
}
|
|
1647
|
-
function prunePreservingBy(src, dst, isPreserved) {
|
|
1686
|
+
function prunePreservingBy(src, dst, isPreserved, isRootPreserved) {
|
|
1648
1687
|
for (const name of readdirSync(dst)) {
|
|
1649
|
-
if (isPreserved(name)) continue;
|
|
1688
|
+
if (isPreserved(name) || isRootPreserved?.(name) === true) continue;
|
|
1650
1689
|
const dstPath = join2(dst, name);
|
|
1651
1690
|
const srcStat = lstatSync(join2(src, name), { throwIfNoEntry: false });
|
|
1652
1691
|
if (srcStat === void 0) {
|
|
@@ -1661,7 +1700,7 @@ function prunePreservingBy(src, dst, isPreserved) {
|
|
|
1661
1700
|
}
|
|
1662
1701
|
}
|
|
1663
1702
|
}
|
|
1664
|
-
function copyExtrasFilteredPreservingBy(src, dst, isPreserved) {
|
|
1703
|
+
function copyExtrasFilteredPreservingBy(src, dst, isPreserved, isRootPreserved) {
|
|
1665
1704
|
const dstStat = lstatSync(dst, { throwIfNoEntry: false });
|
|
1666
1705
|
if (dstStat !== void 0) {
|
|
1667
1706
|
if (dstStat.isDirectory()) {
|
|
@@ -1671,7 +1710,7 @@ function copyExtrasFilteredPreservingBy(src, dst, isPreserved) {
|
|
|
1671
1710
|
`copyExtrasFilteredPreservingBy: type mismatch copying ${quoted(src)} -> ${quoted(dst)}: the repo entry is a file but the local entry is a directory; run nomad pull --force-remote to recover`
|
|
1672
1711
|
);
|
|
1673
1712
|
}
|
|
1674
|
-
prunePreservingBy(src, dst, isPreserved);
|
|
1713
|
+
prunePreservingBy(src, dst, isPreserved, isRootPreserved);
|
|
1675
1714
|
} else {
|
|
1676
1715
|
rmSync(dst, { recursive: true, force: true });
|
|
1677
1716
|
}
|
|
@@ -5364,6 +5403,21 @@ import { dirname as dirname11, join as join42, sep as sep6 } from "node:path";
|
|
|
5364
5403
|
init_config();
|
|
5365
5404
|
import { existsSync as existsSync33, lstatSync as lstatSync12, mkdirSync as mkdirSync11, readdirSync as readdirSync14, rmSync as rmSync14 } from "node:fs";
|
|
5366
5405
|
import { join as join41 } from "node:path";
|
|
5406
|
+
|
|
5407
|
+
// src/skills-sync.tracked.ts
|
|
5408
|
+
init_utils();
|
|
5409
|
+
import { basename as basename3 } from "node:path";
|
|
5410
|
+
function trackedRootSkillsAt(ref, repo) {
|
|
5411
|
+
try {
|
|
5412
|
+
const raw = gitCaptureRaw(["ls-tree", "--name-only", "-z", ref, "--", "shared/skills/"], repo);
|
|
5413
|
+
const entries = raw.split("\0").filter((entry) => entry !== "");
|
|
5414
|
+
return new Set(entries.map((entry) => basename3(entry)));
|
|
5415
|
+
} catch {
|
|
5416
|
+
return /* @__PURE__ */ new Set();
|
|
5417
|
+
}
|
|
5418
|
+
}
|
|
5419
|
+
|
|
5420
|
+
// src/skills-sync.ts
|
|
5367
5421
|
init_utils_fs();
|
|
5368
5422
|
function isGsdOwned(name) {
|
|
5369
5423
|
return name.startsWith(GSD_PREFIX);
|
|
@@ -5379,20 +5433,21 @@ function copySkillsPush(src, dst) {
|
|
|
5379
5433
|
]);
|
|
5380
5434
|
copyExtrasFiltered(src, dst, blockSet);
|
|
5381
5435
|
}
|
|
5382
|
-
function copySkillsPull(src, dst) {
|
|
5383
|
-
copyExtrasFilteredPreservingBy(src, dst, isSkillExcluded);
|
|
5436
|
+
function copySkillsPull(src, dst, isRootPreserved) {
|
|
5437
|
+
copyExtrasFilteredPreservingBy(src, dst, isSkillExcluded, isRootPreserved);
|
|
5384
5438
|
}
|
|
5385
|
-
function syncSkillsPull(ts) {
|
|
5439
|
+
function syncSkillsPull(ts, prePostHeads) {
|
|
5386
5440
|
const sharedSkills = join41(repoHome(), "shared", "skills");
|
|
5387
5441
|
if (!existsSync33(sharedSkills)) return;
|
|
5388
5442
|
const localSkills = join41(claudeHome(), "skills");
|
|
5443
|
+
backupBeforeWrite(localSkills, ts);
|
|
5389
5444
|
const dstStat = lstatSync12(localSkills, { throwIfNoEntry: false });
|
|
5390
5445
|
if (dstStat?.isSymbolicLink() === true) {
|
|
5391
|
-
backupBeforeWrite(localSkills, ts);
|
|
5392
5446
|
rmSync14(localSkills, { recursive: true, force: true });
|
|
5393
5447
|
mkdirSync11(localSkills, { recursive: true });
|
|
5394
5448
|
}
|
|
5395
|
-
|
|
5449
|
+
const tracked = prePostHeads === void 0 ? /* @__PURE__ */ new Set() : trackedRootSkillsAt(prePostHeads.pre, repoHome());
|
|
5450
|
+
copySkillsPull(sharedSkills, localSkills, (name) => !tracked.has(name));
|
|
5396
5451
|
}
|
|
5397
5452
|
function syncSkillsPush() {
|
|
5398
5453
|
const localSkills = join41(claudeHome(), "skills");
|
|
@@ -5613,6 +5668,10 @@ function dispatchActions(findings, actions, opts) {
|
|
|
5613
5668
|
dispatchOne(f, ctx);
|
|
5614
5669
|
}
|
|
5615
5670
|
}
|
|
5671
|
+
|
|
5672
|
+
// src/commands.push.recovery.redact-all.ts
|
|
5673
|
+
init_push_gitleaks_scan();
|
|
5674
|
+
init_utils();
|
|
5616
5675
|
function redactAllDedupeKey(f) {
|
|
5617
5676
|
const sid = sessionIdFromFinding(f);
|
|
5618
5677
|
if (sid !== null) return sid;
|
|
@@ -6384,8 +6443,9 @@ function unstageOne(rel, repo) {
|
|
|
6384
6443
|
}
|
|
6385
6444
|
|
|
6386
6445
|
// src/commands.pull.ts
|
|
6387
|
-
|
|
6388
|
-
import {
|
|
6446
|
+
init_autostash_guard();
|
|
6447
|
+
import { existsSync as existsSync43, mkdirSync as mkdirSync14 } from "node:fs";
|
|
6448
|
+
import { join as join53 } from "node:path";
|
|
6389
6449
|
|
|
6390
6450
|
// src/commands.push.sections.ts
|
|
6391
6451
|
init_color();
|
|
@@ -6822,8 +6882,8 @@ function divergenceCheckExtras(ts, prePostHeads) {
|
|
|
6822
6882
|
|
|
6823
6883
|
// src/preview.ts
|
|
6824
6884
|
init_config();
|
|
6825
|
-
import { existsSync as
|
|
6826
|
-
import { join as
|
|
6885
|
+
import { existsSync as existsSync42 } from "node:fs";
|
|
6886
|
+
import { join as join52 } from "node:path";
|
|
6827
6887
|
|
|
6828
6888
|
// node_modules/diff/libesm/diff/base.js
|
|
6829
6889
|
var Diff = class {
|
|
@@ -7096,6 +7156,26 @@ function diffLinesToUnified(oldStr, newStr) {
|
|
|
7096
7156
|
return lines;
|
|
7097
7157
|
}
|
|
7098
7158
|
|
|
7159
|
+
// src/preview.skills.ts
|
|
7160
|
+
init_config();
|
|
7161
|
+
import { existsSync as existsSync41, readdirSync as readdirSync17 } from "node:fs";
|
|
7162
|
+
import { join as join51 } from "node:path";
|
|
7163
|
+
function buildSkillsPreviewSection() {
|
|
7164
|
+
const s = section("Skills");
|
|
7165
|
+
const sharedSkills = join51(repoHome(), "shared", "skills");
|
|
7166
|
+
if (!existsSync41(sharedSkills)) return s;
|
|
7167
|
+
const sharedNames = readdirSync17(sharedSkills, { encoding: "utf8" }).filter((name) => !isSkillExcluded(name)).sort((a, b) => a.localeCompare(b, "en"));
|
|
7168
|
+
for (const name of sharedNames) addItem(s, name);
|
|
7169
|
+
const localSkills = join51(claudeHome(), "skills");
|
|
7170
|
+
const localOnly = existsSync41(localSkills) ? readdirSync17(localSkills, { encoding: "utf8" }).filter(
|
|
7171
|
+
(name) => !isSkillExcluded(name) && !sharedNames.includes(name)
|
|
7172
|
+
).length : 0;
|
|
7173
|
+
if (localOnly > 0) {
|
|
7174
|
+
addItem(s, `${localOnly} local-only present, not in repo (push to reconcile)`);
|
|
7175
|
+
}
|
|
7176
|
+
return s;
|
|
7177
|
+
}
|
|
7178
|
+
|
|
7099
7179
|
// src/preview.ts
|
|
7100
7180
|
init_utils_json();
|
|
7101
7181
|
var CANONICAL_ORDER_NOTE = "settings.json will be rewritten in canonical key order; no value changes";
|
|
@@ -7109,7 +7189,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
|
|
|
7109
7189
|
return lines.join("\n");
|
|
7110
7190
|
}
|
|
7111
7191
|
function readJsonOrNull(path) {
|
|
7112
|
-
if (!
|
|
7192
|
+
if (!existsSync42(path)) return null;
|
|
7113
7193
|
try {
|
|
7114
7194
|
return readJson(path);
|
|
7115
7195
|
} catch {
|
|
@@ -7123,12 +7203,12 @@ function previewSettings(basePath, hostPath, settingsPath) {
|
|
|
7123
7203
|
}
|
|
7124
7204
|
const notes = [];
|
|
7125
7205
|
const hostOverrides = readJsonOrNull(hostPath);
|
|
7126
|
-
if (hostOverrides === null &&
|
|
7206
|
+
if (hostOverrides === null && existsSync42(hostPath)) {
|
|
7127
7207
|
notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
|
|
7128
7208
|
}
|
|
7129
7209
|
const merged = stripGsdHookEntries(deepMerge(base, hostOverrides ?? {}));
|
|
7130
7210
|
const current = readJsonOrNull(settingsPath);
|
|
7131
|
-
if (current === null &&
|
|
7211
|
+
if (current === null && existsSync42(settingsPath)) {
|
|
7132
7212
|
return { diff: "", notes: [...notes, "malformed; skipping diff"] };
|
|
7133
7213
|
}
|
|
7134
7214
|
const strippedCurrent = stripGsdHookEntries(current ?? {});
|
|
@@ -7162,7 +7242,7 @@ function buildSettingsSectionForPreview(result) {
|
|
|
7162
7242
|
function computePreview(ts, map, verb = "pull") {
|
|
7163
7243
|
const repo = repoHome();
|
|
7164
7244
|
const claude = claudeHome();
|
|
7165
|
-
console.log(`would pull on host=${HOST} (
|
|
7245
|
+
console.log(`would pull on host=${HOST} (preview; nothing applied)`);
|
|
7166
7246
|
console.log("");
|
|
7167
7247
|
const links = section("Symlinks");
|
|
7168
7248
|
applySharedLinks(ts, map, {
|
|
@@ -7170,9 +7250,9 @@ function computePreview(ts, map, verb = "pull") {
|
|
|
7170
7250
|
onPreview: (e) => addItem(links, formatLinkRow(e))
|
|
7171
7251
|
});
|
|
7172
7252
|
const settingsResult = previewSettings(
|
|
7173
|
-
|
|
7174
|
-
|
|
7175
|
-
|
|
7253
|
+
join52(repo, "shared", "settings.base.json"),
|
|
7254
|
+
join52(repo, "hosts", `${HOST}.json`),
|
|
7255
|
+
join52(claude, "settings.json")
|
|
7176
7256
|
);
|
|
7177
7257
|
const settingsSection = buildSettingsSectionForPreview(settingsResult);
|
|
7178
7258
|
const sessions = section("Sessions");
|
|
@@ -7184,10 +7264,11 @@ function computePreview(ts, map, verb = "pull") {
|
|
|
7184
7264
|
if (localOnly > 0) {
|
|
7185
7265
|
addItem(sessions, `${localOnly} local-only present, not in repo (push to reconcile)`);
|
|
7186
7266
|
}
|
|
7267
|
+
const skills = buildSkillsPreviewSection();
|
|
7187
7268
|
const extras = section("Extras");
|
|
7188
7269
|
let extrasSkipped = 0;
|
|
7189
7270
|
let extrasUnmapped = 0;
|
|
7190
|
-
if (
|
|
7271
|
+
if (existsSync42(join52(repo, "path-map.json")) && existsSync42(join52(repo, "shared", "extras"))) {
|
|
7191
7272
|
const extrasResult = remapExtrasPull(ts, { dryRun: true });
|
|
7192
7273
|
for (const entry of extrasResult.wouldPull) {
|
|
7193
7274
|
addItem(extras, entry);
|
|
@@ -7200,7 +7281,7 @@ function computePreview(ts, map, verb = "pull") {
|
|
|
7200
7281
|
summary,
|
|
7201
7282
|
summaryRow(verb, remapResult.unmapped + extrasUnmapped, 0, extrasSkipped, localOnly)
|
|
7202
7283
|
);
|
|
7203
|
-
renderTree([links, settingsSection, sessions, extras, summary]);
|
|
7284
|
+
renderTree([links, settingsSection, sessions, skills, extras, summary]);
|
|
7204
7285
|
return { unmapped: remapResult.unmapped, collisions: 0, localOnly };
|
|
7205
7286
|
}
|
|
7206
7287
|
|
|
@@ -7209,9 +7290,11 @@ init_commands_pull_wedge();
|
|
|
7209
7290
|
|
|
7210
7291
|
// src/commands.pull.recovery.ts
|
|
7211
7292
|
init_config();
|
|
7212
|
-
init_commands_pull_wedge();
|
|
7213
7293
|
init_utils();
|
|
7214
7294
|
init_utils_fs();
|
|
7295
|
+
|
|
7296
|
+
// src/commands.pull.recovery.git.ts
|
|
7297
|
+
init_utils();
|
|
7215
7298
|
import { execFileSync as execFileSync21 } from "node:child_process";
|
|
7216
7299
|
function gitCapture(args, cwd) {
|
|
7217
7300
|
return execFileSync21("git", args, {
|
|
@@ -7220,23 +7303,6 @@ function gitCapture(args, cwd) {
|
|
|
7220
7303
|
maxBuffer: 64 * 1024 * 1024
|
|
7221
7304
|
}).toString().trim();
|
|
7222
7305
|
}
|
|
7223
|
-
function isSyncedConfig(path) {
|
|
7224
|
-
return PUSH_ALLOWED_STATIC.some(
|
|
7225
|
-
(entry) => entry.endsWith("/") ? path.startsWith(entry) : path === entry
|
|
7226
|
-
);
|
|
7227
|
-
}
|
|
7228
|
-
function classifyTouched(touched) {
|
|
7229
|
-
const synced = [];
|
|
7230
|
-
const toolSource = [];
|
|
7231
|
-
for (const p of touched) {
|
|
7232
|
-
if (isSyncedConfig(p)) {
|
|
7233
|
-
synced.push(p);
|
|
7234
|
-
} else {
|
|
7235
|
-
toolSource.push(p);
|
|
7236
|
-
}
|
|
7237
|
-
}
|
|
7238
|
-
return { synced, toolSource };
|
|
7239
|
-
}
|
|
7240
7306
|
function parsePorcelainZ(raw) {
|
|
7241
7307
|
const tracked = [];
|
|
7242
7308
|
const untracked = [];
|
|
@@ -7262,8 +7328,27 @@ function parsePorcelainZ(raw) {
|
|
|
7262
7328
|
}
|
|
7263
7329
|
return { tracked, untracked };
|
|
7264
7330
|
}
|
|
7265
|
-
function parseDirtyPaths(repo) {
|
|
7266
|
-
return parsePorcelainZ(gitStatusPorcelainZ(repo));
|
|
7331
|
+
function parseDirtyPaths(repo, opts = {}) {
|
|
7332
|
+
return parsePorcelainZ(gitStatusPorcelainZ(repo, opts));
|
|
7333
|
+
}
|
|
7334
|
+
|
|
7335
|
+
// src/commands.pull.recovery.ts
|
|
7336
|
+
function isSyncedConfig(path) {
|
|
7337
|
+
return PUSH_ALLOWED_STATIC.some(
|
|
7338
|
+
(entry) => entry.endsWith("/") ? path.startsWith(entry) : path === entry
|
|
7339
|
+
);
|
|
7340
|
+
}
|
|
7341
|
+
function classifyTouched(touched) {
|
|
7342
|
+
const synced = [];
|
|
7343
|
+
const toolSource = [];
|
|
7344
|
+
for (const p of touched) {
|
|
7345
|
+
if (isSyncedConfig(p)) {
|
|
7346
|
+
synced.push(p);
|
|
7347
|
+
} else {
|
|
7348
|
+
toolSource.push(p);
|
|
7349
|
+
}
|
|
7350
|
+
}
|
|
7351
|
+
return { synced, toolSource };
|
|
7267
7352
|
}
|
|
7268
7353
|
function buildRecoverySummary(branchName, strandedLog, untracked) {
|
|
7269
7354
|
const strandedLines = strandedLog.split("\n").filter(Boolean).map((l) => ` ${l}`).join("\n");
|
|
@@ -7289,20 +7374,6 @@ function freshStrandedBranch(repo) {
|
|
|
7289
7374
|
while (exists(`${base}-${n}`)) n++;
|
|
7290
7375
|
return `${base}-${n}`;
|
|
7291
7376
|
}
|
|
7292
|
-
function recoverUnmergedIndex(repo) {
|
|
7293
|
-
gitOrFatal(["reset", "--mixed", "HEAD"], "git reset --mixed HEAD", repo);
|
|
7294
|
-
const dirty = gitCapture(["diff", "--name-only", "-z"], repo).split("\0").filter(Boolean);
|
|
7295
|
-
if (dirty.length > 0) {
|
|
7296
|
-
log(
|
|
7297
|
-
"index cleared, but these files still carry conflict content from the torn-down rebase; review and resolve before the next pull:\n" + dirty.map((p) => ` ${p}`).join("\n")
|
|
7298
|
-
);
|
|
7299
|
-
}
|
|
7300
|
-
if (orphanedAutostashPresent(repo)) {
|
|
7301
|
-
log(
|
|
7302
|
-
'orphaned autostash preserved in the stash list; run "git stash pop" to restore or "git stash drop" to discard it, then re-run "nomad pull"'
|
|
7303
|
-
);
|
|
7304
|
-
}
|
|
7305
|
-
}
|
|
7306
7377
|
function recoverForceRemote(mode, repo) {
|
|
7307
7378
|
if (mode === "merge") {
|
|
7308
7379
|
gitOrFatal(["merge", "--abort"], "git merge --abort", repo);
|
|
@@ -7332,6 +7403,31 @@ function recoverForceRemote(mode, repo) {
|
|
|
7332
7403
|
log(buildRecoverySummary(branchName, strandedLog, untracked));
|
|
7333
7404
|
}
|
|
7334
7405
|
|
|
7406
|
+
// src/commands.pull.recovery.unmerged.ts
|
|
7407
|
+
init_exit_codes();
|
|
7408
|
+
init_commands_pull_wedge();
|
|
7409
|
+
init_utils();
|
|
7410
|
+
function recoverUnmergedIndex(repo) {
|
|
7411
|
+
const conflicted = new Set(
|
|
7412
|
+
gitCapture(["diff", "--diff-filter=U", "--name-only", "-z"], repo).split("\0").filter(Boolean)
|
|
7413
|
+
);
|
|
7414
|
+
gitOrFatal(["reset", "--mixed", "HEAD"], "git reset --mixed HEAD", repo);
|
|
7415
|
+
const { tracked, untracked } = parseDirtyPaths(repo, { untrackedAll: true });
|
|
7416
|
+
const present = /* @__PURE__ */ new Set([...tracked, ...untracked]);
|
|
7417
|
+
const residual = [...conflicted].filter((p) => present.has(p));
|
|
7418
|
+
if (orphanedAutostashPresent(repo)) {
|
|
7419
|
+
log(
|
|
7420
|
+
'orphaned autostash preserved in the stash list; run "git stash pop" to restore or "git stash drop" to discard it, then re-run "nomad pull"'
|
|
7421
|
+
);
|
|
7422
|
+
}
|
|
7423
|
+
if (residual.length > 0) {
|
|
7424
|
+
die(
|
|
7425
|
+
"index cleared, but these files still carry unresolved conflict content from the torn-down rebase or merge:\n" + residual.map((p) => ` ${p}`).join("\n") + '\n\nThe repo is no longer wedged, so nothing else is blocked. Nothing was applied to ~/.claude/: pulling now would publish that content to your live config.\n\nResolve each file above (remove any <<<<<<< / ======= / >>>>>>> markers and keep the content you want; a file left untracked was deleted upstream, so keep or delete it deliberately), commit or checkout the result, then re-run "nomad pull".',
|
|
7426
|
+
{ code: EXIT.CONFLICT }
|
|
7427
|
+
);
|
|
7428
|
+
}
|
|
7429
|
+
}
|
|
7430
|
+
|
|
7335
7431
|
// src/commands.pull.ts
|
|
7336
7432
|
init_exit_codes();
|
|
7337
7433
|
init_utils();
|
|
@@ -7355,7 +7451,7 @@ function capturePrePostHeads(repo, rebase) {
|
|
|
7355
7451
|
function buildWetPullSections(ts, map, prePostHeads) {
|
|
7356
7452
|
applySharedLinks(ts, map);
|
|
7357
7453
|
const { label } = regenerateSettings(ts);
|
|
7358
|
-
syncSkillsPull(ts);
|
|
7454
|
+
syncSkillsPull(ts, prePostHeads);
|
|
7359
7455
|
const remapResult = withSpinner("Syncing sessions", () => remapPull(ts));
|
|
7360
7456
|
const extrasResult = remapExtrasPull(ts, { prePostHeads });
|
|
7361
7457
|
const localOnly = scanLocalOnly();
|
|
@@ -7402,7 +7498,7 @@ function runPullCore(opts = {}) {
|
|
|
7402
7498
|
const ts = freshBackupTs(backup);
|
|
7403
7499
|
handleWedge(repo, forceRemote);
|
|
7404
7500
|
if (!dryRun) {
|
|
7405
|
-
const backupRoot =
|
|
7501
|
+
const backupRoot = join53(backup, ts);
|
|
7406
7502
|
try {
|
|
7407
7503
|
mkdirSync14(backupRoot, { recursive: true });
|
|
7408
7504
|
} catch (err) {
|
|
@@ -7417,12 +7513,12 @@ function runPullCore(opts = {}) {
|
|
|
7417
7513
|
const prePostHeads = capturePrePostHeads(repo, () => {
|
|
7418
7514
|
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
|
|
7419
7515
|
});
|
|
7420
|
-
|
|
7421
|
-
const
|
|
7516
|
+
assertNoAutostashConflict(repo, "nomad pull");
|
|
7517
|
+
const mapPath = join53(repo, "path-map.json");
|
|
7518
|
+
const map = existsSync43(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
7422
7519
|
const divergedKeptLocal = divergenceCheckExtras(ts, dryRun ? prePostHeads : void 0);
|
|
7423
7520
|
if (dryRun) {
|
|
7424
7521
|
computePreview(ts, map, "pull");
|
|
7425
|
-
log("dry-run complete; no mutation");
|
|
7426
7522
|
return { tag: "dry" };
|
|
7427
7523
|
}
|
|
7428
7524
|
const { sections, localOnly, settingsLabel, unmapped, extrasSkipped } = buildWetPullSections(
|
|
@@ -7444,8 +7540,8 @@ function runPullCore(opts = {}) {
|
|
|
7444
7540
|
}
|
|
7445
7541
|
function cmdPull(opts = {}) {
|
|
7446
7542
|
const repo = repoHome();
|
|
7447
|
-
if (!
|
|
7448
|
-
if (!
|
|
7543
|
+
if (!existsSync43(repo)) die(`repo not cloned at ${repo}`);
|
|
7544
|
+
if (!existsSync43(join53(repo, "shared", "settings.base.json"))) {
|
|
7449
7545
|
die("repo not initialized; run 'nomad init' to scaffold");
|
|
7450
7546
|
}
|
|
7451
7547
|
const handle = acquireLock("pull");
|
|
@@ -7453,6 +7549,7 @@ function cmdPull(opts = {}) {
|
|
|
7453
7549
|
try {
|
|
7454
7550
|
const result = runPullCore(opts);
|
|
7455
7551
|
if (result.tag === "wet") renderTree(result.sections);
|
|
7552
|
+
else log("dry-run complete; nothing applied to ~/.claude/");
|
|
7456
7553
|
} catch (err) {
|
|
7457
7554
|
if (err instanceof NomadFatal) {
|
|
7458
7555
|
fail(err.message);
|
|
@@ -7467,13 +7564,13 @@ function cmdPull(opts = {}) {
|
|
|
7467
7564
|
|
|
7468
7565
|
// src/commands.push.ts
|
|
7469
7566
|
init_config();
|
|
7470
|
-
import { existsSync as
|
|
7471
|
-
import { join as
|
|
7567
|
+
import { existsSync as existsSync47 } from "node:fs";
|
|
7568
|
+
import { join as join58 } from "node:path";
|
|
7472
7569
|
|
|
7473
7570
|
// src/commands.push.selection.ts
|
|
7474
7571
|
init_config();
|
|
7475
|
-
import { existsSync as
|
|
7476
|
-
import { join as
|
|
7572
|
+
import { existsSync as existsSync44, statSync as statSync10 } from "node:fs";
|
|
7573
|
+
import { join as join54 } from "node:path";
|
|
7477
7574
|
init_utils_json();
|
|
7478
7575
|
function buildCurrentMap(map) {
|
|
7479
7576
|
const current = {};
|
|
@@ -7482,8 +7579,8 @@ function buildCurrentMap(map) {
|
|
|
7482
7579
|
for (const [, hostMap] of Object.entries(map.projects)) {
|
|
7483
7580
|
const localPath = hostMap[HOST];
|
|
7484
7581
|
if (!localPath) continue;
|
|
7485
|
-
const localDir =
|
|
7486
|
-
if (!
|
|
7582
|
+
const localDir = join54(claude, "projects", encodePath(localPath));
|
|
7583
|
+
if (!existsSync44(localDir)) continue;
|
|
7487
7584
|
for (const f of enumerateSourceFiles(localDir)) {
|
|
7488
7585
|
const st = statSync10(f);
|
|
7489
7586
|
current[f] = { size: st.size, mtime: st.mtimeMs };
|
|
@@ -7514,7 +7611,7 @@ function computePushSelection(map, old, scannerVersion, configHash, fullScan) {
|
|
|
7514
7611
|
};
|
|
7515
7612
|
}
|
|
7516
7613
|
function loadSelectionForPush(mapPath, old, scannerVersion, configHash, fullScan) {
|
|
7517
|
-
const map =
|
|
7614
|
+
const map = existsSync44(mapPath) ? readPathMap(mapPath) : null;
|
|
7518
7615
|
const { selection, newManifest } = computePushSelection(
|
|
7519
7616
|
map,
|
|
7520
7617
|
old,
|
|
@@ -7616,14 +7713,14 @@ function enforceAllowList(statusPorcelain, map) {
|
|
|
7616
7713
|
|
|
7617
7714
|
// src/commands.push.settings.ts
|
|
7618
7715
|
init_config();
|
|
7619
|
-
import { existsSync as
|
|
7620
|
-
import { join as
|
|
7716
|
+
import { existsSync as existsSync45 } from "node:fs";
|
|
7717
|
+
import { join as join55 } from "node:path";
|
|
7621
7718
|
init_utils();
|
|
7622
7719
|
init_utils_fs();
|
|
7623
7720
|
init_utils_json();
|
|
7624
7721
|
function stripGsdHooksFromBase(repo, backup) {
|
|
7625
|
-
const basePath =
|
|
7626
|
-
if (!
|
|
7722
|
+
const basePath = join55(repo, "shared", "settings.base.json");
|
|
7723
|
+
if (!existsSync45(basePath)) return;
|
|
7627
7724
|
let base;
|
|
7628
7725
|
try {
|
|
7629
7726
|
base = readJson(basePath);
|
|
@@ -7637,14 +7734,14 @@ function stripGsdHooksFromBase(repo, backup) {
|
|
|
7637
7734
|
writeJsonAtomic(basePath, stripped);
|
|
7638
7735
|
}
|
|
7639
7736
|
function reportSettingsAheadDrift(repo) {
|
|
7640
|
-
const basePath =
|
|
7641
|
-
if (!
|
|
7642
|
-
const settingsPath =
|
|
7643
|
-
if (!
|
|
7737
|
+
const basePath = join55(repo, "shared", "settings.base.json");
|
|
7738
|
+
if (!existsSync45(basePath)) return;
|
|
7739
|
+
const settingsPath = join55(claudeHome(), "settings.json");
|
|
7740
|
+
if (!existsSync45(settingsPath)) return;
|
|
7644
7741
|
try {
|
|
7645
7742
|
const base = readJson(basePath);
|
|
7646
|
-
const hostPath =
|
|
7647
|
-
const overrides =
|
|
7743
|
+
const hostPath = join55(repo, "hosts", `${HOST}.json`);
|
|
7744
|
+
const overrides = existsSync45(hostPath) ? readJson(hostPath) : {};
|
|
7648
7745
|
const merged = deepMerge(base, overrides);
|
|
7649
7746
|
const settings = readJson(settingsPath);
|
|
7650
7747
|
const { ahead } = classifySettingsDrift(merged, settings);
|
|
@@ -7661,9 +7758,9 @@ function reportSettingsAheadDrift(repo) {
|
|
|
7661
7758
|
// src/commands.push.guards.ts
|
|
7662
7759
|
init_push_checks();
|
|
7663
7760
|
init_utils();
|
|
7664
|
-
import { join as
|
|
7761
|
+
import { join as join56, relative as relative7 } from "node:path";
|
|
7665
7762
|
function guardGitlinks(repo) {
|
|
7666
|
-
const gitlinks = findGitlinks(
|
|
7763
|
+
const gitlinks = findGitlinks(join56(repo, "shared"));
|
|
7667
7764
|
if (gitlinks.length === 0) return;
|
|
7668
7765
|
for (const p of gitlinks) {
|
|
7669
7766
|
const rel = relative7(repo, p).replaceAll("\\", "/");
|
|
@@ -7773,9 +7870,9 @@ init_color();
|
|
|
7773
7870
|
init_config();
|
|
7774
7871
|
init_config_sharedDirs_guard();
|
|
7775
7872
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
7776
|
-
import { copyFileSync, existsSync as
|
|
7873
|
+
import { copyFileSync, existsSync as existsSync46, mkdirSync as mkdirSync15, readdirSync as readdirSync18, rmSync as rmSync17 } from "node:fs";
|
|
7777
7874
|
import { homedir as homedir7 } from "node:os";
|
|
7778
|
-
import { join as
|
|
7875
|
+
import { join as join57, relative as relative8, sep as sep10 } from "node:path";
|
|
7779
7876
|
init_push_leak_verdict();
|
|
7780
7877
|
init_push_gitleaks();
|
|
7781
7878
|
init_utils_fs();
|
|
@@ -7787,7 +7884,7 @@ function stageSessionDir(localDir, dstDir, changed) {
|
|
|
7787
7884
|
const matching = [...changed].filter((p) => p.startsWith(prefix));
|
|
7788
7885
|
if (matching.length === 0) return false;
|
|
7789
7886
|
for (const src of matching) {
|
|
7790
|
-
copyFileAtomic(src,
|
|
7887
|
+
copyFileAtomic(src, join57(dstDir, relative8(localDir, src)));
|
|
7791
7888
|
}
|
|
7792
7889
|
return true;
|
|
7793
7890
|
}
|
|
@@ -7803,14 +7900,14 @@ function stageSessions(tmpRoot, map, changed) {
|
|
|
7803
7900
|
if (!p || p === "TBD") continue;
|
|
7804
7901
|
reverse.set(encodePath(p), logical);
|
|
7805
7902
|
}
|
|
7806
|
-
const localProjects =
|
|
7807
|
-
if (!
|
|
7903
|
+
const localProjects = join57(claudeHome(), "projects");
|
|
7904
|
+
if (!existsSync46(localProjects)) return 0;
|
|
7808
7905
|
let staged = 0;
|
|
7809
|
-
for (const dir of
|
|
7906
|
+
for (const dir of readdirSync18(localProjects)) {
|
|
7810
7907
|
const logical = reverse.get(dir);
|
|
7811
7908
|
if (!logical) continue;
|
|
7812
|
-
const localDir =
|
|
7813
|
-
const dstDir =
|
|
7909
|
+
const localDir = join57(localProjects, dir);
|
|
7910
|
+
const dstDir = join57(tmpRoot, "shared", "projects", logical);
|
|
7814
7911
|
if (stageSessionDir(localDir, dstDir, changed)) staged++;
|
|
7815
7912
|
}
|
|
7816
7913
|
return staged;
|
|
@@ -7826,9 +7923,9 @@ function stageExtras(tmpRoot, map) {
|
|
|
7826
7923
|
if (!localRoot || localRoot === "TBD") continue;
|
|
7827
7924
|
for (const dirname13 of dirnames) {
|
|
7828
7925
|
if (!whitelist.includes(dirname13)) continue;
|
|
7829
|
-
const src =
|
|
7830
|
-
if (!
|
|
7831
|
-
const dst =
|
|
7926
|
+
const src = join57(localRoot, dirname13);
|
|
7927
|
+
if (!existsSync46(src)) continue;
|
|
7928
|
+
const dst = join57(tmpRoot, "shared", "extras", logical, dirname13);
|
|
7832
7929
|
copyExtras(src, dst);
|
|
7833
7930
|
staged++;
|
|
7834
7931
|
}
|
|
@@ -7836,19 +7933,19 @@ function stageExtras(tmpRoot, map) {
|
|
|
7836
7933
|
return staged;
|
|
7837
7934
|
}
|
|
7838
7935
|
function previewPushLeaks(map, opts = {}) {
|
|
7839
|
-
const cacheDir =
|
|
7936
|
+
const cacheDir = join57(homedir7(), ".cache", "claude-nomad");
|
|
7840
7937
|
mkdirSync15(cacheDir, { recursive: true });
|
|
7841
7938
|
const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes4(4).toString("hex")}`;
|
|
7842
|
-
const tmpRoot =
|
|
7939
|
+
const tmpRoot = join57(cacheDir, `push-preview-tree-${stamp}`);
|
|
7843
7940
|
try {
|
|
7844
7941
|
const sessionCount = stageSessions(tmpRoot, map, opts.selection?.changed);
|
|
7845
7942
|
const extrasCount = stageExtras(tmpRoot, map);
|
|
7846
7943
|
if (sessionCount + extrasCount === 0) {
|
|
7847
7944
|
return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
|
|
7848
7945
|
}
|
|
7849
|
-
const ignoreFile =
|
|
7850
|
-
if (
|
|
7851
|
-
copyFileSync(ignoreFile,
|
|
7946
|
+
const ignoreFile = join57(repoHome(), ".gitleaksignore");
|
|
7947
|
+
if (existsSync46(ignoreFile)) {
|
|
7948
|
+
copyFileSync(ignoreFile, join57(tmpRoot, ".gitleaksignore"));
|
|
7852
7949
|
}
|
|
7853
7950
|
let findings;
|
|
7854
7951
|
try {
|
|
@@ -7975,7 +8072,7 @@ async function runPushCore(opts = {}) {
|
|
|
7975
8072
|
const scannerVersion = probeGitleaks();
|
|
7976
8073
|
const configHash = computeConfigHash();
|
|
7977
8074
|
const old = readManifest(manifestPath());
|
|
7978
|
-
const mapPath =
|
|
8075
|
+
const mapPath = join58(repo, "path-map.json");
|
|
7979
8076
|
const { map, selection, newManifest } = loadSelectionForPush(
|
|
7980
8077
|
mapPath,
|
|
7981
8078
|
old,
|
|
@@ -8026,7 +8123,7 @@ async function cmdPush(opts = {}) {
|
|
|
8026
8123
|
const allowRule = opts.allowRule;
|
|
8027
8124
|
guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
|
|
8028
8125
|
const repo = repoHome();
|
|
8029
|
-
if (!
|
|
8126
|
+
if (!existsSync47(repo)) die(`repo not cloned at ${repo}`);
|
|
8030
8127
|
const handle = acquireLock("push");
|
|
8031
8128
|
if (handle === null) process.exit(0);
|
|
8032
8129
|
try {
|
|
@@ -8044,13 +8141,11 @@ async function cmdPush(opts = {}) {
|
|
|
8044
8141
|
}
|
|
8045
8142
|
|
|
8046
8143
|
// src/commands.sync.ts
|
|
8047
|
-
import { existsSync as
|
|
8048
|
-
import { join as
|
|
8144
|
+
import { existsSync as existsSync48 } from "node:fs";
|
|
8145
|
+
import { join as join59 } from "node:path";
|
|
8049
8146
|
init_config();
|
|
8050
8147
|
init_color();
|
|
8051
8148
|
init_utils();
|
|
8052
|
-
init_utils_fs();
|
|
8053
|
-
init_utils_json();
|
|
8054
8149
|
function isNoopSync(pull, pushOutcome) {
|
|
8055
8150
|
if (!pushOutcome.ok) return false;
|
|
8056
8151
|
if (pull.localOnly !== 0 || pull.divergedKeptLocal !== 0) return false;
|
|
@@ -8171,28 +8266,25 @@ async function runSyncWet(verbose) {
|
|
|
8171
8266
|
const pushOutcome = await runSyncPushHalf();
|
|
8172
8267
|
renderWetSync(pull, pushOutcome, verbose);
|
|
8173
8268
|
}
|
|
8174
|
-
async function runSyncDryRun(
|
|
8175
|
-
|
|
8176
|
-
const mapPath = join58(repo, "path-map.json");
|
|
8177
|
-
const map = existsSync47(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
8178
|
-
computePreview(ts, map, "pull");
|
|
8269
|
+
async function runSyncDryRun() {
|
|
8270
|
+
runPullCore({ dryRun: true });
|
|
8179
8271
|
log("push preview below is computed against pre-pull state (a real sync pushes after pull)");
|
|
8180
8272
|
await runPushCore({ dryRun: true });
|
|
8273
|
+
log("dry-run complete; nothing applied to ~/.claude/, nothing pushed");
|
|
8181
8274
|
}
|
|
8182
8275
|
async function cmdSync(opts = {}) {
|
|
8183
8276
|
const dryRun = opts.dryRun === true;
|
|
8184
8277
|
const verbose = opts.verbose === true;
|
|
8185
8278
|
const repo = repoHome();
|
|
8186
|
-
|
|
8187
|
-
if (!
|
|
8188
|
-
if (!existsSync47(join58(repo, "shared", "settings.base.json"))) {
|
|
8279
|
+
if (!existsSync48(repo)) die(`repo not cloned at ${repo}`);
|
|
8280
|
+
if (!existsSync48(join59(repo, "shared", "settings.base.json"))) {
|
|
8189
8281
|
die("repo not initialized; run 'nomad init' to scaffold");
|
|
8190
8282
|
}
|
|
8191
8283
|
const handle = acquireLock("sync");
|
|
8192
8284
|
if (handle === null) process.exit(0);
|
|
8193
8285
|
try {
|
|
8194
8286
|
if (dryRun) {
|
|
8195
|
-
await runSyncDryRun(
|
|
8287
|
+
await runSyncDryRun();
|
|
8196
8288
|
} else {
|
|
8197
8289
|
await runSyncWet(verbose);
|
|
8198
8290
|
}
|
|
@@ -8258,8 +8350,8 @@ init_config();
|
|
|
8258
8350
|
|
|
8259
8351
|
// src/crash-report.write.ts
|
|
8260
8352
|
init_config();
|
|
8261
|
-
import { mkdirSync as mkdirSync16, readdirSync as
|
|
8262
|
-
import { join as
|
|
8353
|
+
import { mkdirSync as mkdirSync16, readdirSync as readdirSync19, statSync as statSync11, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "node:fs";
|
|
8354
|
+
import { join as join61 } from "node:path";
|
|
8263
8355
|
|
|
8264
8356
|
// src/crash-report.ts
|
|
8265
8357
|
var CRASH_MAX_STACK_LINES = 50;
|
|
@@ -8338,15 +8430,15 @@ function buildCrashReport(input) {
|
|
|
8338
8430
|
// src/crash-report.redact.ts
|
|
8339
8431
|
import { mkdtempSync as mkdtempSync2, rmSync as rmSync18, writeFileSync as writeFileSync10 } from "node:fs";
|
|
8340
8432
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8341
|
-
import { join as
|
|
8433
|
+
import { join as join60 } from "node:path";
|
|
8342
8434
|
init_push_gitleaks_scan();
|
|
8343
8435
|
var CRASH_SCAN_TIMEOUT_MS = 3e3;
|
|
8344
8436
|
var SCAN_UNAVAILABLE_ADVISORY = "\n\n[gitleaks value-based scan unavailable; only structural redaction applied. Review before sharing this file publicly.]\n";
|
|
8345
8437
|
function redactWithGitleaks(text, scan = scanFile) {
|
|
8346
8438
|
let dir;
|
|
8347
8439
|
try {
|
|
8348
|
-
dir = mkdtempSync2(
|
|
8349
|
-
const tmp =
|
|
8440
|
+
dir = mkdtempSync2(join60(tmpdir2(), "nomad-crash-scan-"));
|
|
8441
|
+
const tmp = join60(dir, "crash.txt");
|
|
8350
8442
|
writeFileSync10(tmp, text, { mode: 384 });
|
|
8351
8443
|
const findings = scan(tmp, false, CRASH_SCAN_TIMEOUT_MS);
|
|
8352
8444
|
if (findings === null) return text + SCAN_UNAVAILABLE_ADVISORY;
|
|
@@ -8369,9 +8461,9 @@ init_utils();
|
|
|
8369
8461
|
var CRASH_RETENTION_KEEP = 20;
|
|
8370
8462
|
function listCrashFiles(dir = crashDir()) {
|
|
8371
8463
|
try {
|
|
8372
|
-
return
|
|
8464
|
+
return readdirSync19(dir).flatMap((name) => {
|
|
8373
8465
|
try {
|
|
8374
|
-
return [{ name, mtimeMs: statSync11(
|
|
8466
|
+
return [{ name, mtimeMs: statSync11(join61(dir, name)).mtimeMs }];
|
|
8375
8467
|
} catch {
|
|
8376
8468
|
return [];
|
|
8377
8469
|
}
|
|
@@ -8385,14 +8477,14 @@ function pruneCrashDir(dir, keep = CRASH_RETENTION_KEEP) {
|
|
|
8385
8477
|
const targets = prunableByCount(files, keep);
|
|
8386
8478
|
for (const name of targets) {
|
|
8387
8479
|
try {
|
|
8388
|
-
unlinkSync2(
|
|
8480
|
+
unlinkSync2(join61(dir, name));
|
|
8389
8481
|
} catch {
|
|
8390
8482
|
}
|
|
8391
8483
|
}
|
|
8392
8484
|
}
|
|
8393
8485
|
function writeCrashReport(text, dir = crashDir()) {
|
|
8394
8486
|
mkdirSync16(dir, { recursive: true, mode: 448 });
|
|
8395
|
-
const path =
|
|
8487
|
+
const path = join61(dir, `crash-${nowTimestamp()}-${process.pid}.txt`);
|
|
8396
8488
|
writeFileSync11(path, text, { mode: 384 });
|
|
8397
8489
|
pruneCrashDir(dir);
|
|
8398
8490
|
return path;
|
|
@@ -8423,18 +8515,18 @@ function handleCrash(err, argv, opts) {
|
|
|
8423
8515
|
|
|
8424
8516
|
// src/diff.ts
|
|
8425
8517
|
init_config();
|
|
8426
|
-
import { existsSync as
|
|
8427
|
-
import { join as
|
|
8518
|
+
import { existsSync as existsSync49 } from "node:fs";
|
|
8519
|
+
import { join as join62 } from "node:path";
|
|
8428
8520
|
init_utils();
|
|
8429
8521
|
init_utils_fs();
|
|
8430
8522
|
init_utils_json();
|
|
8431
8523
|
function cmdDiff() {
|
|
8432
8524
|
try {
|
|
8433
8525
|
const repo = repoHome();
|
|
8434
|
-
if (!
|
|
8526
|
+
if (!existsSync49(repo)) die(`repo not cloned at ${repo}`);
|
|
8435
8527
|
const ts = freshBackupTs(backupBase());
|
|
8436
|
-
const mapPath =
|
|
8437
|
-
const map =
|
|
8528
|
+
const mapPath = join62(repo, "path-map.json");
|
|
8529
|
+
const map = existsSync49(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
8438
8530
|
divergenceCheckExtras(ts);
|
|
8439
8531
|
computePreview(ts, map, "diff");
|
|
8440
8532
|
} catch (err) {
|
|
@@ -8449,8 +8541,8 @@ function cmdDiff() {
|
|
|
8449
8541
|
|
|
8450
8542
|
// src/init.ts
|
|
8451
8543
|
init_config();
|
|
8452
|
-
import { existsSync as
|
|
8453
|
-
import { join as
|
|
8544
|
+
import { existsSync as existsSync51, mkdirSync as mkdirSync17, writeFileSync as writeFileSync12 } from "node:fs";
|
|
8545
|
+
import { join as join64 } from "node:path";
|
|
8454
8546
|
|
|
8455
8547
|
// src/init.gh-onboard.ts
|
|
8456
8548
|
init_config();
|
|
@@ -8532,33 +8624,33 @@ init_config();
|
|
|
8532
8624
|
init_utils();
|
|
8533
8625
|
init_utils_fs();
|
|
8534
8626
|
init_utils_json();
|
|
8535
|
-
import { copyFileSync as copyFileSync2, cpSync as cpSync10, existsSync as
|
|
8536
|
-
import { join as
|
|
8627
|
+
import { copyFileSync as copyFileSync2, cpSync as cpSync10, existsSync as existsSync50, rmSync as rmSync19, statSync as statSync12 } from "node:fs";
|
|
8628
|
+
import { join as join63 } from "node:path";
|
|
8537
8629
|
function snapshotIntoShared(map) {
|
|
8538
8630
|
const repo = repoHome();
|
|
8539
8631
|
const claude = claudeHome();
|
|
8540
8632
|
for (const name of allSharedLinks(map)) {
|
|
8541
|
-
const src =
|
|
8542
|
-
if (!
|
|
8543
|
-
const dst =
|
|
8633
|
+
const src = join63(claude, name);
|
|
8634
|
+
if (!existsSync50(src)) continue;
|
|
8635
|
+
const dst = join63(repo, "shared", name);
|
|
8544
8636
|
if (statSync12(src).isDirectory()) {
|
|
8545
|
-
const gk =
|
|
8546
|
-
if (
|
|
8637
|
+
const gk = join63(dst, ".gitkeep");
|
|
8638
|
+
if (existsSync50(gk)) rmSync19(gk);
|
|
8547
8639
|
cpSync10(src, dst, { recursive: true, force: false, errorOnExist: true });
|
|
8548
8640
|
} else {
|
|
8549
8641
|
copyFileSync2(src, dst);
|
|
8550
8642
|
}
|
|
8551
8643
|
log(`snapshotted shared/${name} from ${src}`);
|
|
8552
8644
|
}
|
|
8553
|
-
const userSettings =
|
|
8554
|
-
if (
|
|
8645
|
+
const userSettings = join63(claude, "settings.json");
|
|
8646
|
+
if (existsSync50(userSettings)) {
|
|
8555
8647
|
let parsed;
|
|
8556
8648
|
try {
|
|
8557
8649
|
parsed = readJson(userSettings);
|
|
8558
8650
|
} catch (err) {
|
|
8559
8651
|
return die(`malformed ${userSettings}: ${err.message}`);
|
|
8560
8652
|
}
|
|
8561
|
-
const hostFile =
|
|
8653
|
+
const hostFile = join63(repo, "hosts", `${HOST}.json`);
|
|
8562
8654
|
writeJsonAtomic(hostFile, parsed);
|
|
8563
8655
|
log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
|
|
8564
8656
|
}
|
|
@@ -8572,14 +8664,14 @@ var GITATTRIBUTES = "# nomad: sync content is byte-managed, disable all line-end
|
|
|
8572
8664
|
var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
|
|
8573
8665
|
function preflightConflict(repoHome2) {
|
|
8574
8666
|
const candidates = [
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8667
|
+
join64(repoHome2, "shared", "settings.base.json"),
|
|
8668
|
+
join64(repoHome2, "shared", "CLAUDE.md"),
|
|
8669
|
+
join64(repoHome2, "path-map.json"),
|
|
8670
|
+
join64(repoHome2, "hosts"),
|
|
8671
|
+
join64(repoHome2, "shared")
|
|
8580
8672
|
];
|
|
8581
8673
|
for (const c of candidates) {
|
|
8582
|
-
if (
|
|
8674
|
+
if (existsSync51(c)) return c;
|
|
8583
8675
|
}
|
|
8584
8676
|
return null;
|
|
8585
8677
|
}
|
|
@@ -8597,27 +8689,27 @@ function cmdInit(opts = {}) {
|
|
|
8597
8689
|
die(`already initialized; refusing to clobber ${conflict}`);
|
|
8598
8690
|
}
|
|
8599
8691
|
ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
|
|
8600
|
-
mkdirSync17(
|
|
8601
|
-
mkdirSync17(
|
|
8692
|
+
mkdirSync17(join64(repo, "shared"), { recursive: true });
|
|
8693
|
+
mkdirSync17(join64(repo, "hosts"), { recursive: true });
|
|
8602
8694
|
for (const name of SHARED_KEEP_DIRS) {
|
|
8603
|
-
mkdirSync17(
|
|
8695
|
+
mkdirSync17(join64(repo, "shared", name), { recursive: true });
|
|
8604
8696
|
}
|
|
8605
|
-
const userClaudeMd =
|
|
8606
|
-
if (!snapshot || !
|
|
8607
|
-
writeFileSync12(
|
|
8697
|
+
const userClaudeMd = join64(claude, "CLAUDE.md");
|
|
8698
|
+
if (!snapshot || !existsSync51(userClaudeMd)) {
|
|
8699
|
+
writeFileSync12(join64(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
|
|
8608
8700
|
item("created shared/CLAUDE.md");
|
|
8609
8701
|
}
|
|
8610
8702
|
for (const name of SHARED_KEEP_DIRS) {
|
|
8611
|
-
writeFileSync12(
|
|
8703
|
+
writeFileSync12(join64(repo, "shared", name, ".gitkeep"), "");
|
|
8612
8704
|
item(`created shared/${name}/.gitkeep`);
|
|
8613
8705
|
}
|
|
8614
|
-
writeFileSync12(
|
|
8706
|
+
writeFileSync12(join64(repo, "hosts", ".gitkeep"), "");
|
|
8615
8707
|
item("created hosts/.gitkeep");
|
|
8616
|
-
writeJsonAtomic(
|
|
8708
|
+
writeJsonAtomic(join64(repo, "shared", "settings.base.json"), {});
|
|
8617
8709
|
item("created shared/settings.base.json");
|
|
8618
|
-
writeJsonAtomic(
|
|
8710
|
+
writeJsonAtomic(join64(repo, "path-map.json"), { projects: {} });
|
|
8619
8711
|
item("created path-map.json");
|
|
8620
|
-
writeFileSync12(
|
|
8712
|
+
writeFileSync12(join64(repo, ".gitattributes"), GITATTRIBUTES);
|
|
8621
8713
|
item("created .gitattributes");
|
|
8622
8714
|
if (snapshot) {
|
|
8623
8715
|
snapshotIntoShared({ projects: {} });
|
|
@@ -8683,21 +8775,21 @@ function maybeDisableRepoActions(repoHome2, run) {
|
|
|
8683
8775
|
// src/init.prompt.ts
|
|
8684
8776
|
init_config();
|
|
8685
8777
|
init_utils();
|
|
8686
|
-
import { existsSync as
|
|
8687
|
-
import { join as
|
|
8778
|
+
import { existsSync as existsSync52, readdirSync as readdirSync20, statSync as statSync13 } from "node:fs";
|
|
8779
|
+
import { join as join65 } from "node:path";
|
|
8688
8780
|
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
8689
8781
|
function nonEmptyExists(path) {
|
|
8690
|
-
if (!
|
|
8782
|
+
if (!existsSync52(path)) return false;
|
|
8691
8783
|
try {
|
|
8692
|
-
if (statSync13(path).isDirectory()) return
|
|
8784
|
+
if (statSync13(path).isDirectory()) return readdirSync20(path).length > 0;
|
|
8693
8785
|
return true;
|
|
8694
8786
|
} catch {
|
|
8695
8787
|
return false;
|
|
8696
8788
|
}
|
|
8697
8789
|
}
|
|
8698
8790
|
function hasExistingClaudeConfig(claudeHome2) {
|
|
8699
|
-
if (
|
|
8700
|
-
return SHARED_LINKS.some((name) => nonEmptyExists(
|
|
8791
|
+
if (existsSync52(join65(claudeHome2, "settings.json"))) return true;
|
|
8792
|
+
return SHARED_LINKS.some((name) => nonEmptyExists(join65(claudeHome2, name)));
|
|
8701
8793
|
}
|
|
8702
8794
|
async function confirmSnapshotDefault(claudeHome2) {
|
|
8703
8795
|
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
@@ -9004,7 +9096,7 @@ function parseSyncArgs(argv) {
|
|
|
9004
9096
|
// package.json
|
|
9005
9097
|
var package_default = {
|
|
9006
9098
|
name: "claude-nomad",
|
|
9007
|
-
version: "0.62.
|
|
9099
|
+
version: "0.62.4",
|
|
9008
9100
|
type: "module",
|
|
9009
9101
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
9010
9102
|
keywords: [
|
|
@@ -9245,15 +9337,15 @@ var DEFAULT_HELP = [
|
|
|
9245
9337
|
init_config();
|
|
9246
9338
|
init_utils();
|
|
9247
9339
|
init_utils_json();
|
|
9248
|
-
import { existsSync as
|
|
9249
|
-
import { join as
|
|
9340
|
+
import { existsSync as existsSync53, readFileSync as readFileSync20, readdirSync as readdirSync21 } from "node:fs";
|
|
9341
|
+
import { join as join66 } from "node:path";
|
|
9250
9342
|
function resumeCmd(sessionId) {
|
|
9251
9343
|
if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
|
|
9252
9344
|
fail(`invalid session id: ${sessionId}`);
|
|
9253
9345
|
process.exit(1);
|
|
9254
9346
|
}
|
|
9255
|
-
const projectsRoot =
|
|
9256
|
-
if (!
|
|
9347
|
+
const projectsRoot = join66(claudeHome(), "projects");
|
|
9348
|
+
if (!existsSync53(projectsRoot)) {
|
|
9257
9349
|
fail(`${projectsRoot} does not exist`);
|
|
9258
9350
|
process.exit(1);
|
|
9259
9351
|
}
|
|
@@ -9267,8 +9359,8 @@ function resumeCmd(sessionId) {
|
|
|
9267
9359
|
fail(`no cwd field found in ${jsonlPath}`);
|
|
9268
9360
|
process.exit(1);
|
|
9269
9361
|
}
|
|
9270
|
-
const mapPath =
|
|
9271
|
-
if (!
|
|
9362
|
+
const mapPath = join66(repoHome(), "path-map.json");
|
|
9363
|
+
if (!existsSync53(mapPath)) {
|
|
9272
9364
|
fail("path-map.json missing");
|
|
9273
9365
|
process.exit(1);
|
|
9274
9366
|
}
|
|
@@ -9290,9 +9382,9 @@ function resumeCmd(sessionId) {
|
|
|
9290
9382
|
console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
|
|
9291
9383
|
}
|
|
9292
9384
|
function findTranscriptPath(projectsRoot, sessionId) {
|
|
9293
|
-
for (const dir of
|
|
9294
|
-
const candidate =
|
|
9295
|
-
if (
|
|
9385
|
+
for (const dir of readdirSync21(projectsRoot)) {
|
|
9386
|
+
const candidate = join66(projectsRoot, dir, `${sessionId}.jsonl`);
|
|
9387
|
+
if (existsSync53(candidate)) return candidate;
|
|
9296
9388
|
}
|
|
9297
9389
|
return null;
|
|
9298
9390
|
}
|
package/package.json
CHANGED