claude-nomad 0.66.0 → 0.67.1
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 +87 -140
- package/dist/nomad.mjs +226 -149
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,58 +1,77 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.67.1](https://github.com/funkadelic/claude-nomad/compare/v0.67.0...v0.67.1) (2026-08-20)
|
|
4
|
+
|
|
5
|
+
### What's new
|
|
6
|
+
|
|
7
|
+
**On all supported platforms, i.e. \*nix, Mac, WSL2, and native Windows.**
|
|
8
|
+
|
|
9
|
+
- **A project in a folder named `sessions`, `tasks`, `plans`, or `cache` now syncs its own history.** Those names, along with `debug`, `jobs`, `security`, and a few more, belong to nomad's own working folders, so a project of yours that happened to share one was refused as though it held something private. Nothing in it was, and the name alone decided it. Those projects now sync like any other, and nomad still checks what is actually inside a folder exactly as before.
|
|
10
|
+
- **A per-host settings file sitting at the very top of a folder you share now stays out of your sync repo.** Inside a folder you opted into sharing, nomad already blocked `settings.local.json` and anything else shaped like a credential file at every level except the very top, where one could slip past and reach your other machines. The top level is now covered too.
|
|
11
|
+
|
|
12
|
+
**On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and none of these apply to it.
|
|
13
|
+
|
|
14
|
+
- **A pull no longer removes that project's transcripts from your sync repo.** The same name check ran during a pull, where it deleted rather than refused, so every pull stripped that project's saved history out of the sync repo. Each one left a backup under `~/.cache/claude-nomad/backup/`, but never put anything back. Pulls now leave those projects alone.
|
|
15
|
+
- **When a sync cannot remove or back up a file, it now tells you why.** These warnings sometimes printed the word `undefined` where the reason belongs, so you got a filename and nothing else. They now carry the real reason. A failure arriving in an unexpected form could also stop the cleanup partway through, leaving the files after the first problem unhandled and unmentioned. The pass now runs to the end and reports everything it did.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
* **config:** stop a project's own folder name from blocking its history ([#536](https://github.com/funkadelic/claude-nomad/issues/536)) ([1994c55](https://github.com/funkadelic/claude-nomad/commit/1994c55a32c62220e8d7291554f95099b2c12eba))
|
|
21
|
+
* **mirror:** name the reason in sync warnings instead of undefined ([#537](https://github.com/funkadelic/claude-nomad/issues/537)) ([e973ff3](https://github.com/funkadelic/claude-nomad/commit/e973ff392e369694e517c5aabfb26c996836378f))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
* **codeql:** exclude test files from security scanning ([#534](https://github.com/funkadelic/claude-nomad/issues/534)) ([c86a6d3](https://github.com/funkadelic/claude-nomad/commit/c86a6d3c80a640e0a251181c8c0461edef2375da))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Dependencies
|
|
30
|
+
|
|
31
|
+
* bump fast-uri from 3.1.4 to 3.1.5 ([#527](https://github.com/funkadelic/claude-nomad/issues/527)) ([6a8f6ba](https://github.com/funkadelic/claude-nomad/commit/6a8f6baa534dae9d9e6786acf96627c85bb8d400))
|
|
32
|
+
* bump js-yaml from 4.3.0 to 4.3.1 in /docs-site ([#529](https://github.com/funkadelic/claude-nomad/issues/529)) ([a4e541f](https://github.com/funkadelic/claude-nomad/commit/a4e541fd74100527306ebaa713a2a730ffe98af7))
|
|
33
|
+
* bump nanoid from 3.3.15 to 3.3.18 in /docs-site ([#531](https://github.com/funkadelic/claude-nomad/issues/531)) ([0a0f53e](https://github.com/funkadelic/claude-nomad/commit/0a0f53ee46e3b69babd82b84e39c6982791f8896))
|
|
34
|
+
* bump postcss from 8.5.15 to 8.5.26 in /docs-site ([#530](https://github.com/funkadelic/claude-nomad/issues/530)) ([413f75d](https://github.com/funkadelic/claude-nomad/commit/413f75db6c292785f6b7d67fbd629680d5b39e04))
|
|
35
|
+
* bump postcss from 8.5.16 to 8.5.26 ([#532](https://github.com/funkadelic/claude-nomad/issues/532)) ([b03bd62](https://github.com/funkadelic/claude-nomad/commit/b03bd62ab77249bdcc31c78ac2440da434b2de39))
|
|
36
|
+
* bump svgo from 4.0.1 to 4.0.2 in /docs-site ([#528](https://github.com/funkadelic/claude-nomad/issues/528)) ([a424693](https://github.com/funkadelic/claude-nomad/commit/a4246932d01a79082383d6a09e08e7542eb7a873))
|
|
37
|
+
|
|
38
|
+
## [0.67.0](https://github.com/funkadelic/claude-nomad/compare/v0.66.0...v0.67.0) (2026-08-18)
|
|
39
|
+
|
|
40
|
+
### What's new
|
|
41
|
+
|
|
42
|
+
**On all supported platforms, i.e. \*nix, Mac, WSL2, and native Windows.**
|
|
43
|
+
|
|
44
|
+
- **Your backup cache no longer collects empty folders.** Every sync saves a copy of anything it is about to overwrite into a dated folder under `~/.cache/claude-nomad/backup/`, and it creates that folder before it touches your files so a full disk or a permissions problem stops the sync before it has changed anything. A sync that turns out to have nothing to save now clears its own folder instead of leaving an empty one behind, and so does one that stops early.
|
|
45
|
+
- **`nomad clean --backups` removes empty backup folders whatever their age.** They hold nothing to recover, so neither the 14-day default nor `--keep <N>` keeps one alive, and they no longer use up the slots `--keep <N>` reserves: ask for the five newest and you get the five newest that actually hold something. Backups holding anything at all, a file or even a link, are left exactly as before. `nomad clean --backups --dry-run` always prints its preview, since it changes nothing and never waits on another command.
|
|
46
|
+
- **`nomad adopt` no longer runs at the same time as a pull, push, or sync.** It waits its turn the way those commands already wait for each other, so a cleanup or a sync running in another terminal cannot remove or overwrite a backup that adopt is still writing. If another one is already running, adopt says so and does nothing rather than starting a move it cannot finish safely. Previewing with `nomad adopt <name> --dry-run` never waits, since it only reads and prints.
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
* clean up the empty folders in your backup cache ([#525](https://github.com/funkadelic/claude-nomad/issues/525)) ([3f5ae92](https://github.com/funkadelic/claude-nomad/commit/3f5ae92e46173364f5e9c66e4963bcc716c813e6))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
* **ci:** base the drift branch on main on a dispatch run ([#524](https://github.com/funkadelic/claude-nomad/issues/524)) ([3e94c1e](https://github.com/funkadelic/claude-nomad/commit/3e94c1e2df9296a539c71eac873058cb69225d78))
|
|
57
|
+
|
|
3
58
|
## [0.66.0](https://github.com/funkadelic/claude-nomad/compare/v0.65.0...v0.66.0) (2026-08-17)
|
|
4
59
|
|
|
5
60
|
### What's new
|
|
6
61
|
|
|
7
62
|
**On all supported platforms, i.e. \*nix, Mac, WSL2, and native Windows.**
|
|
8
63
|
|
|
9
|
-
- **A folder you share now carries everything inside it.** If you share a folder through
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
too. `--dry-run` gives exactly the same answer as the real run, so you can check a folder before
|
|
21
|
-
committing to it.
|
|
22
|
-
- **An interrupted `nomad adopt` now explains itself instead of writing a crash report.** If
|
|
23
|
-
another program has the directory open, or its permissions block the move partway through, the
|
|
24
|
-
command says what happened, where your content ended up, and what to run next. Your content is
|
|
25
|
-
safe in every one of these cases: either it was never touched on this machine, or it is already
|
|
26
|
-
in the repo.
|
|
27
|
-
|
|
28
|
-
**On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and none of these apply to
|
|
29
|
-
it.
|
|
30
|
-
|
|
31
|
-
- **A push no longer publishes a folder you never asked to share.** Windows keeps your shared
|
|
32
|
-
config as real file copies rather than symlinks, and a push used to pick up any folder sitting in
|
|
33
|
-
your Claude directory and publish it to your other machines. Sharing is now the same deliberate
|
|
34
|
-
step it has always been on macOS and Linux: run `nomad adopt <name>` once, then push. Your
|
|
35
|
-
existing shared folders keep syncing exactly as before.
|
|
36
|
-
- **`nomad doctor --verbose` names a folder your repo does not carry yet.** It lists the local
|
|
37
|
-
copies that are still private to this machine, and the command that shares one.
|
|
38
|
-
- **One unreadable file no longer ends your pull.** A single config file that another program has
|
|
39
|
-
open, or that your account cannot read, used to stop the whole pull with a crash report. Nomad
|
|
40
|
-
now names the file and the reason, then carries on: your settings, your skills, and your session
|
|
41
|
-
history all still sync, and the command finishes normally. The warning also says whether the file
|
|
42
|
-
is still there or has been removed, and points at the backup copy when there is one.
|
|
43
|
-
- **`nomad adopt` recovers cleanly when it cannot restore your local copy.** Adopt finishes by
|
|
44
|
-
copying the folder back out of the sync repo so this machine keeps a usable copy. If that copy
|
|
45
|
-
was blocked, the command used to end in a crash report, with the folder gone from your Claude
|
|
46
|
-
directory and nothing staged for publishing. It now stops with a plain message naming the folder
|
|
47
|
-
and the reason, stages the adopted content either way, and tells you to run `nomad pull` before
|
|
48
|
-
your next push and why that order matters. A half written folder left behind by the interrupted
|
|
49
|
-
copy is removed, so a later push cannot replace your adopted folder with the partial one.
|
|
50
|
-
- **A refused `nomad pull --force-remote` now tells you what actually clears it.** The old advice
|
|
51
|
-
did not work, and would land you back on the same refusal. The message now gives the steps that
|
|
52
|
-
finish the job and names the one that clears the check, and it says that the stuck rebase has
|
|
53
|
-
already been undone by that point, so retrying is now just an ordinary pull. It no longer calls
|
|
54
|
-
every listed file at risk, since the list can include files where the shared repo is simply ahead
|
|
55
|
-
of yours. The quickstart says the same thing and links to the matching recovery steps in the FAQ.
|
|
64
|
+
- **A folder you share now carries everything inside it.** If you share a folder through `sharedDirs`, its subfolders travel with it, including ones with everyday names like `sessions`, `plans`, `tasks`, and `cache`. Those names used to be refused wherever they appeared, so a push could fail, or `nomad adopt` could turn you away, over a folder holding nothing sensitive. Your Claude login, your credential files, your per-host settings, and your local history still never leave the machine, in either direction.
|
|
65
|
+
- **`nomad adopt` checks the whole folder before it moves anything.** It used to copy everything, so a file that never syncs between machines could land in your repo and fail your next push, on content you never chose to add. Adopt now stops before touching anything and names every path it objected to and why: a name that collides with the never-sync list, which renaming clears, or a name that looks like a credential file, where renaming only helps if the new name looks different too. `--dry-run` gives exactly the same answer as the real run, so you can check a folder before committing to it.
|
|
66
|
+
- **An interrupted `nomad adopt` now explains itself instead of writing a crash report.** If another program has the directory open, or its permissions block the move partway through, the command says what happened, where your content ended up, and what to run next. Your content is safe in every one of these cases: either it was never touched on this machine, or it is already in the repo.
|
|
67
|
+
|
|
68
|
+
**On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and none of these apply to it.
|
|
69
|
+
|
|
70
|
+
- **A push no longer publishes a folder you never asked to share.** Windows keeps your shared config as real file copies rather than symlinks, and a push used to pick up any folder sitting in your Claude directory and publish it to your other machines. Sharing is now the same deliberate step it has always been on macOS and Linux: run `nomad adopt <name>` once, then push. Your existing shared folders keep syncing exactly as before.
|
|
71
|
+
- **`nomad doctor --verbose` names a folder your repo does not carry yet.** It lists the local copies that are still private to this machine, and the command that shares one.
|
|
72
|
+
- **One unreadable file no longer ends your pull.** A single config file that another program has open, or that your account cannot read, used to stop the whole pull with a crash report. Nomad now names the file and the reason, then carries on: your settings, your skills, and your session history all still sync, and the command finishes normally. The warning also says whether the file is still there or has been removed, and points at the backup copy when there is one.
|
|
73
|
+
- **`nomad adopt` recovers cleanly when it cannot restore your local copy.** Adopt finishes by copying the folder back out of the sync repo so this machine keeps a usable copy. If that copy was blocked, the command used to end in a crash report, with the folder gone from your Claude directory and nothing staged for publishing. It now stops with a plain message naming the folder and the reason, stages the adopted content either way, and tells you to run `nomad pull` before your next push and why that order matters. A half written folder left behind by the interrupted copy is removed, so a later push cannot replace your adopted folder with the partial one.
|
|
74
|
+
- **A refused `nomad pull --force-remote` now tells you what actually clears it.** The old advice did not work, and would land you back on the same refusal. The message now gives the steps that finish the job and names the one that clears the check, and it says that the stuck rebase has already been undone by that point, so retrying is now just an ordinary pull. It no longer calls every listed file at risk, since the list can include files where the shared repo is simply ahead of yours. The quickstart says the same thing and links to the matching recovery steps in the FAQ.
|
|
56
75
|
|
|
57
76
|
|
|
58
77
|
### Added
|
|
@@ -81,39 +100,16 @@ it.
|
|
|
81
100
|
|
|
82
101
|
### What's new
|
|
83
102
|
|
|
84
|
-
**On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and none of these apply to
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
-
|
|
88
|
-
|
|
89
|
-
the step that saves your local shared-config edits into the repo, every time it ran, even on a
|
|
90
|
-
healthy machine with nothing to rescue. That was the flag's only visible effect on a healthy repo,
|
|
91
|
-
and nothing told you it had happened. It now rescues a stuck repo and nothing else, and when a
|
|
92
|
-
rescue genuinely does replace your copies, the pull says how many and where your previous ones
|
|
93
|
-
were saved.
|
|
94
|
-
- **A pull now says what it did to your shared config.** On Windows your shared config is kept as
|
|
95
|
-
real file copies rather than symlinks, so every pull starts by copying your local edits into the
|
|
96
|
-
sync repo. That step used to be silent. It now prints a `Symlinks` section naming each file it
|
|
97
|
-
picked up, and each file it removed from the repo because you deleted it here. If a file cannot be
|
|
98
|
-
read at all, because another program is holding it open or its permissions changed, the pull names
|
|
99
|
-
it and says why rather than leaving a silent gap. `nomad diff` and `nomad pull --dry-run` read
|
|
100
|
-
from the same place, so a preview now matches what a real pull does.
|
|
101
|
-
- **Windows keeps the same things out of your sync repo that your other machines do.** The Windows
|
|
102
|
-
copy step used to hold back only files whose names looked like passwords or keys. It now skips
|
|
103
|
-
everything on the never-sync list, so the folders Claude Code uses for its own state, named things
|
|
104
|
-
like `sessions`, `tasks`, `plans`, and `cache`, stay on the machine even when they sit inside a
|
|
105
|
-
folder you share. If one of them is already sitting in your sync repo, the pull tells you, and
|
|
106
|
-
takes a backup copy before removing it.
|
|
103
|
+
**On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and none of these apply to it.
|
|
104
|
+
|
|
105
|
+
- **`nomad pull --force-remote` no longer discards edits you have not published.** The flag exists to rescue a sync repo that got stuck part-way through an update. On native Windows it also skipped the step that saves your local shared-config edits into the repo, every time it ran, even on a healthy machine with nothing to rescue. That was the flag's only visible effect on a healthy repo, and nothing told you it had happened. It now rescues a stuck repo and nothing else, and when a rescue genuinely does replace your copies, the pull says how many and where your previous ones were saved.
|
|
106
|
+
- **A pull now says what it did to your shared config.** On Windows your shared config is kept as real file copies rather than symlinks, so every pull starts by copying your local edits into the sync repo. That step used to be silent. It now prints a `Symlinks` section naming each file it picked up, and each file it removed from the repo because you deleted it here. If a file cannot be read at all, because another program is holding it open or its permissions changed, the pull names it and says why rather than leaving a silent gap. `nomad diff` and `nomad pull --dry-run` read from the same place, so a preview now matches what a real pull does.
|
|
107
|
+
- **Windows keeps the same things out of your sync repo that your other machines do.** The Windows copy step used to hold back only files whose names looked like passwords or keys. It now skips everything on the never-sync list, so the folders Claude Code uses for its own state, named things like `sessions`, `tasks`, `plans`, and `cache`, stay on the machine even when they sit inside a folder you share. If one of them is already sitting in your sync repo, the pull tells you, and takes a backup copy before removing it.
|
|
107
108
|
|
|
108
109
|
**On all supported platforms, i.e. \*nix, Mac, WSL2, and native Windows.**
|
|
109
110
|
|
|
110
|
-
- **`nomad doctor` names the files that have drifted.** When it reports that your skills or your
|
|
111
|
-
|
|
112
|
-
again with `--verbose` to find out which files it meant.
|
|
113
|
-
- **`nomad pull --force-remote` tells you when there was nothing to rescue.** On a repo that is not
|
|
114
|
-
stuck it now says so and pulls normally, instead of appearing to do nothing. If it cannot tell
|
|
115
|
-
whether the repo is stuck, it says that too rather than reporting a clean repo. The help text,
|
|
116
|
-
README, and docs site now agree on what the flag does.
|
|
111
|
+
- **`nomad doctor` names the files that have drifted.** When it reports that your skills or your shared config no longer match the repo, it lists them. Before, you got a count and had to run it again with `--verbose` to find out which files it meant.
|
|
112
|
+
- **`nomad pull --force-remote` tells you when there was nothing to rescue.** On a repo that is not stuck it now says so and pulls normally, instead of appearing to do nothing. If it cannot tell whether the repo is stuck, it says that too rather than reporting a clean repo. The help text, README, and docs site now agree on what the flag does.
|
|
117
113
|
|
|
118
114
|
|
|
119
115
|
### Added
|
|
@@ -152,21 +148,9 @@ it.
|
|
|
152
148
|
|
|
153
149
|
### What's new
|
|
154
150
|
|
|
155
|
-
- **A stray dot or space at the end of a file name no longer hides a secret.** nomad never copies
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
`.env.`, did not match, so a file like that could end up in your sync repo. It matches now, and so
|
|
159
|
-
do the other files nomad always keeps out, such as `settings.local.json`. Nothing changes in how
|
|
160
|
-
you use nomad. It simply catches more ways of writing the same names it already blocked.
|
|
161
|
-
- **A `.claude` folder is recognized however it is capitalized.** If a project had a folder named
|
|
162
|
-
`.Claude` instead of `.claude`, nomad checked what was inside it less carefully than it should
|
|
163
|
-
have. Both are treated the same now.
|
|
164
|
-
- **A folder name nomad will never sync is turned down as soon as you add it.** You can ask nomad to
|
|
165
|
-
sync extra folders by listing their names in `path-map.json`. If a name you list is one nomad
|
|
166
|
-
refuses to sync, because it looks like a password file, or because it is a name that causes
|
|
167
|
-
trouble on one of the machines you sync to, you are told straight away and told why. Before,
|
|
168
|
-
nothing was said until a later `nomad push` stopped without explaining itself. `nomad doctor` also
|
|
169
|
-
lists any names in this situation and what to do about them.
|
|
151
|
+
- **A stray dot or space at the end of a file name no longer hides a secret.** nomad never copies password and key files (things like `.env`, `id_rsa`, or `server.pem`) out of the project folders you sync. It spots them by name, and a name with an extra dot or space on the end, such as `.env.`, did not match, so a file like that could end up in your sync repo. It matches now, and so do the other files nomad always keeps out, such as `settings.local.json`. Nothing changes in how you use nomad. It simply catches more ways of writing the same names it already blocked.
|
|
152
|
+
- **A `.claude` folder is recognized however it is capitalized.** If a project had a folder named `.Claude` instead of `.claude`, nomad checked what was inside it less carefully than it should have. Both are treated the same now.
|
|
153
|
+
- **A folder name nomad will never sync is turned down as soon as you add it.** You can ask nomad to sync extra folders by listing their names in `path-map.json`. If a name you list is one nomad refuses to sync, because it looks like a password file, or because it is a name that causes trouble on one of the machines you sync to, you are told straight away and told why. Before, nothing was said until a later `nomad push` stopped without explaining itself. `nomad doctor` also lists any names in this situation and what to do about them.
|
|
170
154
|
|
|
171
155
|
All of this applies on macOS, Linux, WSL2, and native Windows.
|
|
172
156
|
|
|
@@ -185,31 +169,11 @@ All of this applies on macOS, Linux, WSL2, and native Windows.
|
|
|
185
169
|
|
|
186
170
|
### What's new
|
|
187
171
|
|
|
188
|
-
- **Native Windows: deleting a shared config file now sticks.** On native Windows (PowerShell or
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
is copied to the backup directory first. The first pull after upgrading has no record yet, so
|
|
194
|
-
deletions start propagating from the pull after that. macOS, Linux, and WSL2 were never affected:
|
|
195
|
-
they symlink shared config, so deleting the file was always a deletion in the repo.
|
|
196
|
-
- **Native Windows: a name clash during a pull now explains itself.** If a file you created inside a
|
|
197
|
-
synced folder has the same name as one an incoming update adds, the pull stops before changing
|
|
198
|
-
anything. You used to be left with git's own error, which pointed at a copy inside the sync repo
|
|
199
|
-
and told you to move it, and following that advice looped, because the next pull copied your file
|
|
200
|
-
back over it. You now get the file under `~/.claude/` to act on and the two ways to finish, keep
|
|
201
|
-
the incoming version or keep both and merge them, and nomad clears its own copy out of the way for
|
|
202
|
-
you. Your file is untouched either way.
|
|
203
|
-
- **Native Windows shared-config changes show up in a preview first.** `nomad pull --dry-run` and
|
|
204
|
-
`nomad diff` now list both the edits being carried into the repo and the files being removed from
|
|
205
|
-
it, worked out before the pull so the preview and the real run agree.
|
|
206
|
-
- **`nomad doctor` notices a native Windows shared file that has drifted.** When a synced copy under
|
|
207
|
-
`~/.claude/` no longer matches the one in the repo, doctor warns about it instead of reporting the
|
|
208
|
-
machine as healthy. On the symlink platforms there is only ever one file, so there is nothing to
|
|
209
|
-
drift.
|
|
210
|
-
- **`nomad doctor` no longer recognizes `leftArrowOpensAgents`.** Claude Code dropped it from its
|
|
211
|
-
settings schema, so doctor will flag it as an unknown key if you still have it set. This one
|
|
212
|
-
applies everywhere, not just on Windows.
|
|
172
|
+
- **Native Windows: deleting a shared config file now sticks.** On native Windows (PowerShell or cmd), deleting a file inside a synced folder, say `~/.claude/commands/old.md`, used to leave it sitting in the sync repo, and the next `nomad pull` put it straight back. The deletion is now carried into the sync repo instead, so it survives the pull and reaches your other machines. Nothing is removed unless this machine's own record shows it had the file, and every removed file is copied to the backup directory first. The first pull after upgrading has no record yet, so deletions start propagating from the pull after that. macOS, Linux, and WSL2 were never affected: they symlink shared config, so deleting the file was always a deletion in the repo.
|
|
173
|
+
- **Native Windows: a name clash during a pull now explains itself.** If a file you created inside a synced folder has the same name as one an incoming update adds, the pull stops before changing anything. You used to be left with git's own error, which pointed at a copy inside the sync repo and told you to move it, and following that advice looped, because the next pull copied your file back over it. You now get the file under `~/.claude/` to act on and the two ways to finish, keep the incoming version or keep both and merge them, and nomad clears its own copy out of the way for you. Your file is untouched either way.
|
|
174
|
+
- **Native Windows shared-config changes show up in a preview first.** `nomad pull --dry-run` and `nomad diff` now list both the edits being carried into the repo and the files being removed from it, worked out before the pull so the preview and the real run agree.
|
|
175
|
+
- **`nomad doctor` notices a native Windows shared file that has drifted.** When a synced copy under `~/.claude/` no longer matches the one in the repo, doctor warns about it instead of reporting the machine as healthy. On the symlink platforms there is only ever one file, so there is nothing to drift.
|
|
176
|
+
- **`nomad doctor` no longer recognizes `leftArrowOpensAgents`.** Claude Code dropped it from its settings schema, so doctor will flag it as an unknown key if you still have it set. This one applies everywhere, not just on Windows.
|
|
213
177
|
|
|
214
178
|
|
|
215
179
|
### Added
|
|
@@ -235,17 +199,9 @@ All of this applies on macOS, Linux, WSL2, and native Windows.
|
|
|
235
199
|
|
|
236
200
|
### What's new
|
|
237
201
|
|
|
238
|
-
- **`nomad doctor` stops warning about settings you legitimately set.** The list of settings it
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
are no longer reported as unknown keys on an otherwise healthy machine. Entries that were never
|
|
242
|
-
settings at all have been dropped at the same time.
|
|
243
|
-
- **A new Claude Code setting is recognized within a day.** The job that tracks the official
|
|
244
|
-
settings list ran once a week, so a setting added on a Tuesday could be reported as unknown until
|
|
245
|
-
the following Monday. It now runs daily.
|
|
246
|
-
- **The list keeps its own bookkeeping straight from here.** When a setting Claude Code writes
|
|
247
|
-
ahead of its published documentation is later documented, it now moves across on its own instead
|
|
248
|
-
of being recorded in two places at once.
|
|
202
|
+
- **`nomad doctor` stops warning about settings you legitimately set.** The list of settings it recognizes has been updated and synced with the current Claude Code settings schema, so settings the app actually writes (quiet hours, the break reminder, the token-usage reminder, and others) are no longer reported as unknown keys on an otherwise healthy machine. Entries that were never settings at all have been dropped at the same time.
|
|
203
|
+
- **A new Claude Code setting is recognized within a day.** The job that tracks the official settings list ran once a week, so a setting added on a Tuesday could be reported as unknown until the following Monday. It now runs daily.
|
|
204
|
+
- **The list keeps its own bookkeeping straight from here.** When a setting Claude Code writes ahead of its published documentation is later documented, it now moves across on its own instead of being recorded in two places at once.
|
|
249
205
|
|
|
250
206
|
|
|
251
207
|
### Fixed
|
|
@@ -276,18 +232,9 @@ All of this applies on macOS, Linux, WSL2, and native Windows.
|
|
|
276
232
|
|
|
277
233
|
### What's new
|
|
278
234
|
|
|
279
|
-
- **Windows: your own edits to shared config are no longer reverted.** On Windows, editing a synced
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
publish. Your edit is now carried into the sync repo first, so it survives the pull and gets
|
|
283
|
-
published instead.
|
|
284
|
-
- **The secret prompt on push is readable and asks once per secret.** When a push finds a possible
|
|
285
|
-
secret it used to ask the same question several times over for one secret, and the snippet it
|
|
286
|
-
showed was mostly noise. It now asks once per secret and describes what was actually found, so you
|
|
287
|
-
can tell a real credential from a false positive at a glance.
|
|
288
|
-
- **Three ways that prompt could show you a secret it meant to hide are closed**, along with a case
|
|
289
|
-
where allowing one false positive could quietly let a real secret through beside it on the same
|
|
290
|
-
line.
|
|
235
|
+
- **Windows: your own edits to shared config are no longer reverted.** On Windows, editing a synced file like `~/.claude/CLAUDE.md` and then running `nomad pull` overwrote it from the repo, and `nomad sync` went on to publish the reverted version, silently undoing the change you ran sync to publish. Your edit is now carried into the sync repo first, so it survives the pull and gets published instead.
|
|
236
|
+
- **The secret prompt on push is readable and asks once per secret.** When a push finds a possible secret it used to ask the same question several times over for one secret, and the snippet it showed was mostly noise. It now asks once per secret and describes what was actually found, so you can tell a real credential from a false positive at a glance.
|
|
237
|
+
- **Three ways that prompt could show you a secret it meant to hide are closed**, along with a case where allowing one false positive could quietly let a real secret through beside it on the same line.
|
|
291
238
|
|
|
292
239
|
|
|
293
240
|
### Added
|
package/dist/nomad.mjs
CHANGED
|
@@ -70,6 +70,9 @@ function regionKey(segment) {
|
|
|
70
70
|
function isExtrasScoped(segments) {
|
|
71
71
|
return segments[0] === "shared" && segments.length > 1 && regionKey(segments[1]) === "extras";
|
|
72
72
|
}
|
|
73
|
+
function isLogicalNameScoped(segments) {
|
|
74
|
+
return segments[0] === "shared" && segments.length > 1 && UNFILTERED_SHARED_REGIONS.has(regionKey(segments[1]));
|
|
75
|
+
}
|
|
73
76
|
function isSharedNameScoped(segments) {
|
|
74
77
|
return segments[0] === "shared" && segments.length > 1 && !UNFILTERED_SHARED_REGIONS.has(regionKey(segments[1]));
|
|
75
78
|
}
|
|
@@ -83,7 +86,7 @@ function blockSetFor(segments) {
|
|
|
83
86
|
function deniedSegmentFor(path) {
|
|
84
87
|
const segments = path.split("/");
|
|
85
88
|
const blockSet = blockSetFor(segments);
|
|
86
|
-
const scan =
|
|
89
|
+
const scan = isLogicalNameScoped(segments) && segments.length > 3 ? segments.slice(3) : segments;
|
|
87
90
|
for (const segment of scan) {
|
|
88
91
|
if (isDeniedName(blockSet, segment)) return segment;
|
|
89
92
|
}
|
|
@@ -807,6 +810,7 @@ import {
|
|
|
807
810
|
mkdirSync,
|
|
808
811
|
openSync,
|
|
809
812
|
renameSync,
|
|
813
|
+
rmdirSync,
|
|
810
814
|
statSync,
|
|
811
815
|
symlinkSync,
|
|
812
816
|
writeFileSync
|
|
@@ -867,6 +871,12 @@ function freshBackupTs(backupRoot) {
|
|
|
867
871
|
while (existsSync3(join5(backupRoot, `${base}-${n}`))) n++;
|
|
868
872
|
return `${base}-${n}`;
|
|
869
873
|
}
|
|
874
|
+
function discardEmptyBackupDir(backupRoot) {
|
|
875
|
+
try {
|
|
876
|
+
rmdirSync(backupRoot);
|
|
877
|
+
} catch {
|
|
878
|
+
}
|
|
879
|
+
}
|
|
870
880
|
function ensureSymlink(linkPath, target) {
|
|
871
881
|
if (existsSync3(linkPath)) {
|
|
872
882
|
if (lstatSync4(linkPath).isSymbolicLink()) return;
|
|
@@ -1549,7 +1559,7 @@ var init_push_leak_verdict = __esm({
|
|
|
1549
1559
|
|
|
1550
1560
|
// src/commands.adopt.ts
|
|
1551
1561
|
import { existsSync as existsSync5, lstatSync as lstatSync7 } from "node:fs";
|
|
1552
|
-
import { join as
|
|
1562
|
+
import { join as join9 } from "node:path";
|
|
1553
1563
|
|
|
1554
1564
|
// src/commands.adopt.recover.ts
|
|
1555
1565
|
import { lstatSync as lstatSync6, rmSync as rmSync4 } from "node:fs";
|
|
@@ -1557,10 +1567,22 @@ import { dirname as dirname2, join as join7, resolve as resolve2 } from "node:pa
|
|
|
1557
1567
|
|
|
1558
1568
|
// src/commands.adopt.scan.ts
|
|
1559
1569
|
init_config();
|
|
1560
|
-
init_exit_codes();
|
|
1561
|
-
init_utils();
|
|
1562
1570
|
import { lstatSync, readdirSync } from "node:fs";
|
|
1563
1571
|
import { join as join2, relative } from "node:path";
|
|
1572
|
+
|
|
1573
|
+
// src/error-text.ts
|
|
1574
|
+
function errorText(err) {
|
|
1575
|
+
try {
|
|
1576
|
+
const message = err?.message;
|
|
1577
|
+
return typeof message === "string" ? message : String(err);
|
|
1578
|
+
} catch {
|
|
1579
|
+
return "unprintable error value";
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/commands.adopt.scan.ts
|
|
1584
|
+
init_exit_codes();
|
|
1585
|
+
init_utils();
|
|
1564
1586
|
function toForwardSlash(p) {
|
|
1565
1587
|
return p.replaceAll("\\", "/");
|
|
1566
1588
|
}
|
|
@@ -1589,10 +1611,8 @@ function scanOrFatal(name, root, state) {
|
|
|
1589
1611
|
try {
|
|
1590
1612
|
return scanDeniedEntries(root);
|
|
1591
1613
|
} catch (err) {
|
|
1592
|
-
const message = err?.message;
|
|
1593
|
-
const text = typeof message === "string" ? message : String(err);
|
|
1594
1614
|
throw new NomadFatal(
|
|
1595
|
-
`cannot adopt ${name}: could not scan ${root} for never-sync content (${
|
|
1615
|
+
`cannot adopt ${name}: could not scan ${root} for never-sync content (${errorText(err)}). ${state()} Check that it is readable and not being written to, then run \`nomad adopt ${name}\` again.`,
|
|
1596
1616
|
{ code: EXIT.GENERIC_FAILURE }
|
|
1597
1617
|
);
|
|
1598
1618
|
}
|
|
@@ -2332,10 +2352,6 @@ function regenerateSettings(ts, opts = {}) {
|
|
|
2332
2352
|
|
|
2333
2353
|
// src/commands.adopt.recover.ts
|
|
2334
2354
|
init_utils();
|
|
2335
|
-
function errorText(err) {
|
|
2336
|
-
const message = err?.message;
|
|
2337
|
-
return typeof message === "string" ? message : String(err);
|
|
2338
|
-
}
|
|
2339
2355
|
function lexists(p) {
|
|
2340
2356
|
try {
|
|
2341
2357
|
lstatSync6(p);
|
|
@@ -2460,10 +2476,125 @@ init_config_sharedDirs_guard();
|
|
|
2460
2476
|
init_exit_codes();
|
|
2461
2477
|
init_utils();
|
|
2462
2478
|
init_utils_fs();
|
|
2479
|
+
|
|
2480
|
+
// src/utils.lockfile.ts
|
|
2481
|
+
init_config();
|
|
2482
|
+
init_utils();
|
|
2483
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2484
|
+
import { dirname as dirname3, join as join8 } from "node:path";
|
|
2485
|
+
function lockFilePath() {
|
|
2486
|
+
return join8(home(), ".cache", "claude-nomad", "nomad.lock");
|
|
2487
|
+
}
|
|
2488
|
+
function acquireLock(verb) {
|
|
2489
|
+
const lp = lockFilePath();
|
|
2490
|
+
mkdirSync2(dirname3(lp), { recursive: true });
|
|
2491
|
+
try {
|
|
2492
|
+
const fd = openSync2(lp, "wx");
|
|
2493
|
+
try {
|
|
2494
|
+
writeFileSync2(fd, String(process.pid));
|
|
2495
|
+
} catch (writeErr) {
|
|
2496
|
+
try {
|
|
2497
|
+
closeSync2(fd);
|
|
2498
|
+
} catch {
|
|
2499
|
+
}
|
|
2500
|
+
try {
|
|
2501
|
+
unlinkSync(lp);
|
|
2502
|
+
} catch {
|
|
2503
|
+
}
|
|
2504
|
+
throw writeErr;
|
|
2505
|
+
}
|
|
2506
|
+
return { fd, path: lp };
|
|
2507
|
+
} catch (err) {
|
|
2508
|
+
const code = err.code;
|
|
2509
|
+
if (code !== "EEXIST") throw err;
|
|
2510
|
+
return checkStaleAndRetry(verb, lp);
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
function releaseLock(handle) {
|
|
2514
|
+
if (handle === null) return;
|
|
2515
|
+
const lp = handle.path;
|
|
2516
|
+
try {
|
|
2517
|
+
closeSync2(handle.fd);
|
|
2518
|
+
} catch {
|
|
2519
|
+
}
|
|
2520
|
+
try {
|
|
2521
|
+
unlinkSync(lp);
|
|
2522
|
+
} catch (err) {
|
|
2523
|
+
if (err.code !== "ENOENT") throw err;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function unlinkIfSamePid(expectedPidStr, lp) {
|
|
2527
|
+
let current;
|
|
2528
|
+
try {
|
|
2529
|
+
current = readFileSync3(lp, "utf8").trim();
|
|
2530
|
+
} catch {
|
|
2531
|
+
return false;
|
|
2532
|
+
}
|
|
2533
|
+
if (current !== expectedPidStr) return false;
|
|
2534
|
+
try {
|
|
2535
|
+
unlinkSync(lp);
|
|
2536
|
+
return true;
|
|
2537
|
+
} catch {
|
|
2538
|
+
return false;
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
function checkStaleAndRetry(verb, lp) {
|
|
2542
|
+
let pidStr;
|
|
2543
|
+
try {
|
|
2544
|
+
pidStr = readFileSync3(lp, "utf8").trim();
|
|
2545
|
+
} catch {
|
|
2546
|
+
pidStr = "";
|
|
2547
|
+
}
|
|
2548
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
2549
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
2550
|
+
if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
|
|
2551
|
+
warn(`another nomad ${verb} running, skipping`);
|
|
2552
|
+
return null;
|
|
2553
|
+
}
|
|
2554
|
+
try {
|
|
2555
|
+
process.kill(pid, 0);
|
|
2556
|
+
warn(`another nomad ${verb} running, skipping`);
|
|
2557
|
+
return null;
|
|
2558
|
+
} catch (err) {
|
|
2559
|
+
const code = err.code;
|
|
2560
|
+
if (code === "ESRCH") {
|
|
2561
|
+
if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
|
|
2562
|
+
warn(`another nomad ${verb} running, skipping`);
|
|
2563
|
+
return null;
|
|
2564
|
+
}
|
|
2565
|
+
warn(`another nomad ${verb} running, skipping`);
|
|
2566
|
+
return null;
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2569
|
+
function retryOnce(verb, lp) {
|
|
2570
|
+
try {
|
|
2571
|
+
const fd = openSync2(lp, "wx");
|
|
2572
|
+
try {
|
|
2573
|
+
writeFileSync2(fd, String(process.pid));
|
|
2574
|
+
} catch {
|
|
2575
|
+
try {
|
|
2576
|
+
closeSync2(fd);
|
|
2577
|
+
} catch {
|
|
2578
|
+
}
|
|
2579
|
+
try {
|
|
2580
|
+
unlinkSync(lp);
|
|
2581
|
+
} catch {
|
|
2582
|
+
}
|
|
2583
|
+
warn(`another nomad ${verb} running, skipping`);
|
|
2584
|
+
return null;
|
|
2585
|
+
}
|
|
2586
|
+
return { fd, path: lp };
|
|
2587
|
+
} catch {
|
|
2588
|
+
warn(`another nomad ${verb} running, skipping`);
|
|
2589
|
+
return null;
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
// src/commands.adopt.ts
|
|
2463
2594
|
init_utils_json();
|
|
2464
2595
|
var ADOPT_PUSH_HINT = "run `nomad push` to share with other hosts";
|
|
2465
2596
|
function readMapIfPresent(repoHome2) {
|
|
2466
|
-
const mapPath =
|
|
2597
|
+
const mapPath = join9(repoHome2, "path-map.json");
|
|
2467
2598
|
return existsSync5(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
2468
2599
|
}
|
|
2469
2600
|
function isConfiguredTarget(name, map) {
|
|
@@ -2498,7 +2629,7 @@ function adoptStopsEarly(name, linkPath, sharedTarget) {
|
|
|
2498
2629
|
function performAdoptMove(name, linkPath, sharedTarget, repo, backup) {
|
|
2499
2630
|
const ts = freshBackupTs(backup);
|
|
2500
2631
|
const snapshotted = backupBeforeWrite(linkPath, ts);
|
|
2501
|
-
const rel =
|
|
2632
|
+
const rel = join9("shared", name);
|
|
2502
2633
|
const stage = () => gitOrFatal(["add", "--", rel], `git add shared/${name}`, repo);
|
|
2503
2634
|
copyIntoSharedOrFatal(name, linkPath, sharedTarget, repo);
|
|
2504
2635
|
refuseLateDeniedEntries(name, linkPath, sharedTarget, repo, { snapshotted, ts });
|
|
@@ -2538,8 +2669,21 @@ function cmdAdopt(name, opts = {}) {
|
|
|
2538
2669
|
);
|
|
2539
2670
|
process.exit(1);
|
|
2540
2671
|
}
|
|
2541
|
-
const linkPath =
|
|
2542
|
-
const sharedTarget =
|
|
2672
|
+
const linkPath = join9(claude, name);
|
|
2673
|
+
const sharedTarget = join9(repo, "shared", name);
|
|
2674
|
+
if (dryRun) {
|
|
2675
|
+
adoptPreflightAndMove(name, linkPath, sharedTarget, repo, backup, true);
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
const handle = acquireLock("adopt");
|
|
2679
|
+
if (handle === null) process.exit(EXIT.SUCCESS);
|
|
2680
|
+
try {
|
|
2681
|
+
adoptPreflightAndMove(name, linkPath, sharedTarget, repo, backup, false);
|
|
2682
|
+
} finally {
|
|
2683
|
+
releaseLock(handle);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
function adoptPreflightAndMove(name, linkPath, sharedTarget, repo, backup, dryRun) {
|
|
2543
2687
|
if (adoptStopsEarly(name, linkPath, sharedTarget)) return;
|
|
2544
2688
|
refuseDeniedEntries(name, linkPath);
|
|
2545
2689
|
if (dryRun) {
|
|
@@ -2566,8 +2710,8 @@ init_config();
|
|
|
2566
2710
|
import { existsSync as existsSync6 } from "node:fs";
|
|
2567
2711
|
|
|
2568
2712
|
// src/commands.redact.core.ts
|
|
2569
|
-
import { appendFileSync, readFileSync as
|
|
2570
|
-
import { join as
|
|
2713
|
+
import { appendFileSync, readFileSync as readFileSync4 } from "node:fs";
|
|
2714
|
+
import { join as join10 } from "node:path";
|
|
2571
2715
|
function collectMatchIntervals(content, findings) {
|
|
2572
2716
|
const intervals = [];
|
|
2573
2717
|
for (const f of findings) {
|
|
@@ -2625,10 +2769,10 @@ function isAlreadyPresent(line, lines) {
|
|
|
2625
2769
|
function appendGitleaksIgnore(fingerprint, repo) {
|
|
2626
2770
|
const sanitized = fingerprint.replace(/[\r\n]/g, "").trim();
|
|
2627
2771
|
if (sanitized.length === 0) return;
|
|
2628
|
-
const ignPath =
|
|
2772
|
+
const ignPath = join10(repo, ".gitleaksignore");
|
|
2629
2773
|
let raw;
|
|
2630
2774
|
try {
|
|
2631
|
-
raw =
|
|
2775
|
+
raw = readFileSync4(ignPath, "utf8");
|
|
2632
2776
|
} catch {
|
|
2633
2777
|
raw = "";
|
|
2634
2778
|
}
|
|
@@ -2666,121 +2810,6 @@ import { join as join11 } from "node:path";
|
|
|
2666
2810
|
import { createInterface } from "node:readline/promises";
|
|
2667
2811
|
init_utils_fs();
|
|
2668
2812
|
init_utils_json();
|
|
2669
|
-
|
|
2670
|
-
// src/utils.lockfile.ts
|
|
2671
|
-
init_config();
|
|
2672
|
-
init_utils();
|
|
2673
|
-
import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2674
|
-
import { dirname as dirname3, join as join10 } from "node:path";
|
|
2675
|
-
function lockFilePath() {
|
|
2676
|
-
return join10(home(), ".cache", "claude-nomad", "nomad.lock");
|
|
2677
|
-
}
|
|
2678
|
-
function acquireLock(verb) {
|
|
2679
|
-
const lp = lockFilePath();
|
|
2680
|
-
mkdirSync2(dirname3(lp), { recursive: true });
|
|
2681
|
-
try {
|
|
2682
|
-
const fd = openSync2(lp, "wx");
|
|
2683
|
-
try {
|
|
2684
|
-
writeFileSync2(fd, String(process.pid));
|
|
2685
|
-
} catch (writeErr) {
|
|
2686
|
-
try {
|
|
2687
|
-
closeSync2(fd);
|
|
2688
|
-
} catch {
|
|
2689
|
-
}
|
|
2690
|
-
try {
|
|
2691
|
-
unlinkSync(lp);
|
|
2692
|
-
} catch {
|
|
2693
|
-
}
|
|
2694
|
-
throw writeErr;
|
|
2695
|
-
}
|
|
2696
|
-
return { fd, path: lp };
|
|
2697
|
-
} catch (err) {
|
|
2698
|
-
const code = err.code;
|
|
2699
|
-
if (code !== "EEXIST") throw err;
|
|
2700
|
-
return checkStaleAndRetry(verb, lp);
|
|
2701
|
-
}
|
|
2702
|
-
}
|
|
2703
|
-
function releaseLock(handle) {
|
|
2704
|
-
if (handle === null) return;
|
|
2705
|
-
const lp = handle.path;
|
|
2706
|
-
try {
|
|
2707
|
-
closeSync2(handle.fd);
|
|
2708
|
-
} catch {
|
|
2709
|
-
}
|
|
2710
|
-
try {
|
|
2711
|
-
unlinkSync(lp);
|
|
2712
|
-
} catch (err) {
|
|
2713
|
-
if (err.code !== "ENOENT") throw err;
|
|
2714
|
-
}
|
|
2715
|
-
}
|
|
2716
|
-
function unlinkIfSamePid(expectedPidStr, lp) {
|
|
2717
|
-
let current;
|
|
2718
|
-
try {
|
|
2719
|
-
current = readFileSync4(lp, "utf8").trim();
|
|
2720
|
-
} catch {
|
|
2721
|
-
return false;
|
|
2722
|
-
}
|
|
2723
|
-
if (current !== expectedPidStr) return false;
|
|
2724
|
-
try {
|
|
2725
|
-
unlinkSync(lp);
|
|
2726
|
-
return true;
|
|
2727
|
-
} catch {
|
|
2728
|
-
return false;
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
function checkStaleAndRetry(verb, lp) {
|
|
2732
|
-
let pidStr;
|
|
2733
|
-
try {
|
|
2734
|
-
pidStr = readFileSync4(lp, "utf8").trim();
|
|
2735
|
-
} catch {
|
|
2736
|
-
pidStr = "";
|
|
2737
|
-
}
|
|
2738
|
-
const pid = Number.parseInt(pidStr, 10);
|
|
2739
|
-
if (!Number.isFinite(pid) || pid <= 0) {
|
|
2740
|
-
if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
|
|
2741
|
-
warn(`another nomad ${verb} running, skipping`);
|
|
2742
|
-
return null;
|
|
2743
|
-
}
|
|
2744
|
-
try {
|
|
2745
|
-
process.kill(pid, 0);
|
|
2746
|
-
warn(`another nomad ${verb} running, skipping`);
|
|
2747
|
-
return null;
|
|
2748
|
-
} catch (err) {
|
|
2749
|
-
const code = err.code;
|
|
2750
|
-
if (code === "ESRCH") {
|
|
2751
|
-
if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
|
|
2752
|
-
warn(`another nomad ${verb} running, skipping`);
|
|
2753
|
-
return null;
|
|
2754
|
-
}
|
|
2755
|
-
warn(`another nomad ${verb} running, skipping`);
|
|
2756
|
-
return null;
|
|
2757
|
-
}
|
|
2758
|
-
}
|
|
2759
|
-
function retryOnce(verb, lp) {
|
|
2760
|
-
try {
|
|
2761
|
-
const fd = openSync2(lp, "wx");
|
|
2762
|
-
try {
|
|
2763
|
-
writeFileSync2(fd, String(process.pid));
|
|
2764
|
-
} catch {
|
|
2765
|
-
try {
|
|
2766
|
-
closeSync2(fd);
|
|
2767
|
-
} catch {
|
|
2768
|
-
}
|
|
2769
|
-
try {
|
|
2770
|
-
unlinkSync(lp);
|
|
2771
|
-
} catch {
|
|
2772
|
-
}
|
|
2773
|
-
warn(`another nomad ${verb} running, skipping`);
|
|
2774
|
-
return null;
|
|
2775
|
-
}
|
|
2776
|
-
return { fd, path: lp };
|
|
2777
|
-
} catch {
|
|
2778
|
-
warn(`another nomad ${verb} running, skipping`);
|
|
2779
|
-
return null;
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
2782
|
-
|
|
2783
|
-
// src/commands.capture-settings.ts
|
|
2784
2813
|
init_utils();
|
|
2785
2814
|
async function confirmCapture(destLabel, keys) {
|
|
2786
2815
|
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
@@ -2866,6 +2895,9 @@ var TS_SHAPE = /^\d{8}-\d{6}(-\d+)?$/;
|
|
|
2866
2895
|
var DURATION_RE = /^(\d+)([dhm])$/;
|
|
2867
2896
|
var UNIT_MS = { d: 864e5, h: 36e5, m: 6e4 };
|
|
2868
2897
|
var CLEAN_DEFAULT_OLDER_THAN_MS = 14 * 24 * 60 * 60 * 1e3;
|
|
2898
|
+
function nameOf(d) {
|
|
2899
|
+
return d.name;
|
|
2900
|
+
}
|
|
2869
2901
|
function isTsDir(name) {
|
|
2870
2902
|
return TS_SHAPE.test(name);
|
|
2871
2903
|
}
|
|
@@ -2878,6 +2910,19 @@ function listBackupDirs(backupBase2) {
|
|
|
2878
2910
|
if (!existsSync8(backupBase2)) return [];
|
|
2879
2911
|
return readdirSync4(backupBase2).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join12(backupBase2, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
2880
2912
|
}
|
|
2913
|
+
function holdsNoContent(dir) {
|
|
2914
|
+
let entries;
|
|
2915
|
+
try {
|
|
2916
|
+
entries = readdirSync4(dir, { withFileTypes: true });
|
|
2917
|
+
} catch {
|
|
2918
|
+
return false;
|
|
2919
|
+
}
|
|
2920
|
+
for (const entry of entries) {
|
|
2921
|
+
if (!entry.isDirectory()) return false;
|
|
2922
|
+
if (!holdsNoContent(join12(dir, entry.name))) return false;
|
|
2923
|
+
}
|
|
2924
|
+
return true;
|
|
2925
|
+
}
|
|
2881
2926
|
function prunableByAge(dirs, olderThanMs, nowMs) {
|
|
2882
2927
|
return dirs.filter((d) => nowMs - d.mtimeMs > olderThanMs).map((d) => d.name);
|
|
2883
2928
|
}
|
|
@@ -2910,8 +2955,30 @@ function cmdClean(opts, backupBase2 = backupBase()) {
|
|
|
2910
2955
|
}
|
|
2911
2956
|
olderThanMs = parsed;
|
|
2912
2957
|
}
|
|
2958
|
+
if (dryRun) {
|
|
2959
|
+
pruneBackups({ dryRun, olderThanMs, keep }, backupBase2);
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
const handle = acquireLock("clean");
|
|
2963
|
+
if (handle === null) process.exit(EXIT.SUCCESS);
|
|
2964
|
+
try {
|
|
2965
|
+
pruneBackups({ dryRun, olderThanMs, keep }, backupBase2);
|
|
2966
|
+
} finally {
|
|
2967
|
+
releaseLock(handle);
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
function pruneBackups(opts, backupBase2) {
|
|
2971
|
+
const { dryRun, olderThanMs, keep } = opts;
|
|
2913
2972
|
const dirs = listBackupDirs(backupBase2);
|
|
2914
|
-
const
|
|
2973
|
+
const empty = new Set(dirs.filter((d) => holdsNoContent(join12(backupBase2, d.name))).map(nameOf));
|
|
2974
|
+
const aged = new Set(
|
|
2975
|
+
resolveTargets(
|
|
2976
|
+
dirs.filter((d) => !empty.has(d.name)),
|
|
2977
|
+
olderThanMs,
|
|
2978
|
+
keep
|
|
2979
|
+
)
|
|
2980
|
+
);
|
|
2981
|
+
const targets = dirs.filter((d) => empty.has(d.name) || aged.has(d.name)).map(nameOf);
|
|
2915
2982
|
if (dryRun) {
|
|
2916
2983
|
for (const name of targets) item(name);
|
|
2917
2984
|
log(`dry-run: ${targets.length} backup(s) would be removed`);
|
|
@@ -3964,7 +4031,7 @@ function reportBackupsCheck(section2, backupBase2 = backupBase()) {
|
|
|
3964
4031
|
if (count > DOCTOR_BACKUP_COUNT_WARN || sizeMb > DOCTOR_BACKUP_SIZE_WARN_MB) {
|
|
3965
4032
|
addItem(
|
|
3966
4033
|
section2,
|
|
3967
|
-
`${yellow(warnGlyph)} backups: ${count} dirs / ${sizeMb.toFixed(1)} MB (run 'nomad clean --backups --keep <N>'; bare --backups
|
|
4034
|
+
`${yellow(warnGlyph)} backups: ${count} dirs / ${sizeMb.toFixed(1)} MB (run 'nomad clean --backups --keep <N>'; bare --backups prunes empty dirs plus those older than 14d)`
|
|
3968
4035
|
);
|
|
3969
4036
|
}
|
|
3970
4037
|
}
|
|
@@ -8054,8 +8121,8 @@ function emitMirror(onPreview, name, localPath, repoPath) {
|
|
|
8054
8121
|
function emitMirrorWet(onPreview, name, localPath, repoPath) {
|
|
8055
8122
|
if (onPreview) onPreview({ kind: "mirror", name, localPath, repoPath });
|
|
8056
8123
|
}
|
|
8057
|
-
function notShared(
|
|
8058
|
-
return !
|
|
8124
|
+
function notShared(target) {
|
|
8125
|
+
return !existsSync42(target);
|
|
8059
8126
|
}
|
|
8060
8127
|
function mirrorOneSharedName(name, claude, repo, policy, opts) {
|
|
8061
8128
|
const localPath = join56(claude, name);
|
|
@@ -8064,21 +8131,21 @@ function mirrorOneSharedName(name, claude, repo, policy, opts) {
|
|
|
8064
8131
|
try {
|
|
8065
8132
|
stat = lstatSync20(localPath, { throwIfNoEntry: false });
|
|
8066
8133
|
} catch (err) {
|
|
8067
|
-
if (notShared(
|
|
8134
|
+
if (notShared(target)) return;
|
|
8068
8135
|
if (opts.dryRun === true) {
|
|
8069
8136
|
warn(
|
|
8070
|
-
`${name} could not be read (${err
|
|
8137
|
+
`${name} could not be read (${errorText(err)}), so nothing was captured for it and nothing was written. A pull that captures shared edits would skip it too. Check its permissions, or whether another program has it open`
|
|
8071
8138
|
);
|
|
8072
8139
|
return;
|
|
8073
8140
|
}
|
|
8074
8141
|
warn(
|
|
8075
|
-
`${name} could not be read (${err
|
|
8142
|
+
`${name} could not be read (${errorText(err)}), so it was left out of shared/ this run. Check its permissions, or whether another program has it open`
|
|
8076
8143
|
);
|
|
8077
8144
|
return;
|
|
8078
8145
|
}
|
|
8079
8146
|
if (stat === void 0) return;
|
|
8080
8147
|
if (stat.isSymbolicLink()) return;
|
|
8081
|
-
if (notShared(
|
|
8148
|
+
if (notShared(target)) return;
|
|
8082
8149
|
if (opts.dryRun === true) {
|
|
8083
8150
|
emitMirror(opts.onPreview, name, localPath, target);
|
|
8084
8151
|
return;
|
|
@@ -8102,10 +8169,10 @@ function mirrorSharedNames(map, policy, opts = {}) {
|
|
|
8102
8169
|
}
|
|
8103
8170
|
}
|
|
8104
8171
|
function syncSharedLinksPush(map, opts = {}) {
|
|
8105
|
-
mirrorSharedNames(map, {
|
|
8172
|
+
mirrorSharedNames(map, { overlay: false }, opts);
|
|
8106
8173
|
}
|
|
8107
8174
|
function stageLocalSharedEdits(map, ts, opts = {}) {
|
|
8108
|
-
mirrorSharedNames(map, {
|
|
8175
|
+
mirrorSharedNames(map, { overlay: true, backupTs: ts }, opts);
|
|
8109
8176
|
}
|
|
8110
8177
|
function presentAt(abs) {
|
|
8111
8178
|
try {
|
|
@@ -8128,7 +8195,7 @@ function removeUntrackedDenied(repo, path, segment, ts) {
|
|
|
8128
8195
|
backupRepoWrite(abs, ts, repo);
|
|
8129
8196
|
} catch (err) {
|
|
8130
8197
|
warn(
|
|
8131
|
-
`could not snapshot ${abs} before removing it (${err
|
|
8198
|
+
`could not snapshot ${abs} before removing it (${errorText(err)}), so it was left in place: ${denied}, so remove it by hand`
|
|
8132
8199
|
);
|
|
8133
8200
|
return;
|
|
8134
8201
|
}
|
|
@@ -8141,7 +8208,7 @@ function removeUntrackedDenied(repo, path, segment, ts) {
|
|
|
8141
8208
|
const where = snapshotted ? `. A copy was snapshotted under backup/${ts}/repo/ first` : "";
|
|
8142
8209
|
warn(`removed ${path} from the sync repo working tree: ${denied}${where}`);
|
|
8143
8210
|
} catch (err) {
|
|
8144
|
-
warn(`could not remove ${abs}: ${err
|
|
8211
|
+
warn(`could not remove ${abs}: ${errorText(err)}`);
|
|
8145
8212
|
}
|
|
8146
8213
|
}
|
|
8147
8214
|
function reportTrackedDenied(repo, path, segment) {
|
|
@@ -9001,12 +9068,20 @@ function handleWedge(repo, forceRemote) {
|
|
|
9001
9068
|
return die(wedgeMarkerRunbookText(state), { code: EXIT.CONFLICT });
|
|
9002
9069
|
}
|
|
9003
9070
|
function runPullCore(opts = {}) {
|
|
9071
|
+
const backup = backupBase();
|
|
9072
|
+
const ts = freshBackupTs(backup);
|
|
9073
|
+
try {
|
|
9074
|
+
return runPullWithBackupTs(ts, opts);
|
|
9075
|
+
} finally {
|
|
9076
|
+
discardEmptyBackupDir(join61(backup, ts));
|
|
9077
|
+
}
|
|
9078
|
+
}
|
|
9079
|
+
function runPullWithBackupTs(ts, opts) {
|
|
9004
9080
|
const dryRun = opts.dryRun === true;
|
|
9005
9081
|
const forceRemote = opts.forceRemote === true;
|
|
9006
9082
|
const compose = opts.compose === true;
|
|
9007
9083
|
const repo = repoHome();
|
|
9008
9084
|
const backup = backupBase();
|
|
9009
|
-
const ts = freshBackupTs(backup);
|
|
9010
9085
|
const recovered = handleWedge(repo, forceRemote);
|
|
9011
9086
|
if (!dryRun) {
|
|
9012
9087
|
const backupRoot = join61(backup, ts);
|
|
@@ -10638,7 +10713,7 @@ function parseSyncArgs(argv) {
|
|
|
10638
10713
|
// package.json
|
|
10639
10714
|
var package_default = {
|
|
10640
10715
|
name: "claude-nomad",
|
|
10641
|
-
version: "0.
|
|
10716
|
+
version: "0.67.1",
|
|
10642
10717
|
type: "module",
|
|
10643
10718
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
10644
10719
|
keywords: [
|
|
@@ -10867,7 +10942,9 @@ var DEFAULT_HELP = [
|
|
|
10867
10942
|
row(" --backups", "Required: confirm backup pruning is the intended target."),
|
|
10868
10943
|
row(" --dry-run", "List the snapshots that would be removed without deleting."),
|
|
10869
10944
|
row(" --older-than <dur>", "Delete snapshots older than a duration (e.g. 14d, 24h, 30m)."),
|
|
10870
|
-
cont("Default when no retention flag is given: 14d."),
|
|
10945
|
+
cont("Default when no retention flag is given: 14d. A snapshot holding"),
|
|
10946
|
+
cont("nothing is removed in every mode, whatever its age, and never"),
|
|
10947
|
+
cont("uses up one of the --keep slots."),
|
|
10871
10948
|
row(" --keep <N>", "Keep the N most-recent snapshots, delete the rest. Mutually"),
|
|
10872
10949
|
cont("exclusive with --older-than."),
|
|
10873
10950
|
"",
|
package/package.json
CHANGED