claude-nomad 0.25.1 → 0.25.2
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 +7 -0
- package/package.json +1 -1
- package/src/commands.doctor.checks.repo.ts +38 -6
- package/src/utils.lockfile.ts +39 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.25.2](https://github.com/funkadelic/claude-nomad/compare/v0.25.1...v0.25.2) (2026-05-26)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Fixed
|
|
7
|
+
|
|
8
|
+
* 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))
|
|
9
|
+
|
|
3
10
|
## [0.25.1](https://github.com/funkadelic/claude-nomad/compare/v0.25.0...v0.25.1) (2026-05-26)
|
|
4
11
|
|
|
5
12
|
|
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;
|
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`);
|