@bondsmith/bondsmithd 0.1.16 → 0.1.17

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.
Files changed (2) hide show
  1. package/dist/index.js +397 -88
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -20,7 +20,7 @@ var REPLAYED_COMMAND_MESSAGE = "command already used (replay rejected)";
20
20
  var INVALID_MESSAGE_ERROR = "invalid message";
21
21
 
22
22
  // ../../packages/local-agent-protocol/src/protocol-version.ts
23
- var PROTOCOL_VERSION = 20;
23
+ var PROTOCOL_VERSION = 21;
24
24
 
25
25
  // ../../packages/local-agent-protocol/src/branch-name.ts
26
26
  var MAX_BRANCH_NAME_LENGTH = 250;
@@ -48,6 +48,19 @@ var PROJECT_PATH_INVALID_REASONS = [
48
48
  "permission_denied"
49
49
  ];
50
50
 
51
+ // ../../packages/local-agent-protocol/src/project-worktree.ts
52
+ var WORKTREE_REMOVE_REFUSALS = [
53
+ "not_found",
54
+ "locked",
55
+ "session_active",
56
+ "has_changes"
57
+ ];
58
+ var WORKTREE_BRANCH_OUTCOMES = ["deleted", "kept", "not_merged", "failed"];
59
+ var MAX_PROJECT_WORKTREES = 100;
60
+ var MAX_WORKTREE_PATH_LENGTH = 1024;
61
+ var MAX_WORKTREE_BRANCH_LENGTH = 255;
62
+ var MAX_WORKTREE_HEAD_LENGTH = 64;
63
+
51
64
  // ../../packages/local-agent-protocol/src/schemas.ts
52
65
  import { z as z3 } from "zod";
53
66
 
@@ -264,6 +277,36 @@ var projectPathValidationSchema = z3.discriminatedUnion("valid", [
264
277
  z3.object({ valid: z3.literal(false), reason: z3.enum(PROJECT_PATH_INVALID_REASONS) })
265
278
  ]);
266
279
  var noNul = (value) => !value.includes("\0");
