@bondsmith/bondsmithd 0.1.4 → 0.1.6
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/dist/index.js +211 -45
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { createServer } from "http";
|
|
5
5
|
import { existsSync } from "fs";
|
|
6
6
|
import { hostname as hostname2 } from "os";
|
|
7
|
-
import { dirname as dirname4, join as
|
|
7
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
|
|
10
10
|
// ../../packages/local-agent-protocol/src/session.ts
|
|
@@ -951,7 +951,7 @@ import { sign } from "crypto";
|
|
|
951
951
|
import WebSocket from "ws";
|
|
952
952
|
|
|
953
953
|
// src/version.ts
|
|
954
|
-
var DAEMON_VERSION = true ? "0.1.
|
|
954
|
+
var DAEMON_VERSION = true ? "0.1.6" : "dev";
|
|
955
955
|
var VERSION_FLAGS = ["--version", "-v"];
|
|
956
956
|
function versionLine() {
|
|
957
957
|
return `bondsmithd ${DAEMON_VERSION} (protocol ${PROTOCOL_VERSION})`;
|
|
@@ -1378,9 +1378,12 @@ function error(message, requestId) {
|
|
|
1378
1378
|
function errorMessageOf(err, fallback) {
|
|
1379
1379
|
return err instanceof Error ? err.message : fallback;
|
|
1380
1380
|
}
|
|
1381
|
-
function
|
|
1381
|
+
function requestIdOf(message) {
|
|
1382
|
+
return "requestId" in message ? message.requestId : void 0;
|
|
1383
|
+
}
|
|
1384
|
+
function getSessionOrError(sessionManager, conn, sessionId, requestId) {
|
|
1382
1385
|
const session = sessionManager.get(sessionId);
|
|
1383
|
-
if (!session) conn.send(error("unknown session"));
|
|
1386
|
+
if (!session) conn.send(error("unknown session", requestId));
|
|
1384
1387
|
return session;
|
|
1385
1388
|
}
|
|
1386
1389
|
async function handleBrowserMessage(conn, raw, deps) {
|
|
@@ -1400,11 +1403,11 @@ async function handleBrowserMessage(conn, raw, deps) {
|
|
|
1400
1403
|
deps.trustedControllersPath
|
|
1401
1404
|
);
|
|
1402
1405
|
if (!verdict.ok) {
|
|
1403
|
-
conn.send(error(verifyRejectionMessage(verdict.reason)));
|
|
1406
|
+
conn.send(error(verifyRejectionMessage(verdict.reason), requestIdOf(innerMessage.data)));
|
|
1404
1407
|
return false;
|
|
1405
1408
|
}
|
|
1406
1409
|
if (conn.connectionAuth.isRevoked()) {
|
|
1407
|
-
conn.send(error(REVOKED_REJECTION_MESSAGE));
|
|
1410
|
+
conn.send(error(REVOKED_REJECTION_MESSAGE, requestIdOf(innerMessage.data)));
|
|
1408
1411
|
return false;
|
|
1409
1412
|
}
|
|
1410
1413
|
return true;
|
|
@@ -1432,7 +1435,7 @@ async function handleBrowserMessage(conn, raw, deps) {
|
|
|
1432
1435
|
return;
|
|
1433
1436
|
}
|
|
1434
1437
|
if (isSignableAction(parsed.data.type)) {
|
|
1435
|
-
conn.send(error("signature required for this command"));
|
|
1438
|
+
conn.send(error("signature required for this command", requestIdOf(parsed.data)));
|
|
1436
1439
|
return;
|
|
1437
1440
|
}
|
|
1438
1441
|
await dispatch(conn, parsed.data, deps);
|
|
@@ -1571,10 +1574,13 @@ async function dispatch(conn, message, {
|
|
|
1571
1574
|
return;
|
|
1572
1575
|
}
|
|
1573
1576
|
case "workspace.getStatus": {
|
|
1574
|
-
const session = getSessionOrError(sessionManager, conn, message.sessionId);
|
|
1577
|
+
const session = getSessionOrError(sessionManager, conn, message.sessionId, message.requestId);
|
|
1575
1578
|
if (!session) return;
|
|
1576
1579
|
try {
|
|
1577
|
-
const status = await getWorkspaceStatus2(
|
|
1580
|
+
const status = await getWorkspaceStatus2(
|
|
1581
|
+
session.cwd,
|
|
1582
|
+
sessionManager.getBaseline(session.id)
|
|
1583
|
+
);
|
|
1578
1584
|
conn.send({
|
|
1579
1585
|
type: "workspace.status",
|
|
1580
1586
|
sessionId: message.sessionId,
|
|
@@ -1582,15 +1588,20 @@ async function dispatch(conn, message, {
|
|
|
1582
1588
|
status
|
|
1583
1589
|
});
|
|
1584
1590
|
} catch (err) {
|
|
1585
|
-
conn.send(error(errorMessageOf(err, "failed to read workspace status")));
|
|
1591
|
+
conn.send(error(errorMessageOf(err, "failed to read workspace status"), message.requestId));
|
|
1586
1592
|
}
|
|
1587
1593
|
return;
|
|
1588
1594
|
}
|
|
1589
1595
|
case "workspace.getDiff": {
|
|
1590
|
-
const session = getSessionOrError(sessionManager, conn, message.sessionId);
|
|
1596
|
+
const session = getSessionOrError(sessionManager, conn, message.sessionId, message.requestId);
|
|
1591
1597
|
if (!session) return;
|
|
1592
1598
|
try {
|
|
1593
|
-
const diff = await getWorkspaceDiff2(
|
|
1599
|
+
const diff = await getWorkspaceDiff2(
|
|
1600
|
+
session.cwd,
|
|
1601
|
+
message.path,
|
|
1602
|
+
message.staged,
|
|
1603
|
+
sessionManager.getBaseline(session.id)
|
|
1604
|
+
);
|
|
1594
1605
|
conn.send({
|
|
1595
1606
|
type: "workspace.diff",
|
|
1596
1607
|
sessionId: message.sessionId,
|
|
@@ -1598,7 +1609,7 @@ async function dispatch(conn, message, {
|
|
|
1598
1609
|
diff
|
|
1599
1610
|
});
|
|
1600
1611
|
} catch (err) {
|
|
1601
|
-
conn.send(error(errorMessageOf(err, "failed to read workspace diff")));
|
|
1612
|
+
conn.send(error(errorMessageOf(err, "failed to read workspace diff"), message.requestId));
|
|
1602
1613
|
}
|
|
1603
1614
|
return;
|
|
1604
1615
|
}
|
|
@@ -2538,6 +2549,10 @@ function presentPendingAuthorizationOnTerminal(store) {
|
|
|
2538
2549
|
};
|
|
2539
2550
|
}
|
|
2540
2551
|
|
|
2552
|
+
// src/sessions/git/baseline.ts
|
|
2553
|
+
import { lstat } from "fs/promises";
|
|
2554
|
+
import { join as join5 } from "path";
|
|
2555
|
+
|
|
2541
2556
|
// src/sessions/git/base.ts
|
|
2542
2557
|
var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
2543
2558
|
var DEFAULT_BRANCH_CANDIDATES = ["origin/main", "origin/master", "main", "master"];
|
|
@@ -2571,6 +2586,107 @@ async function resolveComparisonBase(cwd) {
|
|
|
2571
2586
|
return mergeBase ? { sha: mergeBase, ref: defaultRef } : { sha: "HEAD", ref: null };
|
|
2572
2587
|
}
|
|
2573
2588
|
|
|
2589
|
+
// src/sessions/git/baseline.ts
|
|
2590
|
+
var SESSION_START_REF = "session start";
|
|
2591
|
+
var MAX_PREEXISTING_FILES = 200;
|
|
2592
|
+
var DELETED = "deleted";
|
|
2593
|
+
async function fingerprintFile(cwd, path) {
|
|
2594
|
+
try {
|
|
2595
|
+
const stats = await lstat(join5(cwd, path));
|
|
2596
|
+
return `${stats.size}:${Math.trunc(stats.mtimeMs)}`;
|
|
2597
|
+
} catch {
|
|
2598
|
+
return DELETED;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
async function headSha(cwd) {
|
|
2602
|
+
try {
|
|
2603
|
+
return (await runGit(["rev-parse", "--verify", "--quiet", "HEAD^{commit}"], cwd)).trim() || null;
|
|
2604
|
+
} catch {
|
|
2605
|
+
return null;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
async function listChangedPaths(cwd, baseSha) {
|
|
2609
|
+
const [tracked, untracked] = await Promise.all([
|
|
2610
|
+
runGitDiff(["--relative", "--name-only", "-z", baseSha], cwd),
|
|
2611
|
+
runGit(["ls-files", "--others", "--exclude-standard", "-z"], cwd)
|
|
2612
|
+
]);
|
|
2613
|
+
const paths = [...tracked.split("\0"), ...untracked.split("\0")].filter((p) => p.length > 0);
|
|
2614
|
+
return [...new Set(paths)];
|
|
2615
|
+
}
|
|
2616
|
+
async function captureBaseline(cwd, options) {
|
|
2617
|
+
try {
|
|
2618
|
+
await runGit(["rev-parse", "--is-inside-work-tree"], cwd);
|
|
2619
|
+
} catch {
|
|
2620
|
+
return null;
|
|
2621
|
+
}
|
|
2622
|
+
try {
|
|
2623
|
+
const baseSha = await headSha(cwd);
|
|
2624
|
+
const preexisting = /* @__PURE__ */ new Map();
|
|
2625
|
+
if (!options.freshCheckout) {
|
|
2626
|
+
const changed = await listChangedPaths(cwd, baseSha ?? EMPTY_TREE_SHA);
|
|
2627
|
+
for (const path of changed.slice(0, MAX_PREEXISTING_FILES)) {
|
|
2628
|
+
preexisting.set(path, await fingerprintFile(cwd, path));
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
return { baseSha, preexisting };
|
|
2632
|
+
} catch (error2) {
|
|
2633
|
+
console.error("Failed to take a session baseline; comparing against the default branch", {
|
|
2634
|
+
cwd,
|
|
2635
|
+
error: error2
|
|
2636
|
+
});
|
|
2637
|
+
return null;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
async function commitExists(cwd, sha) {
|
|
2641
|
+
try {
|
|
2642
|
+
await runGit(["rev-parse", "--verify", "--quiet", `${sha}^{commit}`], cwd);
|
|
2643
|
+
return true;
|
|
2644
|
+
} catch {
|
|
2645
|
+
return false;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
async function resolveBaseFor(cwd, baseline) {
|
|
2649
|
+
if (!baseline) return resolveComparisonBase(cwd);
|
|
2650
|
+
if (baseline.baseSha !== null && !await commitExists(cwd, baseline.baseSha)) {
|
|
2651
|
+
return resolveComparisonBase(cwd);
|
|
2652
|
+
}
|
|
2653
|
+
return { sha: baseline.baseSha ?? EMPTY_TREE_SHA, ref: SESSION_START_REF };
|
|
2654
|
+
}
|
|
2655
|
+
async function dropPreexisting(cwd, entries, baseline) {
|
|
2656
|
+
if (baseline.preexisting.size === 0) return entries;
|
|
2657
|
+
const untouched = await Promise.all(
|
|
2658
|
+
entries.map(async (entry) => {
|
|
2659
|
+
const before = baseline.preexisting.get(entry.path);
|
|
2660
|
+
return before !== void 0 && before === await fingerprintFile(cwd, entry.path);
|
|
2661
|
+
})
|
|
2662
|
+
);
|
|
2663
|
+
return entries.filter((_, index) => !untouched[index]);
|
|
2664
|
+
}
|
|
2665
|
+
function encodeBaseline(baseline) {
|
|
2666
|
+
const serialized = {
|
|
2667
|
+
v: 1,
|
|
2668
|
+
base: baseline.baseSha,
|
|
2669
|
+
files: [...baseline.preexisting]
|
|
2670
|
+
};
|
|
2671
|
+
return Buffer.from(JSON.stringify(serialized), "utf8").toString("base64url");
|
|
2672
|
+
}
|
|
2673
|
+
function isSerializedBaseline(value) {
|
|
2674
|
+
if (typeof value !== "object" || value === null) return false;
|
|
2675
|
+
const candidate = value;
|
|
2676
|
+
return candidate.v === 1 && (candidate.base === null || typeof candidate.base === "string") && Array.isArray(candidate.files) && candidate.files.every(
|
|
2677
|
+
(file) => Array.isArray(file) && file.length === 2 && typeof file[0] === "string" && typeof file[1] === "string"
|
|
2678
|
+
);
|
|
2679
|
+
}
|
|
2680
|
+
function decodeBaseline(encoded) {
|
|
2681
|
+
try {
|
|
2682
|
+
const parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
2683
|
+
if (!isSerializedBaseline(parsed)) return null;
|
|
2684
|
+
return { baseSha: parsed.base, preexisting: new Map(parsed.files) };
|
|
2685
|
+
} catch {
|
|
2686
|
+
return null;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2574
2690
|
// src/sessions/git/status.ts
|
|
2575
2691
|
async function getCurrentBranch(cwd) {
|
|
2576
2692
|
try {
|
|
@@ -2666,15 +2782,15 @@ function parseNameStatus(output) {
|
|
|
2666
2782
|
}
|
|
2667
2783
|
return changed;
|
|
2668
2784
|
}
|
|
2669
|
-
async function getWorkspaceStatus(cwd) {
|
|
2785
|
+
async function getWorkspaceStatus(cwd, baseline) {
|
|
2670
2786
|
const branch = await getCurrentBranch(cwd);
|
|
2671
2787
|
if (branch === null && !await isGitRepo(cwd)) {
|
|
2672
2788
|
return { isGitRepo: false, branch: null, files: [] };
|
|
2673
2789
|
}
|
|
2674
|
-
const base = await
|
|
2790
|
+
const base = await resolveBaseFor(cwd, baseline);
|
|
2675
2791
|
const [nameStatusRaw, numstatRaw, untrackedRaw] = await Promise.all([
|
|
2676
|
-
runGitDiff(["--name-status", "-z", "-M", base.sha], cwd),
|
|
2677
|
-
runGitDiff(["--numstat", "-z", "-M", base.sha], cwd),
|
|
2792
|
+
runGitDiff(["--relative", "--name-status", "-z", "-M", base.sha], cwd),
|
|
2793
|
+
runGitDiff(["--relative", "--numstat", "-z", "-M", base.sha], cwd),
|
|
2678
2794
|
runGit(["ls-files", "--others", "--exclude-standard", "-z"], cwd)
|
|
2679
2795
|
]);
|
|
2680
2796
|
const numstat = parseNumstat(numstatRaw);
|
|
@@ -2698,13 +2814,19 @@ async function getWorkspaceStatus(cwd) {
|
|
|
2698
2814
|
deletions: null,
|
|
2699
2815
|
binary: false
|
|
2700
2816
|
}));
|
|
2701
|
-
|
|
2817
|
+
const files = [...tracked, ...untracked];
|
|
2818
|
+
return {
|
|
2819
|
+
isGitRepo: true,
|
|
2820
|
+
branch,
|
|
2821
|
+
baseRef: base.ref,
|
|
2822
|
+
files: baseline ? await dropPreexisting(cwd, files, baseline) : files
|
|
2823
|
+
};
|
|
2702
2824
|
}
|
|
2703
|
-
async function enrichSessionWithGitMetadata(session) {
|
|
2825
|
+
async function enrichSessionWithGitMetadata(session, baseline) {
|
|
2704
2826
|
const [branch, repositoryFullName, status, lastActivityAt] = await Promise.all([
|
|
2705
2827
|
getCurrentBranch(session.cwd),
|
|
2706
2828
|
getRepositoryFullName(session.cwd),
|
|
2707
|
-
getWorkspaceStatus(session.cwd),
|
|
2829
|
+
getWorkspaceStatus(session.cwd, baseline),
|
|
2708
2830
|
getPaneActivity(session.tmuxSessionName)
|
|
2709
2831
|
]);
|
|
2710
2832
|
const changedFileCount = status.isGitRepo ? new Set(status.files.map((f) => f.path)).size : null;
|
|
@@ -2719,7 +2841,7 @@ async function enrichSessionWithGitMetadata(session) {
|
|
|
2719
2841
|
|
|
2720
2842
|
// src/sessions/git/diff.ts
|
|
2721
2843
|
async function withRenameSource(cwd, baseSha, path) {
|
|
2722
|
-
const nameStatus = await runGitDiff(["--name-status", "-z", "-M", baseSha], cwd);
|
|
2844
|
+
const nameStatus = await runGitDiff(["--relative", "--name-status", "-z", "-M", baseSha], cwd);
|
|
2723
2845
|
const previousPath = parseNameStatus(nameStatus).find((c) => c.path === path)?.previousPath;
|
|
2724
2846
|
return previousPath === void 0 ? [path] : [previousPath, path];
|
|
2725
2847
|
}
|
|
@@ -2727,15 +2849,18 @@ async function isUntracked(cwd, path) {
|
|
|
2727
2849
|
const out = await runGit(["ls-files", "--others", "--exclude-standard", "--", path], cwd);
|
|
2728
2850
|
return out.trim() !== "";
|
|
2729
2851
|
}
|
|
2730
|
-
async function getWorkspaceDiff(cwd, path, staged) {
|
|
2731
|
-
const base = await
|
|
2852
|
+
async function getWorkspaceDiff(cwd, path, staged, baseline) {
|
|
2853
|
+
const base = await resolveBaseFor(cwd, baseline);
|
|
2732
2854
|
const pathspec = await withRenameSource(cwd, base.sha, path);
|
|
2733
|
-
const numstat = await runGitDiff(
|
|
2855
|
+
const numstat = await runGitDiff(
|
|
2856
|
+
["--relative", "--numstat", "-M", base.sha, "--", ...pathspec],
|
|
2857
|
+
cwd
|
|
2858
|
+
);
|
|
2734
2859
|
const binary = /^(-\t-\t)/.test(numstat.trim());
|
|
2735
2860
|
if (binary) {
|
|
2736
2861
|
return { path, staged, patch: null, binary: true };
|
|
2737
2862
|
}
|
|
2738
|
-
let patch = await runGitDiff(["-M", base.sha, "--", ...pathspec], cwd);
|
|
2863
|
+
let patch = await runGitDiff(["--relative", "-M", base.sha, "--", ...pathspec], cwd);
|
|
2739
2864
|
if (patch.trim() === "" && await isUntracked(cwd, path)) {
|
|
2740
2865
|
patch = await runGitDiffAllowingExitCodes(["--no-index", "--", "/dev/null", path], cwd, [
|
|
2741
2866
|
1
|
|
@@ -2814,7 +2939,7 @@ ${new TmuxNotFoundError().message}
|
|
|
2814
2939
|
}
|
|
2815
2940
|
return false;
|
|
2816
2941
|
}
|
|
2817
|
-
const created = { process: ptyProcess,
|
|
2942
|
+
const created = { process: ptyProcess, dataListeners: /* @__PURE__ */ new Set([onData]) };
|
|
2818
2943
|
this.handles.set(sessionId, created);
|
|
2819
2944
|
ptyProcess.onData((data) => {
|
|
2820
2945
|
for (const listener of created.dataListeners) listener(data);
|
|
@@ -2822,12 +2947,9 @@ ${new TmuxNotFoundError().message}
|
|
|
2822
2947
|
ptyProcess.onExit(() => {
|
|
2823
2948
|
if (this.handles.get(sessionId) === created) this.handles.delete(sessionId);
|
|
2824
2949
|
});
|
|
2825
|
-
created.refCount += 1;
|
|
2826
|
-
created.dataListeners.add(onData);
|
|
2827
2950
|
return true;
|
|
2828
2951
|
}
|
|
2829
2952
|
await this.sendPaneSnapshot(tmuxSessionName, onData);
|
|
2830
|
-
handle.refCount += 1;
|
|
2831
2953
|
handle.dataListeners.add(onData);
|
|
2832
2954
|
return true;
|
|
2833
2955
|
}
|
|
@@ -2854,8 +2976,7 @@ ${new TmuxNotFoundError().message}
|
|
|
2854
2976
|
release(sessionId, onData) {
|
|
2855
2977
|
const handle = this.handles.get(sessionId);
|
|
2856
2978
|
if (!handle?.dataListeners.delete(onData)) return;
|
|
2857
|
-
handle.
|
|
2858
|
-
if (handle.refCount <= 0) {
|
|
2979
|
+
if (handle.dataListeners.size === 0) {
|
|
2859
2980
|
handle.process.kill();
|
|
2860
2981
|
this.handles.delete(sessionId);
|
|
2861
2982
|
}
|
|
@@ -2936,7 +3057,7 @@ async function removeWorktree(sourceCwd, worktreePath) {
|
|
|
2936
3057
|
// src/sessions/tmux/create.ts
|
|
2937
3058
|
import { access } from "fs/promises";
|
|
2938
3059
|
import { homedir as homedir5 } from "os";
|
|
2939
|
-
import { join as
|
|
3060
|
+
import { join as join6 } from "path";
|
|
2940
3061
|
var InvalidCwdError = class extends Error {
|
|
2941
3062
|
constructor(cwd) {
|
|
2942
3063
|
super(`working directory does not exist: ${cwd}`);
|
|
@@ -2944,7 +3065,7 @@ var InvalidCwdError = class extends Error {
|
|
|
2944
3065
|
}
|
|
2945
3066
|
};
|
|
2946
3067
|
function worktreePathFor(sessionId) {
|
|
2947
|
-
return
|
|
3068
|
+
return join6(homedir5(), ".bondsmith", "worktrees", sessionId);
|
|
2948
3069
|
}
|
|
2949
3070
|
async function createTmuxSession(input) {
|
|
2950
3071
|
const sourceCwd = expandHome(input.cwd);
|
|
@@ -2963,6 +3084,7 @@ async function createTmuxSession(input) {
|
|
|
2963
3084
|
}
|
|
2964
3085
|
const tmuxSessionName = tmuxSessionNameFor(input.id);
|
|
2965
3086
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3087
|
+
const baseline = await captureBaseline(cwd, { freshCheckout: worktreeOf !== null });
|
|
2966
3088
|
await runTmux(["new-session", "-d", "-s", tmuxSessionName, "-c", cwd, input.command], {
|
|
2967
3089
|
env: filterSpawnEnv(process.env)
|
|
2968
3090
|
});
|
|
@@ -2974,6 +3096,15 @@ async function createTmuxSession(input) {
|
|
|
2974
3096
|
if (worktreeOf) {
|
|
2975
3097
|
await runTmux(["set-option", "-t", tmuxSessionName, "@bondsmith-worktree-of", worktreeOf]);
|
|
2976
3098
|
}
|
|
3099
|
+
if (baseline) {
|
|
3100
|
+
await runTmux([
|
|
3101
|
+
"set-option",
|
|
3102
|
+
"-t",
|
|
3103
|
+
tmuxSessionName,
|
|
3104
|
+
"@bondsmith-baseline",
|
|
3105
|
+
encodeBaseline(baseline)
|
|
3106
|
+
]);
|
|
3107
|
+
}
|
|
2977
3108
|
const session = {
|
|
2978
3109
|
id: input.id,
|
|
2979
3110
|
title: input.title,
|
|
@@ -2988,7 +3119,7 @@ async function createTmuxSession(input) {
|
|
|
2988
3119
|
changedFileCount: null,
|
|
2989
3120
|
worktreeOf
|
|
2990
3121
|
};
|
|
2991
|
-
return enrichSessionWithGitMetadata(session);
|
|
3122
|
+
return { session: await enrichSessionWithGitMetadata(session, baseline), baseline };
|
|
2992
3123
|
}
|
|
2993
3124
|
async function stopTmuxSession(tmuxSessionName) {
|
|
2994
3125
|
await runTmux(["kill-session", "-t", tmuxSessionName]);
|
|
@@ -2996,6 +3127,7 @@ async function stopTmuxSession(tmuxSessionName) {
|
|
|
2996
3127
|
|
|
2997
3128
|
// src/sessions/tmux/parse.ts
|
|
2998
3129
|
var FIELD_COUNT = 7;
|
|
3130
|
+
var BASELINE_FIELD_INDEX = 7;
|
|
2999
3131
|
function parseTmuxSessions(output) {
|
|
3000
3132
|
const sessions = [];
|
|
3001
3133
|
for (const line of output.split("\n")) {
|
|
@@ -3025,6 +3157,19 @@ function parseTmuxSessions(output) {
|
|
|
3025
3157
|
}
|
|
3026
3158
|
return sessions;
|
|
3027
3159
|
}
|
|
3160
|
+
function parseSessionBaselines(output) {
|
|
3161
|
+
const baselines = /* @__PURE__ */ new Map();
|
|
3162
|
+
for (const line of output.split("\n")) {
|
|
3163
|
+
const fields = line.split(" ");
|
|
3164
|
+
const sessionName = fields[0];
|
|
3165
|
+
const id = fields[1];
|
|
3166
|
+
const encoded = fields[BASELINE_FIELD_INDEX];
|
|
3167
|
+
if (!sessionName || !isBondsmithTmuxSessionName(sessionName) || !id || !encoded) continue;
|
|
3168
|
+
const baseline = decodeBaseline(encoded);
|
|
3169
|
+
if (baseline) baselines.set(id, baseline);
|
|
3170
|
+
}
|
|
3171
|
+
return baselines;
|
|
3172
|
+
}
|
|
3028
3173
|
|
|
3029
3174
|
// src/sessions/tmux/discover.ts
|
|
3030
3175
|
var LIST_FORMAT = [
|
|
@@ -3034,15 +3179,21 @@ var LIST_FORMAT = [
|
|
|
3034
3179
|
"#{@bondsmith-command}",
|
|
3035
3180
|
"#{@bondsmith-cwd}",
|
|
3036
3181
|
"#{@bondsmith-started-at}",
|
|
3037
|
-
"#{@bondsmith-worktree-of}"
|
|
3182
|
+
"#{@bondsmith-worktree-of}",
|
|
3183
|
+
"#{@bondsmith-baseline}"
|
|
3038
3184
|
].join(" ");
|
|
3039
3185
|
async function discoverSessions() {
|
|
3040
3186
|
try {
|
|
3041
3187
|
const output = await listTmuxSessions(LIST_FORMAT);
|
|
3042
|
-
const
|
|
3043
|
-
|
|
3188
|
+
const baselines = parseSessionBaselines(output);
|
|
3189
|
+
const sessions = await Promise.all(
|
|
3190
|
+
parseTmuxSessions(output).map(
|
|
3191
|
+
(session) => enrichSessionWithGitMetadata(session, baselines.get(session.id))
|
|
3192
|
+
)
|
|
3193
|
+
);
|
|
3194
|
+
return { sessions, baselines };
|
|
3044
3195
|
} catch (error2) {
|
|
3045
|
-
if (error2 instanceof TmuxNoServerRunningError) return [];
|
|
3196
|
+
if (error2 instanceof TmuxNoServerRunningError) return { sessions: [], baselines: /* @__PURE__ */ new Map() };
|
|
3046
3197
|
if (error2 instanceof TmuxNotFoundError) {
|
|
3047
3198
|
console.error(`
|
|
3048
3199
|
${error2.message}
|
|
@@ -3056,13 +3207,19 @@ ${error2.message}
|
|
|
3056
3207
|
// src/sessions/session-manager.ts
|
|
3057
3208
|
var SessionManager = class {
|
|
3058
3209
|
sessions = /* @__PURE__ */ new Map();
|
|
3210
|
+
// What each session's folder looked like when it started, so its file list shows only its own changes.
|
|
3211
|
+
baselines = /* @__PURE__ */ new Map();
|
|
3059
3212
|
listeners = /* @__PURE__ */ new Set();
|
|
3060
3213
|
async initialize() {
|
|
3061
3214
|
const discovered = await discoverSessions();
|
|
3062
3215
|
this.sessions.clear();
|
|
3063
|
-
|
|
3216
|
+
this.baselines.clear();
|
|
3217
|
+
for (const session of discovered.sessions) {
|
|
3064
3218
|
this.sessions.set(session.id, session);
|
|
3065
3219
|
}
|
|
3220
|
+
for (const [id, baseline] of discovered.baselines) {
|
|
3221
|
+
this.baselines.set(id, baseline);
|
|
3222
|
+
}
|
|
3066
3223
|
}
|
|
3067
3224
|
list() {
|
|
3068
3225
|
return Array.from(this.sessions.values());
|
|
@@ -3070,6 +3227,10 @@ var SessionManager = class {
|
|
|
3070
3227
|
get(sessionId) {
|
|
3071
3228
|
return this.sessions.get(sessionId);
|
|
3072
3229
|
}
|
|
3230
|
+
/** Undefined for a session started before baselines existed, which compares against the default branch. */
|
|
3231
|
+
getBaseline(sessionId) {
|
|
3232
|
+
return this.baselines.get(sessionId);
|
|
3233
|
+
}
|
|
3073
3234
|
onSessionUpdated(listener) {
|
|
3074
3235
|
this.listeners.add(listener);
|
|
3075
3236
|
return () => this.listeners.delete(listener);
|
|
@@ -3079,8 +3240,9 @@ var SessionManager = class {
|
|
|
3079
3240
|
}
|
|
3080
3241
|
async create(input) {
|
|
3081
3242
|
const id = uuidv4();
|
|
3082
|
-
const session = await createTmuxSession({ id, ...input });
|
|
3243
|
+
const { session, baseline } = await createTmuxSession({ id, ...input });
|
|
3083
3244
|
this.sessions.set(id, session);
|
|
3245
|
+
if (baseline) this.baselines.set(id, baseline);
|
|
3084
3246
|
return session;
|
|
3085
3247
|
}
|
|
3086
3248
|
async stop(sessionId) {
|
|
@@ -3140,7 +3302,10 @@ ${error2.message}
|
|
|
3140
3302
|
return;
|
|
3141
3303
|
}
|
|
3142
3304
|
try {
|
|
3143
|
-
const updated = await enrichSessionWithGitMetadata(
|
|
3305
|
+
const updated = await enrichSessionWithGitMetadata(
|
|
3306
|
+
session,
|
|
3307
|
+
this.baselines.get(session.id)
|
|
3308
|
+
);
|
|
3144
3309
|
if (!gitSnapshotEquals(session, updated)) {
|
|
3145
3310
|
this.sessions.set(session.id, updated);
|
|
3146
3311
|
this.emitUpdated(updated);
|
|
@@ -3157,6 +3322,7 @@ ${error2.message}
|
|
|
3157
3322
|
removeAsExited(session) {
|
|
3158
3323
|
const updated = { ...session, status: "exited" };
|
|
3159
3324
|
this.sessions.delete(session.id);
|
|
3325
|
+
this.baselines.delete(session.id);
|
|
3160
3326
|
this.emitUpdated(updated);
|
|
3161
3327
|
return updated;
|
|
3162
3328
|
}
|
|
@@ -3317,10 +3483,10 @@ if (isVersionRequest(process.argv.slice(2))) {
|
|
|
3317
3483
|
process.exit(0);
|
|
3318
3484
|
}
|
|
3319
3485
|
function loadRepoRootEnv() {
|
|
3320
|
-
const repoRoot =
|
|
3321
|
-
if (!existsSync(
|
|
3322
|
-
const envLocal =
|
|
3323
|
-
const envDefault =
|
|
3486
|
+
const repoRoot = join7(dirname4(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
3487
|
+
if (!existsSync(join7(repoRoot, "pnpm-workspace.yaml"))) return;
|
|
3488
|
+
const envLocal = join7(repoRoot, ".env.local");
|
|
3489
|
+
const envDefault = join7(repoRoot, ".env");
|
|
3324
3490
|
loadEnv({ path: existsSync(envLocal) ? envLocal : envDefault });
|
|
3325
3491
|
}
|
|
3326
3492
|
loadRepoRootEnv();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bondsmith/bondsmithd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Local daemon for Bondsmith agent sessions — runs on the user's own machine, pairs with a Bondsmith account, and exposes tmux-backed terminal sessions to the Bondsmith web app over a signed WebSocket protocol.",
|
|
6
6
|
"homepage": "https://bondsmith.dev",
|