claude-nomad 0.38.1 → 0.40.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 +24 -0
- package/README.md +5 -0
- package/dist/nomad.mjs +302 -189
- package/dist/nomad.worker.mjs +24 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.40.0](https://github.com/funkadelic/claude-nomad/compare/v0.39.0...v0.40.0) (2026-06-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
* **spinner:** animate long-running operations ([#233](https://github.com/funkadelic/claude-nomad/issues/233)) ([e6755c3](https://github.com/funkadelic/claude-nomad/commit/e6755c3e5e86b52cbc5f996e9c5aee9d79da4574))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
* **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))
|
|
14
|
+
|
|
15
|
+
## [0.39.0](https://github.com/funkadelic/claude-nomad/compare/v0.38.1...v0.39.0) (2026-06-02)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
* **doctor:** group version checks into Nomad + Dependency sections ([#228](https://github.com/funkadelic/claude-nomad/issues/228)) ([08e8bf7](https://github.com/funkadelic/claude-nomad/commit/08e8bf79a730f1e7f025a1c2ad9dd406f586b05e))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Documentation
|
|
24
|
+
|
|
25
|
+
* **how-it-works:** render repo-layout trees with FileTree ([#229](https://github.com/funkadelic/claude-nomad/issues/229)) ([b52de15](https://github.com/funkadelic/claude-nomad/commit/b52de152416ac5dabb650a1970b53a3b45262396))
|
|
26
|
+
|
|
3
27
|
## [0.38.1](https://github.com/funkadelic/claude-nomad/compare/v0.38.0...v0.38.1) (2026-06-02)
|
|
4
28
|
|
|
5
29
|
|
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
|
@@ -2342,7 +2342,7 @@ init_color();
|
|
|
2342
2342
|
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
2343
2343
|
var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
|
|
2344
2344
|
var PROBE_TIMEOUT_MS = 3e3;
|
|
2345
|
-
var
|
|
2345
|
+
var FETCHER_BASE = "HTTP fetcher";
|
|
2346
2346
|
function parseFirstVersion(line) {
|
|
2347
2347
|
const m = VERSION_TOKEN.exec(line);
|
|
2348
2348
|
return m ? m[1] : null;
|
|
@@ -2366,13 +2366,13 @@ function reportFetcherRow(section2, run) {
|
|
|
2366
2366
|
const curl = probeOptionalDep("curl", run);
|
|
2367
2367
|
const wget = probeOptionalDep("wget", run);
|
|
2368
2368
|
if (curl.status === "present") {
|
|
2369
|
-
addItem(section2, `${green(okGlyph)} ${
|
|
2369
|
+
addItem(section2, `${green(okGlyph)} ${FETCHER_BASE}: curl ${curl.version ?? "(present)"}`);
|
|
2370
2370
|
} else if (wget.status === "present") {
|
|
2371
|
-
addItem(section2, `${green(okGlyph)} ${
|
|
2371
|
+
addItem(section2, `${green(okGlyph)} ${FETCHER_BASE}: wget ${wget.version ?? "(present)"}`);
|
|
2372
2372
|
} else {
|
|
2373
2373
|
addItem(
|
|
2374
2374
|
section2,
|
|
2375
|
-
`${yellow(warnGlyph)} ${
|
|
2375
|
+
`${yellow(warnGlyph)} ${FETCHER_BASE} (curl or wget): not installed (optional; needed for release-version staleness check + nomad doctor --check-schema)`
|
|
2376
2376
|
);
|
|
2377
2377
|
}
|
|
2378
2378
|
}
|
|
@@ -2513,18 +2513,20 @@ function cmdDoctor(opts = {}) {
|
|
|
2513
2513
|
reportRemote(repository);
|
|
2514
2514
|
reportRebaseClean(repository);
|
|
2515
2515
|
reportActionsDrift(repository);
|
|
2516
|
-
const
|
|
2517
|
-
reportVersionCheck(
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2516
|
+
const nomadVersion = section("Nomad Version");
|
|
2517
|
+
reportVersionCheck(nomadVersion);
|
|
2518
|
+
reportBackupsCheck(nomadVersion);
|
|
2519
|
+
const depVersions = section("Dependency Versions");
|
|
2520
|
+
reportNodeEngineCheck(depVersions);
|
|
2521
|
+
reportGitleaksVersionCheck(depVersions);
|
|
2522
|
+
reportOptionalDeps(depVersions);
|
|
2522
2523
|
const sharedScan = section("Shared scan");
|
|
2523
2524
|
if (opts.checkShared === true) reportCheckShared(sharedScan, gitleaksReady);
|
|
2524
2525
|
const schemaScan = section("Schema scan");
|
|
2525
2526
|
if (opts.checkSchema === true) reportCheckSchema(schemaScan);
|
|
2526
2527
|
renderDoctor([
|
|
2527
|
-
|
|
2528
|
+
nomadVersion,
|
|
2529
|
+
depVersions,
|
|
2528
2530
|
host,
|
|
2529
2531
|
links,
|
|
2530
2532
|
hooksScan,
|
|
@@ -2954,8 +2956,8 @@ ${lines}`);
|
|
|
2954
2956
|
}
|
|
2955
2957
|
|
|
2956
2958
|
// src/commands.pull.ts
|
|
2957
|
-
import { existsSync as
|
|
2958
|
-
import { join as
|
|
2959
|
+
import { existsSync as existsSync30, mkdirSync as mkdirSync8 } from "node:fs";
|
|
2960
|
+
import { join as join36 } from "node:path";
|
|
2959
2961
|
|
|
2960
2962
|
// src/commands.push.sections.ts
|
|
2961
2963
|
init_color();
|
|
@@ -3631,156 +3633,23 @@ function computePreview(ts, map) {
|
|
|
3631
3633
|
return { unmapped: remapResult.unmapped, collisions: 0 };
|
|
3632
3634
|
}
|
|
3633
3635
|
|
|
3634
|
-
// src/
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
applySharedLinks(ts, map);
|
|
3640
|
-
const { label } = regenerateSettings(ts);
|
|
3641
|
-
const remapResult = remapPull(ts);
|
|
3642
|
-
const extrasResult = remapExtrasPull(ts);
|
|
3643
|
-
const summary = section("Summary");
|
|
3644
|
-
addItem(
|
|
3645
|
-
summary,
|
|
3646
|
-
summaryRow("pull", remapResult.unmapped + extrasResult.unmapped, 0, extrasResult.skipped)
|
|
3647
|
-
);
|
|
3648
|
-
renderTree([
|
|
3649
|
-
buildSettingsSection(label),
|
|
3650
|
-
buildSessionsSection(remapResult.pulled, remapResult.unmapped),
|
|
3651
|
-
buildExtrasSection(extrasResult.pulled, extrasResult.skipped),
|
|
3652
|
-
summary
|
|
3653
|
-
]);
|
|
3654
|
-
}
|
|
3655
|
-
function cmdPull(opts = {}) {
|
|
3656
|
-
const dryRun = opts.dryRun === true;
|
|
3657
|
-
if (!existsSync28(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
3658
|
-
if (!existsSync28(join33(REPO_HOME, "shared", "settings.base.json"))) {
|
|
3659
|
-
die("repo not initialized; run 'nomad init' to scaffold");
|
|
3660
|
-
}
|
|
3661
|
-
const handle = acquireLock("pull");
|
|
3662
|
-
if (handle === null) process.exit(0);
|
|
3663
|
-
try {
|
|
3664
|
-
const ts = freshBackupTs(BACKUP_BASE);
|
|
3665
|
-
if (!dryRun) {
|
|
3666
|
-
const backupRoot = join33(BACKUP_BASE, ts);
|
|
3667
|
-
try {
|
|
3668
|
-
mkdirSync7(backupRoot, { recursive: true });
|
|
3669
|
-
} catch (err) {
|
|
3670
|
-
die(`could not create backup dir: ${err.message}`);
|
|
3671
|
-
}
|
|
3672
|
-
}
|
|
3673
|
-
log(
|
|
3674
|
-
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
3675
|
-
);
|
|
3676
|
-
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", REPO_HOME);
|
|
3677
|
-
const mapPath = join33(REPO_HOME, "path-map.json");
|
|
3678
|
-
const map = existsSync28(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
3679
|
-
divergenceCheckExtras(ts);
|
|
3680
|
-
if (dryRun) {
|
|
3681
|
-
const previewResult = computePreview(ts, map);
|
|
3682
|
-
log("dry-run complete; no mutation");
|
|
3683
|
-
emitSummary("pull", previewResult.unmapped);
|
|
3684
|
-
} else {
|
|
3685
|
-
applyWetPull(ts, map);
|
|
3686
|
-
}
|
|
3687
|
-
} catch (err) {
|
|
3688
|
-
if (err instanceof NomadFatal) {
|
|
3689
|
-
fail(err.message);
|
|
3690
|
-
process.exitCode = 1;
|
|
3691
|
-
} else {
|
|
3692
|
-
throw err;
|
|
3693
|
-
}
|
|
3694
|
-
} finally {
|
|
3695
|
-
releaseLock(handle);
|
|
3696
|
-
}
|
|
3697
|
-
}
|
|
3698
|
-
|
|
3699
|
-
// src/commands.push.ts
|
|
3700
|
-
init_config();
|
|
3701
|
-
import { existsSync as existsSync31 } from "node:fs";
|
|
3702
|
-
import { join as join38, relative as relative5 } from "node:path";
|
|
3703
|
-
|
|
3704
|
-
// src/commands.push.allowlist.ts
|
|
3705
|
-
init_config();
|
|
3706
|
-
init_config_sharedDirs_guard();
|
|
3707
|
-
init_utils();
|
|
3708
|
-
function isAllowed(path, allowed) {
|
|
3709
|
-
for (const entry of allowed) {
|
|
3710
|
-
if (path === entry) return true;
|
|
3711
|
-
if (entry === "hosts/") {
|
|
3712
|
-
if (/^hosts\/[^/]+\.json$/.test(path)) return true;
|
|
3713
|
-
continue;
|
|
3714
|
-
}
|
|
3715
|
-
if (entry.endsWith("/") && path.startsWith(entry)) return true;
|
|
3716
|
-
}
|
|
3717
|
-
return false;
|
|
3718
|
-
}
|
|
3719
|
-
function isNeverSync(path) {
|
|
3720
|
-
const blockSet = path.startsWith("shared/extras/") ? ALWAYS_NEVER_SYNC : NEVER_SYNC;
|
|
3721
|
-
for (const segment of path.split("/")) {
|
|
3722
|
-
if (blockSet.has(segment)) return true;
|
|
3723
|
-
}
|
|
3724
|
-
return false;
|
|
3725
|
-
}
|
|
3726
|
-
function parsePorcelainZ(statusPorcelain) {
|
|
3727
|
-
const records = statusPorcelain.split("\0");
|
|
3728
|
-
const paths = [];
|
|
3729
|
-
for (let i = 0; i < records.length; i++) {
|
|
3730
|
-
const rec = records[i];
|
|
3731
|
-
if (rec === void 0 || rec === "") continue;
|
|
3732
|
-
if (rec.length < 4) continue;
|
|
3733
|
-
const xy = rec.slice(0, 2);
|
|
3734
|
-
const newPath = rec.slice(3);
|
|
3735
|
-
paths.push(newPath);
|
|
3736
|
-
if (/[RC]/.test(xy)) {
|
|
3737
|
-
const oldPath = records[i + 1];
|
|
3738
|
-
if (oldPath !== void 0 && oldPath !== "") paths.push(oldPath);
|
|
3739
|
-
i++;
|
|
3740
|
-
}
|
|
3741
|
-
}
|
|
3742
|
-
return paths;
|
|
3743
|
-
}
|
|
3744
|
-
function enforceAllowList(statusPorcelain, map) {
|
|
3745
|
-
const extrasWhitelist = SUPPORTED_EXTRAS;
|
|
3746
|
-
const allowed = [
|
|
3747
|
-
...PUSH_ALLOWED_STATIC,
|
|
3748
|
-
...Object.keys(map.projects).map((l) => `shared/projects/${l}/`),
|
|
3749
|
-
...Object.entries(map.extras ?? {}).flatMap(
|
|
3750
|
-
([l, names]) => names.filter((n) => extrasWhitelist.includes(n)).flatMap((n) => [`shared/extras/${l}/${n}`, `shared/extras/${l}/${n}/`])
|
|
3751
|
-
),
|
|
3752
|
-
...(map.sharedDirs ?? []).filter((d) => isValidSharedDir(d)).map((d) => `shared/${d}/`)
|
|
3753
|
-
];
|
|
3754
|
-
const neverSyncHits = [];
|
|
3755
|
-
const violations = [];
|
|
3756
|
-
for (const path of parsePorcelainZ(statusPorcelain)) {
|
|
3757
|
-
if (isNeverSync(path)) {
|
|
3758
|
-
neverSyncHits.push(path);
|
|
3759
|
-
} else if (!isAllowed(path, allowed)) {
|
|
3760
|
-
violations.push(path);
|
|
3761
|
-
}
|
|
3762
|
-
}
|
|
3763
|
-
if (neverSyncHits.length === 0 && violations.length === 0) return;
|
|
3764
|
-
for (const p of neverSyncHits) {
|
|
3765
|
-
fail(`${p} is in NEVER_SYNC and must never be pushed`);
|
|
3766
|
-
}
|
|
3767
|
-
for (const p of violations) {
|
|
3768
|
-
fail(`to sync ${p}, add to PUSH_ALLOWED in src/config.ts`);
|
|
3769
|
-
}
|
|
3770
|
-
throw new NomadFatal("push allow-list violations");
|
|
3771
|
-
}
|
|
3636
|
+
// src/spinner.ts
|
|
3637
|
+
init_color();
|
|
3638
|
+
import { existsSync as existsSync29 } from "node:fs";
|
|
3639
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3640
|
+
import { Worker } from "node:worker_threads";
|
|
3772
3641
|
|
|
3773
3642
|
// src/commands.push.recovery.ts
|
|
3774
3643
|
init_config();
|
|
3775
3644
|
import { readFileSync as readFileSync10, rmSync as rmSync10, writeFileSync as writeFileSync5 } from "node:fs";
|
|
3776
|
-
import { join as
|
|
3645
|
+
import { join as join35 } from "node:path";
|
|
3777
3646
|
import { createInterface } from "node:readline/promises";
|
|
3778
3647
|
|
|
3779
3648
|
// src/commands.push.recovery.redact.ts
|
|
3780
3649
|
init_config();
|
|
3781
3650
|
init_config_sharedDirs_guard();
|
|
3782
|
-
import { cpSync as cpSync5, existsSync as
|
|
3783
|
-
import { dirname as dirname5, join as
|
|
3651
|
+
import { cpSync as cpSync5, existsSync as existsSync28, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
|
|
3652
|
+
import { dirname as dirname5, join as join33, sep as sep2 } from "node:path";
|
|
3784
3653
|
init_push_gitleaks_scan();
|
|
3785
3654
|
init_utils_json();
|
|
3786
3655
|
init_utils();
|
|
@@ -3811,8 +3680,8 @@ function resolveStagedDir(localPath, map) {
|
|
|
3811
3680
|
assertSafeLogical(logical);
|
|
3812
3681
|
const abs = hostMap[HOST];
|
|
3813
3682
|
if (abs === void 0) continue;
|
|
3814
|
-
if (localPath.startsWith(
|
|
3815
|
-
return
|
|
3683
|
+
if (localPath.startsWith(join33(CLAUDE_HOME, "projects", encodePath(abs)) + sep2)) {
|
|
3684
|
+
return join33(REPO_HOME, "shared", "projects", logical);
|
|
3816
3685
|
}
|
|
3817
3686
|
}
|
|
3818
3687
|
return null;
|
|
@@ -3834,7 +3703,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
3834
3703
|
`could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
3835
3704
|
);
|
|
3836
3705
|
}
|
|
3837
|
-
const sessionDir =
|
|
3706
|
+
const sessionDir = join33(dirname5(localPath), sid);
|
|
3838
3707
|
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
3839
3708
|
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
|
|
3840
3709
|
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
@@ -3867,10 +3736,10 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
3867
3736
|
`nothing to redact in the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
3868
3737
|
);
|
|
3869
3738
|
}
|
|
3870
|
-
|
|
3871
|
-
cpSync5(localPath,
|
|
3872
|
-
if (
|
|
3873
|
-
cpSync5(sessionDir,
|
|
3739
|
+
mkdirSync7(stagedProjectDir, { recursive: true });
|
|
3740
|
+
cpSync5(localPath, join33(stagedProjectDir, `${sid}.jsonl`), { force: true });
|
|
3741
|
+
if (existsSync28(sessionDir)) {
|
|
3742
|
+
cpSync5(sessionDir, join33(stagedProjectDir, sid), { force: true, recursive: true });
|
|
3874
3743
|
}
|
|
3875
3744
|
return true;
|
|
3876
3745
|
}
|
|
@@ -3878,13 +3747,13 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
3878
3747
|
// src/commands.push.recovery.drop.ts
|
|
3879
3748
|
init_config();
|
|
3880
3749
|
import { rmSync as rmSync9 } from "node:fs";
|
|
3881
|
-
import { join as
|
|
3750
|
+
import { join as join34 } from "node:path";
|
|
3882
3751
|
function dropSessionFromStaged(sid, map) {
|
|
3883
3752
|
const logicals = Object.keys(map.projects);
|
|
3884
3753
|
if (logicals.length === 0) return false;
|
|
3885
3754
|
for (const logical of logicals) {
|
|
3886
|
-
const jsonl =
|
|
3887
|
-
const dir =
|
|
3755
|
+
const jsonl = join34(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`);
|
|
3756
|
+
const dir = join34(REPO_HOME, "shared", "projects", logical, sid);
|
|
3888
3757
|
rmSync9(jsonl, { force: true });
|
|
3889
3758
|
rmSync9(dir, { recursive: true, force: true });
|
|
3890
3759
|
}
|
|
@@ -4002,7 +3871,7 @@ function applyThenRescan(scanVerdict, repoHome) {
|
|
|
4002
3871
|
return next;
|
|
4003
3872
|
}
|
|
4004
3873
|
function allowThenRescan(append, scanVerdict, repoHome) {
|
|
4005
|
-
const ignPath =
|
|
3874
|
+
const ignPath = join35(repoHome, ".gitleaksignore");
|
|
4006
3875
|
let before;
|
|
4007
3876
|
try {
|
|
4008
3877
|
before = readFileSync10(ignPath, "utf8");
|
|
@@ -4081,6 +3950,225 @@ async function resolveLeakFindings(verdict, ts, map, deps = {}) {
|
|
|
4081
3950
|
return current;
|
|
4082
3951
|
}
|
|
4083
3952
|
|
|
3953
|
+
// src/spinner.ts
|
|
3954
|
+
function formatElapsed(ms) {
|
|
3955
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
3956
|
+
}
|
|
3957
|
+
function writePlainStart(out, label) {
|
|
3958
|
+
out.write(`${label}...
|
|
3959
|
+
`);
|
|
3960
|
+
}
|
|
3961
|
+
function writePlainDone(out, label, ms) {
|
|
3962
|
+
out.write(`${label} done (${formatElapsed(ms)})
|
|
3963
|
+
`);
|
|
3964
|
+
}
|
|
3965
|
+
function writeAnimatedDone(out, label, ms, useTTY) {
|
|
3966
|
+
out.write("\r\x1B[K");
|
|
3967
|
+
const glyph = useTTY ? green(okGlyph) : okGlyph;
|
|
3968
|
+
out.write(`${glyph} ${label} (${formatElapsed(ms)})
|
|
3969
|
+
`);
|
|
3970
|
+
}
|
|
3971
|
+
function resolveWorkerPath(deps = {}) {
|
|
3972
|
+
const check = deps.existsSyncFn ?? existsSync29;
|
|
3973
|
+
const base = deps.baseUrl ?? import.meta.url;
|
|
3974
|
+
const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
|
|
3975
|
+
if (check(mjs)) return mjs;
|
|
3976
|
+
return fileURLToPath4(new URL("./spinner.worker.ts", base));
|
|
3977
|
+
}
|
|
3978
|
+
function makeRealWorker() {
|
|
3979
|
+
return new Worker(resolveWorkerPath());
|
|
3980
|
+
}
|
|
3981
|
+
function startSpinner(label, deps = {}) {
|
|
3982
|
+
const ttyCheck = deps.isTTYCheck ?? (() => isTTY());
|
|
3983
|
+
const env = deps.env ?? process.env;
|
|
3984
|
+
const out = deps.out ?? process.stderr;
|
|
3985
|
+
const now = deps.now ?? Date.now;
|
|
3986
|
+
const startMs = now();
|
|
3987
|
+
const animate = ttyCheck() && !env.CI;
|
|
3988
|
+
let worker = null;
|
|
3989
|
+
let degraded = false;
|
|
3990
|
+
let finalized = false;
|
|
3991
|
+
if (animate) {
|
|
3992
|
+
const factory = deps.makeWorker ?? makeRealWorker;
|
|
3993
|
+
try {
|
|
3994
|
+
worker = factory();
|
|
3995
|
+
worker.unref?.();
|
|
3996
|
+
worker.postMessage({ type: "start", label });
|
|
3997
|
+
} catch {
|
|
3998
|
+
degraded = true;
|
|
3999
|
+
worker = null;
|
|
4000
|
+
writePlainStart(out, label);
|
|
4001
|
+
}
|
|
4002
|
+
} else {
|
|
4003
|
+
writePlainStart(out, label);
|
|
4004
|
+
}
|
|
4005
|
+
function finalize(success, doneLabel) {
|
|
4006
|
+
if (finalized) return;
|
|
4007
|
+
finalized = true;
|
|
4008
|
+
const dl = doneLabel ?? label;
|
|
4009
|
+
const elapsed = now() - startMs;
|
|
4010
|
+
if (animate && !degraded && worker !== null) {
|
|
4011
|
+
worker.postMessage({ type: "pause" });
|
|
4012
|
+
if (success) writeAnimatedDone(out, dl, elapsed, ttyCheck());
|
|
4013
|
+
else out.write("\r\x1B[K");
|
|
4014
|
+
worker.terminate();
|
|
4015
|
+
worker = null;
|
|
4016
|
+
} else if (success) {
|
|
4017
|
+
writePlainDone(out, dl, elapsed);
|
|
4018
|
+
}
|
|
4019
|
+
}
|
|
4020
|
+
return {
|
|
4021
|
+
succeed: (doneLabel) => finalize(true, doneLabel),
|
|
4022
|
+
stop: () => finalize(false)
|
|
4023
|
+
};
|
|
4024
|
+
}
|
|
4025
|
+
|
|
4026
|
+
// src/commands.pull.ts
|
|
4027
|
+
init_utils();
|
|
4028
|
+
init_utils_fs();
|
|
4029
|
+
init_utils_json();
|
|
4030
|
+
function applyWetPull(ts, map) {
|
|
4031
|
+
applySharedLinks(ts, map);
|
|
4032
|
+
const { label } = regenerateSettings(ts);
|
|
4033
|
+
const syncSp = startSpinner("Syncing sessions");
|
|
4034
|
+
let remapResult;
|
|
4035
|
+
try {
|
|
4036
|
+
remapResult = remapPull(ts);
|
|
4037
|
+
syncSp.succeed();
|
|
4038
|
+
} finally {
|
|
4039
|
+
syncSp.stop();
|
|
4040
|
+
}
|
|
4041
|
+
const extrasResult = remapExtrasPull(ts);
|
|
4042
|
+
const summary = section("Summary");
|
|
4043
|
+
addItem(
|
|
4044
|
+
summary,
|
|
4045
|
+
summaryRow("pull", remapResult.unmapped + extrasResult.unmapped, 0, extrasResult.skipped)
|
|
4046
|
+
);
|
|
4047
|
+
renderTree([
|
|
4048
|
+
buildSettingsSection(label),
|
|
4049
|
+
buildSessionsSection(remapResult.pulled, remapResult.unmapped),
|
|
4050
|
+
buildExtrasSection(extrasResult.pulled, extrasResult.skipped),
|
|
4051
|
+
summary
|
|
4052
|
+
]);
|
|
4053
|
+
}
|
|
4054
|
+
function cmdPull(opts = {}) {
|
|
4055
|
+
const dryRun = opts.dryRun === true;
|
|
4056
|
+
if (!existsSync30(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
4057
|
+
if (!existsSync30(join36(REPO_HOME, "shared", "settings.base.json"))) {
|
|
4058
|
+
die("repo not initialized; run 'nomad init' to scaffold");
|
|
4059
|
+
}
|
|
4060
|
+
const handle = acquireLock("pull");
|
|
4061
|
+
if (handle === null) process.exit(0);
|
|
4062
|
+
try {
|
|
4063
|
+
const ts = freshBackupTs(BACKUP_BASE);
|
|
4064
|
+
if (!dryRun) {
|
|
4065
|
+
const backupRoot = join36(BACKUP_BASE, ts);
|
|
4066
|
+
try {
|
|
4067
|
+
mkdirSync8(backupRoot, { recursive: true });
|
|
4068
|
+
} catch (err) {
|
|
4069
|
+
die(`could not create backup dir: ${err.message}`);
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
log(
|
|
4073
|
+
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
4074
|
+
);
|
|
4075
|
+
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", REPO_HOME);
|
|
4076
|
+
const mapPath = join36(REPO_HOME, "path-map.json");
|
|
4077
|
+
const map = existsSync30(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
4078
|
+
divergenceCheckExtras(ts);
|
|
4079
|
+
if (dryRun) {
|
|
4080
|
+
const previewResult = computePreview(ts, map);
|
|
4081
|
+
log("dry-run complete; no mutation");
|
|
4082
|
+
emitSummary("pull", previewResult.unmapped);
|
|
4083
|
+
} else {
|
|
4084
|
+
applyWetPull(ts, map);
|
|
4085
|
+
}
|
|
4086
|
+
} catch (err) {
|
|
4087
|
+
if (err instanceof NomadFatal) {
|
|
4088
|
+
fail(err.message);
|
|
4089
|
+
process.exitCode = 1;
|
|
4090
|
+
} else {
|
|
4091
|
+
throw err;
|
|
4092
|
+
}
|
|
4093
|
+
} finally {
|
|
4094
|
+
releaseLock(handle);
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
4097
|
+
|
|
4098
|
+
// src/commands.push.ts
|
|
4099
|
+
init_config();
|
|
4100
|
+
import { existsSync as existsSync32 } from "node:fs";
|
|
4101
|
+
import { join as join38, relative as relative5 } from "node:path";
|
|
4102
|
+
|
|
4103
|
+
// src/commands.push.allowlist.ts
|
|
4104
|
+
init_config();
|
|
4105
|
+
init_config_sharedDirs_guard();
|
|
4106
|
+
init_utils();
|
|
4107
|
+
function isAllowed(path, allowed) {
|
|
4108
|
+
for (const entry of allowed) {
|
|
4109
|
+
if (path === entry) return true;
|
|
4110
|
+
if (entry === "hosts/") {
|
|
4111
|
+
if (/^hosts\/[^/]+\.json$/.test(path)) return true;
|
|
4112
|
+
continue;
|
|
4113
|
+
}
|
|
4114
|
+
if (entry.endsWith("/") && path.startsWith(entry)) return true;
|
|
4115
|
+
}
|
|
4116
|
+
return false;
|
|
4117
|
+
}
|
|
4118
|
+
function isNeverSync(path) {
|
|
4119
|
+
const blockSet = path.startsWith("shared/extras/") ? ALWAYS_NEVER_SYNC : NEVER_SYNC;
|
|
4120
|
+
for (const segment of path.split("/")) {
|
|
4121
|
+
if (blockSet.has(segment)) return true;
|
|
4122
|
+
}
|
|
4123
|
+
return false;
|
|
4124
|
+
}
|
|
4125
|
+
function parsePorcelainZ(statusPorcelain) {
|
|
4126
|
+
const records = statusPorcelain.split("\0");
|
|
4127
|
+
const paths = [];
|
|
4128
|
+
for (let i = 0; i < records.length; i++) {
|
|
4129
|
+
const rec = records[i];
|
|
4130
|
+
if (rec === void 0 || rec === "") continue;
|
|
4131
|
+
if (rec.length < 4) continue;
|
|
4132
|
+
const xy = rec.slice(0, 2);
|
|
4133
|
+
const newPath = rec.slice(3);
|
|
4134
|
+
paths.push(newPath);
|
|
4135
|
+
if (/[RC]/.test(xy)) {
|
|
4136
|
+
const oldPath = records[i + 1];
|
|
4137
|
+
if (oldPath !== void 0 && oldPath !== "") paths.push(oldPath);
|
|
4138
|
+
i++;
|
|
4139
|
+
}
|
|
4140
|
+
}
|
|
4141
|
+
return paths;
|
|
4142
|
+
}
|
|
4143
|
+
function enforceAllowList(statusPorcelain, map) {
|
|
4144
|
+
const extrasWhitelist = SUPPORTED_EXTRAS;
|
|
4145
|
+
const allowed = [
|
|
4146
|
+
...PUSH_ALLOWED_STATIC,
|
|
4147
|
+
...Object.keys(map.projects).map((l) => `shared/projects/${l}/`),
|
|
4148
|
+
...Object.entries(map.extras ?? {}).flatMap(
|
|
4149
|
+
([l, names]) => names.filter((n) => extrasWhitelist.includes(n)).flatMap((n) => [`shared/extras/${l}/${n}`, `shared/extras/${l}/${n}/`])
|
|
4150
|
+
),
|
|
4151
|
+
...(map.sharedDirs ?? []).filter((d) => isValidSharedDir(d)).map((d) => `shared/${d}/`)
|
|
4152
|
+
];
|
|
4153
|
+
const neverSyncHits = [];
|
|
4154
|
+
const violations = [];
|
|
4155
|
+
for (const path of parsePorcelainZ(statusPorcelain)) {
|
|
4156
|
+
if (isNeverSync(path)) {
|
|
4157
|
+
neverSyncHits.push(path);
|
|
4158
|
+
} else if (!isAllowed(path, allowed)) {
|
|
4159
|
+
violations.push(path);
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
if (neverSyncHits.length === 0 && violations.length === 0) return;
|
|
4163
|
+
for (const p of neverSyncHits) {
|
|
4164
|
+
fail(`${p} is in NEVER_SYNC and must never be pushed`);
|
|
4165
|
+
}
|
|
4166
|
+
for (const p of violations) {
|
|
4167
|
+
fail(`to sync ${p}, add to PUSH_ALLOWED in src/config.ts`);
|
|
4168
|
+
}
|
|
4169
|
+
throw new NomadFatal("push allow-list violations");
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4084
4172
|
// src/commands.push.ts
|
|
4085
4173
|
init_push_leak_verdict();
|
|
4086
4174
|
init_push_checks();
|
|
@@ -4090,7 +4178,7 @@ init_color();
|
|
|
4090
4178
|
init_config();
|
|
4091
4179
|
init_config_sharedDirs_guard();
|
|
4092
4180
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
4093
|
-
import { copyFileSync, existsSync as
|
|
4181
|
+
import { copyFileSync, existsSync as existsSync31, mkdirSync as mkdirSync9, readdirSync as readdirSync10, rmSync as rmSync11 } from "node:fs";
|
|
4094
4182
|
import { homedir as homedir5 } from "node:os";
|
|
4095
4183
|
import { join as join37 } from "node:path";
|
|
4096
4184
|
init_push_leak_verdict();
|
|
@@ -4108,7 +4196,7 @@ function stageSessions(tmpRoot, map) {
|
|
|
4108
4196
|
reverse.set(encodePath(p), logical);
|
|
4109
4197
|
}
|
|
4110
4198
|
const localProjects = join37(CLAUDE_HOME, "projects");
|
|
4111
|
-
if (!
|
|
4199
|
+
if (!existsSync31(localProjects)) return 0;
|
|
4112
4200
|
let staged = 0;
|
|
4113
4201
|
for (const dir of readdirSync10(localProjects)) {
|
|
4114
4202
|
const logical = reverse.get(dir);
|
|
@@ -4130,7 +4218,7 @@ function stageExtras(tmpRoot, map) {
|
|
|
4130
4218
|
for (const dirname6 of dirnames) {
|
|
4131
4219
|
if (!whitelist.includes(dirname6)) continue;
|
|
4132
4220
|
const src = join37(localRoot, dirname6);
|
|
4133
|
-
if (!
|
|
4221
|
+
if (!existsSync31(src)) continue;
|
|
4134
4222
|
const dst = join37(tmpRoot, "shared", "extras", logical, dirname6);
|
|
4135
4223
|
copyExtras(src, dst);
|
|
4136
4224
|
staged++;
|
|
@@ -4150,7 +4238,7 @@ function previewPushLeaks(map) {
|
|
|
4150
4238
|
return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
|
|
4151
4239
|
}
|
|
4152
4240
|
const ignoreFile = join37(REPO_HOME, ".gitleaksignore");
|
|
4153
|
-
if (
|
|
4241
|
+
if (existsSync31(ignoreFile)) {
|
|
4154
4242
|
copyFileSync(ignoreFile, join37(tmpRoot, ".gitleaksignore"));
|
|
4155
4243
|
}
|
|
4156
4244
|
let findings;
|
|
@@ -4184,15 +4272,34 @@ function guardGitlinks() {
|
|
|
4184
4272
|
`gitlink trap: ${gitlinks.length} nested .git ${noun} in shared/; remove before retry`
|
|
4185
4273
|
);
|
|
4186
4274
|
}
|
|
4275
|
+
function runScan() {
|
|
4276
|
+
const sp = startSpinner("Scanning for secrets");
|
|
4277
|
+
try {
|
|
4278
|
+
const verdict = scanPushVerdict();
|
|
4279
|
+
sp.succeed();
|
|
4280
|
+
return verdict;
|
|
4281
|
+
} finally {
|
|
4282
|
+
sp.stop();
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
function runPush() {
|
|
4286
|
+
const sp = startSpinner("Pushing");
|
|
4287
|
+
try {
|
|
4288
|
+
gitOrFatal(["push"], "git push", REPO_HOME);
|
|
4289
|
+
sp.succeed();
|
|
4290
|
+
} finally {
|
|
4291
|
+
sp.stop();
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4187
4294
|
async function commitAndPush(st, ts, map, redactAll, allowAll, allowRule) {
|
|
4188
4295
|
gitOrFatal(["add", "-A"], "git add", REPO_HOME);
|
|
4189
|
-
let verdict =
|
|
4296
|
+
let verdict = runScan();
|
|
4190
4297
|
if (verdict.leak) {
|
|
4191
4298
|
renderPushTree(st, verdict);
|
|
4192
4299
|
verdict = await resolveLeakFindings(verdict, ts, map, { redactAll, allowAll, allowRule });
|
|
4193
4300
|
}
|
|
4194
4301
|
gitOrFatal(["commit", "-m", `chore: sync from ${HOST}`], "git commit", REPO_HOME);
|
|
4195
|
-
|
|
4302
|
+
runPush();
|
|
4196
4303
|
renderPushTree(st, verdict);
|
|
4197
4304
|
}
|
|
4198
4305
|
function runDryRunPreview(st, map) {
|
|
@@ -4225,13 +4332,19 @@ async function cmdPush(opts = {}) {
|
|
|
4225
4332
|
const allowAll = opts.allowAll === true;
|
|
4226
4333
|
const allowRule = opts.allowRule;
|
|
4227
4334
|
guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
|
|
4228
|
-
if (!
|
|
4335
|
+
if (!existsSync32(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
4229
4336
|
const handle = acquireLock("push");
|
|
4230
4337
|
if (handle === null) process.exit(0);
|
|
4231
4338
|
try {
|
|
4232
4339
|
console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
|
|
4233
4340
|
probeGitleaks();
|
|
4234
|
-
|
|
4341
|
+
const rebaseSp = startSpinner("Rebasing onto origin");
|
|
4342
|
+
try {
|
|
4343
|
+
rebaseBeforePush();
|
|
4344
|
+
rebaseSp.succeed();
|
|
4345
|
+
} finally {
|
|
4346
|
+
rebaseSp.stop();
|
|
4347
|
+
}
|
|
4235
4348
|
const ts = freshBackupTs(BACKUP_BASE);
|
|
4236
4349
|
const remap = remapPush(ts, { dryRun });
|
|
4237
4350
|
const extras = remapExtrasPush(ts, { dryRun });
|
|
@@ -4244,7 +4357,7 @@ async function cmdPush(opts = {}) {
|
|
|
4244
4357
|
return;
|
|
4245
4358
|
}
|
|
4246
4359
|
const mapPath = join38(REPO_HOME, "path-map.json");
|
|
4247
|
-
if (!
|
|
4360
|
+
if (!existsSync32(mapPath)) {
|
|
4248
4361
|
if (dryRun) return runDryRunPreview(st, null);
|
|
4249
4362
|
die("path-map.json missing, cannot enforce push allow-list");
|
|
4250
4363
|
}
|
|
@@ -4284,17 +4397,17 @@ init_config();
|
|
|
4284
4397
|
|
|
4285
4398
|
// src/diff.ts
|
|
4286
4399
|
init_config();
|
|
4287
|
-
import { existsSync as
|
|
4400
|
+
import { existsSync as existsSync33 } from "node:fs";
|
|
4288
4401
|
import { join as join39 } from "node:path";
|
|
4289
4402
|
init_utils();
|
|
4290
4403
|
init_utils_fs();
|
|
4291
4404
|
init_utils_json();
|
|
4292
4405
|
function cmdDiff() {
|
|
4293
4406
|
try {
|
|
4294
|
-
if (!
|
|
4407
|
+
if (!existsSync33(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
4295
4408
|
const ts = freshBackupTs(BACKUP_BASE);
|
|
4296
4409
|
const mapPath = join39(REPO_HOME, "path-map.json");
|
|
4297
|
-
const map =
|
|
4410
|
+
const map = existsSync33(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
4298
4411
|
const result = computePreview(ts, map);
|
|
4299
4412
|
emitSummary("diff", result.unmapped);
|
|
4300
4413
|
} catch (err) {
|
|
@@ -4309,7 +4422,7 @@ function cmdDiff() {
|
|
|
4309
4422
|
|
|
4310
4423
|
// src/init.ts
|
|
4311
4424
|
init_config();
|
|
4312
|
-
import { existsSync as
|
|
4425
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4313
4426
|
import { join as join41 } from "node:path";
|
|
4314
4427
|
|
|
4315
4428
|
// src/init.gh-onboard.ts
|
|
@@ -4391,16 +4504,16 @@ init_config();
|
|
|
4391
4504
|
init_utils();
|
|
4392
4505
|
init_utils_fs();
|
|
4393
4506
|
init_utils_json();
|
|
4394
|
-
import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as
|
|
4507
|
+
import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as existsSync34, rmSync as rmSync12, statSync as statSync9 } from "node:fs";
|
|
4395
4508
|
import { join as join40 } from "node:path";
|
|
4396
4509
|
function snapshotIntoShared(map) {
|
|
4397
4510
|
for (const name of allSharedLinks(map)) {
|
|
4398
4511
|
const src = join40(CLAUDE_HOME, name);
|
|
4399
|
-
if (!
|
|
4512
|
+
if (!existsSync34(src)) continue;
|
|
4400
4513
|
const dst = join40(REPO_HOME, "shared", name);
|
|
4401
4514
|
if (statSync9(src).isDirectory()) {
|
|
4402
4515
|
const gk = join40(dst, ".gitkeep");
|
|
4403
|
-
if (
|
|
4516
|
+
if (existsSync34(gk)) rmSync12(gk);
|
|
4404
4517
|
cpSync6(src, dst, { recursive: true, force: false, errorOnExist: true });
|
|
4405
4518
|
} else {
|
|
4406
4519
|
copyFileSync2(src, dst);
|
|
@@ -4408,7 +4521,7 @@ function snapshotIntoShared(map) {
|
|
|
4408
4521
|
log(`snapshotted shared/${name} from ${src}`);
|
|
4409
4522
|
}
|
|
4410
4523
|
const userSettings = join40(CLAUDE_HOME, "settings.json");
|
|
4411
|
-
if (
|
|
4524
|
+
if (existsSync34(userSettings)) {
|
|
4412
4525
|
let parsed;
|
|
4413
4526
|
try {
|
|
4414
4527
|
parsed = readJson(userSettings);
|
|
@@ -4435,7 +4548,7 @@ function preflightConflict(repoHome) {
|
|
|
4435
4548
|
join41(repoHome, "shared")
|
|
4436
4549
|
];
|
|
4437
4550
|
for (const c of candidates) {
|
|
4438
|
-
if (
|
|
4551
|
+
if (existsSync35(c)) return c;
|
|
4439
4552
|
}
|
|
4440
4553
|
return null;
|
|
4441
4554
|
}
|
|
@@ -4454,7 +4567,7 @@ function cmdInit(opts = {}) {
|
|
|
4454
4567
|
mkdirSync10(join41(REPO_HOME, "shared", name), { recursive: true });
|
|
4455
4568
|
}
|
|
4456
4569
|
const userClaudeMd = join41(CLAUDE_HOME, "CLAUDE.md");
|
|
4457
|
-
if (!snapshot || !
|
|
4570
|
+
if (!snapshot || !existsSync35(userClaudeMd)) {
|
|
4458
4571
|
writeFileSync6(join41(REPO_HOME, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
|
|
4459
4572
|
log("created shared/CLAUDE.md");
|
|
4460
4573
|
}
|
|
@@ -4728,7 +4841,7 @@ function parsePushArgs(argv) {
|
|
|
4728
4841
|
// package.json
|
|
4729
4842
|
var package_default = {
|
|
4730
4843
|
name: "claude-nomad",
|
|
4731
|
-
version: "0.
|
|
4844
|
+
version: "0.40.0",
|
|
4732
4845
|
type: "module",
|
|
4733
4846
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
4734
4847
|
keywords: [
|
|
@@ -4918,7 +5031,7 @@ var DEFAULT_HELP = [
|
|
|
4918
5031
|
init_config();
|
|
4919
5032
|
init_utils();
|
|
4920
5033
|
init_utils_json();
|
|
4921
|
-
import { existsSync as
|
|
5034
|
+
import { existsSync as existsSync36, readFileSync as readFileSync11, readdirSync as readdirSync11 } from "node:fs";
|
|
4922
5035
|
import { join as join42 } from "node:path";
|
|
4923
5036
|
function resumeCmd(sessionId) {
|
|
4924
5037
|
if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
|
|
@@ -4926,7 +5039,7 @@ function resumeCmd(sessionId) {
|
|
|
4926
5039
|
process.exit(1);
|
|
4927
5040
|
}
|
|
4928
5041
|
const projectsRoot = join42(CLAUDE_HOME, "projects");
|
|
4929
|
-
if (!
|
|
5042
|
+
if (!existsSync36(projectsRoot)) {
|
|
4930
5043
|
fail(`${projectsRoot} does not exist`);
|
|
4931
5044
|
process.exit(1);
|
|
4932
5045
|
}
|
|
@@ -4941,7 +5054,7 @@ function resumeCmd(sessionId) {
|
|
|
4941
5054
|
process.exit(1);
|
|
4942
5055
|
}
|
|
4943
5056
|
const mapPath = join42(REPO_HOME, "path-map.json");
|
|
4944
|
-
if (!
|
|
5057
|
+
if (!existsSync36(mapPath)) {
|
|
4945
5058
|
fail("path-map.json missing");
|
|
4946
5059
|
process.exit(1);
|
|
4947
5060
|
}
|
|
@@ -4965,7 +5078,7 @@ function resumeCmd(sessionId) {
|
|
|
4965
5078
|
function findTranscriptPath(projectsRoot, sessionId) {
|
|
4966
5079
|
for (const dir of readdirSync11(projectsRoot)) {
|
|
4967
5080
|
const candidate = join42(projectsRoot, dir, `${sessionId}.jsonl`);
|
|
4968
|
-
if (
|
|
5081
|
+
if (existsSync36(candidate)) return candidate;
|
|
4969
5082
|
}
|
|
4970
5083
|
return null;
|
|
4971
5084
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/spinner.worker.ts
|
|
2
|
+
import { parentPort } from "node:worker_threads";
|
|
3
|
+
var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
4
|
+
var INTERVAL_MS = 80;
|
|
5
|
+
var frame = 0;
|
|
6
|
+
var timer = null;
|
|
7
|
+
if (parentPort !== null) {
|
|
8
|
+
parentPort.on("message", (msg) => {
|
|
9
|
+
if (msg.type === "start" && msg.label !== void 0) {
|
|
10
|
+
const label = msg.label;
|
|
11
|
+
frame = 0;
|
|
12
|
+
if (timer !== null) clearInterval(timer);
|
|
13
|
+
timer = setInterval(() => {
|
|
14
|
+
process.stderr.write(`${FRAMES[frame % FRAMES.length]} ${label}\r`);
|
|
15
|
+
frame++;
|
|
16
|
+
}, INTERVAL_MS);
|
|
17
|
+
} else if (msg.type === "pause") {
|
|
18
|
+
if (timer !== null) {
|
|
19
|
+
clearInterval(timer);
|
|
20
|
+
timer = null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
package/package.json
CHANGED