claude-nomad 0.25.1 → 0.25.3
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 +20 -0
- package/README.md +8 -2
- package/package.json +1 -1
- package/src/commands.doctor.checks.repo.ts +38 -6
- package/src/commands.doctor.version.ts +7 -7
- package/src/config.ts +1 -0
- package/src/utils.lockfile.ts +39 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.25.3](https://github.com/funkadelic/claude-nomad/compare/v0.25.2...v0.25.3) (2026-05-26)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Changed
|
|
7
|
+
|
|
8
|
+
* **config:** add skipAutoPermissionPrompt to KNOWN_SETTINGS_KEYS ([#142](https://github.com/funkadelic/claude-nomad/issues/142)) ([09b4d7c](https://github.com/funkadelic/claude-nomad/commit/09b4d7c27d74cc922f9c516296ca6123e1ee84f5))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* document runtime deps and foreground the security posture ([#145](https://github.com/funkadelic/claude-nomad/issues/145)) ([fb029ee](https://github.com/funkadelic/claude-nomad/commit/fb029eec1a6d915224a39cc2755ff76e42618365))
|
|
14
|
+
* refresh hero.svg tagline and doctor mockup ([#144](https://github.com/funkadelic/claude-nomad/issues/144)) ([58a0176](https://github.com/funkadelic/claude-nomad/commit/58a01760e8d977ce4cf46dcd3137c02f3bc6541c))
|
|
15
|
+
|
|
16
|
+
## [0.25.2](https://github.com/funkadelic/claude-nomad/compare/v0.25.1...v0.25.2) (2026-05-26)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
* pre-existing robustness fixes from PR [#136](https://github.com/funkadelic/claude-nomad/issues/136) review ([#140](https://github.com/funkadelic/claude-nomad/issues/140)) ([bf4f443](https://github.com/funkadelic/claude-nomad/commit/bf4f443c50a47efafbb350d45d0e5d6883374f8d))
|
|
22
|
+
|
|
3
23
|
## [0.25.1](https://github.com/funkadelic/claude-nomad/compare/v0.25.0...v0.25.1) (2026-05-26)
|
|
4
24
|
|
|
5
25
|
|
package/README.md
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
**Your entire Claude Code setup, on every machine. History included,
|
|
9
|
+
**Your entire Claude Code setup, on every machine. History included, every push secret-scanned.**
|
|
10
10
|
|
|
11
11
|
Open Claude Code on a second machine and it is a blank slate: none of your custom agents, slash commands, tuned settings, or past conversations. claude-nomad keeps all of it in sync through a private Git repo you control. `nomad push` on one machine, `nomad pull` on the next, and everything is there, conversations included.
|
|
12
12
|
|
|
13
13
|
- **Resume your sessions on any machine.** Start a conversation on your desktop and pick it up on your laptop. claude-nomad remaps the file paths Claude Code embeds in every transcript, so your history follows you instead of getting stranded on the box where it started.
|
|
14
|
-
- **
|
|
14
|
+
- **Secret-scanned, private by default.** Your `~/.claude/` also holds OAuth tokens, MCP credentials, and the full text of every conversation, so claude-nomad is deliberate about what leaves your machine: credentials and ephemeral state never sync, only an explicit allow-list of paths is pushed, and everything that does go up is scanned by [gitleaks](https://github.com/gitleaks/gitleaks) before it leaves your machine; the push aborts on any hit. `nomad init` also disables Actions on your private mirror by default, so transcripts can't leak through CI logs.
|
|
15
15
|
- **One setup, every machine.** Your agents, skills, slash commands, and settings live in one place and follow you everywhere. Per-machine tweaks like model choice, MCP URLs, and env vars merge on top instead of clobbering your shared defaults.
|
|
16
16
|
|
|
17
17
|
Not dotfiles, not rsync. claude-nomad understands Claude Code's state, so your session history survives different file paths and your secrets never ride along.
|
|
@@ -215,8 +215,14 @@ Read these before adopting so you opt in with eyes open.
|
|
|
215
215
|
- Node.js 22.22.1 or newer (24 LTS recommended; the npm `engines` field declares the 22.22.1 floor and surfaces a warning on older runtimes - npm only blocks the install when `engine-strict=true` is configured)
|
|
216
216
|
- `tsx` (ships as a runtime dependency of the published package; no separate global install required)
|
|
217
217
|
- Git
|
|
218
|
+
- [`gitleaks`](https://github.com/gitleaks/gitleaks) (required for `nomad push`, which fail-fasts if it is not on PATH; `nomad doctor` also checks it against the pinned 8.30.x and warns when it is absent or mismatched)
|
|
218
219
|
- A **private** GitHub repo (or any Git remote you control)
|
|
219
220
|
|
|
221
|
+
**Optional:**
|
|
222
|
+
|
|
223
|
+
- `gh` (GitHub CLI), used only by `nomad init` to auto-disable Actions on the private repo; if it is missing or unauthenticated, init prints a manual fallback tip and continues
|
|
224
|
+
- `curl`, used only by the version/update check (the `nomad doctor` latest-release line and the post-`nomad update` check); it degrades silently when curl is absent or offline, so the rest of the CLI works without it
|
|
225
|
+
|
|
220
226
|
## Setup
|
|
221
227
|
|
|
222
228
|
**Why not just fork?** GitHub doesn't let you flip a public fork to private, and your config (especially session transcripts) must stay private. So the bootstrap is a one-time mirror-push into a fresh private repo, not a fork.
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, lstatSync } from 'node:fs';
|
|
1
|
+
import { existsSync, lstatSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -83,16 +83,48 @@ export function reportRepoState(section: DoctorSection): void {
|
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Emits a per-entry status line for each name in SHARED_LINKS
|
|
88
|
+
* (okGlyph/warnGlyph/failGlyph). A non-symlink blocks sync and FAILs via
|
|
89
|
+
* process.exitCode. TOCTOU-safe: lstatSync is wrapped in try/catch so a path
|
|
90
|
+
* that vanishes or becomes unreadable between the probe and the stat yields a
|
|
91
|
+
* row instead of an unhandled throw that aborts the whole doctor run. A symlink
|
|
92
|
+
* whose target cannot be resolved is a WARN (broken-symlink for a missing
|
|
93
|
+
* target, target-unreadable otherwise), never a healthy OK, so a dangling or
|
|
94
|
+
* unreadable link is not masked.
|
|
95
|
+
*/
|
|
87
96
|
export function reportSharedLinks(section: DoctorSection): void {
|
|
88
97
|
for (const name of SHARED_LINKS) {
|
|
89
98
|
const p = join(CLAUDE_HOME, name);
|
|
90
|
-
|
|
91
|
-
|
|
99
|
+
let stat;
|
|
100
|
+
try {
|
|
101
|
+
stat = lstatSync(p);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
104
|
+
if (code === 'ENOENT') {
|
|
105
|
+
addItem(section, `${yellow(warnGlyph)} ${name}: missing`);
|
|
106
|
+
} else {
|
|
107
|
+
addItem(section, `${red(failGlyph)} ${name}: could not stat (${String(code)})`);
|
|
108
|
+
process.exitCode = 1;
|
|
109
|
+
}
|
|
92
110
|
continue;
|
|
93
111
|
}
|
|
94
|
-
if (
|
|
95
|
-
|
|
112
|
+
if (stat.isSymbolicLink()) {
|
|
113
|
+
try {
|
|
114
|
+
// statSync follows the link; a throw means the target does not resolve.
|
|
115
|
+
statSync(p);
|
|
116
|
+
addItem(section, `${green(okGlyph)} ${name}: symlink`);
|
|
117
|
+
} catch (err) {
|
|
118
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
119
|
+
if (code === 'ENOENT') {
|
|
120
|
+
addItem(section, `${yellow(warnGlyph)} ${name}: broken symlink (target missing)`);
|
|
121
|
+
} else {
|
|
122
|
+
addItem(
|
|
123
|
+
section,
|
|
124
|
+
`${yellow(warnGlyph)} ${name}: symlink target unreadable (${String(code)})`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
96
128
|
} else {
|
|
97
129
|
addItem(section, `${red(failGlyph)} ${name}: NOT a symlink (blocks sync)`);
|
|
98
130
|
process.exitCode = 1;
|
|
@@ -123,13 +123,13 @@ function saveCache(latest: string): void {
|
|
|
123
123
|
/**
|
|
124
124
|
* Fetch the latest release tag from the upstream GitHub releases API. Uses
|
|
125
125
|
* `execFileSync('curl', ...)` rather than `node:https` because curl honors
|
|
126
|
-
* system proxies
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
* redirects (`-L`). Returns `null` on ANY failure path
|
|
130
|
-
* `tag_name` field or a tag that fails
|
|
131
|
-
* leading `v` strip. Release tags ship as
|
|
132
|
-
* `release-please-config.json`'s `include-v-in-tag: true`.
|
|
126
|
+
* system proxies and respects the `-m` timeout reliably. curl is optional, not
|
|
127
|
+
* a hard dependency: this is its only consumer, so a host without curl simply
|
|
128
|
+
* skips the version line. 3-second timeout, fail-fast on non-2xx (`-f`), silent
|
|
129
|
+
* (`-s`), follow redirects (`-L`). Returns `null` on ANY failure path (curl
|
|
130
|
+
* missing from PATH, a missing `tag_name` field, or a tag that fails
|
|
131
|
+
* strict-semver validation after the leading `v` strip). Release tags ship as
|
|
132
|
+
* `v<semver>` per `release-please-config.json`'s `include-v-in-tag: true`.
|
|
133
133
|
*/
|
|
134
134
|
function fetchLatestTag(): string | null {
|
|
135
135
|
try {
|
package/src/config.ts
CHANGED
package/src/utils.lockfile.ts
CHANGED
|
@@ -22,7 +22,26 @@ export function acquireLock(verb: string): LockHandle | null {
|
|
|
22
22
|
mkdirSync(dirname(LOCK_PATH), { recursive: true });
|
|
23
23
|
try {
|
|
24
24
|
const fd = openSync(LOCK_PATH, 'wx');
|
|
25
|
-
|
|
25
|
+
try {
|
|
26
|
+
writeFileSync(fd, String(process.pid));
|
|
27
|
+
} catch (writeErr) {
|
|
28
|
+
// PID-write failed after the lock file was created. Best-effort cleanup:
|
|
29
|
+
// close the fd (ignore errors), then unlink the orphaned lock file
|
|
30
|
+
// (ignore ANY unlink failure). The original write error is rethrown
|
|
31
|
+
// unconditionally, so a cleanup failure can never mask it and non-EEXIST
|
|
32
|
+
// throw semantics are preserved.
|
|
33
|
+
try {
|
|
34
|
+
closeSync(fd);
|
|
35
|
+
} catch {
|
|
36
|
+
/* already closed; ignore */
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
unlinkSync(LOCK_PATH);
|
|
40
|
+
} catch {
|
|
41
|
+
/* best-effort cleanup; the original write failure takes precedence */
|
|
42
|
+
}
|
|
43
|
+
throw writeErr;
|
|
44
|
+
}
|
|
26
45
|
return { fd };
|
|
27
46
|
} catch (err) {
|
|
28
47
|
const code = (err as NodeJS.ErrnoException).code;
|
|
@@ -122,7 +141,25 @@ function checkStaleAndRetry(verb: string): LockHandle | null {
|
|
|
122
141
|
function retryOnce(verb: string): LockHandle | null {
|
|
123
142
|
try {
|
|
124
143
|
const fd = openSync(LOCK_PATH, 'wx');
|
|
125
|
-
|
|
144
|
+
try {
|
|
145
|
+
writeFileSync(fd, String(process.pid));
|
|
146
|
+
} catch {
|
|
147
|
+
// Twin of the acquireLock write-failure guard. Best-effort cleanup: close
|
|
148
|
+
// the fd (ignore errors) and unlink the orphaned lock file (ignore ANY
|
|
149
|
+
// unlink failure). retryOnce's null-on-failure contract is preserved.
|
|
150
|
+
try {
|
|
151
|
+
closeSync(fd);
|
|
152
|
+
} catch {
|
|
153
|
+
/* already closed; ignore */
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
unlinkSync(LOCK_PATH);
|
|
157
|
+
} catch {
|
|
158
|
+
/* best-effort cleanup; the null return below is the contract */
|
|
159
|
+
}
|
|
160
|
+
warn(`another nomad ${verb} running, skipping`);
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
126
163
|
return { fd };
|
|
127
164
|
} catch {
|
|
128
165
|
warn(`another nomad ${verb} running, skipping`);
|