@bondsmith/bondsmithd 0.1.2 → 0.1.3
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/README.md +14 -0
- package/dist/index.js +342 -110
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,20 @@ BONDSMITH_RELAY_URL=ws://127.0.0.1:8787 # defaults to production; set this
|
|
|
43
43
|
`BONDSMITH_DEV_TUNNEL_TOKEN` also exists but only applies to the legacy direct-connection path
|
|
44
44
|
(`BONDSMITH_RELAY_ENABLED=false`) — should not be set by hand for a normal install.
|
|
45
45
|
|
|
46
|
+
## Updating
|
|
47
|
+
|
|
48
|
+
Stop the running daemon, then update it and start it again:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install -g @bondsmith/bondsmithd@latest
|
|
52
|
+
bondsmithd
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
If you run it with `npx`, use `npx @bondsmith/bondsmithd@latest` so a cached older copy isn't reused.
|
|
56
|
+
|
|
57
|
+
When the web app and an older daemon disagree, Bondsmith shows "couldn't read a message from your
|
|
58
|
+
daemon". Reload the page first; if that doesn't fix it, update the daemon as above.
|
|
59
|
+
|
|
46
60
|
## What it does
|
|
47
61
|
|
|
48
62
|
- Creates, lists, attaches to, and stops tmux sessions prefixed `bondsmith-*`.
|
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 join6 } from "path";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
|
|
10
10
|
// ../../packages/local-agent-protocol/src/session.ts
|
|
@@ -12,6 +12,19 @@ var SESSION_STATUSES = ["running", "exited"];
|
|
|
12
12
|
|
|
13
13
|
// ../../packages/local-agent-protocol/src/messages.ts
|
|
14
14
|
var REPLAYED_COMMAND_MESSAGE = "command already used (replay rejected)";
|
|
15
|
+
var INVALID_MESSAGE_ERROR = "invalid message";
|
|
16
|
+
|
|
17
|
+
// ../../packages/local-agent-protocol/src/protocol-version.ts
|
|
18
|
+
var PROTOCOL_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
// ../../packages/local-agent-protocol/src/project-path.ts
|
|
21
|
+
var PROJECT_PATH_INVALID_REASONS = [
|
|
22
|
+
"relative_path",
|
|
23
|
+
"not_found",
|
|
24
|
+
"not_a_directory",
|
|
25
|
+
"not_a_git_repo",
|
|
26
|
+
"permission_denied"
|
|
27
|
+
];
|
|
15
28
|
|
|
16
29
|
// ../../packages/local-agent-protocol/src/schemas.ts
|
|
17
30
|
import { z as z2 } from "zod";
|
|
@@ -55,7 +68,8 @@ var localSessionSchema = z2.object({
|
|
|
55
68
|
branch: z2.string().nullable(),
|
|
56
69
|
repositoryFullName: z2.string().nullable(),
|
|
57
70
|
lastActivityAt: z2.string().nullable(),
|
|
58
|
-
changedFileCount: z2.number().int().nonnegative().nullable()
|
|
71
|
+
changedFileCount: z2.number().int().nonnegative().nullable(),
|
|
72
|
+
worktreeOf: z2.string().nullable()
|
|
59
73
|
});
|
|
60
74
|
var workspaceFileEntrySchema = z2.object({
|
|
61
75
|
path: z2.string(),
|
|
@@ -77,6 +91,10 @@ var workspaceFileDiffSchema = z2.object({
|
|
|
77
91
|
patch: z2.string().nullable(),
|
|
78
92
|
binary: z2.boolean()
|
|
79
93
|
});
|
|
94
|
+
var projectPathValidationSchema = z2.discriminatedUnion("valid", [
|
|
95
|
+
z2.object({ valid: z2.literal(true), path: z2.string() }),
|
|
96
|
+
z2.object({ valid: z2.literal(false), reason: z2.enum(PROJECT_PATH_INVALID_REASONS) })
|
|
97
|
+
]);
|
|
80
98
|
var browserMessageSchema = z2.discriminatedUnion("type", [
|
|
81
99
|
z2.object({ type: z2.literal("session.list") }),
|
|
82
100
|
z2.object({
|
|
@@ -84,9 +102,34 @@ var browserMessageSchema = z2.discriminatedUnion("type", [
|
|
|
84
102
|
title: z2.string().min(1).max(200),
|
|
85
103
|
cwd: z2.string().min(1),
|
|
86
104
|
command: z2.string().min(1).max(2e3),
|
|
105
|
+
// Opt-in worktree creation — see messages.ts's own field comment.
|
|
106
|
+
// Most branch-name legality is left to git itself in createWorktree()
|
|
107
|
+
// (whose named error classes surface a rejection back to the
|
|
108
|
+
// browser), EXCEPT a leading "-": git's own argv parsing for
|
|
109
|
+
// `worktree add -b <branch>` treats a leading "-" as a flag, not a
|
|
110
|
+
// branch name, producing a raw multi-line `git branch` usage dump as
|
|
111
|
+
// the error instead of a clean, classified one (confirmed live
|
|
112
|
+
// during adversarial review) — rejected here instead, before it ever
|
|
113
|
+
// reaches git.
|
|
114
|
+
worktreeBranch: z2.string().min(1).max(250).refine((value) => !value.startsWith("-"), {
|
|
115
|
+
message: "branch name can't start with -"
|
|
116
|
+
}).optional(),
|
|
117
|
+
requestId: z2.string().min(1).max(200).optional()
|
|
118
|
+
}),
|
|
119
|
+
z2.object({
|
|
120
|
+
type: z2.literal("session.attach"),
|
|
121
|
+
sessionId: z2.string(),
|
|
122
|
+
// Optional (not required) so an older daemon build, or a stale queued
|
|
123
|
+
// envelope signed before this field existed, still parses — see
|
|
124
|
+
// session.attached/error's own requestId fields for why this matters:
|
|
125
|
+
// without a requestId round-tripped on both the ack and the error
|
|
126
|
+
// path, the browser can only guess which pending attachSession() call
|
|
127
|
+
// an incoming error/ack belongs to (oldest-pending heuristic), which
|
|
128
|
+
// misattributes a daemon error for ANY other in-flight command
|
|
129
|
+
// (observed: a session.resize racing ahead of its own session.attach)
|
|
130
|
+
// to the wrong promise — a confirmed bug, not a hypothetical.
|
|
87
131
|
requestId: z2.string().min(1).max(200).optional()
|
|
88
132
|
}),
|
|
89
|
-
z2.object({ type: z2.literal("session.attach"), sessionId: z2.string() }),
|
|
90
133
|
z2.object({
|
|
91
134
|
type: z2.literal("session.input"),
|
|
92
135
|
sessionId: z2.string(),
|
|
@@ -116,6 +159,11 @@ var browserMessageSchema = z2.discriminatedUnion("type", [
|
|
|
116
159
|
staged: z2.boolean(),
|
|
117
160
|
requestId: z2.string().min(1).max(200)
|
|
118
161
|
}),
|
|
162
|
+
z2.object({
|
|
163
|
+
type: z2.literal("project.validatePath"),
|
|
164
|
+
path: z2.string().min(1).max(4096),
|
|
165
|
+
requestId: z2.string().min(1).max(200)
|
|
166
|
+
}),
|
|
119
167
|
z2.object({ type: z2.literal("daemon.getInfo") }),
|
|
120
168
|
// Second-controller enrollment (ADR-011/ADR-012, BD-433) — deliberately
|
|
121
169
|
// unsigned: the requesting browser has no trusted key yet to sign with.
|
|
@@ -166,8 +214,22 @@ var daemonMessageSchema = z2.discriminatedUnion("type", [
|
|
|
166
214
|
// session" (observed during a reconnect: attachSession() was
|
|
167
215
|
// fire-and-forget, so terminal.onData could fire before attach
|
|
168
216
|
// completed).
|
|
169
|
-
z2.object({
|
|
170
|
-
|
|
217
|
+
z2.object({
|
|
218
|
+
type: z2.literal("session.attached"),
|
|
219
|
+
sessionId: z2.string(),
|
|
220
|
+
requestId: z2.string().optional()
|
|
221
|
+
}),
|
|
222
|
+
z2.object({
|
|
223
|
+
type: z2.literal("error"),
|
|
224
|
+
message: z2.string(),
|
|
225
|
+
// Echoes the triggering message's requestId when it had one, so the
|
|
226
|
+
// browser can match this error to the exact pending request instead
|
|
227
|
+
// of guessing "oldest pending create, then oldest pending attach" —
|
|
228
|
+
// undefined for error paths with no requestId to echo (most session.*
|
|
229
|
+
// commands still don't carry one; see session.attach above for the
|
|
230
|
+
// one this was added to fix).
|
|
231
|
+
requestId: z2.string().optional()
|
|
232
|
+
}),
|
|
171
233
|
z2.object({
|
|
172
234
|
type: z2.literal("workspace.status"),
|
|
173
235
|
sessionId: z2.string(),
|
|
@@ -180,6 +242,11 @@ var daemonMessageSchema = z2.discriminatedUnion("type", [
|
|
|
180
242
|
requestId: z2.string(),
|
|
181
243
|
diff: workspaceFileDiffSchema
|
|
182
244
|
}),
|
|
245
|
+
z2.object({
|
|
246
|
+
type: z2.literal("project.pathValidated"),
|
|
247
|
+
requestId: z2.string(),
|
|
248
|
+
result: projectPathValidationSchema
|
|
249
|
+
}),
|
|
183
250
|
z2.object({
|
|
184
251
|
type: z2.literal("daemon.info"),
|
|
185
252
|
hostname: z2.string(),
|
|
@@ -199,7 +266,10 @@ var daemonMessageSchema = z2.discriminatedUnion("type", [
|
|
|
199
266
|
challenge: z2.string().min(1),
|
|
200
267
|
daemonFingerprint: z2.string().min(1),
|
|
201
268
|
hostname: z2.string(),
|
|
202
|
-
expiresAt: z2.number().int().positive()
|
|
269
|
+
expiresAt: z2.number().int().positive(),
|
|
270
|
+
// .catch(undefined): a malformed informational field must never fail the whole handshake message.
|
|
271
|
+
protocolVersion: z2.number().int().positive().optional().catch(void 0),
|
|
272
|
+
daemonVersion: z2.string().optional().catch(void 0)
|
|
203
273
|
}),
|
|
204
274
|
z2.object({
|
|
205
275
|
type: z2.literal("connection.authenticated"),
|
|
@@ -284,7 +354,8 @@ var SIGNABLE_ACTIONS = [
|
|
|
284
354
|
"session.stop",
|
|
285
355
|
"session.list",
|
|
286
356
|
"workspace.getStatus",
|
|
287
|
-
"workspace.getDiff"
|
|
357
|
+
"workspace.getDiff",
|
|
358
|
+
"project.validatePath"
|
|
288
359
|
];
|
|
289
360
|
var envelopeSchema = z3.object({
|
|
290
361
|
version: z3.literal(1),
|
|
@@ -613,7 +684,7 @@ async function callTRPCQuery(baseUrl, procedure, input) {
|
|
|
613
684
|
// src/pairing/pairing.ts
|
|
614
685
|
var DEFAULT_POLL_INTERVAL_MS = 3e3;
|
|
615
686
|
function sleep(ms) {
|
|
616
|
-
return new Promise((
|
|
687
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
617
688
|
}
|
|
618
689
|
async function ensurePaired(options) {
|
|
619
690
|
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
@@ -750,14 +821,101 @@ async function bindFirstController(params, response) {
|
|
|
750
821
|
}
|
|
751
822
|
}
|
|
752
823
|
|
|
824
|
+
// src/projects/validate-path.ts
|
|
825
|
+
import { stat } from "fs/promises";
|
|
826
|
+
import { isAbsolute, resolve } from "path";
|
|
827
|
+
|
|
828
|
+
// src/sessions/expand-home.ts
|
|
829
|
+
import { homedir } from "os";
|
|
830
|
+
import { join } from "path";
|
|
831
|
+
function expandHome(cwd) {
|
|
832
|
+
if (cwd === "~") return homedir();
|
|
833
|
+
if (cwd.startsWith("~/")) return join(homedir(), cwd.slice(2));
|
|
834
|
+
return cwd;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/sessions/git/client.ts
|
|
838
|
+
import { execFile as execFileCb } from "child_process";
|
|
839
|
+
import { promisify } from "util";
|
|
840
|
+
var execFile = promisify(execFileCb);
|
|
841
|
+
var GitNotARepoError = class extends Error {
|
|
842
|
+
constructor() {
|
|
843
|
+
super("not a git repository");
|
|
844
|
+
this.name = "GitNotARepoError";
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
function isNotARepoError(error2) {
|
|
848
|
+
if (typeof error2 !== "object" || error2 === null) return false;
|
|
849
|
+
const stderr = error2.stderr;
|
|
850
|
+
return typeof stderr === "string" && stderr.includes("not a git repository");
|
|
851
|
+
}
|
|
852
|
+
async function runGit(args, cwd) {
|
|
853
|
+
try {
|
|
854
|
+
const { stdout } = await execFile("git", args, { cwd });
|
|
855
|
+
return stdout;
|
|
856
|
+
} catch (error2) {
|
|
857
|
+
if (isNotARepoError(error2)) throw new GitNotARepoError();
|
|
858
|
+
throw error2;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async function runGitAllowingExitCodes(args, cwd, allowedExitCodes) {
|
|
862
|
+
try {
|
|
863
|
+
const { stdout } = await execFile("git", args, { cwd });
|
|
864
|
+
return stdout;
|
|
865
|
+
} catch (error2) {
|
|
866
|
+
if (isNotARepoError(error2)) throw new GitNotARepoError();
|
|
867
|
+
const code = error2.code;
|
|
868
|
+
const stdout = error2.stdout;
|
|
869
|
+
if (typeof code === "number" && allowedExitCodes.includes(code) && typeof stdout === "string") {
|
|
870
|
+
return stdout;
|
|
871
|
+
}
|
|
872
|
+
throw error2;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// src/projects/validate-path.ts
|
|
877
|
+
var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["ENOENT", "ENOTDIR"]);
|
|
878
|
+
var PERMISSION_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM"]);
|
|
879
|
+
function invalidReasonForFsError(error2) {
|
|
880
|
+
const code = error2?.code;
|
|
881
|
+
if (typeof code !== "string") return null;
|
|
882
|
+
if (NOT_FOUND_CODES.has(code)) return "not_found";
|
|
883
|
+
if (PERMISSION_CODES.has(code)) return "permission_denied";
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
async function validateProjectPath(input) {
|
|
887
|
+
const expanded = expandHome(input.trim());
|
|
888
|
+
if (!isAbsolute(expanded)) return { valid: false, reason: "relative_path" };
|
|
889
|
+
const path = resolve(expanded);
|
|
890
|
+
try {
|
|
891
|
+
const stats = await stat(path);
|
|
892
|
+
if (!stats.isDirectory()) return { valid: false, reason: "not_a_directory" };
|
|
893
|
+
} catch (error2) {
|
|
894
|
+
const reason = invalidReasonForFsError(error2);
|
|
895
|
+
if (reason) return { valid: false, reason };
|
|
896
|
+
throw error2;
|
|
897
|
+
}
|
|
898
|
+
try {
|
|
899
|
+
const insideWorkTree = (await runGit(["rev-parse", "--is-inside-work-tree"], path)).trim();
|
|
900
|
+
if (insideWorkTree !== "true") return { valid: false, reason: "not_a_git_repo" };
|
|
901
|
+
} catch (error2) {
|
|
902
|
+
if (error2 instanceof GitNotARepoError) return { valid: false, reason: "not_a_git_repo" };
|
|
903
|
+
throw error2;
|
|
904
|
+
}
|
|
905
|
+
return { valid: true, path };
|
|
906
|
+
}
|
|
907
|
+
|
|
753
908
|
// src/relay/relay-client.ts
|
|
754
909
|
import { sign } from "crypto";
|
|
755
910
|
import WebSocket from "ws";
|
|
756
911
|
|
|
912
|
+
// src/version.ts
|
|
913
|
+
var DAEMON_VERSION = true ? "0.1.3" : "dev";
|
|
914
|
+
|
|
757
915
|
// src/sessions/tmux/client.ts
|
|
758
|
-
import { execFile as
|
|
759
|
-
import { promisify } from "util";
|
|
760
|
-
var
|
|
916
|
+
import { execFile as execFileCb2 } from "child_process";
|
|
917
|
+
import { promisify as promisify2 } from "util";
|
|
918
|
+
var execFile2 = promisify2(execFileCb2);
|
|
761
919
|
var TmuxNoServerRunningError = class extends Error {
|
|
762
920
|
constructor() {
|
|
763
921
|
super("no tmux server running");
|
|
@@ -793,7 +951,7 @@ function filterSpawnEnv(env) {
|
|
|
793
951
|
}
|
|
794
952
|
async function runTmux(args, options) {
|
|
795
953
|
try {
|
|
796
|
-
const { stdout } = await
|
|
954
|
+
const { stdout } = await execFile2("tmux", args, { ...options, encoding: "utf8" });
|
|
797
955
|
return stdout;
|
|
798
956
|
} catch (error2) {
|
|
799
957
|
if (error2?.code === "ENOENT") throw new TmuxNotFoundError();
|
|
@@ -834,8 +992,8 @@ import { verify as verify2 } from "crypto";
|
|
|
834
992
|
|
|
835
993
|
// src/security/trusted-controllers.ts
|
|
836
994
|
import { mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
837
|
-
import { homedir } from "os";
|
|
838
|
-
import { dirname, join } from "path";
|
|
995
|
+
import { homedir as homedir2 } from "os";
|
|
996
|
+
import { dirname, join as join2 } from "path";
|
|
839
997
|
import { z as z9 } from "zod";
|
|
840
998
|
var trustedControllerSchema = z9.object({
|
|
841
999
|
id: z9.string(),
|
|
@@ -858,7 +1016,7 @@ var trustedControllersFileSchema = z9.object({
|
|
|
858
1016
|
controllers: z9.array(trustedControllerSchema)
|
|
859
1017
|
});
|
|
860
1018
|
function defaultTrustedControllersPath() {
|
|
861
|
-
return
|
|
1019
|
+
return join2(homedir2(), ".bondsmith", "trusted-controllers.json");
|
|
862
1020
|
}
|
|
863
1021
|
async function loadTrustedControllers(path = defaultTrustedControllersPath()) {
|
|
864
1022
|
let raw;
|
|
@@ -1036,6 +1194,7 @@ var BRACKETED_PASTE_END = "\x1B[201~";
|
|
|
1036
1194
|
function createRouterConnection(connectionAuth, send, markControllerId, ptyAttachments) {
|
|
1037
1195
|
const outputListeners = /* @__PURE__ */ new Map();
|
|
1038
1196
|
const attachLocks = /* @__PURE__ */ new Map();
|
|
1197
|
+
let admissionChain = Promise.resolve();
|
|
1039
1198
|
const conn = {
|
|
1040
1199
|
subscriptions: /* @__PURE__ */ new Set(),
|
|
1041
1200
|
send,
|
|
@@ -1045,6 +1204,13 @@ function createRouterConnection(connectionAuth, send, markControllerId, ptyAttac
|
|
|
1045
1204
|
getOutputListener: (sessionId) => outputListeners.get(sessionId),
|
|
1046
1205
|
connectionAuth,
|
|
1047
1206
|
markControllerId,
|
|
1207
|
+
admitInOrder: (fn) => {
|
|
1208
|
+
const next = admissionChain.catch(() => {
|
|
1209
|
+
}).then(fn);
|
|
1210
|
+
admissionChain = next.catch(() => {
|
|
1211
|
+
});
|
|
1212
|
+
return next;
|
|
1213
|
+
},
|
|
1048
1214
|
withAttachLock: (sessionId, fn) => {
|
|
1049
1215
|
const previous = attachLocks.get(sessionId) ?? Promise.resolve();
|
|
1050
1216
|
const next = previous.catch(() => {
|
|
@@ -1065,8 +1231,8 @@ function createRouterConnection(connectionAuth, send, markControllerId, ptyAttac
|
|
|
1065
1231
|
}
|
|
1066
1232
|
return { conn, releaseAllSubscriptions };
|
|
1067
1233
|
}
|
|
1068
|
-
function error(message) {
|
|
1069
|
-
return { type: "error", message };
|
|
1234
|
+
function error(message, requestId) {
|
|
1235
|
+
return { type: "error", message, requestId };
|
|
1070
1236
|
}
|
|
1071
1237
|
function errorMessageOf(err, fallback) {
|
|
1072
1238
|
return err instanceof Error ? err.message : fallback;
|
|
@@ -1082,35 +1248,39 @@ async function handleBrowserMessage(conn, raw, deps) {
|
|
|
1082
1248
|
const { envelope } = signedCommand.data;
|
|
1083
1249
|
const innerMessage = browserMessageSchema.safeParse(envelope.payload);
|
|
1084
1250
|
if (!innerMessage.success || innerMessage.data.type !== envelope.action) {
|
|
1085
|
-
conn.send(error(
|
|
1086
|
-
return;
|
|
1087
|
-
}
|
|
1088
|
-
const verdict = await verifyCommand(
|
|
1089
|
-
signedCommand.data,
|
|
1090
|
-
deps.daemonFingerprint,
|
|
1091
|
-
conn.connectionAuth,
|
|
1092
|
-
deps.trustedControllersPath
|
|
1093
|
-
);
|
|
1094
|
-
if (!verdict.ok) {
|
|
1095
|
-
conn.send(error(verifyRejectionMessage(verdict.reason)));
|
|
1096
|
-
return;
|
|
1097
|
-
}
|
|
1098
|
-
if (conn.connectionAuth.isRevoked()) {
|
|
1099
|
-
conn.send(error(REVOKED_REJECTION_MESSAGE));
|
|
1251
|
+
conn.send(error(INVALID_MESSAGE_ERROR));
|
|
1100
1252
|
return;
|
|
1101
1253
|
}
|
|
1254
|
+
const admitted = await conn.admitInOrder(async () => {
|
|
1255
|
+
const verdict = await verifyCommand(
|
|
1256
|
+
signedCommand.data,
|
|
1257
|
+
deps.daemonFingerprint,
|
|
1258
|
+
conn.connectionAuth,
|
|
1259
|
+
deps.trustedControllersPath
|
|
1260
|
+
);
|
|
1261
|
+
if (!verdict.ok) {
|
|
1262
|
+
conn.send(error(verifyRejectionMessage(verdict.reason)));
|
|
1263
|
+
return false;
|
|
1264
|
+
}
|
|
1265
|
+
if (conn.connectionAuth.isRevoked()) {
|
|
1266
|
+
conn.send(error(REVOKED_REJECTION_MESSAGE));
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
return true;
|
|
1270
|
+
});
|
|
1271
|
+
if (!admitted) return;
|
|
1102
1272
|
await dispatch(conn, innerMessage.data, deps);
|
|
1103
1273
|
return;
|
|
1104
1274
|
}
|
|
1105
1275
|
const parsed = browserMessageSchema.safeParse(raw);
|
|
1106
1276
|
if (!parsed.success) {
|
|
1107
|
-
conn.send(error(
|
|
1277
|
+
conn.send(error(INVALID_MESSAGE_ERROR));
|
|
1108
1278
|
return;
|
|
1109
1279
|
}
|
|
1110
1280
|
if (parsed.data.type === "connection.authenticate") {
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
deps.trustedControllersPath
|
|
1281
|
+
const { signed } = parsed.data;
|
|
1282
|
+
const result = await conn.admitInOrder(
|
|
1283
|
+
() => conn.connectionAuth.authenticate(signed, deps.trustedControllersPath)
|
|
1114
1284
|
);
|
|
1115
1285
|
if (!result.ok) {
|
|
1116
1286
|
conn.send({ type: "connection.authenticationFailed", reason: result.reason });
|
|
@@ -1132,6 +1302,7 @@ async function dispatch(conn, message, {
|
|
|
1132
1302
|
broadcast,
|
|
1133
1303
|
getWorkspaceStatus: getWorkspaceStatus2,
|
|
1134
1304
|
getWorkspaceDiff: getWorkspaceDiff2,
|
|
1305
|
+
validateProjectPath: validateProjectPath2,
|
|
1135
1306
|
hostname: hostname3,
|
|
1136
1307
|
daemonFingerprint,
|
|
1137
1308
|
pendingAuthorizations,
|
|
@@ -1143,22 +1314,26 @@ async function dispatch(conn, message, {
|
|
|
1143
1314
|
return;
|
|
1144
1315
|
}
|
|
1145
1316
|
case "session.create": {
|
|
1146
|
-
const { title, cwd, command, requestId } = message;
|
|
1317
|
+
const { title, cwd, command, worktreeBranch, requestId } = message;
|
|
1147
1318
|
try {
|
|
1148
|
-
const session = await sessionManager.create({ title, cwd, command });
|
|
1319
|
+
const session = await sessionManager.create({ title, cwd, command, worktreeBranch });
|
|
1149
1320
|
broadcast({ type: "session.created", session, requestId });
|
|
1150
1321
|
} catch (err) {
|
|
1151
|
-
conn.send(error(errorMessageOf(err, "failed to create session")));
|
|
1322
|
+
conn.send(error(errorMessageOf(err, "failed to create session"), requestId));
|
|
1152
1323
|
}
|
|
1153
1324
|
return;
|
|
1154
1325
|
}
|
|
1155
1326
|
case "session.attach": {
|
|
1327
|
+
const { requestId } = message;
|
|
1156
1328
|
if (!isValidSessionId(message.sessionId)) {
|
|
1157
|
-
conn.send(error("invalid session id"));
|
|
1329
|
+
conn.send(error("invalid session id", requestId));
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
const session = sessionManager.get(message.sessionId);
|
|
1333
|
+
if (!session) {
|
|
1334
|
+
conn.send(error("unknown session", requestId));
|
|
1158
1335
|
return;
|
|
1159
1336
|
}
|
|
1160
|
-
const session = getSessionOrError(sessionManager, conn, message.sessionId);
|
|
1161
|
-
if (!session) return;
|
|
1162
1337
|
await conn.withAttachLock(message.sessionId, async () => {
|
|
1163
1338
|
const previousListener = conn.getOutputListener(message.sessionId);
|
|
1164
1339
|
const onData = (data) => {
|
|
@@ -1172,7 +1347,7 @@ async function dispatch(conn, message, {
|
|
|
1172
1347
|
)) {
|
|
1173
1348
|
conn.subscriptions.add(message.sessionId);
|
|
1174
1349
|
conn.registerOutputListener(message.sessionId, onData);
|
|
1175
|
-
conn.send({ type: "session.attached", sessionId: message.sessionId });
|
|
1350
|
+
conn.send({ type: "session.attached", sessionId: message.sessionId, requestId });
|
|
1176
1351
|
return;
|
|
1177
1352
|
}
|
|
1178
1353
|
if (previousListener) {
|
|
@@ -1184,12 +1359,12 @@ async function dispatch(conn, message, {
|
|
|
1184
1359
|
onData
|
|
1185
1360
|
);
|
|
1186
1361
|
if (!attached) {
|
|
1187
|
-
conn.send(error("failed to attach to session"));
|
|
1362
|
+
conn.send(error("failed to attach to session", requestId));
|
|
1188
1363
|
return;
|
|
1189
1364
|
}
|
|
1190
1365
|
conn.subscriptions.add(message.sessionId);
|
|
1191
1366
|
conn.registerOutputListener(message.sessionId, onData);
|
|
1192
|
-
conn.send({ type: "session.attached", sessionId: message.sessionId });
|
|
1367
|
+
conn.send({ type: "session.attached", sessionId: message.sessionId, requestId });
|
|
1193
1368
|
});
|
|
1194
1369
|
return;
|
|
1195
1370
|
}
|
|
@@ -1265,6 +1440,15 @@ async function dispatch(conn, message, {
|
|
|
1265
1440
|
}
|
|
1266
1441
|
return;
|
|
1267
1442
|
}
|
|
1443
|
+
case "project.validatePath": {
|
|
1444
|
+
try {
|
|
1445
|
+
const result = await validateProjectPath2(message.path);
|
|
1446
|
+
conn.send({ type: "project.pathValidated", requestId: message.requestId, result });
|
|
1447
|
+
} catch (err) {
|
|
1448
|
+
conn.send(error(errorMessageOf(err, "failed to validate project path"), message.requestId));
|
|
1449
|
+
}
|
|
1450
|
+
return;
|
|
1451
|
+
}
|
|
1268
1452
|
case "daemon.getInfo": {
|
|
1269
1453
|
conn.send({ type: "daemon.info", hostname: hostname3(), daemonFingerprint });
|
|
1270
1454
|
return;
|
|
@@ -1615,7 +1799,9 @@ function startRelayClient(deps) {
|
|
|
1615
1799
|
challenge: challenge.challenge,
|
|
1616
1800
|
daemonFingerprint: deps.daemonFingerprint,
|
|
1617
1801
|
hostname: deps.hostname(),
|
|
1618
|
-
expiresAt: challenge.expiresAt
|
|
1802
|
+
expiresAt: challenge.expiresAt,
|
|
1803
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
1804
|
+
daemonVersion: DAEMON_VERSION
|
|
1619
1805
|
});
|
|
1620
1806
|
}
|
|
1621
1807
|
function handleFrame(frame) {
|
|
@@ -1713,10 +1899,10 @@ function startRelayClient(deps) {
|
|
|
1713
1899
|
// src/security/daemon-identity.ts
|
|
1714
1900
|
import { generateKeyPairSync, sign as sign2, verify as verify4 } from "crypto";
|
|
1715
1901
|
import { mkdir as mkdir2, readFile as readFile2, rename as rename2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
|
|
1716
|
-
import { homedir as
|
|
1717
|
-
import { dirname as dirname2, join as
|
|
1902
|
+
import { homedir as homedir3 } from "os";
|
|
1903
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
1718
1904
|
function defaultCredentialsPath() {
|
|
1719
|
-
return
|
|
1905
|
+
return join3(homedir3(), ".bondsmith", "credentials");
|
|
1720
1906
|
}
|
|
1721
1907
|
async function loadOrCreateDaemonIdentity(credentialsPath = defaultCredentialsPath()) {
|
|
1722
1908
|
const existing = await tryReadIdentity(credentialsPath);
|
|
@@ -1778,10 +1964,10 @@ function extractPemBlock(text, label) {
|
|
|
1778
1964
|
// src/security/dev-tunnel-token-file.ts
|
|
1779
1965
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
1780
1966
|
import { mkdir as mkdir3, readFile as readFile3, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
1781
|
-
import { homedir as
|
|
1782
|
-
import { dirname as dirname3, join as
|
|
1967
|
+
import { homedir as homedir4 } from "os";
|
|
1968
|
+
import { dirname as dirname3, join as join4 } from "path";
|
|
1783
1969
|
function defaultDevTunnelTokenPath() {
|
|
1784
|
-
return
|
|
1970
|
+
return join4(homedir4(), ".bondsmith", "dev-tunnel-token");
|
|
1785
1971
|
}
|
|
1786
1972
|
async function loadOrCreateDevTunnelToken(tokenPath = defaultDevTunnelTokenPath()) {
|
|
1787
1973
|
const existing = await tryReadToken(tokenPath);
|
|
@@ -1889,8 +2075,8 @@ var PendingAuthorizationStore = class {
|
|
|
1889
2075
|
requestToken: randomUUID()
|
|
1890
2076
|
};
|
|
1891
2077
|
let settleFn;
|
|
1892
|
-
const resultPromise = new Promise((
|
|
1893
|
-
settleFn =
|
|
2078
|
+
const resultPromise = new Promise((resolve2) => {
|
|
2079
|
+
settleFn = resolve2;
|
|
1894
2080
|
});
|
|
1895
2081
|
const entry = {
|
|
1896
2082
|
authorization,
|
|
@@ -2114,45 +2300,6 @@ function presentPendingAuthorizationOnTerminal(store) {
|
|
|
2114
2300
|
};
|
|
2115
2301
|
}
|
|
2116
2302
|
|
|
2117
|
-
// src/sessions/git/client.ts
|
|
2118
|
-
import { execFile as execFileCb2 } from "child_process";
|
|
2119
|
-
import { promisify as promisify2 } from "util";
|
|
2120
|
-
var execFile2 = promisify2(execFileCb2);
|
|
2121
|
-
var GitNotARepoError = class extends Error {
|
|
2122
|
-
constructor() {
|
|
2123
|
-
super("not a git repository");
|
|
2124
|
-
this.name = "GitNotARepoError";
|
|
2125
|
-
}
|
|
2126
|
-
};
|
|
2127
|
-
function isNotARepoError(error2) {
|
|
2128
|
-
if (typeof error2 !== "object" || error2 === null) return false;
|
|
2129
|
-
const stderr = error2.stderr;
|
|
2130
|
-
return typeof stderr === "string" && stderr.includes("not a git repository");
|
|
2131
|
-
}
|
|
2132
|
-
async function runGit(args, cwd) {
|
|
2133
|
-
try {
|
|
2134
|
-
const { stdout } = await execFile2("git", args, { cwd });
|
|
2135
|
-
return stdout;
|
|
2136
|
-
} catch (error2) {
|
|
2137
|
-
if (isNotARepoError(error2)) throw new GitNotARepoError();
|
|
2138
|
-
throw error2;
|
|
2139
|
-
}
|
|
2140
|
-
}
|
|
2141
|
-
async function runGitAllowingExitCodes(args, cwd, allowedExitCodes) {
|
|
2142
|
-
try {
|
|
2143
|
-
const { stdout } = await execFile2("git", args, { cwd });
|
|
2144
|
-
return stdout;
|
|
2145
|
-
} catch (error2) {
|
|
2146
|
-
if (isNotARepoError(error2)) throw new GitNotARepoError();
|
|
2147
|
-
const code = error2.code;
|
|
2148
|
-
const stdout = error2.stdout;
|
|
2149
|
-
if (typeof code === "number" && allowedExitCodes.includes(code) && typeof stdout === "string") {
|
|
2150
|
-
return stdout;
|
|
2151
|
-
}
|
|
2152
|
-
throw error2;
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
|
|
2156
2303
|
// src/sessions/git/diff.ts
|
|
2157
2304
|
async function getWorkspaceDiff(cwd, path, staged) {
|
|
2158
2305
|
const numstatArgs = staged ? ["diff", "--cached", "--numstat", "--", path] : ["diff", "--numstat", "--", path];
|
|
@@ -2442,23 +2589,84 @@ ${new TmuxNotFoundError().message}
|
|
|
2442
2589
|
// src/sessions/session-manager.ts
|
|
2443
2590
|
import { v4 as uuidv4 } from "uuid";
|
|
2444
2591
|
|
|
2592
|
+
// src/sessions/git/worktree.ts
|
|
2593
|
+
import { execFile as execFileCb3 } from "child_process";
|
|
2594
|
+
import { promisify as promisify3 } from "util";
|
|
2595
|
+
var execFile3 = promisify3(execFileCb3);
|
|
2596
|
+
var WorktreeBranchNameTakenError = class extends Error {
|
|
2597
|
+
constructor(branch) {
|
|
2598
|
+
super(`a branch named "${branch}" already exists`);
|
|
2599
|
+
this.name = "WorktreeBranchNameTakenError";
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
var WorktreePathExistsError = class extends Error {
|
|
2603
|
+
constructor(path) {
|
|
2604
|
+
super(`worktree path already exists: ${path}`);
|
|
2605
|
+
this.name = "WorktreePathExistsError";
|
|
2606
|
+
}
|
|
2607
|
+
};
|
|
2608
|
+
var WorktreeHasUncommittedChangesError = class extends Error {
|
|
2609
|
+
constructor(path) {
|
|
2610
|
+
super(
|
|
2611
|
+
`worktree at ${path} has uncommitted or untracked changes \u2014 not removing it. Commit or discard the changes, then remove it yourself (git worktree remove ${path}).`
|
|
2612
|
+
);
|
|
2613
|
+
this.name = "WorktreeHasUncommittedChangesError";
|
|
2614
|
+
}
|
|
2615
|
+
};
|
|
2616
|
+
function stderrOf(error2) {
|
|
2617
|
+
if (typeof error2 !== "object" || error2 === null) return "";
|
|
2618
|
+
const stderr = error2.stderr;
|
|
2619
|
+
return typeof stderr === "string" ? stderr : "";
|
|
2620
|
+
}
|
|
2621
|
+
async function createWorktree(sourceCwd, worktreePath, branch) {
|
|
2622
|
+
try {
|
|
2623
|
+
await execFile3("git", ["worktree", "add", "-b", branch, worktreePath], { cwd: sourceCwd });
|
|
2624
|
+
} catch (error2) {
|
|
2625
|
+
const stderr = stderrOf(error2);
|
|
2626
|
+
if (stderr.includes("a branch named")) {
|
|
2627
|
+
throw new WorktreeBranchNameTakenError(branch);
|
|
2628
|
+
}
|
|
2629
|
+
if (stderr.includes("already exists")) {
|
|
2630
|
+
throw new WorktreePathExistsError(worktreePath);
|
|
2631
|
+
}
|
|
2632
|
+
throw error2;
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
async function removeWorktree(sourceCwd, worktreePath) {
|
|
2636
|
+
try {
|
|
2637
|
+
await execFile3("git", ["worktree", "remove", worktreePath], { cwd: sourceCwd });
|
|
2638
|
+
} catch (error2) {
|
|
2639
|
+
const stderr = stderrOf(error2);
|
|
2640
|
+
if (stderr.includes("contains modified or untracked files")) {
|
|
2641
|
+
throw new WorktreeHasUncommittedChangesError(worktreePath);
|
|
2642
|
+
}
|
|
2643
|
+
throw error2;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2445
2647
|
// src/sessions/tmux/create.ts
|
|
2446
2648
|
import { access } from "fs/promises";
|
|
2447
|
-
import { homedir as
|
|
2448
|
-
import { join as
|
|
2649
|
+
import { homedir as homedir5 } from "os";
|
|
2650
|
+
import { join as join5 } from "path";
|
|
2449
2651
|
var InvalidCwdError = class extends Error {
|
|
2450
2652
|
constructor(cwd) {
|
|
2451
2653
|
super(`working directory does not exist: ${cwd}`);
|
|
2452
2654
|
this.name = "InvalidCwdError";
|
|
2453
2655
|
}
|
|
2454
2656
|
};
|
|
2455
|
-
function
|
|
2456
|
-
|
|
2457
|
-
if (cwd.startsWith("~/")) return join4(homedir4(), cwd.slice(2));
|
|
2458
|
-
return cwd;
|
|
2657
|
+
function worktreePathFor(sessionId) {
|
|
2658
|
+
return join5(homedir5(), ".bondsmith", "worktrees", sessionId);
|
|
2459
2659
|
}
|
|
2460
2660
|
async function createTmuxSession(input) {
|
|
2461
|
-
const
|
|
2661
|
+
const sourceCwd = expandHome(input.cwd);
|
|
2662
|
+
let cwd = sourceCwd;
|
|
2663
|
+
let worktreeOf = null;
|
|
2664
|
+
if (input.worktreeBranch) {
|
|
2665
|
+
const worktreePath = worktreePathFor(input.id);
|
|
2666
|
+
await createWorktree(sourceCwd, worktreePath, input.worktreeBranch);
|
|
2667
|
+
cwd = worktreePath;
|
|
2668
|
+
worktreeOf = sourceCwd;
|
|
2669
|
+
}
|
|
2462
2670
|
try {
|
|
2463
2671
|
await access(cwd);
|
|
2464
2672
|
} catch {
|
|
@@ -2474,6 +2682,9 @@ async function createTmuxSession(input) {
|
|
|
2474
2682
|
await runTmux(["set-option", "-t", tmuxSessionName, "@bondsmith-command", input.command]);
|
|
2475
2683
|
await runTmux(["set-option", "-t", tmuxSessionName, "@bondsmith-cwd", cwd]);
|
|
2476
2684
|
await runTmux(["set-option", "-t", tmuxSessionName, "@bondsmith-started-at", startedAt]);
|
|
2685
|
+
if (worktreeOf) {
|
|
2686
|
+
await runTmux(["set-option", "-t", tmuxSessionName, "@bondsmith-worktree-of", worktreeOf]);
|
|
2687
|
+
}
|
|
2477
2688
|
const session = {
|
|
2478
2689
|
id: input.id,
|
|
2479
2690
|
title: input.title,
|
|
@@ -2485,7 +2696,8 @@ async function createTmuxSession(input) {
|
|
|
2485
2696
|
branch: null,
|
|
2486
2697
|
repositoryFullName: null,
|
|
2487
2698
|
lastActivityAt: null,
|
|
2488
|
-
changedFileCount: null
|
|
2699
|
+
changedFileCount: null,
|
|
2700
|
+
worktreeOf
|
|
2489
2701
|
};
|
|
2490
2702
|
return enrichSessionWithGitMetadata(session);
|
|
2491
2703
|
}
|
|
@@ -2494,14 +2706,14 @@ async function stopTmuxSession(tmuxSessionName) {
|
|
|
2494
2706
|
}
|
|
2495
2707
|
|
|
2496
2708
|
// src/sessions/tmux/parse.ts
|
|
2497
|
-
var FIELD_COUNT =
|
|
2709
|
+
var FIELD_COUNT = 7;
|
|
2498
2710
|
function parseTmuxSessions(output) {
|
|
2499
2711
|
const sessions = [];
|
|
2500
2712
|
for (const line of output.split("\n")) {
|
|
2501
2713
|
if (!line.trim()) continue;
|
|
2502
2714
|
const fields = line.split(" ");
|
|
2503
2715
|
if (fields.length < FIELD_COUNT) continue;
|
|
2504
|
-
const [sessionName, id, title, command, cwd, startedAt] = fields;
|
|
2716
|
+
const [sessionName, id, title, command, cwd, startedAt, worktreeOf] = fields;
|
|
2505
2717
|
if (!isBondsmithTmuxSessionName(sessionName)) continue;
|
|
2506
2718
|
if (!id || !title || !command || !cwd || !startedAt) continue;
|
|
2507
2719
|
sessions.push({
|
|
@@ -2515,7 +2727,11 @@ function parseTmuxSessions(output) {
|
|
|
2515
2727
|
branch: null,
|
|
2516
2728
|
repositoryFullName: null,
|
|
2517
2729
|
lastActivityAt: null,
|
|
2518
|
-
changedFileCount: null
|
|
2730
|
+
changedFileCount: null,
|
|
2731
|
+
// tmux reports an unset user-option as an empty string via
|
|
2732
|
+
// #{...}, never literally absent — a non-worktree session (the
|
|
2733
|
+
// common case) has no @bondsmith-worktree-of option set at all.
|
|
2734
|
+
worktreeOf: worktreeOf ? worktreeOf : null
|
|
2519
2735
|
});
|
|
2520
2736
|
}
|
|
2521
2737
|
return sessions;
|
|
@@ -2528,7 +2744,8 @@ var LIST_FORMAT = [
|
|
|
2528
2744
|
"#{@bondsmith-title}",
|
|
2529
2745
|
"#{@bondsmith-command}",
|
|
2530
2746
|
"#{@bondsmith-cwd}",
|
|
2531
|
-
"#{@bondsmith-started-at}"
|
|
2747
|
+
"#{@bondsmith-started-at}",
|
|
2748
|
+
"#{@bondsmith-worktree-of}"
|
|
2532
2749
|
].join(" ");
|
|
2533
2750
|
async function discoverSessions() {
|
|
2534
2751
|
try {
|
|
@@ -2584,6 +2801,17 @@ var SessionManager = class {
|
|
|
2584
2801
|
await stopTmuxSession(session.tmuxSessionName);
|
|
2585
2802
|
} catch {
|
|
2586
2803
|
}
|
|
2804
|
+
if (session.worktreeOf) {
|
|
2805
|
+
try {
|
|
2806
|
+
await removeWorktree(session.worktreeOf, session.cwd);
|
|
2807
|
+
} catch (error2) {
|
|
2808
|
+
console.error("Failed to remove session worktree", {
|
|
2809
|
+
sessionId: session.id,
|
|
2810
|
+
worktreePath: session.cwd,
|
|
2811
|
+
error: error2
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2587
2815
|
return this.removeAsExited(session);
|
|
2588
2816
|
}
|
|
2589
2817
|
/**
|
|
@@ -2731,7 +2959,9 @@ function createDaemonWebSocketServer(httpServer, config, daemonFingerprint, pend
|
|
|
2731
2959
|
challenge: challenge.challenge,
|
|
2732
2960
|
daemonFingerprint,
|
|
2733
2961
|
hostname: hostname(),
|
|
2734
|
-
expiresAt: challenge.expiresAt
|
|
2962
|
+
expiresAt: challenge.expiresAt,
|
|
2963
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
2964
|
+
daemonVersion: DAEMON_VERSION
|
|
2735
2965
|
});
|
|
2736
2966
|
socket.on("message", (raw) => {
|
|
2737
2967
|
let parsed;
|
|
@@ -2747,6 +2977,7 @@ function createDaemonWebSocketServer(httpServer, config, daemonFingerprint, pend
|
|
|
2747
2977
|
broadcast,
|
|
2748
2978
|
getWorkspaceStatus,
|
|
2749
2979
|
getWorkspaceDiff,
|
|
2980
|
+
validateProjectPath,
|
|
2750
2981
|
hostname: () => hostname(),
|
|
2751
2982
|
daemonFingerprint,
|
|
2752
2983
|
pendingAuthorizations,
|
|
@@ -2782,10 +3013,10 @@ function createDaemonWebSocketServer(httpServer, config, daemonFingerprint, pend
|
|
|
2782
3013
|
|
|
2783
3014
|
// src/index.ts
|
|
2784
3015
|
function loadRepoRootEnv() {
|
|
2785
|
-
const repoRoot =
|
|
2786
|
-
if (!existsSync(
|
|
2787
|
-
const envLocal =
|
|
2788
|
-
const envDefault =
|
|
3016
|
+
const repoRoot = join6(dirname4(fileURLToPath(import.meta.url)), "..", "..", "..");
|
|
3017
|
+
if (!existsSync(join6(repoRoot, "pnpm-workspace.yaml"))) return;
|
|
3018
|
+
const envLocal = join6(repoRoot, ".env.local");
|
|
3019
|
+
const envDefault = join6(repoRoot, ".env");
|
|
2789
3020
|
loadEnv({ path: existsSync(envLocal) ? envLocal : envDefault });
|
|
2790
3021
|
}
|
|
2791
3022
|
loadRepoRootEnv();
|
|
@@ -2838,6 +3069,7 @@ async function start() {
|
|
|
2838
3069
|
ptyAttachments,
|
|
2839
3070
|
getWorkspaceStatus,
|
|
2840
3071
|
getWorkspaceDiff,
|
|
3072
|
+
validateProjectPath,
|
|
2841
3073
|
hostname: hostname2,
|
|
2842
3074
|
pendingAuthorizations,
|
|
2843
3075
|
presentPendingAuthorization
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bondsmith/bondsmithd",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
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",
|