claude-nomad 0.54.0 → 0.55.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 +43 -0
- package/README.md +66 -10
- package/dist/nomad.mjs +194 -60
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.55.1](https://github.com/funkadelic/claude-nomad/compare/v0.55.0...v0.55.1) (2026-06-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Fixed
|
|
7
|
+
|
|
8
|
+
* **config:** raise gitleaks scan timeout from 5 min to 15 min ([#367](https://github.com/funkadelic/claude-nomad/issues/367)) ([30e7344](https://github.com/funkadelic/claude-nomad/commit/30e73443be6d58330716418dced44aa6cf772623))
|
|
9
|
+
* **doctor:** de-backtrack super-linear regexes and group test hook ([#363](https://github.com/funkadelic/claude-nomad/issues/363)) ([4fdbdaa](https://github.com/funkadelic/claude-nomad/commit/4fdbdaa595d3e62cf87cab8e147da130c574f272))
|
|
10
|
+
* **push:** bound gitleaks scans and show dry-run leak-scan spinner ([#365](https://github.com/funkadelic/claude-nomad/issues/365)) ([a04d06a](https://github.com/funkadelic/claude-nomad/commit/a04d06a9624f9fa60d68cc01c69009fb0908e05f))
|
|
11
|
+
* **remap:** match Claude Code's transcript-dir path encoder ([#360](https://github.com/funkadelic/claude-nomad/issues/360)) ([77a4835](https://github.com/funkadelic/claude-nomad/commit/77a4835629a04fbf045930b1938b2404f0b00922))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Documentation
|
|
15
|
+
|
|
16
|
+
* correct path-encoding rule and note Windows-style paths ([#362](https://github.com/funkadelic/claude-nomad/issues/362)) ([970aae4](https://github.com/funkadelic/claude-nomad/commit/970aae42ebba734a878f9712ac304f0c393b9398))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Testing
|
|
20
|
+
|
|
21
|
+
* use specific assertions for length and exit-code checks ([#364](https://github.com/funkadelic/claude-nomad/issues/364)) ([95549d4](https://github.com/funkadelic/claude-nomad/commit/95549d4694e5f0dc09ac1aa13d4240e01cb264f9))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Dependencies
|
|
25
|
+
|
|
26
|
+
* **deps-dev:** bump the dev-dependencies group with 6 updates ([#366](https://github.com/funkadelic/claude-nomad/issues/366)) ([70d1057](https://github.com/funkadelic/claude-nomad/commit/70d10578eb8d58010b160b88a6d61eaa9e8405a0))
|
|
27
|
+
|
|
28
|
+
## [0.55.0](https://github.com/funkadelic/claude-nomad/compare/v0.54.0...v0.55.0) (2026-06-28)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
* **doctor:** warn on misaligned host key when NOMAD_HOST is unset ([#356](https://github.com/funkadelic/claude-nomad/issues/356)) ([a13e833](https://github.com/funkadelic/claude-nomad/commit/a13e833a09636f98f1e9c51e9c41f01a4323dea2))
|
|
34
|
+
* **init:** offer to snapshot an existing ~/.claude on init ([#357](https://github.com/funkadelic/claude-nomad/issues/357)) ([92e90b1](https://github.com/funkadelic/claude-nomad/commit/92e90b1977fff51d565fa5d11b3c640790ee1b57))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
* make sync warning messages user-friendly ([#345](https://github.com/funkadelic/claude-nomad/issues/345)) ([002f57d](https://github.com/funkadelic/claude-nomad/commit/002f57de240dfb1349742fa4c2b4ac5e51b5f7ba))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Documentation
|
|
43
|
+
|
|
44
|
+
* clarify onboarding (two-dir model, session sync, push order) ([#355](https://github.com/funkadelic/claude-nomad/issues/355)) ([396fbb8](https://github.com/funkadelic/claude-nomad/commit/396fbb875b4331800ee74834944da17decbcad92))
|
|
45
|
+
|
|
3
46
|
## [0.54.0](https://github.com/funkadelic/claude-nomad/compare/v0.53.3...v0.54.0) (2026-06-27)
|
|
4
47
|
|
|
5
48
|
|
package/README.md
CHANGED
|
@@ -39,7 +39,9 @@ survives different file paths and your secrets never ride along.
|
|
|
39
39
|
live settings syncs normally via `nomad capture-settings`.
|
|
40
40
|
- **Every push is secret-scanned.** Only an explicit allow-list of paths ever leaves the machine,
|
|
41
41
|
credentials never sync, and gitleaks scans the exact files about to be published. The push aborts
|
|
42
|
-
on any hit, with an interactive menu to redact, allow, or drop the finding.
|
|
42
|
+
on any hit, with an interactive menu to redact, allow, or drop the finding. Always publish through
|
|
43
|
+
`nomad push`: the sync repo is an ordinary Git repo, so a manual `git push` from it skips the scan
|
|
44
|
+
entirely and can leak a secret that `nomad push` would have caught.
|
|
43
45
|
- **Preview before you trust it.** `nomad diff` shows offline what a pull would change (gsd-owned
|
|
44
46
|
hook churn is filtered the same as on pull, so the preview matches what a real pull writes), and
|
|
45
47
|
`--dry-run` on pull and push prints the plan without writing anything.
|
|
@@ -47,13 +49,16 @@ survives different file paths and your secrets never ride along.
|
|
|
47
49
|
repo, broken hook references, hooks that would crash on session start because of a missing
|
|
48
50
|
`--preserve-symlinks-main` flag, version drift, oversized backup cache, missing git committer
|
|
49
51
|
identity in the sync repo (a push fails at commit time without one), path-map entries whose local
|
|
50
|
-
project folder no longer exists on this machine,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
project folder no longer exists on this machine, a multi-host repo where this machine's
|
|
53
|
+
hostname-derived key matches no `hosts/<HOST>.json` or path-map entry (a sign `NOMAD_HOST` is
|
|
54
|
+
unset here, so per-host settings and session sync will not line up with the other hosts), synced
|
|
55
|
+
skills with local edits that differ from the shared copy, and settings drift in both directions:
|
|
56
|
+
keys present in the repo merge but absent from your live `settings.json` (behind; the next
|
|
57
|
+
`nomad pull` will restore them, fix: `nomad pull`) and keys present locally but not yet in the
|
|
58
|
+
repo (ahead; local-only additions, fix: `nomad capture-settings`). Each issue includes a fix hint.
|
|
59
|
+
By default the report is compact: it shows only checks that need action plus a one-line verdict.
|
|
60
|
+
Add `--verbose` (or `--all` / `-v`) to see the full per-check tree, including everything that
|
|
61
|
+
passed.
|
|
57
62
|
- **Self-healing sync.** Every overwrite is backed up first, and `nomad pull --force-remote`
|
|
58
63
|
recovers two kinds of stuck sync repo: a repo stuck mid-rebase or mid-merge (aborts the operation,
|
|
59
64
|
parks stranded work on a branch, refuses if shared config is at risk), and a repo where the rebase
|
|
@@ -68,15 +73,27 @@ opt-in per-project sync, transcript redaction, backup pruning, and more.
|
|
|
68
73
|
|
|
69
74
|
## Quickstart
|
|
70
75
|
|
|
76
|
+
nomad works with two directories, and the difference is the one thing worth learning up front:
|
|
77
|
+
|
|
78
|
+
- **`~/claude-nomad/`** is your private sync repo. This is the one you edit.
|
|
79
|
+
- **`~/.claude/`** is Claude Code's live config. nomad regenerates it on every `pull`.
|
|
80
|
+
|
|
81
|
+
Edit the repo, never the live config. In particular, never hand-edit `~/.claude/settings.json`: it
|
|
82
|
+
is rebuilt from the repo on every pull and your changes are lost. Change `shared/settings.base.json`
|
|
83
|
+
(or `hosts/<HOST>.json`) in the repo instead, or run `nomad capture-settings` to pull local changes
|
|
84
|
+
back into the repo (see [Changing settings](#changing-settings)).
|
|
85
|
+
|
|
71
86
|
**First host** (once, ever):
|
|
72
87
|
|
|
73
88
|
```bash
|
|
74
89
|
# 1. Install the CLI.
|
|
75
90
|
$ npm i -g claude-nomad
|
|
76
91
|
|
|
77
|
-
# 2. Create your private sync repo and scaffold it.
|
|
92
|
+
# 2. Create your private sync repo and scaffold it. If you already have a
|
|
93
|
+
# ~/.claude/ worth keeping, init offers to seed the repo from it.
|
|
78
94
|
$ nomad init # prompts for a repo name (default: claude-nomad-config)
|
|
79
|
-
$ nomad init --repo my-config #
|
|
95
|
+
$ nomad init --repo my-config # set the repo name without the prompt
|
|
96
|
+
$ nomad init --snapshot # seed from existing ~/.claude/ without being asked
|
|
80
97
|
|
|
81
98
|
# 3. Add a stable host label to ~/.zshrc or ~/.bashrc, then reload.
|
|
82
99
|
export NOMAD_HOST=<your-host-label>
|
|
@@ -102,6 +119,45 @@ $ nomad pull # apply config to ~/.claude/
|
|
|
102
119
|
$ nomad push # publish local changes (sessions, settings)
|
|
103
120
|
```
|
|
104
121
|
|
|
122
|
+
Pull before you push whenever both machines may have changed. Sync is last-write-wins, so pushing
|
|
123
|
+
stale local state over newer remote state silently overwrites it. The
|
|
124
|
+
[FAQ](https://funkadelic.github.io/claude-nomad/faq/) covers the full push/pull order when both
|
|
125
|
+
sides have changed.
|
|
126
|
+
|
|
127
|
+
### Make your sessions follow you
|
|
128
|
+
|
|
129
|
+
Session history only syncs for projects you list in `path-map.json`, and a fresh `init` starts with
|
|
130
|
+
none, so no sessions sync until you add a mapping. Each entry maps a logical project name to the
|
|
131
|
+
absolute path it lives at on each host:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{
|
|
135
|
+
"projects": {
|
|
136
|
+
"my-app": {
|
|
137
|
+
"laptop": "/Users/you/code/my-app",
|
|
138
|
+
"desktop": "/home/you/projects/my-app"
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The host keys (`laptop`, `desktop`) are the same labels you set in `NOMAD_HOST` on each machine.
|
|
145
|
+
After editing `path-map.json`, `nomad push` publishes the matching sessions and `nomad pull` on
|
|
146
|
+
another host copies them into place, rewriting the embedded file paths so `claude --resume` finds
|
|
147
|
+
them at that host's path.
|
|
148
|
+
|
|
149
|
+
### Changing settings
|
|
150
|
+
|
|
151
|
+
There are two ways a settings change reaches the repo, and the right one depends on where you made
|
|
152
|
+
it:
|
|
153
|
+
|
|
154
|
+
- **You are deciding the change:** edit `shared/settings.base.json` (shared by every host) or
|
|
155
|
+
`hosts/<HOST>.json` (one machine only) in the repo, then `nomad push`.
|
|
156
|
+
- **Something else already wrote it** (Claude Code or a tool added keys to your live
|
|
157
|
+
`~/.claude/settings.json`): run `nomad capture-settings` to promote those keys into the repo
|
|
158
|
+
before the next `nomad pull` overwrites them. Add `--host` to land machine-specific values (such
|
|
159
|
+
as absolute paths) in `hosts/<HOST>.json` instead of the shared base.
|
|
160
|
+
|
|
105
161
|
During `nomad push` and `nomad pull`, long-running steps (rebase, secret scan, git push, session
|
|
106
162
|
sync) show an animated progress indicator on an interactive terminal so the CLI does not look hung.
|
|
107
163
|
In CI and when output is piped, only plain text lines are printed, with no ANSI control codes, so
|
package/dist/nomad.mjs
CHANGED
|
@@ -423,7 +423,7 @@ function allSharedLinks(map) {
|
|
|
423
423
|
}
|
|
424
424
|
return [...SHARED_LINKS, ...extras];
|
|
425
425
|
}
|
|
426
|
-
var SETTINGS_SCHEMA_URL, NPM_REGISTRY_LATEST_URL, GITLEAKS_PINNED_VERSION, HOST, SHARED_LINKS, GSD_PREFIX, GSD_DROPPED_NAMES, SUPPORTED_EXTRAS, ALWAYS_NEVER_SYNC, PUSH_ALLOWED_STATIC;
|
|
426
|
+
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, ALWAYS_NEVER_SYNC, PUSH_ALLOWED_STATIC;
|
|
427
427
|
var init_config = __esm({
|
|
428
428
|
"src/config.ts"() {
|
|
429
429
|
"use strict";
|
|
@@ -434,6 +434,7 @@ var init_config = __esm({
|
|
|
434
434
|
SETTINGS_SCHEMA_URL = "https://json.schemastore.org/claude-code-settings.json";
|
|
435
435
|
NPM_REGISTRY_LATEST_URL = "https://registry.npmjs.org/claude-nomad/latest";
|
|
436
436
|
GITLEAKS_PINNED_VERSION = "8.30.1";
|
|
437
|
+
GITLEAKS_SCAN_TIMEOUT_MS = 9e5;
|
|
437
438
|
HOST = (process.env.NOMAD_HOST || hostname()).toLowerCase();
|
|
438
439
|
SHARED_LINKS = ["CLAUDE.md", "commands", "rules", "my-statusline.cjs"];
|
|
439
440
|
GSD_PREFIX = "gsd-";
|
|
@@ -523,13 +524,23 @@ function sortKeysDeep(value) {
|
|
|
523
524
|
}
|
|
524
525
|
return value;
|
|
525
526
|
}
|
|
526
|
-
var encodePath;
|
|
527
|
+
var ENCODE_MAX, hashPath, encodePath;
|
|
527
528
|
var init_utils_json = __esm({
|
|
528
529
|
"src/utils.json.ts"() {
|
|
529
530
|
"use strict";
|
|
530
531
|
init_config();
|
|
531
532
|
init_utils();
|
|
532
|
-
|
|
533
|
+
ENCODE_MAX = 200;
|
|
534
|
+
hashPath = (s) => {
|
|
535
|
+
let h2 = 0;
|
|
536
|
+
for (let i = 0; i < s.length; i++) h2 = (h2 << 5) - h2 + s.charCodeAt(i) | 0;
|
|
537
|
+
return h2;
|
|
538
|
+
};
|
|
539
|
+
encodePath = (absPath) => {
|
|
540
|
+
const enc = absPath.replace(/[^a-zA-Z0-9]/g, "-");
|
|
541
|
+
if (enc.length <= ENCODE_MAX) return enc;
|
|
542
|
+
return `${enc.slice(0, ENCODE_MAX)}-${Math.abs(hashPath(absPath)).toString(36)}`;
|
|
543
|
+
};
|
|
533
544
|
}
|
|
534
545
|
});
|
|
535
546
|
|
|
@@ -769,7 +780,7 @@ var init_push_gitleaks_config = __esm({
|
|
|
769
780
|
"use strict";
|
|
770
781
|
init_config();
|
|
771
782
|
init_utils();
|
|
772
|
-
OVERLAY_EXTEND_RE =
|
|
783
|
+
OVERLAY_EXTEND_RE = /^[ \t]*(?:\[[ \t]*extend[ \t]*\]|extend[ \t]*[.=])/m;
|
|
773
784
|
TABLE_HEADER_RE = /^\s*\[/;
|
|
774
785
|
OVERLAY_ALLOWLIST_HEADER_RE = /^\s*\[\[?\s*allowlists?\s*\]\]?/;
|
|
775
786
|
OVERLAY_INLINE_ALLOWLIST_RE = /^[ \t]*allowlists?[ \t]*[.=]/m;
|
|
@@ -780,7 +791,7 @@ var init_push_gitleaks_config = __esm({
|
|
|
780
791
|
|
|
781
792
|
// src/push-checks.ts
|
|
782
793
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
783
|
-
import { readdirSync as
|
|
794
|
+
import { readdirSync as readdirSync5, rmSync as rmSync5 } from "node:fs";
|
|
784
795
|
import { homedir as homedir2, platform } from "node:os";
|
|
785
796
|
import { join as join17 } from "node:path";
|
|
786
797
|
function gitleaksInstallHint() {
|
|
@@ -820,7 +831,7 @@ function findGitlinks(dir) {
|
|
|
820
831
|
function walk(current) {
|
|
821
832
|
let entries;
|
|
822
833
|
try {
|
|
823
|
-
entries =
|
|
834
|
+
entries = readdirSync5(current, { withFileTypes: true });
|
|
824
835
|
} catch {
|
|
825
836
|
return;
|
|
826
837
|
}
|
|
@@ -912,10 +923,11 @@ function scanStagedTree(repoDir, forwardStreams = false) {
|
|
|
912
923
|
];
|
|
913
924
|
if (toml !== null) args.push("--config", toml);
|
|
914
925
|
const opts = { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] };
|
|
926
|
+
const gitleaksOpts = { ...opts, timeout: GITLEAKS_SCAN_TIMEOUT_MS };
|
|
915
927
|
try {
|
|
916
928
|
execFileSync8("git", ["init", "-q"], opts);
|
|
917
929
|
execFileSync8("git", ["add", "-A"], opts);
|
|
918
|
-
execFileSync8("gitleaks", args,
|
|
930
|
+
execFileSync8("gitleaks", args, gitleaksOpts);
|
|
919
931
|
return [];
|
|
920
932
|
} catch (err) {
|
|
921
933
|
const e = err;
|
|
@@ -945,7 +957,10 @@ function scanFile(filePath, forwardStreams = false) {
|
|
|
945
957
|
`--report-path=${reportPath}`
|
|
946
958
|
];
|
|
947
959
|
if (toml !== null) args.push("--config", toml);
|
|
948
|
-
const opts = {
|
|
960
|
+
const opts = {
|
|
961
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
962
|
+
timeout: GITLEAKS_SCAN_TIMEOUT_MS
|
|
963
|
+
};
|
|
949
964
|
try {
|
|
950
965
|
execFileSync8("gitleaks", args, opts);
|
|
951
966
|
return [];
|
|
@@ -966,6 +981,7 @@ function scanFile(filePath, forwardStreams = false) {
|
|
|
966
981
|
var init_push_gitleaks_scan = __esm({
|
|
967
982
|
"src/push-gitleaks.scan.ts"() {
|
|
968
983
|
"use strict";
|
|
984
|
+
init_config();
|
|
969
985
|
init_push_gitleaks_config();
|
|
970
986
|
init_utils_fs();
|
|
971
987
|
}
|
|
@@ -1505,6 +1521,14 @@ function partitionByCaptureExclusion(keys) {
|
|
|
1505
1521
|
}
|
|
1506
1522
|
return { promotable, excluded };
|
|
1507
1523
|
}
|
|
1524
|
+
function describeSettings(keys) {
|
|
1525
|
+
const one = keys.length === 1;
|
|
1526
|
+
return {
|
|
1527
|
+
phrase: `${keys.length} ${one ? "setting" : "settings"} (${keys.join(", ")})`,
|
|
1528
|
+
pronoun: one ? "it" : "them",
|
|
1529
|
+
verb: one ? "is" : "are"
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1508
1532
|
var BIN_NODE_RE = /^"?(?:[A-Za-z]:)?[\\/](?:.*[\\/])?bin[\\/]node"?$/;
|
|
1509
1533
|
var LEADING_BIN_NODE_RE = /^"?(?:[A-Za-z]:)?[\\/](?:[^"\s]*[\\/])?bin[\\/]node"?(?=\s)/;
|
|
1510
1534
|
function normalizeNodePathsDeep(value) {
|
|
@@ -1614,14 +1638,16 @@ function regenerateSettings(ts, opts = {}) {
|
|
|
1614
1638
|
const existing = readJson(settingsPath);
|
|
1615
1639
|
const drift = classifySettingsDrift(merged, existing);
|
|
1616
1640
|
if (drift.behind.length > 0) {
|
|
1641
|
+
const { phrase, pronoun } = describeSettings(drift.behind);
|
|
1617
1642
|
warn(
|
|
1618
|
-
`
|
|
1643
|
+
`your settings.json is missing ${phrase} that the synced copy has; run 'nomad pull' to restore ${pronoun}.`
|
|
1619
1644
|
);
|
|
1620
1645
|
}
|
|
1621
1646
|
const { promotable } = partitionByCaptureExclusion(drift.ahead);
|
|
1622
1647
|
if (promotable.length > 0) {
|
|
1648
|
+
const { phrase, pronoun, verb } = describeSettings(promotable);
|
|
1623
1649
|
warn(
|
|
1624
|
-
`
|
|
1650
|
+
`your settings.json has ${phrase} that ${verb} not yet synced; run 'nomad capture-settings' to save ${pronoun} to the repo before the next pull overwrites ${pronoun}.`
|
|
1625
1651
|
);
|
|
1626
1652
|
}
|
|
1627
1653
|
} catch {
|
|
@@ -2069,7 +2095,7 @@ import { join as join36 } from "node:path";
|
|
|
2069
2095
|
// src/commands.doctor.checks.repo.ts
|
|
2070
2096
|
init_color();
|
|
2071
2097
|
init_config();
|
|
2072
|
-
import { existsSync as existsSync9, lstatSync as lstatSync6, statSync as statSync3 } from "node:fs";
|
|
2098
|
+
import { existsSync as existsSync9, lstatSync as lstatSync6, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
|
|
2073
2099
|
import { join as join11 } from "node:path";
|
|
2074
2100
|
|
|
2075
2101
|
// src/commands.doctor.format.ts
|
|
@@ -2189,6 +2215,7 @@ function reasonForPartial(repoHome2, host) {
|
|
|
2189
2215
|
}
|
|
2190
2216
|
|
|
2191
2217
|
// src/commands.doctor.checks.repo.ts
|
|
2218
|
+
init_utils_json();
|
|
2192
2219
|
function isOverrideActive() {
|
|
2193
2220
|
return Boolean(process.env.NOMAD_REPO);
|
|
2194
2221
|
}
|
|
@@ -2206,6 +2233,46 @@ function reportHostAndPaths(section2) {
|
|
|
2206
2233
|
`${existsSync9(claude) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(claude)}`
|
|
2207
2234
|
);
|
|
2208
2235
|
}
|
|
2236
|
+
function pathMapHostKeys() {
|
|
2237
|
+
const mapPath = join11(repoHome(), "path-map.json");
|
|
2238
|
+
if (!existsSync9(mapPath)) return /* @__PURE__ */ new Set();
|
|
2239
|
+
let raw;
|
|
2240
|
+
try {
|
|
2241
|
+
raw = readJson(mapPath);
|
|
2242
|
+
} catch {
|
|
2243
|
+
return /* @__PURE__ */ new Set();
|
|
2244
|
+
}
|
|
2245
|
+
if (validatePathMapShape(raw) !== null) return /* @__PURE__ */ new Set();
|
|
2246
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2247
|
+
for (const hosts of Object.values(raw.projects)) {
|
|
2248
|
+
for (const key of Object.keys(hosts)) keys.add(key);
|
|
2249
|
+
}
|
|
2250
|
+
return keys;
|
|
2251
|
+
}
|
|
2252
|
+
function hostOverrideLabels() {
|
|
2253
|
+
let entries;
|
|
2254
|
+
try {
|
|
2255
|
+
entries = readdirSync2(join11(repoHome(), "hosts"));
|
|
2256
|
+
} catch {
|
|
2257
|
+
return /* @__PURE__ */ new Set();
|
|
2258
|
+
}
|
|
2259
|
+
const labels = /* @__PURE__ */ new Set();
|
|
2260
|
+
for (const entry of entries) {
|
|
2261
|
+
if (entry.endsWith(".json")) labels.add(entry.slice(0, -".json".length));
|
|
2262
|
+
}
|
|
2263
|
+
return labels;
|
|
2264
|
+
}
|
|
2265
|
+
function reportHostKeyAlignment(section2) {
|
|
2266
|
+
if (process.env.NOMAD_HOST) return;
|
|
2267
|
+
const overrideLabels = hostOverrideLabels();
|
|
2268
|
+
const mapKeys = pathMapHostKeys();
|
|
2269
|
+
if (overrideLabels.has(HOST) || mapKeys.has(HOST)) return;
|
|
2270
|
+
if (overrideLabels.size === 0 && mapKeys.size === 0) return;
|
|
2271
|
+
addItem(
|
|
2272
|
+
section2,
|
|
2273
|
+
`${yellow(warnGlyph)} NOMAD_HOST unset: this repo configures other hosts, but the hostname key ${cyan(HOST)} matches no hosts/${HOST}.json or path-map entry; set NOMAD_HOST to the label this host should use so per-host settings and session sync line up`
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2209
2276
|
function reportRepoState(section2) {
|
|
2210
2277
|
const repo = repoHome();
|
|
2211
2278
|
const state = classifyRepoState(repo, HOST);
|
|
@@ -2301,7 +2368,7 @@ function reportDroppedNamesMigration(section2) {
|
|
|
2301
2368
|
// src/commands.doctor.checks.settings.ts
|
|
2302
2369
|
init_color();
|
|
2303
2370
|
init_config();
|
|
2304
|
-
import { existsSync as existsSync10, readdirSync as
|
|
2371
|
+
import { existsSync as existsSync10, readdirSync as readdirSync3 } from "node:fs";
|
|
2305
2372
|
import { join as join12 } from "node:path";
|
|
2306
2373
|
function loadBaseSettings(section2) {
|
|
2307
2374
|
const basePath = join12(repoHome(), "shared", "settings.base.json");
|
|
@@ -2347,7 +2414,7 @@ function reportHostOverrides(section2, base, settings) {
|
|
|
2347
2414
|
);
|
|
2348
2415
|
const hostsDir = join12(repo, "hosts");
|
|
2349
2416
|
if (existsSync10(hostsDir)) {
|
|
2350
|
-
const cands =
|
|
2417
|
+
const cands = readdirSync3(hostsDir).filter((f) => f.endsWith(".json"));
|
|
2351
2418
|
if (cands.length > 0) addItem(section2, `${dim(infoGlyph)} candidates: ${cands.join(", ")}`);
|
|
2352
2419
|
}
|
|
2353
2420
|
process.exitCode = 1;
|
|
@@ -2362,7 +2429,7 @@ function reportHostOverrides(section2, base, settings) {
|
|
|
2362
2429
|
// src/commands.doctor.checks.pathmap.ts
|
|
2363
2430
|
init_color();
|
|
2364
2431
|
init_config();
|
|
2365
|
-
import { existsSync as existsSync11, readdirSync as
|
|
2432
|
+
import { existsSync as existsSync11, readdirSync as readdirSync4 } from "node:fs";
|
|
2366
2433
|
import { join as join13 } from "node:path";
|
|
2367
2434
|
init_utils_json();
|
|
2368
2435
|
function reportMappedProjects(section2, map) {
|
|
@@ -2377,7 +2444,7 @@ function reportUnmappedProjects(section2, map) {
|
|
|
2377
2444
|
if (!existsSync11(localProjects)) return;
|
|
2378
2445
|
let localDirs;
|
|
2379
2446
|
try {
|
|
2380
|
-
localDirs =
|
|
2447
|
+
localDirs = readdirSync4(localProjects);
|
|
2381
2448
|
} catch {
|
|
2382
2449
|
return;
|
|
2383
2450
|
}
|
|
@@ -2666,13 +2733,13 @@ function reportGitIdentity(section2) {
|
|
|
2666
2733
|
|
|
2667
2734
|
// src/commands.doctor.checks.backups.ts
|
|
2668
2735
|
init_color();
|
|
2669
|
-
import { existsSync as existsSync16, lstatSync as lstatSync7, readdirSync as
|
|
2736
|
+
import { existsSync as existsSync16, lstatSync as lstatSync7, readdirSync as readdirSync6 } from "node:fs";
|
|
2670
2737
|
import { join as join19 } from "node:path";
|
|
2671
2738
|
init_config();
|
|
2672
2739
|
var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
|
|
2673
2740
|
function safeReaddir(dir) {
|
|
2674
2741
|
try {
|
|
2675
|
-
return
|
|
2742
|
+
return readdirSync6(dir);
|
|
2676
2743
|
} catch {
|
|
2677
2744
|
return [];
|
|
2678
2745
|
}
|
|
@@ -2848,7 +2915,7 @@ function reportCheckSchema(section2) {
|
|
|
2848
2915
|
init_color();
|
|
2849
2916
|
import { randomBytes } from "node:crypto";
|
|
2850
2917
|
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
2851
|
-
import { existsSync as existsSync19, mkdirSync as mkdirSync5, readdirSync as
|
|
2918
|
+
import { existsSync as existsSync19, mkdirSync as mkdirSync5, readdirSync as readdirSync8, rmSync as rmSync8 } from "node:fs";
|
|
2852
2919
|
import { homedir as homedir4 } from "node:os";
|
|
2853
2920
|
import { join as join24 } from "node:path";
|
|
2854
2921
|
|
|
@@ -2947,7 +3014,7 @@ init_config();
|
|
|
2947
3014
|
|
|
2948
3015
|
// src/remap.ts
|
|
2949
3016
|
init_config_sharedDirs_guard();
|
|
2950
|
-
import { cpSync as cpSync4, existsSync as existsSync18, mkdirSync as mkdirSync4, readdirSync as
|
|
3017
|
+
import { cpSync as cpSync4, existsSync as existsSync18, mkdirSync as mkdirSync4, readdirSync as readdirSync7, renameSync as renameSync3, rmSync as rmSync7, statSync as statSync4 } from "node:fs";
|
|
2951
3018
|
import { join as join23, relative as relative3, sep as sep3 } from "node:path";
|
|
2952
3019
|
|
|
2953
3020
|
// src/extras-sync.guards.ts
|
|
@@ -3087,7 +3154,7 @@ function remapPush(ts, opts = {}) {
|
|
|
3087
3154
|
const reverse = buildReverseMap(map);
|
|
3088
3155
|
if (!existsSync18(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
|
|
3089
3156
|
if (!dryRun) mkdirSync4(repoProjects, { recursive: true });
|
|
3090
|
-
for (const dir of
|
|
3157
|
+
for (const dir of readdirSync7(localProjects)) {
|
|
3091
3158
|
if (dir.endsWith(TMP_SUFFIX)) continue;
|
|
3092
3159
|
const logical = reverse.get(dir);
|
|
3093
3160
|
if (!logical) {
|
|
@@ -3133,7 +3200,7 @@ function buildScanTree(tmpRoot) {
|
|
|
3133
3200
|
}
|
|
3134
3201
|
const localProjects = join24(claudeHome(), "projects");
|
|
3135
3202
|
if (!existsSync19(localProjects)) return { logicalToEncoded, staged, malformed: false };
|
|
3136
|
-
for (const dir of
|
|
3203
|
+
for (const dir of readdirSync8(localProjects)) {
|
|
3137
3204
|
const logical = reverse.get(dir);
|
|
3138
3205
|
if (!logical) continue;
|
|
3139
3206
|
copyDirJsonlOnly(join24(localProjects, dir), join24(tmpRoot, "shared", "projects", logical));
|
|
@@ -3192,7 +3259,7 @@ function reportCheckShared(section2, gitleaksReady) {
|
|
|
3192
3259
|
|
|
3193
3260
|
// src/commands.doctor.checks.hooks.scope.ts
|
|
3194
3261
|
init_color();
|
|
3195
|
-
import { existsSync as existsSync20, readFileSync as readFileSync6, readdirSync as
|
|
3262
|
+
import { existsSync as existsSync20, readFileSync as readFileSync6, readdirSync as readdirSync9, realpathSync as realpathSync2 } from "node:fs";
|
|
3196
3263
|
import { dirname as dirname3, extname, join as join25 } from "node:path";
|
|
3197
3264
|
init_config();
|
|
3198
3265
|
function typeFromPackageJson(pkgPath) {
|
|
@@ -3234,7 +3301,7 @@ function stripCommentsAndStrings(src) {
|
|
|
3234
3301
|
function classifySource(src) {
|
|
3235
3302
|
const code = stripCommentsAndStrings(src);
|
|
3236
3303
|
const cjs = /\brequire\s*\(/.test(code) || /\bmodule\.exports\b/.test(code) || /\bexports\.\w/.test(code);
|
|
3237
|
-
const esm = /^[
|
|
3304
|
+
const esm = /^[ \t]*import\s/m.test(code) || /^[ \t]*export\s/m.test(code) || /\bimport\.meta\b/.test(code);
|
|
3238
3305
|
if (cjs && !esm) return "cjs";
|
|
3239
3306
|
if (esm && !cjs) return "esm";
|
|
3240
3307
|
if (cjs && esm) return "cjs";
|
|
@@ -3245,7 +3312,7 @@ function remedy(family) {
|
|
|
3245
3312
|
}
|
|
3246
3313
|
function safeReaddir2(dir) {
|
|
3247
3314
|
try {
|
|
3248
|
-
return
|
|
3315
|
+
return readdirSync9(dir);
|
|
3249
3316
|
} catch {
|
|
3250
3317
|
return [];
|
|
3251
3318
|
}
|
|
@@ -3293,8 +3360,12 @@ function expandHome(token) {
|
|
|
3293
3360
|
const h2 = home();
|
|
3294
3361
|
return token.replace(/^\$\{HOME\}/, h2).replace(/^\$HOME/, h2).replace(/^~/, h2);
|
|
3295
3362
|
}
|
|
3363
|
+
var TRAILING_SHELL_PUNCT = /* @__PURE__ */ new Set(["'", '"', "`", ";", ")", "|", "&", ">"]);
|
|
3296
3364
|
function stripShellPunctuation(token) {
|
|
3297
|
-
|
|
3365
|
+
const stripped = token.replace(/^['"]+/, "");
|
|
3366
|
+
let end = stripped.length;
|
|
3367
|
+
while (end > 0 && TRAILING_SHELL_PUNCT.has(stripped[end - 1])) end--;
|
|
3368
|
+
return stripped.slice(0, end);
|
|
3298
3369
|
}
|
|
3299
3370
|
function* claudePathsIn(command) {
|
|
3300
3371
|
const claudePrefix = `${claudeHome()}/`;
|
|
@@ -3811,7 +3882,7 @@ import { existsSync as existsSync26, statSync as statSync6 } from "node:fs";
|
|
|
3811
3882
|
import { dirname as dirname5, join as join31 } from "node:path";
|
|
3812
3883
|
|
|
3813
3884
|
// src/commands.redact.subtree.ts
|
|
3814
|
-
import { existsSync as existsSync25, lstatSync as lstatSync8, readFileSync as readFileSync11, readdirSync as
|
|
3885
|
+
import { existsSync as existsSync25, lstatSync as lstatSync8, readFileSync as readFileSync11, readdirSync as readdirSync10, statSync as statSync5, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3815
3886
|
import { join as join30 } from "node:path";
|
|
3816
3887
|
init_utils_fs();
|
|
3817
3888
|
init_utils();
|
|
@@ -3819,7 +3890,7 @@ function collectFiles(dir, out) {
|
|
|
3819
3890
|
if (!existsSync25(dir)) return;
|
|
3820
3891
|
const st = lstatSync8(dir);
|
|
3821
3892
|
if (!st.isDirectory()) return;
|
|
3822
|
-
for (const entry of
|
|
3893
|
+
for (const entry of readdirSync10(dir)) {
|
|
3823
3894
|
const abs = join30(dir, entry);
|
|
3824
3895
|
const lst = lstatSync8(abs);
|
|
3825
3896
|
if (lst.isSymbolicLink()) continue;
|
|
@@ -4580,7 +4651,7 @@ function reportOptionalDeps(section2, run = execFileSync12) {
|
|
|
4580
4651
|
} else {
|
|
4581
4652
|
addItem(
|
|
4582
4653
|
section2,
|
|
4583
|
-
`${yellow(warnGlyph)} gh: not installed (optional;
|
|
4654
|
+
`${yellow(warnGlyph)} gh: not installed (optional; nomad init needs it to create the sync repo and auto-disable Actions, and the Actions-drift check uses it)`
|
|
4584
4655
|
);
|
|
4585
4656
|
}
|
|
4586
4657
|
reportFetcherRow(section2, run);
|
|
@@ -4736,6 +4807,7 @@ function compactSections(sections) {
|
|
|
4736
4807
|
function gatherDoctorSections(opts) {
|
|
4737
4808
|
const host = section("Environment");
|
|
4738
4809
|
reportHostAndPaths(host);
|
|
4810
|
+
reportHostKeyAlignment(host);
|
|
4739
4811
|
reportRepoState(host);
|
|
4740
4812
|
const links = section("Shared links");
|
|
4741
4813
|
const mapPath = join36(repoHome(), "path-map.json");
|
|
@@ -4836,7 +4908,7 @@ function parseDoctorArgs(args) {
|
|
|
4836
4908
|
// src/commands.drop-session.ts
|
|
4837
4909
|
init_config();
|
|
4838
4910
|
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
4839
|
-
import { existsSync as existsSync32, readdirSync as
|
|
4911
|
+
import { existsSync as existsSync32, readdirSync as readdirSync11, statSync as statSync8 } from "node:fs";
|
|
4840
4912
|
import { join as join38, relative as relative4 } from "node:path";
|
|
4841
4913
|
|
|
4842
4914
|
// src/commands.drop-session.git.ts
|
|
@@ -4949,7 +5021,7 @@ function cmdDropSession(id) {
|
|
|
4949
5021
|
}
|
|
4950
5022
|
function collectMatches(repoProjects, id, repo) {
|
|
4951
5023
|
const matches = [];
|
|
4952
|
-
for (const logical of
|
|
5024
|
+
for (const logical of readdirSync11(repoProjects)) {
|
|
4953
5025
|
const candidate = join38(repoProjects, logical, `${id}.jsonl`);
|
|
4954
5026
|
if (existsSync32(candidate)) {
|
|
4955
5027
|
matches.push(relative4(repo, candidate));
|
|
@@ -5086,12 +5158,12 @@ init_config();
|
|
|
5086
5158
|
|
|
5087
5159
|
// src/extras-sync.ts
|
|
5088
5160
|
init_config();
|
|
5089
|
-
import { existsSync as existsSync35 } from "node:fs";
|
|
5161
|
+
import { existsSync as existsSync35, statSync as statSync9 } from "node:fs";
|
|
5090
5162
|
import { join as join42 } from "node:path";
|
|
5091
5163
|
|
|
5092
5164
|
// src/extras-sync.core.ts
|
|
5093
5165
|
init_config();
|
|
5094
|
-
import { cpSync as cpSync6, existsSync as existsSync33, lstatSync as lstatSync9, readdirSync as
|
|
5166
|
+
import { cpSync as cpSync6, existsSync as existsSync33, lstatSync as lstatSync9, readdirSync as readdirSync12, rmSync as rmSync11 } from "node:fs";
|
|
5095
5167
|
import { basename, join as join39 } from "node:path";
|
|
5096
5168
|
init_utils();
|
|
5097
5169
|
init_utils_json();
|
|
@@ -5132,7 +5204,7 @@ function* eachExtrasTarget(v, counts) {
|
|
|
5132
5204
|
}
|
|
5133
5205
|
function stripCollidingDstSymlinks(src, dst, isExcluded) {
|
|
5134
5206
|
if (!existsSync33(dst)) return;
|
|
5135
|
-
for (const name of
|
|
5207
|
+
for (const name of readdirSync12(src)) {
|
|
5136
5208
|
if (isExcluded(name)) continue;
|
|
5137
5209
|
const dstPath = join39(dst, name);
|
|
5138
5210
|
const dstStat = lstatSync9(dstPath, { throwIfNoEntry: false });
|
|
@@ -5180,7 +5252,7 @@ function copyExtrasFiltered(src, dst, blockSet) {
|
|
|
5180
5252
|
});
|
|
5181
5253
|
}
|
|
5182
5254
|
function prunePreservingDenied(src, dst, blockSet) {
|
|
5183
|
-
for (const name of
|
|
5255
|
+
for (const name of readdirSync12(dst)) {
|
|
5184
5256
|
if (isDeniedName(blockSet, name)) continue;
|
|
5185
5257
|
const dstPath = join39(dst, name);
|
|
5186
5258
|
const srcStat = lstatSync9(join39(src, name), { throwIfNoEntry: false });
|
|
@@ -5211,7 +5283,7 @@ function copyExtrasFilteredPreserving(src, dst, blockSet) {
|
|
|
5211
5283
|
});
|
|
5212
5284
|
}
|
|
5213
5285
|
function prunePreservingBy(src, dst, isPreserved) {
|
|
5214
|
-
for (const name of
|
|
5286
|
+
for (const name of readdirSync12(dst)) {
|
|
5215
5287
|
if (isPreserved(name)) continue;
|
|
5216
5288
|
const dstPath = join39(dst, name);
|
|
5217
5289
|
const srcStat = lstatSync9(join39(src, name), { throwIfNoEntry: false });
|
|
@@ -5248,7 +5320,7 @@ init_utils_json();
|
|
|
5248
5320
|
|
|
5249
5321
|
// src/extras-sync.remap.ts
|
|
5250
5322
|
init_config();
|
|
5251
|
-
import { existsSync as existsSync34, mkdirSync as mkdirSync7, readdirSync as
|
|
5323
|
+
import { existsSync as existsSync34, mkdirSync as mkdirSync7, readdirSync as readdirSync13, realpathSync as realpathSync4, rmSync as rmSync12 } from "node:fs";
|
|
5252
5324
|
import { dirname as dirname7, join as join41, sep as sep7 } from "node:path";
|
|
5253
5325
|
|
|
5254
5326
|
// src/extras-sync.planning-diff.ts
|
|
@@ -5349,7 +5421,7 @@ function pruneEmptyAncestors(target, planningRoot) {
|
|
|
5349
5421
|
let dir = dirname7(target);
|
|
5350
5422
|
while (dir !== planningRoot && dir.startsWith(planningRoot + sep7)) {
|
|
5351
5423
|
try {
|
|
5352
|
-
if (
|
|
5424
|
+
if (readdirSync13(dir).length > 0) break;
|
|
5353
5425
|
rmSync12(dir, { recursive: true, force: true });
|
|
5354
5426
|
} catch {
|
|
5355
5427
|
break;
|
|
@@ -5482,6 +5554,15 @@ function remapExtrasPull(ts, opts = {}) {
|
|
|
5482
5554
|
}
|
|
5483
5555
|
|
|
5484
5556
|
// src/extras-sync.ts
|
|
5557
|
+
function divergenceWarnLine(o) {
|
|
5558
|
+
const kind = o.isDir ? "folder" : "file";
|
|
5559
|
+
const name = o.isDir ? `${o.dirname}/` : o.dirname;
|
|
5560
|
+
const one = o.count === 1;
|
|
5561
|
+
const fileCount = one ? "1 file" : `${o.count} files`;
|
|
5562
|
+
const them = one ? "it" : "them";
|
|
5563
|
+
const yours = one ? "your current file is" : "your current files are";
|
|
5564
|
+
return `local ${kind} ${name} in repo ${o.logical} differs from the synced copy in ${fileCount}; the next pull step will overwrite ${them} with the synced version (${yours} backed up to ${o.projectBackupRoot}/)`;
|
|
5565
|
+
}
|
|
5485
5566
|
function divergenceCheckExtras(ts) {
|
|
5486
5567
|
const v = loadValidatedExtras({});
|
|
5487
5568
|
if (v === null) return;
|
|
@@ -5496,7 +5577,13 @@ function divergenceCheckExtras(ts) {
|
|
|
5496
5577
|
if (diff.length === 0) continue;
|
|
5497
5578
|
const projectBackupRoot = join42(backupRoot, encodePath(localRoot));
|
|
5498
5579
|
warn(
|
|
5499
|
-
|
|
5580
|
+
divergenceWarnLine({
|
|
5581
|
+
dirname: dirname8,
|
|
5582
|
+
logical,
|
|
5583
|
+
isDir: statSync9(local).isDirectory(),
|
|
5584
|
+
count: diff.length,
|
|
5585
|
+
projectBackupRoot
|
|
5586
|
+
})
|
|
5500
5587
|
);
|
|
5501
5588
|
for (const f of diff) warn(` ${f}`);
|
|
5502
5589
|
}
|
|
@@ -5504,7 +5591,7 @@ function divergenceCheckExtras(ts) {
|
|
|
5504
5591
|
|
|
5505
5592
|
// src/skills-sync.ts
|
|
5506
5593
|
init_config();
|
|
5507
|
-
import { existsSync as existsSync36, lstatSync as lstatSync10, mkdirSync as mkdirSync8, readdirSync as
|
|
5594
|
+
import { existsSync as existsSync36, lstatSync as lstatSync10, mkdirSync as mkdirSync8, readdirSync as readdirSync14, rmSync as rmSync13 } from "node:fs";
|
|
5508
5595
|
import { join as join43 } from "node:path";
|
|
5509
5596
|
init_utils_fs();
|
|
5510
5597
|
function isGsdOwned(name) {
|
|
@@ -5514,7 +5601,7 @@ function isSkillExcluded(name) {
|
|
|
5514
5601
|
return isGsdOwned(name) || isDeniedName(ALWAYS_NEVER_SYNC, name);
|
|
5515
5602
|
}
|
|
5516
5603
|
function copySkillsPush(src, dst) {
|
|
5517
|
-
const srcNames =
|
|
5604
|
+
const srcNames = readdirSync14(src, { encoding: "utf8" });
|
|
5518
5605
|
const blockSet = /* @__PURE__ */ new Set([
|
|
5519
5606
|
...srcNames.filter((n) => isGsdOwned(n)),
|
|
5520
5607
|
...ALWAYS_NEVER_SYNC
|
|
@@ -6316,7 +6403,7 @@ init_color();
|
|
|
6316
6403
|
init_config();
|
|
6317
6404
|
init_config_sharedDirs_guard();
|
|
6318
6405
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
6319
|
-
import { copyFileSync, existsSync as existsSync39, mkdirSync as mkdirSync10, readdirSync as
|
|
6406
|
+
import { copyFileSync, existsSync as existsSync39, mkdirSync as mkdirSync10, readdirSync as readdirSync15, rmSync as rmSync14 } from "node:fs";
|
|
6320
6407
|
import { homedir as homedir5 } from "node:os";
|
|
6321
6408
|
import { join as join46 } from "node:path";
|
|
6322
6409
|
init_push_leak_verdict();
|
|
@@ -6336,7 +6423,7 @@ function stageSessions(tmpRoot, map) {
|
|
|
6336
6423
|
const localProjects = join46(claudeHome(), "projects");
|
|
6337
6424
|
if (!existsSync39(localProjects)) return 0;
|
|
6338
6425
|
let staged = 0;
|
|
6339
|
-
for (const dir of
|
|
6426
|
+
for (const dir of readdirSync15(localProjects)) {
|
|
6340
6427
|
const logical = reverse.get(dir);
|
|
6341
6428
|
if (!logical) continue;
|
|
6342
6429
|
copyDirJsonlOnly(join46(localProjects, dir), join46(tmpRoot, "shared", "projects", logical));
|
|
@@ -6427,9 +6514,9 @@ function reportSettingsAheadDrift(repo) {
|
|
|
6427
6514
|
const { ahead } = classifySettingsDrift(merged, settings);
|
|
6428
6515
|
const { promotable } = partitionByCaptureExclusion(ahead);
|
|
6429
6516
|
if (promotable.length === 0) return;
|
|
6430
|
-
const
|
|
6517
|
+
const { phrase, pronoun, verb } = describeSettings(promotable);
|
|
6431
6518
|
warn(
|
|
6432
|
-
`settings.json has
|
|
6519
|
+
`your settings.json has ${phrase} that ${verb} not yet in the repo; run 'nomad capture-settings' to save ${pronoun} (or 'nomad capture-settings --host' for host-specific values).`
|
|
6433
6520
|
);
|
|
6434
6521
|
} catch {
|
|
6435
6522
|
}
|
|
@@ -6474,7 +6561,7 @@ function runDryRunPreview(st, map, repo) {
|
|
|
6474
6561
|
renderNoScanTree(st, { noMapHint: true });
|
|
6475
6562
|
return;
|
|
6476
6563
|
}
|
|
6477
|
-
const verdict = previewPushLeaks(map);
|
|
6564
|
+
const verdict = withSpinner("Scanning for secrets", () => previewPushLeaks(map));
|
|
6478
6565
|
renderPushTree(st, verdict);
|
|
6479
6566
|
if (verdict.recovery !== null) fail(verdict.recovery);
|
|
6480
6567
|
}
|
|
@@ -6697,7 +6784,7 @@ init_config();
|
|
|
6697
6784
|
init_utils();
|
|
6698
6785
|
init_utils_fs();
|
|
6699
6786
|
init_utils_json();
|
|
6700
|
-
import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync42, rmSync as rmSync15, statSync as
|
|
6787
|
+
import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync42, rmSync as rmSync15, statSync as statSync10 } from "node:fs";
|
|
6701
6788
|
import { join as join49 } from "node:path";
|
|
6702
6789
|
function snapshotIntoShared(map) {
|
|
6703
6790
|
const repo = repoHome();
|
|
@@ -6706,7 +6793,7 @@ function snapshotIntoShared(map) {
|
|
|
6706
6793
|
const src = join49(claude, name);
|
|
6707
6794
|
if (!existsSync42(src)) continue;
|
|
6708
6795
|
const dst = join49(repo, "shared", name);
|
|
6709
|
-
if (
|
|
6796
|
+
if (statSync10(src).isDirectory()) {
|
|
6710
6797
|
const gk = join49(dst, ".gitkeep");
|
|
6711
6798
|
if (existsSync42(gk)) rmSync15(gk);
|
|
6712
6799
|
cpSync7(src, dst, { recursive: true, force: false, errorOnExist: true });
|
|
@@ -6747,6 +6834,9 @@ function preflightConflict(repoHome2) {
|
|
|
6747
6834
|
}
|
|
6748
6835
|
return null;
|
|
6749
6836
|
}
|
|
6837
|
+
function isAlreadyInitialized(repoHome2) {
|
|
6838
|
+
return preflightConflict(repoHome2) !== null;
|
|
6839
|
+
}
|
|
6750
6840
|
function cmdInit(opts = {}) {
|
|
6751
6841
|
const snapshot = opts.snapshot === true;
|
|
6752
6842
|
const keepActions = opts.keepActions === true;
|
|
@@ -6839,6 +6929,47 @@ function maybeDisableRepoActions(repoHome2, run) {
|
|
|
6839
6929
|
}
|
|
6840
6930
|
}
|
|
6841
6931
|
|
|
6932
|
+
// src/init.prompt.ts
|
|
6933
|
+
init_config();
|
|
6934
|
+
init_utils();
|
|
6935
|
+
import { existsSync as existsSync44, readdirSync as readdirSync16, statSync as statSync11 } from "node:fs";
|
|
6936
|
+
import { join as join51 } from "node:path";
|
|
6937
|
+
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
6938
|
+
function nonEmptyExists(path) {
|
|
6939
|
+
if (!existsSync44(path)) return false;
|
|
6940
|
+
try {
|
|
6941
|
+
if (statSync11(path).isDirectory()) return readdirSync16(path).length > 0;
|
|
6942
|
+
return true;
|
|
6943
|
+
} catch {
|
|
6944
|
+
return false;
|
|
6945
|
+
}
|
|
6946
|
+
}
|
|
6947
|
+
function hasExistingClaudeConfig(claudeHome2) {
|
|
6948
|
+
if (existsSync44(join51(claudeHome2, "settings.json"))) return true;
|
|
6949
|
+
return SHARED_LINKS.some((name) => nonEmptyExists(join51(claudeHome2, name)));
|
|
6950
|
+
}
|
|
6951
|
+
async function confirmSnapshotDefault(claudeHome2) {
|
|
6952
|
+
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
6953
|
+
log(
|
|
6954
|
+
`tip: ${claudeHome2} already has config; re-run 'nomad init --snapshot' to seed the repo from it.`
|
|
6955
|
+
);
|
|
6956
|
+
return false;
|
|
6957
|
+
}
|
|
6958
|
+
log(`Found existing config in ${claudeHome2}.`);
|
|
6959
|
+
const rl = createInterface3({ input: process.stdin, output: process.stdout });
|
|
6960
|
+
try {
|
|
6961
|
+
const answer = await rl.question("Seed the new repo from it? [Y/n] ");
|
|
6962
|
+
return !/^n(o)?$/i.test(answer.trim());
|
|
6963
|
+
} finally {
|
|
6964
|
+
rl.close();
|
|
6965
|
+
}
|
|
6966
|
+
}
|
|
6967
|
+
async function resolveSnapshotChoice(flagSnapshot, claudeHome2, confirm = confirmSnapshotDefault) {
|
|
6968
|
+
if (flagSnapshot) return true;
|
|
6969
|
+
if (!hasExistingClaudeConfig(claudeHome2)) return false;
|
|
6970
|
+
return confirm(claudeHome2);
|
|
6971
|
+
}
|
|
6972
|
+
|
|
6842
6973
|
// src/nomad.dispatch.helpers.ts
|
|
6843
6974
|
var REJECT = { ok: false, advance: 0 };
|
|
6844
6975
|
function applyBool(seen, set) {
|
|
@@ -7098,7 +7229,7 @@ function parsePushArgs(argv) {
|
|
|
7098
7229
|
// package.json
|
|
7099
7230
|
var package_default = {
|
|
7100
7231
|
name: "claude-nomad",
|
|
7101
|
-
version: "0.
|
|
7232
|
+
version: "0.55.1",
|
|
7102
7233
|
type: "module",
|
|
7103
7234
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
7104
7235
|
keywords: [
|
|
@@ -7230,7 +7361,8 @@ var DEFAULT_HELP = [
|
|
|
7230
7361
|
" init",
|
|
7231
7362
|
"Create a private GitHub repo via gh (if none exists), scaffold shared/, hosts/, path-map."
|
|
7232
7363
|
),
|
|
7233
|
-
row(" --snapshot", "
|
|
7364
|
+
row(" --snapshot", "Seed shared/ from the current ~/.claude/. Without this flag, init"),
|
|
7365
|
+
cont("offers the same seed interactively when it detects existing config."),
|
|
7234
7366
|
row(" --keep-actions", "Skip auto-disabling GitHub Actions on the private repo."),
|
|
7235
7367
|
row(
|
|
7236
7368
|
" --repo <name>",
|
|
@@ -7308,7 +7440,8 @@ var DEFAULT_HELP = [
|
|
|
7308
7440
|
"",
|
|
7309
7441
|
row(" --version", "Print the installed CLI version as bare semver to stdout; exits 0."),
|
|
7310
7442
|
"",
|
|
7311
|
-
"
|
|
7443
|
+
"You edit the sync repo (~/claude-nomad/); nomad regenerates ~/.claude/ on every",
|
|
7444
|
+
"pull. Run `nomad doctor` to validate your setup. Edit shared/ or hosts/<HOST>.json",
|
|
7312
7445
|
"in the repo, never ~/.claude/settings.json directly (it is regenerated on",
|
|
7313
7446
|
"every pull)."
|
|
7314
7447
|
].join("\n");
|
|
@@ -7317,15 +7450,15 @@ var DEFAULT_HELP = [
|
|
|
7317
7450
|
init_config();
|
|
7318
7451
|
init_utils();
|
|
7319
7452
|
init_utils_json();
|
|
7320
|
-
import { existsSync as
|
|
7321
|
-
import { join as
|
|
7453
|
+
import { existsSync as existsSync45, readFileSync as readFileSync14, readdirSync as readdirSync17 } from "node:fs";
|
|
7454
|
+
import { join as join52 } from "node:path";
|
|
7322
7455
|
function resumeCmd(sessionId) {
|
|
7323
7456
|
if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
|
|
7324
7457
|
fail(`invalid session id: ${sessionId}`);
|
|
7325
7458
|
process.exit(1);
|
|
7326
7459
|
}
|
|
7327
|
-
const projectsRoot =
|
|
7328
|
-
if (!
|
|
7460
|
+
const projectsRoot = join52(claudeHome(), "projects");
|
|
7461
|
+
if (!existsSync45(projectsRoot)) {
|
|
7329
7462
|
fail(`${projectsRoot} does not exist`);
|
|
7330
7463
|
process.exit(1);
|
|
7331
7464
|
}
|
|
@@ -7339,8 +7472,8 @@ function resumeCmd(sessionId) {
|
|
|
7339
7472
|
fail(`no cwd field found in ${jsonlPath}`);
|
|
7340
7473
|
process.exit(1);
|
|
7341
7474
|
}
|
|
7342
|
-
const mapPath =
|
|
7343
|
-
if (!
|
|
7475
|
+
const mapPath = join52(repoHome(), "path-map.json");
|
|
7476
|
+
if (!existsSync45(mapPath)) {
|
|
7344
7477
|
fail("path-map.json missing");
|
|
7345
7478
|
process.exit(1);
|
|
7346
7479
|
}
|
|
@@ -7362,9 +7495,9 @@ function resumeCmd(sessionId) {
|
|
|
7362
7495
|
console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
|
|
7363
7496
|
}
|
|
7364
7497
|
function findTranscriptPath(projectsRoot, sessionId) {
|
|
7365
|
-
for (const dir of
|
|
7366
|
-
const candidate =
|
|
7367
|
-
if (
|
|
7498
|
+
for (const dir of readdirSync17(projectsRoot)) {
|
|
7499
|
+
const candidate = join52(projectsRoot, dir, `${sessionId}.jsonl`);
|
|
7500
|
+
if (existsSync45(candidate)) return candidate;
|
|
7368
7501
|
}
|
|
7369
7502
|
return null;
|
|
7370
7503
|
}
|
|
@@ -7447,8 +7580,9 @@ try {
|
|
|
7447
7580
|
console.error("usage: nomad init [--snapshot] [--keep-actions] [--repo <name>]");
|
|
7448
7581
|
process.exit(1);
|
|
7449
7582
|
}
|
|
7583
|
+
const snapshot = isAlreadyInitialized(repoHome()) ? initArgs.snapshot : await resolveSnapshotChoice(initArgs.snapshot, claudeHome());
|
|
7450
7584
|
cmdInit({
|
|
7451
|
-
snapshot
|
|
7585
|
+
snapshot,
|
|
7452
7586
|
keepActions: initArgs.keepActions,
|
|
7453
7587
|
repoName: initArgs.repoName
|
|
7454
7588
|
});
|
package/package.json
CHANGED