claude-nomad 0.56.1 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/nomad.mjs CHANGED
@@ -198,6 +198,13 @@ function gitCaptureRaw(args, cwd) {
198
198
  maxBuffer: 64 * 1024 * 1024
199
199
  }).toString();
200
200
  }
201
+ function gitCaptureBuffer(args, cwd) {
202
+ return execFileSync("git", args, {
203
+ cwd,
204
+ stdio: ["ignore", "pipe", "pipe"],
205
+ maxBuffer: 64 * 1024 * 1024
206
+ });
207
+ }
201
208
  function gitOrFatal(args, context, cwd) {
202
209
  try {
203
210
  execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
@@ -207,12 +214,13 @@ function gitOrFatal(args, context, cwd) {
207
214
  throw new NomadFatal(`${context} failed`);
208
215
  }
209
216
  }
210
- var log, warn, fail, item, NomadFatal, die, gitStatusPorcelainZ;
217
+ var log, ok, warn, fail, item, NomadFatal, die, gitStatusPorcelainZ;
211
218
  var init_utils = __esm({
212
219
  "src/utils.ts"() {
213
220
  "use strict";
214
221
  init_color();
215
222
  log = (msg) => console.log(`${dim(infoGlyph)} ${msg}`);
223
+ ok = (msg) => console.log(`${green(okGlyph)} ${msg}`);
216
224
  warn = (msg) => {
217
225
  console.error(`${yellow(warnGlyph)} ${msg}`);
218
226
  };
@@ -461,7 +469,7 @@ var init_config = __esm({
461
469
  "hosts/",
462
470
  "path-map.json",
463
471
  ".gitleaksignore",
464
- // written by nomad push Allow action (D-04)
472
+ // written by nomad push Allow action
465
473
  ".gitleaks.overlay.toml"
466
474
  // user-owned gitleaks allowlist overlay layered on the bundled base
467
475
  ];
@@ -2110,8 +2118,8 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
2110
2118
  }
2111
2119
 
2112
2120
  // src/commands.doctor.ts
2113
- import { existsSync as existsSync31 } from "node:fs";
2114
- import { join as join37 } from "node:path";
2121
+ import { existsSync as existsSync32 } from "node:fs";
2122
+ import { join as join38 } from "node:path";
2115
2123
 
2116
2124
  // src/commands.doctor.checks.repo.ts
2117
2125
  init_color();
@@ -2551,28 +2559,42 @@ init_config();
2551
2559
  // src/extras-sync.diff.ts
2552
2560
  init_utils();
2553
2561
  import { execFileSync as execFileSync2 } from "node:child_process";
2554
- function labelDiffLine(line) {
2555
- const tab = line.indexOf(" ");
2556
- if (tab === -1) return line;
2557
- const status = line.slice(0, tab);
2558
- const path = line.slice(tab + 1);
2559
- if (status === "D") return `${path} (local only)`;
2560
- if (status === "A") return `${path} (repo only)`;
2561
- return path;
2562
+ import { relative as relative2 } from "node:path";
2563
+ function parseNameStatus(stdout) {
2564
+ const entries = [];
2565
+ for (const line of stdout.split("\n")) {
2566
+ if (line.length === 0) continue;
2567
+ const tab = line.indexOf(" ");
2568
+ if (tab === -1) continue;
2569
+ entries.push({ status: line.slice(0, tab), path: line.slice(tab + 1) });
2570
+ }
2571
+ return entries;
2572
+ }
2573
+ function labelEntry(entry) {
2574
+ if (entry.status === "D") return `${entry.path} (local only)`;
2575
+ if (entry.status === "A") return `${entry.path} (repo only)`;
2576
+ return entry.path;
2562
2577
  }
2563
2578
  function parseDiffOutput(stdout) {
2564
- return stdout.split("\n").filter((line) => line.length > 0).map(labelDiffLine);
2579
+ return parseNameStatus(stdout).map(labelEntry);
2565
2580
  }
2566
- function listDivergingFiles(a, b) {
2581
+ function parseModifiedPaths(stdout, a) {
2582
+ return parseNameStatus(stdout).filter((entry) => entry.status === "M").map((entry) => relative2(a, entry.path));
2583
+ }
2584
+ function runNameStatusDiff(a, b, parse) {
2567
2585
  try {
2568
- const stdout = execFileSync2("git", ["diff", "--no-index", "--name-status", a, b], {
2569
- stdio: ["ignore", "pipe", "pipe"]
2570
- }).toString();
2571
- return parseDiffOutput(stdout);
2586
+ const stdout = execFileSync2(
2587
+ "git",
2588
+ ["diff", "--no-index", "--no-renames", "--name-status", a, b],
2589
+ {
2590
+ stdio: ["ignore", "pipe", "pipe"]
2591
+ }
2592
+ ).toString();
2593
+ return parse(stdout);
2572
2594
  } catch (err) {
2573
2595
  const e = err;
2574
2596
  if (e.status === 1 && e.stdout !== void 0) {
2575
- return parseDiffOutput(e.stdout.toString());
2597
+ return parse(e.stdout.toString());
2576
2598
  }
2577
2599
  if (e.code === "ENOENT") {
2578
2600
  warn(`git not on PATH; divergence check skipped for ${a}`);
@@ -2582,6 +2604,12 @@ function listDivergingFiles(a, b) {
2582
2604
  return [];
2583
2605
  }
2584
2606
  }
2607
+ function listDivergingFiles(a, b) {
2608
+ return runNameStatusDiff(a, b, parseDiffOutput);
2609
+ }
2610
+ function listDivergingModified(a, b) {
2611
+ return runNameStatusDiff(a, b, (stdout) => parseModifiedPaths(stdout, a));
2612
+ }
2585
2613
 
2586
2614
  // src/commands.doctor.checks.skills.ts
2587
2615
  function stripSideIndicator(line) {
@@ -2591,15 +2619,15 @@ function stripSideIndicator(line) {
2591
2619
  }
2592
2620
  function isGsdDiffLine(line, localBase, sharedBase) {
2593
2621
  const bare = stripSideIndicator(line);
2594
- let relative7;
2622
+ let relative9;
2595
2623
  if (bare.startsWith(localBase + "/")) {
2596
- relative7 = bare.slice(localBase.length + 1);
2624
+ relative9 = bare.slice(localBase.length + 1);
2597
2625
  } else if (bare.startsWith(sharedBase + "/")) {
2598
- relative7 = bare.slice(sharedBase.length + 1);
2626
+ relative9 = bare.slice(sharedBase.length + 1);
2599
2627
  } else {
2600
- relative7 = bare;
2628
+ relative9 = bare;
2601
2629
  }
2602
- return relative7.split("/")[0].startsWith(GSD_PREFIX);
2630
+ return relative9.split("/")[0].startsWith(GSD_PREFIX);
2603
2631
  }
2604
2632
  function reportSkillsDivergence(section2) {
2605
2633
  const sharedSkills = join14(repoHome(), "shared", "skills");
@@ -2632,7 +2660,7 @@ init_color();
2632
2660
  init_config();
2633
2661
  import { execFileSync as execFileSync5 } from "node:child_process";
2634
2662
  import { existsSync as existsSync15 } from "node:fs";
2635
- import { join as join18, relative as relative2 } from "node:path";
2663
+ import { join as join18, relative as relative3 } from "node:path";
2636
2664
  init_commands_pull_wedge();
2637
2665
  init_push_checks();
2638
2666
  init_utils();
@@ -2659,7 +2687,7 @@ function reportGitlinks(section2) {
2659
2687
  if (existsSync15(sharedDir)) {
2660
2688
  const gitlinks = findGitlinks(sharedDir);
2661
2689
  for (const p of gitlinks) {
2662
- const rel = relative2(repo, p);
2690
+ const rel = relative3(repo, p);
2663
2691
  addItem(
2664
2692
  section2,
2665
2693
  `${red(failGlyph)} gitlink: ${blue(rel)} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`
@@ -2936,9 +2964,9 @@ function reportCheckSchema(section2) {
2936
2964
  init_color();
2937
2965
  import { randomBytes } from "node:crypto";
2938
2966
  import { execFileSync as execFileSync9 } from "node:child_process";
2939
- import { existsSync as existsSync20, mkdirSync as mkdirSync6, readdirSync as readdirSync9, rmSync as rmSync8 } from "node:fs";
2967
+ import { existsSync as existsSync21, mkdirSync as mkdirSync6, readdirSync as readdirSync10, rmSync as rmSync9 } from "node:fs";
2940
2968
  import { homedir as homedir4 } from "node:os";
2941
- import { join as join25 } from "node:path";
2969
+ import { join as join26 } from "node:path";
2942
2970
 
2943
2971
  // src/commands.doctor.check-shared.scan.ts
2944
2972
  init_color();
@@ -3035,8 +3063,22 @@ init_config();
3035
3063
 
3036
3064
  // src/remap.ts
3037
3065
  init_config_sharedDirs_guard();
3038
- import { cpSync as cpSync4, existsSync as existsSync19, mkdirSync as mkdirSync5, readdirSync as readdirSync8, renameSync as renameSync3, rmSync as rmSync7, statSync as statSync5 } from "node:fs";
3039
- import { dirname as dirname4, join as join24, relative as relative3, sep as sep3 } from "node:path";
3066
+ import {
3067
+ cpSync as cpSync5,
3068
+ existsSync as existsSync20,
3069
+ lstatSync as lstatSync9,
3070
+ mkdirSync as mkdirSync5,
3071
+ readdirSync as readdirSync9,
3072
+ renameSync as renameSync3,
3073
+ rmSync as rmSync8,
3074
+ statSync as statSync5
3075
+ } from "node:fs";
3076
+ import { dirname as dirname4, join as join25, relative as relative5, sep as sep3 } from "node:path";
3077
+
3078
+ // src/extras-sync.core.ts
3079
+ init_config();
3080
+ import { cpSync as cpSync4, existsSync as existsSync18, lstatSync as lstatSync8, readdirSync as readdirSync7, readFileSync as readFileSync6, rmSync as rmSync7 } from "node:fs";
3081
+ import { basename, join as join23, relative as relative4 } from "node:path";
3040
3082
 
3041
3083
  // src/extras-sync.guards.ts
3042
3084
  init_utils();
@@ -3055,6 +3097,180 @@ function assertSafeLocalRoot(localRoot, logical) {
3055
3097
  }
3056
3098
  }
3057
3099
 
3100
+ // src/extras-sync.core.ts
3101
+ init_utils();
3102
+ init_utils_json();
3103
+ function loadValidatedExtras(opts) {
3104
+ const repo = repoHome();
3105
+ const mapPath = join23(repo, "path-map.json");
3106
+ const repoExtras = join23(repo, "shared", "extras");
3107
+ if (!existsSync18(mapPath) || opts.requireRepoExtras === true && !existsSync18(repoExtras)) {
3108
+ if (opts.missingMsg !== void 0) log(opts.missingMsg);
3109
+ return null;
3110
+ }
3111
+ const map = readPathMap(mapPath);
3112
+ const extrasMap = map.extras ?? {};
3113
+ if (Object.keys(extrasMap).length === 0) return null;
3114
+ for (const logical of Object.keys(extrasMap)) {
3115
+ assertSafeLogical(logical);
3116
+ const localRoot = map.projects[logical]?.[HOST];
3117
+ if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
3118
+ }
3119
+ return { map, extrasMap };
3120
+ }
3121
+ function* eachExtrasTarget(v, counts) {
3122
+ const whitelist = SUPPORTED_EXTRAS;
3123
+ for (const [logical, dirnames] of Object.entries(v.extrasMap)) {
3124
+ const localRoot = v.map.projects[logical]?.[HOST];
3125
+ if (!localRoot || localRoot === "TBD") {
3126
+ counts.unmapped++;
3127
+ continue;
3128
+ }
3129
+ for (const dirname10 of dirnames) {
3130
+ if (!whitelist.includes(dirname10)) {
3131
+ counts.skipped++;
3132
+ continue;
3133
+ }
3134
+ yield { logical, localRoot, dirname: dirname10 };
3135
+ }
3136
+ }
3137
+ }
3138
+ function stripCollidingDstSymlinks(src, dst, isExcluded) {
3139
+ if (!existsSync18(dst)) return;
3140
+ for (const name of readdirSync7(src)) {
3141
+ if (isExcluded(name)) continue;
3142
+ const dstPath = join23(dst, name);
3143
+ const dstStat = lstatSync8(dstPath, { throwIfNoEntry: false });
3144
+ if (dstStat === void 0) continue;
3145
+ if (dstStat.isSymbolicLink()) {
3146
+ rmSync7(dstPath, { recursive: true, force: true });
3147
+ } else if (dstStat.isDirectory() && lstatSync8(join23(src, name)).isDirectory()) {
3148
+ stripCollidingDstSymlinks(join23(src, name), dstPath, isExcluded);
3149
+ }
3150
+ }
3151
+ }
3152
+ function cpSyncGuarded(src, dst, filter, label) {
3153
+ try {
3154
+ cpSync4(src, dst, { recursive: true, force: true, verbatimSymlinks: true, filter });
3155
+ } catch (err) {
3156
+ const e = err;
3157
+ if (e.code === "EINVAL" || e.code === "ENOTEMPTY" || e.code === "ERR_FS_CP_NON_DIR_TO_DIR" || e.code === "ERR_FS_CP_DIR_TO_NON_DIR") {
3158
+ throw new NomadFatal(
3159
+ `${label}: type collision copying ${JSON.stringify(src)} -> ${JSON.stringify(dst)} (${e.path ?? "unknown path"}): a file/directory type changed upstream; run nomad pull --force-remote to recover`
3160
+ );
3161
+ }
3162
+ throw err;
3163
+ }
3164
+ }
3165
+ function copyExtrasOverlayFiltered(src, dst, blockSet) {
3166
+ stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
3167
+ cpSyncGuarded(
3168
+ src,
3169
+ dst,
3170
+ (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry)),
3171
+ "copyExtrasOverlayFiltered"
3172
+ );
3173
+ }
3174
+ function copyExtrasOverlaySkipDiverged(src, dst, blockSet, divergedSet) {
3175
+ stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
3176
+ cpSyncGuarded(
3177
+ src,
3178
+ dst,
3179
+ (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry)) && !divergedSet.has(relative4(src, srcEntry)),
3180
+ "copyExtrasOverlaySkipDiverged"
3181
+ );
3182
+ }
3183
+ function copyExtras(src, dst) {
3184
+ rmSync7(dst, { recursive: true, force: true });
3185
+ cpSync4(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
3186
+ }
3187
+ function copyExtrasFileSkipDiverged(src, dst) {
3188
+ if (existsSync18(dst)) {
3189
+ let dstBuf;
3190
+ try {
3191
+ dstBuf = readFileSync6(dst);
3192
+ } catch {
3193
+ return;
3194
+ }
3195
+ if (!readFileSync6(src).equals(dstBuf)) return;
3196
+ }
3197
+ copyExtras(src, dst);
3198
+ }
3199
+ function extrasDenySet(dirname10) {
3200
+ return dirname10 === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
3201
+ }
3202
+ function copyExtrasFiltered(src, dst, blockSet) {
3203
+ rmSync7(dst, { recursive: true, force: true });
3204
+ cpSync4(src, dst, {
3205
+ recursive: true,
3206
+ force: true,
3207
+ verbatimSymlinks: true,
3208
+ filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry))
3209
+ });
3210
+ }
3211
+ function prunePreservingDenied(src, dst, blockSet) {
3212
+ for (const name of readdirSync7(dst)) {
3213
+ if (isDeniedName(blockSet, name)) continue;
3214
+ const dstPath = join23(dst, name);
3215
+ const srcStat = lstatSync8(join23(src, name), { throwIfNoEntry: false });
3216
+ if (srcStat === void 0) {
3217
+ rmSync7(dstPath, { recursive: true, force: true });
3218
+ continue;
3219
+ }
3220
+ const dstStat = lstatSync8(dstPath);
3221
+ if (srcStat.isDirectory() && dstStat.isDirectory()) {
3222
+ prunePreservingDenied(join23(src, name), dstPath, blockSet);
3223
+ } else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
3224
+ rmSync7(dstPath, { recursive: true, force: true });
3225
+ }
3226
+ }
3227
+ }
3228
+ function copyExtrasFilteredPreserving(src, dst, blockSet) {
3229
+ const dstStat = lstatSync8(dst, { throwIfNoEntry: false });
3230
+ if (dstStat !== void 0) {
3231
+ if (dstStat.isDirectory()) prunePreservingDenied(src, dst, blockSet);
3232
+ else rmSync7(dst, { recursive: true, force: true });
3233
+ }
3234
+ stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
3235
+ cpSync4(src, dst, {
3236
+ recursive: true,
3237
+ force: true,
3238
+ verbatimSymlinks: true,
3239
+ filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry))
3240
+ });
3241
+ }
3242
+ function prunePreservingBy(src, dst, isPreserved) {
3243
+ for (const name of readdirSync7(dst)) {
3244
+ if (isPreserved(name)) continue;
3245
+ const dstPath = join23(dst, name);
3246
+ const srcStat = lstatSync8(join23(src, name), { throwIfNoEntry: false });
3247
+ if (srcStat === void 0) {
3248
+ rmSync7(dstPath, { recursive: true, force: true });
3249
+ continue;
3250
+ }
3251
+ const dstStat = lstatSync8(dstPath);
3252
+ if (srcStat.isDirectory() && dstStat.isDirectory()) {
3253
+ prunePreservingBy(join23(src, name), dstPath, isPreserved);
3254
+ } else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
3255
+ rmSync7(dstPath, { recursive: true, force: true });
3256
+ }
3257
+ }
3258
+ }
3259
+ function copyExtrasFilteredPreservingBy(src, dst, isPreserved) {
3260
+ const dstStat = lstatSync8(dst, { throwIfNoEntry: false });
3261
+ if (dstStat !== void 0) {
3262
+ if (dstStat.isDirectory()) prunePreservingBy(src, dst, isPreserved);
3263
+ else rmSync7(dst, { recursive: true, force: true });
3264
+ }
3265
+ stripCollidingDstSymlinks(src, dst, isPreserved);
3266
+ cpSync4(src, dst, {
3267
+ recursive: true,
3268
+ force: true,
3269
+ verbatimSymlinks: true,
3270
+ filter: (srcEntry) => srcEntry === src || !isPreserved(basename(srcEntry))
3271
+ });
3272
+ }
3273
+
3058
3274
  // src/remap.ts
3059
3275
  init_config();
3060
3276
 
@@ -3063,8 +3279,8 @@ init_config();
3063
3279
  init_push_gitleaks_config();
3064
3280
  init_utils_fs();
3065
3281
  import { createHash } from "node:crypto";
3066
- import { existsSync as existsSync18, mkdirSync as mkdirSync4, readdirSync as readdirSync7, readFileSync as readFileSync6, statSync as statSync4 } from "node:fs";
3067
- import { dirname as dirname3, join as join23 } from "node:path";
3282
+ import { existsSync as existsSync19, mkdirSync as mkdirSync4, readdirSync as readdirSync8, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
3283
+ import { dirname as dirname3, join as join24 } from "node:path";
3068
3284
  function isChanged(prev, cur, hash) {
3069
3285
  if (prev === void 0) return true;
3070
3286
  if (prev.size !== cur.size) return true;
@@ -3101,11 +3317,11 @@ function shouldFullRescan(old, scannerVersion, configHash, forceFlag) {
3101
3317
  return false;
3102
3318
  }
3103
3319
  function hashFile(absPath) {
3104
- return createHash("sha256").update(readFileSync6(absPath)).digest("hex");
3320
+ return createHash("sha256").update(readFileSync7(absPath)).digest("hex");
3105
3321
  }
3106
3322
  function enumerateDir(dir, results) {
3107
- for (const entry of readdirSync7(dir)) {
3108
- const fullPath = join23(dir, entry);
3323
+ for (const entry of readdirSync8(dir)) {
3324
+ const fullPath = join24(dir, entry);
3109
3325
  const st = statSync4(fullPath);
3110
3326
  if (st.isDirectory()) {
3111
3327
  enumerateDir(fullPath, results);
@@ -3116,8 +3332,8 @@ function enumerateDir(dir, results) {
3116
3332
  }
3117
3333
  function enumerateSourceFiles(projectDir) {
3118
3334
  const results = [];
3119
- for (const entry of readdirSync7(projectDir)) {
3120
- const fullPath = join23(projectDir, entry);
3335
+ for (const entry of readdirSync8(projectDir)) {
3336
+ const fullPath = join24(projectDir, entry);
3121
3337
  const st = statSync4(fullPath);
3122
3338
  if (st.isDirectory()) {
3123
3339
  enumerateDir(fullPath, results);
@@ -3129,15 +3345,15 @@ function enumerateSourceFiles(projectDir) {
3129
3345
  }
3130
3346
  function fileIdentity(p) {
3131
3347
  if (p === null) return "none::absent";
3132
- if (!existsSync18(p)) return `${p}::absent`;
3133
- return `${p}::${createHash("sha256").update(readFileSync6(p)).digest("hex")}`;
3348
+ if (!existsSync19(p)) return `${p}::absent`;
3349
+ return `${p}::${createHash("sha256").update(readFileSync7(p)).digest("hex")}`;
3134
3350
  }
3135
3351
  function computeConfigHash() {
3136
3352
  const repo = repoHome();
3137
3353
  const parts = [
3138
3354
  fileIdentity(resolveTomlPath(repo)),
3139
- fileIdentity(join23(repo, ".gitleaks.overlay.toml")),
3140
- fileIdentity(join23(repo, ".gitleaksignore"))
3355
+ fileIdentity(join24(repo, ".gitleaks.overlay.toml")),
3356
+ fileIdentity(join24(repo, ".gitleaksignore"))
3141
3357
  ];
3142
3358
  return createHash("sha256").update(parts.join("\n")).digest("hex");
3143
3359
  }
@@ -3148,7 +3364,7 @@ function isManifestShape(raw) {
3148
3364
  }
3149
3365
  function readManifest(path) {
3150
3366
  try {
3151
- const raw = readFileSync6(path, "utf8");
3367
+ const raw = readFileSync7(path, "utf8");
3152
3368
  const parsed = JSON.parse(raw);
3153
3369
  if (!isManifestShape(parsed)) return null;
3154
3370
  return parsed;
@@ -3171,18 +3387,19 @@ init_utils_json();
3171
3387
  var TMP_SUFFIX = ".nomad-tmp";
3172
3388
  function atomicMirror(src, dst, options) {
3173
3389
  const tmp = `${dst}${TMP_SUFFIX}`;
3174
- rmSync7(tmp, { recursive: true, force: true });
3175
- cpSync4(src, tmp, options);
3176
- rmSync7(dst, { recursive: true, force: true });
3390
+ rmSync8(tmp, { recursive: true, force: true });
3391
+ cpSync5(src, tmp, options);
3392
+ rmSync8(dst, { recursive: true, force: true });
3177
3393
  renameSync3(tmp, dst);
3178
3394
  }
3179
- function copyDir(src, dst) {
3180
- atomicMirror(src, dst, { recursive: true, force: true });
3395
+ function overlaySessionDir(src, dst) {
3396
+ stripCollidingDstSymlinks(src, dst, () => false);
3397
+ cpSyncGuarded(src, dst, void 0, "overlaySessionDir");
3181
3398
  }
3182
3399
  function copyFileAtomic(src, dst) {
3183
3400
  mkdirSync5(dirname4(dst), { recursive: true });
3184
3401
  const tmp = `${dst}.tmp.${process.pid}`;
3185
- cpSync4(src, tmp, { force: true, preserveTimestamps: true });
3402
+ cpSync5(src, tmp, { force: true, preserveTimestamps: true });
3186
3403
  renameSync3(tmp, dst);
3187
3404
  }
3188
3405
  function hasDeltaForDir(sel, localDir) {
@@ -3198,12 +3415,12 @@ function applySelective(sel, localDir, repoDst) {
3198
3415
  const prefix = `${localDir}${sep3}`;
3199
3416
  for (const src of sel.changed) {
3200
3417
  if (!src.startsWith(prefix)) continue;
3201
- copyFileAtomic(src, join24(repoDst, relative3(localDir, src)));
3418
+ copyFileAtomic(src, join25(repoDst, relative5(localDir, src)));
3202
3419
  }
3203
3420
  for (const src of sel.deleted) {
3204
3421
  if (!src.startsWith(prefix)) continue;
3205
- const dst = join24(repoDst, relative3(localDir, src));
3206
- if (existsSync19(dst)) rmSync7(dst);
3422
+ const dst = join25(repoDst, relative5(localDir, src));
3423
+ if (existsSync20(dst)) rmSync8(dst);
3207
3424
  }
3208
3425
  }
3209
3426
  function copyDirJsonlOnly(src, dst) {
@@ -3211,7 +3428,7 @@ function copyDirJsonlOnly(src, dst) {
3211
3428
  recursive: true,
3212
3429
  force: true,
3213
3430
  filter: (srcPath) => {
3214
- const rel = relative3(src, srcPath);
3431
+ const rel = relative5(src, srcPath);
3215
3432
  if (rel === "") return true;
3216
3433
  if (rel.split(sep3).length > 1) return true;
3217
3434
  if (statSync5(srcPath).isDirectory()) return true;
@@ -3232,15 +3449,15 @@ function remapPull(ts, opts = {}) {
3232
3449
  const wouldPull = [];
3233
3450
  const repo = repoHome();
3234
3451
  const claude = claudeHome();
3235
- const mapPath = join24(repo, "path-map.json");
3236
- const repoProjects = join24(repo, "shared", "projects");
3237
- if (!existsSync19(mapPath) || !existsSync19(repoProjects)) {
3452
+ const mapPath = join25(repo, "path-map.json");
3453
+ const repoProjects = join25(repo, "shared", "projects");
3454
+ if (!existsSync20(mapPath) || !existsSync20(repoProjects)) {
3238
3455
  const text = "no path-map or repo projects dir; skipping session remap";
3239
3456
  emitPreview(opts.onPreview, { kind: "note", text }, text);
3240
3457
  return { unmapped: 0, pulled, wouldPull };
3241
3458
  }
3242
3459
  const map = readPathMap(mapPath);
3243
- const localProjects = join24(claude, "projects");
3460
+ const localProjects = join25(claude, "projects");
3244
3461
  if (!dryRun) mkdirSync5(localProjects, { recursive: true });
3245
3462
  for (const [logical, hosts] of Object.entries(map.projects)) {
3246
3463
  assertSafeLogical(logical);
@@ -3250,9 +3467,9 @@ function remapPull(ts, opts = {}) {
3250
3467
  continue;
3251
3468
  }
3252
3469
  assertSafeLocalRoot(localPath, logical);
3253
- const src = join24(repoProjects, logical);
3254
- if (!existsSync19(src)) continue;
3255
- const dst = join24(localProjects, encodePath(localPath));
3470
+ const src = join25(repoProjects, logical);
3471
+ if (!existsSync20(src)) continue;
3472
+ const dst = join25(localProjects, encodePath(localPath));
3256
3473
  if (dryRun) {
3257
3474
  wouldPull.push(logical);
3258
3475
  emitPreview(
@@ -3263,11 +3480,44 @@ function remapPull(ts, opts = {}) {
3263
3480
  continue;
3264
3481
  }
3265
3482
  backupBeforeWrite(dst, ts);
3266
- copyDir(src, dst);
3483
+ overlaySessionDir(src, dst);
3267
3484
  pulled.push(logical);
3268
3485
  }
3269
3486
  return { unmapped, pulled, wouldPull };
3270
3487
  }
3488
+ function countLocalOnly(src, dst) {
3489
+ let count = 0;
3490
+ for (const name of readdirSync9(dst)) {
3491
+ const dstPath = join25(dst, name);
3492
+ const srcPath = join25(src, name);
3493
+ if (lstatSync9(dstPath).isDirectory()) {
3494
+ count += countLocalOnly(srcPath, dstPath);
3495
+ } else if (lstatSync9(srcPath, { throwIfNoEntry: false }) === void 0) {
3496
+ count++;
3497
+ }
3498
+ }
3499
+ return count;
3500
+ }
3501
+ function scanLocalOnly() {
3502
+ const repo = repoHome();
3503
+ const claude = claudeHome();
3504
+ const mapPath = join25(repo, "path-map.json");
3505
+ const repoProjects = join25(repo, "shared", "projects");
3506
+ if (!existsSync20(mapPath) || !existsSync20(repoProjects)) return 0;
3507
+ const map = readPathMap(mapPath);
3508
+ const localProjects = join25(claude, "projects");
3509
+ let count = 0;
3510
+ for (const [logical, hosts] of Object.entries(map.projects)) {
3511
+ assertSafeLogical(logical);
3512
+ const localPath = hosts[HOST];
3513
+ if (!localPath || localPath === "TBD") continue;
3514
+ assertSafeLocalRoot(localPath, logical);
3515
+ const dst = join25(localProjects, encodePath(localPath));
3516
+ if (!existsSync20(dst)) continue;
3517
+ count += countLocalOnly(join25(repoProjects, logical), dst);
3518
+ }
3519
+ return count;
3520
+ }
3271
3521
  function buildReverseMap(map) {
3272
3522
  const reverse = /* @__PURE__ */ new Map();
3273
3523
  const encodedPaths = /* @__PURE__ */ new Map();
@@ -3299,26 +3549,26 @@ function remapPush(ts, opts = {}) {
3299
3549
  const wouldPush = [];
3300
3550
  const repo = repoHome();
3301
3551
  const claude = claudeHome();
3302
- const mapPath = join24(repo, "path-map.json");
3303
- if (!existsSync19(mapPath)) {
3552
+ const mapPath = join25(repo, "path-map.json");
3553
+ if (!existsSync20(mapPath)) {
3304
3554
  log("no path-map.json; skipping session export");
3305
3555
  return { unmapped: 0, collisions: 0, pushed, wouldPush };
3306
3556
  }
3307
3557
  const map = readPathMap(mapPath);
3308
- const localProjects = join24(claude, "projects");
3309
- const repoProjects = join24(repo, "shared", "projects");
3558
+ const localProjects = join25(claude, "projects");
3559
+ const repoProjects = join25(repo, "shared", "projects");
3310
3560
  const reverse = buildReverseMap(map);
3311
- if (!existsSync19(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
3561
+ if (!existsSync20(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
3312
3562
  if (!dryRun) mkdirSync5(repoProjects, { recursive: true });
3313
- for (const dir of readdirSync8(localProjects)) {
3563
+ for (const dir of readdirSync9(localProjects)) {
3314
3564
  if (dir.endsWith(TMP_SUFFIX)) continue;
3315
3565
  const logical = reverse.get(dir);
3316
3566
  if (!logical) {
3317
3567
  unmapped++;
3318
3568
  continue;
3319
3569
  }
3320
- const localDir = join24(localProjects, dir);
3321
- const repoDst = join24(repoProjects, logical);
3570
+ const localDir = join25(localProjects, dir);
3571
+ const repoDst = join25(repoProjects, logical);
3322
3572
  if (skipForSelection(opts.selection, localDir)) continue;
3323
3573
  if (dryRun) {
3324
3574
  wouldPush.push(logical);
@@ -3342,8 +3592,8 @@ function buildScanTree(tmpRoot) {
3342
3592
  const logicalToEncoded = /* @__PURE__ */ new Map();
3343
3593
  let staged = 0;
3344
3594
  const repo = repoHome();
3345
- const mapPath = join25(repo, "path-map.json");
3346
- if (!existsSync20(mapPath)) return { logicalToEncoded, staged, malformed: false };
3595
+ const mapPath = join26(repo, "path-map.json");
3596
+ if (!existsSync21(mapPath)) return { logicalToEncoded, staged, malformed: false };
3347
3597
  let map;
3348
3598
  try {
3349
3599
  map = readJson(mapPath);
@@ -3360,12 +3610,12 @@ function buildScanTree(tmpRoot) {
3360
3610
  if (!p || p === "TBD") continue;
3361
3611
  reverse.set(encodePath(p), logical);
3362
3612
  }
3363
- const localProjects = join25(claudeHome(), "projects");
3364
- if (!existsSync20(localProjects)) return { logicalToEncoded, staged, malformed: false };
3365
- for (const dir of readdirSync9(localProjects)) {
3613
+ const localProjects = join26(claudeHome(), "projects");
3614
+ if (!existsSync21(localProjects)) return { logicalToEncoded, staged, malformed: false };
3615
+ for (const dir of readdirSync10(localProjects)) {
3366
3616
  const logical = reverse.get(dir);
3367
3617
  if (!logical) continue;
3368
- copyDirJsonlOnly(join25(localProjects, dir), join25(tmpRoot, "shared", "projects", logical));
3618
+ copyDirJsonlOnly(join26(localProjects, dir), join26(tmpRoot, "shared", "projects", logical));
3369
3619
  logicalToEncoded.set(logical, dir);
3370
3620
  staged++;
3371
3621
  }
@@ -3396,11 +3646,11 @@ function ensureGitleaksReady(section2, gitleaksReady) {
3396
3646
  }
3397
3647
  function reportCheckShared(section2, gitleaksReady) {
3398
3648
  if (!ensureGitleaksReady(section2, gitleaksReady)) return;
3399
- const cacheDir = join25(homedir4(), ".cache", "claude-nomad");
3649
+ const cacheDir = join26(homedir4(), ".cache", "claude-nomad");
3400
3650
  mkdirSync6(cacheDir, { recursive: true });
3401
3651
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes(4).toString("hex")}`;
3402
- const reportPath = join25(cacheDir, `check-shared-${stamp}.json`);
3403
- const tmpRoot = join25(cacheDir, `check-shared-tree-${stamp}`);
3652
+ const reportPath = join26(cacheDir, `check-shared-${stamp}.json`);
3653
+ const tmpRoot = join26(cacheDir, `check-shared-tree-${stamp}`);
3404
3654
  try {
3405
3655
  const { logicalToEncoded, staged, malformed } = buildScanTree(tmpRoot);
3406
3656
  if (malformed) {
@@ -3414,19 +3664,19 @@ function reportCheckShared(section2, gitleaksReady) {
3414
3664
  }
3415
3665
  scanAndReport(section2, tmpRoot, staged, logicalToEncoded);
3416
3666
  } finally {
3417
- rmSync8(reportPath, { force: true });
3418
- rmSync8(tmpRoot, { recursive: true, force: true });
3667
+ rmSync9(reportPath, { force: true });
3668
+ rmSync9(tmpRoot, { recursive: true, force: true });
3419
3669
  }
3420
3670
  }
3421
3671
 
3422
3672
  // src/commands.doctor.checks.hooks.scope.ts
3423
3673
  init_color();
3424
- import { existsSync as existsSync21, readFileSync as readFileSync7, readdirSync as readdirSync10, realpathSync as realpathSync2 } from "node:fs";
3425
- import { dirname as dirname5, extname, join as join26 } from "node:path";
3674
+ import { existsSync as existsSync22, readFileSync as readFileSync8, readdirSync as readdirSync11, realpathSync as realpathSync2 } from "node:fs";
3675
+ import { dirname as dirname5, extname, join as join27 } from "node:path";
3426
3676
  init_config();
3427
3677
  function typeFromPackageJson(pkgPath) {
3428
3678
  try {
3429
- const parsed = JSON.parse(readFileSync7(pkgPath, "utf8"));
3679
+ const parsed = JSON.parse(readFileSync8(pkgPath, "utf8"));
3430
3680
  return parsed.type === "module" ? "esm" : "cjs";
3431
3681
  } catch {
3432
3682
  return "cjs";
@@ -3444,8 +3694,8 @@ function effectiveType(hookPath) {
3444
3694
  }
3445
3695
  let dir = dirname5(real);
3446
3696
  for (; ; ) {
3447
- const pkg = join26(dir, "package.json");
3448
- if (existsSync21(pkg)) return typeFromPackageJson(pkg);
3697
+ const pkg = join27(dir, "package.json");
3698
+ if (existsSync22(pkg)) return typeFromPackageJson(pkg);
3449
3699
  const parent = dirname5(dir);
3450
3700
  if (parent === dir) return "cjs";
3451
3701
  dir = parent;
@@ -3474,28 +3724,28 @@ function remedy(family) {
3474
3724
  }
3475
3725
  function safeReaddir2(dir) {
3476
3726
  try {
3477
- return readdirSync10(dir);
3727
+ return readdirSync11(dir);
3478
3728
  } catch {
3479
3729
  return [];
3480
3730
  }
3481
3731
  }
3482
3732
  function safeRead(path) {
3483
3733
  try {
3484
- return readFileSync7(path, "utf8");
3734
+ return readFileSync8(path, "utf8");
3485
3735
  } catch {
3486
3736
  return null;
3487
3737
  }
3488
3738
  }
3489
3739
  function reportHookScopeCheck(section2) {
3490
- const hooksDir = join26(claudeHome(), "hooks");
3491
- if (!existsSync21(hooksDir)) {
3740
+ const hooksDir = join27(claudeHome(), "hooks");
3741
+ if (!existsSync22(hooksDir)) {
3492
3742
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/hooks; skipping module-scope check`);
3493
3743
  return;
3494
3744
  }
3495
3745
  let anyWarn = false;
3496
3746
  for (const name of safeReaddir2(hooksDir)) {
3497
3747
  if (extname(name) !== ".js") continue;
3498
- const abs = join26(hooksDir, name);
3748
+ const abs = join27(hooksDir, name);
3499
3749
  const eff = effectiveType(abs);
3500
3750
  if (eff === null) continue;
3501
3751
  const src = safeRead(abs);
@@ -3515,8 +3765,8 @@ function reportHookScopeCheck(section2) {
3515
3765
 
3516
3766
  // src/commands.doctor.checks.hooks.ts
3517
3767
  init_color();
3518
- import { existsSync as existsSync22 } from "node:fs";
3519
- import { join as join27 } from "node:path";
3768
+ import { existsSync as existsSync23 } from "node:fs";
3769
+ import { join as join28 } from "node:path";
3520
3770
  init_config();
3521
3771
  function expandHome(token) {
3522
3772
  const h2 = home();
@@ -3559,7 +3809,7 @@ function checkEventGroups(section2, event, groups) {
3559
3809
  for (const group of groups) {
3560
3810
  for (const cmd of commandsFromGroup(group)) {
3561
3811
  for (const resolved of claudePathsIn(cmd)) {
3562
- if (existsSync22(resolved)) continue;
3812
+ if (existsSync23(resolved)) continue;
3563
3813
  addItem(
3564
3814
  section2,
3565
3815
  `${red(failGlyph)} hooks/${event}: command target missing: ${resolved} (run nomad pull)`
@@ -3572,8 +3822,8 @@ function checkEventGroups(section2, event, groups) {
3572
3822
  return anyFail;
3573
3823
  }
3574
3824
  function reportHooksTargetCheck(section2) {
3575
- const settingsPath = join27(claudeHome(), "settings.json");
3576
- if (!existsSync22(settingsPath)) {
3825
+ const settingsPath = join28(claudeHome(), "settings.json");
3826
+ if (!existsSync23(settingsPath)) {
3577
3827
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping hook target check`);
3578
3828
  return;
3579
3829
  }
@@ -3596,12 +3846,12 @@ function reportHooksTargetCheck(section2) {
3596
3846
 
3597
3847
  // src/commands.doctor.checks.hooks.preserve-symlinks.ts
3598
3848
  init_color();
3599
- import { existsSync as existsSync24, readFileSync as readFileSync8 } from "node:fs";
3600
- import { join as join29 } from "node:path";
3849
+ import { existsSync as existsSync25, readFileSync as readFileSync9 } from "node:fs";
3850
+ import { join as join30 } from "node:path";
3601
3851
 
3602
3852
  // src/commands.doctor.checks.hooks.preserve-symlinks.probe.ts
3603
- import { closeSync as closeSync3, existsSync as existsSync23, openSync as openSync3, readSync, realpathSync as realpathSync3 } from "node:fs";
3604
- import { dirname as dirname6, join as join28, resolve as resolve2 } from "node:path";
3853
+ import { closeSync as closeSync3, existsSync as existsSync24, openSync as openSync3, readSync, realpathSync as realpathSync3 } from "node:fs";
3854
+ import { dirname as dirname6, join as join29, resolve as resolve2 } from "node:path";
3605
3855
  function suppressedRanges(src) {
3606
3856
  const ranges = [];
3607
3857
  const re = /\/\*[\s\S]*?\*\/|\/\/[^\n]*|'[^']*'|"[^"]*"|`[^`]*`/g;
@@ -3633,12 +3883,12 @@ function topRelativeSpecifiers(src) {
3633
3883
  }
3634
3884
  function specifierIsMissing(specifier, baseDir) {
3635
3885
  const base = resolve2(baseDir, specifier);
3636
- if (existsSync23(base)) return false;
3886
+ if (existsSync24(base)) return false;
3637
3887
  for (const ext of [".js", ".cjs", ".mjs"]) {
3638
- if (existsSync23(base + ext)) return false;
3888
+ if (existsSync24(base + ext)) return false;
3639
3889
  }
3640
3890
  for (const idx of ["index.js", "index.cjs", "index.mjs"]) {
3641
- if (existsSync23(join28(base, idx))) return false;
3891
+ if (existsSync24(join29(base, idx))) return false;
3642
3892
  }
3643
3893
  return true;
3644
3894
  }
@@ -3693,10 +3943,10 @@ function commandTokens(command) {
3693
3943
  return tokens;
3694
3944
  }
3695
3945
  function readPathMapSafe() {
3696
- const mapPath = join29(repoHome(), "path-map.json");
3697
- if (!existsSync24(mapPath)) return { projects: {} };
3946
+ const mapPath = join30(repoHome(), "path-map.json");
3947
+ if (!existsSync25(mapPath)) return { projects: {} };
3698
3948
  try {
3699
- return JSON.parse(readFileSync8(mapPath, "utf8"));
3949
+ return JSON.parse(readFileSync9(mapPath, "utf8"));
3700
3950
  } catch {
3701
3951
  return { projects: {} };
3702
3952
  }
@@ -3767,8 +4017,8 @@ function checkEventForPreserveSymlinks(section2, event, groups, sharedLinkNames)
3767
4017
  return anyWarn;
3768
4018
  }
3769
4019
  function reportPreserveSymlinksCheck(section2) {
3770
- const settingsPath = join29(claudeHome(), "settings.json");
3771
- if (!existsSync24(settingsPath)) {
4020
+ const settingsPath = join30(claudeHome(), "settings.json");
4021
+ if (!existsSync25(settingsPath)) {
3772
4022
  addItem(
3773
4023
  section2,
3774
4024
  `${dim(infoGlyph)} no ~/.claude/settings.json; skipping preserve-symlinks-main check`
@@ -3777,7 +4027,7 @@ function reportPreserveSymlinksCheck(section2) {
3777
4027
  }
3778
4028
  let settings;
3779
4029
  try {
3780
- settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
4030
+ settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
3781
4031
  } catch {
3782
4032
  return;
3783
4033
  }
@@ -3801,8 +4051,8 @@ function reportPreserveSymlinksCheck(section2) {
3801
4051
 
3802
4052
  // src/commands.doctor.checks.settings-drift.ts
3803
4053
  init_color();
3804
- import { existsSync as existsSync25, readFileSync as readFileSync9 } from "node:fs";
3805
- import { join as join30 } from "node:path";
4054
+ import { existsSync as existsSync26, readFileSync as readFileSync10 } from "node:fs";
4055
+ import { join as join31 } from "node:path";
3806
4056
  init_config();
3807
4057
  init_utils_json();
3808
4058
  function diffMergedSettings(merged, settings) {
@@ -3811,7 +4061,7 @@ function diffMergedSettings(merged, settings) {
3811
4061
  }
3812
4062
  function tryReadJson(filePath) {
3813
4063
  try {
3814
- const raw = readFileSync9(filePath, "utf8");
4064
+ const raw = readFileSync10(filePath, "utf8");
3815
4065
  const parsed = JSON.parse(raw);
3816
4066
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
3817
4067
  return parsed;
@@ -3820,7 +4070,7 @@ function tryReadJson(filePath) {
3820
4070
  }
3821
4071
  }
3822
4072
  function reportHooksBaseSelfCleanNote(section2) {
3823
- const basePath = join30(repoHome(), "shared", "settings.base.json");
4073
+ const basePath = join31(repoHome(), "shared", "settings.base.json");
3824
4074
  const base = tryReadJson(basePath);
3825
4075
  if (base === null) return;
3826
4076
  if (!baseHasGsdHookEntries(base)) return;
@@ -3833,14 +4083,14 @@ function reportSettingsDriftCheck(section2) {
3833
4083
  const claude = claudeHome();
3834
4084
  const repo = repoHome();
3835
4085
  const host = HOST;
3836
- const settingsPath = join30(claude, "settings.json");
3837
- const basePath = join30(repo, "shared", "settings.base.json");
3838
- const hostPath = join30(repo, "hosts", `${host}.json`);
3839
- if (!existsSync25(settingsPath)) {
4086
+ const settingsPath = join31(claude, "settings.json");
4087
+ const basePath = join31(repo, "shared", "settings.base.json");
4088
+ const hostPath = join31(repo, "hosts", `${host}.json`);
4089
+ if (!existsSync26(settingsPath)) {
3840
4090
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping merge-drift check`);
3841
4091
  return;
3842
4092
  }
3843
- if (!existsSync25(basePath)) {
4093
+ if (!existsSync26(basePath)) {
3844
4094
  addItem(
3845
4095
  section2,
3846
4096
  `${dim(infoGlyph)} shared/settings.base.json missing; skipping merge-drift check`
@@ -3859,7 +4109,7 @@ function reportSettingsDriftCheck(section2) {
3859
4109
  if (settings === null) {
3860
4110
  return;
3861
4111
  }
3862
- const hostExists = existsSync25(hostPath);
4112
+ const hostExists = existsSync26(hostPath);
3863
4113
  const hostObj = hostExists ? tryReadJson(hostPath) : null;
3864
4114
  if (hostExists && hostObj === null) {
3865
4115
  addItem(
@@ -3908,12 +4158,12 @@ init_config();
3908
4158
 
3909
4159
  // src/commands.doctor.engine.ts
3910
4160
  init_color();
3911
- import { readFileSync as readFileSync11 } from "node:fs";
4161
+ import { readFileSync as readFileSync12 } from "node:fs";
3912
4162
  import { fileURLToPath as fileURLToPath3 } from "node:url";
3913
4163
 
3914
4164
  // src/commands.doctor.version.ts
3915
4165
  init_color();
3916
- import { readFileSync as readFileSync10 } from "node:fs";
4166
+ import { readFileSync as readFileSync11 } from "node:fs";
3917
4167
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3918
4168
  init_config();
3919
4169
  var STRICT_SEMVER = /^\d+\.\d+\.\d+$/;
@@ -3930,7 +4180,7 @@ function compareSemver(a, b) {
3930
4180
  function readLocalVersion() {
3931
4181
  try {
3932
4182
  const pkgPath = fileURLToPath2(new URL("../package.json", import.meta.url));
3933
- const parsed = JSON.parse(readFileSync10(pkgPath, "utf8"));
4183
+ const parsed = JSON.parse(readFileSync11(pkgPath, "utf8"));
3934
4184
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
3935
4185
  return parsed.version;
3936
4186
  }
@@ -3989,7 +4239,7 @@ function parseMinVersion(spec) {
3989
4239
  function readEnginesNode() {
3990
4240
  try {
3991
4241
  const pkgPath = fileURLToPath3(new URL("../package.json", import.meta.url));
3992
- const parsed = JSON.parse(readFileSync11(pkgPath, "utf8"));
4242
+ const parsed = JSON.parse(readFileSync12(pkgPath, "utf8"));
3993
4243
  const node = parsed.engines?.node;
3994
4244
  if (typeof node === "string" && node.length > 0) return node;
3995
4245
  return null;
@@ -4017,44 +4267,44 @@ function reportNodeEngineCheck(section2) {
4017
4267
 
4018
4268
  // src/spinner.ts
4019
4269
  init_color();
4020
- import { existsSync as existsSync29 } from "node:fs";
4270
+ import { existsSync as existsSync30 } from "node:fs";
4021
4271
  import { fileURLToPath as fileURLToPath4 } from "node:url";
4022
4272
  import { Worker } from "node:worker_threads";
4023
4273
 
4024
4274
  // src/commands.push.recovery.ts
4025
4275
  init_config();
4026
- import { readFileSync as readFileSync14, rmSync as rmSync10, writeFileSync as writeFileSync5 } from "node:fs";
4027
- import { join as join35 } from "node:path";
4276
+ import { readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync5 } from "node:fs";
4277
+ import { join as join36 } from "node:path";
4028
4278
  import { createInterface as createInterface2 } from "node:readline/promises";
4029
4279
 
4030
4280
  // src/commands.push.recovery.actions.ts
4031
4281
  init_config();
4032
- import { readFileSync as readFileSync13 } from "node:fs";
4282
+ import { readFileSync as readFileSync14 } from "node:fs";
4033
4283
  import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep5 } from "node:path";
4034
4284
 
4035
4285
  // src/commands.push.recovery.redact.ts
4036
4286
  init_config();
4037
4287
  init_config_sharedDirs_guard();
4038
- import { cpSync as cpSync5, existsSync as existsSync28, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
4039
- import { dirname as dirname8, join as join33, sep as sep4 } from "node:path";
4288
+ import { cpSync as cpSync6, existsSync as existsSync29, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
4289
+ import { dirname as dirname8, join as join34, sep as sep4 } from "node:path";
4040
4290
 
4041
4291
  // src/commands.redact.ts
4042
4292
  init_config();
4043
- import { existsSync as existsSync27, statSync as statSync7 } from "node:fs";
4044
- import { dirname as dirname7, join as join32 } from "node:path";
4293
+ import { existsSync as existsSync28, statSync as statSync7 } from "node:fs";
4294
+ import { dirname as dirname7, join as join33 } from "node:path";
4045
4295
 
4046
4296
  // src/commands.redact.subtree.ts
4047
- import { existsSync as existsSync26, lstatSync as lstatSync8, readFileSync as readFileSync12, readdirSync as readdirSync11, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
4048
- import { join as join31 } from "node:path";
4297
+ import { existsSync as existsSync27, lstatSync as lstatSync10, readFileSync as readFileSync13, readdirSync as readdirSync12, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
4298
+ import { join as join32 } from "node:path";
4049
4299
  init_utils_fs();
4050
4300
  init_utils();
4051
4301
  function collectFiles(dir, out) {
4052
- if (!existsSync26(dir)) return;
4053
- const st = lstatSync8(dir);
4302
+ if (!existsSync27(dir)) return;
4303
+ const st = lstatSync10(dir);
4054
4304
  if (!st.isDirectory()) return;
4055
- for (const entry of readdirSync11(dir)) {
4056
- const abs = join31(dir, entry);
4057
- const lst = lstatSync8(abs);
4305
+ for (const entry of readdirSync12(dir)) {
4306
+ const abs = join32(dir, entry);
4307
+ const lst = lstatSync10(abs);
4058
4308
  if (lst.isSymbolicLink()) continue;
4059
4309
  if (lst.isDirectory()) {
4060
4310
  collectFiles(abs, out);
@@ -4090,7 +4340,7 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
4090
4340
  if (!dryRun && total > 0) {
4091
4341
  for (const { path: filePath, findings } of dirty) {
4092
4342
  backupBeforeWrite(filePath, ts);
4093
- const before = readFileSync12(filePath, "utf8");
4343
+ const before = readFileSync13(filePath, "utf8");
4094
4344
  const after = applyRedactions(before, findings);
4095
4345
  if (after === before) {
4096
4346
  log(
@@ -4158,15 +4408,15 @@ init_utils_json();
4158
4408
  init_utils();
4159
4409
  function resolveLiveTranscript(id) {
4160
4410
  try {
4161
- const mapPath = join32(repoHome(), "path-map.json");
4162
- if (!existsSync27(mapPath)) return null;
4411
+ const mapPath = join33(repoHome(), "path-map.json");
4412
+ if (!existsSync28(mapPath)) return null;
4163
4413
  const projects = readJson(mapPath).projects;
4164
4414
  const claude = claudeHome();
4165
4415
  for (const hostMap of Object.values(projects)) {
4166
4416
  const abs = hostMap[HOST];
4167
4417
  if (abs === void 0) continue;
4168
- const live = join32(claude, "projects", encodePath(abs), `${id}.jsonl`);
4169
- if (existsSync27(live)) return live;
4418
+ const live = join33(claude, "projects", encodePath(abs), `${id}.jsonl`);
4419
+ if (existsSync28(live)) return live;
4170
4420
  }
4171
4421
  return null;
4172
4422
  } catch {
@@ -4186,17 +4436,17 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
4186
4436
  }
4187
4437
  const repo = repoHome();
4188
4438
  const backup = backupBase();
4189
- if (!existsSync27(repo)) die(`repo not cloned at ${repo}`);
4439
+ if (!existsSync28(repo)) die(`repo not cloned at ${repo}`);
4190
4440
  const handle = acquireLock("redact");
4191
4441
  if (handle === null) process.exit(0);
4192
4442
  try {
4193
4443
  const localPath = resolveLiveTranscript(id);
4194
- if (localPath === null || !existsSync27(localPath)) {
4444
+ if (localPath === null || !existsSync28(localPath)) {
4195
4445
  fail(`could not resolve local transcript for session ${id} on this host`);
4196
4446
  process.exitCode = 1;
4197
4447
  return;
4198
4448
  }
4199
- const sessionDir = join32(dirname7(localPath), id);
4449
+ const sessionDir = join33(dirname7(localPath), id);
4200
4450
  const subtreeFiles = listSubtreeFiles(sessionDir);
4201
4451
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
4202
4452
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -4312,8 +4562,8 @@ function resolveStagedDir(localPath, map, claude, repo) {
4312
4562
  assertSafeLogical(logical);
4313
4563
  const abs = hostMap[HOST];
4314
4564
  if (abs === void 0) continue;
4315
- if (localPath.startsWith(join33(claude, "projects", encodePath(abs)) + sep4)) {
4316
- return join33(repo, "shared", "projects", logical);
4565
+ if (localPath.startsWith(join34(claude, "projects", encodePath(abs)) + sep4)) {
4566
+ return join34(repo, "shared", "projects", logical);
4317
4567
  }
4318
4568
  }
4319
4569
  return null;
@@ -4323,7 +4573,7 @@ function preflightRedactable(f, map, nowMs) {
4323
4573
  if (sid === null) return "a finding has no resolvable session id (not a session transcript)";
4324
4574
  const localPath = resolveLiveTranscript(sid);
4325
4575
  if (localPath === null) return `session ${sid}: local transcript not found`;
4326
- const sessionDir = join33(dirname8(localPath), sid);
4576
+ const sessionDir = join34(dirname8(localPath), sid);
4327
4577
  const subtreeFiles = listSubtreeFiles(sessionDir);
4328
4578
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
4329
4579
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -4353,7 +4603,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
4353
4603
  `could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
4354
4604
  );
4355
4605
  }
4356
- const sessionDir = join33(dirname8(localPath), sid);
4606
+ const sessionDir = join34(dirname8(localPath), sid);
4357
4607
  const subtreeFiles = listSubtreeFiles(sessionDir);
4358
4608
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
4359
4609
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -4387,26 +4637,26 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
4387
4637
  );
4388
4638
  }
4389
4639
  mkdirSync7(stagedProjectDir, { recursive: true });
4390
- cpSync5(localPath, join33(stagedProjectDir, `${sid}.jsonl`), { force: true });
4391
- if (existsSync28(sessionDir)) {
4392
- cpSync5(sessionDir, join33(stagedProjectDir, sid), { force: true, recursive: true });
4640
+ cpSync6(localPath, join34(stagedProjectDir, `${sid}.jsonl`), { force: true });
4641
+ if (existsSync29(sessionDir)) {
4642
+ cpSync6(sessionDir, join34(stagedProjectDir, sid), { force: true, recursive: true });
4393
4643
  }
4394
4644
  return true;
4395
4645
  }
4396
4646
 
4397
4647
  // src/commands.push.recovery.drop.ts
4398
4648
  init_config();
4399
- import { rmSync as rmSync9 } from "node:fs";
4400
- import { join as join34 } from "node:path";
4649
+ import { rmSync as rmSync10 } from "node:fs";
4650
+ import { join as join35 } from "node:path";
4401
4651
  function dropSessionFromStaged(sid, map) {
4402
4652
  const logicals = Object.keys(map.projects);
4403
4653
  if (logicals.length === 0) return false;
4404
4654
  const repo = repoHome();
4405
4655
  for (const logical of logicals) {
4406
- const jsonl = join34(repo, "shared", "projects", logical, `${sid}.jsonl`);
4407
- const dir = join34(repo, "shared", "projects", logical, sid);
4408
- rmSync9(jsonl, { force: true });
4409
- rmSync9(dir, { recursive: true, force: true });
4656
+ const jsonl = join35(repo, "shared", "projects", logical, `${sid}.jsonl`);
4657
+ const dir = join35(repo, "shared", "projects", logical, sid);
4658
+ rmSync10(jsonl, { force: true });
4659
+ rmSync10(dir, { recursive: true, force: true });
4410
4660
  }
4411
4661
  return true;
4412
4662
  }
@@ -4440,7 +4690,7 @@ function makeDefaultReadLine(repo) {
4440
4690
  if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot + sep5)) {
4441
4691
  return null;
4442
4692
  }
4443
- const content = readFileSync13(target, "utf8");
4693
+ const content = readFileSync14(target, "utf8");
4444
4694
  const lines = content.split(/\r?\n/);
4445
4695
  const idx = line - 1;
4446
4696
  if (idx < 0 || idx >= lines.length) return null;
@@ -4561,10 +4811,10 @@ function applyThenRescan(scanVerdict, repoHome2) {
4561
4811
  return next;
4562
4812
  }
4563
4813
  function allowThenRescan(append, scanVerdict, repoHome2) {
4564
- const ignPath = join35(repoHome2, ".gitleaksignore");
4814
+ const ignPath = join36(repoHome2, ".gitleaksignore");
4565
4815
  let before;
4566
4816
  try {
4567
- before = readFileSync14(ignPath, "utf8");
4817
+ before = readFileSync15(ignPath, "utf8");
4568
4818
  } catch {
4569
4819
  before = null;
4570
4820
  }
@@ -4572,7 +4822,7 @@ function allowThenRescan(append, scanVerdict, repoHome2) {
4572
4822
  try {
4573
4823
  return applyThenRescan(scanVerdict, repoHome2);
4574
4824
  } catch (err) {
4575
- if (before === null) rmSync10(ignPath, { force: true });
4825
+ if (before === null) rmSync11(ignPath, { force: true });
4576
4826
  else writeFileSync5(ignPath, before, "utf8");
4577
4827
  throw err;
4578
4828
  }
@@ -4660,7 +4910,7 @@ function writeAnimatedDone(out, label, ms, useTTY) {
4660
4910
  `);
4661
4911
  }
4662
4912
  function resolveWorkerPath(deps = {}) {
4663
- const check = deps.existsSyncFn ?? existsSync29;
4913
+ const check = deps.existsSyncFn ?? existsSync30;
4664
4914
  const base = deps.baseUrl ?? import.meta.url;
4665
4915
  const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
4666
4916
  if (check(mjs)) return mjs;
@@ -4727,8 +4977,8 @@ function withSpinner(label, fn, deps) {
4727
4977
  // src/commands.doctor.gitleaks-version.ts
4728
4978
  init_color();
4729
4979
  import { execFileSync as execFileSync11 } from "node:child_process";
4730
- import { existsSync as existsSync30 } from "node:fs";
4731
- import { join as join36 } from "node:path";
4980
+ import { existsSync as existsSync31 } from "node:fs";
4981
+ import { join as join37 } from "node:path";
4732
4982
  init_config();
4733
4983
  var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
4734
4984
  var GITLEAKS_TIMEOUT_MS = 5e3;
@@ -4737,7 +4987,7 @@ function majorMinorOf(value) {
4737
4987
  return m === null ? null : [m[1], m[2]];
4738
4988
  }
4739
4989
  function readGitleaksVersion(run, tomlExists) {
4740
- const tomlPath = join36(repoHome(), ".gitleaks.toml");
4990
+ const tomlPath = join37(repoHome(), ".gitleaks.toml");
4741
4991
  const args = ["version"];
4742
4992
  if (tomlExists(tomlPath)) args.push("--config", tomlPath);
4743
4993
  try {
@@ -4749,7 +4999,7 @@ function readGitleaksVersion(run, tomlExists) {
4749
4999
  return null;
4750
5000
  }
4751
5001
  }
4752
- function reportGitleaksVersionCheck(section2, run = execFileSync11, tomlExists = existsSync30) {
5002
+ function reportGitleaksVersionCheck(section2, run = execFileSync11, tomlExists = existsSync31) {
4753
5003
  const raw = readGitleaksVersion(run, tomlExists);
4754
5004
  if (raw === null) return;
4755
5005
  const local = majorMinorOf(raw);
@@ -4972,8 +5222,8 @@ function gatherDoctorSections(opts) {
4972
5222
  reportHostKeyAlignment(host);
4973
5223
  reportRepoState(host);
4974
5224
  const links = section("Shared links");
4975
- const mapPath = join37(repoHome(), "path-map.json");
4976
- const rawMap = existsSync31(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
5225
+ const mapPath = join38(repoHome(), "path-map.json");
5226
+ const rawMap = existsSync32(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
4977
5227
  const map = rawMap ?? { projects: {} };
4978
5228
  reportSharedLinks(links, map);
4979
5229
  reportDroppedNamesMigration(links);
@@ -5070,8 +5320,8 @@ function parseDoctorArgs(args) {
5070
5320
  // src/commands.drop-session.ts
5071
5321
  init_config();
5072
5322
  import { execFileSync as execFileSync16 } from "node:child_process";
5073
- import { existsSync as existsSync33, readdirSync as readdirSync12, statSync as statSync9 } from "node:fs";
5074
- import { join as join39, relative as relative4 } from "node:path";
5323
+ import { existsSync as existsSync34, readdirSync as readdirSync13, statSync as statSync9 } from "node:fs";
5324
+ import { join as join40, relative as relative6 } from "node:path";
5075
5325
 
5076
5326
  // src/commands.drop-session.git.ts
5077
5327
  import { execFileSync as execFileSync15 } from "node:child_process";
@@ -5113,8 +5363,8 @@ function isInIndex(rel, repo) {
5113
5363
  init_config();
5114
5364
  init_utils();
5115
5365
  init_utils_json();
5116
- import { existsSync as existsSync32 } from "node:fs";
5117
- import { join as join38 } from "node:path";
5366
+ import { existsSync as existsSync33 } from "node:fs";
5367
+ import { join as join39 } from "node:path";
5118
5368
  var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
5119
5369
  function reportScrubHint(id, matches) {
5120
5370
  const live = resolveLiveTranscript2(id, matches);
@@ -5130,8 +5380,8 @@ function reportScrubHint(id, matches) {
5130
5380
  }
5131
5381
  function resolveLiveTranscript2(id, matches) {
5132
5382
  try {
5133
- const mapPath = join38(repoHome(), "path-map.json");
5134
- if (!existsSync32(mapPath)) return null;
5383
+ const mapPath = join39(repoHome(), "path-map.json");
5384
+ if (!existsSync33(mapPath)) return null;
5135
5385
  const projects = readJson(mapPath).projects;
5136
5386
  const claude = claudeHome();
5137
5387
  for (const rel of matches) {
@@ -5139,8 +5389,8 @@ function resolveLiveTranscript2(id, matches) {
5139
5389
  if (logical === void 0) continue;
5140
5390
  const abs = projects[logical]?.[HOST];
5141
5391
  if (abs === void 0) continue;
5142
- const live = join38(claude, "projects", encodePath(abs), `${id}.jsonl`);
5143
- if (existsSync32(live)) return live;
5392
+ const live = join39(claude, "projects", encodePath(abs), `${id}.jsonl`);
5393
+ if (existsSync33(live)) return live;
5144
5394
  }
5145
5395
  return null;
5146
5396
  } catch {
@@ -5156,12 +5406,12 @@ function cmdDropSession(id) {
5156
5406
  process.exit(1);
5157
5407
  }
5158
5408
  const repo = repoHome();
5159
- if (!existsSync33(repo)) die(`repo not cloned at ${repo}`);
5409
+ if (!existsSync34(repo)) die(`repo not cloned at ${repo}`);
5160
5410
  const handle = acquireLock("drop-session");
5161
5411
  if (handle === null) process.exit(0);
5162
5412
  try {
5163
- const repoProjects = join39(repo, "shared", "projects");
5164
- if (!existsSync33(repoProjects)) {
5413
+ const repoProjects = join40(repo, "shared", "projects");
5414
+ if (!existsSync34(repoProjects)) {
5165
5415
  throw new NomadFatal(`no staged session matches ${id}`);
5166
5416
  }
5167
5417
  const matches = collectMatches(repoProjects, id, repo);
@@ -5183,14 +5433,14 @@ function cmdDropSession(id) {
5183
5433
  }
5184
5434
  function collectMatches(repoProjects, id, repo) {
5185
5435
  const matches = [];
5186
- for (const logical of readdirSync12(repoProjects)) {
5187
- const candidate = join39(repoProjects, logical, `${id}.jsonl`);
5188
- if (existsSync33(candidate)) {
5189
- matches.push(relative4(repo, candidate));
5190
- }
5191
- const dir = join39(repoProjects, logical, id);
5192
- if (existsSync33(dir) && statSync9(dir).isDirectory()) {
5193
- const dirRel = relative4(repo, dir);
5436
+ for (const logical of readdirSync13(repoProjects)) {
5437
+ const candidate = join40(repoProjects, logical, `${id}.jsonl`);
5438
+ if (existsSync34(candidate)) {
5439
+ matches.push(relative6(repo, candidate));
5440
+ }
5441
+ const dir = join40(repoProjects, logical, id);
5442
+ if (existsSync34(dir) && statSync9(dir).isDirectory()) {
5443
+ const dirRel = relative6(repo, dir);
5194
5444
  const staged = expandStagedDir(dirRel, repo);
5195
5445
  if (staged.length > 0) matches.push(...staged);
5196
5446
  else matches.push(dirRel);
@@ -5232,7 +5482,7 @@ init_color();
5232
5482
 
5233
5483
  // src/summary.ts
5234
5484
  init_utils();
5235
- function summaryText(verb, unmapped, collisions = 0, extrasSkipped = 0) {
5485
+ function summaryText(verb, unmapped, collisions = 0, extrasSkipped = 0, localOnly = 0) {
5236
5486
  const extras = extrasSkipped > 0 ? `, ${extrasSkipped} extras skipped` : "";
5237
5487
  if (verb === "push") {
5238
5488
  if (unmapped === 0 && collisions === 0 && extrasSkipped === 0) {
@@ -5241,16 +5491,17 @@ function summaryText(verb, unmapped, collisions = 0, extrasSkipped = 0) {
5241
5491
  const base = `summary: ${unmapped} unmapped on push, ${collisions} collisions`;
5242
5492
  return { text: `${base}${extras} (run nomad doctor to list)`, clean: false };
5243
5493
  }
5244
- if (unmapped === 0 && extrasSkipped === 0) {
5494
+ if (unmapped === 0 && extrasSkipped === 0 && localOnly === 0) {
5245
5495
  return { text: "summary: clean", clean: true };
5246
5496
  }
5497
+ const localOnlyPhrase = localOnly > 0 ? `, ${localOnly} local-only present (push to reconcile)` : "";
5247
5498
  return {
5248
- text: `summary: ${unmapped} unmapped on ${verb}${extras} (run nomad doctor to list)`,
5499
+ text: `summary: ${unmapped} unmapped on ${verb}${extras} (run nomad doctor to list)${localOnlyPhrase}`,
5249
5500
  clean: false
5250
5501
  };
5251
5502
  }
5252
- function summaryRow(verb, unmapped, collisions = 0, extrasSkipped = 0) {
5253
- const { text } = summaryText(verb, unmapped, collisions, extrasSkipped);
5503
+ function summaryRow(verb, unmapped, collisions = 0, extrasSkipped = 0, localOnly = 0) {
5504
+ const { text } = summaryText(verb, unmapped, collisions, extrasSkipped, localOnly);
5254
5505
  return text.replace(/^summary: /, "");
5255
5506
  }
5256
5507
 
@@ -5264,11 +5515,17 @@ function buildSettingsSection(label) {
5264
5515
  addItem(s, `${green(okGlyph)} settings.json (base + ${label})`);
5265
5516
  return s;
5266
5517
  }
5267
- function buildSessionsSection(items, unmapped) {
5518
+ function buildSessionsSection(items, unmapped, localOnly = 0) {
5268
5519
  const s = section("Sessions");
5269
5520
  for (const logical of items) addItem(s, `${green(okGlyph)} ${logical}`);
5270
5521
  const skip = collapsedSkipRow(unmapped, "not in path-map (run nomad doctor to list)");
5271
5522
  if (skip !== null) addItem(s, skip);
5523
+ if (localOnly > 0) {
5524
+ addItem(
5525
+ s,
5526
+ `${yellow(warnGlyph)} ${localOnly} local-only present, not in repo (push to reconcile)`
5527
+ );
5528
+ }
5272
5529
  return s;
5273
5530
  }
5274
5531
  function buildExtrasSection(items, extrasSkipped) {
@@ -5322,167 +5579,12 @@ init_config();
5322
5579
  init_config();
5323
5580
  import { existsSync as existsSync36, statSync as statSync10 } from "node:fs";
5324
5581
  import { join as join43 } from "node:path";
5325
-
5326
- // src/extras-sync.core.ts
5327
- init_config();
5328
- import { cpSync as cpSync6, existsSync as existsSync34, lstatSync as lstatSync9, readdirSync as readdirSync13, rmSync as rmSync11 } from "node:fs";
5329
- import { basename, join as join40 } from "node:path";
5330
- init_utils();
5331
- init_utils_json();
5332
- function loadValidatedExtras(opts) {
5333
- const repo = repoHome();
5334
- const mapPath = join40(repo, "path-map.json");
5335
- const repoExtras = join40(repo, "shared", "extras");
5336
- if (!existsSync34(mapPath) || opts.requireRepoExtras === true && !existsSync34(repoExtras)) {
5337
- if (opts.missingMsg !== void 0) log(opts.missingMsg);
5338
- return null;
5339
- }
5340
- const map = readPathMap(mapPath);
5341
- const extrasMap = map.extras ?? {};
5342
- if (Object.keys(extrasMap).length === 0) return null;
5343
- for (const logical of Object.keys(extrasMap)) {
5344
- assertSafeLogical(logical);
5345
- const localRoot = map.projects[logical]?.[HOST];
5346
- if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
5347
- }
5348
- return { map, extrasMap };
5349
- }
5350
- function* eachExtrasTarget(v, counts) {
5351
- const whitelist = SUPPORTED_EXTRAS;
5352
- for (const [logical, dirnames] of Object.entries(v.extrasMap)) {
5353
- const localRoot = v.map.projects[logical]?.[HOST];
5354
- if (!localRoot || localRoot === "TBD") {
5355
- counts.unmapped++;
5356
- continue;
5357
- }
5358
- for (const dirname10 of dirnames) {
5359
- if (!whitelist.includes(dirname10)) {
5360
- counts.skipped++;
5361
- continue;
5362
- }
5363
- yield { logical, localRoot, dirname: dirname10 };
5364
- }
5365
- }
5366
- }
5367
- function stripCollidingDstSymlinks(src, dst, isExcluded) {
5368
- if (!existsSync34(dst)) return;
5369
- for (const name of readdirSync13(src)) {
5370
- if (isExcluded(name)) continue;
5371
- const dstPath = join40(dst, name);
5372
- const dstStat = lstatSync9(dstPath, { throwIfNoEntry: false });
5373
- if (dstStat === void 0) continue;
5374
- if (dstStat.isSymbolicLink()) {
5375
- rmSync11(dstPath, { recursive: true, force: true });
5376
- } else if (dstStat.isDirectory() && lstatSync9(join40(src, name)).isDirectory()) {
5377
- stripCollidingDstSymlinks(join40(src, name), dstPath, isExcluded);
5378
- }
5379
- }
5380
- }
5381
- function copyExtrasOverlayFiltered(src, dst, blockSet) {
5382
- stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
5383
- try {
5384
- cpSync6(src, dst, {
5385
- recursive: true,
5386
- force: true,
5387
- verbatimSymlinks: true,
5388
- filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry))
5389
- });
5390
- } catch (err) {
5391
- const e = err;
5392
- if (e.code === "EINVAL" || e.code === "ENOTEMPTY" || e.code === "ERR_FS_CP_NON_DIR_TO_DIR" || e.code === "ERR_FS_CP_DIR_TO_NON_DIR") {
5393
- throw new NomadFatal(
5394
- `copyExtrasOverlayFiltered: type collision copying ${JSON.stringify(src)} -> ${JSON.stringify(dst)} (${e.path ?? "unknown path"}): a file/directory type changed upstream; run nomad pull --force-remote to recover`
5395
- );
5396
- }
5397
- throw err;
5398
- }
5399
- }
5400
- function copyExtras(src, dst) {
5401
- rmSync11(dst, { recursive: true, force: true });
5402
- cpSync6(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
5403
- }
5404
- function extrasDenySet(dirname10) {
5405
- return dirname10 === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
5406
- }
5407
- function copyExtrasFiltered(src, dst, blockSet) {
5408
- rmSync11(dst, { recursive: true, force: true });
5409
- cpSync6(src, dst, {
5410
- recursive: true,
5411
- force: true,
5412
- verbatimSymlinks: true,
5413
- filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry))
5414
- });
5415
- }
5416
- function prunePreservingDenied(src, dst, blockSet) {
5417
- for (const name of readdirSync13(dst)) {
5418
- if (isDeniedName(blockSet, name)) continue;
5419
- const dstPath = join40(dst, name);
5420
- const srcStat = lstatSync9(join40(src, name), { throwIfNoEntry: false });
5421
- if (srcStat === void 0) {
5422
- rmSync11(dstPath, { recursive: true, force: true });
5423
- continue;
5424
- }
5425
- const dstStat = lstatSync9(dstPath);
5426
- if (srcStat.isDirectory() && dstStat.isDirectory()) {
5427
- prunePreservingDenied(join40(src, name), dstPath, blockSet);
5428
- } else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
5429
- rmSync11(dstPath, { recursive: true, force: true });
5430
- }
5431
- }
5432
- }
5433
- function copyExtrasFilteredPreserving(src, dst, blockSet) {
5434
- const dstStat = lstatSync9(dst, { throwIfNoEntry: false });
5435
- if (dstStat !== void 0) {
5436
- if (dstStat.isDirectory()) prunePreservingDenied(src, dst, blockSet);
5437
- else rmSync11(dst, { recursive: true, force: true });
5438
- }
5439
- stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
5440
- cpSync6(src, dst, {
5441
- recursive: true,
5442
- force: true,
5443
- verbatimSymlinks: true,
5444
- filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry))
5445
- });
5446
- }
5447
- function prunePreservingBy(src, dst, isPreserved) {
5448
- for (const name of readdirSync13(dst)) {
5449
- if (isPreserved(name)) continue;
5450
- const dstPath = join40(dst, name);
5451
- const srcStat = lstatSync9(join40(src, name), { throwIfNoEntry: false });
5452
- if (srcStat === void 0) {
5453
- rmSync11(dstPath, { recursive: true, force: true });
5454
- continue;
5455
- }
5456
- const dstStat = lstatSync9(dstPath);
5457
- if (srcStat.isDirectory() && dstStat.isDirectory()) {
5458
- prunePreservingBy(join40(src, name), dstPath, isPreserved);
5459
- } else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
5460
- rmSync11(dstPath, { recursive: true, force: true });
5461
- }
5462
- }
5463
- }
5464
- function copyExtrasFilteredPreservingBy(src, dst, isPreserved) {
5465
- const dstStat = lstatSync9(dst, { throwIfNoEntry: false });
5466
- if (dstStat !== void 0) {
5467
- if (dstStat.isDirectory()) prunePreservingBy(src, dst, isPreserved);
5468
- else rmSync11(dst, { recursive: true, force: true });
5469
- }
5470
- stripCollidingDstSymlinks(src, dst, isPreserved);
5471
- cpSync6(src, dst, {
5472
- recursive: true,
5473
- force: true,
5474
- verbatimSymlinks: true,
5475
- filter: (srcEntry) => srcEntry === src || !isPreserved(basename(srcEntry))
5476
- });
5477
- }
5478
-
5479
- // src/extras-sync.ts
5480
5582
  init_utils();
5481
5583
  init_utils_json();
5482
5584
 
5483
5585
  // src/extras-sync.remap.ts
5484
5586
  init_config();
5485
- import { existsSync as existsSync35, mkdirSync as mkdirSync8, readdirSync as readdirSync14, realpathSync as realpathSync4, rmSync as rmSync12 } from "node:fs";
5587
+ import { existsSync as existsSync35, mkdirSync as mkdirSync8, readdirSync as readdirSync14, readFileSync as readFileSync16, realpathSync as realpathSync4, rmSync as rmSync12 } from "node:fs";
5486
5588
  import { dirname as dirname9, join as join42, sep as sep7 } from "node:path";
5487
5589
 
5488
5590
  // src/extras-sync.planning-diff.ts
@@ -5611,24 +5713,58 @@ function deletePlanningTarget(target, planningRoot, repoCounterpart) {
5611
5713
  rmSync12(target, { recursive: true, force: true });
5612
5714
  pruneEmptyAncestors(target, planningRoot);
5613
5715
  }
5716
+ function localDivergesFromPreDelete(target, pre, repoRel, repo) {
5717
+ if (!existsSync35(target)) return false;
5718
+ try {
5719
+ const preBlob = gitCaptureBuffer(["show", `${pre}:${repoRel}`], repo);
5720
+ return !readFileSync16(target).equals(preBlob);
5721
+ } catch {
5722
+ return true;
5723
+ }
5724
+ }
5725
+ function planningDiffArgs(pre, post, logical) {
5726
+ return ["diff", "--name-status", "-z", pre, post, "--", `shared/extras/${logical}/.planning/`];
5727
+ }
5728
+ function deletePairsFor(t, raw) {
5729
+ return planningDeleteTargets({ raw, logical: t.logical, localRoot: t.localRoot }).map(
5730
+ (target) => {
5731
+ const relToLocal = target.slice(t.localRoot.length + sep7.length);
5732
+ return {
5733
+ target,
5734
+ relToLocal,
5735
+ repoRel: `shared/extras/${t.logical}/${relToLocal.split(sep7).join("/")}`
5736
+ };
5737
+ }
5738
+ );
5739
+ }
5740
+ function keptDeleteWarnLine(logical, relToLocal) {
5741
+ return `keeping locally-edited ${relToLocal} in ${logical}: deleted upstream but changed locally (push to reconcile; your copy is backed up)`;
5742
+ }
5743
+ function keptDeletePreview(v, prePostHeads, repo) {
5744
+ const kept = [];
5745
+ for (const t of eachExtrasTarget(v, { unmapped: 0, skipped: 0 })) {
5746
+ if (t.dirname !== ".planning") continue;
5747
+ let raw;
5748
+ try {
5749
+ raw = gitCaptureRaw(planningDiffArgs(prePostHeads.pre, prePostHeads.post, t.logical), repo);
5750
+ } catch {
5751
+ continue;
5752
+ }
5753
+ for (const { target, relToLocal, repoRel } of deletePairsFor(t, raw)) {
5754
+ if (localDivergesFromPreDelete(target, prePostHeads.pre, repoRel, repo)) {
5755
+ kept.push({ logical: t.logical, relToLocal });
5756
+ }
5757
+ }
5758
+ }
5759
+ return kept;
5760
+ }
5614
5761
  function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
5615
5762
  const repoExtras = join42(repo, "shared", "extras");
5616
5763
  for (const t of eachExtrasTarget(v, { unmapped: 0, skipped: 0 })) {
5617
5764
  if (t.dirname !== ".planning") continue;
5618
5765
  let raw;
5619
5766
  try {
5620
- raw = gitCaptureRaw(
5621
- [
5622
- "diff",
5623
- "--name-status",
5624
- "-z",
5625
- prePostHeads.pre,
5626
- prePostHeads.post,
5627
- "--",
5628
- `shared/extras/${t.logical}/.planning/`
5629
- ],
5630
- repo
5631
- );
5767
+ raw = gitCaptureRaw(planningDiffArgs(prePostHeads.pre, prePostHeads.post, t.logical), repo);
5632
5768
  } catch (err) {
5633
5769
  const e = err;
5634
5770
  if (e.stderr) process.stderr.write(e.stderr);
@@ -5636,12 +5772,15 @@ function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
5636
5772
  `git diff failed while propagating .planning deletes for ${t.logical}; run nomad pull --force-remote to recover`
5637
5773
  );
5638
5774
  }
5639
- const targets = planningDeleteTargets({ raw, logical: t.logical, localRoot: t.localRoot });
5640
- if (targets.length === 0) continue;
5775
+ const pairs = deletePairsFor(t, raw);
5776
+ if (pairs.length === 0) continue;
5641
5777
  backupExtrasWrite(join42(t.localRoot, t.dirname), ts, t.localRoot);
5642
5778
  const planningRoot = join42(t.localRoot, ".planning");
5643
- for (const target of targets) {
5644
- const relToLocal = target.slice(t.localRoot.length + sep7.length);
5779
+ for (const { target, relToLocal, repoRel } of pairs) {
5780
+ if (localDivergesFromPreDelete(target, prePostHeads.pre, repoRel, repo)) {
5781
+ warn(keptDeleteWarnLine(t.logical, relToLocal));
5782
+ continue;
5783
+ }
5645
5784
  deletePlanningTarget(target, planningRoot, join42(repoExtras, t.logical, relToLocal));
5646
5785
  }
5647
5786
  }
@@ -5666,7 +5805,7 @@ function remapExtrasPush(ts, opts = {}) {
5666
5805
  // Repo-only files survive; local edits propagate (overlay overwrites).
5667
5806
  // The filter prevents ALWAYS_NEVER_SYNC files from landing in the repo
5668
5807
  // working tree before the allow-list gate fires, eliminating the
5669
- // "residue wedges repeat push" regression (WR-02). The allow-list gate
5808
+ // "residue wedges repeat push" regression. The allow-list gate
5670
5809
  // (enforceAllowList / blockSetFor in commands.push.allowlist.ts)
5671
5810
  // remains the hard security boundary.
5672
5811
  // All others: copyExtrasFiltered with per-extra denylist.
@@ -5690,23 +5829,30 @@ function remapExtrasPull(ts, opts = {}) {
5690
5829
  src: join42(repo, "shared", "extras", logical, dirname10),
5691
5830
  dst: join42(localRoot, dirname10)
5692
5831
  }),
5693
- // Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
5832
+ // Snapshot the host-side dst BEFORE the copy step touches it. Anchor on
5694
5833
  // localRoot so the backup tree mirrors the project layout.
5695
5834
  (dst, localRoot) => backupExtrasWrite(dst, ts, localRoot),
5696
5835
  // Pull routing per extra type:
5697
5836
  // `.claude`: copyExtrasFilteredPreserving preserves host-local deny-set
5698
5837
  // files (e.g. settings.local.json) while mirror-pruning synced entries.
5699
- // `.planning`: copyExtrasOverlayFiltered (no rmSync; deny-set filtered)
5700
- // keeps local-only files; the delete pass below propagates upstream
5701
- // removals via the git-diff D set. The filter is defense-in-depth
5702
- // against a repo poisoned out-of-band.
5703
- // All others: copyExtras (exact mirror; rarely carry host-local files).
5838
+ // `.planning`: copyExtrasOverlaySkipDiverged (no rmSync; deny-set filtered)
5839
+ // keeps local-only files AND skips any file whose local copy diverges
5840
+ // from the repo copy (content hash differs), so a local hand-edit wins
5841
+ // on conflict; the delete pass below still propagates
5842
+ // upstream removals via the git-diff D set. The filter is defense-in-
5843
+ // depth against a repo poisoned out-of-band.
5844
+ // All others (a single root-level file, e.g. CLAUDE.md):
5845
+ // copyExtrasFileSkipDiverged keeps a locally-edited file rather than
5846
+ // clobbering it, so the divergence WARN's keep-local promise holds for
5847
+ // every extra, not just `.planning`.
5704
5848
  (src, dst, dirname10) => {
5705
5849
  if (dirname10 === ".claude")
5706
5850
  return copyExtrasFilteredPreserving(src, dst, extrasDenySet(dirname10));
5707
- if (dirname10 === ".planning")
5708
- return copyExtrasOverlayFiltered(src, dst, extrasDenySet(dirname10));
5709
- return copyExtras(src, dst);
5851
+ if (dirname10 === ".planning") {
5852
+ const divergedSet = new Set(listDivergingModified(dst, src));
5853
+ return copyExtrasOverlaySkipDiverged(src, dst, extrasDenySet(dirname10), divergedSet);
5854
+ }
5855
+ return copyExtrasFileSkipDiverged(src, dst);
5710
5856
  }
5711
5857
  );
5712
5858
  if (!dryRun && prePostHeads !== void 0) {
@@ -5721,39 +5867,46 @@ function divergenceWarnLine(o) {
5721
5867
  const name = o.isDir ? `${o.dirname}/` : o.dirname;
5722
5868
  const one = o.count === 1;
5723
5869
  const fileCount = one ? "1 file" : `${o.count} files`;
5724
- const them = one ? "it" : "them";
5725
5870
  const yours = one ? "your current file is" : "your current files are";
5726
- return `local ${kind} ${name} in repo ${o.logical} differs from the synced copy in ${fileCount}; the next pull step will overwrite ${them} with the synced version (${yours} backed up to ${o.projectBackupRoot}/)`;
5871
+ return `local ${kind} ${name} in repo ${o.logical} differs from the synced copy in ${fileCount}; the next pull step will keep your local copy (push to reconcile; ${yours} backed up to ${o.projectBackupRoot}/)`;
5727
5872
  }
5728
- function divergenceCheckExtras(ts) {
5873
+ function divergenceCheckExtras(ts, prePostHeads) {
5729
5874
  const v = loadValidatedExtras({});
5730
- if (v === null) return;
5875
+ if (v === null) return 0;
5731
5876
  const counts = { unmapped: 0, skipped: 0 };
5732
5877
  const backupRoot = join43(backupBase(), ts, "extras");
5733
5878
  const repo = repoHome();
5879
+ let divergedCount = 0;
5734
5880
  for (const { logical, localRoot, dirname: dirname10 } of eachExtrasTarget(v, counts)) {
5735
5881
  const local = join43(localRoot, dirname10);
5736
5882
  const repoEntry = join43(repo, "shared", "extras", logical, dirname10);
5737
5883
  if (!existsSync36(local) || !existsSync36(repoEntry)) continue;
5738
- const diff = listDivergingFiles(local, repoEntry);
5739
- if (diff.length === 0) continue;
5884
+ const modified = listDivergingModified(local, repoEntry);
5885
+ if (modified.length === 0) continue;
5886
+ divergedCount += modified.length;
5740
5887
  const projectBackupRoot = join43(backupRoot, encodePath(localRoot));
5741
5888
  warn(
5742
5889
  divergenceWarnLine({
5743
5890
  dirname: dirname10,
5744
5891
  logical,
5745
5892
  isDir: statSync10(local).isDirectory(),
5746
- count: diff.length,
5893
+ count: modified.length,
5747
5894
  projectBackupRoot
5748
5895
  })
5749
5896
  );
5750
- for (const f of diff) warn(` ${f}`);
5897
+ for (const f of modified) warn(` ${f}`);
5751
5898
  }
5899
+ if (prePostHeads !== void 0) {
5900
+ for (const { logical, relToLocal } of keptDeletePreview(v, prePostHeads, repo)) {
5901
+ warn(keptDeleteWarnLine(logical, relToLocal));
5902
+ }
5903
+ }
5904
+ return divergedCount;
5752
5905
  }
5753
5906
 
5754
5907
  // src/skills-sync.ts
5755
5908
  init_config();
5756
- import { existsSync as existsSync37, lstatSync as lstatSync10, mkdirSync as mkdirSync9, readdirSync as readdirSync15, rmSync as rmSync13 } from "node:fs";
5909
+ import { existsSync as existsSync37, lstatSync as lstatSync11, mkdirSync as mkdirSync9, readdirSync as readdirSync15, rmSync as rmSync13 } from "node:fs";
5757
5910
  import { join as join44 } from "node:path";
5758
5911
  init_utils_fs();
5759
5912
  function isGsdOwned(name) {
@@ -5777,7 +5930,7 @@ function syncSkillsPull(ts) {
5777
5930
  const sharedSkills = join44(repoHome(), "shared", "skills");
5778
5931
  if (!existsSync37(sharedSkills)) return;
5779
5932
  const localSkills = join44(claudeHome(), "skills");
5780
- const dstStat = lstatSync10(localSkills, { throwIfNoEntry: false });
5933
+ const dstStat = lstatSync11(localSkills, { throwIfNoEntry: false });
5781
5934
  if (dstStat?.isSymbolicLink() === true) {
5782
5935
  backupBeforeWrite(localSkills, ts);
5783
5936
  rmSync13(localSkills, { recursive: true, force: true });
@@ -5787,7 +5940,7 @@ function syncSkillsPull(ts) {
5787
5940
  }
5788
5941
  function syncSkillsPush() {
5789
5942
  const localSkills = join44(claudeHome(), "skills");
5790
- const stat = lstatSync10(localSkills, { throwIfNoEntry: false });
5943
+ const stat = lstatSync11(localSkills, { throwIfNoEntry: false });
5791
5944
  if (stat === void 0) return;
5792
5945
  if (stat.isSymbolicLink()) return;
5793
5946
  const sharedSkills = join44(repoHome(), "shared", "skills");
@@ -6153,10 +6306,14 @@ function computePreview(ts, map, verb = "pull") {
6153
6306
  dryRun: true,
6154
6307
  onPreview: (e) => addItem(sessions, formatSessionRow(e))
6155
6308
  });
6309
+ const localOnly = scanLocalOnly();
6310
+ if (localOnly > 0) {
6311
+ addItem(sessions, `${localOnly} local-only present, not in repo (push to reconcile)`);
6312
+ }
6156
6313
  const summary = section("Summary");
6157
- addItem(summary, summaryRow(verb, remapResult.unmapped));
6314
+ addItem(summary, summaryRow(verb, remapResult.unmapped, 0, 0, localOnly));
6158
6315
  renderTree([links, settingsSection, sessions, summary]);
6159
- return { unmapped: remapResult.unmapped, collisions: 0 };
6316
+ return { unmapped: remapResult.unmapped, collisions: 0, localOnly };
6160
6317
  }
6161
6318
 
6162
6319
  // src/commands.pull.ts
@@ -6305,23 +6462,33 @@ function capturePrePostHeads(repo, rebase) {
6305
6462
  if (pre === void 0 || post === void 0) return void 0;
6306
6463
  return { pre, post };
6307
6464
  }
6308
- function applyWetPull(ts, map, prePostHeads) {
6465
+ function buildWetPullSections(ts, map, prePostHeads) {
6309
6466
  applySharedLinks(ts, map);
6310
6467
  const { label } = regenerateSettings(ts);
6311
6468
  syncSkillsPull(ts);
6312
6469
  const remapResult = withSpinner("Syncing sessions", () => remapPull(ts));
6313
6470
  const extrasResult = remapExtrasPull(ts, { prePostHeads });
6471
+ const localOnly = scanLocalOnly();
6314
6472
  const summary = section("Summary");
6315
6473
  addItem(
6316
6474
  summary,
6317
- summaryRow("pull", remapResult.unmapped + extrasResult.unmapped, 0, extrasResult.skipped)
6475
+ summaryRow(
6476
+ "pull",
6477
+ remapResult.unmapped + extrasResult.unmapped,
6478
+ 0,
6479
+ extrasResult.skipped,
6480
+ localOnly
6481
+ )
6318
6482
  );
6319
- renderTree([
6320
- buildSettingsSection(label),
6321
- buildSessionsSection(remapResult.pulled, remapResult.unmapped),
6322
- buildExtrasSection(extrasResult.pulled, extrasResult.skipped),
6323
- summary
6324
- ]);
6483
+ return {
6484
+ sections: [
6485
+ buildSettingsSection(label),
6486
+ buildSessionsSection(remapResult.pulled, remapResult.unmapped, localOnly),
6487
+ buildExtrasSection(extrasResult.pulled, extrasResult.skipped),
6488
+ summary
6489
+ ],
6490
+ localOnly
6491
+ };
6325
6492
  }
6326
6493
  function handleWedge(repo, forceRemote) {
6327
6494
  const wedge = classifyWedge(repo);
@@ -6341,11 +6508,40 @@ function handleWedge(repo, forceRemote) {
6341
6508
  const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
6342
6509
  die(wedgeMarkerRunbookText(state));
6343
6510
  }
6344
- function cmdPull(opts = {}) {
6511
+ function runPullCore(opts = {}) {
6345
6512
  const dryRun = opts.dryRun === true;
6346
6513
  const forceRemote = opts.forceRemote === true;
6347
6514
  const repo = repoHome();
6348
6515
  const backup = backupBase();
6516
+ const ts = freshBackupTs(backup);
6517
+ handleWedge(repo, forceRemote);
6518
+ if (!dryRun) {
6519
+ const backupRoot = join46(backup, ts);
6520
+ try {
6521
+ mkdirSync10(backupRoot, { recursive: true });
6522
+ } catch (err) {
6523
+ die(`could not create backup dir: ${err.message}`);
6524
+ }
6525
+ }
6526
+ log(
6527
+ dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
6528
+ );
6529
+ const prePostHeads = capturePrePostHeads(repo, () => {
6530
+ gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
6531
+ });
6532
+ const mapPath = join46(repo, "path-map.json");
6533
+ const map = existsSync39(mapPath) ? readPathMap(mapPath) : { projects: {} };
6534
+ const divergedKeptLocal = divergenceCheckExtras(ts, dryRun ? prePostHeads : void 0);
6535
+ if (dryRun) {
6536
+ computePreview(ts, map, "pull");
6537
+ log("dry-run complete; no mutation");
6538
+ return { tag: "dry" };
6539
+ }
6540
+ const { sections, localOnly } = buildWetPullSections(ts, map, prePostHeads);
6541
+ return { tag: "wet", sections, localOnly, divergedKeptLocal };
6542
+ }
6543
+ function cmdPull(opts = {}) {
6544
+ const repo = repoHome();
6349
6545
  if (!existsSync39(repo)) die(`repo not cloned at ${repo}`);
6350
6546
  if (!existsSync39(join46(repo, "shared", "settings.base.json"))) {
6351
6547
  die("repo not initialized; run 'nomad init' to scaffold");
@@ -6353,31 +6549,8 @@ function cmdPull(opts = {}) {
6353
6549
  const handle = acquireLock("pull");
6354
6550
  if (handle === null) process.exit(0);
6355
6551
  try {
6356
- const ts = freshBackupTs(backup);
6357
- handleWedge(repo, forceRemote);
6358
- if (!dryRun) {
6359
- const backupRoot = join46(backup, ts);
6360
- try {
6361
- mkdirSync10(backupRoot, { recursive: true });
6362
- } catch (err) {
6363
- die(`could not create backup dir: ${err.message}`);
6364
- }
6365
- }
6366
- log(
6367
- dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
6368
- );
6369
- const prePostHeads = capturePrePostHeads(repo, () => {
6370
- gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
6371
- });
6372
- const mapPath = join46(repo, "path-map.json");
6373
- const map = existsSync39(mapPath) ? readPathMap(mapPath) : { projects: {} };
6374
- divergenceCheckExtras(ts);
6375
- if (dryRun) {
6376
- computePreview(ts, map, "pull");
6377
- log("dry-run complete; no mutation");
6378
- } else {
6379
- applyWetPull(ts, map, prePostHeads);
6380
- }
6552
+ const result = runPullCore(opts);
6553
+ if (result.tag === "wet") renderTree(result.sections);
6381
6554
  } catch (err) {
6382
6555
  if (err instanceof NomadFatal) {
6383
6556
  fail(err.message);
@@ -6586,12 +6759,12 @@ function reportSettingsAheadDrift(repo) {
6586
6759
  // src/commands.push.guards.ts
6587
6760
  init_push_checks();
6588
6761
  init_utils();
6589
- import { join as join49, relative as relative5 } from "node:path";
6762
+ import { join as join49, relative as relative7 } from "node:path";
6590
6763
  function guardGitlinks(repo) {
6591
6764
  const gitlinks = findGitlinks(join49(repo, "shared"));
6592
6765
  if (gitlinks.length === 0) return;
6593
6766
  for (const p of gitlinks) {
6594
- const rel = relative5(repo, p);
6767
+ const rel = relative7(repo, p);
6595
6768
  fail(`gitlink: ${rel} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`);
6596
6769
  }
6597
6770
  const noun = gitlinks.length === 1 ? "entry" : "entries";
@@ -6700,7 +6873,7 @@ init_config_sharedDirs_guard();
6700
6873
  import { randomBytes as randomBytes2 } from "node:crypto";
6701
6874
  import { copyFileSync, existsSync as existsSync42, mkdirSync as mkdirSync11, readdirSync as readdirSync16, rmSync as rmSync14 } from "node:fs";
6702
6875
  import { homedir as homedir5 } from "node:os";
6703
- import { join as join50, relative as relative6, sep as sep8 } from "node:path";
6876
+ import { join as join50, relative as relative8, sep as sep8 } from "node:path";
6704
6877
  init_push_leak_verdict();
6705
6878
  init_push_gitleaks();
6706
6879
  init_utils_fs();
@@ -6712,7 +6885,7 @@ function stageSessionDir(localDir, dstDir, changed) {
6712
6885
  const matching = [...changed].filter((p) => p.startsWith(prefix));
6713
6886
  if (matching.length === 0) return false;
6714
6887
  for (const src of matching) {
6715
- copyFileAtomic(src, join50(dstDir, relative6(localDir, src)));
6888
+ copyFileAtomic(src, join50(dstDir, relative8(localDir, src)));
6716
6889
  }
6717
6890
  return true;
6718
6891
  }
@@ -6802,7 +6975,7 @@ async function commitAndPush(st, ts, map, resolution, repo, newManifest) {
6802
6975
  if (staged.length === toDrop.length) {
6803
6976
  log("nothing to commit");
6804
6977
  renderNoScanTree(st);
6805
- return;
6978
+ return "nothing";
6806
6979
  }
6807
6980
  st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: true });
6808
6981
  let verdict = withSpinner("Scanning for secrets", () => scanPushVerdict(repo));
@@ -6818,6 +6991,7 @@ async function commitAndPush(st, ts, map, resolution, repo, newManifest) {
6818
6991
  warn(`could not write push manifest (next push will full-rescan): ${String(err)}`);
6819
6992
  }
6820
6993
  renderPushTree(st, verdict);
6994
+ return "pushed";
6821
6995
  }
6822
6996
  function runDryRunPreview(st, map, repo, selection) {
6823
6997
  st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: false });
@@ -6834,55 +7008,181 @@ function runDryRunPreview(st, map, repo, selection) {
6834
7008
  init_push_checks();
6835
7009
  init_utils();
6836
7010
  init_utils_fs();
6837
- async function cmdPush(opts = {}) {
7011
+ async function runPushCore(opts = {}) {
6838
7012
  const dryRun = opts.dryRun === true;
6839
7013
  const redactAll = opts.redactAll === true;
6840
7014
  const allowAll = opts.allowAll === true;
6841
7015
  const allowRule = opts.allowRule;
6842
7016
  const fullScan = opts.fullScan === true;
6843
- guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
6844
7017
  const repo = repoHome();
6845
7018
  const backup = backupBase();
7019
+ console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
7020
+ reportSettingsAheadDrift(repo);
7021
+ const scannerVersion = probeGitleaks();
7022
+ const configHash = computeConfigHash();
7023
+ const old = readManifest(manifestPath());
7024
+ const mapPath = join51(repo, "path-map.json");
7025
+ const { map, selection, newManifest } = loadSelectionForPush(
7026
+ mapPath,
7027
+ old,
7028
+ scannerVersion,
7029
+ configHash,
7030
+ fullScan
7031
+ );
7032
+ withSpinner("Rebasing onto origin", () => rebaseBeforePush(repo));
7033
+ const ts = freshBackupTs(backup);
7034
+ const remap = withSpinner("Syncing sessions", () => remapPush(ts, { dryRun, selection }));
7035
+ const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
7036
+ if (!dryRun) {
7037
+ syncSkillsPush();
7038
+ stripGsdHooksFromBase(repo, backup);
7039
+ }
7040
+ const st = { dryRun, remap, extras, globalConfig: [] };
7041
+ guardGitlinks(repo);
7042
+ const status = gitStatusPorcelainZ(repo, { untrackedAll: true });
7043
+ if (!dryRun && !status) {
7044
+ log("nothing to commit");
7045
+ renderNoScanTree(st);
7046
+ return { tag: "nothing" };
7047
+ }
7048
+ if (map === null) {
7049
+ if (dryRun) {
7050
+ runDryRunPreview(st, null, repo, selection);
7051
+ return { tag: "dry" };
7052
+ }
7053
+ return die("path-map.json missing, cannot enforce push allow-list");
7054
+ }
7055
+ if (status) enforceAllowList(status, map);
7056
+ if (dryRun) {
7057
+ runDryRunPreview(st, map, repo, selection);
7058
+ return { tag: "dry" };
7059
+ }
7060
+ const outcome = await commitAndPush(
7061
+ st,
7062
+ ts,
7063
+ map,
7064
+ { redactAll, allowAll, allowRule },
7065
+ repo,
7066
+ newManifest
7067
+ );
7068
+ return outcome === "nothing" ? { tag: "nothing" } : { tag: "pushed" };
7069
+ }
7070
+ async function cmdPush(opts = {}) {
7071
+ const dryRun = opts.dryRun === true;
7072
+ const redactAll = opts.redactAll === true;
7073
+ const allowAll = opts.allowAll === true;
7074
+ const allowRule = opts.allowRule;
7075
+ guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
7076
+ const repo = repoHome();
6846
7077
  if (!existsSync43(repo)) die(`repo not cloned at ${repo}`);
6847
7078
  const handle = acquireLock("push");
6848
7079
  if (handle === null) process.exit(0);
6849
7080
  try {
6850
- console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
6851
- reportSettingsAheadDrift(repo);
6852
- const scannerVersion = probeGitleaks();
6853
- const configHash = computeConfigHash();
6854
- const old = readManifest(manifestPath());
6855
- const mapPath = join51(repo, "path-map.json");
6856
- const { map, selection, newManifest } = loadSelectionForPush(
6857
- mapPath,
6858
- old,
6859
- scannerVersion,
6860
- configHash,
6861
- fullScan
6862
- );
6863
- withSpinner("Rebasing onto origin", () => rebaseBeforePush(repo));
6864
- const ts = freshBackupTs(backup);
6865
- const remap = withSpinner("Syncing sessions", () => remapPush(ts, { dryRun, selection }));
6866
- const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
6867
- if (!dryRun) {
6868
- syncSkillsPush();
6869
- stripGsdHooksFromBase(repo, backup);
6870
- }
6871
- const st = { dryRun, remap, extras, globalConfig: [] };
6872
- guardGitlinks(repo);
6873
- const status = gitStatusPorcelainZ(repo, { untrackedAll: true });
6874
- if (!dryRun && !status) {
6875
- log("nothing to commit");
6876
- renderNoScanTree(st);
6877
- return;
7081
+ await runPushCore(opts);
7082
+ } catch (err) {
7083
+ if (err instanceof NomadFatal) {
7084
+ fail(err.message);
7085
+ process.exitCode = 1;
7086
+ } else {
7087
+ throw err;
7088
+ }
7089
+ } finally {
7090
+ releaseLock(handle);
7091
+ }
7092
+ }
7093
+
7094
+ // src/commands.sync.ts
7095
+ import { existsSync as existsSync44 } from "node:fs";
7096
+ import { join as join52 } from "node:path";
7097
+ init_config();
7098
+ init_utils();
7099
+ init_utils_fs();
7100
+ init_utils_json();
7101
+ function pullHasNoSyncedItems(sections) {
7102
+ return sections.every(
7103
+ (s) => s.header === "Settings" || s.header === "Summary" || s.items.length === 0
7104
+ );
7105
+ }
7106
+ function isNoopSync(pull, pushOutcome) {
7107
+ if (!pushOutcome.ok) return false;
7108
+ if (pull.localOnly !== 0 || pull.divergedKeptLocal !== 0) return false;
7109
+ if (pushOutcome.result.tag !== "nothing") return false;
7110
+ return pullHasNoSyncedItems(pull.sections);
7111
+ }
7112
+ function pullPhrase(pull) {
7113
+ const summary = pull.sections.find((s) => s.header === "Summary");
7114
+ return summary?.items[0] ?? "applied";
7115
+ }
7116
+ function reconciledNotes(pull) {
7117
+ const notes = [];
7118
+ if (pull.divergedKeptLocal > 0) {
7119
+ notes.push(`${pull.divergedKeptLocal} diverged files kept local and pushed`);
7120
+ }
7121
+ if (pull.localOnly > 0) {
7122
+ notes.push(`${pull.localOnly} local-only sessions pushed`);
7123
+ }
7124
+ return notes;
7125
+ }
7126
+ function buildSyncSummarySection(pull, pushOutcome) {
7127
+ const s = section("Summary");
7128
+ if (!pushOutcome.ok) {
7129
+ addItem(s, `pull: applied, push: failed (${pushOutcome.message})`);
7130
+ return s;
7131
+ }
7132
+ addItem(s, `pull: ${pullPhrase(pull)}`);
7133
+ addItem(s, `push: ${pushOutcome.result.tag === "pushed" ? "pushed" : "nothing to push"}`);
7134
+ for (const note of reconciledNotes(pull)) addItem(s, note);
7135
+ return s;
7136
+ }
7137
+ function renderWetSync(pull, pushOutcome) {
7138
+ if (isNoopSync(pull, pushOutcome)) {
7139
+ ok("already in sync");
7140
+ return;
7141
+ }
7142
+ renderTree(pull.sections);
7143
+ renderTree([buildSyncSummarySection(pull, pushOutcome)]);
7144
+ }
7145
+ async function runSyncPushHalf() {
7146
+ try {
7147
+ const result = await runPushCore();
7148
+ return { ok: true, result };
7149
+ } catch (err) {
7150
+ if (err instanceof NomadFatal) {
7151
+ process.exitCode = 1;
7152
+ return { ok: false, message: err.message };
6878
7153
  }
6879
- if (map === null) {
6880
- if (dryRun) return runDryRunPreview(st, null, repo, selection);
6881
- return die("path-map.json missing, cannot enforce push allow-list");
7154
+ throw err;
7155
+ }
7156
+ }
7157
+ async function runSyncWet() {
7158
+ const pull = runPullCore();
7159
+ const pushOutcome = await runSyncPushHalf();
7160
+ renderWetSync(pull, pushOutcome);
7161
+ }
7162
+ async function runSyncDryRun(repo, backup) {
7163
+ const ts = freshBackupTs(backup);
7164
+ const mapPath = join52(repo, "path-map.json");
7165
+ const map = existsSync44(mapPath) ? readPathMap(mapPath) : { projects: {} };
7166
+ computePreview(ts, map, "pull");
7167
+ log("push preview below is computed against pre-pull state (a real sync pushes after pull)");
7168
+ await runPushCore({ dryRun: true });
7169
+ }
7170
+ async function cmdSync(opts = {}) {
7171
+ const dryRun = opts.dryRun === true;
7172
+ const repo = repoHome();
7173
+ const backup = backupBase();
7174
+ if (!existsSync44(repo)) die(`repo not cloned at ${repo}`);
7175
+ if (!existsSync44(join52(repo, "shared", "settings.base.json"))) {
7176
+ die("repo not initialized; run 'nomad init' to scaffold");
7177
+ }
7178
+ const handle = acquireLock("sync");
7179
+ if (handle === null) process.exit(0);
7180
+ try {
7181
+ if (dryRun) {
7182
+ await runSyncDryRun(repo, backup);
7183
+ } else {
7184
+ await runSyncWet();
6882
7185
  }
6883
- if (status) enforceAllowList(status, map);
6884
- if (dryRun) return runDryRunPreview(st, map, repo, selection);
6885
- await commitAndPush(st, ts, map, { redactAll, allowAll, allowRule }, repo, newManifest);
6886
7186
  } catch (err) {
6887
7187
  if (err instanceof NomadFatal) {
6888
7188
  fail(err.message);
@@ -6939,18 +7239,19 @@ init_config();
6939
7239
 
6940
7240
  // src/diff.ts
6941
7241
  init_config();
6942
- import { existsSync as existsSync44 } from "node:fs";
6943
- import { join as join52 } from "node:path";
7242
+ import { existsSync as existsSync45 } from "node:fs";
7243
+ import { join as join53 } from "node:path";
6944
7244
  init_utils();
6945
7245
  init_utils_fs();
6946
7246
  init_utils_json();
6947
7247
  function cmdDiff() {
6948
7248
  try {
6949
7249
  const repo = repoHome();
6950
- if (!existsSync44(repo)) die(`repo not cloned at ${repo}`);
7250
+ if (!existsSync45(repo)) die(`repo not cloned at ${repo}`);
6951
7251
  const ts = freshBackupTs(backupBase());
6952
- const mapPath = join52(repo, "path-map.json");
6953
- const map = existsSync44(mapPath) ? readPathMap(mapPath) : { projects: {} };
7252
+ const mapPath = join53(repo, "path-map.json");
7253
+ const map = existsSync45(mapPath) ? readPathMap(mapPath) : { projects: {} };
7254
+ divergenceCheckExtras(ts);
6954
7255
  computePreview(ts, map, "diff");
6955
7256
  } catch (err) {
6956
7257
  if (err instanceof NomadFatal) {
@@ -6964,8 +7265,8 @@ function cmdDiff() {
6964
7265
 
6965
7266
  // src/init.ts
6966
7267
  init_config();
6967
- import { existsSync as existsSync46, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6 } from "node:fs";
6968
- import { join as join54 } from "node:path";
7268
+ import { existsSync as existsSync47, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6 } from "node:fs";
7269
+ import { join as join55 } from "node:path";
6969
7270
 
6970
7271
  // src/init.gh-onboard.ts
6971
7272
  init_config();
@@ -7047,33 +7348,33 @@ init_config();
7047
7348
  init_utils();
7048
7349
  init_utils_fs();
7049
7350
  init_utils_json();
7050
- import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync45, rmSync as rmSync15, statSync as statSync12 } from "node:fs";
7051
- import { join as join53 } from "node:path";
7351
+ import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync46, rmSync as rmSync15, statSync as statSync12 } from "node:fs";
7352
+ import { join as join54 } from "node:path";
7052
7353
  function snapshotIntoShared(map) {
7053
7354
  const repo = repoHome();
7054
7355
  const claude = claudeHome();
7055
7356
  for (const name of allSharedLinks(map)) {
7056
- const src = join53(claude, name);
7057
- if (!existsSync45(src)) continue;
7058
- const dst = join53(repo, "shared", name);
7357
+ const src = join54(claude, name);
7358
+ if (!existsSync46(src)) continue;
7359
+ const dst = join54(repo, "shared", name);
7059
7360
  if (statSync12(src).isDirectory()) {
7060
- const gk = join53(dst, ".gitkeep");
7061
- if (existsSync45(gk)) rmSync15(gk);
7361
+ const gk = join54(dst, ".gitkeep");
7362
+ if (existsSync46(gk)) rmSync15(gk);
7062
7363
  cpSync7(src, dst, { recursive: true, force: false, errorOnExist: true });
7063
7364
  } else {
7064
7365
  copyFileSync2(src, dst);
7065
7366
  }
7066
7367
  log(`snapshotted shared/${name} from ${src}`);
7067
7368
  }
7068
- const userSettings = join53(claude, "settings.json");
7069
- if (existsSync45(userSettings)) {
7369
+ const userSettings = join54(claude, "settings.json");
7370
+ if (existsSync46(userSettings)) {
7070
7371
  let parsed;
7071
7372
  try {
7072
7373
  parsed = readJson(userSettings);
7073
7374
  } catch (err) {
7074
7375
  return die(`malformed ${userSettings}: ${err.message}`);
7075
7376
  }
7076
- const hostFile = join53(repo, "hosts", `${HOST}.json`);
7377
+ const hostFile = join54(repo, "hosts", `${HOST}.json`);
7077
7378
  writeJsonAtomic(hostFile, parsed);
7078
7379
  log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
7079
7380
  }
@@ -7086,14 +7387,14 @@ var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.cl
7086
7387
  var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
7087
7388
  function preflightConflict(repoHome2) {
7088
7389
  const candidates = [
7089
- join54(repoHome2, "shared", "settings.base.json"),
7090
- join54(repoHome2, "shared", "CLAUDE.md"),
7091
- join54(repoHome2, "path-map.json"),
7092
- join54(repoHome2, "hosts"),
7093
- join54(repoHome2, "shared")
7390
+ join55(repoHome2, "shared", "settings.base.json"),
7391
+ join55(repoHome2, "shared", "CLAUDE.md"),
7392
+ join55(repoHome2, "path-map.json"),
7393
+ join55(repoHome2, "hosts"),
7394
+ join55(repoHome2, "shared")
7094
7395
  ];
7095
7396
  for (const c of candidates) {
7096
- if (existsSync46(c)) return c;
7397
+ if (existsSync47(c)) return c;
7097
7398
  }
7098
7399
  return null;
7099
7400
  }
@@ -7111,25 +7412,25 @@ function cmdInit(opts = {}) {
7111
7412
  die(`already initialized; refusing to clobber ${conflict}`);
7112
7413
  }
7113
7414
  ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
7114
- mkdirSync12(join54(repo, "shared"), { recursive: true });
7115
- mkdirSync12(join54(repo, "hosts"), { recursive: true });
7415
+ mkdirSync12(join55(repo, "shared"), { recursive: true });
7416
+ mkdirSync12(join55(repo, "hosts"), { recursive: true });
7116
7417
  for (const name of SHARED_KEEP_DIRS) {
7117
- mkdirSync12(join54(repo, "shared", name), { recursive: true });
7418
+ mkdirSync12(join55(repo, "shared", name), { recursive: true });
7118
7419
  }
7119
- const userClaudeMd = join54(claude, "CLAUDE.md");
7120
- if (!snapshot || !existsSync46(userClaudeMd)) {
7121
- writeFileSync6(join54(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
7420
+ const userClaudeMd = join55(claude, "CLAUDE.md");
7421
+ if (!snapshot || !existsSync47(userClaudeMd)) {
7422
+ writeFileSync6(join55(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
7122
7423
  item("created shared/CLAUDE.md");
7123
7424
  }
7124
7425
  for (const name of SHARED_KEEP_DIRS) {
7125
- writeFileSync6(join54(repo, "shared", name, ".gitkeep"), "");
7426
+ writeFileSync6(join55(repo, "shared", name, ".gitkeep"), "");
7126
7427
  item(`created shared/${name}/.gitkeep`);
7127
7428
  }
7128
- writeFileSync6(join54(repo, "hosts", ".gitkeep"), "");
7429
+ writeFileSync6(join55(repo, "hosts", ".gitkeep"), "");
7129
7430
  item("created hosts/.gitkeep");
7130
- writeJsonAtomic(join54(repo, "shared", "settings.base.json"), {});
7431
+ writeJsonAtomic(join55(repo, "shared", "settings.base.json"), {});
7131
7432
  item("created shared/settings.base.json");
7132
- writeJsonAtomic(join54(repo, "path-map.json"), { projects: {} });
7433
+ writeJsonAtomic(join55(repo, "path-map.json"), { projects: {} });
7133
7434
  item("created path-map.json");
7134
7435
  if (snapshot) {
7135
7436
  snapshotIntoShared({ projects: {} });
@@ -7195,11 +7496,11 @@ function maybeDisableRepoActions(repoHome2, run) {
7195
7496
  // src/init.prompt.ts
7196
7497
  init_config();
7197
7498
  init_utils();
7198
- import { existsSync as existsSync47, readdirSync as readdirSync17, statSync as statSync13 } from "node:fs";
7199
- import { join as join55 } from "node:path";
7499
+ import { existsSync as existsSync48, readdirSync as readdirSync17, statSync as statSync13 } from "node:fs";
7500
+ import { join as join56 } from "node:path";
7200
7501
  import { createInterface as createInterface3 } from "node:readline/promises";
7201
7502
  function nonEmptyExists(path) {
7202
- if (!existsSync47(path)) return false;
7503
+ if (!existsSync48(path)) return false;
7203
7504
  try {
7204
7505
  if (statSync13(path).isDirectory()) return readdirSync17(path).length > 0;
7205
7506
  return true;
@@ -7208,8 +7509,8 @@ function nonEmptyExists(path) {
7208
7509
  }
7209
7510
  }
7210
7511
  function hasExistingClaudeConfig(claudeHome2) {
7211
- if (existsSync47(join55(claudeHome2, "settings.json"))) return true;
7212
- return SHARED_LINKS.some((name) => nonEmptyExists(join55(claudeHome2, name)));
7512
+ if (existsSync48(join56(claudeHome2, "settings.json"))) return true;
7513
+ return SHARED_LINKS.some((name) => nonEmptyExists(join56(claudeHome2, name)));
7213
7514
  }
7214
7515
  async function confirmSnapshotDefault(claudeHome2) {
7215
7516
  if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
@@ -7493,10 +7794,27 @@ function parsePushArgs(argv) {
7493
7794
  };
7494
7795
  }
7495
7796
 
7797
+ // src/nomad.dispatch.sync.ts
7798
+ function parseSyncArgs(argv) {
7799
+ let dryRun = false;
7800
+ let i = 3;
7801
+ while (i < argv.length) {
7802
+ const token = argv[i];
7803
+ if (token === "--dry-run") {
7804
+ if (dryRun) return null;
7805
+ dryRun = true;
7806
+ } else {
7807
+ return null;
7808
+ }
7809
+ i++;
7810
+ }
7811
+ return { dryRun };
7812
+ }
7813
+
7496
7814
  // package.json
7497
7815
  var package_default = {
7498
7816
  name: "claude-nomad",
7499
- version: "0.56.1",
7817
+ version: "0.57.0",
7500
7818
  type: "module",
7501
7819
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
7502
7820
  keywords: [
@@ -7625,6 +7943,12 @@ var DEFAULT_HELP = [
7625
7943
  cont("when no finding survives. No TTY required. Never skips the scan."),
7626
7944
  cont("Mutually exclusive with --redact-all/--allow. Cannot combine with --dry-run."),
7627
7945
  "",
7946
+ row(" sync", "Pull (retain-merge), then push, under one lock. One-shot; hides ordering."),
7947
+ row(" --dry-run", "Stack the pull preview then the push preview; mutates nothing."),
7948
+ cont("Fails fast on a pull-half error; a wedged repo hints at nomad pull --force-remote"),
7949
+ cont("(sync itself takes no --force-remote). Mid-push leak recovery behaves exactly"),
7950
+ cont("like nomad push."),
7951
+ "",
7628
7952
  row(" diff", "Offline preview of what `pull` would change against local repo state."),
7629
7953
  cont("No git pull, no lock acquired."),
7630
7954
  "",
@@ -7721,15 +8045,15 @@ var DEFAULT_HELP = [
7721
8045
  init_config();
7722
8046
  init_utils();
7723
8047
  init_utils_json();
7724
- import { existsSync as existsSync48, readFileSync as readFileSync15, readdirSync as readdirSync18 } from "node:fs";
7725
- import { join as join56 } from "node:path";
8048
+ import { existsSync as existsSync49, readFileSync as readFileSync17, readdirSync as readdirSync18 } from "node:fs";
8049
+ import { join as join57 } from "node:path";
7726
8050
  function resumeCmd(sessionId) {
7727
8051
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
7728
8052
  fail(`invalid session id: ${sessionId}`);
7729
8053
  process.exit(1);
7730
8054
  }
7731
- const projectsRoot = join56(claudeHome(), "projects");
7732
- if (!existsSync48(projectsRoot)) {
8055
+ const projectsRoot = join57(claudeHome(), "projects");
8056
+ if (!existsSync49(projectsRoot)) {
7733
8057
  fail(`${projectsRoot} does not exist`);
7734
8058
  process.exit(1);
7735
8059
  }
@@ -7743,8 +8067,8 @@ function resumeCmd(sessionId) {
7743
8067
  fail(`no cwd field found in ${jsonlPath}`);
7744
8068
  process.exit(1);
7745
8069
  }
7746
- const mapPath = join56(repoHome(), "path-map.json");
7747
- if (!existsSync48(mapPath)) {
8070
+ const mapPath = join57(repoHome(), "path-map.json");
8071
+ if (!existsSync49(mapPath)) {
7748
8072
  fail("path-map.json missing");
7749
8073
  process.exit(1);
7750
8074
  }
@@ -7767,13 +8091,13 @@ function resumeCmd(sessionId) {
7767
8091
  }
7768
8092
  function findTranscriptPath(projectsRoot, sessionId) {
7769
8093
  for (const dir of readdirSync18(projectsRoot)) {
7770
- const candidate = join56(projectsRoot, dir, `${sessionId}.jsonl`);
7771
- if (existsSync48(candidate)) return candidate;
8094
+ const candidate = join57(projectsRoot, dir, `${sessionId}.jsonl`);
8095
+ if (existsSync49(candidate)) return candidate;
7772
8096
  }
7773
8097
  return null;
7774
8098
  }
7775
8099
  function extractRecordedCwd(jsonlPath) {
7776
- for (const line of readFileSync15(jsonlPath, "utf8").split("\n")) {
8100
+ for (const line of readFileSync17(jsonlPath, "utf8").split("\n")) {
7777
8101
  if (!line.trim()) continue;
7778
8102
  try {
7779
8103
  const obj = JSON.parse(line);
@@ -7846,6 +8170,15 @@ try {
7846
8170
  });
7847
8171
  break;
7848
8172
  }
8173
+ case "sync": {
8174
+ const syncArgs = parseSyncArgs(process.argv);
8175
+ if (syncArgs === null) {
8176
+ console.error("usage: nomad sync [--dry-run]");
8177
+ process.exit(1);
8178
+ }
8179
+ await cmdSync({ dryRun: syncArgs.dryRun });
8180
+ break;
8181
+ }
7849
8182
  case "init": {
7850
8183
  const initArgs = parseInitArgs(process.argv);
7851
8184
  if (initArgs === null) {