280
+ var projectWorktreeSchema = z3.object({
281
+ path: z3.string().max(MAX_WORKTREE_PATH_LENGTH),
282
+ branch: z3.string().max(MAX_WORKTREE_BRANCH_LENGTH).nullable(),
283
+ head: z3.string().max(MAX_WORKTREE_HEAD_LENGTH),
284
+ prunable: z3.boolean(),
285
+ locked: z3.boolean(),
286
+ dirty: z3.boolean(),
287
+ sessionId: z3.string().nullable()
288
+ });
289
+ var worktreeRemoveResultSchema = z3.discriminatedUnion("removed", [
290
+ z3.object({ removed: z3.literal(true), branch: z3.enum(WORKTREE_BRANCH_OUTCOMES) }),
291
+ z3.object({ removed: z3.literal(false), reason: z3.enum(WORKTREE_REMOVE_REFUSALS) })
292
+ ]);
293
+ var WORKTREE_LIST_REPLY_OVERHEAD_BYTES = 4096;
294
+ var utf8Bytes = (value) => new TextEncoder().encode(JSON.stringify(value)).length;
295
+ function fitsWorktreeLimits(row) {
296
+ return row.path.length <= MAX_WORKTREE_PATH_LENGTH && (row.branch === null || row.branch.length <= MAX_WORKTREE_BRANCH_LENGTH) && row.head.length <= MAX_WORKTREE_HEAD_LENGTH;
297
+ }
298
+ function fitWorktreeReply(worktrees) {
299
+ const kept = [];
300
+ let bytes = WORKTREE_LIST_REPLY_OVERHEAD_BYTES;
301
+ for (const row of worktrees) {
302
+ if (!fitsWorktreeLimits(row)) continue;
303
+ const rowBytes = utf8Bytes(row) + 1;
304
+ if (kept.length >= MAX_PROJECT_WORKTREES || bytes + rowBytes > MAX_DAEMON_REPLY_BYTES) break;
305
+ kept.push(row);
306
+ bytes += rowBytes;
307
+ }
308
+ return { worktrees: kept, truncated: kept.length < worktrees.length };
309
+ }
267
310
  var worktreeSetupSchema = z3.object({
268
311
  copyGlobs: z3.array(z3.string().min(1).max(MAX_COPY_GLOB_LENGTH).refine(noNul).refine(isSafeCopyGlob)).max(MAX_COPY_GLOBS),
269
312
  setupCommand: z3.string().max(MAX_SETUP_COMMAND_LENGTH).refine(noNul).optional()
@@ -360,6 +403,19 @@ var browserMessageSchema = z3.discriminatedUnion("type", [
360
403
  path: z3.string().min(1).max(4096),
361
404
  requestId: z3.string().min(1).max(200)
362
405
  }),
406
+ z3.object({
407
+ type: z3.literal("worktree.list"),
408
+ repoPath: z3.string().min(1).max(4096).refine(noNul),
409
+ requestId: z3.string().min(1).max(200)
410
+ }),
411
+ z3.object({
412
+ type: z3.literal("worktree.remove"),
413
+ repoPath: z3.string().min(1).max(4096).refine(noNul),
414
+ worktreePath: z3.string().min(1).max(4096).refine(noNul),
415
+ force: z3.boolean(),
416
+ deleteBranch: z3.boolean(),
417
+ requestId: z3.string().min(1).max(200)
418
+ }),
363
419
  z3.object({ type: z3.literal("daemon.getInfo") }),
364
420
  // Second-controller enrollment (ADR-011/ADR-012, BD-433) — deliberately
365
421
  // unsigned: the requesting browser has no trusted key yet to sign with.
@@ -462,6 +518,17 @@ var daemonMessageSchema = z3.discriminatedUnion("type", [
462
518
  requestId: z3.string(),
463
519
  result: projectPathValidationSchema
464
520
  }),
521
+ z3.object({
522
+ type: z3.literal("worktree.listed"),
523
+ requestId: z3.string(),
524
+ worktrees: z3.array(projectWorktreeSchema).max(MAX_PROJECT_WORKTREES),
525
+ truncated: z3.boolean()
526
+ }),
527
+ z3.object({
528
+ type: z3.literal("worktree.removed"),
529
+ requestId: z3.string(),
530
+ result: worktreeRemoveResultSchema
531
+ }),
465
532
  z3.object({
466
533
  type: z3.literal("daemon.info"),
467
534
  hostname: z3.string(),
@@ -551,7 +618,9 @@ var SIGNABLE_ACTIONS = [
551
618
  "workspace.getDiff",
552
619
  "workspace.readFileRange",
553
620
  "workspace.getPlan",
554
- "project.validatePath"
621
+ "project.validatePath",
622
+ "worktree.list",
623
+ "worktree.remove"
555
624
  ];
556
625
  var envelopeSchema = z4.object({
557
626
  version: z4.literal(1),
@@ -2171,7 +2240,7 @@ function listInstalledAgents(env = process.env) {
2171
2240
  }
2172
2241
 
2173
2242
  // src/version.ts
2174
- var DAEMON_VERSION = true ? "0.1.16" : "dev";
2243
+ var DAEMON_VERSION = true ? "0.1.17" : "dev";
2175
2244
  var VERSION_FLAGS = ["--version", "-v"];
2176
2245
  function versionLine() {
2177
2246
  return `bondsmithd ${DAEMON_VERSION} (protocol ${PROTOCOL_VERSION})`;
@@ -2644,6 +2713,19 @@ function createRouterConnection(connectionAuth, send, markControllerId, ptyAttac
2644
2713
  }
2645
2714
  return { conn, releaseAllSubscriptions };
2646
2715
  }
2716
+ var removalQueues = /* @__PURE__ */ new Map();
2717
+ function inRepoOrder(repoPath, task) {
2718
+ const result = (removalQueues.get(repoPath) ?? Promise.resolve()).then(task);
2719
+ const tail = result.then(
2720
+ () => void 0,
2721
+ () => void 0
2722
+ );
2723
+ removalQueues.set(repoPath, tail);
2724
+ void tail.then(() => {
2725
+ if (removalQueues.get(repoPath) === tail) removalQueues.delete(repoPath);
2726
+ });
2727
+ return result;
2728
+ }
2647
2729
  function error(message, requestId) {
2648
2730
  return { type: "error", message, requestId };
2649
2731
  }
@@ -2773,6 +2855,8 @@ async function dispatch(conn, message, {
2773
2855
  getWorkspaceDiff: getWorkspaceDiff2,
2774
2856
  readFileRange: readFileRange2,
2775
2857
  validateProjectPath: validateProjectPath2,
2858
+ listProjectWorktrees: listProjectWorktrees2,
2859
+ removeManagedWorktree: removeManagedWorktree2,
2776
2860
  hostname: hostname3,
2777
2861
  daemonFingerprint,
2778
2862
  pendingAuthorizations,
@@ -3026,6 +3110,47 @@ async function dispatch(conn, message, {
3026
3110
  }
3027
3111
  return;
3028
3112
  }
3113
+ case "worktree.list": {
3114
+ try {
3115
+ const project = await validateProjectPath2(message.repoPath);
3116
+ if (!project.valid) {
3117
+ conn.send(error(`can't list worktrees: ${project.reason}`, message.requestId));
3118
+ return;
3119
+ }
3120
+ const worktrees = await listProjectWorktrees2(project.path, sessionManager.list());
3121
+ conn.send({
3122
+ type: "worktree.listed",
3123
+ requestId: message.requestId,
3124
+ ...fitWorktreeReply(worktrees)
3125
+ });
3126
+ } catch (err) {
3127
+ conn.send(error(errorMessageOf(err, "failed to list worktrees"), message.requestId));
3128
+ }
3129
+ return;
3130
+ }
3131
+ case "worktree.remove": {
3132
+ try {
3133
+ const project = await validateProjectPath2(message.repoPath);
3134
+ if (!project.valid) {
3135
+ conn.send(error(`can't remove a worktree: ${project.reason}`, message.requestId));
3136
+ return;
3137
+ }
3138
+ const result = await inRepoOrder(
3139
+ project.path,
3140
+ () => removeManagedWorktree2({
3141
+ repoPath: project.path,
3142
+ worktreePath: message.worktreePath,
3143
+ force: message.force,
3144
+ deleteBranch: message.deleteBranch,
3145
+ getSessions: () => sessionManager.list()
3146
+ })
3147
+ );
3148
+ conn.send({ type: "worktree.removed", requestId: message.requestId, result });
3149
+ } catch (err) {
3150
+ conn.send(error(errorMessageOf(err, "failed to remove the worktree"), message.requestId));
3151
+ }
3152
+ return;
3153
+ }
3029
3154
  case "daemon.getInfo": {
3030
3155
  conn.send({ type: "daemon.info", hostname: hostname3(), daemonFingerprint });
3031
3156
  return;
@@ -3926,6 +4051,256 @@ async function getWorkspaceDiff(cwd, path, staged, baseline) {
3926
4051
  }
3927
4052
  }
3928
4053
 
4054
+ // src/sessions/git/managed-worktree.ts
4055
+ import { realpath as realpath4 } from "fs/promises";
4056
+ import { sep as sep2 } from "path";
4057
+
4058
+ // src/sessions/git/worktree.ts
4059
+ import { execFile as execFileCb3 } from "child_process";
4060
+ import { promisify as promisify3 } from "util";
4061
+ var execFile3 = promisify3(execFileCb3);
4062
+ var WorktreeBranchNameTakenError = class extends Error {
4063
+ constructor(branch) {
4064
+ super(`a branch named "${branch}" already exists`);
4065
+ this.name = "WorktreeBranchNameTakenError";
4066
+ }
4067
+ };
4068
+ var WorktreePathExistsError = class extends Error {
4069
+ constructor(path) {
4070
+ super(`worktree path already exists: ${path}`);
4071
+ this.name = "WorktreePathExistsError";
4072
+ }
4073
+ };
4074
+ var WorktreeHasUncommittedChangesError = class extends Error {
4075
+ constructor(path) {
4076
+ super(
4077
+ `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}).`
4078
+ );
4079
+ this.name = "WorktreeHasUncommittedChangesError";
4080
+ }
4081
+ };
4082
+ function stderrOf(error2) {
4083
+ if (typeof error2 !== "object" || error2 === null) return "";
4084
+ const stderr = error2.stderr;
4085
+ return typeof stderr === "string" ? stderr : "";
4086
+ }
4087
+ async function createWorktree(sourceCwd, worktreePath, branch, startPoint) {
4088
+ const args = ["worktree", "add", "--no-track", "-b", branch, worktreePath];
4089
+ if (startPoint) args.push(startPoint);
4090
+ try {
4091
+ await execFile3("git", [...SAFE_GIT_CONFIG, ...args], { cwd: sourceCwd });
4092
+ } catch (error2) {
4093
+ const stderr = stderrOf(error2);
4094
+ if (stderr.includes("a branch named")) {
4095
+ throw new WorktreeBranchNameTakenError(branch);
4096
+ }
4097
+ if (stderr.includes("already exists")) {
4098
+ throw new WorktreePathExistsError(worktreePath);
4099
+ }
4100
+ throw error2;
4101
+ }
4102
+ }
4103
+ async function removeWorktree(sourceCwd, worktreePath, { force = false } = {}) {
4104
+ const args = ["worktree", "remove", ...force ? ["--force"] : [], "--", worktreePath];
4105
+ try {
4106
+ await execFile3("git", [...SAFE_GIT_CONFIG, ...args], { cwd: sourceCwd });
4107
+ } catch (error2) {
4108
+ const stderr = stderrOf(error2);
4109
+ if (stderr.includes("contains modified or untracked files")) {
4110
+ throw new WorktreeHasUncommittedChangesError(worktreePath);
4111
+ }
4112
+ throw error2;
4113
+ }
4114
+ }
4115
+ var BranchNotMergedError = class extends Error {
4116
+ constructor(branch) {
4117
+ super(`the branch "${branch}" is not fully merged, so it was kept`);
4118
+ this.name = "BranchNotMergedError";
4119
+ }
4120
+ };
4121
+ async function deleteMergedBranch(sourceCwd, branch) {
4122
+ try {
4123
+ await execFile3("git", [...SAFE_GIT_CONFIG, "branch", "-d", "--", branch], { cwd: sourceCwd });
4124
+ } catch (error2) {
4125
+ if (stderrOf(error2).includes("not fully merged")) throw new BranchNotMergedError(branch);
4126
+ throw error2;
4127
+ }
4128
+ }
4129
+
4130
+ // src/sessions/git/worktree-list.ts
4131
+ import { realpath as realpath3 } from "fs/promises";
4132
+
4133
+ // src/sessions/worktree-root.ts
4134
+ import { lstat as lstat3, realpath as realpath2 } from "fs/promises";
4135
+ import { homedir as homedir6 } from "os";
4136
+ import { basename, dirname as dirname4, isAbsolute as isAbsolute3, join as join9, resolve as resolve2 } from "path";
4137
+ function worktreeRoot() {
4138
+ return join9(homedir6(), ".bondsmith", "worktrees");
4139
+ }
4140
+ function worktreePathFor(sessionId) {
4141
+ return join9(worktreeRoot(), sessionId);
4142
+ }
4143
+ async function canonicalPath(path) {
4144
+ try {
4145
+ return await realpath2(path);
4146
+ } catch (error2) {
4147
+ const code = error2.code;
4148
+ if (code !== "ENOENT" && code !== "ENOTDIR") throw error2;
4149
+ const parent = dirname4(path);
4150
+ if (parent === path) return path;
4151
+ return join9(await canonicalPath(parent), basename(path));
4152
+ }
4153
+ }
4154
+ async function managedWorktreeLocation(path, root = worktreeRoot()) {
4155
+ if (!isAbsolute3(path) || path.includes("\0") || path !== resolve2(path)) return null;
4156
+ const name = basename(path);
4157
+ if (!isValidSessionId(name)) return null;
4158
+ const realRoot = await canonicalPath(root);
4159
+ if (await canonicalPath(dirname4(path)) !== realRoot) return null;
4160
+ return join9(realRoot, name);
4161
+ }
4162
+ async function requireManagedWorktreeDirectory(path, root = worktreeRoot()) {
4163
+ const location = await managedWorktreeLocation(path, root);
4164
+ if (!location) return null;
4165
+ try {
4166
+ if (!(await lstat3(location)).isDirectory()) return null;
4167
+ return await realpath2(location);
4168
+ } catch (error2) {
4169
+ if (error2.code === "ENOENT") return null;
4170
+ throw error2;
4171
+ }
4172
+ }
4173
+
4174
+ // src/sessions/git/worktree-list.ts
4175
+ var BRANCH_REF_PREFIX = "refs/heads/";
4176
+ function parseWorktreeList(output) {
4177
+ const entries = [];
4178
+ let current = null;
4179
+ for (const field of output.split("\0")) {
4180
+ if (field === "") {
4181
+ if (current) entries.push(current);
4182
+ current = null;
4183
+ } else if (field.startsWith("worktree ")) {
4184
+ current = {
4185
+ path: field.slice("worktree ".length),
4186
+ branch: null,
4187
+ head: "",
4188
+ prunable: false,
4189
+ locked: false
4190
+ };
4191
+ } else if (current && field.startsWith("HEAD ")) {
4192
+ current.head = field.slice("HEAD ".length);
4193
+ } else if (current && field.startsWith("branch ")) {
4194
+ const ref = field.slice("branch ".length);
4195
+ current.branch = ref.startsWith(BRANCH_REF_PREFIX) ? ref.slice(BRANCH_REF_PREFIX.length) : ref;
4196
+ } else if (current && (field === "prunable" || field.startsWith("prunable "))) {
4197
+ current.prunable = true;
4198
+ } else if (current && (field === "locked" || field.startsWith("locked "))) {
4199
+ current.locked = true;
4200
+ }
4201
+ }
4202
+ if (current) entries.push(current);
4203
+ return entries;
4204
+ }
4205
+ async function listDaemonWorktrees(repoPath) {
4206
+ const [, ...linked] = parseWorktreeList(
4207
+ await runGit(["worktree", "list", "--porcelain", "-z"], repoPath)
4208
+ );
4209
+ const ownPath = await realpath3(repoPath);
4210
+ const locations = await Promise.all(linked.map((entry) => managedWorktreeLocation(entry.path)));
4211
+ return linked.flatMap((entry, index) => {
4212
+ const location = locations[index];
4213
+ return location && location !== ownPath ? [{ ...entry, path: location }] : [];
4214
+ });
4215
+ }
4216
+ async function findManagedWorktree(repoPath, requestedPath) {
4217
+ const location = await requireManagedWorktreeDirectory(requestedPath);
4218
+ if (!location) return null;
4219
+ const entries = await listDaemonWorktrees(repoPath);
4220
+ return entries.find((entry) => entry.path === location) ?? null;
4221
+ }
4222
+
4223
+ // src/sessions/git/managed-worktree.ts
4224
+ var STATUS_CONCURRENCY = 8;
4225
+ async function mapWithConcurrency(items, limit, map) {
4226
+ const results = new Array(items.length);
4227
+ let next = 0;
4228
+ let failed = false;
4229
+ const worker = async () => {
4230
+ while (!failed && next < items.length) {
4231
+ const index = next++;
4232
+ try {
4233
+ results[index] = await map(items[index]);
4234
+ } catch (error2) {
4235
+ failed = true;
4236
+ throw error2;
4237
+ }
4238
+ }
4239
+ };
4240
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
4241
+ return results;
4242
+ }
4243
+ async function sessionInWorktree(worktreePath, sessions) {
4244
+ for (const session of sessions) {
4245
+ const real = await realpath4(session.cwd).catch(() => session.cwd);
4246
+ const inside = [session.cwd, real].some(
4247
+ (cwd) => cwd === worktreePath || cwd.startsWith(`${worktreePath}${sep2}`)
4248
+ );
4249
+ if (inside) return session;
4250
+ }
4251
+ return null;
4252
+ }
4253
+ async function hasChanges(worktreePath) {
4254
+ try {
4255
+ return (await runGit(["status", "--porcelain", "--untracked-files=normal"], worktreePath)).trim() !== "";
4256
+ } catch {
4257
+ return true;
4258
+ }
4259
+ }
4260
+ async function listProjectWorktrees(repoPath, sessions) {
4261
+ const entries = await listDaemonWorktrees(repoPath);
4262
+ return mapWithConcurrency(entries, STATUS_CONCURRENCY, async (entry) => ({
4263
+ path: entry.path,
4264
+ branch: entry.branch,
4265
+ head: entry.head,
4266
+ prunable: entry.prunable,
4267
+ locked: entry.locked,
4268
+ dirty: entry.prunable ? false : await hasChanges(entry.path),
4269
+ sessionId: (await sessionInWorktree(entry.path, sessions))?.id ?? null
4270
+ }));
4271
+ }
4272
+ async function dropBranch(repoPath, branch) {
4273
+ try {
4274
+ await deleteMergedBranch(repoPath, branch);
4275
+ return "deleted";
4276
+ } catch (error2) {
4277
+ if (error2 instanceof BranchNotMergedError) return "not_merged";
4278
+ console.error("Failed to delete the branch of a removed worktree", { branch, error: error2 });
4279
+ return "failed";
4280
+ }
4281
+ }
4282
+ async function removeManagedWorktree(input) {
4283
+ const entry = await findManagedWorktree(input.repoPath, input.worktreePath);
4284
+ if (!entry) return { removed: false, reason: "not_found" };
4285
+ if (entry.locked) return { removed: false, reason: "locked" };
4286
+ if (await sessionInWorktree(entry.path, input.getSessions())) {
4287
+ return { removed: false, reason: "session_active" };
4288
+ }
4289
+ try {
4290
+ await removeWorktree(input.repoPath, entry.path, { force: input.force });
4291
+ } catch (error2) {
4292
+ if (error2 instanceof WorktreeHasUncommittedChangesError) {
4293
+ return { removed: false, reason: "has_changes" };
4294
+ }
4295
+ throw error2;
4296
+ }
4297
+ const branch = input.deleteBranch && entry.branch ? await dropBranch(input.repoPath, entry.branch) : "kept";
4298
+ console.log(
4299
+ `Removed worktree ${entry.path} (branch ${entry.branch ?? "none"}: ${branch}${input.force ? ", forced" : ""})`
4300
+ );
4301
+ return { removed: true, branch };
4302
+ }
4303
+
3929
4304
  // src/ws/server.ts
3930
4305
  import { hostname } from "os";
3931
4306
  import { WebSocketServer } from "ws";
@@ -4058,12 +4433,12 @@ import { v4 as uuidv4 } from "uuid";
4058
4433
 
4059
4434
  // src/sessions/agent-reports/lifecycle.ts
4060
4435
  import { readdir, rm as rm4, stat as stat3 } from "fs/promises";
4061
- import { basename, join as join9 } from "path";
4436
+ import { basename as basename2, join as join10 } from "path";
4062
4437
  var SWEEP_MIN_AGE_MS = 6e4;
4063
4438
  async function removeHookPlanFiles(sessionId, plansDir = HOOK_PLANS_DIR) {
4064
4439
  await Promise.all(
4065
4440
  [PLAN_FILE_SUFFIX, TASKS_FILE_SUFFIX].map(
4066
- (suffix) => rm4(join9(plansDir, `${basename(sessionId)}${suffix}`), { force: true })
4441
+ (suffix) => rm4(join10(plansDir, `${basename2(sessionId)}${suffix}`), { force: true })
4067
4442
  )
4068
4443
  );
4069
4444
  }
@@ -4087,7 +4462,7 @@ async function sweepOrphanedHookPlanFiles(liveSessionIds, plansDir = HOOK_PLANS_
4087
4462
  names.map(async (name) => {
4088
4463
  const sessionId = hookFileSessionId(name);
4089
4464
  if (sessionId === null || liveSessionIds.has(sessionId)) return;
4090
- const path = join9(plansDir, name);
4465
+ const path = join10(plansDir, name);
4091
4466
  const modifiedMs = await stat3(path).then((info) => info.mtimeMs).catch(() => null);
4092
4467
  if (modifiedMs === null || now - modifiedMs < SWEEP_MIN_AGE_MS) return;
4093
4468
  await rm4(path, { force: true });
@@ -4095,65 +4470,6 @@ async function sweepOrphanedHookPlanFiles(liveSessionIds, plansDir = HOOK_PLANS_
4095
4470
  );
4096
4471
  }
4097
4472
 
4098
- // src/sessions/git/worktree.ts
4099
- import { execFile as execFileCb3 } from "child_process";
4100
- import { promisify as promisify3 } from "util";
4101
- var execFile3 = promisify3(execFileCb3);
4102
- var WorktreeBranchNameTakenError = class extends Error {
4103
- constructor(branch) {
4104
- super(`a branch named "${branch}" already exists`);
4105
- this.name = "WorktreeBranchNameTakenError";
4106
- }
4107
- };
4108
- var WorktreePathExistsError = class extends Error {
4109
- constructor(path) {
4110
- super(`worktree path already exists: ${path}`);
4111
- this.name = "WorktreePathExistsError";
4112
- }
4113
- };
4114
- var WorktreeHasUncommittedChangesError = class extends Error {
4115
- constructor(path) {
4116
- super(
4117
- `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}).`
4118
- );
4119
- this.name = "WorktreeHasUncommittedChangesError";
4120
- }
4121
- };
4122
- function stderrOf(error2) {
4123
- if (typeof error2 !== "object" || error2 === null) return "";
4124
- const stderr = error2.stderr;
4125
- return typeof stderr === "string" ? stderr : "";
4126
- }
4127
- async function createWorktree(sourceCwd, worktreePath, branch, startPoint) {
4128
- const args = ["worktree", "add", "--no-track", "-b", branch, worktreePath];
4129
- if (startPoint) args.push(startPoint);
4130
- try {
4131
- await execFile3("git", [...SAFE_GIT_CONFIG, ...args], { cwd: sourceCwd });
4132
- } catch (error2) {
4133
- const stderr = stderrOf(error2);
4134
- if (stderr.includes("a branch named")) {
4135
- throw new WorktreeBranchNameTakenError(branch);
4136
- }
4137
- if (stderr.includes("already exists")) {
4138
- throw new WorktreePathExistsError(worktreePath);
4139
- }
4140
- throw error2;
4141
- }
4142
- }
4143
- async function removeWorktree(sourceCwd, worktreePath) {
4144
- try {
4145
- await execFile3("git", [...SAFE_GIT_CONFIG, "worktree", "remove", worktreePath], {
4146
- cwd: sourceCwd
4147
- });
4148
- } catch (error2) {
4149
- const stderr = stderrOf(error2);
4150
- if (stderr.includes("contains modified or untracked files")) {
4151
- throw new WorktreeHasUncommittedChangesError(worktreePath);
4152
- }
4153
- throw error2;
4154
- }
4155
- }
4156
-
4157
4473
  // src/sessions/worktree-setup/setup-command.ts
4158
4474
  var SETUP_FAILED_OPTION = "@bondsmith-setup-failed";
4159
4475
  var WRAPPER_SCRIPT = `printf '\\033[1m%s\\033[0m\\n' 'Setting up worktree\u2026'
@@ -4296,8 +4612,8 @@ function plainAdapter(id) {
4296
4612
  import { execFile as execFile4 } from "child_process";
4297
4613
  import { randomUUID as randomUUID2 } from "crypto";
4298
4614
  import { mkdir as mkdir4, rename as rename4, rm as rm5, writeFile as writeFile4 } from "fs/promises";
4299
- import { homedir as homedir6 } from "os";
4300
- import { join as join10 } from "path";
4615
+ import { homedir as homedir7 } from "os";
4616
+ import { join as join11 } from "path";
4301
4617
  import { promisify as promisify4 } from "util";
4302
4618
 
4303
4619
  // src/sessions/session-env.ts
@@ -4426,7 +4742,7 @@ function claudeSettings() {
4426
4742
 
4427
4743
  // src/sessions/agents/claude/hooks.ts
4428
4744
  var execFileAsync = promisify4(execFile4);
4429
- var CLAUDE_HOOKS_DIR = join10(homedir6(), ".bondsmith", "hooks");
4745
+ var CLAUDE_HOOKS_DIR = join11(homedir7(), ".bondsmith", "hooks");
4430
4746
  var AGENT_HOOKS_ENV_VAR = "BONDSMITH_AGENT_HOOKS";
4431
4747
  var HOOKS_OFF_VALUES = /^(off|0|false|no)$/i;
4432
4748
  var CLAUDE_BINARY = "claude";
@@ -4479,7 +4795,7 @@ async function prepareAgentCommand(command, {
4479
4795
  if (HOOKS_OFF_VALUES.test(env[AGENT_HOOKS_ENV_VAR] ?? "")) return command;
4480
4796
  if (!isPlainClaudeCommand(command)) return command;
4481
4797
  if (!await supportsSettings()) return command;
4482
- const file = join10(hooksDir, "claude-settings.json");
4798
+ const file = join11(hooksDir, "claude-settings.json");
4483
4799
  const staging = `${file}.${randomUUID2()}.tmp`;
4484
4800
  try {
4485
4801
  await mkdir4(hooksDir, { recursive: true });
@@ -4545,26 +4861,15 @@ async function resolveWorktreeStartPoint(sourceCwd, baseBranch) {
4545
4861
  throw new WorktreeBaseBranchNotFoundError(baseBranch);
4546
4862
  }
4547
4863
 
4548
- // src/sessions/worktree-root.ts
4549
- import { lstat as lstat3, realpath as realpath2 } from "fs/promises";
4550
- import { homedir as homedir7 } from "os";
4551
- import { basename as basename2, dirname as dirname4, isAbsolute as isAbsolute3, join as join11, resolve as resolve2 } from "path";
4552
- function worktreeRoot() {
4553
- return join11(homedir7(), ".bondsmith", "worktrees");
4554
- }
4555
- function worktreePathFor(sessionId) {
4556
- return join11(worktreeRoot(), sessionId);
4557
- }
4558
-
4559
4864
  // src/sessions/worktree-setup/copy-files.ts
4560
4865
  import { constants as constants4 } from "fs";
4561
- import { copyFile, glob, lstat as lstat4, mkdir as mkdir5, realpath as realpath3 } from "fs/promises";
4562
- import { dirname as dirname5, isAbsolute as isAbsolute4, join as join12, relative, sep as sep2 } from "path";
4866
+ import { copyFile, glob, lstat as lstat4, mkdir as mkdir5, realpath as realpath5 } from "fs/promises";
4867
+ import { dirname as dirname5, isAbsolute as isAbsolute4, join as join12, relative, sep as sep3 } from "path";
4563
4868
  var GIT_EXIT_NONE_IGNORED = 1;
4564
4869
  var TRACKED_CHECK_BATCH = 200;
4565
4870
  function isInside(root, path) {
4566
4871
  const rel = relative(root, path);
4567
- return rel !== ".." && !rel.startsWith(`..${sep2}`) && !isAbsolute4(rel);
4872
+ return rel !== ".." && !rel.startsWith(`..${sep3}`) && !isAbsolute4(rel);
4568
4873
  }
4569
4874
  async function ignoredPaths(worktree, paths) {
4570
4875
  if (paths.length === 0) return /* @__PURE__ */ new Set();
@@ -4598,11 +4903,11 @@ async function copyFilesIntoWorktree(sourceRoot, worktreeRoot2, globs) {
4598
4903
  const patterns = globs.filter(isSafeCopyGlob);
4599
4904
  if (patterns.length === 0) return result;
4600
4905
  try {
4601
- const source = await realpath3(sourceRoot);
4602
- const worktree = await realpath3(worktreeRoot2);
4906
+ const source = await realpath5(sourceRoot);
4907
+ const worktree = await realpath5(worktreeRoot2);
4603
4908
  const candidates = [];
4604
4909
  for await (const match of glob(patterns, { cwd: source })) {
4605
- if (match.split(sep2).includes(".git")) continue;
4910
+ if (match.split(sep3).includes(".git")) continue;
4606
4911
  try {
4607
4912
  if (await isCopyCandidate(source, worktree, match)) candidates.push(match);
4608
4913
  } catch (error2) {
@@ -4644,7 +4949,7 @@ async function copyFilesIntoWorktree(sourceRoot, worktreeRoot2, globs) {
4644
4949
  async function isCopyCandidate(source, worktree, match) {
4645
4950
  const from = join12(source, match);
4646
4951
  if (!(await lstat4(from)).isFile()) return false;
4647
- if (!isInside(source, await realpath3(from))) return false;
4952
+ if (!isInside(source, await realpath5(from))) return false;
4648
4953
  try {
4649
4954
  await lstat4(join12(worktree, match));
4650
4955
  return false;
@@ -4656,7 +4961,7 @@ async function isCopyCandidate(source, worktree, match) {
4656
4961
  async function copyOne(source, worktree, match) {
4657
4962
  const to = join12(worktree, match);
4658
4963
  await mkdir5(dirname5(to), { recursive: true });
4659
- if (!isInside(worktree, await realpath3(dirname5(to)))) return false;
4964
+ if (!isInside(worktree, await realpath5(dirname5(to)))) return false;
4660
4965
  try {
4661
4966
  await copyFile(join12(source, match), to, constants4.COPYFILE_EXCL);
4662
4967
  } catch (error2) {
@@ -5125,6 +5430,8 @@ function createDaemonWebSocketServer(httpServer, config, daemonFingerprint, pend
5125
5430
  getWorkspaceDiff,
5126
5431
  readFileRange,
5127
5432
  validateProjectPath,
5433
+ listProjectWorktrees,
5434
+ removeManagedWorktree,
5128
5435
  hostname: () => hostname(),
5129
5436
  daemonFingerprint,
5130
5437
  pendingAuthorizations,
@@ -5230,6 +5537,8 @@ async function start() {
5230
5537
  getWorkspaceDiff,
5231
5538
  readFileRange,
5232
5539
  validateProjectPath,
5540
+ listProjectWorktrees,
5541
+ removeManagedWorktree,
5233
5542
  hostname: hostname2,
5234
5543
  pendingAuthorizations,
5235
5544
  presentPendingAuthorization,
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
+ "version": "0.1.17",
3
+ "protocolVersion": 21,
2
4
  "name": "@bondsmith/bondsmithd",
3
- "version": "0.1.16",
4
5
  "private": false,
5
6
  "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
7
  "homepage": "https://bondsmith.dev",