claude-nomad 0.40.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.41.0](https://github.com/funkadelic/claude-nomad/compare/v0.40.0...v0.41.0) (2026-06-03)
4
+
5
+
6
+ ### Added
7
+
8
+ * **diff:** render diff and pull --dry-run as a glyph-free tree ([#236](https://github.com/funkadelic/claude-nomad/issues/236)) ([6cf1aff](https://github.com/funkadelic/claude-nomad/commit/6cf1aff60f7ac5710c990fb303c6408c0b07ff66))
9
+ * **spinner:** animate remap sync, fix stray frames ([#234](https://github.com/funkadelic/claude-nomad/issues/234)) ([5a71563](https://github.com/funkadelic/claude-nomad/commit/5a715637c8ab43ab2d536353f55512abe15cfdd3))
10
+
3
11
  ## [0.40.0](https://github.com/funkadelic/claude-nomad/compare/v0.39.0...v0.40.0) (2026-06-03)
4
12
 
5
13
 
package/dist/nomad.mjs CHANGED
@@ -170,13 +170,12 @@ function gitOrFatal(args, context, cwd) {
170
170
  throw new NomadFatal(`${context} failed`);
171
171
  }
172
172
  }
173
- var log, ok, warn, fail, NomadFatal, die, gitStatusPorcelainZ;
173
+ var log, warn, fail, NomadFatal, die, gitStatusPorcelainZ;
174
174
  var init_utils = __esm({
175
175
  "src/utils.ts"() {
176
176
  "use strict";
177
177
  init_color();
178
178
  log = (msg) => console.log(`${dim(infoGlyph)} ${msg}`);
179
- ok = (msg) => console.log(`${green(okGlyph)} ${msg}`);
180
179
  warn = (msg) => {
181
180
  console.error(`${yellow(warnGlyph)} ${msg}`);
182
181
  };
@@ -1225,8 +1224,8 @@ init_color();
1225
1224
  // src/output-tree.ts
1226
1225
  init_color();
1227
1226
  var FAIL_GLYPH_BARE = "\u2717";
1228
- function section(header) {
1229
- return { header, items: [] };
1227
+ function section(header, raw = false) {
1228
+ return { header, items: [], raw };
1230
1229
  }
1231
1230
  function addItem(s, text) {
1232
1231
  s.items.push(text);
@@ -1234,7 +1233,17 @@ function addItem(s, text) {
1234
1233
  function sectionFailed(s) {
1235
1234
  return s.items.some((line) => line.includes(failGlyph));
1236
1235
  }
1236
+ function renderRawItems(items) {
1237
+ for (const item of items) {
1238
+ console.log(item === "" ? "" : ` ${item}`);
1239
+ }
1240
+ }
1237
1241
  function renderSection(s) {
1242
+ if (s.raw) {
1243
+ console.log(s.header);
1244
+ renderRawItems(s.items);
1245
+ return;
1246
+ }
1238
1247
  const header = sectionFailed(s) ? `${red(FAIL_GLYPH_BARE)} ${s.header}` : s.header;
1239
1248
  console.log(header);
1240
1249
  const lastContent = s.items.reduce((acc, item, j) => item === "" ? acc : j, -1);
@@ -1845,6 +1854,10 @@ function copyDirJsonlOnly(src, dst) {
1845
1854
  }
1846
1855
  });
1847
1856
  }
1857
+ function emitPreview(onPreview, event, fallback) {
1858
+ if (onPreview) onPreview(event);
1859
+ else log(fallback);
1860
+ }
1848
1861
  function remapPull(ts, opts = {}) {
1849
1862
  const dryRun = opts.dryRun === true;
1850
1863
  let unmapped = 0;
@@ -1853,7 +1866,8 @@ function remapPull(ts, opts = {}) {
1853
1866
  const mapPath = join17(REPO_HOME, "path-map.json");
1854
1867
  const repoProjects = join17(REPO_HOME, "shared", "projects");
1855
1868
  if (!existsSync13(mapPath) || !existsSync13(repoProjects)) {
1856
- log("no path-map or repo projects dir; skipping session remap");
1869
+ const text = "no path-map or repo projects dir; skipping session remap";
1870
+ emitPreview(opts.onPreview, { kind: "note", text }, text);
1857
1871
  return { unmapped: 0, pulled, wouldPull };
1858
1872
  }
1859
1873
  const map = readJson(mapPath);
@@ -1871,7 +1885,11 @@ function remapPull(ts, opts = {}) {
1871
1885
  const dst = join17(localProjects, encodePath(localPath));
1872
1886
  if (dryRun) {
1873
1887
  wouldPull.push(logical);
1874
- log(`would overwrite: ${dst} (from ${src})`);
1888
+ emitPreview(
1889
+ opts.onPreview,
1890
+ { kind: "overwrite", dst, src },
1891
+ `would overwrite: ${dst} (from ${src})`
1892
+ );
1875
1893
  continue;
1876
1894
  }
1877
1895
  backupBeforeWrite(dst, ts);
@@ -2986,14 +3004,6 @@ function summaryRow(verb, unmapped, collisions = 0, extrasSkipped = 0) {
2986
3004
  const { text, clean } = summaryText(verb, unmapped, collisions, extrasSkipped);
2987
3005
  return clean ? `${green(okGlyph)} ${text}` : `${yellow(warnGlyph)} ${text}`;
2988
3006
  }
2989
- function emitSummary(verb, unmapped, collisions = 0, extrasSkipped = 0) {
2990
- const { text, clean } = summaryText(verb, unmapped, collisions, extrasSkipped);
2991
- if (clean) {
2992
- ok(text);
2993
- return;
2994
- }
2995
- warn(text);
2996
- }
2997
3007
 
2998
3008
  // src/commands.push.sections.ts
2999
3009
  function collapsedSkipRow(n, noun) {
@@ -3236,6 +3246,20 @@ init_utils_fs();
3236
3246
  init_utils_json();
3237
3247
  import { existsSync as existsSync26, lstatSync as lstatSync7, rmSync as rmSync8 } from "node:fs";
3238
3248
  import { join as join31 } from "node:path";
3249
+ function emitAutoMove(onPreview, linkPath, ts, name) {
3250
+ if (onPreview) {
3251
+ onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
3252
+ } else {
3253
+ log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
3254
+ }
3255
+ }
3256
+ function emitCreate(onPreview, from, to) {
3257
+ if (onPreview) {
3258
+ onPreview({ kind: "create", from, to });
3259
+ } else {
3260
+ log(`would create symlink: ${from} -> ${to}`);
3261
+ }
3262
+ }
3239
3263
  function applySharedLinks(ts, map, opts = {}) {
3240
3264
  const dryRun = opts.dryRun === true;
3241
3265
  const linkNames = allSharedLinks(map);
@@ -3246,7 +3270,7 @@ function applySharedLinks(ts, map, opts = {}) {
3246
3270
  if (lstatSync7(linkPath).isSymbolicLink()) continue;
3247
3271
  if (!existsSync26(target)) continue;
3248
3272
  if (dryRun) {
3249
- log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
3273
+ emitAutoMove(opts.onPreview, linkPath, ts, name);
3250
3274
  continue;
3251
3275
  }
3252
3276
  backupBeforeWrite(linkPath, ts);
@@ -3256,7 +3280,7 @@ function applySharedLinks(ts, map, opts = {}) {
3256
3280
  const target = join31(REPO_HOME, "shared", name);
3257
3281
  if (!existsSync26(target)) continue;
3258
3282
  if (dryRun) {
3259
- log(`would create symlink: ${join31(CLAUDE_HOME, name)} -> ${target}`);
3283
+ emitCreate(opts.onPreview, join31(CLAUDE_HOME, name), target);
3260
3284
  continue;
3261
3285
  }
3262
3286
  ensureSymlink(join31(CLAUDE_HOME, name), target);
@@ -3575,7 +3599,6 @@ function diffLinesToUnified(oldStr, newStr) {
3575
3599
  }
3576
3600
 
3577
3601
  // src/preview.ts
3578
- init_utils();
3579
3602
  init_utils_json();
3580
3603
  function diffJsonStrings(currentJsonText, newJsonText) {
3581
3604
  if (currentJsonText === newJsonText) return "";
@@ -3597,39 +3620,64 @@ function readJsonOrNull(path) {
3597
3620
  function previewSettings(basePath, hostPath, settingsPath) {
3598
3621
  const base = readJsonOrNull(basePath);
3599
3622
  if (base === null) {
3600
- log("settings.json: section skipped (base or current missing)");
3601
- return;
3623
+ return { diff: "", notes: ["section skipped (base or current missing)"] };
3602
3624
  }
3625
+ const notes = [];
3603
3626
  const hostOverrides = readJsonOrNull(hostPath);
3604
3627
  if (hostOverrides === null && existsSync27(hostPath)) {
3605
- log(`settings.json: malformed hosts/${HOST}.json; ignoring overrides`);
3628
+ notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
3606
3629
  }
3607
3630
  const merged = deepMerge(base, hostOverrides ?? {});
3608
3631
  const current = readJsonOrNull(settingsPath);
3609
3632
  if (current === null && existsSync27(settingsPath)) {
3610
- log("settings.json: malformed; skipping diff");
3611
- return;
3633
+ return { diff: "", notes: [...notes, "malformed; skipping diff"] };
3612
3634
  }
3613
3635
  const diff = diffJsonStrings(
3614
3636
  JSON.stringify(current ?? {}, null, 2),
3615
3637
  JSON.stringify(merged, null, 2)
3616
3638
  );
3617
- if (diff === "") {
3618
- log("settings.json: no changes");
3619
- } else {
3620
- log("settings.json:");
3621
- for (const line of diff.split("\n")) log(line);
3639
+ return { diff, notes };
3640
+ }
3641
+ function formatLinkRow(e) {
3642
+ return `${e.kind} ${e.from} -> ${e.to}`;
3643
+ }
3644
+ function formatSessionRow(e) {
3645
+ return e.kind === "overwrite" ? `overwrite ${e.dst} (from ${e.src})` : e.text;
3646
+ }
3647
+ function buildSettingsSectionForPreview(result) {
3648
+ const s = section("settings.json", true);
3649
+ if (result.diff !== "") {
3650
+ for (const line of result.diff.split("\n")) {
3651
+ addItem(s, line);
3652
+ }
3622
3653
  }
3654
+ for (const note of result.notes) {
3655
+ addItem(s, `note: ${note}`);
3656
+ }
3657
+ return s;
3623
3658
  }
3624
- function computePreview(ts, map) {
3625
- log(`would pull on host=${HOST} (dry-run; no mutation)`);
3626
- applySharedLinks(ts, map, { dryRun: true });
3627
- previewSettings(
3659
+ function computePreview(ts, map, verb = "pull") {
3660
+ console.log(`would pull on host=${HOST} (dry-run; no mutation)`);
3661
+ console.log("");
3662
+ const links = section("Symlinks");
3663
+ applySharedLinks(ts, map, {
3664
+ dryRun: true,
3665
+ onPreview: (e) => addItem(links, formatLinkRow(e))
3666
+ });
3667
+ const settingsResult = previewSettings(
3628
3668
  join32(REPO_HOME, "shared", "settings.base.json"),
3629
3669
  join32(REPO_HOME, "hosts", `${HOST}.json`),
3630
3670
  join32(CLAUDE_HOME, "settings.json")
3631
3671
  );
3632
- const remapResult = remapPull(ts, { dryRun: true });
3672
+ const settingsSection = buildSettingsSectionForPreview(settingsResult);
3673
+ const sessions = section("Sessions");
3674
+ const remapResult = remapPull(ts, {
3675
+ dryRun: true,
3676
+ onPreview: (e) => addItem(sessions, formatSessionRow(e))
3677
+ });
3678
+ const summary = section("Summary");
3679
+ addItem(summary, summaryRow(verb, remapResult.unmapped));
3680
+ renderTree([links, settingsSection, sessions, summary]);
3633
3681
  return { unmapped: remapResult.unmapped, collisions: 0 };
3634
3682
  }
3635
3683
 
@@ -4009,10 +4057,10 @@ function startSpinner(label, deps = {}) {
4009
4057
  const elapsed = now() - startMs;
4010
4058
  if (animate && !degraded && worker !== null) {
4011
4059
  worker.postMessage({ type: "pause" });
4012
- if (success) writeAnimatedDone(out, dl, elapsed, ttyCheck());
4013
- else out.write("\r\x1B[K");
4014
4060
  worker.terminate();
4015
4061
  worker = null;
4062
+ if (success) writeAnimatedDone(out, dl, elapsed, ttyCheck());
4063
+ else out.write("\r\x1B[K");
4016
4064
  } else if (success) {
4017
4065
  writePlainDone(out, dl, elapsed);
4018
4066
  }
@@ -4022,6 +4070,16 @@ function startSpinner(label, deps = {}) {
4022
4070
  stop: () => finalize(false)
4023
4071
  };
4024
4072
  }
4073
+ function withSpinner(label, fn, deps) {
4074
+ const sp = startSpinner(label, deps);
4075
+ try {
4076
+ const result = fn();
4077
+ sp.succeed();
4078
+ return result;
4079
+ } finally {
4080
+ sp.stop();
4081
+ }
4082
+ }
4025
4083
 
4026
4084
  // src/commands.pull.ts
4027
4085
  init_utils();
@@ -4030,14 +4088,7 @@ init_utils_json();
4030
4088
  function applyWetPull(ts, map) {
4031
4089
  applySharedLinks(ts, map);
4032
4090
  const { label } = regenerateSettings(ts);
4033
- const syncSp = startSpinner("Syncing sessions");
4034
- let remapResult;
4035
- try {
4036
- remapResult = remapPull(ts);
4037
- syncSp.succeed();
4038
- } finally {
4039
- syncSp.stop();
4040
- }
4091
+ const remapResult = withSpinner("Syncing sessions", () => remapPull(ts));
4041
4092
  const extrasResult = remapExtrasPull(ts);
4042
4093
  const summary = section("Summary");
4043
4094
  addItem(
@@ -4077,9 +4128,8 @@ function cmdPull(opts = {}) {
4077
4128
  const map = existsSync30(mapPath) ? readPathMap(mapPath) : { projects: {} };
4078
4129
  divergenceCheckExtras(ts);
4079
4130
  if (dryRun) {
4080
- const previewResult = computePreview(ts, map);
4131
+ computePreview(ts, map, "pull");
4081
4132
  log("dry-run complete; no mutation");
4082
- emitSummary("pull", previewResult.unmapped);
4083
4133
  } else {
4084
4134
  applyWetPull(ts, map);
4085
4135
  }
@@ -4272,34 +4322,15 @@ function guardGitlinks() {
4272
4322
  `gitlink trap: ${gitlinks.length} nested .git ${noun} in shared/; remove before retry`
4273
4323
  );
4274
4324
  }
4275
- function runScan() {
4276
- const sp = startSpinner("Scanning for secrets");
4277
- try {
4278
- const verdict = scanPushVerdict();
4279
- sp.succeed();
4280
- return verdict;
4281
- } finally {
4282
- sp.stop();
4283
- }
4284
- }
4285
- function runPush() {
4286
- const sp = startSpinner("Pushing");
4287
- try {
4288
- gitOrFatal(["push"], "git push", REPO_HOME);
4289
- sp.succeed();
4290
- } finally {
4291
- sp.stop();
4292
- }
4293
- }
4294
4325
  async function commitAndPush(st, ts, map, redactAll, allowAll, allowRule) {
4295
4326
  gitOrFatal(["add", "-A"], "git add", REPO_HOME);
4296
- let verdict = runScan();
4327
+ let verdict = withSpinner("Scanning for secrets", scanPushVerdict);
4297
4328
  if (verdict.leak) {
4298
4329
  renderPushTree(st, verdict);
4299
4330
  verdict = await resolveLeakFindings(verdict, ts, map, { redactAll, allowAll, allowRule });
4300
4331
  }
4301
4332
  gitOrFatal(["commit", "-m", `chore: sync from ${HOST}`], "git commit", REPO_HOME);
4302
- runPush();
4333
+ withSpinner("Pushing", () => gitOrFatal(["push"], "git push", REPO_HOME));
4303
4334
  renderPushTree(st, verdict);
4304
4335
  }
4305
4336
  function runDryRunPreview(st, map) {
@@ -4338,16 +4369,10 @@ async function cmdPush(opts = {}) {
4338
4369
  try {
4339
4370
  console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
4340
4371
  probeGitleaks();
4341
- const rebaseSp = startSpinner("Rebasing onto origin");
4342
- try {
4343
- rebaseBeforePush();
4344
- rebaseSp.succeed();
4345
- } finally {
4346
- rebaseSp.stop();
4347
- }
4372
+ withSpinner("Rebasing onto origin", rebaseBeforePush);
4348
4373
  const ts = freshBackupTs(BACKUP_BASE);
4349
- const remap = remapPush(ts, { dryRun });
4350
- const extras = remapExtrasPush(ts, { dryRun });
4374
+ const remap = withSpinner("Syncing sessions", () => remapPush(ts, { dryRun }));
4375
+ const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
4351
4376
  const st = { dryRun, remap, extras };
4352
4377
  guardGitlinks();
4353
4378
  const status = gitStatusPorcelainZ(REPO_HOME, { untrackedAll: true });
@@ -4408,8 +4433,7 @@ function cmdDiff() {
4408
4433
  const ts = freshBackupTs(BACKUP_BASE);
4409
4434
  const mapPath = join39(REPO_HOME, "path-map.json");
4410
4435
  const map = existsSync33(mapPath) ? readPathMap(mapPath) : { projects: {} };
4411
- const result = computePreview(ts, map);
4412
- emitSummary("diff", result.unmapped);
4436
+ computePreview(ts, map, "diff");
4413
4437
  } catch (err) {
4414
4438
  if (err instanceof NomadFatal) {
4415
4439
  fail(err.message);
@@ -4841,7 +4865,7 @@ function parsePushArgs(argv) {
4841
4865
  // package.json
4842
4866
  var package_default = {
4843
4867
  name: "claude-nomad",
4844
- version: "0.40.0",
4868
+ version: "0.41.0",
4845
4869
  type: "module",
4846
4870
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
4847
4871
  keywords: [
@@ -4854,7 +4878,7 @@ var package_default = {
4854
4878
  type: "git",
4855
4879
  url: "git+https://github.com/funkadelic/claude-nomad.git"
4856
4880
  },
4857
- homepage: "https://github.com/funkadelic/claude-nomad#readme",
4881
+ homepage: "https://funkadelic.github.io/claude-nomad/",
4858
4882
  bugs: {
4859
4883
  url: "https://github.com/funkadelic/claude-nomad/issues"
4860
4884
  },
@@ -1,7 +1,9 @@
1
1
  // src/spinner.worker.ts
2
+ import { writeSync } from "node:fs";
2
3
  import { parentPort } from "node:worker_threads";
3
4
  var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
4
5
  var INTERVAL_MS = 80;
6
+ var STDERR_FD = 2;
5
7
  var frame = 0;
6
8
  var timer = null;
7
9
  if (parentPort !== null) {
@@ -11,7 +13,7 @@ if (parentPort !== null) {
11
13
  frame = 0;
12
14
  if (timer !== null) clearInterval(timer);
13
15
  timer = setInterval(() => {
14
- process.stderr.write(`${FRAMES[frame % FRAMES.length]} ${label}\r`);
16
+ writeSync(STDERR_FD, `${FRAMES[frame % FRAMES.length]} ${label}\r`);
15
17
  frame++;
16
18
  }, INTERVAL_MS);
17
19
  } else if (msg.type === "pause") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-nomad",
3
- "version": "0.40.0",
3
+ "version": "0.41.0",
4
4
  "type": "module",
5
5
  "description": "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
6
6
  "keywords": [
@@ -13,7 +13,7 @@
13
13
  "type": "git",
14
14
  "url": "git+https://github.com/funkadelic/claude-nomad.git"
15
15
  },
16
- "homepage": "https://github.com/funkadelic/claude-nomad#readme",
16
+ "homepage": "https://funkadelic.github.io/claude-nomad/",
17
17
  "bugs": {
18
18
  "url": "https://github.com/funkadelic/claude-nomad/issues"
19
19
  },