claude-nomad 0.64.1 → 0.65.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 +71 -0
- package/README.md +34 -77
- package/dist/nomad.mjs +453 -217
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.65.0](https://github.com/funkadelic/claude-nomad/compare/v0.64.1...v0.65.0) (2026-08-13)
|
|
4
|
+
|
|
5
|
+
### What's new
|
|
6
|
+
|
|
7
|
+
**On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and none of these apply to
|
|
8
|
+
it.
|
|
9
|
+
|
|
10
|
+
- **`nomad pull --force-remote` no longer discards edits you have not published.** The flag exists
|
|
11
|
+
to rescue a sync repo that got stuck part-way through an update. On native Windows it also skipped
|
|
12
|
+
the step that saves your local shared-config edits into the repo, every time it ran, even on a
|
|
13
|
+
healthy machine with nothing to rescue. That was the flag's only visible effect on a healthy repo,
|
|
14
|
+
and nothing told you it had happened. It now rescues a stuck repo and nothing else, and when a
|
|
15
|
+
rescue genuinely does replace your copies, the pull says how many and where your previous ones
|
|
16
|
+
were saved.
|
|
17
|
+
- **A pull now says what it did to your shared config.** On Windows your shared config is kept as
|
|
18
|
+
real file copies rather than symlinks, so every pull starts by copying your local edits into the
|
|
19
|
+
sync repo. That step used to be silent. It now prints a `Symlinks` section naming each file it
|
|
20
|
+
picked up, and each file it removed from the repo because you deleted it here. If a file cannot be
|
|
21
|
+
read at all, because another program is holding it open or its permissions changed, the pull names
|
|
22
|
+
it and says why rather than leaving a silent gap. `nomad diff` and `nomad pull --dry-run` read
|
|
23
|
+
from the same place, so a preview now matches what a real pull does.
|
|
24
|
+
- **Windows keeps the same things out of your sync repo that your other machines do.** The Windows
|
|
25
|
+
copy step used to hold back only files whose names looked like passwords or keys. It now skips
|
|
26
|
+
everything on the never-sync list, so the folders Claude Code uses for its own state, named things
|
|
27
|
+
like `sessions`, `tasks`, `plans`, and `cache`, stay on the machine even when they sit inside a
|
|
28
|
+
folder you share. If one of them is already sitting in your sync repo, the pull tells you, and
|
|
29
|
+
takes a backup copy before removing it.
|
|
30
|
+
|
|
31
|
+
**On all supported platforms, i.e. \*nix, Mac, WSL2, and native Windows.**
|
|
32
|
+
|
|
33
|
+
- **`nomad doctor` names the files that have drifted.** When it reports that your skills or your
|
|
34
|
+
shared config no longer match the repo, it lists them. Before, you got a count and had to run it
|
|
35
|
+
again with `--verbose` to find out which files it meant.
|
|
36
|
+
- **`nomad pull --force-remote` tells you when there was nothing to rescue.** On a repo that is not
|
|
37
|
+
stuck it now says so and pulls normally, instead of appearing to do nothing. If it cannot tell
|
|
38
|
+
whether the repo is stuck, it says that too rather than reporting a clean repo. The help text,
|
|
39
|
+
README, and docs site now agree on what the flag does.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Added
|
|
43
|
+
|
|
44
|
+
* **doctor:** open with the nomad wordmark and name the diverging files ([#508](https://github.com/funkadelic/claude-nomad/issues/508)) ([1fea0c7](https://github.com/funkadelic/claude-nomad/commit/1fea0c7913b38f934d529d888a0aef96dc1dfc08))
|
|
45
|
+
* make the win32 pre-pull mirror visible and gate what it writes ([#503](https://github.com/funkadelic/claude-nomad/issues/503)) ([8d3a76d](https://github.com/funkadelic/claude-nomad/commit/8d3a76dd42c89efd38b2a76b2684b8b4014878ac))
|
|
46
|
+
* **pull:** report the shared files a pull removes, not just the ones it captures ([#510](https://github.com/funkadelic/claude-nomad/issues/510)) ([9ab6ef5](https://github.com/funkadelic/claude-nomad/commit/9ab6ef5b9961a8542eb25a650ada27bd3bdf9de6))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
* **mirror:** report what the never-sync cleanup actually removed ([#509](https://github.com/funkadelic/claude-nomad/issues/509)) ([6596ed3](https://github.com/funkadelic/claude-nomad/commit/6596ed341d4e764b29a56bb73f900aa6dd48fcc9))
|
|
52
|
+
* **mirror:** warn when the win32 mirror skips a name it cannot stat ([#505](https://github.com/funkadelic/claude-nomad/issues/505)) ([5806db6](https://github.com/funkadelic/claude-nomad/commit/5806db6e2d2861747738d498d0255fe0483a6650))
|
|
53
|
+
* **pull:** make --force-remote mean wedge recovery again ([#506](https://github.com/funkadelic/claude-nomad/issues/506)) ([2bf8d5d](https://github.com/funkadelic/claude-nomad/commit/2bf8d5d3b19e8c022ff1137be96b0f009e5503df))
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
### Changed
|
|
57
|
+
|
|
58
|
+
* fail the gitleaks install on a download error, not a checksum mismatch ([#504](https://github.com/funkadelic/claude-nomad/issues/504)) ([724dd3c](https://github.com/funkadelic/claude-nomad/commit/724dd3c276bdea113f9719e5b62ee0741a3c3885))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
### Documentation
|
|
62
|
+
|
|
63
|
+
* describe what the force-remote recovery warning actually counts ([#507](https://github.com/funkadelic/claude-nomad/issues/507)) ([6235858](https://github.com/funkadelic/claude-nomad/commit/6235858e40459e136deff4b081187c89b9e7b670))
|
|
64
|
+
* keep the Windows explainer in one place ([#511](https://github.com/funkadelic/claude-nomad/issues/511)) ([9a9287f](https://github.com/funkadelic/claude-nomad/commit/9a9287f87d86e2353ca4a0d936346482e37a1041))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
### Dependencies
|
|
68
|
+
|
|
69
|
+
* bump @astrojs/starlight from 0.41.6 to 0.41.7 in /docs-site in the prod-dependencies group ([#499](https://github.com/funkadelic/claude-nomad/issues/499)) ([61ecb3f](https://github.com/funkadelic/claude-nomad/commit/61ecb3fef0213c6bc0f2a36713da0e166e356238))
|
|
70
|
+
* bump astro from 7.1.6 to 7.2.0 in /docs-site ([#500](https://github.com/funkadelic/claude-nomad/issues/500)) ([4909e02](https://github.com/funkadelic/claude-nomad/commit/4909e02e5754081432e567cae2344989ab8b1309))
|
|
71
|
+
* bump the codeql-action group across 1 directory with 2 updates ([#498](https://github.com/funkadelic/claude-nomad/issues/498)) ([ad3310d](https://github.com/funkadelic/claude-nomad/commit/ad3310d33e9662ae407d304ef949efd1bebf9c3e))
|
|
72
|
+
* bump the dev-dependencies group across 1 directory with 4 updates ([#501](https://github.com/funkadelic/claude-nomad/issues/501)) ([446c6da](https://github.com/funkadelic/claude-nomad/commit/446c6dae3905609bef1d3ec4366d58e50651c44a))
|
|
73
|
+
|
|
3
74
|
## [0.64.1](https://github.com/funkadelic/claude-nomad/compare/v0.64.0...v0.64.1) (2026-08-07)
|
|
4
75
|
|
|
5
76
|
### What's new
|
package/README.md
CHANGED
|
@@ -71,7 +71,8 @@ survives different file paths and your secrets never ride along.
|
|
|
71
71
|
parks stranded work on a branch, refuses if shared config is at risk), and a repo where the rebase
|
|
72
72
|
was interrupted but the git index was left with unmerged entries and no active operation (clears
|
|
73
73
|
the index via `git reset --mixed HEAD`, surfaces any orphaned stash entry left by the interrupted
|
|
74
|
-
autostash, never discards working-tree edits).
|
|
74
|
+
autostash, never discards working-tree edits). On a repo that is not stuck, the flag reports there
|
|
75
|
+
is nothing to recover and pulls normally instead of doing nothing silently.
|
|
75
76
|
- **Easy off.** `nomad eject` replaces every managed `~/.claude/` symlink with a real copy in one
|
|
76
77
|
step, so your setup keeps working after you delete the sync checkout and uninstall the CLI.
|
|
77
78
|
|
|
@@ -118,27 +119,40 @@ export NOMAD_HOST=<your-host-label> # add to ~/.zshrc or ~/.bashrc
|
|
|
118
119
|
$ nomad pull
|
|
119
120
|
```
|
|
120
121
|
|
|
122
|
+
Everyday loop on any host:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
$ nomad doctor # confirm setup
|
|
126
|
+
$ nomad sync # pull config, then publish local changes, in one step
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`nomad sync` is the command to reach for day to day: it always pulls first (so changes from your
|
|
130
|
+
other machines land before anything is pushed, and work that exists only on this machine is kept,
|
|
131
|
+
not deleted) and then pushes, under one lock, so there is no ordering to remember. Output is compact
|
|
132
|
+
by default: a run prints only a short Sync summary, not the full status tree; pass
|
|
133
|
+
`nomad sync --verbose` (or `--all` / `-v`) to see the full tree. `nomad pull` and `nomad push` are
|
|
134
|
+
still available as lower-level commands for cases `sync` does not cover: recovering a wedged repo
|
|
135
|
+
with `nomad pull --force-remote`, or resolving a detected secret without the interactive menu via
|
|
136
|
+
`nomad push --redact-all` / `--allow` / `--allow-all` (see [Changing settings](#changing-settings)
|
|
137
|
+
and [Recovery flows](https://funkadelic.github.io/claude-nomad/recovery/)). The
|
|
138
|
+
[FAQ](https://funkadelic.github.io/claude-nomad/faq/) covers what `sync` does under the hood and the
|
|
139
|
+
push/pull order it enforces.
|
|
140
|
+
|
|
121
141
|
### Windows
|
|
122
142
|
|
|
123
143
|
claude-nomad runs natively on Windows (PowerShell or cmd), and WSL2 works too. The everyday loop is
|
|
124
144
|
the same either way; the difference is under the hood.
|
|
125
145
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
overwritten. Enabling Developer Mode does not change this; copies are used on every native Windows
|
|
132
|
-
host either way.
|
|
133
|
-
|
|
134
|
-
Two things come from native Windows specifically, both covered in the bullets below: a
|
|
135
|
-
`.gitleaksignore` allow entry may not travel to a macOS, Linux, or WSL2 host, and deep session paths
|
|
136
|
-
can hit the native Windows 260-character path limit.
|
|
137
|
-
|
|
138
|
-
The native Windows steps are the same as above with a couple of PowerShell-specific swaps:
|
|
146
|
+
Native Windows cannot use symlinks, so claude-nomad keeps a real copy of your shared config in
|
|
147
|
+
`~/.claude/` instead of a symlink. `nomad pull` and `nomad sync` both mirror your local copies into
|
|
148
|
+
the repo before they fetch, so an edit you have not published yet is captured rather than
|
|
149
|
+
overwritten. If a file cannot be read, the pull leaves it out and warns naming the file: fix that
|
|
150
|
+
and run again before you rely on the result.
|
|
139
151
|
|
|
140
152
|
```powershell
|
|
141
|
-
# 1. Install the CLI.
|
|
153
|
+
# 1. Install prerequisites and the CLI. (Using Scoop instead? scoop install gh gitleaks.)
|
|
154
|
+
> winget install GitHub.cli
|
|
155
|
+
> winget install gitleaks.gitleaks
|
|
142
156
|
> npm i -g claude-nomad
|
|
143
157
|
|
|
144
158
|
# 2. Create your private sync repo and scaffold it.
|
|
@@ -155,66 +169,9 @@ The native Windows steps are the same as above with a couple of PowerShell-speci
|
|
|
155
169
|
> nomad push
|
|
156
170
|
```
|
|
157
171
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
- **Installing gh:** `winget install GitHub.cli` (or `scoop install gh`), then `gh auth login`.
|
|
163
|
-
Needed before `nomad init` on the first host; later hosts only clone with it.
|
|
164
|
-
- **Installing gitleaks:** `winget install gitleaks.gitleaks` (or `scoop install gitleaks` if you
|
|
165
|
-
use Scoop). `nomad doctor` prints the same hint whenever gitleaks is missing from PATH.
|
|
166
|
-
- **Shared config is copied, not symlinked.** On macOS and Linux, files like `CLAUDE.md` and your
|
|
167
|
-
skills live in the sync repo and are symlinked into `~/.claude/`, so there is one source of truth
|
|
168
|
-
on disk. Creating a symlink on native Windows needs Developer Mode or admin rights, so there these
|
|
169
|
-
are real copies instead, whether or not you have Developer Mode enabled. WSL2 is unaffected and
|
|
170
|
-
behaves like Linux. What this means for you: nothing extra. On native Windows both `nomad pull`
|
|
171
|
-
and `nomad sync` mirror your local copies into the repo before they fetch, so an unpublished edit
|
|
172
|
-
is captured rather than reverted. A file you delete from a shared directory is handled the same
|
|
173
|
-
way: it is removed from the sync repo by the next pull, exactly as deleting inside a symlinked
|
|
174
|
-
directory already removes it on macOS or Linux. The removal is left uncommitted, so it publishes
|
|
175
|
-
on your next push and passes the same secret scan as everything else, and the file is snapshotted
|
|
176
|
-
to the backup dir first. The safety rule behind this: nomad only removes a file it has a record of
|
|
177
|
-
having given this machine, so a repo file this machine has never synced is never touched. That
|
|
178
|
-
record is also why the first pull after you upgrade to this version is an exception: there is
|
|
179
|
-
nothing to compare against yet, so a deletion made before that pull comes back once, and deleting
|
|
180
|
-
it again sticks. If a file you created has the same name as one another machine just created, the
|
|
181
|
-
pull stops before it changes anything and tells you which file under `~/.claude/` to move or
|
|
182
|
-
rename, with the two ways to finish. Nothing is lost either way: your file stays exactly as you
|
|
183
|
-
left it and the update simply waits for the next pull. The one command that deliberately takes the
|
|
184
|
-
repo's version is `nomad pull --force-remote`, which is what that flag is for; the copy it
|
|
185
|
-
replaces is snapshotted to the backup dir first. This is the same behavior claude-nomad's
|
|
186
|
-
`skills/` sync already has on every platform.
|
|
187
|
-
- **A `.gitleaksignore` allow entry may not travel across hosts.** gitleaks fingerprints each
|
|
188
|
-
finding using the file path exactly as it saw it: backslashes on native Windows, forward slashes
|
|
189
|
-
on macOS/Linux/WSL2. If you allow a finding with `nomad push --allow` (or `nomad allow`) on native
|
|
190
|
-
Windows, the identical finding can reappear as "new" the first time it is scanned from a macOS,
|
|
191
|
-
Linux, or WSL2 host, and the same happens in reverse. This is a known gitleaks limitation, not a
|
|
192
|
-
claude-nomad bug; just allow it again from the other host.
|
|
193
|
-
- **Line endings stay put.** A fresh `nomad init` writes a `.gitattributes` with `* -text`, so Git
|
|
194
|
-
never converts line endings between hosts. If you are joining a sync repo created before this file
|
|
195
|
-
existed, add that one line from any host (or watch for the `nomad doctor` warning that nudges
|
|
196
|
-
you), otherwise a native Windows checkout with the common `core.autocrlf=true` Git default would
|
|
197
|
-
rewrite every text file's line endings, and every host would then see the whole tree as
|
|
198
|
-
permanently changed.
|
|
199
|
-
|
|
200
|
-
Everyday loop on any host:
|
|
201
|
-
|
|
202
|
-
```bash
|
|
203
|
-
$ nomad doctor # confirm setup
|
|
204
|
-
$ nomad sync # pull config, then publish local changes, in one step
|
|
205
|
-
```
|
|
206
|
-
|
|
207
|
-
`nomad sync` is the command to reach for day to day: it always pulls first (so changes from your
|
|
208
|
-
other machines land before anything is pushed, and work that exists only on this machine is kept,
|
|
209
|
-
not deleted) and then pushes, under one lock, so there is no ordering to remember. Output is compact
|
|
210
|
-
by default: a run prints only a short Sync summary, not the full status tree; pass
|
|
211
|
-
`nomad sync --verbose` (or `--all` / `-v`) to see the full tree. `nomad pull` and `nomad push` are
|
|
212
|
-
still available as lower-level commands for cases `sync` does not cover: recovering a wedged repo
|
|
213
|
-
with `nomad pull --force-remote`, or resolving a detected secret without the interactive menu via
|
|
214
|
-
`nomad push --redact-all` / `--allow` / `--allow-all` (see [Changing settings](#changing-settings)
|
|
215
|
-
and [Recovery flows](https://funkadelic.github.io/claude-nomad/recovery/)). The
|
|
216
|
-
[FAQ](https://funkadelic.github.io/claude-nomad/faq/) covers what `sync` does under the hood and the
|
|
217
|
-
push/pull order it enforces.
|
|
172
|
+
See the full explainer at <https://funkadelic.github.io/claude-nomad/quickstart/#windows> for how
|
|
173
|
+
the copy-sync behaves on pull and push, why a `.gitleaksignore` allow entry may not travel to a
|
|
174
|
+
macOS, Linux, or WSL2 host, the 260-character path limit, and the line-endings setting.
|
|
218
175
|
|
|
219
176
|
### Make your sessions follow you
|
|
220
177
|
|
|
@@ -321,8 +278,8 @@ independently from the CLI, but requires nomad `>= 0.35.0` because it calls rece
|
|
|
321
278
|
- `gh` ([GitHub CLI](https://cli.github.com/)), required by `nomad init`
|
|
322
279
|
|
|
323
280
|
Works on macOS, Linux (including WSL2), and native Windows (PowerShell or cmd). See
|
|
324
|
-
[Windows](#windows) above for the native Windows equivalents of the install and host-label steps
|
|
325
|
-
the copy-sync trade-off
|
|
281
|
+
[Windows](#windows) above for the native Windows equivalents of the install and host-label steps and
|
|
282
|
+
the copy-sync trade-off.
|
|
326
283
|
|
|
327
284
|
**Optional:** [curl](https://curl.se/) or [wget](https://www.gnu.org/software/wget/) for the
|
|
328
285
|
version-staleness check and `nomad doctor --check-schema`. The CLI works without them. The opt-in
|
package/dist/nomad.mjs
CHANGED
|
@@ -58,7 +58,26 @@ function isDeniedName(blockSet, name) {
|
|
|
58
58
|
function isClaudeExtraName(name) {
|
|
59
59
|
return stripTrailingDotsAndWhitespace(name).toLowerCase() === ".claude";
|
|
60
60
|
}
|
|
61
|
-
|
|
61
|
+
function isExtrasScoped(segments) {
|
|
62
|
+
return segments[0] === "shared" && segments[1] === "extras";
|
|
63
|
+
}
|
|
64
|
+
function blockSetFor(segments) {
|
|
65
|
+
if (!isExtrasScoped(segments)) return NEVER_SYNC;
|
|
66
|
+
return isClaudeExtraName(segments[3] ?? "") ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
|
|
67
|
+
}
|
|
68
|
+
function deniedSegmentFor(path) {
|
|
69
|
+
const segments = path.split("/");
|
|
70
|
+
const blockSet = blockSetFor(segments);
|
|
71
|
+
const scan = isExtrasScoped(segments) ? segments.slice(4) : segments;
|
|
72
|
+
for (const segment of scan) {
|
|
73
|
+
if (isDeniedName(blockSet, segment)) return segment;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function isNeverSync(path) {
|
|
78
|
+
return deniedSegmentFor(path) !== null;
|
|
79
|
+
}
|
|
80
|
+
var NEVER_SYNC, CLAUDE_EXTRA_NEVER_SYNC, ALWAYS_NEVER_SYNC, SECRET_FILE_PATTERNS;
|
|
62
81
|
var init_config_never_sync = __esm({
|
|
63
82
|
"src/config.never-sync.ts"() {
|
|
64
83
|
"use strict";
|
|
@@ -88,6 +107,13 @@ var init_config_never_sync = __esm({
|
|
|
88
107
|
"sessions"
|
|
89
108
|
]);
|
|
90
109
|
CLAUDE_EXTRA_NEVER_SYNC = /* @__PURE__ */ new Set([...NEVER_SYNC, "projects"]);
|
|
110
|
+
ALWAYS_NEVER_SYNC = /* @__PURE__ */ new Set([
|
|
111
|
+
".claude.json",
|
|
112
|
+
".credentials.json",
|
|
113
|
+
"settings.local.json",
|
|
114
|
+
"history.jsonl",
|
|
115
|
+
"stats-cache.json"
|
|
116
|
+
]);
|
|
91
117
|
SECRET_FILE_PATTERNS = [
|
|
92
118
|
/^\.env(\..+)?$/i,
|
|
93
119
|
// .env, .env.local, .env.production
|
|
@@ -639,7 +665,7 @@ function sharedDirEntries(map) {
|
|
|
639
665
|
const raw = map.sharedDirs;
|
|
640
666
|
return Array.isArray(raw) ? raw : [];
|
|
641
667
|
}
|
|
642
|
-
var SETTINGS_SCHEMA_URL, NPM_REGISTRY_LATEST_URL, GITLEAKS_PINNED_VERSION, GITLEAKS_SCAN_TIMEOUT_MS, HOST, SHARED_LINKS, GSD_PREFIX, GSD_DROPPED_NAMES, SUPPORTED_EXTRAS,
|
|
668
|
+
var SETTINGS_SCHEMA_URL, NPM_REGISTRY_LATEST_URL, GITLEAKS_PINNED_VERSION, GITLEAKS_SCAN_TIMEOUT_MS, HOST, SHARED_LINKS, GSD_PREFIX, GSD_DROPPED_NAMES, SUPPORTED_EXTRAS, PUSH_ALLOWED_STATIC;
|
|
643
669
|
var init_config = __esm({
|
|
644
670
|
"src/config.ts"() {
|
|
645
671
|
"use strict";
|
|
@@ -656,13 +682,6 @@ var init_config = __esm({
|
|
|
656
682
|
GSD_PREFIX = "gsd-";
|
|
657
683
|
GSD_DROPPED_NAMES = ["hooks", "agents"];
|
|
658
684
|
SUPPORTED_EXTRAS = [".planning", "CLAUDE.md", ".claude"];
|
|
659
|
-
ALWAYS_NEVER_SYNC = /* @__PURE__ */ new Set([
|
|
660
|
-
".claude.json",
|
|
661
|
-
".credentials.json",
|
|
662
|
-
"settings.local.json",
|
|
663
|
-
"history.jsonl",
|
|
664
|
-
"stats-cache.json"
|
|
665
|
-
]);
|
|
666
685
|
PUSH_ALLOWED_STATIC = [
|
|
667
686
|
"shared/CLAUDE.md",
|
|
668
687
|
"shared/my-statusline.cjs",
|
|
@@ -894,13 +913,20 @@ function probeUnmergedIndex(repo) {
|
|
|
894
913
|
}
|
|
895
914
|
return raw.split("\0").some(Boolean) ? "unmerged" : "clean";
|
|
896
915
|
}
|
|
897
|
-
function
|
|
898
|
-
|
|
916
|
+
function classifyWedgeWithProbe(repo) {
|
|
917
|
+
const mode = detectWedge(repo);
|
|
918
|
+
if (mode !== null) return { state: mode, probe: "clean" };
|
|
919
|
+
const probe = probeUnmergedIndex(repo);
|
|
920
|
+
return { state: probe === "unmerged" ? "unmerged-index" : null, probe };
|
|
899
921
|
}
|
|
900
922
|
function classifyWedge(repo) {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
923
|
+
return classifyWedgeWithProbe(repo).state;
|
|
924
|
+
}
|
|
925
|
+
function cleanRepoForceRemoteMessage(probe) {
|
|
926
|
+
if (probe === "error") {
|
|
927
|
+
return "could not determine whether the repo is wedged; continuing with a normal pull";
|
|
928
|
+
}
|
|
929
|
+
return "repo is clean, nothing to recover; continuing with a normal pull";
|
|
904
930
|
}
|
|
905
931
|
function unmergedIndexRunbookText(resumeCmd2) {
|
|
906
932
|
return `repo has an unmerged index with no active rebase or merge in progress (torn-down rebase or merge left stage-2/3 entries behind).
|
|
@@ -2090,7 +2116,7 @@ function applySharedLinks(ts, map, opts = {}) {
|
|
|
2090
2116
|
const dryRun = opts.dryRun === true;
|
|
2091
2117
|
const claude = claudeHome();
|
|
2092
2118
|
const repo = repoHome();
|
|
2093
|
-
const linkNames = allSharedLinks(map);
|
|
2119
|
+
const linkNames = allSharedLinks(map, { quiet: opts.quietNames === true });
|
|
2094
2120
|
if (process.platform === "win32") {
|
|
2095
2121
|
applySharedLinksWin32(linkNames, claude, repo, ts, dryRun, opts.onPreview);
|
|
2096
2122
|
return;
|
|
@@ -2108,35 +2134,6 @@ function applySharedLinks(ts, map, opts = {}) {
|
|
|
2108
2134
|
ensureSymlink(linkPath, target);
|
|
2109
2135
|
}
|
|
2110
2136
|
}
|
|
2111
|
-
function syncSharedLinksPush(map) {
|
|
2112
|
-
mirrorSharedNames(map, { adoptNew: true, overlay: false });
|
|
2113
|
-
}
|
|
2114
|
-
function stageLocalSharedEdits(map, ts) {
|
|
2115
|
-
mirrorSharedNames(map, { adoptNew: false, overlay: true, backupTs: ts });
|
|
2116
|
-
}
|
|
2117
|
-
function mirrorOneSharedName(name, claude, repo, policy) {
|
|
2118
|
-
const localPath = join5(claude, name);
|
|
2119
|
-
const stat = lstatSync4(localPath, { throwIfNoEntry: false });
|
|
2120
|
-
if (stat === void 0) return;
|
|
2121
|
-
if (stat.isSymbolicLink()) return;
|
|
2122
|
-
const target = join5(repo, "shared", name);
|
|
2123
|
-
if (!policy.adoptNew && !existsSync4(target)) return;
|
|
2124
|
-
if (policy.backupTs !== void 0) backupRepoWrite(target, policy.backupTs, repo);
|
|
2125
|
-
if (policy.overlay && stat.isDirectory()) {
|
|
2126
|
-
copyExtrasOverlayFiltered(localPath, target, ALWAYS_NEVER_SYNC);
|
|
2127
|
-
} else {
|
|
2128
|
-
copyExtrasFiltered(localPath, target, ALWAYS_NEVER_SYNC);
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
function mirrorSharedNames(map, policy) {
|
|
2132
|
-
if (process.platform !== "win32") return;
|
|
2133
|
-
if (map === null) return;
|
|
2134
|
-
const claude = claudeHome();
|
|
2135
|
-
const repo = repoHome();
|
|
2136
|
-
for (const name of allSharedLinks(map)) {
|
|
2137
|
-
mirrorOneSharedName(name, claude, repo, policy);
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
2137
|
function readExistingSettings(settingsPath) {
|
|
2141
2138
|
if (!existsSync4(settingsPath)) return { existing: {}, present: false, malformed: false };
|
|
2142
2139
|
try {
|
|
@@ -2867,7 +2864,7 @@ import { join as join45 } from "node:path";
|
|
|
2867
2864
|
init_color();
|
|
2868
2865
|
init_config();
|
|
2869
2866
|
import { existsSync as existsSync11, lstatSync as lstatSync8, readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
|
|
2870
|
-
import {
|
|
2867
|
+
import { join as join13, win32 as win32Path } from "node:path";
|
|
2871
2868
|
|
|
2872
2869
|
// src/commands.doctor.format.ts
|
|
2873
2870
|
init_color();
|
|
@@ -3121,19 +3118,30 @@ function reportRepoState(section2) {
|
|
|
3121
3118
|
function repoHasSharedSource(name) {
|
|
3122
3119
|
return existsSync11(join13(repoHome(), "shared", name));
|
|
3123
3120
|
}
|
|
3124
|
-
function win32CopyOkRow(name) {
|
|
3125
|
-
|
|
3121
|
+
function win32CopyOkRow(name, exempt = 0) {
|
|
3122
|
+
const note = exempt > 0 ? dim(` (${exempt} never-synced path(s) not compared)`) : "";
|
|
3123
|
+
return { line: `${green(okGlyph)} ${name}: real copy (win32 copy-sync)${note}`, fail: false };
|
|
3124
|
+
}
|
|
3125
|
+
function relativeUnder(root, path) {
|
|
3126
|
+
const rel = win32Path.relative(root, path);
|
|
3127
|
+
const segments = rel.split(win32Path.sep);
|
|
3128
|
+
if (rel === "" || segments[0] === ".." || win32Path.isAbsolute(rel)) return null;
|
|
3129
|
+
return segments.join("/");
|
|
3126
3130
|
}
|
|
3127
|
-
function
|
|
3128
|
-
|
|
3131
|
+
function divergingRepoPath(line, name, local, shared) {
|
|
3132
|
+
const path = line.replace(/ \((?:local|repo) only\)$/, "");
|
|
3133
|
+
const rel = relativeUnder(local, path) ?? relativeUnder(shared, path);
|
|
3134
|
+
return rel === null ? `shared/${name}` : `shared/${name}/${rel}`;
|
|
3129
3135
|
}
|
|
3130
3136
|
function classifyWin32Copy(name, p) {
|
|
3131
3137
|
const sharedPath = join13(repoHome(), "shared", name);
|
|
3132
3138
|
if (!existsSync11(sharedPath)) return win32CopyOkRow(name);
|
|
3133
|
-
const
|
|
3134
|
-
|
|
3139
|
+
const compared = listDivergingFiles(p, sharedPath);
|
|
3140
|
+
const diverging = compared.filter(
|
|
3141
|
+
(line) => deniedSegmentFor(divergingRepoPath(line, name, p, sharedPath)) === null
|
|
3135
3142
|
);
|
|
3136
|
-
|
|
3143
|
+
const exempt = compared.length - diverging.length;
|
|
3144
|
+
if (diverging.length === 0) return win32CopyOkRow(name, exempt);
|
|
3137
3145
|
return {
|
|
3138
3146
|
line: `${yellow(warnGlyph)} ${name}: ${diverging.length} file(s) diverge from shared/${name}`,
|
|
3139
3147
|
fail: false,
|
|
@@ -5389,12 +5397,12 @@ import { join as join37 } from "node:path";
|
|
|
5389
5397
|
|
|
5390
5398
|
// src/skills-sync.tracked.ts
|
|
5391
5399
|
init_utils();
|
|
5392
|
-
import { basename as
|
|
5400
|
+
import { basename as basename3 } from "node:path";
|
|
5393
5401
|
function trackedRootSkillsAt(ref, repo) {
|
|
5394
5402
|
try {
|
|
5395
5403
|
const raw = gitCaptureRaw(["ls-tree", "--name-only", "-z", ref, "--", "shared/skills/"], repo);
|
|
5396
5404
|
const entries = raw.split("\0").filter((entry) => entry !== "");
|
|
5397
|
-
return new Set(entries.map((entry) =>
|
|
5405
|
+
return new Set(entries.map((entry) => basename3(entry)));
|
|
5398
5406
|
} catch {
|
|
5399
5407
|
return /* @__PURE__ */ new Set();
|
|
5400
5408
|
}
|
|
@@ -6110,8 +6118,8 @@ function sessionSuffix(finding) {
|
|
|
6110
6118
|
const sid = sessionIdFromFinding(finding);
|
|
6111
6119
|
if (sid === null) return "";
|
|
6112
6120
|
const rawBasename = finding.File.replace(/^.*\//, "");
|
|
6113
|
-
const
|
|
6114
|
-
return
|
|
6121
|
+
const basename4 = rawBasename.endsWith(".jsonl") ? rawBasename.slice(0, -".jsonl".length) : rawBasename;
|
|
6122
|
+
return basename4 === sid ? "" : ` (session: ${sid})`;
|
|
6115
6123
|
}
|
|
6116
6124
|
function renderFindingBlock(finding, readLine, siblings = []) {
|
|
6117
6125
|
const lines = [` file: ${finding.File}:${finding.StartLine}${sessionSuffix(finding)}`];
|
|
@@ -6780,6 +6788,20 @@ function buildVerdictSection(sections) {
|
|
|
6780
6788
|
return summary;
|
|
6781
6789
|
}
|
|
6782
6790
|
|
|
6791
|
+
// src/commands.doctor.banner.ts
|
|
6792
|
+
var NOMAD_BANNER = [
|
|
6793
|
+
" __",
|
|
6794
|
+
" ____ ____ ____ ___ ____ _ ____/ /",
|
|
6795
|
+
" / __ \\ / __ \\ / __ `__ \\ / __ `// __ /",
|
|
6796
|
+
" / / / // /_/ // / / / / // /_/ // /_/ /",
|
|
6797
|
+
String.raw`/_/ /_/ \____//_/ /_/ /_/ \__,_/ \__,_/`
|
|
6798
|
+
];
|
|
6799
|
+
function printBanner(log2, stdout) {
|
|
6800
|
+
if (stdout.isTTY !== true) return;
|
|
6801
|
+
for (const line of NOMAD_BANNER) log2(line);
|
|
6802
|
+
log2("");
|
|
6803
|
+
}
|
|
6804
|
+
|
|
6783
6805
|
// src/commands.doctor.compact.ts
|
|
6784
6806
|
init_color();
|
|
6785
6807
|
var ALWAYS_FULL = /* @__PURE__ */ new Set([
|
|
@@ -6798,18 +6820,27 @@ function isRepoStateLine(item2) {
|
|
|
6798
6820
|
function isCopySyncModalityLine(item2) {
|
|
6799
6821
|
return item2.includes("sync modality: copy-sync");
|
|
6800
6822
|
}
|
|
6823
|
+
function isKeptEnvironmentRow(item2) {
|
|
6824
|
+
return isRepoStateLine(item2) || isCopySyncModalityLine(item2) || isProblem(item2);
|
|
6825
|
+
}
|
|
6826
|
+
function keepWithChildren(items, keep) {
|
|
6827
|
+
const out = [];
|
|
6828
|
+
let parentKept = false;
|
|
6829
|
+
for (const item2 of items) {
|
|
6830
|
+
if (isChild(item2)) {
|
|
6831
|
+
if (parentKept) out.push(item2);
|
|
6832
|
+
continue;
|
|
6833
|
+
}
|
|
6834
|
+
parentKept = keep(item2);
|
|
6835
|
+
if (parentKept) out.push(item2);
|
|
6836
|
+
}
|
|
6837
|
+
return out;
|
|
6838
|
+
}
|
|
6801
6839
|
function compactSections(sections) {
|
|
6802
6840
|
return sections.map((s) => {
|
|
6803
6841
|
if (ALWAYS_FULL.has(s.header)) return s;
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
...s,
|
|
6807
|
-
items: s.items.filter(
|
|
6808
|
-
(it) => isRepoStateLine(it) || isCopySyncModalityLine(it) || isProblem(it)
|
|
6809
|
-
)
|
|
6810
|
-
};
|
|
6811
|
-
}
|
|
6812
|
-
return { ...s, items: s.items.filter(isProblem) };
|
|
6842
|
+
const keep = s.header === "Environment" ? isKeptEnvironmentRow : isProblem;
|
|
6843
|
+
return { ...s, items: keepWithChildren(s.items, keep) };
|
|
6813
6844
|
});
|
|
6814
6845
|
}
|
|
6815
6846
|
|
|
@@ -6886,6 +6917,7 @@ function gatherDoctorSections(opts) {
|
|
|
6886
6917
|
return [...body, buildVerdictSection(body)];
|
|
6887
6918
|
}
|
|
6888
6919
|
function cmdDoctor(opts = {}) {
|
|
6920
|
+
printBanner(console.log, process.stdout);
|
|
6889
6921
|
const makeSpinner = opts.startSpinner ?? startSpinner;
|
|
6890
6922
|
const sp = makeSpinner("Running checks");
|
|
6891
6923
|
let report;
|
|
@@ -7415,7 +7447,8 @@ function remapExtrasPush(ts, opts = {}) {
|
|
|
7415
7447
|
// The filter prevents ALWAYS_NEVER_SYNC files from landing in the repo
|
|
7416
7448
|
// working tree before the allow-list gate fires, eliminating the
|
|
7417
7449
|
// "residue wedges repeat push" regression. The allow-list gate
|
|
7418
|
-
// (enforceAllowList
|
|
7450
|
+
// (enforceAllowList in commands.push.allowlist.ts, blockSetFor in
|
|
7451
|
+
// config.never-sync.ts)
|
|
7419
7452
|
// remains the hard security boundary.
|
|
7420
7453
|
// All others: copyExtrasFiltered with per-extra denylist.
|
|
7421
7454
|
(src, dst, dirname14) => dirname14 === ".planning" ? copyExtrasOverlayFiltered(src, dst, extrasDenySet(dirname14)) : copyExtrasFiltered(src, dst, extrasDenySet(dirname14))
|
|
@@ -7566,10 +7599,10 @@ function collectSharedFiles(dir, claude, scan) {
|
|
|
7566
7599
|
addLocalPath(abs, claude, scan);
|
|
7567
7600
|
}
|
|
7568
7601
|
}
|
|
7569
|
-
function enumerateLocalSharedScan(map) {
|
|
7602
|
+
function enumerateLocalSharedScan(map, opts = {}) {
|
|
7570
7603
|
const claude = claudeHome();
|
|
7571
7604
|
const scan = { files: {}, declined: [] };
|
|
7572
|
-
for (const name of allSharedLinks(map)) {
|
|
7605
|
+
for (const name of opts.linkNames ?? allSharedLinks(map, { quiet: opts.quiet === true })) {
|
|
7573
7606
|
if (isDeniedName(ALWAYS_NEVER_SYNC, name)) {
|
|
7574
7607
|
scan.declined.push(name);
|
|
7575
7608
|
continue;
|
|
@@ -7578,13 +7611,13 @@ function enumerateLocalSharedScan(map) {
|
|
|
7578
7611
|
}
|
|
7579
7612
|
return scan;
|
|
7580
7613
|
}
|
|
7581
|
-
function enumerateLocalSharedFiles(map) {
|
|
7582
|
-
return enumerateLocalSharedScan(map).files;
|
|
7614
|
+
function enumerateLocalSharedFiles(map, opts = {}) {
|
|
7615
|
+
return enumerateLocalSharedScan(map, opts).files;
|
|
7583
7616
|
}
|
|
7584
|
-
function buildSharedBaseline(map) {
|
|
7617
|
+
function buildSharedBaseline(map, opts = {}) {
|
|
7585
7618
|
const claude = claudeHome();
|
|
7586
7619
|
const files = {};
|
|
7587
|
-
for (const [key, st] of Object.entries(enumerateLocalSharedFiles(map))) {
|
|
7620
|
+
for (const [key, st] of Object.entries(enumerateLocalSharedFiles(map, opts))) {
|
|
7588
7621
|
try {
|
|
7589
7622
|
files[key] = { size: st.size, mtime: st.mtime, hash: hashFile(join51(claude, key)) };
|
|
7590
7623
|
} catch {
|
|
@@ -7592,27 +7625,69 @@ function buildSharedBaseline(map) {
|
|
|
7592
7625
|
}
|
|
7593
7626
|
return buildManifest(files, SHARED_BASELINE_KIND, SHARED_BASELINE_CONFIG_HASH);
|
|
7594
7627
|
}
|
|
7595
|
-
function writeSharedBaseline(map) {
|
|
7628
|
+
function writeSharedBaseline(map, opts = {}) {
|
|
7596
7629
|
if (process.platform !== "win32") return;
|
|
7597
7630
|
try {
|
|
7598
|
-
writeManifest(sharedBaselinePath(), buildSharedBaseline(map));
|
|
7631
|
+
writeManifest(sharedBaselinePath(), buildSharedBaseline(map, opts));
|
|
7599
7632
|
} catch (err) {
|
|
7600
7633
|
warn(`could not record the shared-config baseline: ${err.message}`);
|
|
7601
7634
|
}
|
|
7602
7635
|
}
|
|
7603
7636
|
|
|
7604
7637
|
// src/commands.pull.win32.ts
|
|
7605
|
-
|
|
7638
|
+
init_color();
|
|
7606
7639
|
import { existsSync as existsSync44 } from "node:fs";
|
|
7607
7640
|
import { join as join56 } from "node:path";
|
|
7608
7641
|
|
|
7609
|
-
// src/git
|
|
7642
|
+
// src/commands.pull.recovery.git.ts
|
|
7643
|
+
init_utils();
|
|
7610
7644
|
import { execFileSync as execFileSync21 } from "node:child_process";
|
|
7645
|
+
function gitCapture(args, cwd) {
|
|
7646
|
+
return execFileSync21("git", args, {
|
|
7647
|
+
cwd,
|
|
7648
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
7649
|
+
maxBuffer: 64 * 1024 * 1024
|
|
7650
|
+
}).toString().trim();
|
|
7651
|
+
}
|
|
7652
|
+
function parsePorcelainZ(raw) {
|
|
7653
|
+
const tracked = [];
|
|
7654
|
+
const untracked = [];
|
|
7655
|
+
if (!raw) return { tracked, untracked };
|
|
7656
|
+
const records = raw.split("\0");
|
|
7657
|
+
for (let i = 0; i < records.length; i++) {
|
|
7658
|
+
const record = records[i];
|
|
7659
|
+
if (record.length < 3) continue;
|
|
7660
|
+
const xy = record.slice(0, 2);
|
|
7661
|
+
const filePath = record.slice(3);
|
|
7662
|
+
if (xy === "??") {
|
|
7663
|
+
untracked.push(filePath);
|
|
7664
|
+
continue;
|
|
7665
|
+
}
|
|
7666
|
+
tracked.push(filePath);
|
|
7667
|
+
if (xy.startsWith("R") || xy.startsWith("C")) {
|
|
7668
|
+
const src = records[i + 1];
|
|
7669
|
+
if (src) {
|
|
7670
|
+
tracked.push(src);
|
|
7671
|
+
i++;
|
|
7672
|
+
}
|
|
7673
|
+
}
|
|
7674
|
+
}
|
|
7675
|
+
return { tracked, untracked };
|
|
7676
|
+
}
|
|
7677
|
+
function parseDirtyPaths(repo, opts = {}) {
|
|
7678
|
+
return parsePorcelainZ(gitStatusPorcelainZ(repo, opts));
|
|
7679
|
+
}
|
|
7680
|
+
|
|
7681
|
+
// src/commands.pull.win32.ts
|
|
7682
|
+
init_config();
|
|
7683
|
+
|
|
7684
|
+
// src/git-probe.ts
|
|
7685
|
+
import { execFileSync as execFileSync22 } from "node:child_process";
|
|
7611
7686
|
var PROBE_TIMEOUT_MS4 = 1e4;
|
|
7612
7687
|
var PROBE_MAX_BUFFER = 64 * 1024 * 1024;
|
|
7613
7688
|
function gitProbe(args, repo) {
|
|
7614
7689
|
try {
|
|
7615
|
-
return
|
|
7690
|
+
return execFileSync22("git", args, {
|
|
7616
7691
|
cwd: repo,
|
|
7617
7692
|
stdio: ["ignore", "pipe", "pipe"],
|
|
7618
7693
|
timeout: PROBE_TIMEOUT_MS4,
|
|
@@ -7623,37 +7698,10 @@ function gitProbe(args, repo) {
|
|
|
7623
7698
|
}
|
|
7624
7699
|
}
|
|
7625
7700
|
|
|
7626
|
-
// src/links.captures.ts
|
|
7627
|
-
init_config();
|
|
7628
|
-
import { existsSync as existsSync40, lstatSync as lstatSync17 } from "node:fs";
|
|
7629
|
-
import { join as join52 } from "node:path";
|
|
7630
|
-
function planSharedLinkCaptures(map) {
|
|
7631
|
-
if (process.platform !== "win32") return [];
|
|
7632
|
-
if (map === null) return [];
|
|
7633
|
-
const claude = claudeHome();
|
|
7634
|
-
const repo = repoHome();
|
|
7635
|
-
const plan = [];
|
|
7636
|
-
for (const name of allSharedLinks(map)) {
|
|
7637
|
-
const localPath = join52(claude, name);
|
|
7638
|
-
let stat;
|
|
7639
|
-
try {
|
|
7640
|
-
stat = lstatSync17(localPath, { throwIfNoEntry: false });
|
|
7641
|
-
} catch {
|
|
7642
|
-
continue;
|
|
7643
|
-
}
|
|
7644
|
-
if (stat === void 0) continue;
|
|
7645
|
-
if (stat.isSymbolicLink()) continue;
|
|
7646
|
-
const repoPath = join52(repo, "shared", name);
|
|
7647
|
-
if (!existsSync40(repoPath)) continue;
|
|
7648
|
-
plan.push({ name, localPath, repoPath });
|
|
7649
|
-
}
|
|
7650
|
-
return plan;
|
|
7651
|
-
}
|
|
7652
|
-
|
|
7653
7701
|
// src/links.deletions.ts
|
|
7654
7702
|
init_config();
|
|
7655
|
-
import { existsSync as
|
|
7656
|
-
import { join as
|
|
7703
|
+
import { existsSync as existsSync40, lstatSync as lstatSync17, rmSync as rmSync17 } from "node:fs";
|
|
7704
|
+
import { join as join52, relative as relative8, resolve as resolve4, sep as sep13 } from "node:path";
|
|
7657
7705
|
init_utils();
|
|
7658
7706
|
init_utils_fs();
|
|
7659
7707
|
function isUnknown(declined, key) {
|
|
@@ -7663,27 +7711,28 @@ function deletionFor(key, names, claude, sharedRoot) {
|
|
|
7663
7711
|
const segments = key.split("/");
|
|
7664
7712
|
const name = segments[0];
|
|
7665
7713
|
if (!names.has(name)) return null;
|
|
7666
|
-
if (!
|
|
7714
|
+
if (!existsSync40(join52(claude, name))) return null;
|
|
7667
7715
|
if (segments.some((segment) => isDeniedName(ALWAYS_NEVER_SYNC, segment))) return null;
|
|
7668
7716
|
const repoPath = resolve4(sharedRoot, key);
|
|
7669
|
-
const rel = relative8(
|
|
7717
|
+
const rel = relative8(join52(sharedRoot, name), repoPath);
|
|
7670
7718
|
if (rel === "" || rel === ".." || rel.startsWith(`..${sep13}`)) return null;
|
|
7671
7719
|
if (segments.some((segment) => segment === "" || segment === "." || segment === ".."))
|
|
7672
7720
|
return null;
|
|
7673
|
-
if (!
|
|
7674
|
-
return { name, localPath:
|
|
7721
|
+
if (!existsSync40(repoPath)) return null;
|
|
7722
|
+
return { kind: "deletion", name, localPath: join52(claude, key), repoPath };
|
|
7675
7723
|
}
|
|
7676
|
-
function planSharedLinkDeletions(map) {
|
|
7724
|
+
function planSharedLinkDeletions(map, opts = {}) {
|
|
7677
7725
|
if (process.platform !== "win32") return [];
|
|
7678
7726
|
if (map === null) return [];
|
|
7679
7727
|
const baseline = readSharedBaseline();
|
|
7680
7728
|
if (baseline === null) return [];
|
|
7681
7729
|
const claude = claudeHome();
|
|
7682
|
-
const sharedRoot =
|
|
7683
|
-
const
|
|
7730
|
+
const sharedRoot = join52(repoHome(), "shared");
|
|
7731
|
+
const linkNames = opts.linkNames ?? allSharedLinks(map);
|
|
7732
|
+
const scan = enumerateLocalSharedScan(map, { linkNames });
|
|
7684
7733
|
const present = new Set(Object.keys(scan.files).map((key) => key.toLowerCase()));
|
|
7685
7734
|
const declined = scan.declined.map((path) => path.toLowerCase());
|
|
7686
|
-
const names = new Set(
|
|
7735
|
+
const names = new Set(linkNames);
|
|
7687
7736
|
const plan = [];
|
|
7688
7737
|
for (const key of Object.keys(baseline.files)) {
|
|
7689
7738
|
const folded = key.toLowerCase();
|
|
@@ -7694,17 +7743,151 @@ function planSharedLinkDeletions(map) {
|
|
|
7694
7743
|
}
|
|
7695
7744
|
return plan;
|
|
7696
7745
|
}
|
|
7697
|
-
function applySharedLinkDeletions(map, ts) {
|
|
7746
|
+
function applySharedLinkDeletions(map, ts, opts = {}) {
|
|
7698
7747
|
const repo = repoHome();
|
|
7699
|
-
|
|
7748
|
+
const removed = [];
|
|
7749
|
+
for (const entry of planSharedLinkDeletions(map, opts)) {
|
|
7700
7750
|
try {
|
|
7701
|
-
if (!
|
|
7751
|
+
if (!lstatSync17(entry.repoPath).isFile()) continue;
|
|
7702
7752
|
backupRepoWrite(entry.repoPath, ts, repo);
|
|
7703
7753
|
rmSync17(entry.repoPath, { force: true });
|
|
7754
|
+
removed.push(entry);
|
|
7704
7755
|
} catch (err) {
|
|
7705
7756
|
warn(`could not remove ${entry.repoPath}: ${err.message}`);
|
|
7706
7757
|
}
|
|
7707
7758
|
}
|
|
7759
|
+
return removed;
|
|
7760
|
+
}
|
|
7761
|
+
|
|
7762
|
+
// src/links.mirror.ts
|
|
7763
|
+
init_config();
|
|
7764
|
+
import { existsSync as existsSync41, lstatSync as lstatSync18, rmSync as rmSync18 } from "node:fs";
|
|
7765
|
+
import { join as join53 } from "node:path";
|
|
7766
|
+
init_utils();
|
|
7767
|
+
init_utils_fs();
|
|
7768
|
+
function emitMirror(onPreview, name, localPath, repoPath) {
|
|
7769
|
+
if (onPreview) {
|
|
7770
|
+
onPreview({ kind: "mirror", name, localPath, repoPath });
|
|
7771
|
+
} else {
|
|
7772
|
+
log(`would capture: ${localPath} -> ${repoPath}`);
|
|
7773
|
+
}
|
|
7774
|
+
}
|
|
7775
|
+
function emitMirrorWet(onPreview, name, localPath, repoPath) {
|
|
7776
|
+
if (onPreview) onPreview({ kind: "mirror", name, localPath, repoPath });
|
|
7777
|
+
}
|
|
7778
|
+
function notShared(policy, target) {
|
|
7779
|
+
return !policy.adoptNew && !existsSync41(target);
|
|
7780
|
+
}
|
|
7781
|
+
function mirrorOneSharedName(name, claude, repo, policy, opts) {
|
|
7782
|
+
const localPath = join53(claude, name);
|
|
7783
|
+
const target = join53(repo, "shared", name);
|
|
7784
|
+
let stat;
|
|
7785
|
+
try {
|
|
7786
|
+
stat = lstatSync18(localPath, { throwIfNoEntry: false });
|
|
7787
|
+
} catch (err) {
|
|
7788
|
+
if (notShared(policy, target)) return;
|
|
7789
|
+
warn(
|
|
7790
|
+
`${name} could not be read (${err.message}), so it was left out of shared/ this run. Check its permissions, or whether another program has it open`
|
|
7791
|
+
);
|
|
7792
|
+
return;
|
|
7793
|
+
}
|
|
7794
|
+
if (stat === void 0) return;
|
|
7795
|
+
if (stat.isSymbolicLink()) return;
|
|
7796
|
+
if (notShared(policy, target)) return;
|
|
7797
|
+
if (opts.dryRun === true) {
|
|
7798
|
+
emitMirror(opts.onPreview, name, localPath, target);
|
|
7799
|
+
return;
|
|
7800
|
+
}
|
|
7801
|
+
if (policy.backupTs !== void 0) backupRepoWrite(target, policy.backupTs, repo);
|
|
7802
|
+
if (policy.overlay && stat.isDirectory()) {
|
|
7803
|
+
copyExtrasOverlayFiltered(localPath, target, NEVER_SYNC);
|
|
7804
|
+
} else {
|
|
7805
|
+
copyExtrasFiltered(localPath, target, NEVER_SYNC);
|
|
7806
|
+
}
|
|
7807
|
+
emitMirrorWet(opts.onPreview, name, localPath, target);
|
|
7808
|
+
}
|
|
7809
|
+
function mirrorSharedNames(map, policy, opts = {}) {
|
|
7810
|
+
if (process.platform !== "win32") return;
|
|
7811
|
+
if (map === null) return;
|
|
7812
|
+
const claude = claudeHome();
|
|
7813
|
+
const repo = repoHome();
|
|
7814
|
+
const linkNames = opts.linkNames ?? allSharedLinks(map);
|
|
7815
|
+
for (const name of linkNames) {
|
|
7816
|
+
mirrorOneSharedName(name, claude, repo, policy, opts);
|
|
7817
|
+
}
|
|
7818
|
+
}
|
|
7819
|
+
function syncSharedLinksPush(map, opts = {}) {
|
|
7820
|
+
mirrorSharedNames(map, { adoptNew: true, overlay: false }, opts);
|
|
7821
|
+
}
|
|
7822
|
+
function stageLocalSharedEdits(map, ts, opts = {}) {
|
|
7823
|
+
mirrorSharedNames(map, { adoptNew: false, overlay: true, backupTs: ts }, opts);
|
|
7824
|
+
}
|
|
7825
|
+
function presentAt(abs) {
|
|
7826
|
+
try {
|
|
7827
|
+
return lstatSync18(abs, { throwIfNoEntry: false }) !== void 0;
|
|
7828
|
+
} catch {
|
|
7829
|
+
return true;
|
|
7830
|
+
}
|
|
7831
|
+
}
|
|
7832
|
+
function removeUntrackedDenied(repo, path, segment, ts) {
|
|
7833
|
+
const abs = join53(repo, path);
|
|
7834
|
+
const denied = `the path segment "${segment}" is on the never-sync list`;
|
|
7835
|
+
if (!presentAt(abs)) {
|
|
7836
|
+
warn(
|
|
7837
|
+
`nothing was removed for ${path}: ${denied}, but nothing is at that path now. Either it went away after git listed it, in which case there is nothing left to do, or its name did not survive the decode of git's output and the real file is still in the sync repo under a name nomad cannot address. Look for it in ${repo} with "git status --untracked-files=all -- shared/"`
|
|
7838
|
+
);
|
|
7839
|
+
return;
|
|
7840
|
+
}
|
|
7841
|
+
const snapshotted = existsSync41(abs);
|
|
7842
|
+
try {
|
|
7843
|
+
backupRepoWrite(abs, ts, repo);
|
|
7844
|
+
} catch (err) {
|
|
7845
|
+
warn(
|
|
7846
|
+
`could not snapshot ${abs} before removing it (${err.message}), so it was left in place: ${denied}, so remove it by hand`
|
|
7847
|
+
);
|
|
7848
|
+
return;
|
|
7849
|
+
}
|
|
7850
|
+
try {
|
|
7851
|
+
rmSync18(abs, { recursive: true, force: true });
|
|
7852
|
+
if (presentAt(abs)) {
|
|
7853
|
+
warn(`could not remove ${abs}: ${denied}, so remove it by hand`);
|
|
7854
|
+
return;
|
|
7855
|
+
}
|
|
7856
|
+
const where = snapshotted ? `. A copy was snapshotted under backup/${ts}/repo/ first` : "";
|
|
7857
|
+
warn(`removed ${path} from the sync repo working tree: ${denied}${where}`);
|
|
7858
|
+
} catch (err) {
|
|
7859
|
+
warn(`could not remove ${abs}: ${err.message}`);
|
|
7860
|
+
}
|
|
7861
|
+
}
|
|
7862
|
+
function reportTrackedDenied(repo, path, segment) {
|
|
7863
|
+
const inHead = gitProbe(["ls-tree", "--name-only", "HEAD", "--", path], repo);
|
|
7864
|
+
const denied = `the path segment "${segment}" is on the never-sync list`;
|
|
7865
|
+
if (inHead === null) {
|
|
7866
|
+
warn(
|
|
7867
|
+
`could not check ${path} against HEAD: ${denied}. Nothing was changed. Inspect it with "git status -- ${path}" and take it out of shared/ before committing`
|
|
7868
|
+
);
|
|
7869
|
+
return;
|
|
7870
|
+
}
|
|
7871
|
+
if (inHead.trim() === "") {
|
|
7872
|
+
warn(
|
|
7873
|
+
`${path} is staged and has no committed version: ${denied}. Nothing was changed. Run "git rm --cached -- ${path}" to take it out of the index; that leaves the file on disk but makes it untracked, which the next nomad pull removes from the sync repo working tree (snapshotting it into the backup cache first), so move it outside shared/ instead if you want to keep it. If it is the destination half of a staged rename, "git diff --cached --name-status" names the source, whose content IS committed, so restore that half with "git checkout HEAD -- <source>" rather than leaving its deletion staged`
|
|
7874
|
+
);
|
|
7875
|
+
return;
|
|
7876
|
+
}
|
|
7877
|
+
if (!existsSync41(join53(repo, path))) return;
|
|
7878
|
+
warn(
|
|
7879
|
+
`${path} is tracked and has changes against HEAD: ${denied}. Nothing was changed. Run "git checkout HEAD -- ${path}" to put the committed content back, or move the file outside shared/ if you want to keep it. Neither of those takes the committed copy out of the repo: "git rm -- ${path}" and a commit does that going forward, and if it holds a real secret, rotate it and rewrite history, because nomad only changes your local worktree and index and cannot scrub what a previous push already sent to the remote`
|
|
7880
|
+
);
|
|
7881
|
+
}
|
|
7882
|
+
function revertDeniedMirrorPaths(repo, status, ts) {
|
|
7883
|
+
for (const path of new Set(status.untracked)) {
|
|
7884
|
+
const segment = deniedSegmentFor(path);
|
|
7885
|
+
if (segment !== null) removeUntrackedDenied(repo, path, segment, ts);
|
|
7886
|
+
}
|
|
7887
|
+
for (const path of new Set(status.tracked)) {
|
|
7888
|
+
const segment = deniedSegmentFor(path);
|
|
7889
|
+
if (segment !== null) reportTrackedDenied(repo, path, segment);
|
|
7890
|
+
}
|
|
7708
7891
|
}
|
|
7709
7892
|
|
|
7710
7893
|
// src/preview.ts
|
|
@@ -8051,8 +8234,8 @@ function formatLinkRow(e) {
|
|
|
8051
8234
|
if (e.kind === "copy") return `would copy ${e.from} -> ${e.to}`;
|
|
8052
8235
|
return `${e.kind} ${e.from} -> ${e.to}`;
|
|
8053
8236
|
}
|
|
8054
|
-
function
|
|
8055
|
-
return `would capture ${
|
|
8237
|
+
function formatMirrorRow(e) {
|
|
8238
|
+
return `would capture ${e.localPath} -> ${e.repoPath}`;
|
|
8056
8239
|
}
|
|
8057
8240
|
function formatDeletionRow(d) {
|
|
8058
8241
|
return `would remove ${d.repoPath} (gone from ${d.localPath})`;
|
|
@@ -8060,6 +8243,23 @@ function formatDeletionRow(d) {
|
|
|
8060
8243
|
function formatSessionRow(e) {
|
|
8061
8244
|
return e.kind === "overwrite" ? `overwrite ${e.dst} (from ${e.src})` : e.text;
|
|
8062
8245
|
}
|
|
8246
|
+
function appendMirrorPlanRows(links, ts, map, plans) {
|
|
8247
|
+
if (plans) {
|
|
8248
|
+
for (const capture of plans.captures) addItem(links, formatMirrorRow(capture));
|
|
8249
|
+
for (const deletion of plans.deletions) addItem(links, formatDeletionRow(deletion));
|
|
8250
|
+
return plans.namesDerived;
|
|
8251
|
+
}
|
|
8252
|
+
const linkNames = allSharedLinks(map);
|
|
8253
|
+
stageLocalSharedEdits(map, ts, {
|
|
8254
|
+
dryRun: true,
|
|
8255
|
+
onPreview: (e) => addItem(links, formatMirrorRow(e)),
|
|
8256
|
+
linkNames
|
|
8257
|
+
});
|
|
8258
|
+
for (const deletion of planSharedLinkDeletions(map, { linkNames })) {
|
|
8259
|
+
addItem(links, formatDeletionRow(deletion));
|
|
8260
|
+
}
|
|
8261
|
+
return true;
|
|
8262
|
+
}
|
|
8063
8263
|
function buildSettingsSectionForPreview(result) {
|
|
8064
8264
|
const s = section("settings.json", true);
|
|
8065
8265
|
if (result.diff !== "") {
|
|
@@ -8078,18 +8278,10 @@ function computePreview(ts, map, verb = "pull", plans) {
|
|
|
8078
8278
|
console.log(`would pull on host=${HOST} (preview; nothing applied)`);
|
|
8079
8279
|
console.log("");
|
|
8080
8280
|
const links = section("Symlinks");
|
|
8081
|
-
const
|
|
8082
|
-
captures: planSharedLinkCaptures(map),
|
|
8083
|
-
deletions: planSharedLinkDeletions(map)
|
|
8084
|
-
};
|
|
8085
|
-
for (const capture of captures) {
|
|
8086
|
-
addItem(links, formatCaptureRow(capture));
|
|
8087
|
-
}
|
|
8088
|
-
for (const deletion of deletions) {
|
|
8089
|
-
addItem(links, formatDeletionRow(deletion));
|
|
8090
|
-
}
|
|
8281
|
+
const namesDerived = appendMirrorPlanRows(links, ts, map, plans);
|
|
8091
8282
|
applySharedLinks(ts, map, {
|
|
8092
8283
|
dryRun: true,
|
|
8284
|
+
quietNames: namesDerived,
|
|
8093
8285
|
onPreview: (e) => addItem(links, formatLinkRow(e))
|
|
8094
8286
|
});
|
|
8095
8287
|
const settingsResult = previewSettings(
|
|
@@ -8139,6 +8331,19 @@ function readMapForMirror(mapPath) {
|
|
|
8139
8331
|
return null;
|
|
8140
8332
|
}
|
|
8141
8333
|
}
|
|
8334
|
+
function sharedDirsUnchanged(before, after) {
|
|
8335
|
+
return JSON.stringify(before) === JSON.stringify(after);
|
|
8336
|
+
}
|
|
8337
|
+
function namesAlreadyReported(namesDerived, derivedSharedDirs, map) {
|
|
8338
|
+
return namesDerived && sharedDirsUnchanged(derivedSharedDirs, map.sharedDirs);
|
|
8339
|
+
}
|
|
8340
|
+
function plansAgainst(plans, map) {
|
|
8341
|
+
if (plans === void 0) return void 0;
|
|
8342
|
+
return {
|
|
8343
|
+
...plans,
|
|
8344
|
+
namesDerived: namesAlreadyReported(plans.namesDerived, plans.derivedSharedDirs, map)
|
|
8345
|
+
};
|
|
8346
|
+
}
|
|
8142
8347
|
function untrackedUnderShared(repo) {
|
|
8143
8348
|
const out = gitProbe(["ls-files", "--others", "--exclude-standard", "-z", "--", "shared/"], repo);
|
|
8144
8349
|
if (out === null) return null;
|
|
@@ -8148,27 +8353,94 @@ function newlyUntracked(before, after) {
|
|
|
8148
8353
|
if (before === null || after === null) return [];
|
|
8149
8354
|
return [...after].filter((p) => !before.has(p));
|
|
8150
8355
|
}
|
|
8356
|
+
function revertDeniedUnderShared(repo, ts) {
|
|
8357
|
+
const out = gitProbe(
|
|
8358
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", "shared/"],
|
|
8359
|
+
repo
|
|
8360
|
+
);
|
|
8361
|
+
if (out === null) return;
|
|
8362
|
+
revertDeniedMirrorPaths(repo, parsePorcelainZ(out), ts);
|
|
8363
|
+
}
|
|
8151
8364
|
function reconcileSharedLinksBeforePull(repo, ts) {
|
|
8152
|
-
if (process.platform !== "win32")
|
|
8365
|
+
if (process.platform !== "win32") {
|
|
8366
|
+
return { mirrored: [], events: [], namesDerived: false, derivedSharedDirs: void 0 };
|
|
8367
|
+
}
|
|
8153
8368
|
const before = untrackedUnderShared(repo);
|
|
8369
|
+
const events = [];
|
|
8370
|
+
let linkNames = [];
|
|
8371
|
+
let namesDerived = false;
|
|
8372
|
+
let derivedSharedDirs;
|
|
8154
8373
|
try {
|
|
8155
8374
|
const map = readMapForMirror(join56(repo, "path-map.json"));
|
|
8156
|
-
|
|
8157
|
-
|
|
8375
|
+
if (map !== null) {
|
|
8376
|
+
linkNames = allSharedLinks(map);
|
|
8377
|
+
namesDerived = true;
|
|
8378
|
+
derivedSharedDirs = map.sharedDirs;
|
|
8379
|
+
}
|
|
8380
|
+
stageLocalSharedEdits(map, ts, { onPreview: (e) => events.push(e), linkNames });
|
|
8381
|
+
events.push(...applySharedLinkDeletions(map, ts, { linkNames }));
|
|
8158
8382
|
} catch (err) {
|
|
8159
8383
|
warn(`could not reconcile local shared edits before the pull: ${err.message}`);
|
|
8160
8384
|
}
|
|
8161
|
-
|
|
8385
|
+
revertDeniedUnderShared(repo, ts);
|
|
8386
|
+
return {
|
|
8387
|
+
mirrored: newlyUntracked(before, untrackedUnderShared(repo)),
|
|
8388
|
+
events,
|
|
8389
|
+
namesDerived,
|
|
8390
|
+
derivedSharedDirs
|
|
8391
|
+
};
|
|
8392
|
+
}
|
|
8393
|
+
function buildMirrorSection(events, discard) {
|
|
8394
|
+
const s = section("Symlinks");
|
|
8395
|
+
for (const e of events) {
|
|
8396
|
+
addItem(
|
|
8397
|
+
s,
|
|
8398
|
+
e.kind === "deletion" ? `removed ${e.repoPath} (gone from ${e.localPath})` : `captured ${e.localPath} -> ${e.repoPath}`
|
|
8399
|
+
);
|
|
8400
|
+
}
|
|
8401
|
+
if (discard) {
|
|
8402
|
+
const names = discard.count === 1 ? "name was" : "names were";
|
|
8403
|
+
addItem(
|
|
8404
|
+
s,
|
|
8405
|
+
`${yellow(warnGlyph)} recovered from a wedged repo, ${discard.count} shared ${names} restored from the repo copy, previous host copies backed up to ${discard.backupPath}`
|
|
8406
|
+
);
|
|
8407
|
+
}
|
|
8408
|
+
return s;
|
|
8162
8409
|
}
|
|
8163
|
-
function
|
|
8410
|
+
function describeSkippedMirrorDiscard(repo, ts) {
|
|
8411
|
+
if (process.platform !== "win32") return null;
|
|
8412
|
+
try {
|
|
8413
|
+
const map = readMapForMirror(join56(repo, "path-map.json"));
|
|
8414
|
+
const linkNames = map !== null ? allSharedLinks(map, { quiet: true }) : void 0;
|
|
8415
|
+
const captures = [];
|
|
8416
|
+
stageLocalSharedEdits(map, ts, {
|
|
8417
|
+
dryRun: true,
|
|
8418
|
+
onPreview: (e) => captures.push(e),
|
|
8419
|
+
linkNames
|
|
8420
|
+
});
|
|
8421
|
+
if (captures.length === 0) return null;
|
|
8422
|
+
return { count: captures.length, backupPath: join56(backupBase(), ts) };
|
|
8423
|
+
} catch {
|
|
8424
|
+
return null;
|
|
8425
|
+
}
|
|
8426
|
+
}
|
|
8427
|
+
function planSharedReconcileBeforePull(repo, ts) {
|
|
8164
8428
|
const map = readMapForMirror(join56(repo, "path-map.json"));
|
|
8165
|
-
|
|
8429
|
+
const captures = [];
|
|
8430
|
+
const linkNames = process.platform === "win32" && map !== null ? allSharedLinks(map) : void 0;
|
|
8431
|
+
stageLocalSharedEdits(map, ts, { dryRun: true, onPreview: (e) => captures.push(e), linkNames });
|
|
8432
|
+
return {
|
|
8433
|
+
captures,
|
|
8434
|
+
deletions: planSharedLinkDeletions(map, { linkNames }),
|
|
8435
|
+
namesDerived: linkNames !== void 0,
|
|
8436
|
+
derivedSharedDirs: map?.sharedDirs
|
|
8437
|
+
};
|
|
8166
8438
|
}
|
|
8167
8439
|
|
|
8168
8440
|
// src/commands.pull.collision.ts
|
|
8169
8441
|
init_config();
|
|
8170
8442
|
init_exit_codes();
|
|
8171
|
-
import { existsSync as existsSync45, lstatSync as lstatSync19, readFileSync as readFileSync20, rmSync as
|
|
8443
|
+
import { existsSync as existsSync45, lstatSync as lstatSync19, readFileSync as readFileSync20, rmSync as rmSync19 } from "node:fs";
|
|
8172
8444
|
import { join as join57 } from "node:path";
|
|
8173
8445
|
init_utils();
|
|
8174
8446
|
var SHARED_PREFIX = /^shared\//;
|
|
@@ -8237,7 +8509,7 @@ function removeMirroredCopies(repo, repoRelPaths) {
|
|
|
8237
8509
|
try {
|
|
8238
8510
|
if (lstatSync19(abs, { throwIfNoEntry: false })?.isFile() !== true) continue;
|
|
8239
8511
|
if (!matchesLocalOriginal(repo, rel)) continue;
|
|
8240
|
-
|
|
8512
|
+
rmSync19(abs, { force: true });
|
|
8241
8513
|
} catch (err) {
|
|
8242
8514
|
warn(`could not remove nomad's copy at ${abs}: ${err.message}`);
|
|
8243
8515
|
}
|
|
@@ -8264,47 +8536,6 @@ init_commands_pull_wedge();
|
|
|
8264
8536
|
init_config();
|
|
8265
8537
|
init_utils();
|
|
8266
8538
|
init_utils_fs();
|
|
8267
|
-
|
|
8268
|
-
// src/commands.pull.recovery.git.ts
|
|
8269
|
-
init_utils();
|
|
8270
|
-
import { execFileSync as execFileSync22 } from "node:child_process";
|
|
8271
|
-
function gitCapture(args, cwd) {
|
|
8272
|
-
return execFileSync22("git", args, {
|
|
8273
|
-
cwd,
|
|
8274
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
8275
|
-
maxBuffer: 64 * 1024 * 1024
|
|
8276
|
-
}).toString().trim();
|
|
8277
|
-
}
|
|
8278
|
-
function parsePorcelainZ(raw) {
|
|
8279
|
-
const tracked = [];
|
|
8280
|
-
const untracked = [];
|
|
8281
|
-
if (!raw) return { tracked, untracked };
|
|
8282
|
-
const records = raw.split("\0");
|
|
8283
|
-
for (let i = 0; i < records.length; i++) {
|
|
8284
|
-
const record = records[i];
|
|
8285
|
-
if (record.length < 3) continue;
|
|
8286
|
-
const xy = record.slice(0, 2);
|
|
8287
|
-
const filePath = record.slice(3);
|
|
8288
|
-
if (xy === "??") {
|
|
8289
|
-
untracked.push(filePath);
|
|
8290
|
-
continue;
|
|
8291
|
-
}
|
|
8292
|
-
tracked.push(filePath);
|
|
8293
|
-
if (xy.startsWith("R") || xy.startsWith("C")) {
|
|
8294
|
-
const src = records[i + 1];
|
|
8295
|
-
if (src) {
|
|
8296
|
-
tracked.push(src);
|
|
8297
|
-
i++;
|
|
8298
|
-
}
|
|
8299
|
-
}
|
|
8300
|
-
}
|
|
8301
|
-
return { tracked, untracked };
|
|
8302
|
-
}
|
|
8303
|
-
function parseDirtyPaths(repo, opts = {}) {
|
|
8304
|
-
return parsePorcelainZ(gitStatusPorcelainZ(repo, opts));
|
|
8305
|
-
}
|
|
8306
|
-
|
|
8307
|
-
// src/commands.pull.recovery.ts
|
|
8308
8539
|
function isSyncedConfig(path) {
|
|
8309
8540
|
return PUSH_ALLOWED_STATIC.some(
|
|
8310
8541
|
(entry) => entry.endsWith("/") ? path.startsWith(entry) : path === entry
|
|
@@ -8420,9 +8651,9 @@ function capturePrePostHeads(repo, rebase) {
|
|
|
8420
8651
|
if (pre === void 0 || post === void 0) return void 0;
|
|
8421
8652
|
return { pre, post };
|
|
8422
8653
|
}
|
|
8423
|
-
function buildWetPullSections(ts, map, prePostHeads) {
|
|
8424
|
-
applySharedLinks(ts, map);
|
|
8425
|
-
writeSharedBaseline(map);
|
|
8654
|
+
function buildWetPullSections(ts, map, prePostHeads, namesDerived = false) {
|
|
8655
|
+
applySharedLinks(ts, map, { quietNames: namesDerived });
|
|
8656
|
+
writeSharedBaseline(map, { quiet: true });
|
|
8426
8657
|
const { label } = regenerateSettings(ts);
|
|
8427
8658
|
syncSkillsPull(ts, prePostHeads);
|
|
8428
8659
|
const remapResult = withSpinner("Syncing sessions", () => remapPull(ts));
|
|
@@ -8445,22 +8676,24 @@ function buildWetPullSections(ts, map, prePostHeads) {
|
|
|
8445
8676
|
};
|
|
8446
8677
|
}
|
|
8447
8678
|
function handleWedge(repo, forceRemote) {
|
|
8448
|
-
const wedge =
|
|
8449
|
-
if (wedge === null)
|
|
8679
|
+
const { state: wedge, probe } = classifyWedgeWithProbe(repo);
|
|
8680
|
+
if (wedge === null) {
|
|
8681
|
+
if (forceRemote) log(cleanRepoForceRemoteMessage(probe));
|
|
8682
|
+
return false;
|
|
8683
|
+
}
|
|
8450
8684
|
if (wedge === "unmerged-index") {
|
|
8451
8685
|
if (forceRemote) {
|
|
8452
8686
|
recoverUnmergedIndex(repo);
|
|
8453
|
-
|
|
8454
|
-
die(unmergedIndexRunbookText("nomad pull"), { code: EXIT.CONFLICT });
|
|
8687
|
+
return false;
|
|
8455
8688
|
}
|
|
8456
|
-
return;
|
|
8689
|
+
return die(unmergedIndexRunbookText("nomad pull"), { code: EXIT.CONFLICT });
|
|
8457
8690
|
}
|
|
8458
8691
|
if (forceRemote) {
|
|
8459
8692
|
recoverForceRemote(wedge, repo);
|
|
8460
|
-
return;
|
|
8693
|
+
return true;
|
|
8461
8694
|
}
|
|
8462
8695
|
const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
|
|
8463
|
-
die(wedgeMarkerRunbookText(state), { code: EXIT.CONFLICT });
|
|
8696
|
+
return die(wedgeMarkerRunbookText(state), { code: EXIT.CONFLICT });
|
|
8464
8697
|
}
|
|
8465
8698
|
function runPullCore(opts = {}) {
|
|
8466
8699
|
const dryRun = opts.dryRun === true;
|
|
@@ -8469,7 +8702,7 @@ function runPullCore(opts = {}) {
|
|
|
8469
8702
|
const repo = repoHome();
|
|
8470
8703
|
const backup = backupBase();
|
|
8471
8704
|
const ts = freshBackupTs(backup);
|
|
8472
|
-
handleWedge(repo, forceRemote);
|
|
8705
|
+
const recovered = handleWedge(repo, forceRemote);
|
|
8473
8706
|
if (!dryRun) {
|
|
8474
8707
|
const backupRoot = join58(backup, ts);
|
|
8475
8708
|
try {
|
|
@@ -8483,8 +8716,13 @@ function runPullCore(opts = {}) {
|
|
|
8483
8716
|
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
8484
8717
|
);
|
|
8485
8718
|
}
|
|
8486
|
-
const
|
|
8487
|
-
|
|
8719
|
+
const {
|
|
8720
|
+
mirrored,
|
|
8721
|
+
events: mirrorEvents,
|
|
8722
|
+
namesDerived,
|
|
8723
|
+
derivedSharedDirs
|
|
8724
|
+
} = !dryRun && !recovered ? reconcileSharedLinksBeforePull(repo, ts) : { mirrored: [], events: [], namesDerived: false, derivedSharedDirs: void 0 };
|
|
8725
|
+
const sharedPlans = dryRun ? planSharedReconcileBeforePull(repo, ts) : void 0;
|
|
8488
8726
|
const prePostHeads = capturePrePostHeads(repo, () => {
|
|
8489
8727
|
pullWithCollisionRunbook(repo, mirrored);
|
|
8490
8728
|
});
|
|
@@ -8493,18 +8731,20 @@ function runPullCore(opts = {}) {
|
|
|
8493
8731
|
const map = existsSync46(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
8494
8732
|
const divergedKeptLocal = divergenceCheckExtras(ts, dryRun ? prePostHeads : void 0);
|
|
8495
8733
|
if (dryRun) {
|
|
8496
|
-
computePreview(ts, map, "pull", sharedPlans);
|
|
8734
|
+
computePreview(ts, map, "pull", plansAgainst(sharedPlans, map));
|
|
8497
8735
|
return { tag: "dry" };
|
|
8498
8736
|
}
|
|
8737
|
+
const discardedMirror = recovered && !dryRun ? describeSkippedMirrorDiscard(repo, ts) : void 0;
|
|
8499
8738
|
const { sections, localOnly, settingsLabel, unmapped, extrasSkipped } = buildWetPullSections(
|
|
8500
8739
|
ts,
|
|
8501
8740
|
map,
|
|
8502
|
-
prePostHeads
|
|
8741
|
+
prePostHeads,
|
|
8742
|
+
namesAlreadyReported(namesDerived, derivedSharedDirs, map)
|
|
8503
8743
|
);
|
|
8504
8744
|
const incomingChanges = prePostHeads === void 0 ? true : prePostHeads.pre !== prePostHeads.post;
|
|
8505
8745
|
return {
|
|
8506
8746
|
tag: "wet",
|
|
8507
|
-
sections,
|
|
8747
|
+
sections: [buildMirrorSection(mirrorEvents, discardedMirror), ...sections],
|
|
8508
8748
|
localOnly,
|
|
8509
8749
|
divergedKeptLocal,
|
|
8510
8750
|
incomingChanges,
|
|
@@ -8612,19 +8852,6 @@ function isAllowed(path, allowed) {
|
|
|
8612
8852
|
}
|
|
8613
8853
|
return false;
|
|
8614
8854
|
}
|
|
8615
|
-
function blockSetFor(segments) {
|
|
8616
|
-
if (segments[0] !== "shared" || segments[1] !== "extras") return NEVER_SYNC;
|
|
8617
|
-
return isClaudeExtraName(segments[3] ?? "") ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
|
|
8618
|
-
}
|
|
8619
|
-
function isNeverSync(path) {
|
|
8620
|
-
const segments = path.split("/");
|
|
8621
|
-
const blockSet = blockSetFor(segments);
|
|
8622
|
-
const scan = segments[0] === "shared" && segments[1] === "extras" ? segments.slice(4) : segments;
|
|
8623
|
-
for (const segment of scan) {
|
|
8624
|
-
if (isDeniedName(blockSet, segment)) return true;
|
|
8625
|
-
}
|
|
8626
|
-
return false;
|
|
8627
|
-
}
|
|
8628
8855
|
var GSD_HOOKS_SUPPORT_FILES = /* @__PURE__ */ new Set(["managed-hooks-registry.cjs", "package.json"]);
|
|
8629
8856
|
var GSD_HOOKS_SUPPORT_DIR = "lib";
|
|
8630
8857
|
function isGsdDropped(path) {
|
|
@@ -8849,7 +9076,7 @@ init_color();
|
|
|
8849
9076
|
init_config();
|
|
8850
9077
|
init_config_sharedDirs_guard();
|
|
8851
9078
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
8852
|
-
import { copyFileSync, existsSync as existsSync49, lstatSync as lstatSync20, mkdirSync as mkdirSync15, readdirSync as readdirSync19, rmSync as
|
|
9079
|
+
import { copyFileSync, existsSync as existsSync49, lstatSync as lstatSync20, mkdirSync as mkdirSync15, readdirSync as readdirSync19, rmSync as rmSync20 } from "node:fs";
|
|
8853
9080
|
import { homedir as homedir7 } from "node:os";
|
|
8854
9081
|
import { join as join62, relative as relative10, sep as sep14 } from "node:path";
|
|
8855
9082
|
init_push_leak_verdict();
|
|
@@ -8947,7 +9174,7 @@ function previewPushLeaks(map, opts = {}) {
|
|
|
8947
9174
|
}
|
|
8948
9175
|
return verdictFromFindings(findings);
|
|
8949
9176
|
} finally {
|
|
8950
|
-
|
|
9177
|
+
rmSync20(tmpRoot, { recursive: true, force: true });
|
|
8951
9178
|
}
|
|
8952
9179
|
}
|
|
8953
9180
|
|
|
@@ -9076,7 +9303,8 @@ async function runPushCore(opts = {}) {
|
|
|
9076
9303
|
const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
|
|
9077
9304
|
if (!dryRun) {
|
|
9078
9305
|
syncSkillsPush();
|
|
9079
|
-
|
|
9306
|
+
const linkNames = map !== null ? allSharedLinks(map) : void 0;
|
|
9307
|
+
syncSharedLinksPush(map, { linkNames });
|
|
9080
9308
|
stripGsdHooksFromBase(repo, backup);
|
|
9081
9309
|
}
|
|
9082
9310
|
const st = { dryRun, remap, extras, globalConfig: [] };
|
|
@@ -9199,7 +9427,14 @@ function buildSyncSummarySection(pull, pushOutcome) {
|
|
|
9199
9427
|
for (const row2 of buildSkipAndCollisionRows(pull, result)) addItem(s, row2);
|
|
9200
9428
|
return s;
|
|
9201
9429
|
}
|
|
9202
|
-
var SYNC_SECTION_ORDER = [
|
|
9430
|
+
var SYNC_SECTION_ORDER = [
|
|
9431
|
+
"Symlinks",
|
|
9432
|
+
"Settings",
|
|
9433
|
+
"Global config",
|
|
9434
|
+
"Sessions",
|
|
9435
|
+
"Extras",
|
|
9436
|
+
"Leak scan"
|
|
9437
|
+
];
|
|
9203
9438
|
function isDroppedSummaryHeader(header) {
|
|
9204
9439
|
return header === PULL_SUMMARY_HEADER || header === "Push summary";
|
|
9205
9440
|
}
|
|
@@ -9428,7 +9663,7 @@ function buildCrashReport(input) {
|
|
|
9428
9663
|
}
|
|
9429
9664
|
|
|
9430
9665
|
// src/crash-report.redact.ts
|
|
9431
|
-
import { mkdtempSync as mkdtempSync3, rmSync as
|
|
9666
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync21, writeFileSync as writeFileSync10 } from "node:fs";
|
|
9432
9667
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
9433
9668
|
import { join as join65 } from "node:path";
|
|
9434
9669
|
init_push_gitleaks_scan();
|
|
@@ -9448,7 +9683,7 @@ function redactWithGitleaks(text, scan = scanFile) {
|
|
|
9448
9683
|
} finally {
|
|
9449
9684
|
if (dir !== void 0) {
|
|
9450
9685
|
try {
|
|
9451
|
-
|
|
9686
|
+
rmSync21(dir, { recursive: true, force: true });
|
|
9452
9687
|
} catch {
|
|
9453
9688
|
}
|
|
9454
9689
|
}
|
|
@@ -9626,7 +9861,7 @@ init_config();
|
|
|
9626
9861
|
init_utils();
|
|
9627
9862
|
init_utils_fs();
|
|
9628
9863
|
init_utils_json();
|
|
9629
|
-
import { copyFileSync as copyFileSync2, cpSync as cpSync10, existsSync as existsSync53, rmSync as
|
|
9864
|
+
import { copyFileSync as copyFileSync2, cpSync as cpSync10, existsSync as existsSync53, rmSync as rmSync22, statSync as statSync13 } from "node:fs";
|
|
9630
9865
|
import { join as join68 } from "node:path";
|
|
9631
9866
|
function snapshotIntoShared(map) {
|
|
9632
9867
|
const repo = repoHome();
|
|
@@ -9637,7 +9872,7 @@ function snapshotIntoShared(map) {
|
|
|
9637
9872
|
const dst = join68(repo, "shared", name);
|
|
9638
9873
|
if (statSync13(src).isDirectory()) {
|
|
9639
9874
|
const gk = join68(dst, ".gitkeep");
|
|
9640
|
-
if (existsSync53(gk))
|
|
9875
|
+
if (existsSync53(gk)) rmSync22(gk);
|
|
9641
9876
|
cpSync10(src, dst, { recursive: true, force: false, errorOnExist: true });
|
|
9642
9877
|
} else {
|
|
9643
9878
|
copyFileSync2(src, dst);
|
|
@@ -10098,7 +10333,7 @@ function parseSyncArgs(argv) {
|
|
|
10098
10333
|
// package.json
|
|
10099
10334
|
var package_default = {
|
|
10100
10335
|
name: "claude-nomad",
|
|
10101
|
-
version: "0.
|
|
10336
|
+
version: "0.65.0",
|
|
10102
10337
|
type: "module",
|
|
10103
10338
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
10104
10339
|
keywords: [
|
|
@@ -10209,7 +10444,8 @@ var DEFAULT_HELP = [
|
|
|
10209
10444
|
cont("abort the in-progress rebase/merge, park stranded commits on"),
|
|
10210
10445
|
cont("nomad/stranded-<ts>, reset to origin/main, and re-pull. Refuses"),
|
|
10211
10446
|
cont("if stranded or dirty tracked changes touch synced config (shared/,"),
|
|
10212
|
-
cont("hosts/, path-map.json). Cannot combine with --dry-run."),
|
|
10447
|
+
cont("hosts/, path-map.json). Cannot combine with --dry-run. On a clean"),
|
|
10448
|
+
cont("repo, reports nothing to recover and pulls normally."),
|
|
10213
10449
|
"",
|
|
10214
10450
|
row(" push", "Rebase, run safety checks (gitleaks, gitlinks, allow-list), commit, push."),
|
|
10215
10451
|
row(" --dry-run", "Run pre-checks (rebase, gitleaks probe, gitlink scan) and preview"),
|
package/package.json
CHANGED