claude-nomad 0.39.0 → 0.41.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 +20 -0
- package/README.md +5 -0
- package/dist/nomad.mjs +350 -215
- package/dist/nomad.worker.mjs +26 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.41.0](https://github.com/funkadelic/claude-nomad/compare/v0.40.0...v0.41.0) (2026-06-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
* **diff:** render diff and pull --dry-run as a glyph-free tree ([#236](https://github.com/funkadelic/claude-nomad/issues/236)) ([6cf1aff](https://github.com/funkadelic/claude-nomad/commit/6cf1aff60f7ac5710c990fb303c6408c0b07ff66))
|
|
9
|
+
* **spinner:** animate remap sync, fix stray frames ([#234](https://github.com/funkadelic/claude-nomad/issues/234)) ([5a71563](https://github.com/funkadelic/claude-nomad/commit/5a715637c8ab43ab2d536353f55512abe15cfdd3))
|
|
10
|
+
|
|
11
|
+
## [0.40.0](https://github.com/funkadelic/claude-nomad/compare/v0.39.0...v0.40.0) (2026-06-03)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
* **spinner:** animate long-running operations ([#233](https://github.com/funkadelic/claude-nomad/issues/233)) ([e6755c3](https://github.com/funkadelic/claude-nomad/commit/e6755c3e5e86b52cbc5f996e9c5aee9d79da4574))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
* **docs-site:** build gate, link validation, and page metadata ([#231](https://github.com/funkadelic/claude-nomad/issues/231)) ([d4ad8ec](https://github.com/funkadelic/claude-nomad/commit/d4ad8ec0c414a91153a584156ccee2b32d824360))
|
|
22
|
+
|
|
3
23
|
## [0.39.0](https://github.com/funkadelic/claude-nomad/compare/v0.38.1...v0.39.0) (2026-06-02)
|
|
4
24
|
|
|
5
25
|
|
package/README.md
CHANGED
|
@@ -57,6 +57,11 @@ $ nomad pull # apply config to ~/.claude/
|
|
|
57
57
|
$ nomad push # publish local changes (sessions, settings)
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
During `nomad push` and `nomad pull`, long-running steps (rebase, secret scan, git push, session
|
|
61
|
+
sync) show an animated progress indicator on an interactive terminal so the CLI does not look hung.
|
|
62
|
+
In CI and when output is piped, only plain text lines are printed, with no ANSI control codes, so
|
|
63
|
+
log output remains grep-stable.
|
|
64
|
+
|
|
60
65
|
When `nomad push` detects a potential secret, it drops into an interactive menu (TTY) or aborts with
|
|
61
66
|
a recovery hint (non-TTY/CI). Three non-interactive recovery paths are available without the menu:
|
|
62
67
|
|
package/dist/nomad.mjs
CHANGED
|
@@ -170,13 +170,12 @@ function gitOrFatal(args, context, cwd) {
|
|
|
170
170
|
throw new NomadFatal(`${context} failed`);
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
-
var log,
|
|
173
|
+
var log, warn, fail, NomadFatal, die, gitStatusPorcelainZ;
|
|
174
174
|
var init_utils = __esm({
|
|
175
175
|
"src/utils.ts"() {
|
|
176
176
|
"use strict";
|
|
177
177
|
init_color();
|
|
178
178
|
log = (msg) => console.log(`${dim(infoGlyph)} ${msg}`);
|
|
179
|
-
ok = (msg) => console.log(`${green(okGlyph)} ${msg}`);
|
|
180
179
|
warn = (msg) => {
|
|
181
180
|
console.error(`${yellow(warnGlyph)} ${msg}`);
|
|
182
181
|
};
|
|
@@ -1225,8 +1224,8 @@ init_color();
|
|
|
1225
1224
|
// src/output-tree.ts
|
|
1226
1225
|
init_color();
|
|
1227
1226
|
var FAIL_GLYPH_BARE = "\u2717";
|
|
1228
|
-
function section(header) {
|
|
1229
|
-
return { header, items: [] };
|
|
1227
|
+
function section(header, raw = false) {
|
|
1228
|
+
return { header, items: [], raw };
|
|
1230
1229
|
}
|
|
1231
1230
|
function addItem(s, text) {
|
|
1232
1231
|
s.items.push(text);
|
|
@@ -1234,7 +1233,17 @@ function addItem(s, text) {
|
|
|
1234
1233
|
function sectionFailed(s) {
|
|
1235
1234
|
return s.items.some((line) => line.includes(failGlyph));
|
|
1236
1235
|
}
|
|
1236
|
+
function renderRawItems(items) {
|
|
1237
|
+
for (const item of items) {
|
|
1238
|
+
console.log(item === "" ? "" : ` ${item}`);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1237
1241
|
function renderSection(s) {
|
|
1242
|
+
if (s.raw) {
|
|
1243
|
+
console.log(s.header);
|
|
1244
|
+
renderRawItems(s.items);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1238
1247
|
const header = sectionFailed(s) ? `${red(FAIL_GLYPH_BARE)} ${s.header}` : s.header;
|
|
1239
1248
|
console.log(header);
|
|
1240
1249
|
const lastContent = s.items.reduce((acc, item, j) => item === "" ? acc : j, -1);
|
|
@@ -1845,6 +1854,10 @@ function copyDirJsonlOnly(src, dst) {
|
|
|
1845
1854
|
}
|
|
1846
1855
|
});
|
|
1847
1856
|
}
|
|
1857
|
+
function emitPreview(onPreview, event, fallback) {
|
|
1858
|
+
if (onPreview) onPreview(event);
|
|
1859
|
+
else log(fallback);
|
|
1860
|
+
}
|
|
1848
1861
|
function remapPull(ts, opts = {}) {
|
|
1849
1862
|
const dryRun = opts.dryRun === true;
|
|
1850
1863
|
let unmapped = 0;
|
|
@@ -1853,7 +1866,8 @@ function remapPull(ts, opts = {}) {
|
|
|
1853
1866
|
const mapPath = join17(REPO_HOME, "path-map.json");
|
|
1854
1867
|
const repoProjects = join17(REPO_HOME, "shared", "projects");
|
|
1855
1868
|
if (!existsSync13(mapPath) || !existsSync13(repoProjects)) {
|
|
1856
|
-
|
|
1869
|
+
const text = "no path-map or repo projects dir; skipping session remap";
|
|
1870
|
+
emitPreview(opts.onPreview, { kind: "note", text }, text);
|
|
1857
1871
|
return { unmapped: 0, pulled, wouldPull };
|
|
1858
1872
|
}
|
|
1859
1873
|
const map = readJson(mapPath);
|
|
@@ -1871,7 +1885,11 @@ function remapPull(ts, opts = {}) {
|
|
|
1871
1885
|
const dst = join17(localProjects, encodePath(localPath));
|
|
1872
1886
|
if (dryRun) {
|
|
1873
1887
|
wouldPull.push(logical);
|
|
1874
|
-
|
|
1888
|
+
emitPreview(
|
|
1889
|
+
opts.onPreview,
|
|
1890
|
+
{ kind: "overwrite", dst, src },
|
|
1891
|
+
`would overwrite: ${dst} (from ${src})`
|
|
1892
|
+
);
|
|
1875
1893
|
continue;
|
|
1876
1894
|
}
|
|
1877
1895
|
backupBeforeWrite(dst, ts);
|
|
@@ -2956,8 +2974,8 @@ ${lines}`);
|
|
|
2956
2974
|
}
|
|
2957
2975
|
|
|
2958
2976
|
// src/commands.pull.ts
|
|
2959
|
-
import { existsSync as
|
|
2960
|
-
import { join as
|
|
2977
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync8 } from "node:fs";
|
|
2978
|
+
import { join as join36 } from "node:path";
|
|
2961
2979
|
|
|
2962
2980
|
// src/commands.push.sections.ts
|
|
2963
2981
|
init_color();
|
|
@@ -2986,14 +3004,6 @@ function summaryRow(verb, unmapped, collisions = 0, extrasSkipped = 0) {
|
|
|
2986
3004
|
const { text, clean } = summaryText(verb, unmapped, collisions, extrasSkipped);
|
|
2987
3005
|
return clean ? `${green(okGlyph)} ${text}` : `${yellow(warnGlyph)} ${text}`;
|
|
2988
3006
|
}
|
|
2989
|
-
function emitSummary(verb, unmapped, collisions = 0, extrasSkipped = 0) {
|
|
2990
|
-
const { text, clean } = summaryText(verb, unmapped, collisions, extrasSkipped);
|
|
2991
|
-
if (clean) {
|
|
2992
|
-
ok(text);
|
|
2993
|
-
return;
|
|
2994
|
-
}
|
|
2995
|
-
warn(text);
|
|
2996
|
-
}
|
|
2997
3007
|
|
|
2998
3008
|
// src/commands.push.sections.ts
|
|
2999
3009
|
function collapsedSkipRow(n, noun) {
|
|
@@ -3236,6 +3246,20 @@ init_utils_fs();
|
|
|
3236
3246
|
init_utils_json();
|
|
3237
3247
|
import { existsSync as existsSync26, lstatSync as lstatSync7, rmSync as rmSync8 } from "node:fs";
|
|
3238
3248
|
import { join as join31 } from "node:path";
|
|
3249
|
+
function emitAutoMove(onPreview, linkPath, ts, name) {
|
|
3250
|
+
if (onPreview) {
|
|
3251
|
+
onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
|
|
3252
|
+
} else {
|
|
3253
|
+
log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
|
|
3254
|
+
}
|
|
3255
|
+
}
|
|
3256
|
+
function emitCreate(onPreview, from, to) {
|
|
3257
|
+
if (onPreview) {
|
|
3258
|
+
onPreview({ kind: "create", from, to });
|
|
3259
|
+
} else {
|
|
3260
|
+
log(`would create symlink: ${from} -> ${to}`);
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3239
3263
|
function applySharedLinks(ts, map, opts = {}) {
|
|
3240
3264
|
const dryRun = opts.dryRun === true;
|
|
3241
3265
|
const linkNames = allSharedLinks(map);
|
|
@@ -3246,7 +3270,7 @@ function applySharedLinks(ts, map, opts = {}) {
|
|
|
3246
3270
|
if (lstatSync7(linkPath).isSymbolicLink()) continue;
|
|
3247
3271
|
if (!existsSync26(target)) continue;
|
|
3248
3272
|
if (dryRun) {
|
|
3249
|
-
|
|
3273
|
+
emitAutoMove(opts.onPreview, linkPath, ts, name);
|
|
3250
3274
|
continue;
|
|
3251
3275
|
}
|
|
3252
3276
|
backupBeforeWrite(linkPath, ts);
|
|
@@ -3256,7 +3280,7 @@ function applySharedLinks(ts, map, opts = {}) {
|
|
|
3256
3280
|
const target = join31(REPO_HOME, "shared", name);
|
|
3257
3281
|
if (!existsSync26(target)) continue;
|
|
3258
3282
|
if (dryRun) {
|
|
3259
|
-
|
|
3283
|
+
emitCreate(opts.onPreview, join31(CLAUDE_HOME, name), target);
|
|
3260
3284
|
continue;
|
|
3261
3285
|
}
|
|
3262
3286
|
ensureSymlink(join31(CLAUDE_HOME, name), target);
|
|
@@ -3575,7 +3599,6 @@ function diffLinesToUnified(oldStr, newStr) {
|
|
|
3575
3599
|
}
|
|
3576
3600
|
|
|
3577
3601
|
// src/preview.ts
|
|
3578
|
-
init_utils();
|
|
3579
3602
|
init_utils_json();
|
|
3580
3603
|
function diffJsonStrings(currentJsonText, newJsonText) {
|
|
3581
3604
|
if (currentJsonText === newJsonText) return "";
|
|
@@ -3597,192 +3620,84 @@ function readJsonOrNull(path) {
|
|
|
3597
3620
|
function previewSettings(basePath, hostPath, settingsPath) {
|
|
3598
3621
|
const base = readJsonOrNull(basePath);
|
|
3599
3622
|
if (base === null) {
|
|
3600
|
-
|
|
3601
|
-
return;
|
|
3623
|
+
return { diff: "", notes: ["section skipped (base or current missing)"] };
|
|
3602
3624
|
}
|
|
3625
|
+
const notes = [];
|
|
3603
3626
|
const hostOverrides = readJsonOrNull(hostPath);
|
|
3604
3627
|
if (hostOverrides === null && existsSync27(hostPath)) {
|
|
3605
|
-
|
|
3628
|
+
notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
|
|
3606
3629
|
}
|
|
3607
3630
|
const merged = deepMerge(base, hostOverrides ?? {});
|
|
3608
3631
|
const current = readJsonOrNull(settingsPath);
|
|
3609
3632
|
if (current === null && existsSync27(settingsPath)) {
|
|
3610
|
-
|
|
3611
|
-
return;
|
|
3633
|
+
return { diff: "", notes: [...notes, "malformed; skipping diff"] };
|
|
3612
3634
|
}
|
|
3613
3635
|
const diff = diffJsonStrings(
|
|
3614
3636
|
JSON.stringify(current ?? {}, null, 2),
|
|
3615
3637
|
JSON.stringify(merged, null, 2)
|
|
3616
3638
|
);
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3639
|
+
return { diff, notes };
|
|
3640
|
+
}
|
|
3641
|
+
function formatLinkRow(e) {
|
|
3642
|
+
return `${e.kind} ${e.from} -> ${e.to}`;
|
|
3643
|
+
}
|
|
3644
|
+
function formatSessionRow(e) {
|
|
3645
|
+
return e.kind === "overwrite" ? `overwrite ${e.dst} (from ${e.src})` : e.text;
|
|
3646
|
+
}
|
|
3647
|
+
function buildSettingsSectionForPreview(result) {
|
|
3648
|
+
const s = section("settings.json", true);
|
|
3649
|
+
if (result.diff !== "") {
|
|
3650
|
+
for (const line of result.diff.split("\n")) {
|
|
3651
|
+
addItem(s, line);
|
|
3652
|
+
}
|
|
3622
3653
|
}
|
|
3654
|
+
for (const note of result.notes) {
|
|
3655
|
+
addItem(s, `note: ${note}`);
|
|
3656
|
+
}
|
|
3657
|
+
return s;
|
|
3623
3658
|
}
|
|
3624
|
-
function computePreview(ts, map) {
|
|
3625
|
-
log(`would pull on host=${HOST} (dry-run; no mutation)`);
|
|
3626
|
-
|
|
3627
|
-
|
|
3659
|
+
function computePreview(ts, map, verb = "pull") {
|
|
3660
|
+
console.log(`would pull on host=${HOST} (dry-run; no mutation)`);
|
|
3661
|
+
console.log("");
|
|
3662
|
+
const links = section("Symlinks");
|
|
3663
|
+
applySharedLinks(ts, map, {
|
|
3664
|
+
dryRun: true,
|
|
3665
|
+
onPreview: (e) => addItem(links, formatLinkRow(e))
|
|
3666
|
+
});
|
|
3667
|
+
const settingsResult = previewSettings(
|
|
3628
3668
|
join32(REPO_HOME, "shared", "settings.base.json"),
|
|
3629
3669
|
join32(REPO_HOME, "hosts", `${HOST}.json`),
|
|
3630
3670
|
join32(CLAUDE_HOME, "settings.json")
|
|
3631
3671
|
);
|
|
3632
|
-
const
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
init_utils_fs();
|
|
3639
|
-
init_utils_json();
|
|
3640
|
-
function applyWetPull(ts, map) {
|
|
3641
|
-
applySharedLinks(ts, map);
|
|
3642
|
-
const { label } = regenerateSettings(ts);
|
|
3643
|
-
const remapResult = remapPull(ts);
|
|
3644
|
-
const extrasResult = remapExtrasPull(ts);
|
|
3672
|
+
const settingsSection = buildSettingsSectionForPreview(settingsResult);
|
|
3673
|
+
const sessions = section("Sessions");
|
|
3674
|
+
const remapResult = remapPull(ts, {
|
|
3675
|
+
dryRun: true,
|
|
3676
|
+
onPreview: (e) => addItem(sessions, formatSessionRow(e))
|
|
3677
|
+
});
|
|
3645
3678
|
const summary = section("Summary");
|
|
3646
|
-
addItem(
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
);
|
|
3650
|
-
renderTree([
|
|
3651
|
-
buildSettingsSection(label),
|
|
3652
|
-
buildSessionsSection(remapResult.pulled, remapResult.unmapped),
|
|
3653
|
-
buildExtrasSection(extrasResult.pulled, extrasResult.skipped),
|
|
3654
|
-
summary
|
|
3655
|
-
]);
|
|
3656
|
-
}
|
|
3657
|
-
function cmdPull(opts = {}) {
|
|
3658
|
-
const dryRun = opts.dryRun === true;
|
|
3659
|
-
if (!existsSync28(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
3660
|
-
if (!existsSync28(join33(REPO_HOME, "shared", "settings.base.json"))) {
|
|
3661
|
-
die("repo not initialized; run 'nomad init' to scaffold");
|
|
3662
|
-
}
|
|
3663
|
-
const handle = acquireLock("pull");
|
|
3664
|
-
if (handle === null) process.exit(0);
|
|
3665
|
-
try {
|
|
3666
|
-
const ts = freshBackupTs(BACKUP_BASE);
|
|
3667
|
-
if (!dryRun) {
|
|
3668
|
-
const backupRoot = join33(BACKUP_BASE, ts);
|
|
3669
|
-
try {
|
|
3670
|
-
mkdirSync7(backupRoot, { recursive: true });
|
|
3671
|
-
} catch (err) {
|
|
3672
|
-
die(`could not create backup dir: ${err.message}`);
|
|
3673
|
-
}
|
|
3674
|
-
}
|
|
3675
|
-
log(
|
|
3676
|
-
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
3677
|
-
);
|
|
3678
|
-
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", REPO_HOME);
|
|
3679
|
-
const mapPath = join33(REPO_HOME, "path-map.json");
|
|
3680
|
-
const map = existsSync28(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
3681
|
-
divergenceCheckExtras(ts);
|
|
3682
|
-
if (dryRun) {
|
|
3683
|
-
const previewResult = computePreview(ts, map);
|
|
3684
|
-
log("dry-run complete; no mutation");
|
|
3685
|
-
emitSummary("pull", previewResult.unmapped);
|
|
3686
|
-
} else {
|
|
3687
|
-
applyWetPull(ts, map);
|
|
3688
|
-
}
|
|
3689
|
-
} catch (err) {
|
|
3690
|
-
if (err instanceof NomadFatal) {
|
|
3691
|
-
fail(err.message);
|
|
3692
|
-
process.exitCode = 1;
|
|
3693
|
-
} else {
|
|
3694
|
-
throw err;
|
|
3695
|
-
}
|
|
3696
|
-
} finally {
|
|
3697
|
-
releaseLock(handle);
|
|
3698
|
-
}
|
|
3679
|
+
addItem(summary, summaryRow(verb, remapResult.unmapped));
|
|
3680
|
+
renderTree([links, settingsSection, sessions, summary]);
|
|
3681
|
+
return { unmapped: remapResult.unmapped, collisions: 0 };
|
|
3699
3682
|
}
|
|
3700
3683
|
|
|
3701
|
-
// src/
|
|
3702
|
-
|
|
3703
|
-
import { existsSync as
|
|
3704
|
-
import {
|
|
3705
|
-
|
|
3706
|
-
// src/commands.push.allowlist.ts
|
|
3707
|
-
init_config();
|
|
3708
|
-
init_config_sharedDirs_guard();
|
|
3709
|
-
init_utils();
|
|
3710
|
-
function isAllowed(path, allowed) {
|
|
3711
|
-
for (const entry of allowed) {
|
|
3712
|
-
if (path === entry) return true;
|
|
3713
|
-
if (entry === "hosts/") {
|
|
3714
|
-
if (/^hosts\/[^/]+\.json$/.test(path)) return true;
|
|
3715
|
-
continue;
|
|
3716
|
-
}
|
|
3717
|
-
if (entry.endsWith("/") && path.startsWith(entry)) return true;
|
|
3718
|
-
}
|
|
3719
|
-
return false;
|
|
3720
|
-
}
|
|
3721
|
-
function isNeverSync(path) {
|
|
3722
|
-
const blockSet = path.startsWith("shared/extras/") ? ALWAYS_NEVER_SYNC : NEVER_SYNC;
|
|
3723
|
-
for (const segment of path.split("/")) {
|
|
3724
|
-
if (blockSet.has(segment)) return true;
|
|
3725
|
-
}
|
|
3726
|
-
return false;
|
|
3727
|
-
}
|
|
3728
|
-
function parsePorcelainZ(statusPorcelain) {
|
|
3729
|
-
const records = statusPorcelain.split("\0");
|
|
3730
|
-
const paths = [];
|
|
3731
|
-
for (let i = 0; i < records.length; i++) {
|
|
3732
|
-
const rec = records[i];
|
|
3733
|
-
if (rec === void 0 || rec === "") continue;
|
|
3734
|
-
if (rec.length < 4) continue;
|
|
3735
|
-
const xy = rec.slice(0, 2);
|
|
3736
|
-
const newPath = rec.slice(3);
|
|
3737
|
-
paths.push(newPath);
|
|
3738
|
-
if (/[RC]/.test(xy)) {
|
|
3739
|
-
const oldPath = records[i + 1];
|
|
3740
|
-
if (oldPath !== void 0 && oldPath !== "") paths.push(oldPath);
|
|
3741
|
-
i++;
|
|
3742
|
-
}
|
|
3743
|
-
}
|
|
3744
|
-
return paths;
|
|
3745
|
-
}
|
|
3746
|
-
function enforceAllowList(statusPorcelain, map) {
|
|
3747
|
-
const extrasWhitelist = SUPPORTED_EXTRAS;
|
|
3748
|
-
const allowed = [
|
|
3749
|
-
...PUSH_ALLOWED_STATIC,
|
|
3750
|
-
...Object.keys(map.projects).map((l) => `shared/projects/${l}/`),
|
|
3751
|
-
...Object.entries(map.extras ?? {}).flatMap(
|
|
3752
|
-
([l, names]) => names.filter((n) => extrasWhitelist.includes(n)).flatMap((n) => [`shared/extras/${l}/${n}`, `shared/extras/${l}/${n}/`])
|
|
3753
|
-
),
|
|
3754
|
-
...(map.sharedDirs ?? []).filter((d) => isValidSharedDir(d)).map((d) => `shared/${d}/`)
|
|
3755
|
-
];
|
|
3756
|
-
const neverSyncHits = [];
|
|
3757
|
-
const violations = [];
|
|
3758
|
-
for (const path of parsePorcelainZ(statusPorcelain)) {
|
|
3759
|
-
if (isNeverSync(path)) {
|
|
3760
|
-
neverSyncHits.push(path);
|
|
3761
|
-
} else if (!isAllowed(path, allowed)) {
|
|
3762
|
-
violations.push(path);
|
|
3763
|
-
}
|
|
3764
|
-
}
|
|
3765
|
-
if (neverSyncHits.length === 0 && violations.length === 0) return;
|
|
3766
|
-
for (const p of neverSyncHits) {
|
|
3767
|
-
fail(`${p} is in NEVER_SYNC and must never be pushed`);
|
|
3768
|
-
}
|
|
3769
|
-
for (const p of violations) {
|
|
3770
|
-
fail(`to sync ${p}, add to PUSH_ALLOWED in src/config.ts`);
|
|
3771
|
-
}
|
|
3772
|
-
throw new NomadFatal("push allow-list violations");
|
|
3773
|
-
}
|
|
3684
|
+
// src/spinner.ts
|
|
3685
|
+
init_color();
|
|
3686
|
+
import { existsSync as existsSync29 } from "node:fs";
|
|
3687
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3688
|
+
import { Worker } from "node:worker_threads";
|
|
3774
3689
|
|
|
3775
3690
|
// src/commands.push.recovery.ts
|
|
3776
3691
|
init_config();
|
|
3777
3692
|
import { readFileSync as readFileSync10, rmSync as rmSync10, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3778
|
-
import { join as
|
|
3693
|
+
import { join as join35 } from "node:path";
|
|
3779
3694
|
import { createInterface } from "node:readline/promises";
|
|
3780
3695
|
|
|
3781
3696
|
// src/commands.push.recovery.redact.ts
|
|
3782
3697
|
init_config();
|
|
3783
3698
|
init_config_sharedDirs_guard();
|
|
3784
|
-
import { cpSync as cpSync5, existsSync as
|
|
3785
|
-
import { dirname as dirname5, join as
|
|
3699
|
+
import { cpSync as cpSync5, existsSync as existsSync28, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
|
|
3700
|
+
import { dirname as dirname5, join as join33, sep as sep2 } from "node:path";
|
|
3786
3701
|
init_push_gitleaks_scan();
|
|
3787
3702
|
init_utils_json();
|
|
3788
3703
|
init_utils();
|
|
@@ -3813,8 +3728,8 @@ function resolveStagedDir(localPath, map) {
|
|
|
3813
3728
|
assertSafeLogical(logical);
|
|
3814
3729
|
const abs = hostMap[HOST];
|
|
3815
3730
|
if (abs === void 0) continue;
|
|
3816
|
-
if (localPath.startsWith(
|
|
3817
|
-
return
|
|
3731
|
+
if (localPath.startsWith(join33(CLAUDE_HOME, "projects", encodePath(abs)) + sep2)) {
|
|
3732
|
+
return join33(REPO_HOME, "shared", "projects", logical);
|
|
3818
3733
|
}
|
|
3819
3734
|
}
|
|
3820
3735
|
return null;
|
|
@@ -3836,7 +3751,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
3836
3751
|
`could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
3837
3752
|
);
|
|
3838
3753
|
}
|
|
3839
|
-
const sessionDir =
|
|
3754
|
+
const sessionDir = join33(dirname5(localPath), sid);
|
|
3840
3755
|
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
3841
3756
|
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
|
|
3842
3757
|
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
@@ -3869,10 +3784,10 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
3869
3784
|
`nothing to redact in the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
3870
3785
|
);
|
|
3871
3786
|
}
|
|
3872
|
-
|
|
3873
|
-
cpSync5(localPath,
|
|
3874
|
-
if (
|
|
3875
|
-
cpSync5(sessionDir,
|
|
3787
|
+
mkdirSync7(stagedProjectDir, { recursive: true });
|
|
3788
|
+
cpSync5(localPath, join33(stagedProjectDir, `${sid}.jsonl`), { force: true });
|
|
3789
|
+
if (existsSync28(sessionDir)) {
|
|
3790
|
+
cpSync5(sessionDir, join33(stagedProjectDir, sid), { force: true, recursive: true });
|
|
3876
3791
|
}
|
|
3877
3792
|
return true;
|
|
3878
3793
|
}
|
|
@@ -3880,13 +3795,13 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
3880
3795
|
// src/commands.push.recovery.drop.ts
|
|
3881
3796
|
init_config();
|
|
3882
3797
|
import { rmSync as rmSync9 } from "node:fs";
|
|
3883
|
-
import { join as
|
|
3798
|
+
import { join as join34 } from "node:path";
|
|
3884
3799
|
function dropSessionFromStaged(sid, map) {
|
|
3885
3800
|
const logicals = Object.keys(map.projects);
|
|
3886
3801
|
if (logicals.length === 0) return false;
|
|
3887
3802
|
for (const logical of logicals) {
|
|
3888
|
-
const jsonl =
|
|
3889
|
-
const dir =
|
|
3803
|
+
const jsonl = join34(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`);
|
|
3804
|
+
const dir = join34(REPO_HOME, "shared", "projects", logical, sid);
|
|
3890
3805
|
rmSync9(jsonl, { force: true });
|
|
3891
3806
|
rmSync9(dir, { recursive: true, force: true });
|
|
3892
3807
|
}
|
|
@@ -4004,7 +3919,7 @@ function applyThenRescan(scanVerdict, repoHome) {
|
|
|
4004
3919
|
return next;
|
|
4005
3920
|
}
|
|
4006
3921
|
function allowThenRescan(append, scanVerdict, repoHome) {
|
|
4007
|
-
const ignPath =
|
|
3922
|
+
const ignPath = join35(repoHome, ".gitleaksignore");
|
|
4008
3923
|
let before;
|
|
4009
3924
|
try {
|
|
4010
3925
|
before = readFileSync10(ignPath, "utf8");
|
|
@@ -4083,6 +3998,227 @@ async function resolveLeakFindings(verdict, ts, map, deps = {}) {
|
|
|
4083
3998
|
return current;
|
|
4084
3999
|
}
|
|
4085
4000
|
|
|
4001
|
+
// src/spinner.ts
|
|
4002
|
+
function formatElapsed(ms) {
|
|
4003
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
4004
|
+
}
|
|
4005
|
+
function writePlainStart(out, label) {
|
|
4006
|
+
out.write(`${label}...
|
|
4007
|
+
`);
|
|
4008
|
+
}
|
|
4009
|
+
function writePlainDone(out, label, ms) {
|
|
4010
|
+
out.write(`${label} done (${formatElapsed(ms)})
|
|
4011
|
+
`);
|
|
4012
|
+
}
|
|
4013
|
+
function writeAnimatedDone(out, label, ms, useTTY) {
|
|
4014
|
+
out.write("\r\x1B[K");
|
|
4015
|
+
const glyph = useTTY ? green(okGlyph) : okGlyph;
|
|
4016
|
+
out.write(`${glyph} ${label} (${formatElapsed(ms)})
|
|
4017
|
+
`);
|
|
4018
|
+
}
|
|
4019
|
+
function resolveWorkerPath(deps = {}) {
|
|
4020
|
+
const check = deps.existsSyncFn ?? existsSync29;
|
|
4021
|
+
const base = deps.baseUrl ?? import.meta.url;
|
|
4022
|
+
const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
|
|
4023
|
+
if (check(mjs)) return mjs;
|
|
4024
|
+
return fileURLToPath4(new URL("./spinner.worker.ts", base));
|
|
4025
|
+
}
|
|
4026
|
+
function makeRealWorker() {
|
|
4027
|
+
return new Worker(resolveWorkerPath());
|
|
4028
|
+
}
|
|
4029
|
+
function startSpinner(label, deps = {}) {
|
|
4030
|
+
const ttyCheck = deps.isTTYCheck ?? (() => isTTY());
|
|
4031
|
+
const env = deps.env ?? process.env;
|
|
4032
|
+
const out = deps.out ?? process.stderr;
|
|
4033
|
+
const now = deps.now ?? Date.now;
|
|
4034
|
+
const startMs = now();
|
|
4035
|
+
const animate = ttyCheck() && !env.CI;
|
|
4036
|
+
let worker = null;
|
|
4037
|
+
let degraded = false;
|
|
4038
|
+
let finalized = false;
|
|
4039
|
+
if (animate) {
|
|
4040
|
+
const factory = deps.makeWorker ?? makeRealWorker;
|
|
4041
|
+
try {
|
|
4042
|
+
worker = factory();
|
|
4043
|
+
worker.unref?.();
|
|
4044
|
+
worker.postMessage({ type: "start", label });
|
|
4045
|
+
} catch {
|
|
4046
|
+
degraded = true;
|
|
4047
|
+
worker = null;
|
|
4048
|
+
writePlainStart(out, label);
|
|
4049
|
+
}
|
|
4050
|
+
} else {
|
|
4051
|
+
writePlainStart(out, label);
|
|
4052
|
+
}
|
|
4053
|
+
function finalize(success, doneLabel) {
|
|
4054
|
+
if (finalized) return;
|
|
4055
|
+
finalized = true;
|
|
4056
|
+
const dl = doneLabel ?? label;
|
|
4057
|
+
const elapsed = now() - startMs;
|
|
4058
|
+
if (animate && !degraded && worker !== null) {
|
|
4059
|
+
worker.postMessage({ type: "pause" });
|
|
4060
|
+
worker.terminate();
|
|
4061
|
+
worker = null;
|
|
4062
|
+
if (success) writeAnimatedDone(out, dl, elapsed, ttyCheck());
|
|
4063
|
+
else out.write("\r\x1B[K");
|
|
4064
|
+
} else if (success) {
|
|
4065
|
+
writePlainDone(out, dl, elapsed);
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
return {
|
|
4069
|
+
succeed: (doneLabel) => finalize(true, doneLabel),
|
|
4070
|
+
stop: () => finalize(false)
|
|
4071
|
+
};
|
|
4072
|
+
}
|
|
4073
|
+
function withSpinner(label, fn, deps) {
|
|
4074
|
+
const sp = startSpinner(label, deps);
|
|
4075
|
+
try {
|
|
4076
|
+
const result = fn();
|
|
4077
|
+
sp.succeed();
|
|
4078
|
+
return result;
|
|
4079
|
+
} finally {
|
|
4080
|
+
sp.stop();
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
// src/commands.pull.ts
|
|
4085
|
+
init_utils();
|
|
4086
|
+
init_utils_fs();
|
|
4087
|
+
init_utils_json();
|
|
4088
|
+
function applyWetPull(ts, map) {
|
|
4089
|
+
applySharedLinks(ts, map);
|
|
4090
|
+
const { label } = regenerateSettings(ts);
|
|
4091
|
+
const remapResult = withSpinner("Syncing sessions", () => remapPull(ts));
|
|
4092
|
+
const extrasResult = remapExtrasPull(ts);
|
|
4093
|
+
const summary = section("Summary");
|
|
4094
|
+
addItem(
|
|
4095
|
+
summary,
|
|
4096
|
+
summaryRow("pull", remapResult.unmapped + extrasResult.unmapped, 0, extrasResult.skipped)
|
|
4097
|
+
);
|
|
4098
|
+
renderTree([
|
|
4099
|
+
buildSettingsSection(label),
|
|
4100
|
+
buildSessionsSection(remapResult.pulled, remapResult.unmapped),
|
|
4101
|
+
buildExtrasSection(extrasResult.pulled, extrasResult.skipped),
|
|
4102
|
+
summary
|
|
4103
|
+
]);
|
|
4104
|
+
}
|
|
4105
|
+
function cmdPull(opts = {}) {
|
|
4106
|
+
const dryRun = opts.dryRun === true;
|
|
4107
|
+
if (!existsSync30(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
4108
|
+
if (!existsSync30(join36(REPO_HOME, "shared", "settings.base.json"))) {
|
|
4109
|
+
die("repo not initialized; run 'nomad init' to scaffold");
|
|
4110
|
+
}
|
|
4111
|
+
const handle = acquireLock("pull");
|
|
4112
|
+
if (handle === null) process.exit(0);
|
|
4113
|
+
try {
|
|
4114
|
+
const ts = freshBackupTs(BACKUP_BASE);
|
|
4115
|
+
if (!dryRun) {
|
|
4116
|
+
const backupRoot = join36(BACKUP_BASE, ts);
|
|
4117
|
+
try {
|
|
4118
|
+
mkdirSync8(backupRoot, { recursive: true });
|
|
4119
|
+
} catch (err) {
|
|
4120
|
+
die(`could not create backup dir: ${err.message}`);
|
|
4121
|
+
}
|
|
4122
|
+
}
|
|
4123
|
+
log(
|
|
4124
|
+
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
4125
|
+
);
|
|
4126
|
+
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", REPO_HOME);
|
|
4127
|
+
const mapPath = join36(REPO_HOME, "path-map.json");
|
|
4128
|
+
const map = existsSync30(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
4129
|
+
divergenceCheckExtras(ts);
|
|
4130
|
+
if (dryRun) {
|
|
4131
|
+
computePreview(ts, map, "pull");
|
|
4132
|
+
log("dry-run complete; no mutation");
|
|
4133
|
+
} else {
|
|
4134
|
+
applyWetPull(ts, map);
|
|
4135
|
+
}
|
|
4136
|
+
} catch (err) {
|
|
4137
|
+
if (err instanceof NomadFatal) {
|
|
4138
|
+
fail(err.message);
|
|
4139
|
+
process.exitCode = 1;
|
|
4140
|
+
} else {
|
|
4141
|
+
throw err;
|
|
4142
|
+
}
|
|
4143
|
+
} finally {
|
|
4144
|
+
releaseLock(handle);
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
|
|
4148
|
+
// src/commands.push.ts
|
|
4149
|
+
init_config();
|
|
4150
|
+
import { existsSync as existsSync32 } from "node:fs";
|
|
4151
|
+
import { join as join38, relative as relative5 } from "node:path";
|
|
4152
|
+
|
|
4153
|
+
// src/commands.push.allowlist.ts
|
|
4154
|
+
init_config();
|
|
4155
|
+
init_config_sharedDirs_guard();
|
|
4156
|
+
init_utils();
|
|
4157
|
+
function isAllowed(path, allowed) {
|
|
4158
|
+
for (const entry of allowed) {
|
|
4159
|
+
if (path === entry) return true;
|
|
4160
|
+
if (entry === "hosts/") {
|
|
4161
|
+
if (/^hosts\/[^/]+\.json$/.test(path)) return true;
|
|
4162
|
+
continue;
|
|
4163
|
+
}
|
|
4164
|
+
if (entry.endsWith("/") && path.startsWith(entry)) return true;
|
|
4165
|
+
}
|
|
4166
|
+
return false;
|
|
4167
|
+
}
|
|
4168
|
+
function isNeverSync(path) {
|
|
4169
|
+
const blockSet = path.startsWith("shared/extras/") ? ALWAYS_NEVER_SYNC : NEVER_SYNC;
|
|
4170
|
+
for (const segment of path.split("/")) {
|
|
4171
|
+
if (blockSet.has(segment)) return true;
|
|
4172
|
+
}
|
|
4173
|
+
return false;
|
|
4174
|
+
}
|
|
4175
|
+
function parsePorcelainZ(statusPorcelain) {
|
|
4176
|
+
const records = statusPorcelain.split("\0");
|
|
4177
|
+
const paths = [];
|
|
4178
|
+
for (let i = 0; i < records.length; i++) {
|
|
4179
|
+
const rec = records[i];
|
|
4180
|
+
if (rec === void 0 || rec === "") continue;
|
|
4181
|
+
if (rec.length < 4) continue;
|
|
4182
|
+
const xy = rec.slice(0, 2);
|
|
4183
|
+
const newPath = rec.slice(3);
|
|
4184
|
+
paths.push(newPath);
|
|
4185
|
+
if (/[RC]/.test(xy)) {
|
|
4186
|
+
const oldPath = records[i + 1];
|
|
4187
|
+
if (oldPath !== void 0 && oldPath !== "") paths.push(oldPath);
|
|
4188
|
+
i++;
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
return paths;
|
|
4192
|
+
}
|
|
4193
|
+
function enforceAllowList(statusPorcelain, map) {
|
|
4194
|
+
const extrasWhitelist = SUPPORTED_EXTRAS;
|
|
4195
|
+
const allowed = [
|
|
4196
|
+
...PUSH_ALLOWED_STATIC,
|
|
4197
|
+
...Object.keys(map.projects).map((l) => `shared/projects/${l}/`),
|
|
4198
|
+
...Object.entries(map.extras ?? {}).flatMap(
|
|
4199
|
+
([l, names]) => names.filter((n) => extrasWhitelist.includes(n)).flatMap((n) => [`shared/extras/${l}/${n}`, `shared/extras/${l}/${n}/`])
|
|
4200
|
+
),
|
|
4201
|
+
...(map.sharedDirs ?? []).filter((d) => isValidSharedDir(d)).map((d) => `shared/${d}/`)
|
|
4202
|
+
];
|
|
4203
|
+
const neverSyncHits = [];
|
|
4204
|
+
const violations = [];
|
|
4205
|
+
for (const path of parsePorcelainZ(statusPorcelain)) {
|
|
4206
|
+
if (isNeverSync(path)) {
|
|
4207
|
+
neverSyncHits.push(path);
|
|
4208
|
+
} else if (!isAllowed(path, allowed)) {
|
|
4209
|
+
violations.push(path);
|
|
4210
|
+
}
|
|
4211
|
+
}
|
|
4212
|
+
if (neverSyncHits.length === 0 && violations.length === 0) return;
|
|
4213
|
+
for (const p of neverSyncHits) {
|
|
4214
|
+
fail(`${p} is in NEVER_SYNC and must never be pushed`);
|
|
4215
|
+
}
|
|
4216
|
+
for (const p of violations) {
|
|
4217
|
+
fail(`to sync ${p}, add to PUSH_ALLOWED in src/config.ts`);
|
|
4218
|
+
}
|
|
4219
|
+
throw new NomadFatal("push allow-list violations");
|
|
4220
|
+
}
|
|
4221
|
+
|
|
4086
4222
|
// src/commands.push.ts
|
|
4087
4223
|
init_push_leak_verdict();
|
|
4088
4224
|
init_push_checks();
|
|
@@ -4092,7 +4228,7 @@ init_color();
|
|
|
4092
4228
|
init_config();
|
|
4093
4229
|
init_config_sharedDirs_guard();
|
|
4094
4230
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
4095
|
-
import { copyFileSync, existsSync as
|
|
4231
|
+
import { copyFileSync, existsSync as existsSync31, mkdirSync as mkdirSync9, readdirSync as readdirSync10, rmSync as rmSync11 } from "node:fs";
|
|
4096
4232
|
import { homedir as homedir5 } from "node:os";
|
|
4097
4233
|
import { join as join37 } from "node:path";
|
|
4098
4234
|
init_push_leak_verdict();
|
|
@@ -4110,7 +4246,7 @@ function stageSessions(tmpRoot, map) {
|
|
|
4110
4246
|
reverse.set(encodePath(p), logical);
|
|
4111
4247
|
}
|
|
4112
4248
|
const localProjects = join37(CLAUDE_HOME, "projects");
|
|
4113
|
-
if (!
|
|
4249
|
+
if (!existsSync31(localProjects)) return 0;
|
|
4114
4250
|
let staged = 0;
|
|
4115
4251
|
for (const dir of readdirSync10(localProjects)) {
|
|
4116
4252
|
const logical = reverse.get(dir);
|
|
@@ -4132,7 +4268,7 @@ function stageExtras(tmpRoot, map) {
|
|
|
4132
4268
|
for (const dirname6 of dirnames) {
|
|
4133
4269
|
if (!whitelist.includes(dirname6)) continue;
|
|
4134
4270
|
const src = join37(localRoot, dirname6);
|
|
4135
|
-
if (!
|
|
4271
|
+
if (!existsSync31(src)) continue;
|
|
4136
4272
|
const dst = join37(tmpRoot, "shared", "extras", logical, dirname6);
|
|
4137
4273
|
copyExtras(src, dst);
|
|
4138
4274
|
staged++;
|
|
@@ -4152,7 +4288,7 @@ function previewPushLeaks(map) {
|
|
|
4152
4288
|
return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
|
|
4153
4289
|
}
|
|
4154
4290
|
const ignoreFile = join37(REPO_HOME, ".gitleaksignore");
|
|
4155
|
-
if (
|
|
4291
|
+
if (existsSync31(ignoreFile)) {
|
|
4156
4292
|
copyFileSync(ignoreFile, join37(tmpRoot, ".gitleaksignore"));
|
|
4157
4293
|
}
|
|
4158
4294
|
let findings;
|
|
@@ -4188,13 +4324,13 @@ function guardGitlinks() {
|
|
|
4188
4324
|
}
|
|
4189
4325
|
async function commitAndPush(st, ts, map, redactAll, allowAll, allowRule) {
|
|
4190
4326
|
gitOrFatal(["add", "-A"], "git add", REPO_HOME);
|
|
4191
|
-
let verdict = scanPushVerdict
|
|
4327
|
+
let verdict = withSpinner("Scanning for secrets", scanPushVerdict);
|
|
4192
4328
|
if (verdict.leak) {
|
|
4193
4329
|
renderPushTree(st, verdict);
|
|
4194
4330
|
verdict = await resolveLeakFindings(verdict, ts, map, { redactAll, allowAll, allowRule });
|
|
4195
4331
|
}
|
|
4196
4332
|
gitOrFatal(["commit", "-m", `chore: sync from ${HOST}`], "git commit", REPO_HOME);
|
|
4197
|
-
gitOrFatal(["push"], "git push", REPO_HOME);
|
|
4333
|
+
withSpinner("Pushing", () => gitOrFatal(["push"], "git push", REPO_HOME));
|
|
4198
4334
|
renderPushTree(st, verdict);
|
|
4199
4335
|
}
|
|
4200
4336
|
function runDryRunPreview(st, map) {
|
|
@@ -4227,16 +4363,16 @@ async function cmdPush(opts = {}) {
|
|
|
4227
4363
|
const allowAll = opts.allowAll === true;
|
|
4228
4364
|
const allowRule = opts.allowRule;
|
|
4229
4365
|
guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
|
|
4230
|
-
if (!
|
|
4366
|
+
if (!existsSync32(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
4231
4367
|
const handle = acquireLock("push");
|
|
4232
4368
|
if (handle === null) process.exit(0);
|
|
4233
4369
|
try {
|
|
4234
4370
|
console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
|
|
4235
4371
|
probeGitleaks();
|
|
4236
|
-
rebaseBeforePush
|
|
4372
|
+
withSpinner("Rebasing onto origin", rebaseBeforePush);
|
|
4237
4373
|
const ts = freshBackupTs(BACKUP_BASE);
|
|
4238
|
-
const remap = remapPush(ts, { dryRun });
|
|
4239
|
-
const extras = remapExtrasPush(ts, { dryRun });
|
|
4374
|
+
const remap = withSpinner("Syncing sessions", () => remapPush(ts, { dryRun }));
|
|
4375
|
+
const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
|
|
4240
4376
|
const st = { dryRun, remap, extras };
|
|
4241
4377
|
guardGitlinks();
|
|
4242
4378
|
const status = gitStatusPorcelainZ(REPO_HOME, { untrackedAll: true });
|
|
@@ -4246,7 +4382,7 @@ async function cmdPush(opts = {}) {
|
|
|
4246
4382
|
return;
|
|
4247
4383
|
}
|
|
4248
4384
|
const mapPath = join38(REPO_HOME, "path-map.json");
|
|
4249
|
-
if (!
|
|
4385
|
+
if (!existsSync32(mapPath)) {
|
|
4250
4386
|
if (dryRun) return runDryRunPreview(st, null);
|
|
4251
4387
|
die("path-map.json missing, cannot enforce push allow-list");
|
|
4252
4388
|
}
|
|
@@ -4286,19 +4422,18 @@ init_config();
|
|
|
4286
4422
|
|
|
4287
4423
|
// src/diff.ts
|
|
4288
4424
|
init_config();
|
|
4289
|
-
import { existsSync as
|
|
4425
|
+
import { existsSync as existsSync33 } from "node:fs";
|
|
4290
4426
|
import { join as join39 } from "node:path";
|
|
4291
4427
|
init_utils();
|
|
4292
4428
|
init_utils_fs();
|
|
4293
4429
|
init_utils_json();
|
|
4294
4430
|
function cmdDiff() {
|
|
4295
4431
|
try {
|
|
4296
|
-
if (!
|
|
4432
|
+
if (!existsSync33(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
4297
4433
|
const ts = freshBackupTs(BACKUP_BASE);
|
|
4298
4434
|
const mapPath = join39(REPO_HOME, "path-map.json");
|
|
4299
|
-
const map =
|
|
4300
|
-
|
|
4301
|
-
emitSummary("diff", result.unmapped);
|
|
4435
|
+
const map = existsSync33(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
4436
|
+
computePreview(ts, map, "diff");
|
|
4302
4437
|
} catch (err) {
|
|
4303
4438
|
if (err instanceof NomadFatal) {
|
|
4304
4439
|
fail(err.message);
|
|
@@ -4311,7 +4446,7 @@ function cmdDiff() {
|
|
|
4311
4446
|
|
|
4312
4447
|
// src/init.ts
|
|
4313
4448
|
init_config();
|
|
4314
|
-
import { existsSync as
|
|
4449
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4315
4450
|
import { join as join41 } from "node:path";
|
|
4316
4451
|
|
|
4317
4452
|
// src/init.gh-onboard.ts
|
|
@@ -4393,16 +4528,16 @@ init_config();
|
|
|
4393
4528
|
init_utils();
|
|
4394
4529
|
init_utils_fs();
|
|
4395
4530
|
init_utils_json();
|
|
4396
|
-
import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as
|
|
4531
|
+
import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as existsSync34, rmSync as rmSync12, statSync as statSync9 } from "node:fs";
|
|
4397
4532
|
import { join as join40 } from "node:path";
|
|
4398
4533
|
function snapshotIntoShared(map) {
|
|
4399
4534
|
for (const name of allSharedLinks(map)) {
|
|
4400
4535
|
const src = join40(CLAUDE_HOME, name);
|
|
4401
|
-
if (!
|
|
4536
|
+
if (!existsSync34(src)) continue;
|
|
4402
4537
|
const dst = join40(REPO_HOME, "shared", name);
|
|
4403
4538
|
if (statSync9(src).isDirectory()) {
|
|
4404
4539
|
const gk = join40(dst, ".gitkeep");
|
|
4405
|
-
if (
|
|
4540
|
+
if (existsSync34(gk)) rmSync12(gk);
|
|
4406
4541
|
cpSync6(src, dst, { recursive: true, force: false, errorOnExist: true });
|
|
4407
4542
|
} else {
|
|
4408
4543
|
copyFileSync2(src, dst);
|
|
@@ -4410,7 +4545,7 @@ function snapshotIntoShared(map) {
|
|
|
4410
4545
|
log(`snapshotted shared/${name} from ${src}`);
|
|
4411
4546
|
}
|
|
4412
4547
|
const userSettings = join40(CLAUDE_HOME, "settings.json");
|
|
4413
|
-
if (
|
|
4548
|
+
if (existsSync34(userSettings)) {
|
|
4414
4549
|
let parsed;
|
|
4415
4550
|
try {
|
|
4416
4551
|
parsed = readJson(userSettings);
|
|
@@ -4437,7 +4572,7 @@ function preflightConflict(repoHome) {
|
|
|
4437
4572
|
join41(repoHome, "shared")
|
|
4438
4573
|
];
|
|
4439
4574
|
for (const c of candidates) {
|
|
4440
|
-
if (
|
|
4575
|
+
if (existsSync35(c)) return c;
|
|
4441
4576
|
}
|
|
4442
4577
|
return null;
|
|
4443
4578
|
}
|
|
@@ -4456,7 +4591,7 @@ function cmdInit(opts = {}) {
|
|
|
4456
4591
|
mkdirSync10(join41(REPO_HOME, "shared", name), { recursive: true });
|
|
4457
4592
|
}
|
|
4458
4593
|
const userClaudeMd = join41(CLAUDE_HOME, "CLAUDE.md");
|
|
4459
|
-
if (!snapshot || !
|
|
4594
|
+
if (!snapshot || !existsSync35(userClaudeMd)) {
|
|
4460
4595
|
writeFileSync6(join41(REPO_HOME, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
|
|
4461
4596
|
log("created shared/CLAUDE.md");
|
|
4462
4597
|
}
|
|
@@ -4730,7 +4865,7 @@ function parsePushArgs(argv) {
|
|
|
4730
4865
|
// package.json
|
|
4731
4866
|
var package_default = {
|
|
4732
4867
|
name: "claude-nomad",
|
|
4733
|
-
version: "0.
|
|
4868
|
+
version: "0.41.0",
|
|
4734
4869
|
type: "module",
|
|
4735
4870
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
4736
4871
|
keywords: [
|
|
@@ -4743,7 +4878,7 @@ var package_default = {
|
|
|
4743
4878
|
type: "git",
|
|
4744
4879
|
url: "git+https://github.com/funkadelic/claude-nomad.git"
|
|
4745
4880
|
},
|
|
4746
|
-
homepage: "https://github.
|
|
4881
|
+
homepage: "https://funkadelic.github.io/claude-nomad/",
|
|
4747
4882
|
bugs: {
|
|
4748
4883
|
url: "https://github.com/funkadelic/claude-nomad/issues"
|
|
4749
4884
|
},
|
|
@@ -4920,7 +5055,7 @@ var DEFAULT_HELP = [
|
|
|
4920
5055
|
init_config();
|
|
4921
5056
|
init_utils();
|
|
4922
5057
|
init_utils_json();
|
|
4923
|
-
import { existsSync as
|
|
5058
|
+
import { existsSync as existsSync36, readFileSync as readFileSync11, readdirSync as readdirSync11 } from "node:fs";
|
|
4924
5059
|
import { join as join42 } from "node:path";
|
|
4925
5060
|
function resumeCmd(sessionId) {
|
|
4926
5061
|
if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
|
|
@@ -4928,7 +5063,7 @@ function resumeCmd(sessionId) {
|
|
|
4928
5063
|
process.exit(1);
|
|
4929
5064
|
}
|
|
4930
5065
|
const projectsRoot = join42(CLAUDE_HOME, "projects");
|
|
4931
|
-
if (!
|
|
5066
|
+
if (!existsSync36(projectsRoot)) {
|
|
4932
5067
|
fail(`${projectsRoot} does not exist`);
|
|
4933
5068
|
process.exit(1);
|
|
4934
5069
|
}
|
|
@@ -4943,7 +5078,7 @@ function resumeCmd(sessionId) {
|
|
|
4943
5078
|
process.exit(1);
|
|
4944
5079
|
}
|
|
4945
5080
|
const mapPath = join42(REPO_HOME, "path-map.json");
|
|
4946
|
-
if (!
|
|
5081
|
+
if (!existsSync36(mapPath)) {
|
|
4947
5082
|
fail("path-map.json missing");
|
|
4948
5083
|
process.exit(1);
|
|
4949
5084
|
}
|
|
@@ -4967,7 +5102,7 @@ function resumeCmd(sessionId) {
|
|
|
4967
5102
|
function findTranscriptPath(projectsRoot, sessionId) {
|
|
4968
5103
|
for (const dir of readdirSync11(projectsRoot)) {
|
|
4969
5104
|
const candidate = join42(projectsRoot, dir, `${sessionId}.jsonl`);
|
|
4970
|
-
if (
|
|
5105
|
+
if (existsSync36(candidate)) return candidate;
|
|
4971
5106
|
}
|
|
4972
5107
|
return null;
|
|
4973
5108
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/spinner.worker.ts
|
|
2
|
+
import { writeSync } from "node:fs";
|
|
3
|
+
import { parentPort } from "node:worker_threads";
|
|
4
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
5
|
+
var INTERVAL_MS = 80;
|
|
6
|
+
var STDERR_FD = 2;
|
|
7
|
+
var frame = 0;
|
|
8
|
+
var timer = null;
|
|
9
|
+
if (parentPort !== null) {
|
|
10
|
+
parentPort.on("message", (msg) => {
|
|
11
|
+
if (msg.type === "start" && msg.label !== void 0) {
|
|
12
|
+
const label = msg.label;
|
|
13
|
+
frame = 0;
|
|
14
|
+
if (timer !== null) clearInterval(timer);
|
|
15
|
+
timer = setInterval(() => {
|
|
16
|
+
writeSync(STDERR_FD, `${FRAMES[frame % FRAMES.length]} ${label}\r`);
|
|
17
|
+
frame++;
|
|
18
|
+
}, INTERVAL_MS);
|
|
19
|
+
} else if (msg.type === "pause") {
|
|
20
|
+
if (timer !== null) {
|
|
21
|
+
clearInterval(timer);
|
|
22
|
+
timer = null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-nomad",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.41.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": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"type": "git",
|
|
14
14
|
"url": "git+https://github.com/funkadelic/claude-nomad.git"
|
|
15
15
|
},
|
|
16
|
-
"homepage": "https://github.
|
|
16
|
+
"homepage": "https://funkadelic.github.io/claude-nomad/",
|
|
17
17
|
"bugs": {
|
|
18
18
|
"url": "https://github.com/funkadelic/claude-nomad/issues"
|
|
19
19
|
},
|