claude-nomad 0.47.1 → 0.49.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/README.md +17 -0
- package/dist/nomad.mjs +105 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.49.0](https://github.com/funkadelic/claude-nomad/compare/v0.48.0...v0.49.0) (2026-06-10)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
* **plugin:** add companion Claude Code plugin ([#281](https://github.com/funkadelic/claude-nomad/issues/281)) ([af85994](https://github.com/funkadelic/claude-nomad/commit/af859940453b6f9419d26072be4b0e04aaea1e8f))
|
|
9
|
+
|
|
10
|
+
## [0.48.0](https://github.com/funkadelic/claude-nomad/compare/v0.47.1...v0.48.0) (2026-06-09)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
* **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))
|
|
16
|
+
* **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))
|
|
17
|
+
|
|
3
18
|
## [0.47.1](https://github.com/funkadelic/claude-nomad/compare/v0.47.0...v0.47.1) (2026-06-09)
|
|
4
19
|
|
|
5
20
|
|
package/README.md
CHANGED
|
@@ -111,6 +111,21 @@ commits on a `nomad/stranded-<ts>` branch, and resets to `origin/main`, then re-
|
|
|
111
111
|
any stranded or dirty tracked changes touch synced config (shared/, hosts/, path-map.json), so
|
|
112
112
|
config you care about is never silently discarded.
|
|
113
113
|
|
|
114
|
+
## Claude Code plugin
|
|
115
|
+
|
|
116
|
+
An optional companion plugin puts nomad one slash away inside Claude Code and warns you at session
|
|
117
|
+
start when your synced setup has drifted. It is a thin layer over the CLI: install `claude-nomad`
|
|
118
|
+
first, then add the plugin.
|
|
119
|
+
|
|
120
|
+
```text
|
|
121
|
+
/plugin marketplace add funkadelic/claude-nomad
|
|
122
|
+
/plugin install nomad@claude-nomad
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
It adds `/nomad:pull`, `/nomad:diff`, `/nomad:push` (preview only), `/nomad:doctor`, and
|
|
126
|
+
`/nomad:clean`, plus a session-start drift check. See the
|
|
127
|
+
[plugin guide](https://funkadelic.github.io/claude-nomad/plugin/) for details.
|
|
128
|
+
|
|
114
129
|
## Requirements
|
|
115
130
|
|
|
116
131
|
- Node.js 22.22.1 or newer (24 LTS recommended)
|
|
@@ -128,6 +143,8 @@ version-staleness check and `nomad doctor --check-schema`. The CLI works without
|
|
|
128
143
|
- [Setup and migration](https://funkadelic.github.io/claude-nomad/quickstart/) -- full setup
|
|
129
144
|
walkthrough, migrating an existing `~/.claude/`
|
|
130
145
|
- [Commands reference](https://funkadelic.github.io/claude-nomad/commands/) -- all CLI flags
|
|
146
|
+
- [Claude Code plugin](https://funkadelic.github.io/claude-nomad/plugin/) -- /nomad slash commands
|
|
147
|
+
and the session-start drift check
|
|
131
148
|
- [Recovery flows](https://funkadelic.github.io/claude-nomad/recovery/) -- backups, drop-session,
|
|
132
149
|
redact, gitleaks allowlist, non-interactive allow
|
|
133
150
|
- [FAQ](https://funkadelic.github.io/claude-nomad/faq/) -- common questions, like the right
|
package/dist/nomad.mjs
CHANGED
|
@@ -1920,7 +1920,7 @@ function reportBackupsCheck(section2, backupBase2 = backupBase()) {
|
|
|
1920
1920
|
if (count > DOCTOR_BACKUP_COUNT_WARN || sizeMb > DOCTOR_BACKUP_SIZE_WARN_MB) {
|
|
1921
1921
|
addItem(
|
|
1922
1922
|
section2,
|
|
1923
|
-
`${yellow(warnGlyph)} backups: ${count} dirs / ${sizeMb.toFixed(1)} MB (run 'nomad clean --backups')`
|
|
1923
|
+
`${yellow(warnGlyph)} backups: ${count} dirs / ${sizeMb.toFixed(1)} MB (run 'nomad clean --backups --keep <N>'; bare --backups only prunes dirs older than 14d)`
|
|
1924
1924
|
);
|
|
1925
1925
|
}
|
|
1926
1926
|
}
|
|
@@ -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
|
];
|
|
@@ -5171,6 +5179,77 @@ function enforceAllowList(statusPorcelain, map) {
|
|
|
5171
5179
|
throw new NomadFatal("push allow-list violations");
|
|
5172
5180
|
}
|
|
5173
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
|
+
|
|
5174
5253
|
// src/commands.push.ts
|
|
5175
5254
|
init_push_leak_verdict();
|
|
5176
5255
|
init_push_checks();
|
|
@@ -5276,6 +5355,7 @@ function guardGitlinks(repo) {
|
|
|
5276
5355
|
}
|
|
5277
5356
|
async function commitAndPush(st, ts, map, redactAll, allowAll, allowRule, repo) {
|
|
5278
5357
|
gitOrFatal(["add", "-A"], "git add", repo);
|
|
5358
|
+
st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: true });
|
|
5279
5359
|
let verdict = withSpinner("Scanning for secrets", () => scanPushVerdict(repo));
|
|
5280
5360
|
if (verdict.leak) {
|
|
5281
5361
|
renderPushTree(st, verdict);
|
|
@@ -5285,7 +5365,8 @@ async function commitAndPush(st, ts, map, redactAll, allowAll, allowRule, repo)
|
|
|
5285
5365
|
withSpinner("Pushing", () => gitOrFatal(["push"], "git push", repo));
|
|
5286
5366
|
renderPushTree(st, verdict);
|
|
5287
5367
|
}
|
|
5288
|
-
function runDryRunPreview(st, map) {
|
|
5368
|
+
function runDryRunPreview(st, map, repo) {
|
|
5369
|
+
st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: false });
|
|
5289
5370
|
if (map === null) {
|
|
5290
5371
|
renderNoScanTree(st, { noMapHint: true });
|
|
5291
5372
|
return;
|
|
@@ -5327,7 +5408,7 @@ async function cmdPush(opts = {}) {
|
|
|
5327
5408
|
const ts = freshBackupTs(backup);
|
|
5328
5409
|
const remap = withSpinner("Syncing sessions", () => remapPush(ts, { dryRun }));
|
|
5329
5410
|
const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
|
|
5330
|
-
const st = { dryRun, remap, extras };
|
|
5411
|
+
const st = { dryRun, remap, extras, globalConfig: [] };
|
|
5331
5412
|
guardGitlinks(repo);
|
|
5332
5413
|
const status = gitStatusPorcelainZ(repo, { untrackedAll: true });
|
|
5333
5414
|
if (!dryRun && !status) {
|
|
@@ -5337,12 +5418,12 @@ async function cmdPush(opts = {}) {
|
|
|
5337
5418
|
}
|
|
5338
5419
|
const mapPath = join43(repo, "path-map.json");
|
|
5339
5420
|
if (!existsSync37(mapPath)) {
|
|
5340
|
-
if (dryRun) return runDryRunPreview(st, null);
|
|
5421
|
+
if (dryRun) return runDryRunPreview(st, null, repo);
|
|
5341
5422
|
die("path-map.json missing, cannot enforce push allow-list");
|
|
5342
5423
|
}
|
|
5343
5424
|
const map = readPathMap(mapPath);
|
|
5344
5425
|
if (status) enforceAllowList(status, map);
|
|
5345
|
-
if (dryRun) return runDryRunPreview(st, map);
|
|
5426
|
+
if (dryRun) return runDryRunPreview(st, map, repo);
|
|
5346
5427
|
await commitAndPush(st, ts, map, redactAll, allowAll, allowRule, repo);
|
|
5347
5428
|
} catch (err) {
|
|
5348
5429
|
if (err instanceof NomadFatal) {
|
|
@@ -5357,9 +5438,17 @@ async function cmdPush(opts = {}) {
|
|
|
5357
5438
|
}
|
|
5358
5439
|
|
|
5359
5440
|
// src/commands.update.ts
|
|
5360
|
-
import { execFileSync as
|
|
5441
|
+
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
5361
5442
|
init_utils();
|
|
5362
|
-
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...");
|
|
5363
5452
|
try {
|
|
5364
5453
|
run("npm", ["update", "-g", "claude-nomad"], { stdio: "inherit" });
|
|
5365
5454
|
} catch (err) {
|
|
@@ -5369,6 +5458,12 @@ function cmdUpdate(run = execFileSync15) {
|
|
|
5369
5458
|
}
|
|
5370
5459
|
throw new NomadFatal(`npm update -g claude-nomad failed: ${e.message}`);
|
|
5371
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
|
+
}
|
|
5372
5467
|
}
|
|
5373
5468
|
|
|
5374
5469
|
// src/nomad.ts
|
|
@@ -5406,14 +5501,14 @@ import { join as join46 } from "node:path";
|
|
|
5406
5501
|
|
|
5407
5502
|
// src/init.gh-onboard.ts
|
|
5408
5503
|
init_config();
|
|
5409
|
-
import { execFileSync as
|
|
5504
|
+
import { execFileSync as execFileSync17 } from "node:child_process";
|
|
5410
5505
|
init_utils();
|
|
5411
5506
|
var DEFAULT_REPO_NAME = "claude-nomad-config";
|
|
5412
5507
|
function isValidRepoName(name) {
|
|
5413
5508
|
return /^[A-Za-z0-9._-]{1,100}$/.test(name);
|
|
5414
5509
|
}
|
|
5415
5510
|
var GH_NETWORK_TIMEOUT_MS = 3e4;
|
|
5416
|
-
function ensureOriginRepo(repoName, run =
|
|
5511
|
+
function ensureOriginRepo(repoName, run = execFileSync17) {
|
|
5417
5512
|
if (!isValidRepoName(repoName)) {
|
|
5418
5513
|
die(
|
|
5419
5514
|
`invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
|
|
@@ -5864,7 +5959,7 @@ function parsePushArgs(argv) {
|
|
|
5864
5959
|
// package.json
|
|
5865
5960
|
var package_default = {
|
|
5866
5961
|
name: "claude-nomad",
|
|
5867
|
-
version: "0.
|
|
5962
|
+
version: "0.49.0",
|
|
5868
5963
|
type: "module",
|
|
5869
5964
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
5870
5965
|
keywords: [
|
package/package.json
CHANGED