@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.
- package/dist/index.js +397 -88
- 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 =
|
|
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.
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
4300
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
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
|
|
4562
|
-
import { dirname as dirname5, isAbsolute as isAbsolute4, join as join12, relative, sep as
|
|
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(`..${
|
|
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
|
|
4602
|
-
const worktree = await
|
|
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(
|
|
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
|
|
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
|
|
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",
|