claude-nomad 0.47.0 → 0.48.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 +15 -0
- package/dist/nomad.mjs +152 -23
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.48.0](https://github.com/funkadelic/claude-nomad/compare/v0.47.1...v0.48.0) (2026-06-09)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
* **push:** surface changed shared config in push output ([#278](https://github.com/funkadelic/claude-nomad/issues/278)) ([4774661](https://github.com/funkadelic/claude-nomad/commit/477466138e203f861e60c72cb6522f25393b3987))
|
|
9
|
+
* **update:** frame update output and report new version ([#279](https://github.com/funkadelic/claude-nomad/issues/279)) ([9342de1](https://github.com/funkadelic/claude-nomad/commit/9342de1187db1fd115cc73cb2b516a9477b67a54))
|
|
10
|
+
|
|
11
|
+
## [0.47.1](https://github.com/funkadelic/claude-nomad/compare/v0.47.0...v0.47.1) (2026-06-09)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
* **extras:** preserve host-local deny-set files on .claude pull ([#276](https://github.com/funkadelic/claude-nomad/issues/276)) ([3742c3e](https://github.com/funkadelic/claude-nomad/commit/3742c3e4067c1c986e7f182011581d4c867847b0))
|
|
17
|
+
|
|
3
18
|
## [0.47.0](https://github.com/funkadelic/claude-nomad/compare/v0.46.0...v0.47.0) (2026-06-09)
|
|
4
19
|
|
|
5
20
|
|
package/dist/nomad.mjs
CHANGED
|
@@ -4166,10 +4166,18 @@ function buildExtrasSection(items, extrasSkipped) {
|
|
|
4166
4166
|
if (skip !== null) addItem(s, skip);
|
|
4167
4167
|
return s;
|
|
4168
4168
|
}
|
|
4169
|
+
function buildGlobalConfigSection(rows) {
|
|
4170
|
+
const s = section("Global config");
|
|
4171
|
+
for (const row2 of rows) {
|
|
4172
|
+
addItem(s, `${green(okGlyph)} ${row2.label} ${row2.path}`);
|
|
4173
|
+
}
|
|
4174
|
+
return s;
|
|
4175
|
+
}
|
|
4169
4176
|
function syncedSections(st) {
|
|
4170
4177
|
const sessions = st.dryRun ? st.remap.wouldPush : st.remap.pushed;
|
|
4171
4178
|
const extras = st.dryRun ? st.extras.wouldPush : st.extras.pushed;
|
|
4172
4179
|
return [
|
|
4180
|
+
buildGlobalConfigSection(st.globalConfig),
|
|
4173
4181
|
buildSessionsSection(sessions, st.remap.unmapped),
|
|
4174
4182
|
buildExtrasSection(extras, st.extras.skipped)
|
|
4175
4183
|
];
|
|
@@ -4240,7 +4248,7 @@ function listDivergingFiles(a, b) {
|
|
|
4240
4248
|
|
|
4241
4249
|
// src/extras-sync.core.ts
|
|
4242
4250
|
init_config();
|
|
4243
|
-
import { cpSync as cpSync6, existsSync as existsSync30, rmSync as rmSync10 } from "node:fs";
|
|
4251
|
+
import { cpSync as cpSync6, existsSync as existsSync30, lstatSync as lstatSync8, readdirSync as readdirSync11, rmSync as rmSync10 } from "node:fs";
|
|
4244
4252
|
import { basename, join as join36 } from "node:path";
|
|
4245
4253
|
|
|
4246
4254
|
// src/extras-sync.guards.ts
|
|
@@ -4314,6 +4322,36 @@ function copyExtrasFiltered(src, dst, blockSet) {
|
|
|
4314
4322
|
filter: (srcEntry) => srcEntry === src || !blockSet.has(basename(srcEntry))
|
|
4315
4323
|
});
|
|
4316
4324
|
}
|
|
4325
|
+
function prunePreservingDenied(src, dst, blockSet) {
|
|
4326
|
+
for (const name of readdirSync11(dst)) {
|
|
4327
|
+
if (blockSet.has(name)) continue;
|
|
4328
|
+
const dstPath = join36(dst, name);
|
|
4329
|
+
const srcStat = lstatSync8(join36(src, name), { throwIfNoEntry: false });
|
|
4330
|
+
if (srcStat === void 0) {
|
|
4331
|
+
rmSync10(dstPath, { recursive: true, force: true });
|
|
4332
|
+
continue;
|
|
4333
|
+
}
|
|
4334
|
+
const dstStat = lstatSync8(dstPath);
|
|
4335
|
+
if (srcStat.isDirectory() && dstStat.isDirectory()) {
|
|
4336
|
+
prunePreservingDenied(join36(src, name), dstPath, blockSet);
|
|
4337
|
+
} else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
|
|
4338
|
+
rmSync10(dstPath, { recursive: true, force: true });
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
}
|
|
4342
|
+
function copyExtrasFilteredPreserving(src, dst, blockSet) {
|
|
4343
|
+
const dstStat = lstatSync8(dst, { throwIfNoEntry: false });
|
|
4344
|
+
if (dstStat !== void 0) {
|
|
4345
|
+
if (dstStat.isDirectory()) prunePreservingDenied(src, dst, blockSet);
|
|
4346
|
+
else rmSync10(dst, { recursive: true, force: true });
|
|
4347
|
+
}
|
|
4348
|
+
cpSync6(src, dst, {
|
|
4349
|
+
recursive: true,
|
|
4350
|
+
force: true,
|
|
4351
|
+
verbatimSymlinks: true,
|
|
4352
|
+
filter: (srcEntry) => srcEntry === src || !blockSet.has(basename(srcEntry))
|
|
4353
|
+
});
|
|
4354
|
+
}
|
|
4317
4355
|
|
|
4318
4356
|
// src/extras-sync.ts
|
|
4319
4357
|
init_utils();
|
|
@@ -4381,12 +4419,16 @@ function remapExtrasPull(ts, opts = {}) {
|
|
|
4381
4419
|
// Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
|
|
4382
4420
|
// localRoot so the backup tree mirrors the project layout.
|
|
4383
4421
|
(dst, localRoot) => backupExtrasWrite(dst, ts, localRoot),
|
|
4384
|
-
// Pull
|
|
4385
|
-
//
|
|
4386
|
-
//
|
|
4387
|
-
//
|
|
4388
|
-
//
|
|
4389
|
-
|
|
4422
|
+
// Pull routes `.claude` through copyExtrasFilteredPreserving so host-local
|
|
4423
|
+
// deny-set files already on disk (e.g. settings.local.json) are preserved
|
|
4424
|
+
// instead of being wiped by a blanket rmSync. The same deny-set filter still
|
|
4425
|
+
// strips blocked basenames from the src copy (defense-in-depth: a repo
|
|
4426
|
+
// poisoned out-of-band cannot restore a blocked per-host file). Synced
|
|
4427
|
+
// non-deny files that are absent from src are still mirror-pruned. This
|
|
4428
|
+
// preservation is `.claude`-only; `.planning` and `CLAUDE.md` use the
|
|
4429
|
+
// exact-mirror copyExtras (documented restore semantics; they rarely carry
|
|
4430
|
+
// host-local files, so the exact mirror is the correct default).
|
|
4431
|
+
(src, dst, dirname7) => dirname7 === ".claude" ? copyExtrasFilteredPreserving(src, dst, extrasDenySet(dirname7)) : copyExtras(src, dst)
|
|
4390
4432
|
);
|
|
4391
4433
|
return { unmapped, skipped, pulled: done, wouldPull: would };
|
|
4392
4434
|
}
|
|
@@ -4417,7 +4459,7 @@ init_config();
|
|
|
4417
4459
|
init_utils();
|
|
4418
4460
|
init_utils_fs();
|
|
4419
4461
|
init_utils_json();
|
|
4420
|
-
import { existsSync as existsSync33, lstatSync as
|
|
4462
|
+
import { existsSync as existsSync33, lstatSync as lstatSync9, rmSync as rmSync11 } from "node:fs";
|
|
4421
4463
|
import { join as join39 } from "node:path";
|
|
4422
4464
|
function emitAutoMove(onPreview, linkPath, ts, name) {
|
|
4423
4465
|
if (onPreview) {
|
|
@@ -4434,14 +4476,14 @@ function emitCreate(onPreview, from, to) {
|
|
|
4434
4476
|
}
|
|
4435
4477
|
}
|
|
4436
4478
|
function isAlreadySymlink(linkPath) {
|
|
4437
|
-
return existsSync33(linkPath) &&
|
|
4479
|
+
return existsSync33(linkPath) && lstatSync9(linkPath).isSymbolicLink();
|
|
4438
4480
|
}
|
|
4439
4481
|
function runAutoMovePasses(linkNames, claude, repo, ts, dryRun, onPreview) {
|
|
4440
4482
|
for (const name of linkNames) {
|
|
4441
4483
|
const linkPath = join39(claude, name);
|
|
4442
4484
|
const target = join39(repo, "shared", name);
|
|
4443
4485
|
if (!existsSync33(linkPath)) continue;
|
|
4444
|
-
if (
|
|
4486
|
+
if (lstatSync9(linkPath).isSymbolicLink()) continue;
|
|
4445
4487
|
if (!existsSync33(target)) continue;
|
|
4446
4488
|
if (dryRun) {
|
|
4447
4489
|
emitAutoMove(onPreview, linkPath, ts, name);
|
|
@@ -5137,6 +5179,77 @@ function enforceAllowList(statusPorcelain, map) {
|
|
|
5137
5179
|
throw new NomadFatal("push allow-list violations");
|
|
5138
5180
|
}
|
|
5139
5181
|
|
|
5182
|
+
// src/push-global-config.ts
|
|
5183
|
+
init_config();
|
|
5184
|
+
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
5185
|
+
var STATUS_LABELS = {
|
|
5186
|
+
A: "add",
|
|
5187
|
+
M: "modify",
|
|
5188
|
+
D: "delete",
|
|
5189
|
+
R: "rename",
|
|
5190
|
+
C: "copy",
|
|
5191
|
+
T: "type-change",
|
|
5192
|
+
U: "unmerged",
|
|
5193
|
+
X: "unknown"
|
|
5194
|
+
};
|
|
5195
|
+
function labelForStatus(statusToken) {
|
|
5196
|
+
const letter = statusToken[0] ?? "";
|
|
5197
|
+
return STATUS_LABELS[letter] ?? "change";
|
|
5198
|
+
}
|
|
5199
|
+
function buildPrefixSets(hostname2) {
|
|
5200
|
+
const exactPrefixes = /* @__PURE__ */ new Set();
|
|
5201
|
+
const dirPrefixes = [];
|
|
5202
|
+
for (const name of SHARED_LINKS) {
|
|
5203
|
+
const p = `shared/${name}`;
|
|
5204
|
+
if (name.includes(".")) {
|
|
5205
|
+
exactPrefixes.add(p);
|
|
5206
|
+
} else {
|
|
5207
|
+
dirPrefixes.push(p);
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
exactPrefixes.add("shared/settings.base.json");
|
|
5211
|
+
exactPrefixes.add(`hosts/${hostname2}.json`);
|
|
5212
|
+
return { exactPrefixes, dirPrefixes };
|
|
5213
|
+
}
|
|
5214
|
+
function isInScope(filePath, exactPrefixes, dirPrefixes) {
|
|
5215
|
+
if (filePath.startsWith("shared/projects/") || filePath.startsWith("shared/extras/")) {
|
|
5216
|
+
return false;
|
|
5217
|
+
}
|
|
5218
|
+
if (exactPrefixes.has(filePath)) return true;
|
|
5219
|
+
return dirPrefixes.some((prefix) => filePath.startsWith(`${prefix}/`));
|
|
5220
|
+
}
|
|
5221
|
+
function collectGlobalConfigChanges(repoHome2, hostname2, opts) {
|
|
5222
|
+
const args = opts.staged ? ["diff", "--cached", "--name-status", "-z"] : ["diff", "HEAD", "--name-status", "-z"];
|
|
5223
|
+
const raw = execFileSync15("git", args, {
|
|
5224
|
+
cwd: repoHome2,
|
|
5225
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5226
|
+
}).toString();
|
|
5227
|
+
const { exactPrefixes, dirPrefixes } = buildPrefixSets(hostname2);
|
|
5228
|
+
const changes = [];
|
|
5229
|
+
const tokens = raw.split("\0");
|
|
5230
|
+
if (tokens.at(-1) === "") tokens.pop();
|
|
5231
|
+
let i = 0;
|
|
5232
|
+
while (i < tokens.length) {
|
|
5233
|
+
const statusToken = tokens[i++] ?? "";
|
|
5234
|
+
if (statusToken === "") continue;
|
|
5235
|
+
const firstLetter = statusToken[0] ?? "";
|
|
5236
|
+
const isRenameOrCopy = firstLetter === "R" || firstLetter === "C";
|
|
5237
|
+
if (isRenameOrCopy) {
|
|
5238
|
+
i++;
|
|
5239
|
+
const newPath = tokens[i++] ?? "";
|
|
5240
|
+
if (isInScope(newPath, exactPrefixes, dirPrefixes)) {
|
|
5241
|
+
changes.push({ status: firstLetter, label: labelForStatus(statusToken), path: newPath });
|
|
5242
|
+
}
|
|
5243
|
+
} else {
|
|
5244
|
+
const filePath = tokens[i++] ?? "";
|
|
5245
|
+
if (isInScope(filePath, exactPrefixes, dirPrefixes)) {
|
|
5246
|
+
changes.push({ status: firstLetter, label: labelForStatus(statusToken), path: filePath });
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
}
|
|
5250
|
+
return changes;
|
|
5251
|
+
}
|
|
5252
|
+
|
|
5140
5253
|
// src/commands.push.ts
|
|
5141
5254
|
init_push_leak_verdict();
|
|
5142
5255
|
init_push_checks();
|
|
@@ -5146,7 +5259,7 @@ init_color();
|
|
|
5146
5259
|
init_config();
|
|
5147
5260
|
init_config_sharedDirs_guard();
|
|
5148
5261
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
5149
|
-
import { copyFileSync, existsSync as existsSync36, mkdirSync as mkdirSync9, readdirSync as
|
|
5262
|
+
import { copyFileSync, existsSync as existsSync36, mkdirSync as mkdirSync9, readdirSync as readdirSync12, rmSync as rmSync12 } from "node:fs";
|
|
5150
5263
|
import { homedir as homedir5 } from "node:os";
|
|
5151
5264
|
import { join as join42 } from "node:path";
|
|
5152
5265
|
init_push_leak_verdict();
|
|
@@ -5166,7 +5279,7 @@ function stageSessions(tmpRoot, map) {
|
|
|
5166
5279
|
const localProjects = join42(claudeHome(), "projects");
|
|
5167
5280
|
if (!existsSync36(localProjects)) return 0;
|
|
5168
5281
|
let staged = 0;
|
|
5169
|
-
for (const dir of
|
|
5282
|
+
for (const dir of readdirSync12(localProjects)) {
|
|
5170
5283
|
const logical = reverse.get(dir);
|
|
5171
5284
|
if (!logical) continue;
|
|
5172
5285
|
copyDirJsonlOnly(join42(localProjects, dir), join42(tmpRoot, "shared", "projects", logical));
|
|
@@ -5242,6 +5355,7 @@ function guardGitlinks(repo) {
|
|
|
5242
5355
|
}
|
|
5243
5356
|
async function commitAndPush(st, ts, map, redactAll, allowAll, allowRule, repo) {
|
|
5244
5357
|
gitOrFatal(["add", "-A"], "git add", repo);
|
|
5358
|
+
st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: true });
|
|
5245
5359
|
let verdict = withSpinner("Scanning for secrets", () => scanPushVerdict(repo));
|
|
5246
5360
|
if (verdict.leak) {
|
|
5247
5361
|
renderPushTree(st, verdict);
|
|
@@ -5251,7 +5365,8 @@ async function commitAndPush(st, ts, map, redactAll, allowAll, allowRule, repo)
|
|
|
5251
5365
|
withSpinner("Pushing", () => gitOrFatal(["push"], "git push", repo));
|
|
5252
5366
|
renderPushTree(st, verdict);
|
|
5253
5367
|
}
|
|
5254
|
-
function runDryRunPreview(st, map) {
|
|
5368
|
+
function runDryRunPreview(st, map, repo) {
|
|
5369
|
+
st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: false });
|
|
5255
5370
|
if (map === null) {
|
|
5256
5371
|
renderNoScanTree(st, { noMapHint: true });
|
|
5257
5372
|
return;
|
|
@@ -5293,7 +5408,7 @@ async function cmdPush(opts = {}) {
|
|
|
5293
5408
|
const ts = freshBackupTs(backup);
|
|
5294
5409
|
const remap = withSpinner("Syncing sessions", () => remapPush(ts, { dryRun }));
|
|
5295
5410
|
const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
|
|
5296
|
-
const st = { dryRun, remap, extras };
|
|
5411
|
+
const st = { dryRun, remap, extras, globalConfig: [] };
|
|
5297
5412
|
guardGitlinks(repo);
|
|
5298
5413
|
const status = gitStatusPorcelainZ(repo, { untrackedAll: true });
|
|
5299
5414
|
if (!dryRun && !status) {
|
|
@@ -5303,12 +5418,12 @@ async function cmdPush(opts = {}) {
|
|
|
5303
5418
|
}
|
|
5304
5419
|
const mapPath = join43(repo, "path-map.json");
|
|
5305
5420
|
if (!existsSync37(mapPath)) {
|
|
5306
|
-
if (dryRun) return runDryRunPreview(st, null);
|
|
5421
|
+
if (dryRun) return runDryRunPreview(st, null, repo);
|
|
5307
5422
|
die("path-map.json missing, cannot enforce push allow-list");
|
|
5308
5423
|
}
|
|
5309
5424
|
const map = readPathMap(mapPath);
|
|
5310
5425
|
if (status) enforceAllowList(status, map);
|
|
5311
|
-
if (dryRun) return runDryRunPreview(st, map);
|
|
5426
|
+
if (dryRun) return runDryRunPreview(st, map, repo);
|
|
5312
5427
|
await commitAndPush(st, ts, map, redactAll, allowAll, allowRule, repo);
|
|
5313
5428
|
} catch (err) {
|
|
5314
5429
|
if (err instanceof NomadFatal) {
|
|
@@ -5323,9 +5438,17 @@ async function cmdPush(opts = {}) {
|
|
|
5323
5438
|
}
|
|
5324
5439
|
|
|
5325
5440
|
// src/commands.update.ts
|
|
5326
|
-
import { execFileSync as
|
|
5441
|
+
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
5327
5442
|
init_utils();
|
|
5328
|
-
function
|
|
5443
|
+
function readInstalledVersion(run = execFileSync16) {
|
|
5444
|
+
try {
|
|
5445
|
+
return run("nomad", ["--version"], { encoding: "utf8" }).toString().trim() || null;
|
|
5446
|
+
} catch {
|
|
5447
|
+
return null;
|
|
5448
|
+
}
|
|
5449
|
+
}
|
|
5450
|
+
function cmdUpdate(run = execFileSync16) {
|
|
5451
|
+
console.log("Updating claude-nomad CLI via npm...");
|
|
5329
5452
|
try {
|
|
5330
5453
|
run("npm", ["update", "-g", "claude-nomad"], { stdio: "inherit" });
|
|
5331
5454
|
} catch (err) {
|
|
@@ -5335,6 +5458,12 @@ function cmdUpdate(run = execFileSync15) {
|
|
|
5335
5458
|
}
|
|
5336
5459
|
throw new NomadFatal(`npm update -g claude-nomad failed: ${e.message}`);
|
|
5337
5460
|
}
|
|
5461
|
+
const version = readInstalledVersion(run);
|
|
5462
|
+
if (version) {
|
|
5463
|
+
console.log(`claude-nomad is now at v${version}`);
|
|
5464
|
+
} else {
|
|
5465
|
+
console.log('Update complete. Run "nomad --version" to confirm the new version.');
|
|
5466
|
+
}
|
|
5338
5467
|
}
|
|
5339
5468
|
|
|
5340
5469
|
// src/nomad.ts
|
|
@@ -5372,14 +5501,14 @@ import { join as join46 } from "node:path";
|
|
|
5372
5501
|
|
|
5373
5502
|
// src/init.gh-onboard.ts
|
|
5374
5503
|
init_config();
|
|
5375
|
-
import { execFileSync as
|
|
5504
|
+
import { execFileSync as execFileSync17 } from "node:child_process";
|
|
5376
5505
|
init_utils();
|
|
5377
5506
|
var DEFAULT_REPO_NAME = "claude-nomad-config";
|
|
5378
5507
|
function isValidRepoName(name) {
|
|
5379
5508
|
return /^[A-Za-z0-9._-]{1,100}$/.test(name);
|
|
5380
5509
|
}
|
|
5381
5510
|
var GH_NETWORK_TIMEOUT_MS = 3e4;
|
|
5382
|
-
function ensureOriginRepo(repoName, run =
|
|
5511
|
+
function ensureOriginRepo(repoName, run = execFileSync17) {
|
|
5383
5512
|
if (!isValidRepoName(repoName)) {
|
|
5384
5513
|
die(
|
|
5385
5514
|
`invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
|
|
@@ -5830,7 +5959,7 @@ function parsePushArgs(argv) {
|
|
|
5830
5959
|
// package.json
|
|
5831
5960
|
var package_default = {
|
|
5832
5961
|
name: "claude-nomad",
|
|
5833
|
-
version: "0.
|
|
5962
|
+
version: "0.48.0",
|
|
5834
5963
|
type: "module",
|
|
5835
5964
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
5836
5965
|
keywords: [
|
|
@@ -6031,7 +6160,7 @@ var DEFAULT_HELP = [
|
|
|
6031
6160
|
init_config();
|
|
6032
6161
|
init_utils();
|
|
6033
6162
|
init_utils_json();
|
|
6034
|
-
import { existsSync as existsSync41, readFileSync as readFileSync14, readdirSync as
|
|
6163
|
+
import { existsSync as existsSync41, readFileSync as readFileSync14, readdirSync as readdirSync13 } from "node:fs";
|
|
6035
6164
|
import { join as join47 } from "node:path";
|
|
6036
6165
|
function resumeCmd(sessionId) {
|
|
6037
6166
|
if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
|
|
@@ -6076,7 +6205,7 @@ function resumeCmd(sessionId) {
|
|
|
6076
6205
|
console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
|
|
6077
6206
|
}
|
|
6078
6207
|
function findTranscriptPath(projectsRoot, sessionId) {
|
|
6079
|
-
for (const dir of
|
|
6208
|
+
for (const dir of readdirSync13(projectsRoot)) {
|
|
6080
6209
|
const candidate = join47(projectsRoot, dir, `${sessionId}.jsonl`);
|
|
6081
6210
|
if (existsSync41(candidate)) return candidate;
|
|
6082
6211
|
}
|
package/package.json
CHANGED