claude-nomad 0.35.0 → 0.36.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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.36.0](https://github.com/funkadelic/claude-nomad/compare/v0.35.0...v0.36.0) (2026-06-01)
4
+
5
+
6
+ ### Added
7
+
8
+ * **doctor:** require an HTTP fetcher (curl or wget) for the version check ([#210](https://github.com/funkadelic/claude-nomad/issues/210)) ([96b5a53](https://github.com/funkadelic/claude-nomad/commit/96b5a532e688cca9acebb1c6780d4106b2f21f5b))
9
+
10
+
11
+ ### Changed
12
+
13
+ * **backup:** route backup-path writers through BACKUP_BASE ([#211](https://github.com/funkadelic/claude-nomad/issues/211)) ([7033c29](https://github.com/funkadelic/claude-nomad/commit/7033c29c9af76db029f44d0f1717ea1831ba122e))
14
+ * **tests:** label the test matrix jobs with the Node version ([#208](https://github.com/funkadelic/claude-nomad/issues/208)) ([3ada7e1](https://github.com/funkadelic/claude-nomad/commit/3ada7e12f38fc43f517842435b46c967c9312b25))
15
+
16
+
17
+ ### Documentation
18
+
19
+ * **hero:** add hooks/, align glyph + terminal, tighten spacing ([#213](https://github.com/funkadelic/claude-nomad/issues/213)) ([d0bf93f](https://github.com/funkadelic/claude-nomad/commit/d0bf93f93fb21f65b8c5893b70bacbbf05d8ebe6))
20
+ * refresh contributor and user docs for recent changes ([#212](https://github.com/funkadelic/claude-nomad/issues/212)) ([d909097](https://github.com/funkadelic/claude-nomad/commit/d9090977fdf445316128ca9a0fefdec966105b58))
21
+
3
22
  ## [0.35.0](https://github.com/funkadelic/claude-nomad/compare/v0.34.1...v0.35.0) (2026-05-31)
4
23
 
5
24
 
package/README.md CHANGED
@@ -396,10 +396,12 @@ Read these before adopting so you opt in with eyes open.
396
396
 
397
397
  **Optional:**
398
398
 
399
- - [curl](https://curl.se/), used by the version-staleness check (`nomad doctor` latest-release line)
400
- and by `nomad doctor --check-schema`; it degrades silently when curl is absent or offline, so the
401
- rest of the CLI works without it. `nomad doctor` reports its presence in the Version Checks
402
- section.
399
+ - [curl](https://curl.se/) or [wget](https://www.gnu.org/software/wget/), the HTTP fetcher behind
400
+ the version-staleness check (`nomad doctor` latest-release line) and
401
+ `nomad doctor --check-schema`. curl is tried first and wget is the fallback, so either one works.
402
+ The checks soft-skip (no error, no exit-code change) when neither is present, so the rest of the
403
+ CLI works without it; `nomad doctor` shows a single "HTTP fetcher (curl or wget)" row that is OK
404
+ when either is installed and warns only when both are absent.
403
405
 
404
406
  ## Setup
405
407
 
@@ -545,31 +547,31 @@ to this host.
545
547
 
546
548
  ## Commands
547
549
 
548
- | Command | Description |
549
- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
550
- | `nomad init` | Create a private GitHub repo via `gh`, wire it as `origin`, disable Actions, scaffold `shared/`, `hosts/`, `path-map.json`, and push. Prompts for a repo name (default: `claude-nomad-config`). `gh` must be installed and authenticated; exits with FATAL otherwise. Refuses to clobber existing scaffold. See [Privacy by default](#privacy-by-default). |
551
- | `nomad init --repo <name>` | Non-interactive: use `<name>` as the private repo name without prompting. Useful in scripts. |
552
- | `nomad init --snapshot` | Overlay current host's `~/.claude/` into `shared/` and write `~/.claude/settings.json` verbatim into `hosts/<NOMAD_HOST>.json`. Originals not modified. Same auto-disable behavior as `nomad init`. |
553
- | `nomad init --keep-actions` | Skip the Actions-disable step. Combinable with `--snapshot` and `--repo`. Use when an org policy already governs Actions, or you intentionally want CI on the private repo. |
554
- | `nomad pull` | `git pull --rebase --autostash`, apply symlinks, regenerate `settings.json`, remap session paths, and pull opted-in per-project extras. Errors out if scaffold missing. |
555
- | `nomad pull --dry-run` | Network-aware preview: acquire lock + `git pull --rebase`, print planned changes (symlink moves, `settings.json` diff, transcript overwrites), exit without writing. |
556
- | `nomad diff` | Offline, lockless twin of `pull --dry-run`. No network, no lock. Works against the current local repo state. |
557
- | `nomad push` | Export local sessions and opted-in per-project extras to logical names, commit (`chore: sync from <NOMAD_HOST>`), push. |
558
- | `nomad push --dry-run` | Run pre-push safety checks (gitleaks probe, rebase, remap preview, gitlink scan, allow-list) and a read-only gitleaks leak preview over a throwaway temp copy of the sessions and extras this host would stage; skip stage, commit, and push. Exits 1 if a leak is found in the preview. Nothing is written to the sync repo. |
559
- | `nomad push --redact-all` | Redact all findings non-interactively (backup written first) without a TTY. Does not auto-Allow findings. After redaction re-stages and re-scans; aborts with the session-aware FATAL if any finding survives. Use this in scripts or when you are confident every finding is a real secret that should be scrubbed. See [Recovery flow: push-time interactive menu](#recovery-flow-push-time-interactive-menu). |
560
- | `nomad drop-session <id>` | Surgically unstage every `shared/projects/*/<id>.jsonl` and the sibling `shared/projects/*/<id>/` subagent directory from the staged tree of `~/claude-nomad/`. Idempotent; the local `~/.claude/projects/<encoded>/<id>.jsonl` and `<id>/` tree are preserved. See [Recovery flows](#recovery-flows). |
561
- | `nomad adopt <name>` | Back up, then move a pre-existing `~/.claude/<name>` directory into `shared/<name>`, recreate the symlink so this host keeps working, and stage the result for push. `<name>` must already be listed in `SHARED_LINKS` or in the `sharedDirs` field of `path-map.json`; adopt is a mover, not a config editor, so it never writes `path-map.json` itself. |
562
- | `nomad adopt <name> --dry-run` | Preview the planned backup, move, and `git add` without touching the filesystem or the git index. |
563
- | `nomad redact <session-id>` | Rewrite the secret span in the local source transcript for a session, backed up to `~/.cache/claude-nomad/backup/`. Refuses to touch a session that was modified recently (potential active session). Safe to re-run. See [`nomad redact <session-id>`](#nomad-redact-session-id). |
564
- | `nomad redact --rule <id>` | Limit redaction to findings of one gitleaks rule id only. |
565
- | `nomad redact --dry-run` | Show what `nomad redact` would change without writing anything. |
566
- | `nomad clean --backups` | Delete old backup snapshots under `~/.cache/claude-nomad/backup/`. By default removes snapshots older than 14 days; pass `--older-than <dur>` (e.g. `7d`, `24h`) to change the age, or `--keep <N>` to keep the N newest and delete the rest (the two flags cannot be combined). Always preview with `--dry-run` first. See [Pruning old backups](#pruning-old-backups). |
567
- | `nomad update` | Update the `nomad` CLI binary from npm (`npm update -g claude-nomad`). Does NOT pull your sync data; run `nomad pull` separately for that. See [Upgrading the CLI](#upgrading-the-cli). |
568
- | `nomad doctor` | Read-only health check. Each line carries a status glyph (`✓` pass, `✗` fail, `⚠︎` warn); any `✗` sets `process.exitCode = 1` (`⚠︎` does not). Includes an offline-tolerant release-version staleness check, a Hook targets check that fails (`✗`, exit 1) when `settings.json` references a hook command whose script under `~/.claude/` is missing on this host, plus two `⚠︎`-only drift checks: gitleaks version drift and, on a private GitHub mirror, re-enabled Actions. |
569
- | `nomad doctor --resume-cmd <id>` | Print a host-local `cd ... && claude --resume <id>` line for a session (see [Cross-OS resume](#cross-os-resume)). |
570
- | `nomad doctor --check-shared` | Read-only gitleaks preflight: stages the session transcripts a `push` would publish into a temp tree and scans them, failing (`✗`, exit 1) per affected session with rotate-and-scrub guidance. Skips with a `⚠︎` when gitleaks is not on PATH. See [Recovery flow: gitleaks FATAL on a session JSONL](#recovery-flow-gitleaks-fatal-on-a-session-jsonl). |
571
- | `nomad doctor --check-schema` | Read-only: fetches the live Claude Code settings schema and lists any `~/.claude/settings.json` key absent from it (candidates for the hand-maintained `APP_ONLY_KEYS` list). Non-fatal and offline-tolerant: skips with a `⚠︎` when curl is missing or the schema is unreachable. |
572
- | `nomad --version` | Print the installed CLI version as bare semver to stdout; exits 0. Used by the npm-publish smoke test and useful for ad-hoc upgrade checks. |
550
+ | Command | Description |
551
+ | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
552
+ | `nomad init` | Create a private GitHub repo via `gh`, wire it as `origin`, disable Actions, scaffold `shared/`, `hosts/`, `path-map.json`, and push. Prompts for a repo name (default: `claude-nomad-config`). `gh` must be installed and authenticated; exits with FATAL otherwise. Refuses to clobber existing scaffold. See [Privacy by default](#privacy-by-default). |
553
+ | `nomad init --repo <name>` | Non-interactive: use `<name>` as the private repo name without prompting. Useful in scripts. |
554
+ | `nomad init --snapshot` | Overlay current host's `~/.claude/` into `shared/` and write `~/.claude/settings.json` verbatim into `hosts/<NOMAD_HOST>.json`. Originals not modified. Same auto-disable behavior as `nomad init`. |
555
+ | `nomad init --keep-actions` | Skip the Actions-disable step. Combinable with `--snapshot` and `--repo`. Use when an org policy already governs Actions, or you intentionally want CI on the private repo. |
556
+ | `nomad pull` | `git pull --rebase --autostash`, apply symlinks, regenerate `settings.json`, remap session paths, and pull opted-in per-project extras. Errors out if scaffold missing. |
557
+ | `nomad pull --dry-run` | Network-aware preview: acquire lock + `git pull --rebase`, print planned changes (symlink moves, `settings.json` diff, transcript overwrites), exit without writing. |
558
+ | `nomad diff` | Offline, lockless twin of `pull --dry-run`. No network, no lock. Works against the current local repo state. |
559
+ | `nomad push` | Export local sessions and opted-in per-project extras to logical names, commit (`chore: sync from <NOMAD_HOST>`), push. |
560
+ | `nomad push --dry-run` | Run pre-push safety checks (gitleaks probe, rebase, remap preview, gitlink scan, allow-list) and a read-only gitleaks leak preview over a throwaway temp copy of the sessions and extras this host would stage; skip stage, commit, and push. Exits 1 if a leak is found in the preview. Nothing is written to the sync repo. |
561
+ | `nomad push --redact-all` | Redact all findings non-interactively (backup written first) without a TTY. Does not auto-Allow findings. After redaction re-stages and re-scans; aborts with the session-aware FATAL if any finding survives. Use this in scripts or when you are confident every finding is a real secret that should be scrubbed. See [Recovery flow: push-time interactive menu](#recovery-flow-push-time-interactive-menu). |
562
+ | `nomad drop-session <id>` | Surgically unstage every `shared/projects/*/<id>.jsonl` and the sibling `shared/projects/*/<id>/` subagent directory from the staged tree of `~/claude-nomad/`. Idempotent; the local `~/.claude/projects/<encoded>/<id>.jsonl` and `<id>/` tree are preserved. See [Recovery flows](#recovery-flows). |
563
+ | `nomad adopt <name>` | Back up, then move a pre-existing `~/.claude/<name>` directory into `shared/<name>`, recreate the symlink so this host keeps working, and stage the result for push. `<name>` must already be listed in `SHARED_LINKS` or in the `sharedDirs` field of `path-map.json`; adopt is a mover, not a config editor, so it never writes `path-map.json` itself. |
564
+ | `nomad adopt <name> --dry-run` | Preview the planned backup, move, and `git add` without touching the filesystem or the git index. |
565
+ | `nomad redact <session-id>` | Rewrite the secret span in the local source transcript for a session, backed up to `~/.cache/claude-nomad/backup/`. Refuses to touch a session that was modified recently (potential active session). Safe to re-run. See [`nomad redact <session-id>`](#nomad-redact-session-id). |
566
+ | `nomad redact --rule <id>` | Limit redaction to findings of one gitleaks rule id only. |
567
+ | `nomad redact --dry-run` | Show what `nomad redact` would change without writing anything. |
568
+ | `nomad clean --backups` | Delete old backup snapshots under `~/.cache/claude-nomad/backup/`. By default removes snapshots older than 14 days; pass `--older-than <dur>` (e.g. `7d`, `24h`) to change the age, or `--keep <N>` to keep the N newest and delete the rest (the two flags cannot be combined). Always preview with `--dry-run` first. See [Pruning old backups](#pruning-old-backups). |
569
+ | `nomad update` | Update the `nomad` CLI binary from npm (`npm update -g claude-nomad`). Does NOT pull your sync data; run `nomad pull` separately for that. See [Upgrading the CLI](#upgrading-the-cli). |
570
+ | `nomad doctor` | Read-only health check. Each line carries a status glyph (`✓` pass, `✗` fail, `⚠︎` warn); any `✗` sets `process.exitCode = 1` (`⚠︎` does not). Includes an offline-tolerant release-version staleness check, a Hook targets check that fails (`✗`, exit 1) when `settings.json` references a hook command whose script under `~/.claude/` is missing on this host, plus a set of `⚠︎`-only checks: gitleaks version drift; on a private GitHub mirror, re-enabled Actions; optional-dependency presence (`gh` and the curl-or-wget HTTP fetcher); a backups-cache size/count nudge toward `nomad clean --backups`; an ESM/CommonJS hook-scope mismatch; and a Node-engine floor check. |
571
+ | `nomad doctor --resume-cmd <id>` | Print a host-local `cd ... && claude --resume <id>` line for a session (see [Cross-OS resume](#cross-os-resume)). |
572
+ | `nomad doctor --check-shared` | Read-only gitleaks preflight: stages the session transcripts a `push` would publish into a temp tree and scans them, failing (`✗`, exit 1) per affected session with rotate-and-scrub guidance. Skips with a `⚠︎` when gitleaks is not on PATH. See [Recovery flow: gitleaks FATAL on a session JSONL](#recovery-flow-gitleaks-fatal-on-a-session-jsonl). |
573
+ | `nomad doctor --check-schema` | Read-only: fetches the live Claude Code settings schema and lists any `~/.claude/settings.json` key absent from it (candidates for the hand-maintained `APP_ONLY_KEYS` list). Non-fatal and offline-tolerant: skips with a `⚠︎` when neither curl nor wget is available or the schema is unreachable. |
574
+ | `nomad --version` | Print the installed CLI version as bare semver to stdout; exits 0. Used by the npm-publish smoke test and useful for ad-hoc upgrade checks. |
573
575
 
574
576
  The version-check emits ``⚠︎ claude-nomad: <local> -> <latest> (run `nomad update`)`` when the local
575
577
  install is behind the latest upstream release, and `✓ claude-nomad: <local> (latest)` when current.
package/dist/nomad.mjs CHANGED
@@ -341,7 +341,7 @@ function allSharedLinks(map) {
341
341
  }
342
342
  return [...SHARED_LINKS, ...extras];
343
343
  }
344
- var HOME, CLAUDE_HOME, BACKUP_BASE, REPO_HOME, SETTINGS_SCHEMA_URL, GITLEAKS_PINNED_VERSION, HOST, SHARED_LINKS, SUPPORTED_EXTRAS, ALWAYS_NEVER_SYNC, NEVER_SYNC, PUSH_ALLOWED_STATIC;
344
+ var HOME, CLAUDE_HOME, BACKUP_BASE, REPO_HOME, SETTINGS_SCHEMA_URL, NPM_REGISTRY_LATEST_URL, GITLEAKS_PINNED_VERSION, HOST, SHARED_LINKS, SUPPORTED_EXTRAS, ALWAYS_NEVER_SYNC, NEVER_SYNC, PUSH_ALLOWED_STATIC;
345
345
  var init_config = __esm({
346
346
  "src/config.ts"() {
347
347
  "use strict";
@@ -353,6 +353,7 @@ var init_config = __esm({
353
353
  BACKUP_BASE = join(HOME, ".cache", "claude-nomad", "backup");
354
354
  REPO_HOME = process.env.NOMAD_REPO || resolve(HOME, "claude-nomad");
355
355
  SETTINGS_SCHEMA_URL = "https://json.schemastore.org/claude-code-settings.json";
356
+ NPM_REGISTRY_LATEST_URL = "https://registry.npmjs.org/claude-nomad/latest";
356
357
  GITLEAKS_PINNED_VERSION = "8.30.1";
357
358
  HOST = (process.env.NOMAD_HOST || hostname()).toLowerCase();
358
359
  SHARED_LINKS = [
@@ -508,7 +509,7 @@ function backupBeforeWrite(absPath, ts) {
508
509
  if (!existsSync(absPath)) return;
509
510
  const rel = relative(CLAUDE_HOME, absPath);
510
511
  if (rel.startsWith("..") || rel === "") return;
511
- const backupRoot = join2(HOME, ".cache", "claude-nomad", "backup", ts);
512
+ const backupRoot = join2(BACKUP_BASE, ts);
512
513
  const dst = join2(backupRoot, rel);
513
514
  mkdirSync(dirname(dst), { recursive: true });
514
515
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
@@ -517,7 +518,7 @@ function backupRepoWrite(absPath, ts, repoHome) {
517
518
  if (!existsSync(absPath)) return;
518
519
  const rel = relative(repoHome, absPath);
519
520
  if (rel.startsWith("..") || rel === "") return;
520
- const backupRoot = join2(HOME, ".cache", "claude-nomad", "backup", ts, "repo");
521
+ const backupRoot = join2(BACKUP_BASE, ts, "repo");
521
522
  const dst = join2(backupRoot, rel);
522
523
  mkdirSync(dirname(dst), { recursive: true });
523
524
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
@@ -526,7 +527,7 @@ function backupExtrasWrite(absPath, ts, projectRoot) {
526
527
  if (!existsSync(absPath)) return;
527
528
  const rel = relative(projectRoot, absPath);
528
529
  if (rel.startsWith("..") || rel === "") return;
529
- const backupRoot = join2(HOME, ".cache", "claude-nomad", "backup", ts, "extras");
530
+ const backupRoot = join2(BACKUP_BASE, ts, "extras");
530
531
  const dst = join2(backupRoot, encodePath(projectRoot), rel);
531
532
  mkdirSync(dirname(dst), { recursive: true });
532
533
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
@@ -972,8 +973,7 @@ function isValidAdoptName(name) {
972
973
  return isValidSharedDir(name);
973
974
  }
974
975
  function performAdoptMove(name, linkPath, sharedTarget) {
975
- const backupBase = join3(HOME, ".cache", "claude-nomad", "backup");
976
- const ts = freshBackupTs(backupBase);
976
+ const ts = freshBackupTs(BACKUP_BASE);
977
977
  backupBeforeWrite(linkPath, ts);
978
978
  cpSync2(linkPath, sharedTarget, { recursive: true, force: true, preserveTimestamps: true });
979
979
  rmSync(linkPath, { recursive: true, force: true });
@@ -1010,8 +1010,7 @@ function cmdAdopt(name, opts = {}) {
1010
1010
  process.exit(1);
1011
1011
  }
1012
1012
  if (dryRun) {
1013
- const backupBase = join3(HOME, ".cache", "claude-nomad", "backup");
1014
- const ts = freshBackupTs(backupBase);
1013
+ const ts = freshBackupTs(BACKUP_BASE);
1015
1014
  log(`would backup: ${linkPath} -> backup/${ts}/${name}`);
1016
1015
  log(`would move: ${linkPath} -> shared/${name}`);
1017
1016
  log(`would stage: shared/${name}`);
@@ -1118,7 +1117,7 @@ function sectionFailed(s) {
1118
1117
  function renderSection(s) {
1119
1118
  const header = sectionFailed(s) ? `${red(FAIL_GLYPH_BARE)} ${s.header}` : s.header;
1120
1119
  console.log(header);
1121
- const lastContent = s.items.reduce((acc, item, j) => item !== "" ? j : acc, -1);
1120
+ const lastContent = s.items.reduce((acc, item, j) => item === "" ? acc : j, -1);
1122
1121
  for (let j = 0; j < s.items.length; j++) {
1123
1122
  if (s.items[j] === "") console.log("");
1124
1123
  else console.log(`${j === lastContent ? " \u2514 " : " \u251C "}${s.items[j]}`);
@@ -1532,15 +1531,36 @@ function reportBackupsCheck(section2, backupBase = BACKUP_BASE) {
1532
1531
 
1533
1532
  // src/commands.doctor.check-schema.ts
1534
1533
  init_color();
1535
- import { execFileSync as execFileSync5 } from "node:child_process";
1536
1534
  import { existsSync as existsSync12 } from "node:fs";
1537
1535
  import { join as join14 } from "node:path";
1538
1536
  init_config();
1539
- function fetchSchemaKeys() {
1537
+
1538
+ // src/http-fetch.ts
1539
+ import { execFileSync as execFileSync5 } from "node:child_process";
1540
+ var FETCH_TIMEOUT_MS = 3e3;
1541
+ function fetchUrl(url, run = execFileSync5) {
1540
1542
  try {
1541
- const raw = execFileSync5("curl", ["-fsSL", "-m", "3", SETTINGS_SCHEMA_URL], {
1542
- stdio: ["ignore", "pipe", "pipe"]
1543
+ return run("curl", ["-fsSL", "-m", "3", url], {
1544
+ stdio: ["ignore", "pipe", "pipe"],
1545
+ timeout: FETCH_TIMEOUT_MS
1543
1546
  }).toString();
1547
+ } catch {
1548
+ try {
1549
+ return run("wget", ["-qO-", "--timeout=3", "--tries=1", url], {
1550
+ stdio: ["ignore", "pipe", "pipe"],
1551
+ timeout: FETCH_TIMEOUT_MS
1552
+ }).toString();
1553
+ } catch {
1554
+ return null;
1555
+ }
1556
+ }
1557
+ }
1558
+
1559
+ // src/commands.doctor.check-schema.ts
1560
+ function fetchSchemaKeys() {
1561
+ try {
1562
+ const raw = fetchUrl(SETTINGS_SCHEMA_URL);
1563
+ if (raw === null) return null;
1544
1564
  const parsed = JSON.parse(raw);
1545
1565
  if (typeof parsed.properties !== "object" || parsed.properties === null) return null;
1546
1566
  return Object.keys(parsed.properties);
@@ -1560,7 +1580,7 @@ function reportCheckSchema(section2) {
1560
1580
  if (liveKeys === null) {
1561
1581
  addItem(
1562
1582
  section2,
1563
- `${yellow(warnGlyph)} schema check skipped (offline, curl missing, or schema unreachable)`
1583
+ `${yellow(warnGlyph)} schema check skipped (offline, curl or wget missing, or schema unreachable)`
1564
1584
  );
1565
1585
  return;
1566
1586
  }
@@ -2058,9 +2078,9 @@ import { fileURLToPath as fileURLToPath3 } from "node:url";
2058
2078
 
2059
2079
  // src/commands.doctor.version.ts
2060
2080
  init_color();
2061
- import { execFileSync as execFileSync7 } from "node:child_process";
2062
2081
  import { readFileSync as readFileSync5 } from "node:fs";
2063
2082
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2083
+ init_config();
2064
2084
  var STRICT_SEMVER = /^\d+\.\d+\.\d+$/;
2065
2085
  var STRICT_SEMVER_PREFIX = /^(\d+\.\d+\.\d+)(?:[-+]|$)/;
2066
2086
  function compareSemver(a, b) {
@@ -2086,10 +2106,8 @@ function readLocalVersion() {
2086
2106
  }
2087
2107
  function fetchLatestVersion() {
2088
2108
  try {
2089
- const url = "https://registry.npmjs.org/claude-nomad/latest";
2090
- const raw = execFileSync7("curl", ["-fsSL", "-m", "3", url], {
2091
- stdio: ["ignore", "pipe", "pipe"]
2092
- }).toString();
2109
+ const raw = fetchUrl(NPM_REGISTRY_LATEST_URL);
2110
+ if (raw === null) return null;
2093
2111
  const parsed = JSON.parse(raw);
2094
2112
  if (typeof parsed.version !== "string") return null;
2095
2113
  if (!STRICT_SEMVER.test(parsed.version)) return null;
@@ -2158,7 +2176,7 @@ function reportNodeEngineCheck(section2) {
2158
2176
 
2159
2177
  // src/commands.doctor.gitleaks-version.ts
2160
2178
  init_color();
2161
- import { execFileSync as execFileSync8 } from "node:child_process";
2179
+ import { execFileSync as execFileSync7 } from "node:child_process";
2162
2180
  import { existsSync as existsSync17 } from "node:fs";
2163
2181
  import { join as join20 } from "node:path";
2164
2182
  init_config();
@@ -2181,7 +2199,7 @@ function readGitleaksVersion(run, tomlExists) {
2181
2199
  return null;
2182
2200
  }
2183
2201
  }
2184
- function reportGitleaksVersionCheck(section2, run = execFileSync8, tomlExists = existsSync17) {
2202
+ function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync17) {
2185
2203
  const raw = readGitleaksVersion(run, tomlExists);
2186
2204
  if (raw === null) return;
2187
2205
  const local = majorMinorOf(raw);
@@ -2201,15 +2219,20 @@ function reportGitleaksVersionCheck(section2, run = execFileSync8, tomlExists =
2201
2219
 
2202
2220
  // src/commands.doctor.checks.deps.ts
2203
2221
  init_color();
2204
- import { execFileSync as execFileSync9 } from "node:child_process";
2222
+ import { execFileSync as execFileSync8 } from "node:child_process";
2205
2223
  var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
2224
+ var PROBE_TIMEOUT_MS = 3e3;
2225
+ var FETCHER_LABEL = "HTTP fetcher (curl or wget)";
2206
2226
  function parseFirstVersion(line) {
2207
2227
  const m = VERSION_TOKEN.exec(line);
2208
2228
  return m ? m[1] : null;
2209
2229
  }
2210
2230
  function probeOptionalDep(bin, run) {
2211
2231
  try {
2212
- const firstLine = run(bin, ["--version"], { stdio: ["ignore", "pipe", "pipe"] }).toString().split("\n")[0].trim();
2232
+ const firstLine = run(bin, ["--version"], {
2233
+ stdio: ["ignore", "pipe", "pipe"],
2234
+ timeout: PROBE_TIMEOUT_MS
2235
+ }).toString().split("\n")[0].trim();
2213
2236
  const version = parseFirstVersion(firstLine);
2214
2237
  return { status: "present", version };
2215
2238
  } catch (err) {
@@ -2219,34 +2242,40 @@ function probeOptionalDep(bin, run) {
2219
2242
  return { status: "present", version: null };
2220
2243
  }
2221
2244
  }
2222
- function reportOptionalDeps(section2, run = execFileSync9) {
2223
- const gh = probeOptionalDep("gh", run);
2224
- if (gh.status === "present") {
2225
- addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
2245
+ function reportFetcherRow(section2, run) {
2246
+ const curl = probeOptionalDep("curl", run);
2247
+ const wget = probeOptionalDep("wget", run);
2248
+ if (curl.status === "present") {
2249
+ addItem(section2, `${green(okGlyph)} ${FETCHER_LABEL}: ${curl.version ?? "present"}`);
2250
+ } else if (wget.status === "present") {
2251
+ addItem(section2, `${green(okGlyph)} ${FETCHER_LABEL}: ${wget.version ?? "present"}`);
2226
2252
  } else {
2227
2253
  addItem(
2228
2254
  section2,
2229
- `${yellow(warnGlyph)} gh: not installed (optional; needed for nomad init Actions auto-disable + mirror-Actions drift check)`
2255
+ `${yellow(warnGlyph)} ${FETCHER_LABEL}: not installed (optional; needed for release-version staleness check + nomad doctor --check-schema)`
2230
2256
  );
2231
2257
  }
2232
- const curl = probeOptionalDep("curl", run);
2233
- if (curl.status === "present") {
2234
- addItem(section2, `${green(okGlyph)} curl: ${curl.version ?? "present"}`);
2258
+ }
2259
+ function reportOptionalDeps(section2, run = execFileSync8) {
2260
+ const gh = probeOptionalDep("gh", run);
2261
+ if (gh.status === "present") {
2262
+ addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
2235
2263
  } else {
2236
2264
  addItem(
2237
2265
  section2,
2238
- `${yellow(warnGlyph)} curl: not installed (optional; needed for release-version staleness check + nomad doctor --check-schema)`
2266
+ `${yellow(warnGlyph)} gh: not installed (optional; needed for nomad init Actions auto-disable + mirror-Actions drift check)`
2239
2267
  );
2240
2268
  }
2269
+ reportFetcherRow(section2, run);
2241
2270
  }
2242
2271
 
2243
2272
  // src/commands.doctor.mirror-actions.ts
2244
2273
  init_color();
2245
- import { execFileSync as execFileSync11 } from "node:child_process";
2274
+ import { execFileSync as execFileSync10 } from "node:child_process";
2246
2275
  init_config();
2247
2276
 
2248
2277
  // src/gh-actions.ts
2249
- import { execFileSync as execFileSync10 } from "node:child_process";
2278
+ import { execFileSync as execFileSync9 } from "node:child_process";
2250
2279
  var GH_TIMEOUT_MS = 5e3;
2251
2280
  function parseGitHubRemote(remoteUrl) {
2252
2281
  const normalized = remoteUrl.trim().replace(/\/$/, "");
@@ -2254,7 +2283,7 @@ function parseGitHubRemote(remoteUrl) {
2254
2283
  if (m === null) return null;
2255
2284
  return { owner: m[1], repo: m[2] };
2256
2285
  }
2257
- function ghAuthStatus(run = execFileSync10) {
2286
+ function ghAuthStatus(run = execFileSync9) {
2258
2287
  try {
2259
2288
  run("gh", ["auth", "status"], {
2260
2289
  stdio: ["ignore", "ignore", "ignore"],
@@ -2268,7 +2297,7 @@ function ghAuthStatus(run = execFileSync10) {
2268
2297
  return "gh-probe-error";
2269
2298
  }
2270
2299
  }
2271
- function isRepoPrivate(ref, run = execFileSync10) {
2300
+ function isRepoPrivate(ref, run = execFileSync9) {
2272
2301
  const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
2273
2302
  stdio: ["ignore", "pipe", "ignore"],
2274
2303
  timeout: GH_TIMEOUT_MS
@@ -2276,7 +2305,7 @@ function isRepoPrivate(ref, run = execFileSync10) {
2276
2305
  const parsed = JSON.parse(out);
2277
2306
  return parsed.isPrivate === true;
2278
2307
  }
2279
- function isActionsEnabled(ref, run = execFileSync10) {
2308
+ function isActionsEnabled(ref, run = execFileSync9) {
2280
2309
  const out = run(
2281
2310
  "gh",
2282
2311
  ["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
@@ -2284,7 +2313,7 @@ function isActionsEnabled(ref, run = execFileSync10) {
2284
2313
  ).toString().trim();
2285
2314
  return out === "true";
2286
2315
  }
2287
- function disableActions(ref, run = execFileSync10) {
2316
+ function disableActions(ref, run = execFileSync9) {
2288
2317
  run(
2289
2318
  "gh",
2290
2319
  [
@@ -2298,7 +2327,7 @@ function disableActions(ref, run = execFileSync10) {
2298
2327
  { stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
2299
2328
  );
2300
2329
  }
2301
- function readOriginRemote(cwd, run = execFileSync10) {
2330
+ function readOriginRemote(cwd, run = execFileSync9) {
2302
2331
  return run("git", ["remote", "get-url", "origin"], {
2303
2332
  cwd,
2304
2333
  stdio: ["ignore", "pipe", "ignore"]
@@ -2306,7 +2335,7 @@ function readOriginRemote(cwd, run = execFileSync10) {
2306
2335
  }
2307
2336
 
2308
2337
  // src/commands.doctor.mirror-actions.ts
2309
- function reportMirrorActions(section2, run = execFileSync11) {
2338
+ function reportMirrorActions(section2, run = execFileSync10) {
2310
2339
  let remote;
2311
2340
  try {
2312
2341
  remote = readOriginRemote(REPO_HOME, run);
@@ -2390,16 +2419,16 @@ function cmdDoctor(opts = {}) {
2390
2419
 
2391
2420
  // src/commands.drop-session.ts
2392
2421
  init_config();
2393
- import { execFileSync as execFileSync13 } from "node:child_process";
2422
+ import { execFileSync as execFileSync12 } from "node:child_process";
2394
2423
  import { existsSync as existsSync20, readdirSync as readdirSync8, statSync as statSync5 } from "node:fs";
2395
2424
  import { join as join24, relative as relative4 } from "node:path";
2396
2425
 
2397
2426
  // src/commands.drop-session.git.ts
2398
2427
  init_config();
2399
- import { execFileSync as execFileSync12 } from "node:child_process";
2428
+ import { execFileSync as execFileSync11 } from "node:child_process";
2400
2429
  function expandStagedDir(dirRel) {
2401
2430
  try {
2402
- const out = execFileSync12("git", ["ls-files", "-z", "--", dirRel], {
2431
+ const out = execFileSync11("git", ["ls-files", "-z", "--", dirRel], {
2403
2432
  cwd: REPO_HOME,
2404
2433
  stdio: ["ignore", "pipe", "pipe"]
2405
2434
  });
@@ -2410,7 +2439,7 @@ function expandStagedDir(dirRel) {
2410
2439
  }
2411
2440
  function isTrackedInHead(rel) {
2412
2441
  try {
2413
- execFileSync12("git", ["cat-file", "-e", `HEAD:${rel}`], {
2442
+ execFileSync11("git", ["cat-file", "-e", `HEAD:${rel}`], {
2414
2443
  cwd: REPO_HOME,
2415
2444
  stdio: ["ignore", "pipe", "pipe"]
2416
2445
  });
@@ -2421,7 +2450,7 @@ function isTrackedInHead(rel) {
2421
2450
  }
2422
2451
  function isInIndex(rel) {
2423
2452
  try {
2424
- const out = execFileSync12("git", ["ls-files", "--", rel], {
2453
+ const out = execFileSync11("git", ["ls-files", "--", rel], {
2425
2454
  cwd: REPO_HOME,
2426
2455
  stdio: ["ignore", "pipe", "pipe"]
2427
2456
  });
@@ -2635,12 +2664,12 @@ function unstageOne(rel) {
2635
2664
  }
2636
2665
  try {
2637
2666
  if (isTrackedInHead(rel)) {
2638
- execFileSync13("git", ["restore", "--staged", "--worktree", "--", rel], {
2667
+ execFileSync12("git", ["restore", "--staged", "--worktree", "--", rel], {
2639
2668
  cwd: REPO_HOME,
2640
2669
  stdio: ["ignore", "pipe", "pipe"]
2641
2670
  });
2642
2671
  } else {
2643
- execFileSync13("git", ["rm", "--cached", "-f", "--", rel], {
2672
+ execFileSync12("git", ["rm", "--cached", "-f", "--", rel], {
2644
2673
  cwd: REPO_HOME,
2645
2674
  stdio: ["ignore", "pipe", "pipe"]
2646
2675
  });
@@ -2785,8 +2814,7 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
2785
2814
  );
2786
2815
  return;
2787
2816
  }
2788
- const backupBase = join26(HOME, ".cache", "claude-nomad", "backup");
2789
- const ts = freshBackupTs(backupBase);
2817
+ const ts = freshBackupTs(BACKUP_BASE);
2790
2818
  backupBeforeWrite(localPath, ts);
2791
2819
  const original = readFileSync8(localPath, "utf8");
2792
2820
  const redacted = applyRedactions(original, findings);
@@ -2906,10 +2934,10 @@ import { join as join29 } from "node:path";
2906
2934
 
2907
2935
  // src/extras-sync.diff.ts
2908
2936
  init_utils();
2909
- import { execFileSync as execFileSync14 } from "node:child_process";
2937
+ import { execFileSync as execFileSync13 } from "node:child_process";
2910
2938
  function listDivergingFiles(a, b) {
2911
2939
  try {
2912
- const stdout = execFileSync14("git", ["diff", "--no-index", "--name-only", a, b], {
2940
+ const stdout = execFileSync13("git", ["diff", "--no-index", "--name-only", a, b], {
2913
2941
  stdio: ["ignore", "pipe", "pipe"]
2914
2942
  }).toString();
2915
2943
  return stdout.split("\n").filter((line) => line.length > 0);
@@ -3062,7 +3090,7 @@ function divergenceCheckExtras(ts) {
3062
3090
  const v = loadValidatedExtras({});
3063
3091
  if (v === null) return;
3064
3092
  const counts = { unmapped: 0, skipped: 0 };
3065
- const backupRoot = join29(HOME, ".cache", "claude-nomad", "backup", ts, "extras");
3093
+ const backupRoot = join29(BACKUP_BASE, ts, "extras");
3066
3094
  for (const { logical, localRoot, dirname: dirname4 } of eachExtrasTarget(v, counts)) {
3067
3095
  const local = join29(localRoot, dirname4);
3068
3096
  const repo = join29(REPO_HOME, "shared", "extras", logical, dirname4);
@@ -4027,9 +4055,9 @@ async function cmdPush(opts = {}) {
4027
4055
  }
4028
4056
 
4029
4057
  // src/commands.update.ts
4030
- import { execFileSync as execFileSync15 } from "node:child_process";
4058
+ import { execFileSync as execFileSync14 } from "node:child_process";
4031
4059
  init_utils();
4032
- function cmdUpdate(run = execFileSync15) {
4060
+ function cmdUpdate(run = execFileSync14) {
4033
4061
  try {
4034
4062
  run("npm", ["update", "-g", "claude-nomad"], { stdio: "inherit" });
4035
4063
  } catch (err) {
@@ -4076,14 +4104,14 @@ import { join as join39 } from "node:path";
4076
4104
 
4077
4105
  // src/init.gh-onboard.ts
4078
4106
  init_config();
4079
- import { execFileSync as execFileSync16 } from "node:child_process";
4107
+ import { execFileSync as execFileSync15 } from "node:child_process";
4080
4108
  init_utils();
4081
4109
  var DEFAULT_REPO_NAME = "claude-nomad-config";
4082
4110
  function isValidRepoName(name) {
4083
4111
  return /^[A-Za-z0-9._-]{1,100}$/.test(name);
4084
4112
  }
4085
4113
  var GH_NETWORK_TIMEOUT_MS = 3e4;
4086
- function ensureOriginRepo(repoName, run = execFileSync16) {
4114
+ function ensureOriginRepo(repoName, run = execFileSync15) {
4087
4115
  if (!isValidRepoName(repoName)) {
4088
4116
  die(
4089
4117
  `invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
@@ -4436,7 +4464,7 @@ function parseCleanArgs(argv) {
4436
4464
  // package.json
4437
4465
  var package_default = {
4438
4466
  name: "claude-nomad",
4439
- version: "0.35.0",
4467
+ version: "0.36.0",
4440
4468
  type: "module",
4441
4469
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
4442
4470
  keywords: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-nomad",
3
- "version": "0.35.0",
3
+ "version": "0.36.0",
4
4
  "type": "module",
5
5
  "description": "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
6
6
  "keywords": [