claude-nomad 0.64.0 → 0.65.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 +104 -0
- package/README.md +34 -77
- package/dist/nomad.mjs +750 -297
- package/package.json +1 -1
package/dist/nomad.mjs
CHANGED
|
@@ -42,13 +42,42 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
42
42
|
));
|
|
43
43
|
|
|
44
44
|
// src/config.never-sync.ts
|
|
45
|
+
function stripTrailingDotsAndWhitespace(name) {
|
|
46
|
+
let end = name.length;
|
|
47
|
+
while (end > 0 && (name[end - 1] === "." || /\s/.test(name[end - 1]))) end -= 1;
|
|
48
|
+
return name.slice(0, end);
|
|
49
|
+
}
|
|
45
50
|
function isSecretFileName(name) {
|
|
46
|
-
|
|
51
|
+
const stripped = stripTrailingDotsAndWhitespace(name);
|
|
52
|
+
return SECRET_FILE_PATTERNS.some((re) => re.test(stripped));
|
|
47
53
|
}
|
|
48
54
|
function isDeniedName(blockSet, name) {
|
|
49
|
-
|
|
55
|
+
const stripped = stripTrailingDotsAndWhitespace(name);
|
|
56
|
+
return blockSet.has(name) || blockSet.has(name.toLowerCase()) || blockSet.has(stripped) || blockSet.has(stripped.toLowerCase()) || isSecretFileName(name);
|
|
57
|
+
}
|
|
58
|
+
function isClaudeExtraName(name) {
|
|
59
|
+
return stripTrailingDotsAndWhitespace(name).toLowerCase() === ".claude";
|
|
60
|
+
}
|
|
61
|
+
function isExtrasScoped(segments) {
|
|
62
|
+
return segments[0] === "shared" && segments[1] === "extras";
|
|
63
|
+
}
|
|
64
|
+
function blockSetFor(segments) {
|
|
65
|
+
if (!isExtrasScoped(segments)) return NEVER_SYNC;
|
|
66
|
+
return isClaudeExtraName(segments[3] ?? "") ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
|
|
67
|
+
}
|
|
68
|
+
function deniedSegmentFor(path) {
|
|
69
|
+
const segments = path.split("/");
|
|
70
|
+
const blockSet = blockSetFor(segments);
|
|
71
|
+
const scan = isExtrasScoped(segments) ? segments.slice(4) : segments;
|
|
72
|
+
for (const segment of scan) {
|
|
73
|
+
if (isDeniedName(blockSet, segment)) return segment;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function isNeverSync(path) {
|
|
78
|
+
return deniedSegmentFor(path) !== null;
|
|
50
79
|
}
|
|
51
|
-
var NEVER_SYNC, CLAUDE_EXTRA_NEVER_SYNC, SECRET_FILE_PATTERNS;
|
|
80
|
+
var NEVER_SYNC, CLAUDE_EXTRA_NEVER_SYNC, ALWAYS_NEVER_SYNC, SECRET_FILE_PATTERNS;
|
|
52
81
|
var init_config_never_sync = __esm({
|
|
53
82
|
"src/config.never-sync.ts"() {
|
|
54
83
|
"use strict";
|
|
@@ -78,6 +107,13 @@ var init_config_never_sync = __esm({
|
|
|
78
107
|
"sessions"
|
|
79
108
|
]);
|
|
80
109
|
CLAUDE_EXTRA_NEVER_SYNC = /* @__PURE__ */ new Set([...NEVER_SYNC, "projects"]);
|
|
110
|
+
ALWAYS_NEVER_SYNC = /* @__PURE__ */ new Set([
|
|
111
|
+
".claude.json",
|
|
112
|
+
".credentials.json",
|
|
113
|
+
"settings.local.json",
|
|
114
|
+
"history.jsonl",
|
|
115
|
+
"stats-cache.json"
|
|
116
|
+
]);
|
|
81
117
|
SECRET_FILE_PATTERNS = [
|
|
82
118
|
/^\.env(\..+)?$/i,
|
|
83
119
|
// .env, .env.local, .env.production
|
|
@@ -302,14 +338,67 @@ function assertSafeLogical(logical) {
|
|
|
302
338
|
);
|
|
303
339
|
}
|
|
304
340
|
}
|
|
341
|
+
function validateSharedDirEntry(entry) {
|
|
342
|
+
if (typeof entry !== "string") {
|
|
343
|
+
return { reason: "not-a-string", message: "not a string" };
|
|
344
|
+
}
|
|
345
|
+
if (!SAFE_SEGMENT.test(entry) || entry === "." || entry === "..") {
|
|
346
|
+
return {
|
|
347
|
+
reason: "not-a-segment",
|
|
348
|
+
message: 'not a single path segment (contains a path separator, an unsupported character, "." or "..")'
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
const addressed = stripTrailingDots(entry);
|
|
352
|
+
const named = classifyDeniedName(addressed);
|
|
353
|
+
if (named !== null) {
|
|
354
|
+
return addressed === entry ? named : {
|
|
355
|
+
reason: named.reason,
|
|
356
|
+
message: `a trailing-dot name shadowing ${named.message}`
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
if (entry.endsWith(".")) {
|
|
360
|
+
return {
|
|
361
|
+
reason: "win32-alias",
|
|
362
|
+
message: 'a trailing-dot name (git for Windows refuses to create or check out a path ending in ".")'
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
function stripTrailingDots(name) {
|
|
368
|
+
let end = name.length;
|
|
369
|
+
while (end > 0 && name[end - 1] === ".") end -= 1;
|
|
370
|
+
return name.slice(0, end);
|
|
371
|
+
}
|
|
372
|
+
function classifyDeniedName(name) {
|
|
373
|
+
const folded = name.toLowerCase();
|
|
374
|
+
if (NEVER_SYNC.has(name) || NEVER_SYNC.has(folded)) {
|
|
375
|
+
return { reason: "never-sync", message: "a never-sync name" };
|
|
376
|
+
}
|
|
377
|
+
if (RESERVED_SHARED.has(name) || RESERVED_SHARED_FOLDED.has(folded)) {
|
|
378
|
+
return { reason: "reserved", message: "a reserved shared/ name" };
|
|
379
|
+
}
|
|
380
|
+
const dot = folded.indexOf(".");
|
|
381
|
+
const stem = dot === -1 ? folded : folded.slice(0, dot);
|
|
382
|
+
if (WIN32_DEVICE_NAMES.has(stem)) {
|
|
383
|
+
return { reason: "reserved", message: "a reserved Windows device name" };
|
|
384
|
+
}
|
|
385
|
+
if (isSecretFileName(name)) {
|
|
386
|
+
return {
|
|
387
|
+
reason: "secret-shaped",
|
|
388
|
+
message: "a credential-shaped filename (.env, id_rsa, credentials, *.pem, *.key and similar)"
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
function mayJoinRefusedEntry(entry, reason, remediable) {
|
|
394
|
+
if (UNJOINABLE_REASONS.has(reason)) return false;
|
|
395
|
+
if (entry.endsWith(".")) return true;
|
|
396
|
+
return remediable.has(reason);
|
|
397
|
+
}
|
|
305
398
|
function isValidSharedDir(entry) {
|
|
306
|
-
|
|
307
|
-
if (!SAFE_SEGMENT.test(entry) || entry === "." || entry === "..") return false;
|
|
308
|
-
if (NEVER_SYNC.has(entry)) return false;
|
|
309
|
-
if (RESERVED_SHARED.has(entry)) return false;
|
|
310
|
-
return true;
|
|
399
|
+
return validateSharedDirEntry(entry) === null;
|
|
311
400
|
}
|
|
312
|
-
var SAFE_LOGICAL, SAFE_SEGMENT, RESERVED_SHARED;
|
|
401
|
+
var SAFE_LOGICAL, SAFE_SEGMENT, RESERVED_SHARED, RESERVED_SHARED_FOLDED, WIN32_DEVICE_NAMES, UNJOINABLE_REASONS;
|
|
313
402
|
var init_config_sharedDirs_guard = __esm({
|
|
314
403
|
"src/config.sharedDirs.guard.ts"() {
|
|
315
404
|
"use strict";
|
|
@@ -331,6 +420,19 @@ var init_config_sharedDirs_guard = __esm({
|
|
|
331
420
|
"extras",
|
|
332
421
|
"projects"
|
|
333
422
|
]);
|
|
423
|
+
RESERVED_SHARED_FOLDED = new Set([...RESERVED_SHARED].map((name) => name.toLowerCase()));
|
|
424
|
+
WIN32_DEVICE_NAMES = /* @__PURE__ */ new Set([
|
|
425
|
+
"con",
|
|
426
|
+
"prn",
|
|
427
|
+
"aux",
|
|
428
|
+
"nul",
|
|
429
|
+
...Array.from({ length: 9 }, (_unused, i) => `com${i + 1}`),
|
|
430
|
+
...Array.from({ length: 9 }, (_unused, i) => `lpt${i + 1}`)
|
|
431
|
+
]);
|
|
432
|
+
UNJOINABLE_REASONS = /* @__PURE__ */ new Set([
|
|
433
|
+
"not-a-string",
|
|
434
|
+
"not-a-segment"
|
|
435
|
+
]);
|
|
334
436
|
}
|
|
335
437
|
});
|
|
336
438
|
|
|
@@ -541,20 +643,29 @@ function manifestPath() {
|
|
|
541
643
|
function sharedBaselinePath() {
|
|
542
644
|
return join(home(), ".cache", "claude-nomad", `shared-baseline-${encodeURIComponent(HOST)}.json`);
|
|
543
645
|
}
|
|
544
|
-
function allSharedLinks(map) {
|
|
646
|
+
function allSharedLinks(map, opts = {}) {
|
|
647
|
+
const emit = opts.quiet === true ? () => void 0 : warn;
|
|
648
|
+
const raw = map.sharedDirs;
|
|
649
|
+
if (raw !== void 0 && !Array.isArray(raw)) {
|
|
650
|
+
emit("sharedDirs in path-map.json is not an array; ignoring the whole field");
|
|
651
|
+
return [...SHARED_LINKS];
|
|
652
|
+
}
|
|
545
653
|
const extras = [];
|
|
546
|
-
for (const entry of
|
|
547
|
-
|
|
654
|
+
for (const entry of raw ?? []) {
|
|
655
|
+
const rejection = validateSharedDirEntry(entry);
|
|
656
|
+
if (rejection === null) {
|
|
548
657
|
extras.push(entry);
|
|
549
658
|
} else {
|
|
550
|
-
|
|
551
|
-
`sharedDirs entry ${JSON.stringify(entry)} is invalid (path separator, reserved name, or NEVER_SYNC); skipping`
|
|
552
|
-
);
|
|
659
|
+
emit(`sharedDirs entry ${JSON.stringify(entry)} rejected: ${rejection.message}; skipping`);
|
|
553
660
|
}
|
|
554
661
|
}
|
|
555
662
|
return [...SHARED_LINKS, ...extras];
|
|
556
663
|
}
|
|
557
|
-
|
|
664
|
+
function sharedDirEntries(map) {
|
|
665
|
+
const raw = map.sharedDirs;
|
|
666
|
+
return Array.isArray(raw) ? raw : [];
|
|
667
|
+
}
|
|
668
|
+
var SETTINGS_SCHEMA_URL, NPM_REGISTRY_LATEST_URL, GITLEAKS_PINNED_VERSION, GITLEAKS_SCAN_TIMEOUT_MS, HOST, SHARED_LINKS, GSD_PREFIX, GSD_DROPPED_NAMES, SUPPORTED_EXTRAS, PUSH_ALLOWED_STATIC;
|
|
558
669
|
var init_config = __esm({
|
|
559
670
|
"src/config.ts"() {
|
|
560
671
|
"use strict";
|
|
@@ -571,13 +682,6 @@ var init_config = __esm({
|
|
|
571
682
|
GSD_PREFIX = "gsd-";
|
|
572
683
|
GSD_DROPPED_NAMES = ["hooks", "agents"];
|
|
573
684
|
SUPPORTED_EXTRAS = [".planning", "CLAUDE.md", ".claude"];
|
|
574
|
-
ALWAYS_NEVER_SYNC = /* @__PURE__ */ new Set([
|
|
575
|
-
".claude.json",
|
|
576
|
-
".credentials.json",
|
|
577
|
-
"settings.local.json",
|
|
578
|
-
"history.jsonl",
|
|
579
|
-
"stats-cache.json"
|
|
580
|
-
]);
|
|
581
685
|
PUSH_ALLOWED_STATIC = [
|
|
582
686
|
"shared/CLAUDE.md",
|
|
583
687
|
"shared/my-statusline.cjs",
|
|
@@ -809,13 +913,20 @@ function probeUnmergedIndex(repo) {
|
|
|
809
913
|
}
|
|
810
914
|
return raw.split("\0").some(Boolean) ? "unmerged" : "clean";
|
|
811
915
|
}
|
|
812
|
-
function
|
|
813
|
-
|
|
916
|
+
function classifyWedgeWithProbe(repo) {
|
|
917
|
+
const mode = detectWedge(repo);
|
|
918
|
+
if (mode !== null) return { state: mode, probe: "clean" };
|
|
919
|
+
const probe = probeUnmergedIndex(repo);
|
|
920
|
+
return { state: probe === "unmerged" ? "unmerged-index" : null, probe };
|
|
814
921
|
}
|
|
815
922
|
function classifyWedge(repo) {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
923
|
+
return classifyWedgeWithProbe(repo).state;
|
|
924
|
+
}
|
|
925
|
+
function cleanRepoForceRemoteMessage(probe) {
|
|
926
|
+
if (probe === "error") {
|
|
927
|
+
return "could not determine whether the repo is wedged; continuing with a normal pull";
|
|
928
|
+
}
|
|
929
|
+
return "repo is clean, nothing to recover; continuing with a normal pull";
|
|
819
930
|
}
|
|
820
931
|
function unmergedIndexRunbookText(resumeCmd2) {
|
|
821
932
|
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).
|
|
@@ -1422,6 +1533,7 @@ var init_push_leak_verdict = __esm({
|
|
|
1422
1533
|
// src/commands.adopt.ts
|
|
1423
1534
|
init_config();
|
|
1424
1535
|
init_config_sharedDirs_guard();
|
|
1536
|
+
init_exit_codes();
|
|
1425
1537
|
import { cpSync as cpSync4, existsSync as existsSync5, lstatSync as lstatSync5, rmSync as rmSync4 } from "node:fs";
|
|
1426
1538
|
import { join as join6 } from "node:path";
|
|
1427
1539
|
|
|
@@ -1892,7 +2004,7 @@ function copyExtrasFileSkipDiverged(src, dst) {
|
|
|
1892
2004
|
copyExtras(src, dst);
|
|
1893
2005
|
}
|
|
1894
2006
|
function extrasDenySet(dirname14) {
|
|
1895
|
-
return dirname14
|
|
2007
|
+
return isClaudeExtraName(dirname14) ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
|
|
1896
2008
|
}
|
|
1897
2009
|
function copyExtrasFiltered(src, dst, blockSet) {
|
|
1898
2010
|
rmSync2(dst, { recursive: true, force: true });
|
|
@@ -2004,7 +2116,7 @@ function applySharedLinks(ts, map, opts = {}) {
|
|
|
2004
2116
|
const dryRun = opts.dryRun === true;
|
|
2005
2117
|
const claude = claudeHome();
|
|
2006
2118
|
const repo = repoHome();
|
|
2007
|
-
const linkNames = allSharedLinks(map);
|
|
2119
|
+
const linkNames = allSharedLinks(map, { quiet: opts.quietNames === true });
|
|
2008
2120
|
if (process.platform === "win32") {
|
|
2009
2121
|
applySharedLinksWin32(linkNames, claude, repo, ts, dryRun, opts.onPreview);
|
|
2010
2122
|
return;
|
|
@@ -2022,35 +2134,6 @@ function applySharedLinks(ts, map, opts = {}) {
|
|
|
2022
2134
|
ensureSymlink(linkPath, target);
|
|
2023
2135
|
}
|
|
2024
2136
|
}
|
|
2025
|
-
function syncSharedLinksPush(map) {
|
|
2026
|
-
mirrorSharedNames(map, { adoptNew: true, overlay: false });
|
|
2027
|
-
}
|
|
2028
|
-
function stageLocalSharedEdits(map, ts) {
|
|
2029
|
-
mirrorSharedNames(map, { adoptNew: false, overlay: true, backupTs: ts });
|
|
2030
|
-
}
|
|
2031
|
-
function mirrorOneSharedName(name, claude, repo, policy) {
|
|
2032
|
-
const localPath = join5(claude, name);
|
|
2033
|
-
const stat = lstatSync4(localPath, { throwIfNoEntry: false });
|
|
2034
|
-
if (stat === void 0) return;
|
|
2035
|
-
if (stat.isSymbolicLink()) return;
|
|
2036
|
-
const target = join5(repo, "shared", name);
|
|
2037
|
-
if (!policy.adoptNew && !existsSync4(target)) return;
|
|
2038
|
-
if (policy.backupTs !== void 0) backupRepoWrite(target, policy.backupTs, repo);
|
|
2039
|
-
if (policy.overlay && stat.isDirectory()) {
|
|
2040
|
-
copyExtrasOverlayFiltered(localPath, target, ALWAYS_NEVER_SYNC);
|
|
2041
|
-
} else {
|
|
2042
|
-
copyExtrasFiltered(localPath, target, ALWAYS_NEVER_SYNC);
|
|
2043
|
-
}
|
|
2044
|
-
}
|
|
2045
|
-
function mirrorSharedNames(map, policy) {
|
|
2046
|
-
if (process.platform !== "win32") return;
|
|
2047
|
-
if (map === null) return;
|
|
2048
|
-
const claude = claudeHome();
|
|
2049
|
-
const repo = repoHome();
|
|
2050
|
-
for (const name of allSharedLinks(map)) {
|
|
2051
|
-
mirrorOneSharedName(name, claude, repo, policy);
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
2137
|
function readExistingSettings(settingsPath) {
|
|
2055
2138
|
if (!existsSync4(settingsPath)) return { existing: {}, present: false, malformed: false };
|
|
2056
2139
|
try {
|
|
@@ -2133,7 +2216,7 @@ function readMapIfPresent(repoHome2) {
|
|
|
2133
2216
|
return existsSync5(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
2134
2217
|
}
|
|
2135
2218
|
function isConfiguredTarget(name, map) {
|
|
2136
|
-
return SHARED_LINKS.includes(name) || (map.
|
|
2219
|
+
return SHARED_LINKS.includes(name) || sharedDirEntries(map).includes(name);
|
|
2137
2220
|
}
|
|
2138
2221
|
function isValidAdoptName(name) {
|
|
2139
2222
|
if (SHARED_LINKS.includes(name)) return true;
|
|
@@ -2155,6 +2238,13 @@ function performAdoptMove(name, linkPath, sharedTarget, repo, backup) {
|
|
|
2155
2238
|
}
|
|
2156
2239
|
function cmdAdopt(name, opts = {}) {
|
|
2157
2240
|
const dryRun = opts.dryRun === true;
|
|
2241
|
+
const rejection = validateSharedDirEntry(name);
|
|
2242
|
+
if (rejection !== null && rejection.reason === "secret-shaped") {
|
|
2243
|
+
throw new NomadFatal(
|
|
2244
|
+
`cannot adopt ${JSON.stringify(name)}: ${rejection.message}. If it is listed in sharedDirs in path-map.json, remove it there too.`,
|
|
2245
|
+
{ code: EXIT.GENERIC_FAILURE }
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2158
2248
|
if (!isValidAdoptName(name)) {
|
|
2159
2249
|
fail(`invalid name: ${JSON.stringify(name)}`);
|
|
2160
2250
|
process.exit(1);
|
|
@@ -2568,6 +2658,7 @@ function cmdClean(opts, backupBase2 = backupBase()) {
|
|
|
2568
2658
|
|
|
2569
2659
|
// src/commands.eject.ts
|
|
2570
2660
|
init_config();
|
|
2661
|
+
init_config_sharedDirs_guard();
|
|
2571
2662
|
init_utils();
|
|
2572
2663
|
init_utils_fs();
|
|
2573
2664
|
init_utils_json();
|
|
@@ -2705,6 +2796,19 @@ function materializeOneOrDie(name, linkPath, sharedRoot, done) {
|
|
|
2705
2796
|
);
|
|
2706
2797
|
}
|
|
2707
2798
|
}
|
|
2799
|
+
var WIDENED_REASONS = /* @__PURE__ */ new Set([
|
|
2800
|
+
"never-sync",
|
|
2801
|
+
"reserved",
|
|
2802
|
+
"secret-shaped"
|
|
2803
|
+
]);
|
|
2804
|
+
function ejectNames(map) {
|
|
2805
|
+
const alreadyMaterialized = sharedDirEntries(map).filter((entry) => {
|
|
2806
|
+
if (typeof entry !== "string") return false;
|
|
2807
|
+
const rejection = validateSharedDirEntry(entry);
|
|
2808
|
+
return rejection === null || mayJoinRefusedEntry(entry, rejection.reason, WIDENED_REASONS);
|
|
2809
|
+
});
|
|
2810
|
+
return [.../* @__PURE__ */ new Set([...allSharedLinks(map), ...alreadyMaterialized])];
|
|
2811
|
+
}
|
|
2708
2812
|
function defaultEjectRoots() {
|
|
2709
2813
|
return { claudeHome: claudeHome(), repoHome: repoHome() };
|
|
2710
2814
|
}
|
|
@@ -2712,16 +2816,31 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
|
|
|
2712
2816
|
const dryRun = opts.dryRun === true;
|
|
2713
2817
|
const { claudeHome: claudeHome2, repoHome: repoHome2 } = roots;
|
|
2714
2818
|
const map = readMapIfPresent2(repoHome2);
|
|
2715
|
-
const names =
|
|
2819
|
+
const names = ejectNames(map);
|
|
2716
2820
|
const classifications = /* @__PURE__ */ new Map();
|
|
2717
2821
|
for (const name of names) {
|
|
2718
2822
|
classifications.set(name, classifyName(join11(claudeHome2, name)));
|
|
2719
2823
|
}
|
|
2824
|
+
const base = new Set(allSharedLinks(map, { quiet: true }));
|
|
2825
|
+
for (const name of names) {
|
|
2826
|
+
if (base.has(name)) continue;
|
|
2827
|
+
if (classifications.get(name) !== "materialize") continue;
|
|
2828
|
+
item(`processing rejected entry already present on this host: ${name}`);
|
|
2829
|
+
}
|
|
2720
2830
|
const dangling = names.filter((n) => classifications.get(n) === "dangling");
|
|
2721
|
-
|
|
2831
|
+
const danglingBase = dangling.filter((n) => base.has(n));
|
|
2832
|
+
const danglingRefused = dangling.filter((n) => !base.has(n));
|
|
2833
|
+
if (danglingBase.length > 0) {
|
|
2722
2834
|
fail(
|
|
2723
|
-
`dangling symlink(s): ${
|
|
2835
|
+
`dangling symlink(s): ${danglingBase.join(", ")}. run \`nomad pull\` first to restore the missing target, then re-run \`nomad eject\``
|
|
2724
2836
|
);
|
|
2837
|
+
}
|
|
2838
|
+
if (danglingRefused.length > 0) {
|
|
2839
|
+
fail(
|
|
2840
|
+
`dangling symlink(s) for a refused name nomad cannot restore: ${danglingRefused.join(", ")}. recover the content by hand if you need it, then remove the dead link and re-run \`nomad eject\``
|
|
2841
|
+
);
|
|
2842
|
+
}
|
|
2843
|
+
if (dangling.length > 0) {
|
|
2725
2844
|
process.exit(1);
|
|
2726
2845
|
}
|
|
2727
2846
|
const sharedRoot = resolveSharedRoot(repoHome2);
|
|
@@ -2745,7 +2864,7 @@ import { join as join45 } from "node:path";
|
|
|
2745
2864
|
init_color();
|
|
2746
2865
|
init_config();
|
|
2747
2866
|
import { existsSync as existsSync11, lstatSync as lstatSync8, readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
|
|
2748
|
-
import {
|
|
2867
|
+
import { join as join13, win32 as win32Path } from "node:path";
|
|
2749
2868
|
|
|
2750
2869
|
// src/commands.doctor.format.ts
|
|
2751
2870
|
init_color();
|
|
@@ -2999,19 +3118,30 @@ function reportRepoState(section2) {
|
|
|
2999
3118
|
function repoHasSharedSource(name) {
|
|
3000
3119
|
return existsSync11(join13(repoHome(), "shared", name));
|
|
3001
3120
|
}
|
|
3002
|
-
function win32CopyOkRow(name) {
|
|
3003
|
-
|
|
3121
|
+
function win32CopyOkRow(name, exempt = 0) {
|
|
3122
|
+
const note = exempt > 0 ? dim(` (${exempt} never-synced path(s) not compared)`) : "";
|
|
3123
|
+
return { line: `${green(okGlyph)} ${name}: real copy (win32 copy-sync)${note}`, fail: false };
|
|
3124
|
+
}
|
|
3125
|
+
function relativeUnder(root, path) {
|
|
3126
|
+
const rel = win32Path.relative(root, path);
|
|
3127
|
+
const segments = rel.split(win32Path.sep);
|
|
3128
|
+
if (rel === "" || segments[0] === ".." || win32Path.isAbsolute(rel)) return null;
|
|
3129
|
+
return segments.join("/");
|
|
3004
3130
|
}
|
|
3005
|
-
function
|
|
3006
|
-
|
|
3131
|
+
function divergingRepoPath(line, name, local, shared) {
|
|
3132
|
+
const path = line.replace(/ \((?:local|repo) only\)$/, "");
|
|
3133
|
+
const rel = relativeUnder(local, path) ?? relativeUnder(shared, path);
|
|
3134
|
+
return rel === null ? `shared/${name}` : `shared/${name}/${rel}`;
|
|
3007
3135
|
}
|
|
3008
3136
|
function classifyWin32Copy(name, p) {
|
|
3009
3137
|
const sharedPath = join13(repoHome(), "shared", name);
|
|
3010
3138
|
if (!existsSync11(sharedPath)) return win32CopyOkRow(name);
|
|
3011
|
-
const
|
|
3012
|
-
|
|
3139
|
+
const compared = listDivergingFiles(p, sharedPath);
|
|
3140
|
+
const diverging = compared.filter(
|
|
3141
|
+
(line) => deniedSegmentFor(divergingRepoPath(line, name, p, sharedPath)) === null
|
|
3013
3142
|
);
|
|
3014
|
-
|
|
3143
|
+
const exempt = compared.length - diverging.length;
|
|
3144
|
+
if (diverging.length === 0) return win32CopyOkRow(name, exempt);
|
|
3015
3145
|
return {
|
|
3016
3146
|
line: `${yellow(warnGlyph)} ${name}: ${diverging.length} file(s) diverge from shared/${name}`,
|
|
3017
3147
|
fail: false,
|
|
@@ -3066,7 +3196,7 @@ function classifySymlinkTarget(name, p) {
|
|
|
3066
3196
|
}
|
|
3067
3197
|
function reportSharedLinks(section2, map) {
|
|
3068
3198
|
const claude = claudeHome();
|
|
3069
|
-
for (const name of allSharedLinks(map)) {
|
|
3199
|
+
for (const name of allSharedLinks(map, { quiet: true })) {
|
|
3070
3200
|
const p = join13(claude, name);
|
|
3071
3201
|
const { line, fail: fail2, children } = classifySharedLink(name, p);
|
|
3072
3202
|
addItem(section2, line);
|
|
@@ -3158,8 +3288,9 @@ function reportHostOverrides(section2, base, settings) {
|
|
|
3158
3288
|
// src/commands.doctor.checks.pathmap.ts
|
|
3159
3289
|
init_color();
|
|
3160
3290
|
init_config();
|
|
3161
|
-
|
|
3162
|
-
import {
|
|
3291
|
+
init_config_sharedDirs_guard();
|
|
3292
|
+
import { existsSync as existsSync13, lstatSync as lstatSync9, readdirSync as readdirSync6, realpathSync as realpathSync2 } from "node:fs";
|
|
3293
|
+
import { join as join15, sep as sep4 } from "node:path";
|
|
3163
3294
|
init_utils_json();
|
|
3164
3295
|
function reportMappedProjects(section2, map) {
|
|
3165
3296
|
const mapped = Object.entries(map.projects).filter(([, hosts]) => hosts[HOST]);
|
|
@@ -3199,6 +3330,95 @@ function reportCurrentHostPathsMissing(section2, map) {
|
|
|
3199
3330
|
}
|
|
3200
3331
|
}
|
|
3201
3332
|
}
|
|
3333
|
+
function hasSharedLeftover(entry) {
|
|
3334
|
+
try {
|
|
3335
|
+
return lstatSync9(join15(repoHome(), "shared", entry), { throwIfNoEntry: false }) !== void 0;
|
|
3336
|
+
} catch {
|
|
3337
|
+
return false;
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
var PROBEABLE_REASONS = /* @__PURE__ */ new Set([
|
|
3341
|
+
"never-sync",
|
|
3342
|
+
"secret-shaped"
|
|
3343
|
+
]);
|
|
3344
|
+
function classifyLocalLink(entry) {
|
|
3345
|
+
const linkPath = join15(claudeHome(), entry);
|
|
3346
|
+
let stat;
|
|
3347
|
+
try {
|
|
3348
|
+
stat = lstatSync9(linkPath, { throwIfNoEntry: false });
|
|
3349
|
+
} catch {
|
|
3350
|
+
return "absent";
|
|
3351
|
+
}
|
|
3352
|
+
if (stat?.isSymbolicLink() !== true) return "absent";
|
|
3353
|
+
let target;
|
|
3354
|
+
try {
|
|
3355
|
+
target = realpathSync2(linkPath);
|
|
3356
|
+
} catch {
|
|
3357
|
+
return "dangling";
|
|
3358
|
+
}
|
|
3359
|
+
let root;
|
|
3360
|
+
try {
|
|
3361
|
+
root = realpathSync2(join15(repoHome(), "shared"));
|
|
3362
|
+
} catch {
|
|
3363
|
+
return "foreign";
|
|
3364
|
+
}
|
|
3365
|
+
return target.startsWith(root + sep4) ? "managed" : "foreign";
|
|
3366
|
+
}
|
|
3367
|
+
function reportRejectedSharedDirs(section2, map) {
|
|
3368
|
+
const raw = map.sharedDirs;
|
|
3369
|
+
if (raw !== void 0 && !Array.isArray(raw)) {
|
|
3370
|
+
addItem(
|
|
3371
|
+
section2,
|
|
3372
|
+
`${yellow(warnGlyph)} path-map: sharedDirs is not an array (${typeof raw}); the whole field is ignored`
|
|
3373
|
+
);
|
|
3374
|
+
return;
|
|
3375
|
+
}
|
|
3376
|
+
const entries = Array.isArray(raw) ? raw : [];
|
|
3377
|
+
const probable = [];
|
|
3378
|
+
for (const entry of entries) {
|
|
3379
|
+
const rejection = validateSharedDirEntry(entry);
|
|
3380
|
+
if (rejection === null) continue;
|
|
3381
|
+
if (typeof entry === "string" && mayJoinRefusedEntry(entry, rejection.reason, PROBEABLE_REASONS)) {
|
|
3382
|
+
probable.push(entry);
|
|
3383
|
+
}
|
|
3384
|
+
addItem(
|
|
3385
|
+
section2,
|
|
3386
|
+
`${yellow(warnGlyph)} path-map: sharedDirs entry ${JSON.stringify(entry)} rejected: ${rejection.message}; skipping`
|
|
3387
|
+
);
|
|
3388
|
+
}
|
|
3389
|
+
reportRejectedLeftovers(section2, probable);
|
|
3390
|
+
}
|
|
3391
|
+
function reportRejectedLeftovers(section2, probable) {
|
|
3392
|
+
for (const entry of probable) {
|
|
3393
|
+
const link = classifyLocalLink(entry);
|
|
3394
|
+
if (link === "managed") {
|
|
3395
|
+
addItem(
|
|
3396
|
+
section2,
|
|
3397
|
+
`${yellow(warnGlyph)} path-map: entry ${JSON.stringify(entry)} is a symlink under ~/.claude/ pointing into shared/; copy the content out first (cp -RL), then remove both. nomad will not do it for you`
|
|
3398
|
+
);
|
|
3399
|
+
continue;
|
|
3400
|
+
}
|
|
3401
|
+
if (link === "dangling") {
|
|
3402
|
+
addItem(
|
|
3403
|
+
section2,
|
|
3404
|
+
`${yellow(warnGlyph)} path-map: entry ${JSON.stringify(entry)} is a DANGLING symlink under ~/.claude/ (its target does not resolve); do not delete anything under shared/ yet, since a copy there may be the only one left. nomad cannot restore this name, because the entry is refused: recover the content by hand if you need it, then remove the dead link`
|
|
3405
|
+
);
|
|
3406
|
+
continue;
|
|
3407
|
+
}
|
|
3408
|
+
if (link === "foreign") {
|
|
3409
|
+
addItem(
|
|
3410
|
+
section2,
|
|
3411
|
+
`${yellow(warnGlyph)} path-map: entry ${JSON.stringify(entry)} is a symlink under ~/.claude/ pointing OUTSIDE shared/; nomad does not manage it, so leave it alone and just drop the entry from sharedDirs`
|
|
3412
|
+
);
|
|
3413
|
+
}
|
|
3414
|
+
if (hasSharedLeftover(entry)) {
|
|
3415
|
+
addItem(
|
|
3416
|
+
section2,
|
|
3417
|
+
`${yellow(warnGlyph)} path-map: shared/ entry ${JSON.stringify(entry)} exists in the repo working tree; remove it by hand, nomad will not delete it`
|
|
3418
|
+
);
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3202
3422
|
function reportPathCollisions(section2, map) {
|
|
3203
3423
|
const seen = /* @__PURE__ */ new Map();
|
|
3204
3424
|
let collisionCount = 0;
|
|
@@ -3230,6 +3450,7 @@ function reportPathMap(section2) {
|
|
|
3230
3450
|
}
|
|
3231
3451
|
const map = readJsonSafe(mapPath, mapPath, section2);
|
|
3232
3452
|
if (map === null) return;
|
|
3453
|
+
reportRejectedSharedDirs(section2, map);
|
|
3233
3454
|
const shapeError = validatePathMapShape(map);
|
|
3234
3455
|
if (shapeError !== null) {
|
|
3235
3456
|
addItem(section2, `${red(failGlyph)} ${shapeError}`);
|
|
@@ -3425,7 +3646,7 @@ function reportGitIdentity(section2) {
|
|
|
3425
3646
|
|
|
3426
3647
|
// src/commands.doctor.checks.backups.ts
|
|
3427
3648
|
init_color();
|
|
3428
|
-
import { existsSync as existsSync18, lstatSync as
|
|
3649
|
+
import { existsSync as existsSync18, lstatSync as lstatSync10, readdirSync as readdirSync8 } from "node:fs";
|
|
3429
3650
|
import { join as join21 } from "node:path";
|
|
3430
3651
|
init_config();
|
|
3431
3652
|
var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
|
|
@@ -3443,7 +3664,7 @@ function dirSizeBytes(dir) {
|
|
|
3443
3664
|
let bytes = 0;
|
|
3444
3665
|
for (const entry of safeReaddir(dir)) {
|
|
3445
3666
|
const full = join21(dir, entry);
|
|
3446
|
-
const st =
|
|
3667
|
+
const st = lstatSync10(full, { throwIfNoEntry: false });
|
|
3447
3668
|
if (!st) continue;
|
|
3448
3669
|
if (st.isSymbolicLink()) continue;
|
|
3449
3670
|
if (st.isDirectory()) bytes += dirSizeBytes(full);
|
|
@@ -3922,8 +4143,8 @@ init_config();
|
|
|
3922
4143
|
|
|
3923
4144
|
// src/remap.ts
|
|
3924
4145
|
init_config_sharedDirs_guard();
|
|
3925
|
-
import { cpSync as cpSync6, existsSync as existsSync21, lstatSync as
|
|
3926
|
-
import { dirname as dirname6, join as join28, relative as relative5, sep as
|
|
4146
|
+
import { cpSync as cpSync6, existsSync as existsSync21, lstatSync as lstatSync11, mkdirSync as mkdirSync7, readdirSync as readdirSync10, rmSync as rmSync11, statSync as statSync5 } from "node:fs";
|
|
4147
|
+
import { dirname as dirname6, join as join28, relative as relative5, sep as sep5 } from "node:path";
|
|
3927
4148
|
init_config();
|
|
3928
4149
|
|
|
3929
4150
|
// src/push-manifest.ts
|
|
@@ -4055,7 +4276,7 @@ function copyFileAtomic(src, dst) {
|
|
|
4055
4276
|
renameAtomicRetry(tmp, dst);
|
|
4056
4277
|
}
|
|
4057
4278
|
function hasDeltaForDir(sel, localDir) {
|
|
4058
|
-
const prefix = `${localDir}${
|
|
4279
|
+
const prefix = `${localDir}${sep5}`;
|
|
4059
4280
|
for (const p of sel.changed) if (p.startsWith(prefix)) return true;
|
|
4060
4281
|
for (const p of sel.deleted) if (p.startsWith(prefix)) return true;
|
|
4061
4282
|
return false;
|
|
@@ -4064,7 +4285,7 @@ function skipForSelection(sel, localDir) {
|
|
|
4064
4285
|
return sel !== void 0 && !hasDeltaForDir(sel, localDir);
|
|
4065
4286
|
}
|
|
4066
4287
|
function applySelective(sel, localDir, repoDst) {
|
|
4067
|
-
const prefix = `${localDir}${
|
|
4288
|
+
const prefix = `${localDir}${sep5}`;
|
|
4068
4289
|
for (const src of sel.changed) {
|
|
4069
4290
|
if (!src.startsWith(prefix)) continue;
|
|
4070
4291
|
copyFileAtomic(src, join28(repoDst, relative5(localDir, src)));
|
|
@@ -4082,7 +4303,7 @@ function copyDirJsonlOnly(src, dst) {
|
|
|
4082
4303
|
filter: (srcPath) => {
|
|
4083
4304
|
const rel = relative5(src, srcPath);
|
|
4084
4305
|
if (rel === "") return true;
|
|
4085
|
-
if (rel.split(
|
|
4306
|
+
if (rel.split(sep5).length > 1) return true;
|
|
4086
4307
|
if (statSync5(srcPath).isDirectory()) return true;
|
|
4087
4308
|
if (srcPath.endsWith(".jsonl")) return true;
|
|
4088
4309
|
item(`skip ${rel}: extension not in allowlist`);
|
|
@@ -4142,9 +4363,9 @@ function countLocalOnly(src, dst) {
|
|
|
4142
4363
|
for (const name of readdirSync10(dst)) {
|
|
4143
4364
|
const dstPath = join28(dst, name);
|
|
4144
4365
|
const srcPath = join28(src, name);
|
|
4145
|
-
if (
|
|
4366
|
+
if (lstatSync11(dstPath).isDirectory()) {
|
|
4146
4367
|
count += countLocalOnly(srcPath, dstPath);
|
|
4147
|
-
} else if (
|
|
4368
|
+
} else if (lstatSync11(srcPath, { throwIfNoEntry: false }) === void 0) {
|
|
4148
4369
|
count++;
|
|
4149
4370
|
}
|
|
4150
4371
|
}
|
|
@@ -4334,7 +4555,7 @@ function reportCheckShared(section2, gitleaksReady) {
|
|
|
4334
4555
|
|
|
4335
4556
|
// src/commands.doctor.checks.hooks.scope.ts
|
|
4336
4557
|
init_color();
|
|
4337
|
-
import { existsSync as existsSync23, readFileSync as readFileSync8, readdirSync as readdirSync12, realpathSync as
|
|
4558
|
+
import { existsSync as existsSync23, readFileSync as readFileSync8, readdirSync as readdirSync12, realpathSync as realpathSync3 } from "node:fs";
|
|
4338
4559
|
import { dirname as dirname7, extname, join as join30 } from "node:path";
|
|
4339
4560
|
init_config();
|
|
4340
4561
|
function typeFromPackageJson(pkgPath) {
|
|
@@ -4351,7 +4572,7 @@ function effectiveType(hookPath) {
|
|
|
4351
4572
|
if (ext === ".cjs") return "cjs";
|
|
4352
4573
|
let real;
|
|
4353
4574
|
try {
|
|
4354
|
-
real =
|
|
4575
|
+
real = realpathSync3(hookPath);
|
|
4355
4576
|
} catch {
|
|
4356
4577
|
return null;
|
|
4357
4578
|
}
|
|
@@ -4513,7 +4734,7 @@ import { existsSync as existsSync26, readFileSync as readFileSync9 } from "node:
|
|
|
4513
4734
|
import { join as join33 } from "node:path";
|
|
4514
4735
|
|
|
4515
4736
|
// src/commands.doctor.checks.hooks.preserve-symlinks.probe.ts
|
|
4516
|
-
import { closeSync as closeSync3, existsSync as existsSync25, openSync as openSync3, readSync, realpathSync as
|
|
4737
|
+
import { closeSync as closeSync3, existsSync as existsSync25, openSync as openSync3, readSync, realpathSync as realpathSync4 } from "node:fs";
|
|
4517
4738
|
import { dirname as dirname8, join as join32, resolve as resolve2 } from "node:path";
|
|
4518
4739
|
function suppressedRanges(src) {
|
|
4519
4740
|
const ranges = [];
|
|
@@ -4558,7 +4779,7 @@ function specifierIsMissing(specifier, baseDir) {
|
|
|
4558
4779
|
function relativeRequireTargetsBroken(scriptPath) {
|
|
4559
4780
|
let realPath;
|
|
4560
4781
|
try {
|
|
4561
|
-
realPath =
|
|
4782
|
+
realPath = realpathSync4(scriptPath);
|
|
4562
4783
|
} catch {
|
|
4563
4784
|
return false;
|
|
4564
4785
|
}
|
|
@@ -4702,7 +4923,7 @@ function reportPreserveSymlinksCheck(section2) {
|
|
|
4702
4923
|
return;
|
|
4703
4924
|
}
|
|
4704
4925
|
const map = readPathMapSafe();
|
|
4705
|
-
const sharedLinkNames = allSharedLinks(map);
|
|
4926
|
+
const sharedLinkNames = allSharedLinks(map, { quiet: true });
|
|
4706
4927
|
let anyWarn = false;
|
|
4707
4928
|
for (const [event, groups] of Object.entries(hooks)) {
|
|
4708
4929
|
if (!Array.isArray(groups)) continue;
|
|
@@ -5052,21 +5273,21 @@ import { createInterface as createInterface2 } from "node:readline/promises";
|
|
|
5052
5273
|
// src/commands.push.recovery.actions.ts
|
|
5053
5274
|
init_config();
|
|
5054
5275
|
import { readFileSync as readFileSync17 } from "node:fs";
|
|
5055
|
-
import { isAbsolute as isAbsolute2, resolve as resolve3, sep as
|
|
5276
|
+
import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep9 } from "node:path";
|
|
5056
5277
|
|
|
5057
5278
|
// src/commands.push.recovery.memory.ts
|
|
5058
5279
|
init_config();
|
|
5059
5280
|
init_config_sharedDirs_guard();
|
|
5060
5281
|
import {
|
|
5061
5282
|
cpSync as cpSync7,
|
|
5062
|
-
lstatSync as
|
|
5283
|
+
lstatSync as lstatSync12,
|
|
5063
5284
|
mkdirSync as mkdirSync9,
|
|
5064
5285
|
readFileSync as readFileSync14,
|
|
5065
|
-
realpathSync as
|
|
5286
|
+
realpathSync as realpathSync5,
|
|
5066
5287
|
statSync as statSync6,
|
|
5067
5288
|
writeFileSync as writeFileSync6
|
|
5068
5289
|
} from "node:fs";
|
|
5069
|
-
import { dirname as dirname9, join as join36, sep as
|
|
5290
|
+
import { dirname as dirname9, join as join36, sep as sep6 } from "node:path";
|
|
5070
5291
|
init_push_gitleaks_scan();
|
|
5071
5292
|
|
|
5072
5293
|
// src/rel-path-guard.ts
|
|
@@ -5097,12 +5318,12 @@ function resolveMemoryLocalPath(logical, relPath, map) {
|
|
|
5097
5318
|
if (abs === void 0) return null;
|
|
5098
5319
|
const memoryRoot = join36(claudeHome(), "projects", encodePath(abs), "memory");
|
|
5099
5320
|
const localPath = join36(memoryRoot, ...relPath.split("/"));
|
|
5100
|
-
if (localPath !== memoryRoot && !localPath.startsWith(memoryRoot +
|
|
5321
|
+
if (localPath !== memoryRoot && !localPath.startsWith(memoryRoot + sep6)) return null;
|
|
5101
5322
|
try {
|
|
5102
|
-
if (
|
|
5103
|
-
const realLocal =
|
|
5104
|
-
const realRoot =
|
|
5105
|
-
if (!realLocal.startsWith(realRoot +
|
|
5323
|
+
if (lstatSync12(localPath).isSymbolicLink()) return null;
|
|
5324
|
+
const realLocal = realpathSync5(localPath);
|
|
5325
|
+
const realRoot = realpathSync5(memoryRoot);
|
|
5326
|
+
if (!realLocal.startsWith(realRoot + sep6)) return null;
|
|
5106
5327
|
if (!statSync6(realLocal).isFile()) return null;
|
|
5107
5328
|
} catch {
|
|
5108
5329
|
return null;
|
|
@@ -5160,28 +5381,28 @@ function applyMemoryRedact(f, ts, map, scan = scanFile) {
|
|
|
5160
5381
|
init_config();
|
|
5161
5382
|
import {
|
|
5162
5383
|
cpSync as cpSync8,
|
|
5163
|
-
lstatSync as
|
|
5384
|
+
lstatSync as lstatSync14,
|
|
5164
5385
|
mkdirSync as mkdirSync11,
|
|
5165
5386
|
readFileSync as readFileSync15,
|
|
5166
|
-
realpathSync as
|
|
5387
|
+
realpathSync as realpathSync6,
|
|
5167
5388
|
statSync as statSync7,
|
|
5168
5389
|
writeFileSync as writeFileSync7
|
|
5169
5390
|
} from "node:fs";
|
|
5170
|
-
import { dirname as dirname10, join as join38, sep as
|
|
5391
|
+
import { dirname as dirname10, join as join38, sep as sep7 } from "node:path";
|
|
5171
5392
|
|
|
5172
5393
|
// src/skills-sync.ts
|
|
5173
5394
|
init_config();
|
|
5174
|
-
import { existsSync as existsSync29, lstatSync as
|
|
5395
|
+
import { existsSync as existsSync29, lstatSync as lstatSync13, mkdirSync as mkdirSync10, readdirSync as readdirSync13, rmSync as rmSync13 } from "node:fs";
|
|
5175
5396
|
import { join as join37 } from "node:path";
|
|
5176
5397
|
|
|
5177
5398
|
// src/skills-sync.tracked.ts
|
|
5178
5399
|
init_utils();
|
|
5179
|
-
import { basename as
|
|
5400
|
+
import { basename as basename3 } from "node:path";
|
|
5180
5401
|
function trackedRootSkillsAt(ref, repo) {
|
|
5181
5402
|
try {
|
|
5182
5403
|
const raw = gitCaptureRaw(["ls-tree", "--name-only", "-z", ref, "--", "shared/skills/"], repo);
|
|
5183
5404
|
const entries = raw.split("\0").filter((entry) => entry !== "");
|
|
5184
|
-
return new Set(entries.map((entry) =>
|
|
5405
|
+
return new Set(entries.map((entry) => basename3(entry)));
|
|
5185
5406
|
} catch {
|
|
5186
5407
|
return /* @__PURE__ */ new Set();
|
|
5187
5408
|
}
|
|
@@ -5211,7 +5432,7 @@ function syncSkillsPull(ts, prePostHeads) {
|
|
|
5211
5432
|
if (!existsSync29(sharedSkills)) return;
|
|
5212
5433
|
const localSkills = join37(claudeHome(), "skills");
|
|
5213
5434
|
backupBeforeWrite(localSkills, ts);
|
|
5214
|
-
const dstStat =
|
|
5435
|
+
const dstStat = lstatSync13(localSkills, { throwIfNoEntry: false });
|
|
5215
5436
|
if (dstStat?.isSymbolicLink() === true) {
|
|
5216
5437
|
rmSync13(localSkills, { recursive: true, force: true });
|
|
5217
5438
|
mkdirSync10(localSkills, { recursive: true });
|
|
@@ -5221,7 +5442,7 @@ function syncSkillsPull(ts, prePostHeads) {
|
|
|
5221
5442
|
}
|
|
5222
5443
|
function syncSkillsPush() {
|
|
5223
5444
|
const localSkills = join37(claudeHome(), "skills");
|
|
5224
|
-
const stat =
|
|
5445
|
+
const stat = lstatSync13(localSkills, { throwIfNoEntry: false });
|
|
5225
5446
|
if (stat === void 0) return;
|
|
5226
5447
|
if (stat.isSymbolicLink()) return;
|
|
5227
5448
|
const sharedSkills = join37(repoHome(), "shared", "skills");
|
|
@@ -5250,12 +5471,12 @@ function resolveSkillLocalPath(name, relPath) {
|
|
|
5250
5471
|
const skillsRoot = join38(claudeHome(), "skills");
|
|
5251
5472
|
const skillRoot = join38(skillsRoot, name);
|
|
5252
5473
|
const localPath = join38(skillRoot, ...relPath.split("/"));
|
|
5253
|
-
if (localPath !== skillRoot && !localPath.startsWith(skillRoot +
|
|
5474
|
+
if (localPath !== skillRoot && !localPath.startsWith(skillRoot + sep7)) return null;
|
|
5254
5475
|
try {
|
|
5255
|
-
if (
|
|
5256
|
-
const realLocal =
|
|
5257
|
-
const realRoot =
|
|
5258
|
-
if (!realLocal.startsWith(realRoot +
|
|
5476
|
+
if (lstatSync14(localPath).isSymbolicLink()) return null;
|
|
5477
|
+
const realLocal = realpathSync6(localPath);
|
|
5478
|
+
const realRoot = realpathSync6(skillsRoot);
|
|
5479
|
+
if (!realLocal.startsWith(realRoot + sep7)) return null;
|
|
5259
5480
|
if (!statSync7(realLocal).isFile()) return null;
|
|
5260
5481
|
} catch {
|
|
5261
5482
|
return null;
|
|
@@ -5380,7 +5601,7 @@ function applyDeferredAllows(findings, state, repo) {
|
|
|
5380
5601
|
init_config();
|
|
5381
5602
|
init_config_sharedDirs_guard();
|
|
5382
5603
|
import { cpSync as cpSync9, existsSync as existsSync32, mkdirSync as mkdirSync12 } from "node:fs";
|
|
5383
|
-
import { dirname as dirname12, join as join41, sep as
|
|
5604
|
+
import { dirname as dirname12, join as join41, sep as sep8 } from "node:path";
|
|
5384
5605
|
|
|
5385
5606
|
// src/commands.redact.ts
|
|
5386
5607
|
init_config();
|
|
@@ -5388,17 +5609,17 @@ import { existsSync as existsSync31 } from "node:fs";
|
|
|
5388
5609
|
import { dirname as dirname11, join as join40 } from "node:path";
|
|
5389
5610
|
|
|
5390
5611
|
// src/commands.redact.subtree.ts
|
|
5391
|
-
import { existsSync as existsSync30, lstatSync as
|
|
5612
|
+
import { existsSync as existsSync30, lstatSync as lstatSync15, readFileSync as readFileSync16, readdirSync as readdirSync14, statSync as statSync8, writeFileSync as writeFileSync8 } from "node:fs";
|
|
5392
5613
|
import { join as join39 } from "node:path";
|
|
5393
5614
|
init_utils_fs();
|
|
5394
5615
|
init_utils();
|
|
5395
5616
|
function collectFiles(dir, out) {
|
|
5396
5617
|
if (!existsSync30(dir)) return;
|
|
5397
|
-
const st =
|
|
5618
|
+
const st = lstatSync15(dir);
|
|
5398
5619
|
if (!st.isDirectory()) return;
|
|
5399
5620
|
for (const entry of readdirSync14(dir)) {
|
|
5400
5621
|
const abs = join39(dir, entry);
|
|
5401
|
-
const lst =
|
|
5622
|
+
const lst = lstatSync15(abs);
|
|
5402
5623
|
if (lst.isSymbolicLink()) continue;
|
|
5403
5624
|
if (lst.isDirectory()) {
|
|
5404
5625
|
collectFiles(abs, out);
|
|
@@ -5606,7 +5827,7 @@ function resolveStagedDir(localPath, map, claude, repo) {
|
|
|
5606
5827
|
assertSafeLogical(logical);
|
|
5607
5828
|
const abs = hostMap[HOST];
|
|
5608
5829
|
if (abs === void 0) continue;
|
|
5609
|
-
if (localPath.startsWith(join41(claude, "projects", encodePath(abs)) +
|
|
5830
|
+
if (localPath.startsWith(join41(claude, "projects", encodePath(abs)) + sep8)) {
|
|
5610
5831
|
return join41(repo, "shared", "projects", logical);
|
|
5611
5832
|
}
|
|
5612
5833
|
}
|
|
@@ -5897,8 +6118,8 @@ function sessionSuffix(finding) {
|
|
|
5897
6118
|
const sid = sessionIdFromFinding(finding);
|
|
5898
6119
|
if (sid === null) return "";
|
|
5899
6120
|
const rawBasename = finding.File.replace(/^.*\//, "");
|
|
5900
|
-
const
|
|
5901
|
-
return
|
|
6121
|
+
const basename4 = rawBasename.endsWith(".jsonl") ? rawBasename.slice(0, -".jsonl".length) : rawBasename;
|
|
6122
|
+
return basename4 === sid ? "" : ` (session: ${sid})`;
|
|
5902
6123
|
}
|
|
5903
6124
|
function renderFindingBlock(finding, readLine, siblings = []) {
|
|
5904
6125
|
const lines = [` file: ${finding.File}:${finding.StartLine}${sessionSuffix(finding)}`];
|
|
@@ -5970,7 +6191,7 @@ function makeDefaultReadLine(repo) {
|
|
|
5970
6191
|
try {
|
|
5971
6192
|
const repoRoot = resolve3(repo);
|
|
5972
6193
|
const target = resolve3(repoRoot, file);
|
|
5973
|
-
if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot +
|
|
6194
|
+
if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot + sep9)) {
|
|
5974
6195
|
return null;
|
|
5975
6196
|
}
|
|
5976
6197
|
let lines = cache.get(target);
|
|
@@ -6567,6 +6788,20 @@ function buildVerdictSection(sections) {
|
|
|
6567
6788
|
return summary;
|
|
6568
6789
|
}
|
|
6569
6790
|
|
|
6791
|
+
// src/commands.doctor.banner.ts
|
|
6792
|
+
var NOMAD_BANNER = [
|
|
6793
|
+
" __",
|
|
6794
|
+
" ____ ____ ____ ___ ____ _ ____/ /",
|
|
6795
|
+
" / __ \\ / __ \\ / __ `__ \\ / __ `// __ /",
|
|
6796
|
+
" / / / // /_/ // / / / / // /_/ // /_/ /",
|
|
6797
|
+
String.raw`/_/ /_/ \____//_/ /_/ /_/ \__,_/ \__,_/`
|
|
6798
|
+
];
|
|
6799
|
+
function printBanner(log2, stdout) {
|
|
6800
|
+
if (stdout.isTTY !== true) return;
|
|
6801
|
+
for (const line of NOMAD_BANNER) log2(line);
|
|
6802
|
+
log2("");
|
|
6803
|
+
}
|
|
6804
|
+
|
|
6570
6805
|
// src/commands.doctor.compact.ts
|
|
6571
6806
|
init_color();
|
|
6572
6807
|
var ALWAYS_FULL = /* @__PURE__ */ new Set([
|
|
@@ -6585,18 +6820,27 @@ function isRepoStateLine(item2) {
|
|
|
6585
6820
|
function isCopySyncModalityLine(item2) {
|
|
6586
6821
|
return item2.includes("sync modality: copy-sync");
|
|
6587
6822
|
}
|
|
6823
|
+
function isKeptEnvironmentRow(item2) {
|
|
6824
|
+
return isRepoStateLine(item2) || isCopySyncModalityLine(item2) || isProblem(item2);
|
|
6825
|
+
}
|
|
6826
|
+
function keepWithChildren(items, keep) {
|
|
6827
|
+
const out = [];
|
|
6828
|
+
let parentKept = false;
|
|
6829
|
+
for (const item2 of items) {
|
|
6830
|
+
if (isChild(item2)) {
|
|
6831
|
+
if (parentKept) out.push(item2);
|
|
6832
|
+
continue;
|
|
6833
|
+
}
|
|
6834
|
+
parentKept = keep(item2);
|
|
6835
|
+
if (parentKept) out.push(item2);
|
|
6836
|
+
}
|
|
6837
|
+
return out;
|
|
6838
|
+
}
|
|
6588
6839
|
function compactSections(sections) {
|
|
6589
6840
|
return sections.map((s) => {
|
|
6590
6841
|
if (ALWAYS_FULL.has(s.header)) return s;
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
...s,
|
|
6594
|
-
items: s.items.filter(
|
|
6595
|
-
(it) => isRepoStateLine(it) || isCopySyncModalityLine(it) || isProblem(it)
|
|
6596
|
-
)
|
|
6597
|
-
};
|
|
6598
|
-
}
|
|
6599
|
-
return { ...s, items: s.items.filter(isProblem) };
|
|
6842
|
+
const keep = s.header === "Environment" ? isKeptEnvironmentRow : isProblem;
|
|
6843
|
+
return { ...s, items: keepWithChildren(s.items, keep) };
|
|
6600
6844
|
});
|
|
6601
6845
|
}
|
|
6602
6846
|
|
|
@@ -6673,6 +6917,7 @@ function gatherDoctorSections(opts) {
|
|
|
6673
6917
|
return [...body, buildVerdictSection(body)];
|
|
6674
6918
|
}
|
|
6675
6919
|
function cmdDoctor(opts = {}) {
|
|
6920
|
+
printBanner(console.log, process.stdout);
|
|
6676
6921
|
const makeSpinner = opts.startSpinner ?? startSpinner;
|
|
6677
6922
|
const sp = makeSpinner("Running checks");
|
|
6678
6923
|
let report;
|
|
@@ -6980,11 +7225,11 @@ init_utils_json();
|
|
|
6980
7225
|
|
|
6981
7226
|
// src/extras-sync.remap.ts
|
|
6982
7227
|
init_config();
|
|
6983
|
-
import { existsSync as existsSync38, mkdirSync as mkdirSync13, readdirSync as readdirSync16, readFileSync as readFileSync19, realpathSync as
|
|
6984
|
-
import { dirname as dirname13, join as join49, sep as
|
|
7228
|
+
import { existsSync as existsSync38, mkdirSync as mkdirSync13, readdirSync as readdirSync16, readFileSync as readFileSync19, realpathSync as realpathSync7, rmSync as rmSync16 } from "node:fs";
|
|
7229
|
+
import { dirname as dirname13, join as join49, sep as sep11 } from "node:path";
|
|
6985
7230
|
|
|
6986
7231
|
// src/extras-sync.planning-diff.ts
|
|
6987
|
-
import { join as join48, normalize as normalize2, sep as
|
|
7232
|
+
import { join as join48, normalize as normalize2, sep as sep10 } from "node:path";
|
|
6988
7233
|
init_utils();
|
|
6989
7234
|
function processRecord(fields, i, changed, deleted) {
|
|
6990
7235
|
const status = fields[i];
|
|
@@ -7037,7 +7282,7 @@ function planningDeleteTargets(opts) {
|
|
|
7037
7282
|
const logicalPrefix = "shared/extras/" + logical + "/";
|
|
7038
7283
|
const prefix = logicalPrefix + ".planning/";
|
|
7039
7284
|
const planningRoot = join48(localRoot, ".planning");
|
|
7040
|
-
const planningRootBoundary = planningRoot +
|
|
7285
|
+
const planningRootBoundary = planningRoot + sep10;
|
|
7041
7286
|
const targets = [];
|
|
7042
7287
|
for (const repoPath of deleted) {
|
|
7043
7288
|
if (!repoPath.startsWith(prefix)) {
|
|
@@ -7079,7 +7324,7 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
|
|
|
7079
7324
|
}
|
|
7080
7325
|
function pruneEmptyAncestors(target, planningRoot) {
|
|
7081
7326
|
let dir = dirname13(target);
|
|
7082
|
-
while (dir !== planningRoot && dir.startsWith(planningRoot +
|
|
7327
|
+
while (dir !== planningRoot && dir.startsWith(planningRoot + sep11)) {
|
|
7083
7328
|
try {
|
|
7084
7329
|
if (readdirSync16(dir).length > 0) break;
|
|
7085
7330
|
rmSync16(dir, { recursive: true, force: true });
|
|
@@ -7091,13 +7336,13 @@ function pruneEmptyAncestors(target, planningRoot) {
|
|
|
7091
7336
|
}
|
|
7092
7337
|
function tryRealpath(dir) {
|
|
7093
7338
|
try {
|
|
7094
|
-
return
|
|
7339
|
+
return realpathSync7(dir);
|
|
7095
7340
|
} catch {
|
|
7096
7341
|
return void 0;
|
|
7097
7342
|
}
|
|
7098
7343
|
}
|
|
7099
7344
|
function isInsidePlanningRoot(parentReal, rootReal) {
|
|
7100
|
-
return parentReal === rootReal || parentReal.startsWith(rootReal +
|
|
7345
|
+
return parentReal === rootReal || parentReal.startsWith(rootReal + sep11);
|
|
7101
7346
|
}
|
|
7102
7347
|
function deletePlanningTarget(target, planningRoot, repoCounterpart) {
|
|
7103
7348
|
if (existsSync38(repoCounterpart)) return;
|
|
@@ -7124,11 +7369,11 @@ function planningDiffArgs(pre, post, logical) {
|
|
|
7124
7369
|
function deletePairsFor(t, raw) {
|
|
7125
7370
|
return planningDeleteTargets({ raw, logical: t.logical, localRoot: t.localRoot }).map(
|
|
7126
7371
|
(target) => {
|
|
7127
|
-
const relToLocal = target.slice(t.localRoot.length +
|
|
7372
|
+
const relToLocal = target.slice(t.localRoot.length + sep11.length);
|
|
7128
7373
|
return {
|
|
7129
7374
|
target,
|
|
7130
7375
|
relToLocal,
|
|
7131
|
-
repoRel: `shared/extras/${t.logical}/${relToLocal.split(
|
|
7376
|
+
repoRel: `shared/extras/${t.logical}/${relToLocal.split(sep11).join("/")}`
|
|
7132
7377
|
};
|
|
7133
7378
|
}
|
|
7134
7379
|
);
|
|
@@ -7202,7 +7447,8 @@ function remapExtrasPush(ts, opts = {}) {
|
|
|
7202
7447
|
// The filter prevents ALWAYS_NEVER_SYNC files from landing in the repo
|
|
7203
7448
|
// working tree before the allow-list gate fires, eliminating the
|
|
7204
7449
|
// "residue wedges repeat push" regression. The allow-list gate
|
|
7205
|
-
// (enforceAllowList
|
|
7450
|
+
// (enforceAllowList in commands.push.allowlist.ts, blockSetFor in
|
|
7451
|
+
// config.never-sync.ts)
|
|
7206
7452
|
// remains the hard security boundary.
|
|
7207
7453
|
// All others: copyExtrasFiltered with per-extra denylist.
|
|
7208
7454
|
(src, dst, dirname14) => dirname14 === ".planning" ? copyExtrasOverlayFiltered(src, dst, extrasDenySet(dirname14)) : copyExtrasFiltered(src, dst, extrasDenySet(dirname14))
|
|
@@ -7302,8 +7548,8 @@ function divergenceCheckExtras(ts, prePostHeads) {
|
|
|
7302
7548
|
|
|
7303
7549
|
// src/links.baseline.ts
|
|
7304
7550
|
init_config();
|
|
7305
|
-
import { lstatSync as
|
|
7306
|
-
import { join as join51, relative as relative7, sep as
|
|
7551
|
+
import { lstatSync as lstatSync16, readdirSync as readdirSync17 } from "node:fs";
|
|
7552
|
+
import { join as join51, relative as relative7, sep as sep12 } from "node:path";
|
|
7307
7553
|
init_utils();
|
|
7308
7554
|
var SHARED_BASELINE_KIND = "shared-links-baseline/1";
|
|
7309
7555
|
var SHARED_BASELINE_CONFIG_HASH = "not-applicable";
|
|
@@ -7314,13 +7560,13 @@ function readSharedBaseline() {
|
|
|
7314
7560
|
return parsed;
|
|
7315
7561
|
}
|
|
7316
7562
|
function baselineKey(claude, abs) {
|
|
7317
|
-
return relative7(claude, abs).split(
|
|
7563
|
+
return relative7(claude, abs).split(sep12).join("/");
|
|
7318
7564
|
}
|
|
7319
7565
|
function addLocalPath(abs, claude, scan) {
|
|
7320
7566
|
const key = baselineKey(claude, abs);
|
|
7321
7567
|
let st;
|
|
7322
7568
|
try {
|
|
7323
|
-
st =
|
|
7569
|
+
st = lstatSync16(abs, { throwIfNoEntry: false });
|
|
7324
7570
|
} catch {
|
|
7325
7571
|
scan.declined.push(key);
|
|
7326
7572
|
return;
|
|
@@ -7353,10 +7599,10 @@ function collectSharedFiles(dir, claude, scan) {
|
|
|
7353
7599
|
addLocalPath(abs, claude, scan);
|
|
7354
7600
|
}
|
|
7355
7601
|
}
|
|
7356
|
-
function enumerateLocalSharedScan(map) {
|
|
7602
|
+
function enumerateLocalSharedScan(map, opts = {}) {
|
|
7357
7603
|
const claude = claudeHome();
|
|
7358
7604
|
const scan = { files: {}, declined: [] };
|
|
7359
|
-
for (const name of allSharedLinks(map)) {
|
|
7605
|
+
for (const name of opts.linkNames ?? allSharedLinks(map, { quiet: opts.quiet === true })) {
|
|
7360
7606
|
if (isDeniedName(ALWAYS_NEVER_SYNC, name)) {
|
|
7361
7607
|
scan.declined.push(name);
|
|
7362
7608
|
continue;
|
|
@@ -7365,13 +7611,13 @@ function enumerateLocalSharedScan(map) {
|
|
|
7365
7611
|
}
|
|
7366
7612
|
return scan;
|
|
7367
7613
|
}
|
|
7368
|
-
function enumerateLocalSharedFiles(map) {
|
|
7369
|
-
return enumerateLocalSharedScan(map).files;
|
|
7614
|
+
function enumerateLocalSharedFiles(map, opts = {}) {
|
|
7615
|
+
return enumerateLocalSharedScan(map, opts).files;
|
|
7370
7616
|
}
|
|
7371
|
-
function buildSharedBaseline(map) {
|
|
7617
|
+
function buildSharedBaseline(map, opts = {}) {
|
|
7372
7618
|
const claude = claudeHome();
|
|
7373
7619
|
const files = {};
|
|
7374
|
-
for (const [key, st] of Object.entries(enumerateLocalSharedFiles(map))) {
|
|
7620
|
+
for (const [key, st] of Object.entries(enumerateLocalSharedFiles(map, opts))) {
|
|
7375
7621
|
try {
|
|
7376
7622
|
files[key] = { size: st.size, mtime: st.mtime, hash: hashFile(join51(claude, key)) };
|
|
7377
7623
|
} catch {
|
|
@@ -7379,27 +7625,69 @@ function buildSharedBaseline(map) {
|
|
|
7379
7625
|
}
|
|
7380
7626
|
return buildManifest(files, SHARED_BASELINE_KIND, SHARED_BASELINE_CONFIG_HASH);
|
|
7381
7627
|
}
|
|
7382
|
-
function writeSharedBaseline(map) {
|
|
7628
|
+
function writeSharedBaseline(map, opts = {}) {
|
|
7383
7629
|
if (process.platform !== "win32") return;
|
|
7384
7630
|
try {
|
|
7385
|
-
writeManifest(sharedBaselinePath(), buildSharedBaseline(map));
|
|
7631
|
+
writeManifest(sharedBaselinePath(), buildSharedBaseline(map, opts));
|
|
7386
7632
|
} catch (err) {
|
|
7387
7633
|
warn(`could not record the shared-config baseline: ${err.message}`);
|
|
7388
7634
|
}
|
|
7389
7635
|
}
|
|
7390
7636
|
|
|
7391
7637
|
// src/commands.pull.win32.ts
|
|
7392
|
-
|
|
7638
|
+
init_color();
|
|
7393
7639
|
import { existsSync as existsSync44 } from "node:fs";
|
|
7394
7640
|
import { join as join56 } from "node:path";
|
|
7395
7641
|
|
|
7396
|
-
// src/git
|
|
7642
|
+
// src/commands.pull.recovery.git.ts
|
|
7643
|
+
init_utils();
|
|
7397
7644
|
import { execFileSync as execFileSync21 } from "node:child_process";
|
|
7645
|
+
function gitCapture(args, cwd) {
|
|
7646
|
+
return execFileSync21("git", args, {
|
|
7647
|
+
cwd,
|
|
7648
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7649
|
+
maxBuffer: 64 * 1024 * 1024
|
|
7650
|
+
}).toString().trim();
|
|
7651
|
+
}
|
|
7652
|
+
function parsePorcelainZ(raw) {
|
|
7653
|
+
const tracked = [];
|
|
7654
|
+
const untracked = [];
|
|
7655
|
+
if (!raw) return { tracked, untracked };
|
|
7656
|
+
const records = raw.split("\0");
|
|
7657
|
+
for (let i = 0; i < records.length; i++) {
|
|
7658
|
+
const record = records[i];
|
|
7659
|
+
if (record.length < 3) continue;
|
|
7660
|
+
const xy = record.slice(0, 2);
|
|
7661
|
+
const filePath = record.slice(3);
|
|
7662
|
+
if (xy === "??") {
|
|
7663
|
+
untracked.push(filePath);
|
|
7664
|
+
continue;
|
|
7665
|
+
}
|
|
7666
|
+
tracked.push(filePath);
|
|
7667
|
+
if (xy.startsWith("R") || xy.startsWith("C")) {
|
|
7668
|
+
const src = records[i + 1];
|
|
7669
|
+
if (src) {
|
|
7670
|
+
tracked.push(src);
|
|
7671
|
+
i++;
|
|
7672
|
+
}
|
|
7673
|
+
}
|
|
7674
|
+
}
|
|
7675
|
+
return { tracked, untracked };
|
|
7676
|
+
}
|
|
7677
|
+
function parseDirtyPaths(repo, opts = {}) {
|
|
7678
|
+
return parsePorcelainZ(gitStatusPorcelainZ(repo, opts));
|
|
7679
|
+
}
|
|
7680
|
+
|
|
7681
|
+
// src/commands.pull.win32.ts
|
|
7682
|
+
init_config();
|
|
7683
|
+
|
|
7684
|
+
// src/git-probe.ts
|
|
7685
|
+
import { execFileSync as execFileSync22 } from "node:child_process";
|
|
7398
7686
|
var PROBE_TIMEOUT_MS4 = 1e4;
|
|
7399
7687
|
var PROBE_MAX_BUFFER = 64 * 1024 * 1024;
|
|
7400
7688
|
function gitProbe(args, repo) {
|
|
7401
7689
|
try {
|
|
7402
|
-
return
|
|
7690
|
+
return execFileSync22("git", args, {
|
|
7403
7691
|
cwd: repo,
|
|
7404
7692
|
stdio: ["ignore", "pipe", "pipe"],
|
|
7405
7693
|
timeout: PROBE_TIMEOUT_MS4,
|
|
@@ -7410,37 +7698,10 @@ function gitProbe(args, repo) {
|
|
|
7410
7698
|
}
|
|
7411
7699
|
}
|
|
7412
7700
|
|
|
7413
|
-
// src/links.captures.ts
|
|
7414
|
-
init_config();
|
|
7415
|
-
import { existsSync as existsSync40, lstatSync as lstatSync16 } from "node:fs";
|
|
7416
|
-
import { join as join52 } from "node:path";
|
|
7417
|
-
function planSharedLinkCaptures(map) {
|
|
7418
|
-
if (process.platform !== "win32") return [];
|
|
7419
|
-
if (map === null) return [];
|
|
7420
|
-
const claude = claudeHome();
|
|
7421
|
-
const repo = repoHome();
|
|
7422
|
-
const plan = [];
|
|
7423
|
-
for (const name of allSharedLinks(map)) {
|
|
7424
|
-
const localPath = join52(claude, name);
|
|
7425
|
-
let stat;
|
|
7426
|
-
try {
|
|
7427
|
-
stat = lstatSync16(localPath, { throwIfNoEntry: false });
|
|
7428
|
-
} catch {
|
|
7429
|
-
continue;
|
|
7430
|
-
}
|
|
7431
|
-
if (stat === void 0) continue;
|
|
7432
|
-
if (stat.isSymbolicLink()) continue;
|
|
7433
|
-
const repoPath = join52(repo, "shared", name);
|
|
7434
|
-
if (!existsSync40(repoPath)) continue;
|
|
7435
|
-
plan.push({ name, localPath, repoPath });
|
|
7436
|
-
}
|
|
7437
|
-
return plan;
|
|
7438
|
-
}
|
|
7439
|
-
|
|
7440
7701
|
// src/links.deletions.ts
|
|
7441
7702
|
init_config();
|
|
7442
|
-
import { existsSync as
|
|
7443
|
-
import { join as
|
|
7703
|
+
import { existsSync as existsSync40, lstatSync as lstatSync17, rmSync as rmSync17 } from "node:fs";
|
|
7704
|
+
import { join as join52, relative as relative8, resolve as resolve4, sep as sep13 } from "node:path";
|
|
7444
7705
|
init_utils();
|
|
7445
7706
|
init_utils_fs();
|
|
7446
7707
|
function isUnknown(declined, key) {
|
|
@@ -7450,27 +7711,28 @@ function deletionFor(key, names, claude, sharedRoot) {
|
|
|
7450
7711
|
const segments = key.split("/");
|
|
7451
7712
|
const name = segments[0];
|
|
7452
7713
|
if (!names.has(name)) return null;
|
|
7453
|
-
if (!
|
|
7714
|
+
if (!existsSync40(join52(claude, name))) return null;
|
|
7454
7715
|
if (segments.some((segment) => isDeniedName(ALWAYS_NEVER_SYNC, segment))) return null;
|
|
7455
7716
|
const repoPath = resolve4(sharedRoot, key);
|
|
7456
|
-
const rel = relative8(
|
|
7457
|
-
if (rel === "" || rel === ".." || rel.startsWith(`..${
|
|
7717
|
+
const rel = relative8(join52(sharedRoot, name), repoPath);
|
|
7718
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep13}`)) return null;
|
|
7458
7719
|
if (segments.some((segment) => segment === "" || segment === "." || segment === ".."))
|
|
7459
7720
|
return null;
|
|
7460
|
-
if (!
|
|
7461
|
-
return { name, localPath:
|
|
7721
|
+
if (!existsSync40(repoPath)) return null;
|
|
7722
|
+
return { kind: "deletion", name, localPath: join52(claude, key), repoPath };
|
|
7462
7723
|
}
|
|
7463
|
-
function planSharedLinkDeletions(map) {
|
|
7724
|
+
function planSharedLinkDeletions(map, opts = {}) {
|
|
7464
7725
|
if (process.platform !== "win32") return [];
|
|
7465
7726
|
if (map === null) return [];
|
|
7466
7727
|
const baseline = readSharedBaseline();
|
|
7467
7728
|
if (baseline === null) return [];
|
|
7468
7729
|
const claude = claudeHome();
|
|
7469
|
-
const sharedRoot =
|
|
7470
|
-
const
|
|
7730
|
+
const sharedRoot = join52(repoHome(), "shared");
|
|
7731
|
+
const linkNames = opts.linkNames ?? allSharedLinks(map);
|
|
7732
|
+
const scan = enumerateLocalSharedScan(map, { linkNames });
|
|
7471
7733
|
const present = new Set(Object.keys(scan.files).map((key) => key.toLowerCase()));
|
|
7472
7734
|
const declined = scan.declined.map((path) => path.toLowerCase());
|
|
7473
|
-
const names = new Set(
|
|
7735
|
+
const names = new Set(linkNames);
|
|
7474
7736
|
const plan = [];
|
|
7475
7737
|
for (const key of Object.keys(baseline.files)) {
|
|
7476
7738
|
const folded = key.toLowerCase();
|
|
@@ -7481,17 +7743,151 @@ function planSharedLinkDeletions(map) {
|
|
|
7481
7743
|
}
|
|
7482
7744
|
return plan;
|
|
7483
7745
|
}
|
|
7484
|
-
function applySharedLinkDeletions(map, ts) {
|
|
7746
|
+
function applySharedLinkDeletions(map, ts, opts = {}) {
|
|
7485
7747
|
const repo = repoHome();
|
|
7486
|
-
|
|
7748
|
+
const removed = [];
|
|
7749
|
+
for (const entry of planSharedLinkDeletions(map, opts)) {
|
|
7487
7750
|
try {
|
|
7488
7751
|
if (!lstatSync17(entry.repoPath).isFile()) continue;
|
|
7489
7752
|
backupRepoWrite(entry.repoPath, ts, repo);
|
|
7490
7753
|
rmSync17(entry.repoPath, { force: true });
|
|
7754
|
+
removed.push(entry);
|
|
7491
7755
|
} catch (err) {
|
|
7492
7756
|
warn(`could not remove ${entry.repoPath}: ${err.message}`);
|
|
7493
7757
|
}
|
|
7494
7758
|
}
|
|
7759
|
+
return removed;
|
|
7760
|
+
}
|
|
7761
|
+
|
|
7762
|
+
// src/links.mirror.ts
|
|
7763
|
+
init_config();
|
|
7764
|
+
import { existsSync as existsSync41, lstatSync as lstatSync18, rmSync as rmSync18 } from "node:fs";
|
|
7765
|
+
import { join as join53 } from "node:path";
|
|
7766
|
+
init_utils();
|
|
7767
|
+
init_utils_fs();
|
|
7768
|
+
function emitMirror(onPreview, name, localPath, repoPath) {
|
|
7769
|
+
if (onPreview) {
|
|
7770
|
+
onPreview({ kind: "mirror", name, localPath, repoPath });
|
|
7771
|
+
} else {
|
|
7772
|
+
log(`would capture: ${localPath} -> ${repoPath}`);
|
|
7773
|
+
}
|
|
7774
|
+
}
|
|
7775
|
+
function emitMirrorWet(onPreview, name, localPath, repoPath) {
|
|
7776
|
+
if (onPreview) onPreview({ kind: "mirror", name, localPath, repoPath });
|
|
7777
|
+
}
|
|
7778
|
+
function notShared(policy, target) {
|
|
7779
|
+
return !policy.adoptNew && !existsSync41(target);
|
|
7780
|
+
}
|
|
7781
|
+
function mirrorOneSharedName(name, claude, repo, policy, opts) {
|
|
7782
|
+
const localPath = join53(claude, name);
|
|
7783
|
+
const target = join53(repo, "shared", name);
|
|
7784
|
+
let stat;
|
|
7785
|
+
try {
|
|
7786
|
+
stat = lstatSync18(localPath, { throwIfNoEntry: false });
|
|
7787
|
+
} catch (err) {
|
|
7788
|
+
if (notShared(policy, target)) return;
|
|
7789
|
+
warn(
|
|
7790
|
+
`${name} could not be read (${err.message}), so it was left out of shared/ this run. Check its permissions, or whether another program has it open`
|
|
7791
|
+
);
|
|
7792
|
+
return;
|
|
7793
|
+
}
|
|
7794
|
+
if (stat === void 0) return;
|
|
7795
|
+
if (stat.isSymbolicLink()) return;
|
|
7796
|
+
if (notShared(policy, target)) return;
|
|
7797
|
+
if (opts.dryRun === true) {
|
|
7798
|
+
emitMirror(opts.onPreview, name, localPath, target);
|
|
7799
|
+
return;
|
|
7800
|
+
}
|
|
7801
|
+
if (policy.backupTs !== void 0) backupRepoWrite(target, policy.backupTs, repo);
|
|
7802
|
+
if (policy.overlay && stat.isDirectory()) {
|
|
7803
|
+
copyExtrasOverlayFiltered(localPath, target, NEVER_SYNC);
|
|
7804
|
+
} else {
|
|
7805
|
+
copyExtrasFiltered(localPath, target, NEVER_SYNC);
|
|
7806
|
+
}
|
|
7807
|
+
emitMirrorWet(opts.onPreview, name, localPath, target);
|
|
7808
|
+
}
|
|
7809
|
+
function mirrorSharedNames(map, policy, opts = {}) {
|
|
7810
|
+
if (process.platform !== "win32") return;
|
|
7811
|
+
if (map === null) return;
|
|
7812
|
+
const claude = claudeHome();
|
|
7813
|
+
const repo = repoHome();
|
|
7814
|
+
const linkNames = opts.linkNames ?? allSharedLinks(map);
|
|
7815
|
+
for (const name of linkNames) {
|
|
7816
|
+
mirrorOneSharedName(name, claude, repo, policy, opts);
|
|
7817
|
+
}
|
|
7818
|
+
}
|
|
7819
|
+
function syncSharedLinksPush(map, opts = {}) {
|
|
7820
|
+
mirrorSharedNames(map, { adoptNew: true, overlay: false }, opts);
|
|
7821
|
+
}
|
|
7822
|
+
function stageLocalSharedEdits(map, ts, opts = {}) {
|
|
7823
|
+
mirrorSharedNames(map, { adoptNew: false, overlay: true, backupTs: ts }, opts);
|
|
7824
|
+
}
|
|
7825
|
+
function presentAt(abs) {
|
|
7826
|
+
try {
|
|
7827
|
+
return lstatSync18(abs, { throwIfNoEntry: false }) !== void 0;
|
|
7828
|
+
} catch {
|
|
7829
|
+
return true;
|
|
7830
|
+
}
|
|
7831
|
+
}
|
|
7832
|
+
function removeUntrackedDenied(repo, path, segment, ts) {
|
|
7833
|
+
const abs = join53(repo, path);
|
|
7834
|
+
const denied = `the path segment "${segment}" is on the never-sync list`;
|
|
7835
|
+
if (!presentAt(abs)) {
|
|
7836
|
+
warn(
|
|
7837
|
+
`nothing was removed for ${path}: ${denied}, but nothing is at that path now. Either it went away after git listed it, in which case there is nothing left to do, or its name did not survive the decode of git's output and the real file is still in the sync repo under a name nomad cannot address. Look for it in ${repo} with "git status --untracked-files=all -- shared/"`
|
|
7838
|
+
);
|
|
7839
|
+
return;
|
|
7840
|
+
}
|
|
7841
|
+
const snapshotted = existsSync41(abs);
|
|
7842
|
+
try {
|
|
7843
|
+
backupRepoWrite(abs, ts, repo);
|
|
7844
|
+
} catch (err) {
|
|
7845
|
+
warn(
|
|
7846
|
+
`could not snapshot ${abs} before removing it (${err.message}), so it was left in place: ${denied}, so remove it by hand`
|
|
7847
|
+
);
|
|
7848
|
+
return;
|
|
7849
|
+
}
|
|
7850
|
+
try {
|
|
7851
|
+
rmSync18(abs, { recursive: true, force: true });
|
|
7852
|
+
if (presentAt(abs)) {
|
|
7853
|
+
warn(`could not remove ${abs}: ${denied}, so remove it by hand`);
|
|
7854
|
+
return;
|
|
7855
|
+
}
|
|
7856
|
+
const where = snapshotted ? `. A copy was snapshotted under backup/${ts}/repo/ first` : "";
|
|
7857
|
+
warn(`removed ${path} from the sync repo working tree: ${denied}${where}`);
|
|
7858
|
+
} catch (err) {
|
|
7859
|
+
warn(`could not remove ${abs}: ${err.message}`);
|
|
7860
|
+
}
|
|
7861
|
+
}
|
|
7862
|
+
function reportTrackedDenied(repo, path, segment) {
|
|
7863
|
+
const inHead = gitProbe(["ls-tree", "--name-only", "HEAD", "--", path], repo);
|
|
7864
|
+
const denied = `the path segment "${segment}" is on the never-sync list`;
|
|
7865
|
+
if (inHead === null) {
|
|
7866
|
+
warn(
|
|
7867
|
+
`could not check ${path} against HEAD: ${denied}. Nothing was changed. Inspect it with "git status -- ${path}" and take it out of shared/ before committing`
|
|
7868
|
+
);
|
|
7869
|
+
return;
|
|
7870
|
+
}
|
|
7871
|
+
if (inHead.trim() === "") {
|
|
7872
|
+
warn(
|
|
7873
|
+
`${path} is staged and has no committed version: ${denied}. Nothing was changed. Run "git rm --cached -- ${path}" to take it out of the index; that leaves the file on disk but makes it untracked, which the next nomad pull removes from the sync repo working tree (snapshotting it into the backup cache first), so move it outside shared/ instead if you want to keep it. If it is the destination half of a staged rename, "git diff --cached --name-status" names the source, whose content IS committed, so restore that half with "git checkout HEAD -- <source>" rather than leaving its deletion staged`
|
|
7874
|
+
);
|
|
7875
|
+
return;
|
|
7876
|
+
}
|
|
7877
|
+
if (!existsSync41(join53(repo, path))) return;
|
|
7878
|
+
warn(
|
|
7879
|
+
`${path} is tracked and has changes against HEAD: ${denied}. Nothing was changed. Run "git checkout HEAD -- ${path}" to put the committed content back, or move the file outside shared/ if you want to keep it. Neither of those takes the committed copy out of the repo: "git rm -- ${path}" and a commit does that going forward, and if it holds a real secret, rotate it and rewrite history, because nomad only changes your local worktree and index and cannot scrub what a previous push already sent to the remote`
|
|
7880
|
+
);
|
|
7881
|
+
}
|
|
7882
|
+
function revertDeniedMirrorPaths(repo, status, ts) {
|
|
7883
|
+
for (const path of new Set(status.untracked)) {
|
|
7884
|
+
const segment = deniedSegmentFor(path);
|
|
7885
|
+
if (segment !== null) removeUntrackedDenied(repo, path, segment, ts);
|
|
7886
|
+
}
|
|
7887
|
+
for (const path of new Set(status.tracked)) {
|
|
7888
|
+
const segment = deniedSegmentFor(path);
|
|
7889
|
+
if (segment !== null) reportTrackedDenied(repo, path, segment);
|
|
7890
|
+
}
|
|
7495
7891
|
}
|
|
7496
7892
|
|
|
7497
7893
|
// src/preview.ts
|
|
@@ -7838,8 +8234,8 @@ function formatLinkRow(e) {
|
|
|
7838
8234
|
if (e.kind === "copy") return `would copy ${e.from} -> ${e.to}`;
|
|
7839
8235
|
return `${e.kind} ${e.from} -> ${e.to}`;
|
|
7840
8236
|
}
|
|
7841
|
-
function
|
|
7842
|
-
return `would capture ${
|
|
8237
|
+
function formatMirrorRow(e) {
|
|
8238
|
+
return `would capture ${e.localPath} -> ${e.repoPath}`;
|
|
7843
8239
|
}
|
|
7844
8240
|
function formatDeletionRow(d) {
|
|
7845
8241
|
return `would remove ${d.repoPath} (gone from ${d.localPath})`;
|
|
@@ -7847,6 +8243,23 @@ function formatDeletionRow(d) {
|
|
|
7847
8243
|
function formatSessionRow(e) {
|
|
7848
8244
|
return e.kind === "overwrite" ? `overwrite ${e.dst} (from ${e.src})` : e.text;
|
|
7849
8245
|
}
|
|
8246
|
+
function appendMirrorPlanRows(links, ts, map, plans) {
|
|
8247
|
+
if (plans) {
|
|
8248
|
+
for (const capture of plans.captures) addItem(links, formatMirrorRow(capture));
|
|
8249
|
+
for (const deletion of plans.deletions) addItem(links, formatDeletionRow(deletion));
|
|
8250
|
+
return plans.namesDerived;
|
|
8251
|
+
}
|
|
8252
|
+
const linkNames = allSharedLinks(map);
|
|
8253
|
+
stageLocalSharedEdits(map, ts, {
|
|
8254
|
+
dryRun: true,
|
|
8255
|
+
onPreview: (e) => addItem(links, formatMirrorRow(e)),
|
|
8256
|
+
linkNames
|
|
8257
|
+
});
|
|
8258
|
+
for (const deletion of planSharedLinkDeletions(map, { linkNames })) {
|
|
8259
|
+
addItem(links, formatDeletionRow(deletion));
|
|
8260
|
+
}
|
|
8261
|
+
return true;
|
|
8262
|
+
}
|
|
7850
8263
|
function buildSettingsSectionForPreview(result) {
|
|
7851
8264
|
const s = section("settings.json", true);
|
|
7852
8265
|
if (result.diff !== "") {
|
|
@@ -7865,18 +8278,10 @@ function computePreview(ts, map, verb = "pull", plans) {
|
|
|
7865
8278
|
console.log(`would pull on host=${HOST} (preview; nothing applied)`);
|
|
7866
8279
|
console.log("");
|
|
7867
8280
|
const links = section("Symlinks");
|
|
7868
|
-
const
|
|
7869
|
-
captures: planSharedLinkCaptures(map),
|
|
7870
|
-
deletions: planSharedLinkDeletions(map)
|
|
7871
|
-
};
|
|
7872
|
-
for (const capture of captures) {
|
|
7873
|
-
addItem(links, formatCaptureRow(capture));
|
|
7874
|
-
}
|
|
7875
|
-
for (const deletion of deletions) {
|
|
7876
|
-
addItem(links, formatDeletionRow(deletion));
|
|
7877
|
-
}
|
|
8281
|
+
const namesDerived = appendMirrorPlanRows(links, ts, map, plans);
|
|
7878
8282
|
applySharedLinks(ts, map, {
|
|
7879
8283
|
dryRun: true,
|
|
8284
|
+
quietNames: namesDerived,
|
|
7880
8285
|
onPreview: (e) => addItem(links, formatLinkRow(e))
|
|
7881
8286
|
});
|
|
7882
8287
|
const settingsResult = previewSettings(
|
|
@@ -7926,6 +8331,19 @@ function readMapForMirror(mapPath) {
|
|
|
7926
8331
|
return null;
|
|
7927
8332
|
}
|
|
7928
8333
|
}
|
|
8334
|
+
function sharedDirsUnchanged(before, after) {
|
|
8335
|
+
return JSON.stringify(before) === JSON.stringify(after);
|
|
8336
|
+
}
|
|
8337
|
+
function namesAlreadyReported(namesDerived, derivedSharedDirs, map) {
|
|
8338
|
+
return namesDerived && sharedDirsUnchanged(derivedSharedDirs, map.sharedDirs);
|
|
8339
|
+
}
|
|
8340
|
+
function plansAgainst(plans, map) {
|
|
8341
|
+
if (plans === void 0) return void 0;
|
|
8342
|
+
return {
|
|
8343
|
+
...plans,
|
|
8344
|
+
namesDerived: namesAlreadyReported(plans.namesDerived, plans.derivedSharedDirs, map)
|
|
8345
|
+
};
|
|
8346
|
+
}
|
|
7929
8347
|
function untrackedUnderShared(repo) {
|
|
7930
8348
|
const out = gitProbe(["ls-files", "--others", "--exclude-standard", "-z", "--", "shared/"], repo);
|
|
7931
8349
|
if (out === null) return null;
|
|
@@ -7935,27 +8353,94 @@ function newlyUntracked(before, after) {
|
|
|
7935
8353
|
if (before === null || after === null) return [];
|
|
7936
8354
|
return [...after].filter((p) => !before.has(p));
|
|
7937
8355
|
}
|
|
8356
|
+
function revertDeniedUnderShared(repo, ts) {
|
|
8357
|
+
const out = gitProbe(
|
|
8358
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", "shared/"],
|
|
8359
|
+
repo
|
|
8360
|
+
);
|
|
8361
|
+
if (out === null) return;
|
|
8362
|
+
revertDeniedMirrorPaths(repo, parsePorcelainZ(out), ts);
|
|
8363
|
+
}
|
|
7938
8364
|
function reconcileSharedLinksBeforePull(repo, ts) {
|
|
7939
|
-
if (process.platform !== "win32")
|
|
8365
|
+
if (process.platform !== "win32") {
|
|
8366
|
+
return { mirrored: [], events: [], namesDerived: false, derivedSharedDirs: void 0 };
|
|
8367
|
+
}
|
|
7940
8368
|
const before = untrackedUnderShared(repo);
|
|
8369
|
+
const events = [];
|
|
8370
|
+
let linkNames = [];
|
|
8371
|
+
let namesDerived = false;
|
|
8372
|
+
let derivedSharedDirs;
|
|
7941
8373
|
try {
|
|
7942
8374
|
const map = readMapForMirror(join56(repo, "path-map.json"));
|
|
7943
|
-
|
|
7944
|
-
|
|
8375
|
+
if (map !== null) {
|
|
8376
|
+
linkNames = allSharedLinks(map);
|
|
8377
|
+
namesDerived = true;
|
|
8378
|
+
derivedSharedDirs = map.sharedDirs;
|
|
8379
|
+
}
|
|
8380
|
+
stageLocalSharedEdits(map, ts, { onPreview: (e) => events.push(e), linkNames });
|
|
8381
|
+
events.push(...applySharedLinkDeletions(map, ts, { linkNames }));
|
|
7945
8382
|
} catch (err) {
|
|
7946
8383
|
warn(`could not reconcile local shared edits before the pull: ${err.message}`);
|
|
7947
8384
|
}
|
|
7948
|
-
|
|
8385
|
+
revertDeniedUnderShared(repo, ts);
|
|
8386
|
+
return {
|
|
8387
|
+
mirrored: newlyUntracked(before, untrackedUnderShared(repo)),
|
|
8388
|
+
events,
|
|
8389
|
+
namesDerived,
|
|
8390
|
+
derivedSharedDirs
|
|
8391
|
+
};
|
|
8392
|
+
}
|
|
8393
|
+
function buildMirrorSection(events, discard) {
|
|
8394
|
+
const s = section("Symlinks");
|
|
8395
|
+
for (const e of events) {
|
|
8396
|
+
addItem(
|
|
8397
|
+
s,
|
|
8398
|
+
e.kind === "deletion" ? `removed ${e.repoPath} (gone from ${e.localPath})` : `captured ${e.localPath} -> ${e.repoPath}`
|
|
8399
|
+
);
|
|
8400
|
+
}
|
|
8401
|
+
if (discard) {
|
|
8402
|
+
const names = discard.count === 1 ? "name was" : "names were";
|
|
8403
|
+
addItem(
|
|
8404
|
+
s,
|
|
8405
|
+
`${yellow(warnGlyph)} recovered from a wedged repo, ${discard.count} shared ${names} restored from the repo copy, previous host copies backed up to ${discard.backupPath}`
|
|
8406
|
+
);
|
|
8407
|
+
}
|
|
8408
|
+
return s;
|
|
7949
8409
|
}
|
|
7950
|
-
function
|
|
8410
|
+
function describeSkippedMirrorDiscard(repo, ts) {
|
|
8411
|
+
if (process.platform !== "win32") return null;
|
|
8412
|
+
try {
|
|
8413
|
+
const map = readMapForMirror(join56(repo, "path-map.json"));
|
|
8414
|
+
const linkNames = map !== null ? allSharedLinks(map, { quiet: true }) : void 0;
|
|
8415
|
+
const captures = [];
|
|
8416
|
+
stageLocalSharedEdits(map, ts, {
|
|
8417
|
+
dryRun: true,
|
|
8418
|
+
onPreview: (e) => captures.push(e),
|
|
8419
|
+
linkNames
|
|
8420
|
+
});
|
|
8421
|
+
if (captures.length === 0) return null;
|
|
8422
|
+
return { count: captures.length, backupPath: join56(backupBase(), ts) };
|
|
8423
|
+
} catch {
|
|
8424
|
+
return null;
|
|
8425
|
+
}
|
|
8426
|
+
}
|
|
8427
|
+
function planSharedReconcileBeforePull(repo, ts) {
|
|
7951
8428
|
const map = readMapForMirror(join56(repo, "path-map.json"));
|
|
7952
|
-
|
|
8429
|
+
const captures = [];
|
|
8430
|
+
const linkNames = process.platform === "win32" && map !== null ? allSharedLinks(map) : void 0;
|
|
8431
|
+
stageLocalSharedEdits(map, ts, { dryRun: true, onPreview: (e) => captures.push(e), linkNames });
|
|
8432
|
+
return {
|
|
8433
|
+
captures,
|
|
8434
|
+
deletions: planSharedLinkDeletions(map, { linkNames }),
|
|
8435
|
+
namesDerived: linkNames !== void 0,
|
|
8436
|
+
derivedSharedDirs: map?.sharedDirs
|
|
8437
|
+
};
|
|
7953
8438
|
}
|
|
7954
8439
|
|
|
7955
8440
|
// src/commands.pull.collision.ts
|
|
7956
8441
|
init_config();
|
|
7957
8442
|
init_exit_codes();
|
|
7958
|
-
import { existsSync as existsSync45, lstatSync as
|
|
8443
|
+
import { existsSync as existsSync45, lstatSync as lstatSync19, readFileSync as readFileSync20, rmSync as rmSync19 } from "node:fs";
|
|
7959
8444
|
import { join as join57 } from "node:path";
|
|
7960
8445
|
init_utils();
|
|
7961
8446
|
var SHARED_PREFIX = /^shared\//;
|
|
@@ -8022,9 +8507,9 @@ function removeMirroredCopies(repo, repoRelPaths) {
|
|
|
8022
8507
|
for (const rel of repoRelPaths.filter(isContainedMirrorPath)) {
|
|
8023
8508
|
const abs = join57(repo, rel);
|
|
8024
8509
|
try {
|
|
8025
|
-
if (
|
|
8510
|
+
if (lstatSync19(abs, { throwIfNoEntry: false })?.isFile() !== true) continue;
|
|
8026
8511
|
if (!matchesLocalOriginal(repo, rel)) continue;
|
|
8027
|
-
|
|
8512
|
+
rmSync19(abs, { force: true });
|
|
8028
8513
|
} catch (err) {
|
|
8029
8514
|
warn(`could not remove nomad's copy at ${abs}: ${err.message}`);
|
|
8030
8515
|
}
|
|
@@ -8051,47 +8536,6 @@ init_commands_pull_wedge();
|
|
|
8051
8536
|
init_config();
|
|
8052
8537
|
init_utils();
|
|
8053
8538
|
init_utils_fs();
|
|
8054
|
-
|
|
8055
|
-
// src/commands.pull.recovery.git.ts
|
|
8056
|
-
init_utils();
|
|
8057
|
-
import { execFileSync as execFileSync22 } from "node:child_process";
|
|
8058
|
-
function gitCapture(args, cwd) {
|
|
8059
|
-
return execFileSync22("git", args, {
|
|
8060
|
-
cwd,
|
|
8061
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
8062
|
-
maxBuffer: 64 * 1024 * 1024
|
|
8063
|
-
}).toString().trim();
|
|
8064
|
-
}
|
|
8065
|
-
function parsePorcelainZ(raw) {
|
|
8066
|
-
const tracked = [];
|
|
8067
|
-
const untracked = [];
|
|
8068
|
-
if (!raw) return { tracked, untracked };
|
|
8069
|
-
const records = raw.split("\0");
|
|
8070
|
-
for (let i = 0; i < records.length; i++) {
|
|
8071
|
-
const record = records[i];
|
|
8072
|
-
if (record.length < 3) continue;
|
|
8073
|
-
const xy = record.slice(0, 2);
|
|
8074
|
-
const filePath = record.slice(3);
|
|
8075
|
-
if (xy === "??") {
|
|
8076
|
-
untracked.push(filePath);
|
|
8077
|
-
continue;
|
|
8078
|
-
}
|
|
8079
|
-
tracked.push(filePath);
|
|
8080
|
-
if (xy.startsWith("R") || xy.startsWith("C")) {
|
|
8081
|
-
const src = records[i + 1];
|
|
8082
|
-
if (src) {
|
|
8083
|
-
tracked.push(src);
|
|
8084
|
-
i++;
|
|
8085
|
-
}
|
|
8086
|
-
}
|
|
8087
|
-
}
|
|
8088
|
-
return { tracked, untracked };
|
|
8089
|
-
}
|
|
8090
|
-
function parseDirtyPaths(repo, opts = {}) {
|
|
8091
|
-
return parsePorcelainZ(gitStatusPorcelainZ(repo, opts));
|
|
8092
|
-
}
|
|
8093
|
-
|
|
8094
|
-
// src/commands.pull.recovery.ts
|
|
8095
8539
|
function isSyncedConfig(path) {
|
|
8096
8540
|
return PUSH_ALLOWED_STATIC.some(
|
|
8097
8541
|
(entry) => entry.endsWith("/") ? path.startsWith(entry) : path === entry
|
|
@@ -8207,9 +8651,9 @@ function capturePrePostHeads(repo, rebase) {
|
|
|
8207
8651
|
if (pre === void 0 || post === void 0) return void 0;
|
|
8208
8652
|
return { pre, post };
|
|
8209
8653
|
}
|
|
8210
|
-
function buildWetPullSections(ts, map, prePostHeads) {
|
|
8211
|
-
applySharedLinks(ts, map);
|
|
8212
|
-
writeSharedBaseline(map);
|
|
8654
|
+
function buildWetPullSections(ts, map, prePostHeads, namesDerived = false) {
|
|
8655
|
+
applySharedLinks(ts, map, { quietNames: namesDerived });
|
|
8656
|
+
writeSharedBaseline(map, { quiet: true });
|
|
8213
8657
|
const { label } = regenerateSettings(ts);
|
|
8214
8658
|
syncSkillsPull(ts, prePostHeads);
|
|
8215
8659
|
const remapResult = withSpinner("Syncing sessions", () => remapPull(ts));
|
|
@@ -8232,22 +8676,24 @@ function buildWetPullSections(ts, map, prePostHeads) {
|
|
|
8232
8676
|
};
|
|
8233
8677
|
}
|
|
8234
8678
|
function handleWedge(repo, forceRemote) {
|
|
8235
|
-
const wedge =
|
|
8236
|
-
if (wedge === null)
|
|
8679
|
+
const { state: wedge, probe } = classifyWedgeWithProbe(repo);
|
|
8680
|
+
if (wedge === null) {
|
|
8681
|
+
if (forceRemote) log(cleanRepoForceRemoteMessage(probe));
|
|
8682
|
+
return false;
|
|
8683
|
+
}
|
|
8237
8684
|
if (wedge === "unmerged-index") {
|
|
8238
8685
|
if (forceRemote) {
|
|
8239
8686
|
recoverUnmergedIndex(repo);
|
|
8240
|
-
|
|
8241
|
-
die(unmergedIndexRunbookText("nomad pull"), { code: EXIT.CONFLICT });
|
|
8687
|
+
return false;
|
|
8242
8688
|
}
|
|
8243
|
-
return;
|
|
8689
|
+
return die(unmergedIndexRunbookText("nomad pull"), { code: EXIT.CONFLICT });
|
|
8244
8690
|
}
|
|
8245
8691
|
if (forceRemote) {
|
|
8246
8692
|
recoverForceRemote(wedge, repo);
|
|
8247
|
-
return;
|
|
8693
|
+
return true;
|
|
8248
8694
|
}
|
|
8249
8695
|
const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
|
|
8250
|
-
die(wedgeMarkerRunbookText(state), { code: EXIT.CONFLICT });
|
|
8696
|
+
return die(wedgeMarkerRunbookText(state), { code: EXIT.CONFLICT });
|
|
8251
8697
|
}
|
|
8252
8698
|
function runPullCore(opts = {}) {
|
|
8253
8699
|
const dryRun = opts.dryRun === true;
|
|
@@ -8256,7 +8702,7 @@ function runPullCore(opts = {}) {
|
|
|
8256
8702
|
const repo = repoHome();
|
|
8257
8703
|
const backup = backupBase();
|
|
8258
8704
|
const ts = freshBackupTs(backup);
|
|
8259
|
-
handleWedge(repo, forceRemote);
|
|
8705
|
+
const recovered = handleWedge(repo, forceRemote);
|
|
8260
8706
|
if (!dryRun) {
|
|
8261
8707
|
const backupRoot = join58(backup, ts);
|
|
8262
8708
|
try {
|
|
@@ -8270,8 +8716,13 @@ function runPullCore(opts = {}) {
|
|
|
8270
8716
|
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
8271
8717
|
);
|
|
8272
8718
|
}
|
|
8273
|
-
const
|
|
8274
|
-
|
|
8719
|
+
const {
|
|
8720
|
+
mirrored,
|
|
8721
|
+
events: mirrorEvents,
|
|
8722
|
+
namesDerived,
|
|
8723
|
+
derivedSharedDirs
|
|
8724
|
+
} = !dryRun && !recovered ? reconcileSharedLinksBeforePull(repo, ts) : { mirrored: [], events: [], namesDerived: false, derivedSharedDirs: void 0 };
|
|
8725
|
+
const sharedPlans = dryRun ? planSharedReconcileBeforePull(repo, ts) : void 0;
|
|
8275
8726
|
const prePostHeads = capturePrePostHeads(repo, () => {
|
|
8276
8727
|
pullWithCollisionRunbook(repo, mirrored);
|
|
8277
8728
|
});
|
|
@@ -8280,18 +8731,20 @@ function runPullCore(opts = {}) {
|
|
|
8280
8731
|
const map = existsSync46(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
8281
8732
|
const divergedKeptLocal = divergenceCheckExtras(ts, dryRun ? prePostHeads : void 0);
|
|
8282
8733
|
if (dryRun) {
|
|
8283
|
-
computePreview(ts, map, "pull", sharedPlans);
|
|
8734
|
+
computePreview(ts, map, "pull", plansAgainst(sharedPlans, map));
|
|
8284
8735
|
return { tag: "dry" };
|
|
8285
8736
|
}
|
|
8737
|
+
const discardedMirror = recovered && !dryRun ? describeSkippedMirrorDiscard(repo, ts) : void 0;
|
|
8286
8738
|
const { sections, localOnly, settingsLabel, unmapped, extrasSkipped } = buildWetPullSections(
|
|
8287
8739
|
ts,
|
|
8288
8740
|
map,
|
|
8289
|
-
prePostHeads
|
|
8741
|
+
prePostHeads,
|
|
8742
|
+
namesAlreadyReported(namesDerived, derivedSharedDirs, map)
|
|
8290
8743
|
);
|
|
8291
8744
|
const incomingChanges = prePostHeads === void 0 ? true : prePostHeads.pre !== prePostHeads.post;
|
|
8292
8745
|
return {
|
|
8293
8746
|
tag: "wet",
|
|
8294
|
-
sections,
|
|
8747
|
+
sections: [buildMirrorSection(mirrorEvents, discardedMirror), ...sections],
|
|
8295
8748
|
localOnly,
|
|
8296
8749
|
divergedKeptLocal,
|
|
8297
8750
|
incomingChanges,
|
|
@@ -8399,19 +8852,6 @@ function isAllowed(path, allowed) {
|
|
|
8399
8852
|
}
|
|
8400
8853
|
return false;
|
|
8401
8854
|
}
|
|
8402
|
-
function blockSetFor(segments) {
|
|
8403
|
-
if (segments[0] !== "shared" || segments[1] !== "extras") return NEVER_SYNC;
|
|
8404
|
-
return segments[3] === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
|
|
8405
|
-
}
|
|
8406
|
-
function isNeverSync(path) {
|
|
8407
|
-
const segments = path.split("/");
|
|
8408
|
-
const blockSet = blockSetFor(segments);
|
|
8409
|
-
const scan = segments[0] === "shared" && segments[1] === "extras" ? segments.slice(4) : segments;
|
|
8410
|
-
for (const segment of scan) {
|
|
8411
|
-
if (isDeniedName(blockSet, segment)) return true;
|
|
8412
|
-
}
|
|
8413
|
-
return false;
|
|
8414
|
-
}
|
|
8415
8855
|
var GSD_HOOKS_SUPPORT_FILES = /* @__PURE__ */ new Set(["managed-hooks-registry.cjs", "package.json"]);
|
|
8416
8856
|
var GSD_HOOKS_SUPPORT_DIR = "lib";
|
|
8417
8857
|
function isGsdDropped(path) {
|
|
@@ -8451,7 +8891,11 @@ function enforceAllowList(statusPorcelain, map) {
|
|
|
8451
8891
|
...Object.entries(map.extras ?? {}).flatMap(
|
|
8452
8892
|
([l, names]) => names.filter((n) => extrasWhitelist.includes(n)).flatMap((n) => [`shared/extras/${l}/${n}`, `shared/extras/${l}/${n}/`])
|
|
8453
8893
|
),
|
|
8454
|
-
|
|
8894
|
+
// Predicate passed by reference, not wrapped in an arrow: `isValidSharedDir`
|
|
8895
|
+
// is a type guard, and an arrow returning its result is just a boolean, so
|
|
8896
|
+
// wrapping it would leave `d` as `unknown` and let a non-string reach the
|
|
8897
|
+
// template literal as "[object Object]".
|
|
8898
|
+
...sharedDirEntries(map).filter(isValidSharedDir).map((d) => `shared/${d}/`)
|
|
8455
8899
|
];
|
|
8456
8900
|
const neverSyncHits = [];
|
|
8457
8901
|
const violations = [];
|
|
@@ -8632,9 +9076,9 @@ init_color();
|
|
|
8632
9076
|
init_config();
|
|
8633
9077
|
init_config_sharedDirs_guard();
|
|
8634
9078
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
8635
|
-
import { copyFileSync, existsSync as existsSync49, lstatSync as
|
|
9079
|
+
import { copyFileSync, existsSync as existsSync49, lstatSync as lstatSync20, mkdirSync as mkdirSync15, readdirSync as readdirSync19, rmSync as rmSync20 } from "node:fs";
|
|
8636
9080
|
import { homedir as homedir7 } from "node:os";
|
|
8637
|
-
import { join as join62, relative as relative10, sep as
|
|
9081
|
+
import { join as join62, relative as relative10, sep as sep14 } from "node:path";
|
|
8638
9082
|
init_push_leak_verdict();
|
|
8639
9083
|
init_push_gitleaks();
|
|
8640
9084
|
init_utils_fs();
|
|
@@ -8642,7 +9086,7 @@ init_utils_json();
|
|
|
8642
9086
|
var NOTHING_TO_SCAN_ROW = `${dim(infoGlyph)} nothing to scan, no leaks`;
|
|
8643
9087
|
function stageSessionDir(localDir, dstDir, changed) {
|
|
8644
9088
|
if (changed !== void 0) {
|
|
8645
|
-
const prefix = `${localDir}${
|
|
9089
|
+
const prefix = `${localDir}${sep14}`;
|
|
8646
9090
|
const matching = [...changed].filter((p) => p.startsWith(prefix));
|
|
8647
9091
|
if (matching.length === 0) return false;
|
|
8648
9092
|
for (const src of matching) {
|
|
@@ -8696,7 +9140,7 @@ function stageExtras(tmpRoot, map) {
|
|
|
8696
9140
|
}
|
|
8697
9141
|
function stageSkills(tmpRoot) {
|
|
8698
9142
|
const localSkills = join62(claudeHome(), "skills");
|
|
8699
|
-
const stat =
|
|
9143
|
+
const stat = lstatSync20(localSkills, { throwIfNoEntry: false });
|
|
8700
9144
|
if (stat === void 0 || stat.isSymbolicLink()) return 0;
|
|
8701
9145
|
const names = readdirSync19(localSkills, { encoding: "utf8" }).filter((n) => !isSkillExcluded(n));
|
|
8702
9146
|
if (names.length === 0) return 0;
|
|
@@ -8730,7 +9174,7 @@ function previewPushLeaks(map, opts = {}) {
|
|
|
8730
9174
|
}
|
|
8731
9175
|
return verdictFromFindings(findings);
|
|
8732
9176
|
} finally {
|
|
8733
|
-
|
|
9177
|
+
rmSync20(tmpRoot, { recursive: true, force: true });
|
|
8734
9178
|
}
|
|
8735
9179
|
}
|
|
8736
9180
|
|
|
@@ -8859,7 +9303,8 @@ async function runPushCore(opts = {}) {
|
|
|
8859
9303
|
const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
|
|
8860
9304
|
if (!dryRun) {
|
|
8861
9305
|
syncSkillsPush();
|
|
8862
|
-
|
|
9306
|
+
const linkNames = map !== null ? allSharedLinks(map) : void 0;
|
|
9307
|
+
syncSharedLinksPush(map, { linkNames });
|
|
8863
9308
|
stripGsdHooksFromBase(repo, backup);
|
|
8864
9309
|
}
|
|
8865
9310
|
const st = { dryRun, remap, extras, globalConfig: [] };
|
|
@@ -8982,7 +9427,14 @@ function buildSyncSummarySection(pull, pushOutcome) {
|
|
|
8982
9427
|
for (const row2 of buildSkipAndCollisionRows(pull, result)) addItem(s, row2);
|
|
8983
9428
|
return s;
|
|
8984
9429
|
}
|
|
8985
|
-
var SYNC_SECTION_ORDER = [
|
|
9430
|
+
var SYNC_SECTION_ORDER = [
|
|
9431
|
+
"Symlinks",
|
|
9432
|
+
"Settings",
|
|
9433
|
+
"Global config",
|
|
9434
|
+
"Sessions",
|
|
9435
|
+
"Extras",
|
|
9436
|
+
"Leak scan"
|
|
9437
|
+
];
|
|
8986
9438
|
function isDroppedSummaryHeader(header) {
|
|
8987
9439
|
return header === PULL_SUMMARY_HEADER || header === "Push summary";
|
|
8988
9440
|
}
|
|
@@ -9211,7 +9663,7 @@ function buildCrashReport(input) {
|
|
|
9211
9663
|
}
|
|
9212
9664
|
|
|
9213
9665
|
// src/crash-report.redact.ts
|
|
9214
|
-
import { mkdtempSync as mkdtempSync3, rmSync as
|
|
9666
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync21, writeFileSync as writeFileSync10 } from "node:fs";
|
|
9215
9667
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
9216
9668
|
import { join as join65 } from "node:path";
|
|
9217
9669
|
init_push_gitleaks_scan();
|
|
@@ -9231,7 +9683,7 @@ function redactWithGitleaks(text, scan = scanFile) {
|
|
|
9231
9683
|
} finally {
|
|
9232
9684
|
if (dir !== void 0) {
|
|
9233
9685
|
try {
|
|
9234
|
-
|
|
9686
|
+
rmSync21(dir, { recursive: true, force: true });
|
|
9235
9687
|
} catch {
|
|
9236
9688
|
}
|
|
9237
9689
|
}
|
|
@@ -9409,7 +9861,7 @@ init_config();
|
|
|
9409
9861
|
init_utils();
|
|
9410
9862
|
init_utils_fs();
|
|
9411
9863
|
init_utils_json();
|
|
9412
|
-
import { copyFileSync as copyFileSync2, cpSync as cpSync10, existsSync as existsSync53, rmSync as
|
|
9864
|
+
import { copyFileSync as copyFileSync2, cpSync as cpSync10, existsSync as existsSync53, rmSync as rmSync22, statSync as statSync13 } from "node:fs";
|
|
9413
9865
|
import { join as join68 } from "node:path";
|
|
9414
9866
|
function snapshotIntoShared(map) {
|
|
9415
9867
|
const repo = repoHome();
|
|
@@ -9420,7 +9872,7 @@ function snapshotIntoShared(map) {
|
|
|
9420
9872
|
const dst = join68(repo, "shared", name);
|
|
9421
9873
|
if (statSync13(src).isDirectory()) {
|
|
9422
9874
|
const gk = join68(dst, ".gitkeep");
|
|
9423
|
-
if (existsSync53(gk))
|
|
9875
|
+
if (existsSync53(gk)) rmSync22(gk);
|
|
9424
9876
|
cpSync10(src, dst, { recursive: true, force: false, errorOnExist: true });
|
|
9425
9877
|
} else {
|
|
9426
9878
|
copyFileSync2(src, dst);
|
|
@@ -9881,7 +10333,7 @@ function parseSyncArgs(argv) {
|
|
|
9881
10333
|
// package.json
|
|
9882
10334
|
var package_default = {
|
|
9883
10335
|
name: "claude-nomad",
|
|
9884
|
-
version: "0.
|
|
10336
|
+
version: "0.65.0",
|
|
9885
10337
|
type: "module",
|
|
9886
10338
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
9887
10339
|
keywords: [
|
|
@@ -9992,7 +10444,8 @@ var DEFAULT_HELP = [
|
|
|
9992
10444
|
cont("abort the in-progress rebase/merge, park stranded commits on"),
|
|
9993
10445
|
cont("nomad/stranded-<ts>, reset to origin/main, and re-pull. Refuses"),
|
|
9994
10446
|
cont("if stranded or dirty tracked changes touch synced config (shared/,"),
|
|
9995
|
-
cont("hosts/, path-map.json). Cannot combine with --dry-run."),
|
|
10447
|
+
cont("hosts/, path-map.json). Cannot combine with --dry-run. On a clean"),
|
|
10448
|
+
cont("repo, reports nothing to recover and pulls normally."),
|
|
9996
10449
|
"",
|
|
9997
10450
|
row(" push", "Rebase, run safety checks (gitleaks, gitlinks, allow-list), commit, push."),
|
|
9998
10451
|
row(" --dry-run", "Run pre-checks (rebase, gitleaks probe, gitlink scan) and preview"),
|