claude-nomad 0.67.2 → 0.68.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 +70 -8
- package/README.md +41 -40
- package/dist/nomad.mjs +197 -117
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,74 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.68.0](https://github.com/funkadelic/claude-nomad/compare/v0.67.3...v0.68.0) (2026-09-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### What's new
|
|
7
|
+
|
|
8
|
+
**On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and this does not apply to it.
|
|
9
|
+
|
|
10
|
+
- **A never-sync warning during `nomad pull` now suggests commands you can paste as they are.** Before a pull, nomad warns when a file that should never travel between machines turns up in your sync repo, and the warning names the git commands to deal with it. A filename containing a space split the suggested command in two, so pasting it acted on neither half. The paths are now quoted.
|
|
11
|
+
|
|
12
|
+
**On every platform.**
|
|
13
|
+
|
|
14
|
+
- **`nomad doctor` now warns when your sync repo is carrying a file that should never sync**, such as a credential or a per-machine settings file. Until now one of those was only noticed while it was new or changed, so a file that was committed and then left alone went unmentioned, and the check that did look ran only on Windows. Doctor names the file and the command that stops tracking it without deleting your copy. It only warns.
|
|
15
|
+
- **`nomad adopt` no longer fails on a deeply nested folder.** Adopting a shared folder scans it first, and a very deep tree could crash that scan. The scan handles any depth now.
|
|
16
|
+
- **A backup nomad cannot write is now reported as a plain error.** Nomad copies anything it is about to overwrite into a backup first. When that copy fails, say a full disk or a permissions problem, you get one message naming what it was copying, where it was going, and why it failed, instead of a crash report file. The message also says a partial copy may be left behind.
|
|
17
|
+
|
|
18
|
+
Everything else in this release is dependency updates.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
* **doctor:** warn when the sync repo tracks a never-synced file ([#562](https://github.com/funkadelic/claude-nomad/issues/562)) ([a1b3f71](https://github.com/funkadelic/claude-nomad/commit/a1b3f71a0a3deecac81e0bcc9e8ee214c1e50a4e))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
* **adopt:** handle deep directories and report a failed backup ([#560](https://github.com/funkadelic/claude-nomad/issues/560)) ([2b19ccb](https://github.com/funkadelic/claude-nomad/commit/2b19ccba5959efb7b42d16ee0db862df4d92a0ac))
|
|
29
|
+
* **backup:** report a failed backup instead of writing a crash report ([#561](https://github.com/funkadelic/claude-nomad/issues/561)) ([9d8484c](https://github.com/funkadelic/claude-nomad/commit/9d8484cdb59eb5a5905dfb0a3e16ae952acead5f))
|
|
30
|
+
* **pull:** quote file paths in the never-sync warning's suggested commands ([#559](https://github.com/funkadelic/claude-nomad/issues/559)) ([070cc17](https://github.com/funkadelic/claude-nomad/commit/070cc1709263d46cc50d01e99c345b3955d729ed))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Dependencies
|
|
34
|
+
|
|
35
|
+
* bump fast-uri from 3.1.5 to 3.1.7 ([#563](https://github.com/funkadelic/claude-nomad/issues/563)) ([7bf62e3](https://github.com/funkadelic/claude-nomad/commit/7bf62e3e481246b7c8125e31fb2df6654241dd54))
|
|
36
|
+
* bump the codeql-action group across 1 directory with 2 updates ([#556](https://github.com/funkadelic/claude-nomad/issues/556)) ([ad3ae73](https://github.com/funkadelic/claude-nomad/commit/ad3ae739043ae24f560d0ddd56baff79bb6b6ad6))
|
|
37
|
+
* bump the dev-dependencies group across 1 directory with 3 updates ([#555](https://github.com/funkadelic/claude-nomad/issues/555)) ([02aebe2](https://github.com/funkadelic/claude-nomad/commit/02aebe2a6407dcd4be67fe9e33723c735a1c44cd))
|
|
38
|
+
* bump the prod-dependencies group in /docs-site with 3 updates ([#557](https://github.com/funkadelic/claude-nomad/issues/557)) ([b6b5c8a](https://github.com/funkadelic/claude-nomad/commit/b6b5c8aebebcc8e4c73ac12d511bc972bda553d7))
|
|
39
|
+
|
|
40
|
+
## [0.67.3](https://github.com/funkadelic/claude-nomad/compare/v0.67.2...v0.67.3) (2026-08-29)
|
|
41
|
+
|
|
42
|
+
### What's new
|
|
43
|
+
|
|
44
|
+
**On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and this does not apply to it.
|
|
45
|
+
|
|
46
|
+
- **`nomad pull` now warns about a file that should never be synced, even when your gitignore hides it.** Before a pull, nomad checks your sync repo for files that have no business travelling between machines, such as credentials or per-host settings, and warns when one turns up under `shared/`. A file covered by a gitignore rule was invisible to that check, so it could sit there unmentioned. Nomad now names the file, along with the `git check-ignore` command that tells you which rule hid it, which is often a rule in your global git config rather than anything you wrote for this repo. It only warns. Nothing is deleted, because nomad cannot publish a file git ignores, so there is nothing to undo.
|
|
47
|
+
|
|
48
|
+
The rest of this release is internal: a regression test for the Windows `nomad sync` path, a stricter check on the test suite's own mock handling, and test-result reporting in CI. Nothing else changes what nomad does.
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
* **pull:** report gitignored denylisted paths under shared/ ([#553](https://github.com/funkadelic/claude-nomad/issues/553)) ([b226902](https://github.com/funkadelic/claude-nomad/commit/b226902e4361a3325a2fd0495e5abbd532f7d806))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
* report test results to Codecov Test Analytics ([#551](https://github.com/funkadelic/claude-nomad/issues/551)) ([a1fb555](https://github.com/funkadelic/claude-nomad/commit/a1fb5558185e6a97a8a026eeb05d3ef6a9834f28))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
### Documentation
|
|
63
|
+
|
|
64
|
+
* clean up the documentation prose ([#549](https://github.com/funkadelic/claude-nomad/issues/549)) ([cd8c40d](https://github.com/funkadelic/claude-nomad/commit/cd8c40d2f2877a868876e9ab338f4884060e90d9))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
### Testing
|
|
68
|
+
|
|
69
|
+
* scope the doMock/doUnmock symmetry guard to every describe ([#552](https://github.com/funkadelic/claude-nomad/issues/552)) ([ebe4af9](https://github.com/funkadelic/claude-nomad/commit/ebe4af988fed37b9ced1a3a42dd736fb1901c888))
|
|
70
|
+
* **sync:** add a cross-platform parity journey for nomad sync ([f87464d](https://github.com/funkadelic/claude-nomad/commit/f87464dcf7bf23341f1b8b8d4ae2eaefe0cc4e08))
|
|
71
|
+
|
|
3
72
|
## [0.67.2](https://github.com/funkadelic/claude-nomad/compare/v0.67.1...v0.67.2) (2026-08-24)
|
|
4
73
|
|
|
5
74
|
### What's new
|
|
@@ -528,14 +597,7 @@ All of this applies on macOS, Linux, WSL2, and native Windows.
|
|
|
528
597
|
|
|
529
598
|
* **pull:** retain unpushed local work and surface it in the preview ([#376](https://github.com/funkadelic/claude-nomad/issues/376)) ([3f70019](https://github.com/funkadelic/claude-nomad/commit/3f70019c89ccd77726e6330d9623e106bb9ff69c))
|
|
530
599
|
|
|
531
|
-
What this means: `nomad pull` no longer discards work you have not pushed
|
|
532
|
-
yet. Chat sessions, subagent history, and memory files that exist only on
|
|
533
|
-
this machine now survive a pull, and a synced file you have edited locally
|
|
534
|
-
(a project's `.planning` notes or its `CLAUDE.md`) is kept instead of being
|
|
535
|
-
overwritten when it differs from the incoming copy, with a note to push and
|
|
536
|
-
reconcile. `nomad diff` and `pull --dry-run` now show the count of retained
|
|
537
|
-
local-only items and the keep-local warning, where before they reported
|
|
538
|
-
`clean` / `0 collisions`.
|
|
600
|
+
What this means: `nomad pull` no longer discards work you have not pushed yet. Chat sessions, subagent history, and memory files that exist only on this machine now survive a pull, and a synced file you have edited locally (a project's `.planning` notes or its `CLAUDE.md`) is kept instead of being overwritten when it differs from the incoming copy, with a note to push and reconcile. `nomad diff` and `pull --dry-run` now show the count of retained local-only items and the keep-local warning, where before they reported `clean` / `0 collisions`.
|
|
539
601
|
|
|
540
602
|
## [0.56.1](https://github.com/funkadelic/claude-nomad/compare/v0.56.0...v0.56.1) (2026-06-30)
|
|
541
603
|
|
package/README.md
CHANGED
|
@@ -17,8 +17,8 @@ private Git repo you control. Run `nomad sync` on any machine and everything is
|
|
|
17
17
|
included; it pulls in your latest config first, then publishes your local changes, so you never have
|
|
18
18
|
to remember which one to run first.
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
survives different file paths and your secrets never ride along.
|
|
20
|
+
Dotfiles managers and rsync copy files. **claude-nomad** understands Claude Code's state, so your
|
|
21
|
+
session history survives different file paths and your secrets never ride along.
|
|
22
22
|
|
|
23
23
|
**Full documentation: <https://funkadelic.github.io/claude-nomad/>**
|
|
24
24
|
|
|
@@ -54,18 +54,20 @@ survives different file paths and your secrets never ride along.
|
|
|
54
54
|
- **One command tells you what is wrong.** `nomad doctor` is a read-only health check: wedged sync
|
|
55
55
|
repo, broken hook references, hooks that would crash on session start because of a missing
|
|
56
56
|
`--preserve-symlinks-main` flag, version drift, oversized backup cache, missing git committer
|
|
57
|
-
identity in the sync repo (a push fails at commit time without one),
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
identity in the sync repo (a push fails at commit time without one), a never-synced file such as a
|
|
58
|
+
credential or a per-host settings file that Git already tracks inside the repo's `shared/` folder
|
|
59
|
+
(once it is committed the pull-side guard can no longer see it, so doctor names the file and the
|
|
60
|
+
folder name that blocks it), path-map entries whose local project folder no longer exists on this
|
|
61
|
+
machine, a multi-host repo where this machine's hostname-derived key matches no
|
|
62
|
+
`hosts/<HOST>.json` or path-map entry (a sign `NOMAD_HOST` is unset here, so per-host settings and
|
|
63
|
+
session sync will not line up with the other hosts), synced skills with local edits that differ
|
|
64
|
+
from the shared copy, a native Windows shared-config copy that has drifted from the repo's copy (a
|
|
65
|
+
warning, not a failure), and settings drift in both directions: keys present in the repo merge but
|
|
66
|
+
absent from your live `settings.json` (behind; the next `nomad pull` will restore them, fix:
|
|
67
|
+
`nomad pull`) and keys present locally but not yet in the repo (ahead; local-only additions, fix:
|
|
68
|
+
`nomad capture-settings`). Each issue includes a fix hint. By default the report is compact: it
|
|
69
|
+
shows only checks that need action plus a one-line verdict. Add `--verbose` (or `--all` / `-v`) to
|
|
70
|
+
see the full per-check tree, including everything that passed.
|
|
69
71
|
- **Self-healing sync.** Every overwrite is backed up first, and `nomad pull --force-remote`
|
|
70
72
|
recovers two kinds of stuck sync repo: a repo stuck mid-rebase or mid-merge (aborts the operation,
|
|
71
73
|
parks stranded work on a branch, refuses if shared config is at risk), and a repo where the rebase
|
|
@@ -81,7 +83,7 @@ opt-in per-project sync, transcript redaction, backup pruning, and more.
|
|
|
81
83
|
|
|
82
84
|
## Quickstart
|
|
83
85
|
|
|
84
|
-
nomad works with two directories
|
|
86
|
+
nomad works with two directories:
|
|
85
87
|
|
|
86
88
|
- **`~/claude-nomad/`** is your private sync repo. This is the one you edit.
|
|
87
89
|
- **`~/.claude/`** is Claude Code's live config. nomad regenerates it on every `pull`.
|
|
@@ -222,18 +224,17 @@ log output remains grep-stable.
|
|
|
222
224
|
When `nomad push` detects a potential secret, it drops into an interactive menu (TTY) or aborts with
|
|
223
225
|
a recovery hint (non-TTY/CI). Three non-interactive recovery paths are available without the menu:
|
|
224
226
|
|
|
225
|
-
- `nomad push --full-scan
|
|
227
|
+
- `nomad push --full-scan`: ignore the per-host push manifest and rescan all transcripts, then
|
|
226
228
|
rewrite the manifest on success. Use this after upgrading gitleaks, after editing a gitleaks
|
|
227
229
|
config file, or whenever you want to be certain nothing slipped through an incremental scan.
|
|
228
230
|
Composes freely with `--dry-run` and all resolution modes.
|
|
229
|
-
- `nomad push --redact-all
|
|
231
|
+
- `nomad push --redact-all`: scrub every finding from the local transcript in place, then push.
|
|
230
232
|
All-or-nothing: if any finding cannot be redacted (an active session, or one that does not map to
|
|
231
233
|
a synced transcript), nothing is changed and the push stops so you can handle those sessions.
|
|
232
|
-
- `nomad push --allow <rule
|
|
234
|
+
- `nomad push --allow <rule>`: record findings matching one gitleaks rule id as false positives
|
|
233
235
|
(appends their fingerprints to `.gitleaksignore`), then re-scan and push.
|
|
234
|
-
- `nomad push --allow-all
|
|
235
|
-
|
|
236
|
-
- `nomad allow <fingerprint>...` -- pre-record specific fingerprints in `.gitleaksignore` without
|
|
236
|
+
- `nomad push --allow-all`: record every current finding as a false positive, then re-scan and push.
|
|
237
|
+
- `nomad allow <fingerprint>...`: pre-record specific fingerprints in `.gitleaksignore` without
|
|
237
238
|
going through a push cycle.
|
|
238
239
|
|
|
239
240
|
All allow paths always re-scan after writing the allowlist; a surviving finding still aborts the
|
|
@@ -314,16 +315,16 @@ false alarm from a concurrent run. Value `3` is reserved for future use.
|
|
|
314
315
|
|
|
315
316
|
## Crash reports
|
|
316
317
|
|
|
317
|
-
If `nomad` ever hits an unexpected bug, it
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
318
|
+
If `nomad` ever hits an unexpected bug, it does not dump a raw stack trace. It prints a short "this
|
|
319
|
+
looks like a bug" banner (with a link to the issue tracker) and writes a small report you can attach
|
|
320
|
+
to a bug report if you choose. The exit code is unchanged: an unexpected crash exits `1`, and a
|
|
321
|
+
documented failure still exits with its own code from the table above. A prompt you cancel yourself
|
|
322
|
+
is not a crash: it exits `130` and writes no report.
|
|
322
323
|
|
|
323
324
|
What this means for you:
|
|
324
325
|
|
|
325
326
|
- **The report stays on your machine.** It is written to `~/.cache/claude-nomad/crash/` and nothing
|
|
326
|
-
is ever uploaded anywhere. The file
|
|
327
|
+
is ever uploaded anywhere. The file sits there until you decide to share it or delete it.
|
|
327
328
|
- **The saved report is scrubbed twice.** First a structural pass rewrites your home directory to
|
|
328
329
|
`~` and your hostname to a placeholder (absolute paths are personal information a secret scanner
|
|
329
330
|
would not catch), and redacts credential-shaped tokens it recognizes (GitHub, GitLab, Slack, and
|
|
@@ -342,24 +343,24 @@ What this means for you:
|
|
|
342
343
|
contents of any file.
|
|
343
344
|
- **The directory manages itself.** Only the most recent reports are kept; older ones are pruned
|
|
344
345
|
automatically on the next crash. There is no `nomad clean` flag for the crash directory, by
|
|
345
|
-
design
|
|
346
|
+
design.
|
|
346
347
|
|
|
347
348
|
## Learn more
|
|
348
349
|
|
|
349
|
-
- [How it works](https://funkadelic.github.io/claude-nomad/how-it-works/)
|
|
350
|
-
|
|
351
|
-
- [GSD-aware sync](https://funkadelic.github.io/claude-nomad/gsd-aware-sync/)
|
|
350
|
+
- [How it works](https://funkadelic.github.io/claude-nomad/how-it-works/): path remapping, settings
|
|
351
|
+
merge, what syncs and what doesn't
|
|
352
|
+
- [GSD-aware sync](https://funkadelic.github.io/claude-nomad/gsd-aware-sync/): what nomad does for
|
|
352
353
|
GSD users out of the box
|
|
353
|
-
- [Setup and migration](https://funkadelic.github.io/claude-nomad/quickstart/)
|
|
354
|
+
- [Setup and migration](https://funkadelic.github.io/claude-nomad/quickstart/): full setup
|
|
354
355
|
walkthrough, migrating an existing `~/.claude/`
|
|
355
|
-
- [Recipes](https://funkadelic.github.io/claude-nomad/recipes/)
|
|
356
|
-
|
|
357
|
-
- [Commands reference](https://funkadelic.github.io/claude-nomad/commands/)
|
|
358
|
-
- [Claude Code plugin](https://funkadelic.github.io/claude-nomad/plugin/)
|
|
359
|
-
|
|
360
|
-
- [Recovery flows](https://funkadelic.github.io/claude-nomad/recovery/)
|
|
356
|
+
- [Recipes](https://funkadelic.github.io/claude-nomad/recipes/): copy-pasteable example configs for
|
|
357
|
+
common setups, from scratch to cross-OS remapping and GSD integration
|
|
358
|
+
- [Commands reference](https://funkadelic.github.io/claude-nomad/commands/): all CLI flags
|
|
359
|
+
- [Claude Code plugin](https://funkadelic.github.io/claude-nomad/plugin/): /nomad slash commands and
|
|
360
|
+
the session-start drift check
|
|
361
|
+
- [Recovery flows](https://funkadelic.github.io/claude-nomad/recovery/): backups, drop-session,
|
|
361
362
|
redact, gitleaks allowlist, non-interactive allow
|
|
362
|
-
- [FAQ](https://funkadelic.github.io/claude-nomad/faq/)
|
|
363
|
-
|
|
363
|
+
- [FAQ](https://funkadelic.github.io/claude-nomad/faq/): common questions, like the right push/pull
|
|
364
|
+
order when both sides have changes
|
|
364
365
|
- [Contributing](https://funkadelic.github.io/claude-nomad/contributing/)
|
|
365
366
|
- [Security policy](https://funkadelic.github.io/claude-nomad/security/)
|
package/dist/nomad.mjs
CHANGED
|
@@ -721,6 +721,21 @@ var init_config = __esm({
|
|
|
721
721
|
}
|
|
722
722
|
});
|
|
723
723
|
|
|
724
|
+
// src/error-text.ts
|
|
725
|
+
function errorText(err) {
|
|
726
|
+
try {
|
|
727
|
+
const message = err?.message;
|
|
728
|
+
return typeof message === "string" ? message : String(err);
|
|
729
|
+
} catch {
|
|
730
|
+
return "unprintable error value";
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
var init_error_text = __esm({
|
|
734
|
+
"src/error-text.ts"() {
|
|
735
|
+
"use strict";
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
724
739
|
// src/utils.json.ts
|
|
725
740
|
import { readFileSync } from "node:fs";
|
|
726
741
|
function readJson(path) {
|
|
@@ -886,17 +901,22 @@ function ensureSymlink(linkPath, target) {
|
|
|
886
901
|
symlinkSync(target, linkPath);
|
|
887
902
|
log(`linked ${linkPath} -> ${target}`);
|
|
888
903
|
}
|
|
889
|
-
function backupUnder(absPath, anchor, destRoot) {
|
|
904
|
+
function backupUnder(absPath, anchor, destRoot, hint) {
|
|
890
905
|
if (!existsSync3(absPath)) return false;
|
|
891
906
|
const rel = relative3(anchor, absPath);
|
|
892
907
|
if (rel === "" || rel === ".." || rel.startsWith(`..${sep2}`)) return false;
|
|
893
908
|
const dst = join5(destRoot, rel);
|
|
894
|
-
|
|
895
|
-
|
|
909
|
+
try {
|
|
910
|
+
mkdirSync(dirname(dst), { recursive: true });
|
|
911
|
+
cpSync3(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
|
|
912
|
+
} catch (err) {
|
|
913
|
+
const fragment = `could not back up ${absPath} into ${dst} (${errorText(err)}); a partial copy may be left there`;
|
|
914
|
+
throw new NomadFatal(hint === void 0 ? fragment : `${fragment}. ${hint}`);
|
|
915
|
+
}
|
|
896
916
|
return true;
|
|
897
917
|
}
|
|
898
|
-
function backupBeforeWrite(absPath, ts) {
|
|
899
|
-
return backupUnder(absPath, claudeHome(), join5(backupBase(), ts));
|
|
918
|
+
function backupBeforeWrite(absPath, ts, hint) {
|
|
919
|
+
return backupUnder(absPath, claudeHome(), join5(backupBase(), ts), hint);
|
|
900
920
|
}
|
|
901
921
|
function backupRepoWrite(absPath, ts, repoHome2) {
|
|
902
922
|
backupUnder(absPath, repoHome2, join5(backupBase(), ts, "repo"));
|
|
@@ -909,6 +929,7 @@ var init_utils_fs = __esm({
|
|
|
909
929
|
"src/utils.fs.ts"() {
|
|
910
930
|
"use strict";
|
|
911
931
|
init_config();
|
|
932
|
+
init_error_text();
|
|
912
933
|
init_utils_json();
|
|
913
934
|
init_utils();
|
|
914
935
|
RENAME_RETRY_MAX_ATTEMPTS = 5;
|
|
@@ -1134,7 +1155,7 @@ var init_autostash_guard = __esm({
|
|
|
1134
1155
|
});
|
|
1135
1156
|
|
|
1136
1157
|
// src/push-checks.ts
|
|
1137
|
-
import { execFileSync as
|
|
1158
|
+
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
1138
1159
|
import { readdirSync as readdirSync8, rmSync as rmSync7 } from "node:fs";
|
|
1139
1160
|
import { homedir as homedir2, platform } from "node:os";
|
|
1140
1161
|
import { delimiter, join as join23 } from "node:path";
|
|
@@ -1201,7 +1222,7 @@ function probeGitleaks() {
|
|
|
1201
1222
|
const args = ["version"];
|
|
1202
1223
|
if (toml !== null) args.push("--config", toml);
|
|
1203
1224
|
try {
|
|
1204
|
-
return
|
|
1225
|
+
return execFileSync5("gitleaks", args, { stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
|
|
1205
1226
|
} catch (err) {
|
|
1206
1227
|
const e = err;
|
|
1207
1228
|
if (e.code === "ENOENT") throw new NomadFatal(gitleaksInstallHint());
|
|
@@ -1221,7 +1242,7 @@ function rebaseBeforePush(repo) {
|
|
|
1221
1242
|
const wedge = classifyWedge(repo);
|
|
1222
1243
|
if (wedge !== null) throw new NomadFatal(wedgePreflight(wedge), { code: EXIT.CONFLICT });
|
|
1223
1244
|
try {
|
|
1224
|
-
|
|
1245
|
+
execFileSync5("git", ["pull", "--rebase", "--autostash"], {
|
|
1225
1246
|
cwd: repo,
|
|
1226
1247
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1227
1248
|
});
|
|
@@ -1246,7 +1267,7 @@ var init_push_checks = __esm({
|
|
|
1246
1267
|
});
|
|
1247
1268
|
|
|
1248
1269
|
// src/push-gitleaks.scan.ts
|
|
1249
|
-
import { execFileSync as
|
|
1270
|
+
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
1250
1271
|
import { mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, rmSync as rmSync8 } from "node:fs";
|
|
1251
1272
|
import { homedir as homedir3 } from "node:os";
|
|
1252
1273
|
import { join as join27 } from "node:path";
|
|
@@ -1313,9 +1334,9 @@ function scanStagedTree(repoDir, forwardStreams = false) {
|
|
|
1313
1334
|
const opts = { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] };
|
|
1314
1335
|
const gitleaksOpts = { ...opts, timeout: GITLEAKS_SCAN_TIMEOUT_MS };
|
|
1315
1336
|
try {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1337
|
+
execFileSync9("git", ["init", "-q"], opts);
|
|
1338
|
+
execFileSync9("git", ["add", "-A"], opts);
|
|
1339
|
+
execFileSync9("gitleaks", args, gitleaksOpts);
|
|
1319
1340
|
return [];
|
|
1320
1341
|
} catch (err) {
|
|
1321
1342
|
const e = err;
|
|
@@ -1348,7 +1369,7 @@ function scanFile(filePath, forwardStreams = false, timeoutMs = GITLEAKS_SCAN_TI
|
|
|
1348
1369
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1349
1370
|
timeout: timeoutMs
|
|
1350
1371
|
};
|
|
1351
|
-
|
|
1372
|
+
execFileSync9("gitleaks", args, opts);
|
|
1352
1373
|
return [];
|
|
1353
1374
|
} catch (err) {
|
|
1354
1375
|
const e = err;
|
|
@@ -1567,43 +1588,37 @@ import { dirname as dirname2, join as join7, resolve as resolve2 } from "node:pa
|
|
|
1567
1588
|
|
|
1568
1589
|
// src/commands.adopt.scan.ts
|
|
1569
1590
|
init_config();
|
|
1570
|
-
|
|
1571
|
-
import { join as join2, relative } from "node:path";
|
|
1572
|
-
|
|
1573
|
-
// src/error-text.ts
|
|
1574
|
-
function errorText(err) {
|
|
1575
|
-
try {
|
|
1576
|
-
const message = err?.message;
|
|
1577
|
-
return typeof message === "string" ? message : String(err);
|
|
1578
|
-
} catch {
|
|
1579
|
-
return "unprintable error value";
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
// src/commands.adopt.scan.ts
|
|
1591
|
+
init_error_text();
|
|
1584
1592
|
init_exit_codes();
|
|
1585
1593
|
init_utils();
|
|
1594
|
+
import { lstatSync, readdirSync } from "node:fs";
|
|
1595
|
+
import { join as join2, relative } from "node:path";
|
|
1586
1596
|
function toForwardSlash(p) {
|
|
1587
1597
|
return p.replaceAll("\\", "/");
|
|
1588
1598
|
}
|
|
1589
|
-
function walk(root
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1599
|
+
function walk(root) {
|
|
1600
|
+
const out = [];
|
|
1601
|
+
const stack = [root];
|
|
1602
|
+
while (stack.length > 0) {
|
|
1603
|
+
const dir = stack.pop();
|
|
1604
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1605
|
+
const match = matchDeniedName(ALWAYS_NEVER_SYNC, entry.name);
|
|
1606
|
+
if (match !== null) {
|
|
1607
|
+
out.push({
|
|
1608
|
+
path: toForwardSlash(relative(root, join2(dir, entry.name))),
|
|
1609
|
+
matched: match.axis === "name" ? match.entry : null
|
|
1610
|
+
});
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
if (entry.isDirectory()) stack.push(join2(dir, entry.name));
|
|
1598
1614
|
}
|
|
1599
|
-
if (entry.isDirectory()) walk(root, join2(dir, entry.name), out);
|
|
1600
1615
|
}
|
|
1616
|
+
return out;
|
|
1601
1617
|
}
|
|
1602
1618
|
function scanDeniedEntries(root) {
|
|
1603
1619
|
const stat = lstatSync(root, { throwIfNoEntry: false });
|
|
1604
1620
|
if (!stat?.isDirectory()) return [];
|
|
1605
|
-
const out =
|
|
1606
|
-
walk(root, root, out);
|
|
1621
|
+
const out = walk(root);
|
|
1607
1622
|
out.sort((a, b) => a.path.localeCompare(b.path));
|
|
1608
1623
|
return out;
|
|
1609
1624
|
}
|
|
@@ -1652,6 +1667,7 @@ function refuseDeniedEntries(name, root) {
|
|
|
1652
1667
|
|
|
1653
1668
|
// src/commands.adopt.recover.ts
|
|
1654
1669
|
init_config();
|
|
1670
|
+
init_error_text();
|
|
1655
1671
|
init_exit_codes();
|
|
1656
1672
|
|
|
1657
1673
|
// src/extras-sync.core.ts
|
|
@@ -2689,7 +2705,11 @@ function adoptStopsEarly(name, linkPath, sharedTarget) {
|
|
|
2689
2705
|
}
|
|
2690
2706
|
function performAdoptMove(name, linkPath, sharedTarget, repo, backup) {
|
|
2691
2707
|
const ts = freshBackupTs(backup);
|
|
2692
|
-
const snapshotted = backupBeforeWrite(
|
|
2708
|
+
const snapshotted = backupBeforeWrite(
|
|
2709
|
+
linkPath,
|
|
2710
|
+
ts,
|
|
2711
|
+
`Nothing was removed from ${linkPath} and nothing was written to the repo. Fix what the error names, then run \`nomad adopt ${name}\` again.`
|
|
2712
|
+
);
|
|
2693
2713
|
const rel = join9("shared", name);
|
|
2694
2714
|
const stage = () => gitOrFatal(["add", "--", rel], `git add shared/${name}`, repo);
|
|
2695
2715
|
copyIntoSharedOrFatal(name, linkPath, sharedTarget, repo);
|
|
@@ -3996,15 +4016,34 @@ function reportSkillsDivergence(section2) {
|
|
|
3996
4016
|
// src/commands.doctor.checks.git-state.ts
|
|
3997
4017
|
init_color();
|
|
3998
4018
|
init_config();
|
|
3999
|
-
import { execFileSync as
|
|
4019
|
+
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
4000
4020
|
import { existsSync as existsSync17 } from "node:fs";
|
|
4001
4021
|
import { join as join24, relative as relative5 } from "node:path";
|
|
4002
4022
|
init_commands_pull_wedge();
|
|
4023
|
+
|
|
4024
|
+
// src/git-probe.ts
|
|
4025
|
+
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
4026
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
4027
|
+
var PROBE_MAX_BUFFER = 64 * 1024 * 1024;
|
|
4028
|
+
function gitProbe(args, repo) {
|
|
4029
|
+
try {
|
|
4030
|
+
return execFileSync4("git", args, {
|
|
4031
|
+
cwd: repo,
|
|
4032
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4033
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
4034
|
+
maxBuffer: PROBE_MAX_BUFFER
|
|
4035
|
+
}).toString();
|
|
4036
|
+
} catch {
|
|
4037
|
+
return null;
|
|
4038
|
+
}
|
|
4039
|
+
}
|
|
4040
|
+
|
|
4041
|
+
// src/commands.doctor.checks.git-state.ts
|
|
4003
4042
|
init_push_checks();
|
|
4004
4043
|
init_utils();
|
|
4005
4044
|
function reportGitleaksProbe(section2) {
|
|
4006
4045
|
try {
|
|
4007
|
-
|
|
4046
|
+
execFileSync6("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
4008
4047
|
return true;
|
|
4009
4048
|
} catch (err) {
|
|
4010
4049
|
if (err.code === "ENOENT") {
|
|
@@ -4040,7 +4079,7 @@ function reportGitlinks(section2) {
|
|
|
4040
4079
|
}
|
|
4041
4080
|
function reportRemote(section2) {
|
|
4042
4081
|
try {
|
|
4043
|
-
const url =
|
|
4082
|
+
const url = execFileSync6("git", ["remote", "get-url", "origin"], {
|
|
4044
4083
|
cwd: repoHome(),
|
|
4045
4084
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4046
4085
|
}).toString().trim();
|
|
@@ -4097,7 +4136,7 @@ function reportGitIdentity(section2) {
|
|
|
4097
4136
|
const missing = [];
|
|
4098
4137
|
for (const field of ["user.name", "user.email"]) {
|
|
4099
4138
|
try {
|
|
4100
|
-
const val =
|
|
4139
|
+
const val = execFileSync6("git", ["config", field], {
|
|
4101
4140
|
cwd: repo,
|
|
4102
4141
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4103
4142
|
}).toString().trim();
|
|
@@ -4117,6 +4156,45 @@ function reportGitIdentity(section2) {
|
|
|
4117
4156
|
addItem(section2, `${green(okGlyph)} git identity: user.name and user.email configured`);
|
|
4118
4157
|
}
|
|
4119
4158
|
}
|
|
4159
|
+
var MAX_TRACKED_DENIED_ROWS = 5;
|
|
4160
|
+
var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
|
|
4161
|
+
function renderable(text) {
|
|
4162
|
+
return text.replace(CONTROL_CHARS, " ");
|
|
4163
|
+
}
|
|
4164
|
+
function reportTrackedDeniedShared(section2) {
|
|
4165
|
+
const out = gitProbe(["ls-files", "-z", "--", ":(icase)shared/"], repoHome());
|
|
4166
|
+
if (out === null) {
|
|
4167
|
+
addItem(
|
|
4168
|
+
section2,
|
|
4169
|
+
`${dim(infoGlyph)} never-sync scan: git could not list shared/, so nothing was scanned`
|
|
4170
|
+
);
|
|
4171
|
+
return;
|
|
4172
|
+
}
|
|
4173
|
+
const hits = /* @__PURE__ */ new Map();
|
|
4174
|
+
for (const path of out.split("\0")) {
|
|
4175
|
+
const segment = path === "" ? null : deniedSegmentFor(path);
|
|
4176
|
+
if (segment !== null) hits.set(path, segment);
|
|
4177
|
+
}
|
|
4178
|
+
if (hits.size === 0) {
|
|
4179
|
+
addItem(
|
|
4180
|
+
section2,
|
|
4181
|
+
`${green(okGlyph)} never-sync scan: no tracked path under shared/ is denylisted`
|
|
4182
|
+
);
|
|
4183
|
+
return;
|
|
4184
|
+
}
|
|
4185
|
+
for (const [path, segment] of [...hits].slice(0, MAX_TRACKED_DENIED_ROWS)) {
|
|
4186
|
+
addItem(
|
|
4187
|
+
section2,
|
|
4188
|
+
`${yellow(warnGlyph)} never-sync: git tracks ${blue(renderable(path))} (the path segment "${renderable(segment)}" is blocked by the never-sync boundary)`
|
|
4189
|
+
);
|
|
4190
|
+
}
|
|
4191
|
+
const overflow = hits.size - MAX_TRACKED_DENIED_ROWS;
|
|
4192
|
+
const more = overflow > 0 ? `${overflow} more not listed, run git ls-files -- ":(icase)shared/" in the sync repo to see them all. ` : "";
|
|
4193
|
+
addItem(
|
|
4194
|
+
section2,
|
|
4195
|
+
`${yellow(warnGlyph)} never-sync: ${more}Run git rm --cached -- "<path>" and commit to stop tracking each one; that leaves the file on disk, so move it outside shared/ if you want to keep it. If one holds a real secret, rotate it and rewrite history too, because nomad only changes your local worktree and index and cannot scrub what a previous push already sent`
|
|
4196
|
+
);
|
|
4197
|
+
}
|
|
4120
4198
|
|
|
4121
4199
|
// src/commands.doctor.checks.backups.ts
|
|
4122
4200
|
init_color();
|
|
@@ -4166,14 +4244,14 @@ function reportBackupsCheck(section2, backupBase2 = backupBase()) {
|
|
|
4166
4244
|
|
|
4167
4245
|
// src/commands.doctor.check-remote.ts
|
|
4168
4246
|
init_color();
|
|
4169
|
-
import { execFileSync as
|
|
4247
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
4170
4248
|
init_config();
|
|
4171
4249
|
init_utils_json();
|
|
4172
4250
|
var GIT_REMOTE_TIMEOUT_MS = 3e3;
|
|
4173
4251
|
function readRemotePathMap(sec, repo) {
|
|
4174
4252
|
let rawJson;
|
|
4175
4253
|
try {
|
|
4176
|
-
rawJson =
|
|
4254
|
+
rawJson = execFileSync7("git", ["show", "origin/main:path-map.json"], {
|
|
4177
4255
|
cwd: repo,
|
|
4178
4256
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4179
4257
|
timeout: GIT_REMOTE_TIMEOUT_MS
|
|
@@ -4206,7 +4284,7 @@ function reportCheckRemote(section2) {
|
|
|
4206
4284
|
const repo = repoHome();
|
|
4207
4285
|
let names;
|
|
4208
4286
|
try {
|
|
4209
|
-
const out =
|
|
4287
|
+
const out = execFileSync7("git", ["ls-tree", "--name-only", "origin/main"], {
|
|
4210
4288
|
cwd: repo,
|
|
4211
4289
|
stdio: ["ignore", "pipe", "pipe"],
|
|
4212
4290
|
timeout: GIT_REMOTE_TIMEOUT_MS
|
|
@@ -4238,9 +4316,9 @@ import { join as join26 } from "node:path";
|
|
|
4238
4316
|
init_config();
|
|
4239
4317
|
|
|
4240
4318
|
// src/http-fetch.ts
|
|
4241
|
-
import { execFileSync as
|
|
4319
|
+
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
4242
4320
|
var FETCH_TIMEOUT_MS = 3e3;
|
|
4243
|
-
function fetchUrl(url, run =
|
|
4321
|
+
function fetchUrl(url, run = execFileSync8) {
|
|
4244
4322
|
try {
|
|
4245
4323
|
return run("curl", ["-fsSL", "-m", "3", url], {
|
|
4246
4324
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -4301,14 +4379,14 @@ function reportCheckSchema(section2) {
|
|
|
4301
4379
|
// src/commands.doctor.check-shared.ts
|
|
4302
4380
|
init_color();
|
|
4303
4381
|
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
4304
|
-
import { execFileSync as
|
|
4382
|
+
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
4305
4383
|
import { existsSync as existsSync22, mkdirSync as mkdirSync8, readdirSync as readdirSync12, rmSync as rmSync12 } from "node:fs";
|
|
4306
4384
|
import { homedir as homedir6 } from "node:os";
|
|
4307
4385
|
import { join as join33 } from "node:path";
|
|
4308
4386
|
|
|
4309
4387
|
// src/commands.doctor.check-shared.memory.ts
|
|
4310
4388
|
init_color();
|
|
4311
|
-
import { execFileSync as
|
|
4389
|
+
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
4312
4390
|
import { randomBytes } from "node:crypto";
|
|
4313
4391
|
import { mkdirSync as mkdirSync4, rmSync as rmSync9, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4314
4392
|
import { homedir as homedir4 } from "node:os";
|
|
@@ -4320,7 +4398,7 @@ var MEMORY_MD_PATH = /^shared\/projects\/([^/]+)\/memory\/[^/]+\.md$/;
|
|
|
4320
4398
|
function buildMemoryScanTree(tmpRoot) {
|
|
4321
4399
|
let paths;
|
|
4322
4400
|
try {
|
|
4323
|
-
const out =
|
|
4401
|
+
const out = execFileSync10(
|
|
4324
4402
|
"git",
|
|
4325
4403
|
["-C", repoHome(), "ls-tree", "-r", "-z", "--name-only", "HEAD", "--", "shared/projects"],
|
|
4326
4404
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1e4 }
|
|
@@ -4336,7 +4414,7 @@ function buildMemoryScanTree(tmpRoot) {
|
|
|
4336
4414
|
if (m?.[1] === void 0) continue;
|
|
4337
4415
|
let blob;
|
|
4338
4416
|
try {
|
|
4339
|
-
blob =
|
|
4417
|
+
blob = execFileSync10("git", ["-C", repoHome(), "cat-file", "blob", `HEAD:${rel}`], {
|
|
4340
4418
|
stdio: ["ignore", "pipe", "ignore"],
|
|
4341
4419
|
maxBuffer: 67108864,
|
|
4342
4420
|
timeout: 1e4
|
|
@@ -4504,7 +4582,7 @@ function scanAndReport(section2, tmpRoot, staged, logicalToEncoded) {
|
|
|
4504
4582
|
|
|
4505
4583
|
// src/commands.doctor.check-shared.skills.ts
|
|
4506
4584
|
init_color();
|
|
4507
|
-
import { execFileSync as
|
|
4585
|
+
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
4508
4586
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
4509
4587
|
import { mkdirSync as mkdirSync5, rmSync as rmSync10, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4510
4588
|
import { homedir as homedir5 } from "node:os";
|
|
@@ -4516,7 +4594,7 @@ var SKILL_PATH = /^shared\/skills\/([^/]+)\/(.+)$/;
|
|
|
4516
4594
|
function buildSkillScanTree(tmpRoot) {
|
|
4517
4595
|
let paths;
|
|
4518
4596
|
try {
|
|
4519
|
-
const out =
|
|
4597
|
+
const out = execFileSync11(
|
|
4520
4598
|
"git",
|
|
4521
4599
|
["-C", repoHome(), "ls-tree", "-r", "-z", "--name-only", "HEAD", "--", "shared/skills"],
|
|
4522
4600
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1e4 }
|
|
@@ -4532,7 +4610,7 @@ function buildSkillScanTree(tmpRoot) {
|
|
|
4532
4610
|
if (m?.[1] === void 0) continue;
|
|
4533
4611
|
let blob;
|
|
4534
4612
|
try {
|
|
4535
|
-
blob =
|
|
4613
|
+
blob = execFileSync11("git", ["-C", repoHome(), "cat-file", "blob", `HEAD:${rel}`], {
|
|
4536
4614
|
stdio: ["ignore", "pipe", "ignore"],
|
|
4537
4615
|
maxBuffer: 67108864,
|
|
4538
4616
|
timeout: 1e4
|
|
@@ -4970,7 +5048,7 @@ function buildScanTree(tmpRoot) {
|
|
|
4970
5048
|
}
|
|
4971
5049
|
function probeGitleaksForScan() {
|
|
4972
5050
|
try {
|
|
4973
|
-
|
|
5051
|
+
execFileSync12("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
4974
5052
|
return "ok";
|
|
4975
5053
|
} catch (err) {
|
|
4976
5054
|
if (err.code === "ENOENT") return "missing";
|
|
@@ -5626,16 +5704,16 @@ function reportNodeEngineCheck(section2) {
|
|
|
5626
5704
|
|
|
5627
5705
|
// src/commands.doctor.checks.longpaths.ts
|
|
5628
5706
|
init_color();
|
|
5629
|
-
import { execFileSync as
|
|
5707
|
+
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
5630
5708
|
init_config();
|
|
5631
|
-
var
|
|
5709
|
+
var PROBE_TIMEOUT_MS2 = 3e3;
|
|
5632
5710
|
var LONGPATHS_REG_KEY = String.raw`HKLM\SYSTEM\CurrentControlSet\Control\FileSystem`;
|
|
5633
5711
|
var LONGPATHS_REG_VALUE = "LongPathsEnabled";
|
|
5634
5712
|
function probeGitLongpaths(run) {
|
|
5635
5713
|
try {
|
|
5636
5714
|
const out = run("git", ["-C", repoHome(), "config", "--get", "core.longpaths"], {
|
|
5637
5715
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5638
|
-
timeout:
|
|
5716
|
+
timeout: PROBE_TIMEOUT_MS2
|
|
5639
5717
|
}).toString().trim();
|
|
5640
5718
|
return out === "true" || out === "1";
|
|
5641
5719
|
} catch {
|
|
@@ -5646,7 +5724,7 @@ function probeRegistryLongpaths(run) {
|
|
|
5646
5724
|
try {
|
|
5647
5725
|
const out = run("reg", ["query", LONGPATHS_REG_KEY, "/v", LONGPATHS_REG_VALUE], {
|
|
5648
5726
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5649
|
-
timeout:
|
|
5727
|
+
timeout: PROBE_TIMEOUT_MS2
|
|
5650
5728
|
}).toString();
|
|
5651
5729
|
return /0x1\b/.test(out);
|
|
5652
5730
|
} catch {
|
|
@@ -5660,7 +5738,7 @@ function addLongpathsRow(section2, label, enabled2, remediation) {
|
|
|
5660
5738
|
}
|
|
5661
5739
|
addItem(section2, `${yellow(warnGlyph)} ${label}: not enabled (${remediation})`);
|
|
5662
5740
|
}
|
|
5663
|
-
function reportLongPathsCheck(section2, run =
|
|
5741
|
+
function reportLongPathsCheck(section2, run = execFileSync13) {
|
|
5664
5742
|
if (process.platform !== "win32") return;
|
|
5665
5743
|
addLongpathsRow(
|
|
5666
5744
|
section2,
|
|
@@ -5684,11 +5762,11 @@ function reportSyncModality(section2) {
|
|
|
5684
5762
|
|
|
5685
5763
|
// src/commands.doctor.checks.crlf.ts
|
|
5686
5764
|
init_color();
|
|
5687
|
-
import { execFileSync as
|
|
5765
|
+
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
5688
5766
|
import { existsSync as existsSync28, readFileSync as readFileSync13 } from "node:fs";
|
|
5689
5767
|
import { join as join39 } from "node:path";
|
|
5690
5768
|
init_config();
|
|
5691
|
-
var
|
|
5769
|
+
var PROBE_TIMEOUT_MS3 = 3e3;
|
|
5692
5770
|
var GUARD_LINE = /^\*\s+-text\b/;
|
|
5693
5771
|
function hasGitattributesGuard(repo) {
|
|
5694
5772
|
try {
|
|
@@ -5704,7 +5782,7 @@ function probeAutocrlf(repo, run) {
|
|
|
5704
5782
|
try {
|
|
5705
5783
|
const out = run("git", ["-C", repo, "config", "core.autocrlf"], {
|
|
5706
5784
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5707
|
-
timeout:
|
|
5785
|
+
timeout: PROBE_TIMEOUT_MS3
|
|
5708
5786
|
}).toString().trim();
|
|
5709
5787
|
if (out === "true" || out === "input") return "active";
|
|
5710
5788
|
if (out === "false") return "disabled";
|
|
@@ -5723,7 +5801,7 @@ function addExposedRow(section2, repo, verdict) {
|
|
|
5723
5801
|
const remediation = `add a .gitattributes with a \`* -text\` line, or run \`git config core.autocrlf false\`, in ${repo}`;
|
|
5724
5802
|
addItem(section2, `${yellow(warnGlyph)} CRLF guard: ${risk}; ${remediation}`);
|
|
5725
5803
|
}
|
|
5726
|
-
function reportCrlfGuardCheck(section2, run =
|
|
5804
|
+
function reportCrlfGuardCheck(section2, run = execFileSync14) {
|
|
5727
5805
|
const repo = repoHome();
|
|
5728
5806
|
if (hasGitattributesGuard(repo)) {
|
|
5729
5807
|
addItem(section2, `${green(okGlyph)} CRLF guard: .gitattributes (* -text) present`);
|
|
@@ -6148,10 +6226,10 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
|
|
|
6148
6226
|
|
|
6149
6227
|
// src/commands.pushed-history.ts
|
|
6150
6228
|
init_utils();
|
|
6151
|
-
import { execFileSync as
|
|
6229
|
+
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
6152
6230
|
function pushedRef(repo) {
|
|
6153
6231
|
try {
|
|
6154
|
-
const ref =
|
|
6232
|
+
const ref = execFileSync15("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
|
|
6155
6233
|
cwd: repo,
|
|
6156
6234
|
stdio: ["ignore", "pipe", "ignore"]
|
|
6157
6235
|
}).toString().trim();
|
|
@@ -6164,7 +6242,7 @@ function sessionInPushedHistory(id, repo) {
|
|
|
6164
6242
|
const ref = pushedRef(repo);
|
|
6165
6243
|
if (ref === null) return false;
|
|
6166
6244
|
try {
|
|
6167
|
-
const out =
|
|
6245
|
+
const out = execFileSync15(
|
|
6168
6246
|
"git",
|
|
6169
6247
|
[
|
|
6170
6248
|
"log",
|
|
@@ -6497,7 +6575,7 @@ var FRAGMENT_SEP = "...";
|
|
|
6497
6575
|
var NEAR_WINDOW = 40;
|
|
6498
6576
|
var VALUE_PAD = 16;
|
|
6499
6577
|
var SIBLING_PAD = 2;
|
|
6500
|
-
var
|
|
6578
|
+
var CONTROL_CHARS2 = /[\x00-\x1f\x7f]/g;
|
|
6501
6579
|
function classifyCharset(value) {
|
|
6502
6580
|
if (/^[0-9a-fA-F]+$/.test(value)) return "hex";
|
|
6503
6581
|
if (/^[0-9a-zA-Z]+$/.test(value)) return "alphanumeric";
|
|
@@ -6571,7 +6649,7 @@ function resolveSpanContext(finding, readLine, siblings = []) {
|
|
|
6571
6649
|
return { value: aligned.value, near: emptyToNull(near), exact: aligned.exact };
|
|
6572
6650
|
}
|
|
6573
6651
|
function formatNear(text) {
|
|
6574
|
-
const stripped = elideTokenRuns(text.replace(
|
|
6652
|
+
const stripped = elideTokenRuns(text.replace(CONTROL_CHARS2, ""));
|
|
6575
6653
|
const truncated = stripped.length > NEAR_WINDOW;
|
|
6576
6654
|
const window = stripped.slice(-NEAR_WINDOW).trim();
|
|
6577
6655
|
if (window.length === 0) return null;
|
|
@@ -7048,7 +7126,7 @@ function withSpinner(label, fn, deps) {
|
|
|
7048
7126
|
|
|
7049
7127
|
// src/commands.doctor.gitleaks-version.ts
|
|
7050
7128
|
init_color();
|
|
7051
|
-
import { execFileSync as
|
|
7129
|
+
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
7052
7130
|
import { existsSync as existsSync34 } from "node:fs";
|
|
7053
7131
|
import { join as join48 } from "node:path";
|
|
7054
7132
|
init_config();
|
|
@@ -7071,7 +7149,7 @@ function readGitleaksVersion(run, tomlExists) {
|
|
|
7071
7149
|
return null;
|
|
7072
7150
|
}
|
|
7073
7151
|
}
|
|
7074
|
-
function reportGitleaksVersionCheck(section2, run =
|
|
7152
|
+
function reportGitleaksVersionCheck(section2, run = execFileSync16, tomlExists = existsSync34) {
|
|
7075
7153
|
const raw = readGitleaksVersion(run, tomlExists);
|
|
7076
7154
|
if (raw === null) return;
|
|
7077
7155
|
const local = majorMinorOf(raw);
|
|
@@ -7091,9 +7169,9 @@ function reportGitleaksVersionCheck(section2, run = execFileSync15, tomlExists =
|
|
|
7091
7169
|
|
|
7092
7170
|
// src/commands.doctor.checks.deps.ts
|
|
7093
7171
|
init_color();
|
|
7094
|
-
import { execFileSync as
|
|
7172
|
+
import { execFileSync as execFileSync17 } from "node:child_process";
|
|
7095
7173
|
var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
|
|
7096
|
-
var
|
|
7174
|
+
var PROBE_TIMEOUT_MS4 = 3e3;
|
|
7097
7175
|
var FETCHER_BASE = "HTTP fetcher";
|
|
7098
7176
|
function parseFirstVersion(line) {
|
|
7099
7177
|
const m = VERSION_TOKEN.exec(line);
|
|
@@ -7103,7 +7181,7 @@ function probeOptionalDep(bin, run) {
|
|
|
7103
7181
|
try {
|
|
7104
7182
|
const firstLine = run(bin, ["--version"], {
|
|
7105
7183
|
stdio: ["ignore", "pipe", "pipe"],
|
|
7106
|
-
timeout:
|
|
7184
|
+
timeout: PROBE_TIMEOUT_MS4
|
|
7107
7185
|
}).toString().split("\n")[0].trim();
|
|
7108
7186
|
const version = parseFirstVersion(firstLine);
|
|
7109
7187
|
return { status: "present", version };
|
|
@@ -7128,7 +7206,7 @@ function reportFetcherRow(section2, run) {
|
|
|
7128
7206
|
);
|
|
7129
7207
|
}
|
|
7130
7208
|
}
|
|
7131
|
-
function reportOptionalDeps(section2, run =
|
|
7209
|
+
function reportOptionalDeps(section2, run = execFileSync17) {
|
|
7132
7210
|
const gh = probeOptionalDep("gh", run);
|
|
7133
7211
|
if (gh.status === "present") {
|
|
7134
7212
|
addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
|
|
@@ -7143,11 +7221,11 @@ function reportOptionalDeps(section2, run = execFileSync16) {
|
|
|
7143
7221
|
|
|
7144
7222
|
// src/commands.doctor.actions-drift.ts
|
|
7145
7223
|
init_color();
|
|
7146
|
-
import { execFileSync as
|
|
7224
|
+
import { execFileSync as execFileSync19 } from "node:child_process";
|
|
7147
7225
|
init_config();
|
|
7148
7226
|
|
|
7149
7227
|
// src/gh-actions.ts
|
|
7150
|
-
import { execFileSync as
|
|
7228
|
+
import { execFileSync as execFileSync18 } from "node:child_process";
|
|
7151
7229
|
var GH_TIMEOUT_MS = 5e3;
|
|
7152
7230
|
function parseGitHubRemote(remoteUrl) {
|
|
7153
7231
|
const normalized = remoteUrl.trim().replace(/\/$/, "");
|
|
@@ -7155,7 +7233,7 @@ function parseGitHubRemote(remoteUrl) {
|
|
|
7155
7233
|
if (m === null) return null;
|
|
7156
7234
|
return { owner: m[1], repo: m[2] };
|
|
7157
7235
|
}
|
|
7158
|
-
function ghAuthStatus(run =
|
|
7236
|
+
function ghAuthStatus(run = execFileSync18) {
|
|
7159
7237
|
try {
|
|
7160
7238
|
run("gh", ["auth", "status"], {
|
|
7161
7239
|
stdio: ["ignore", "ignore", "ignore"],
|
|
@@ -7169,7 +7247,7 @@ function ghAuthStatus(run = execFileSync17) {
|
|
|
7169
7247
|
return "gh-probe-error";
|
|
7170
7248
|
}
|
|
7171
7249
|
}
|
|
7172
|
-
function isRepoPrivate(ref, run =
|
|
7250
|
+
function isRepoPrivate(ref, run = execFileSync18) {
|
|
7173
7251
|
const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
|
|
7174
7252
|
stdio: ["ignore", "pipe", "ignore"],
|
|
7175
7253
|
timeout: GH_TIMEOUT_MS
|
|
@@ -7177,7 +7255,7 @@ function isRepoPrivate(ref, run = execFileSync17) {
|
|
|
7177
7255
|
const parsed = JSON.parse(out);
|
|
7178
7256
|
return parsed.isPrivate === true;
|
|
7179
7257
|
}
|
|
7180
|
-
function isActionsEnabled(ref, run =
|
|
7258
|
+
function isActionsEnabled(ref, run = execFileSync18) {
|
|
7181
7259
|
const out = run(
|
|
7182
7260
|
"gh",
|
|
7183
7261
|
["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
|
|
@@ -7185,7 +7263,7 @@ function isActionsEnabled(ref, run = execFileSync17) {
|
|
|
7185
7263
|
).toString().trim();
|
|
7186
7264
|
return out === "true";
|
|
7187
7265
|
}
|
|
7188
|
-
function disableActions(ref, run =
|
|
7266
|
+
function disableActions(ref, run = execFileSync18) {
|
|
7189
7267
|
run(
|
|
7190
7268
|
"gh",
|
|
7191
7269
|
[
|
|
@@ -7199,7 +7277,7 @@ function disableActions(ref, run = execFileSync17) {
|
|
|
7199
7277
|
{ stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
|
|
7200
7278
|
);
|
|
7201
7279
|
}
|
|
7202
|
-
function readOriginRemote(cwd, run =
|
|
7280
|
+
function readOriginRemote(cwd, run = execFileSync18) {
|
|
7203
7281
|
return run("git", ["remote", "get-url", "origin"], {
|
|
7204
7282
|
cwd,
|
|
7205
7283
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -7207,7 +7285,7 @@ function readOriginRemote(cwd, run = execFileSync17) {
|
|
|
7207
7285
|
}
|
|
7208
7286
|
|
|
7209
7287
|
// src/commands.doctor.actions-drift.ts
|
|
7210
|
-
function reportActionsDrift(section2, run =
|
|
7288
|
+
function reportActionsDrift(section2, run = execFileSync19) {
|
|
7211
7289
|
let remote;
|
|
7212
7290
|
try {
|
|
7213
7291
|
remote = readOriginRemote(repoHome(), run);
|
|
@@ -7347,6 +7425,7 @@ function gatherDoctorSections(opts) {
|
|
|
7347
7425
|
reportPathMap(pathMap);
|
|
7348
7426
|
const neverSync = section("Never-sync");
|
|
7349
7427
|
reportNeverSync(neverSync);
|
|
7428
|
+
reportTrackedDeniedShared(neverSync);
|
|
7350
7429
|
const skills = section("Skills");
|
|
7351
7430
|
reportSkillsDivergence(skills);
|
|
7352
7431
|
const repository = section("Repository");
|
|
@@ -7426,15 +7505,15 @@ function parseDoctorArgs(args) {
|
|
|
7426
7505
|
|
|
7427
7506
|
// src/commands.drop-session.ts
|
|
7428
7507
|
init_config();
|
|
7429
|
-
import { execFileSync as
|
|
7508
|
+
import { execFileSync as execFileSync21 } from "node:child_process";
|
|
7430
7509
|
import { existsSync as existsSync37, readdirSync as readdirSync16, statSync as statSync10 } from "node:fs";
|
|
7431
7510
|
import { join as join51, relative as relative7 } from "node:path";
|
|
7432
7511
|
|
|
7433
7512
|
// src/commands.drop-session.git.ts
|
|
7434
|
-
import { execFileSync as
|
|
7513
|
+
import { execFileSync as execFileSync20 } from "node:child_process";
|
|
7435
7514
|
function expandStagedDir(dirRel, repo) {
|
|
7436
7515
|
try {
|
|
7437
|
-
const out =
|
|
7516
|
+
const out = execFileSync20("git", ["ls-files", "-z", "--", dirRel], {
|
|
7438
7517
|
cwd: repo,
|
|
7439
7518
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7440
7519
|
});
|
|
@@ -7446,7 +7525,7 @@ function expandStagedDir(dirRel, repo) {
|
|
|
7446
7525
|
function isTrackedInHead(rel, repo) {
|
|
7447
7526
|
try {
|
|
7448
7527
|
const treePath = process.platform === "win32" ? rel.replaceAll("\\", "/") : rel;
|
|
7449
|
-
|
|
7528
|
+
execFileSync20("git", ["cat-file", "-e", `HEAD:${treePath}`], {
|
|
7450
7529
|
cwd: repo,
|
|
7451
7530
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7452
7531
|
});
|
|
@@ -7457,7 +7536,7 @@ function isTrackedInHead(rel, repo) {
|
|
|
7457
7536
|
}
|
|
7458
7537
|
function isInIndex(rel, repo) {
|
|
7459
7538
|
try {
|
|
7460
|
-
const out =
|
|
7539
|
+
const out = execFileSync20("git", ["ls-files", "--", rel], {
|
|
7461
7540
|
cwd: repo,
|
|
7462
7541
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7463
7542
|
});
|
|
@@ -7563,12 +7642,12 @@ function unstageOne(rel, repo) {
|
|
|
7563
7642
|
}
|
|
7564
7643
|
try {
|
|
7565
7644
|
if (isTrackedInHead(rel, repo)) {
|
|
7566
|
-
|
|
7645
|
+
execFileSync21("git", ["restore", "--staged", "--worktree", "--", rel], {
|
|
7567
7646
|
cwd: repo,
|
|
7568
7647
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7569
7648
|
});
|
|
7570
7649
|
} else {
|
|
7571
|
-
|
|
7650
|
+
execFileSync21("git", ["rm", "--cached", "-f", "--", rel], {
|
|
7572
7651
|
cwd: repo,
|
|
7573
7652
|
stdio: ["ignore", "pipe", "pipe"]
|
|
7574
7653
|
});
|
|
@@ -8115,9 +8194,9 @@ import { join as join60 } from "node:path";
|
|
|
8115
8194
|
|
|
8116
8195
|
// src/commands.pull.recovery.git.ts
|
|
8117
8196
|
init_utils();
|
|
8118
|
-
import { execFileSync as
|
|
8197
|
+
import { execFileSync as execFileSync22 } from "node:child_process";
|
|
8119
8198
|
function gitCapture(args, cwd) {
|
|
8120
|
-
return
|
|
8199
|
+
return execFileSync22("git", args, {
|
|
8121
8200
|
cwd,
|
|
8122
8201
|
stdio: ["ignore", "pipe", "pipe"],
|
|
8123
8202
|
maxBuffer: 64 * 1024 * 1024
|
|
@@ -8155,23 +8234,6 @@ function parseDirtyPaths(repo, opts = {}) {
|
|
|
8155
8234
|
// src/commands.pull.win32.ts
|
|
8156
8235
|
init_config();
|
|
8157
8236
|
|
|
8158
|
-
// src/git-probe.ts
|
|
8159
|
-
import { execFileSync as execFileSync22 } from "node:child_process";
|
|
8160
|
-
var PROBE_TIMEOUT_MS4 = 1e4;
|
|
8161
|
-
var PROBE_MAX_BUFFER = 64 * 1024 * 1024;
|
|
8162
|
-
function gitProbe(args, repo) {
|
|
8163
|
-
try {
|
|
8164
|
-
return execFileSync22("git", args, {
|
|
8165
|
-
cwd: repo,
|
|
8166
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
8167
|
-
timeout: PROBE_TIMEOUT_MS4,
|
|
8168
|
-
maxBuffer: PROBE_MAX_BUFFER
|
|
8169
|
-
}).toString();
|
|
8170
|
-
} catch {
|
|
8171
|
-
return null;
|
|
8172
|
-
}
|
|
8173
|
-
}
|
|
8174
|
-
|
|
8175
8237
|
// src/links.deletions.ts
|
|
8176
8238
|
init_config();
|
|
8177
8239
|
import { existsSync as existsSync40, lstatSync as lstatSync19, rmSync as rmSync17 } from "node:fs";
|
|
@@ -8235,6 +8297,7 @@ function applySharedLinkDeletions(map, ts, opts = {}) {
|
|
|
8235
8297
|
|
|
8236
8298
|
// src/links.mirror.ts
|
|
8237
8299
|
init_config();
|
|
8300
|
+
init_error_text();
|
|
8238
8301
|
import { existsSync as existsSync41, lstatSync as lstatSync20, rmSync as rmSync18, statSync as statSync12 } from "node:fs";
|
|
8239
8302
|
import { join as join57 } from "node:path";
|
|
8240
8303
|
init_utils();
|
|
@@ -8402,19 +8465,19 @@ function reportTrackedDenied(repo, path, segment) {
|
|
|
8402
8465
|
const denied = `the path segment "${segment}" is on the never-sync list`;
|
|
8403
8466
|
if (inHead === null) {
|
|
8404
8467
|
warn(
|
|
8405
|
-
`could not check ${path} against HEAD: ${denied}. Nothing was changed. Inspect it
|
|
8468
|
+
`could not check ${path} against HEAD: ${denied}. Nothing was changed. Inspect it by running git status -- "${path}" and take it out of shared/ before committing`
|
|
8406
8469
|
);
|
|
8407
8470
|
return;
|
|
8408
8471
|
}
|
|
8409
8472
|
if (inHead.trim() === "") {
|
|
8410
8473
|
warn(
|
|
8411
|
-
`${path} is staged and has no committed version: ${denied}. Nothing was changed. Run
|
|
8474
|
+
`${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, run git diff --cached --name-status to name the source, whose content IS committed, so restore that half with git checkout HEAD -- "<source>" rather than leaving its deletion staged`
|
|
8412
8475
|
);
|
|
8413
8476
|
return;
|
|
8414
8477
|
}
|
|
8415
8478
|
if (!existsSync41(join57(repo, path))) return;
|
|
8416
8479
|
warn(
|
|
8417
|
-
`${path} is tracked and has changes against HEAD: ${denied}. Nothing was changed. Run
|
|
8480
|
+
`${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`
|
|
8418
8481
|
);
|
|
8419
8482
|
}
|
|
8420
8483
|
function revertDeniedMirrorPaths(repo, status, ts) {
|
|
@@ -8891,13 +8954,30 @@ function newlyUntracked(before, after) {
|
|
|
8891
8954
|
if (before === null || after === null) return [];
|
|
8892
8955
|
return [...after].filter((p) => !before.has(p));
|
|
8893
8956
|
}
|
|
8957
|
+
function ignoredPathsUnderShared(repo) {
|
|
8958
|
+
const out = gitProbe(
|
|
8959
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignored", "--", "shared/"],
|
|
8960
|
+
repo
|
|
8961
|
+
);
|
|
8962
|
+
if (out === null) return [];
|
|
8963
|
+
return out.split("\0").filter((record) => record.startsWith("!! ")).map((record) => record.slice(3));
|
|
8964
|
+
}
|
|
8965
|
+
function reportIgnoredDenied(repo) {
|
|
8966
|
+
for (const path of new Set(ignoredPathsUnderShared(repo))) {
|
|
8967
|
+
const segment = deniedSegmentFor(path);
|
|
8968
|
+
if (segment === null) continue;
|
|
8969
|
+
warn(
|
|
8970
|
+
`${path} is inside shared/ but git ignores it: the path segment "${segment}" is on the never-sync list. Nothing was changed, and nothing published it: nomad only ever stages with "git add -A", which skips an ignored path, so it has not reached the index or the remote. It stays invisible to the rest of this gate for the same reason, so move it outside shared/ if you want it kept, or delete it. Note the ignore rule may come from your global core.excludesFile rather than from this repo; run git check-ignore -v -- "${path}" to see which file and line the rule came from`
|
|
8971
|
+
);
|
|
8972
|
+
}
|
|
8973
|
+
}
|
|
8894
8974
|
function revertDeniedUnderShared(repo, ts) {
|
|
8895
8975
|
const out = gitProbe(
|
|
8896
8976
|
["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", "shared/"],
|
|
8897
8977
|
repo
|
|
8898
8978
|
);
|
|
8899
|
-
if (out
|
|
8900
|
-
|
|
8979
|
+
if (out !== null) revertDeniedMirrorPaths(repo, parsePorcelainZ(out), ts);
|
|
8980
|
+
reportIgnoredDenied(repo);
|
|
8901
8981
|
}
|
|
8902
8982
|
function reconcileSharedLinksBeforePull(repo, ts) {
|
|
8903
8983
|
if (process.platform !== "win32") {
|
|
@@ -10899,7 +10979,7 @@ function parseSyncArgs(argv) {
|
|
|
10899
10979
|
// package.json
|
|
10900
10980
|
var package_default = {
|
|
10901
10981
|
name: "claude-nomad",
|
|
10902
|
-
version: "0.
|
|
10982
|
+
version: "0.68.0",
|
|
10903
10983
|
type: "module",
|
|
10904
10984
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
10905
10985
|
keywords: [
|
package/package.json
CHANGED