wmdev 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/backend/dist/server.js +227 -34
- package/frontend/dist/assets/index-C3n4a8bx.js +32 -0
- package/frontend/dist/assets/index-DANb8sJ_.css +32 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-D9tls-CM.css +0 -32
- package/frontend/dist/assets/index-Dp6OOEt3.js +0 -25
package/README.md
CHANGED
|
@@ -62,6 +62,10 @@ wmdev uses two config files in the project root:
|
|
|
62
62
|
### `.wmdev.yaml` schema
|
|
63
63
|
|
|
64
64
|
```yaml
|
|
65
|
+
# Project name displayed in the sidebar header and browser tab title.
|
|
66
|
+
# Falls back to "Dashboard" if omitted.
|
|
67
|
+
name: string
|
|
68
|
+
|
|
65
69
|
# Services to monitor — each maps a display name to a port env var.
|
|
66
70
|
# The dashboard polls these ports and shows health status badges.
|
|
67
71
|
# When portStart is set, wmdev auto-allocates ports for new worktrees
|
|
@@ -112,6 +116,8 @@ linkedRepos: []
|
|
|
112
116
|
### Example
|
|
113
117
|
|
|
114
118
|
```yaml
|
|
119
|
+
name: My Project
|
|
120
|
+
|
|
115
121
|
services:
|
|
116
122
|
- name: BE
|
|
117
123
|
portEnv: BACKEND_PORT
|
|
@@ -149,6 +155,7 @@ linkedRepos:
|
|
|
149
155
|
|
|
150
156
|
| Parameter | Type | Required | Description |
|
|
151
157
|
|-----------|------|----------|-------------|
|
|
158
|
+
| `name` | string | no | Project name shown in the sidebar header and browser tab title. Defaults to "Dashboard" |
|
|
152
159
|
| `services[].name` | string | yes | Display name shown in the dashboard |
|
|
153
160
|
| `services[].portEnv` | string | yes | Env var containing the service port (read from each worktree's `.env.local`) |
|
|
154
161
|
| `services[].portStart` | number | no | Base port for slot 0. When set, wmdev auto-allocates ports for new worktrees |
|
package/backend/dist/server.js
CHANGED
|
@@ -6941,8 +6941,13 @@ async function readEnvLocal(wtDir) {
|
|
|
6941
6941
|
for (const line of text.split(`
|
|
6942
6942
|
`)) {
|
|
6943
6943
|
const match = line.match(/^(\w+)=(.*)$/);
|
|
6944
|
-
if (match)
|
|
6945
|
-
|
|
6944
|
+
if (match) {
|
|
6945
|
+
let val = match[2];
|
|
6946
|
+
if (val.length >= 2 && val.startsWith("'") && val.endsWith("'")) {
|
|
6947
|
+
val = val.slice(1, -1);
|
|
6948
|
+
}
|
|
6949
|
+
env[match[1]] = val;
|
|
6950
|
+
}
|
|
6946
6951
|
}
|
|
6947
6952
|
return env;
|
|
6948
6953
|
} catch {
|
|
@@ -6959,12 +6964,14 @@ async function writeEnvLocal(wtDir, entries) {
|
|
|
6959
6964
|
`);
|
|
6960
6965
|
} catch {}
|
|
6961
6966
|
for (const [key, value] of Object.entries(entries)) {
|
|
6967
|
+
const needsQuoting = /[{}"\s$`\\!#|;&()<>]/.test(value);
|
|
6968
|
+
const safe = needsQuoting ? `'${value}'` : value;
|
|
6962
6969
|
const pattern = new RegExp(`^${key}=`);
|
|
6963
6970
|
const idx = lines.findIndex((l) => pattern.test(l));
|
|
6964
6971
|
if (idx >= 0) {
|
|
6965
|
-
lines[idx] = `${key}=${
|
|
6972
|
+
lines[idx] = `${key}=${safe}`;
|
|
6966
6973
|
} else {
|
|
6967
|
-
lines.push(`${key}=${
|
|
6974
|
+
lines.push(`${key}=${safe}`);
|
|
6968
6975
|
}
|
|
6969
6976
|
}
|
|
6970
6977
|
await Bun.write(filePath, lines.join(`
|
|
@@ -7060,7 +7067,8 @@ var DEFAULT_CONFIG = {
|
|
|
7060
7067
|
services: [],
|
|
7061
7068
|
profiles: { default: { name: "default" } },
|
|
7062
7069
|
autoName: false,
|
|
7063
|
-
linkedRepos: []
|
|
7070
|
+
linkedRepos: [],
|
|
7071
|
+
startupEnvs: {}
|
|
7064
7072
|
};
|
|
7065
7073
|
function hasAutoName(dir) {
|
|
7066
7074
|
try {
|
|
@@ -7097,14 +7105,23 @@ function loadConfig(dir) {
|
|
|
7097
7105
|
repo: r.repo,
|
|
7098
7106
|
alias: typeof r.alias === "string" ? r.alias : r.repo.split("/").pop()
|
|
7099
7107
|
})) : [];
|
|
7108
|
+
let startupEnvs = {};
|
|
7109
|
+
if (parsed.startupEnvs && typeof parsed.startupEnvs === "object" && !Array.isArray(parsed.startupEnvs)) {
|
|
7110
|
+
const raw = parsed.startupEnvs;
|
|
7111
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
7112
|
+
startupEnvs[k] = typeof v === "string" ? v : String(v);
|
|
7113
|
+
}
|
|
7114
|
+
}
|
|
7100
7115
|
return {
|
|
7116
|
+
...typeof parsed.name === "string" ? { name: parsed.name } : {},
|
|
7101
7117
|
services: Array.isArray(parsed.services) ? parsed.services : DEFAULT_CONFIG.services,
|
|
7102
7118
|
profiles: {
|
|
7103
7119
|
default: defaultProfile?.name ? defaultProfile : DEFAULT_CONFIG.profiles.default,
|
|
7104
7120
|
...sandboxProfile?.name && sandboxProfile?.image ? { sandbox: sandboxProfile } : {}
|
|
7105
7121
|
},
|
|
7106
7122
|
autoName,
|
|
7107
|
-
linkedRepos
|
|
7123
|
+
linkedRepos,
|
|
7124
|
+
startupEnvs
|
|
7108
7125
|
};
|
|
7109
7126
|
} catch {
|
|
7110
7127
|
return DEFAULT_CONFIG;
|
|
@@ -7589,6 +7606,48 @@ function parseBranchFromOutput(output) {
|
|
|
7589
7606
|
const match = output.match(/branch:\s*(\S+)/i);
|
|
7590
7607
|
return match?.[1] ?? null;
|
|
7591
7608
|
}
|
|
7609
|
+
async function initWorktreeEnv(branch, opts) {
|
|
7610
|
+
const profile = opts?.profile ?? "default";
|
|
7611
|
+
const agent = opts?.agent ?? "claude";
|
|
7612
|
+
const porcelainResult = Bun.spawnSync(["git", "worktree", "list", "--porcelain"], { stdout: "pipe", stderr: "pipe" });
|
|
7613
|
+
const worktreeMap = parseWorktreePorcelain(new TextDecoder().decode(porcelainResult.stdout));
|
|
7614
|
+
const wtDir = worktreeMap.get(branch) ?? null;
|
|
7615
|
+
if (!wtDir)
|
|
7616
|
+
return;
|
|
7617
|
+
const existingEnv = await readEnvLocal(wtDir);
|
|
7618
|
+
const allPaths = [...worktreeMap.values()];
|
|
7619
|
+
const existingEnvs = await readAllWorktreeEnvs(allPaths, wtDir);
|
|
7620
|
+
const portAssignments = opts?.services ? allocatePorts(existingEnvs, opts.services) : {};
|
|
7621
|
+
const defaults = { ...portAssignments, PROFILE: profile, AGENT: agent, ...opts?.envOverrides };
|
|
7622
|
+
const toWrite = {};
|
|
7623
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
7624
|
+
if (!existingEnv[key])
|
|
7625
|
+
toWrite[key] = value;
|
|
7626
|
+
}
|
|
7627
|
+
if (Object.keys(toWrite).length > 0) {
|
|
7628
|
+
await writeEnvLocal(wtDir, toWrite);
|
|
7629
|
+
}
|
|
7630
|
+
const rpcPort = Bun.env.BACKEND_PORT || "5111";
|
|
7631
|
+
const hooksConfig = {
|
|
7632
|
+
hooks: {
|
|
7633
|
+
Stop: [{ hooks: [{ type: "command", command: `WORKMUX_RPC_PORT=${rpcPort} ~/.config/workmux/hooks/notify-stop.sh`, async: true }] }],
|
|
7634
|
+
PostToolUse: [{ matcher: "Bash", hooks: [{ type: "command", command: `WORKMUX_RPC_PORT=${rpcPort} ~/.config/workmux/hooks/notify-pr.sh`, async: true }] }]
|
|
7635
|
+
}
|
|
7636
|
+
};
|
|
7637
|
+
await mkdir2(`${wtDir}/.claude`, { recursive: true });
|
|
7638
|
+
const settingsPath = `${wtDir}/.claude/settings.local.json`;
|
|
7639
|
+
let existing = {};
|
|
7640
|
+
try {
|
|
7641
|
+
const file = Bun.file(settingsPath);
|
|
7642
|
+
if (await file.exists()) {
|
|
7643
|
+
existing = await file.json();
|
|
7644
|
+
}
|
|
7645
|
+
} catch {}
|
|
7646
|
+
const existingHooks = existing.hooks ?? {};
|
|
7647
|
+
const merged = { ...existing, hooks: { ...existingHooks, ...hooksConfig.hooks } };
|
|
7648
|
+
await Bun.write(settingsPath, JSON.stringify(merged, null, 2) + `
|
|
7649
|
+
`);
|
|
7650
|
+
}
|
|
7592
7651
|
async function addWorktree(rawBranch, opts) {
|
|
7593
7652
|
ensureTmux();
|
|
7594
7653
|
const profile = opts?.profile ?? "default";
|
|
@@ -7643,35 +7702,10 @@ async function addWorktree(rawBranch, opts) {
|
|
|
7643
7702
|
branch = parsed;
|
|
7644
7703
|
}
|
|
7645
7704
|
const windowTarget = `wm-${branch}`;
|
|
7705
|
+
await initWorktreeEnv(branch, { profile, agent, services: opts?.services, envOverrides: opts?.envOverrides });
|
|
7646
7706
|
const porcelainResult = Bun.spawnSync(["git", "worktree", "list", "--porcelain"], { stdout: "pipe", stderr: "pipe" });
|
|
7647
7707
|
const worktreeMap = parseWorktreePorcelain(new TextDecoder().decode(porcelainResult.stdout));
|
|
7648
7708
|
const wtDir = worktreeMap.get(branch) ?? null;
|
|
7649
|
-
if (wtDir) {
|
|
7650
|
-
const allPaths = [...worktreeMap.values()];
|
|
7651
|
-
const existingEnvs = await readAllWorktreeEnvs(allPaths, wtDir);
|
|
7652
|
-
const portAssignments = opts?.services ? allocatePorts(existingEnvs, opts.services) : {};
|
|
7653
|
-
await writeEnvLocal(wtDir, { ...portAssignments, PROFILE: profile, AGENT: agent });
|
|
7654
|
-
const rpcPort = Bun.env.BACKEND_PORT || "5111";
|
|
7655
|
-
const hooksConfig = {
|
|
7656
|
-
hooks: {
|
|
7657
|
-
Stop: [{ hooks: [{ type: "command", command: `WORKMUX_RPC_PORT=${rpcPort} ~/.config/workmux/hooks/notify-stop.sh`, async: true }] }],
|
|
7658
|
-
PostToolUse: [{ matcher: "Bash", hooks: [{ type: "command", command: `WORKMUX_RPC_PORT=${rpcPort} ~/.config/workmux/hooks/notify-pr.sh`, async: true }] }]
|
|
7659
|
-
}
|
|
7660
|
-
};
|
|
7661
|
-
await mkdir2(`${wtDir}/.claude`, { recursive: true });
|
|
7662
|
-
const settingsPath = `${wtDir}/.claude/settings.local.json`;
|
|
7663
|
-
let existing = {};
|
|
7664
|
-
try {
|
|
7665
|
-
const file = Bun.file(settingsPath);
|
|
7666
|
-
if (await file.exists()) {
|
|
7667
|
-
existing = await file.json();
|
|
7668
|
-
}
|
|
7669
|
-
} catch {}
|
|
7670
|
-
const existingHooks = existing.hooks ?? {};
|
|
7671
|
-
const merged = { ...existing, hooks: { ...existingHooks, ...hooksConfig.hooks } };
|
|
7672
|
-
await Bun.write(settingsPath, JSON.stringify(merged, null, 2) + `
|
|
7673
|
-
`);
|
|
7674
|
-
}
|
|
7675
7709
|
const env = wtDir ? await readEnvLocal(wtDir) : {};
|
|
7676
7710
|
log.debug(`[workmux:add] branch=${branch} dir=${wtDir ?? "(not found)"} env=${JSON.stringify(env)}`);
|
|
7677
7711
|
if (hasSystemPrompt && profileConfig) {
|
|
@@ -7819,6 +7853,52 @@ async function checkDirty(dir) {
|
|
|
7819
7853
|
]);
|
|
7820
7854
|
return status || ahead;
|
|
7821
7855
|
}
|
|
7856
|
+
async function cleanupStaleWindows(activeBranches, worktreeBaseDir) {
|
|
7857
|
+
try {
|
|
7858
|
+
const proc = Bun.spawn(["tmux", "list-panes", "-a", "-F", "#{session_name}:#{window_name} #{pane_current_path}"], { stdout: "pipe", stderr: "pipe" });
|
|
7859
|
+
if (await proc.exited !== 0)
|
|
7860
|
+
return;
|
|
7861
|
+
const output = (await new Response(proc.stdout).text()).trim();
|
|
7862
|
+
if (!output)
|
|
7863
|
+
return;
|
|
7864
|
+
const toKill = [];
|
|
7865
|
+
const seen = new Set;
|
|
7866
|
+
for (const line of output.split(`
|
|
7867
|
+
`)) {
|
|
7868
|
+
const spaceIdx = line.indexOf(" ");
|
|
7869
|
+
if (spaceIdx === -1)
|
|
7870
|
+
continue;
|
|
7871
|
+
const target = line.slice(0, spaceIdx);
|
|
7872
|
+
const panePath = line.slice(spaceIdx + 1);
|
|
7873
|
+
const colonIdx = target.indexOf(":");
|
|
7874
|
+
if (colonIdx === -1)
|
|
7875
|
+
continue;
|
|
7876
|
+
const session = target.slice(0, colonIdx);
|
|
7877
|
+
const windowName = target.slice(colonIdx + 1);
|
|
7878
|
+
if (!windowName.startsWith("wm-"))
|
|
7879
|
+
continue;
|
|
7880
|
+
if (seen.has(windowName))
|
|
7881
|
+
continue;
|
|
7882
|
+
const branch = windowName.slice(3);
|
|
7883
|
+
if (activeBranches.has(branch))
|
|
7884
|
+
continue;
|
|
7885
|
+
if (!panePath.startsWith(worktreeBaseDir))
|
|
7886
|
+
continue;
|
|
7887
|
+
toKill.push({ session, windowName });
|
|
7888
|
+
seen.add(windowName);
|
|
7889
|
+
}
|
|
7890
|
+
await Promise.all(toKill.map(async ({ session, windowName }) => {
|
|
7891
|
+
log.info(`[cleanup] killing stale tmux window "${windowName}" (no matching worktree)`);
|
|
7892
|
+
const kill = Bun.spawn(["tmux", "kill-window", "-t", `${session}:${windowName}`], {
|
|
7893
|
+
stdout: "ignore",
|
|
7894
|
+
stderr: "ignore"
|
|
7895
|
+
});
|
|
7896
|
+
await kill.exited;
|
|
7897
|
+
}));
|
|
7898
|
+
} catch (err) {
|
|
7899
|
+
log.warn(`[cleanup] cleanupStaleWindows failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
7900
|
+
}
|
|
7901
|
+
}
|
|
7822
7902
|
async function mergeWorktree(name) {
|
|
7823
7903
|
log.debug(`[workmux:merge] running: workmux merge ${name}`);
|
|
7824
7904
|
await removeContainer(name);
|
|
@@ -8019,6 +8099,13 @@ function write(worktreeName, data) {
|
|
|
8019
8099
|
log.error(`[term] write(${worktreeName}) stdin closed`, err);
|
|
8020
8100
|
}
|
|
8021
8101
|
}
|
|
8102
|
+
async function sendKeys(worktreeName, hexBytes) {
|
|
8103
|
+
const session = sessions.get(worktreeName);
|
|
8104
|
+
if (!session)
|
|
8105
|
+
return;
|
|
8106
|
+
const windowTarget = `${session.groupedSessionName}:wm-${worktreeName}`;
|
|
8107
|
+
await asyncTmux(["tmux", "send-keys", "-t", windowTarget, "-H", ...hexBytes]);
|
|
8108
|
+
}
|
|
8022
8109
|
async function resize(worktreeName, cols, rows) {
|
|
8023
8110
|
const session = sessions.get(worktreeName);
|
|
8024
8111
|
if (!session)
|
|
@@ -8097,6 +8184,20 @@ function mapChecks(checks) {
|
|
|
8097
8184
|
runId: parseRunId(c.detailsUrl)
|
|
8098
8185
|
}));
|
|
8099
8186
|
}
|
|
8187
|
+
function parseReviewComments(json) {
|
|
8188
|
+
const raw = JSON.parse(json);
|
|
8189
|
+
const sorted = raw.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
8190
|
+
return sorted.slice(0, PR_FETCH_LIMIT).map((c) => ({
|
|
8191
|
+
type: "inline",
|
|
8192
|
+
author: c.user?.login ?? "unknown",
|
|
8193
|
+
body: c.body ?? "",
|
|
8194
|
+
createdAt: c.created_at ?? "",
|
|
8195
|
+
path: c.path ?? "",
|
|
8196
|
+
line: c.line ?? null,
|
|
8197
|
+
diffHunk: c.diff_hunk ?? "",
|
|
8198
|
+
isReply: c.in_reply_to_id !== undefined
|
|
8199
|
+
}));
|
|
8200
|
+
}
|
|
8100
8201
|
function parsePrResponse(json, repoLabel) {
|
|
8101
8202
|
const prs = new Map;
|
|
8102
8203
|
const entries = JSON.parse(json);
|
|
@@ -8111,6 +8212,7 @@ function parsePrResponse(json, repoLabel) {
|
|
|
8111
8212
|
ciStatus: summarizeChecks(entry.statusCheckRollup),
|
|
8112
8213
|
ciChecks: mapChecks(entry.statusCheckRollup),
|
|
8113
8214
|
comments: (entry.comments ?? []).map((c) => ({
|
|
8215
|
+
type: "comment",
|
|
8114
8216
|
author: c.author?.login ?? "unknown",
|
|
8115
8217
|
body: c.body ?? "",
|
|
8116
8218
|
createdAt: c.createdAt ?? ""
|
|
@@ -8161,6 +8263,45 @@ async function fetchAllPrs(repoSlug, repoLabel, cwd) {
|
|
|
8161
8263
|
return { ok: false, error: `failed to parse gh output for ${label}: ${err}` };
|
|
8162
8264
|
}
|
|
8163
8265
|
}
|
|
8266
|
+
async function mapWithConcurrency(items, limit, fn) {
|
|
8267
|
+
const results = new Array(items.length);
|
|
8268
|
+
let next = 0;
|
|
8269
|
+
async function worker() {
|
|
8270
|
+
while (next < items.length) {
|
|
8271
|
+
const idx = next++;
|
|
8272
|
+
results[idx] = await fn(items[idx]);
|
|
8273
|
+
}
|
|
8274
|
+
}
|
|
8275
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
8276
|
+
return results;
|
|
8277
|
+
}
|
|
8278
|
+
async function fetchReviewComments(prNumber, repoSlug, cwd) {
|
|
8279
|
+
const repoFlag = repoSlug ? repoSlug : "{owner}/{repo}";
|
|
8280
|
+
const args = [
|
|
8281
|
+
"gh",
|
|
8282
|
+
"api",
|
|
8283
|
+
`repos/${repoFlag}/pulls/${prNumber}/comments`,
|
|
8284
|
+
"--paginate"
|
|
8285
|
+
];
|
|
8286
|
+
const proc = Bun.spawn(args, {
|
|
8287
|
+
stdout: "pipe",
|
|
8288
|
+
stderr: "pipe",
|
|
8289
|
+
...cwd ? { cwd } : {}
|
|
8290
|
+
});
|
|
8291
|
+
const timeout = Bun.sleep(GH_TIMEOUT_MS).then(() => {
|
|
8292
|
+
proc.kill();
|
|
8293
|
+
return "timeout";
|
|
8294
|
+
});
|
|
8295
|
+
const raceResult = await Promise.race([proc.exited, timeout]);
|
|
8296
|
+
if (raceResult === "timeout" || raceResult !== 0)
|
|
8297
|
+
return [];
|
|
8298
|
+
try {
|
|
8299
|
+
const json = await new Response(proc.stdout).text();
|
|
8300
|
+
return parseReviewComments(json);
|
|
8301
|
+
} catch {
|
|
8302
|
+
return [];
|
|
8303
|
+
}
|
|
8304
|
+
}
|
|
8164
8305
|
async function fetchPrState(url) {
|
|
8165
8306
|
const proc = Bun.spawn(["gh", "pr", "view", url, "--json", "state"], {
|
|
8166
8307
|
stdout: "pipe",
|
|
@@ -8217,6 +8358,22 @@ async function syncPrStatus(getWorktreePaths, linkedRepos, projectDir) {
|
|
|
8217
8358
|
branchPrs.set(branch, existing);
|
|
8218
8359
|
}
|
|
8219
8360
|
}
|
|
8361
|
+
const reviewTuples = [];
|
|
8362
|
+
for (const entries of branchPrs.values()) {
|
|
8363
|
+
for (const entry of entries) {
|
|
8364
|
+
if (entry.state === "open") {
|
|
8365
|
+
const repoSlug = entry.repo ? linkedRepos.find((lr) => lr.alias === entry.repo)?.repo : undefined;
|
|
8366
|
+
reviewTuples.push({ entry, repoSlug });
|
|
8367
|
+
}
|
|
8368
|
+
}
|
|
8369
|
+
}
|
|
8370
|
+
if (reviewTuples.length > 0) {
|
|
8371
|
+
const reviewResults = await mapWithConcurrency(reviewTuples, 5, (t) => fetchReviewComments(t.entry.number, t.repoSlug, projectDir));
|
|
8372
|
+
for (let i = 0;i < reviewTuples.length; i++) {
|
|
8373
|
+
const entry = reviewTuples[i].entry;
|
|
8374
|
+
entry.comments = [...entry.comments, ...reviewResults[i]].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
8375
|
+
}
|
|
8376
|
+
}
|
|
8220
8377
|
const wtPaths = await getWorktreePaths();
|
|
8221
8378
|
const seen = new Set;
|
|
8222
8379
|
for (const [branch, entries] of branchPrs) {
|
|
@@ -8638,6 +8795,8 @@ function parseWsMessage(raw) {
|
|
|
8638
8795
|
switch (m.type) {
|
|
8639
8796
|
case "input":
|
|
8640
8797
|
return typeof m.data === "string" ? { type: "input", data: m.data } : null;
|
|
8798
|
+
case "sendKeys":
|
|
8799
|
+
return Array.isArray(m.hexBytes) && m.hexBytes.every((b) => typeof b === "string") ? { type: "sendKeys", hexBytes: m.hexBytes } : null;
|
|
8641
8800
|
case "selectPane":
|
|
8642
8801
|
return typeof m.pane === "number" ? { type: "selectPane", pane: m.pane } : null;
|
|
8643
8802
|
case "resize":
|
|
@@ -8778,7 +8937,11 @@ async function apiGetWorktrees(req) {
|
|
|
8778
8937
|
fetchAssignedIssues()
|
|
8779
8938
|
]);
|
|
8780
8939
|
const linearIssues = linearResult.ok ? linearResult.data : [];
|
|
8781
|
-
const
|
|
8940
|
+
const activeBranches = new Set(worktrees.map((wt) => wt.branch));
|
|
8941
|
+
activeBranches.add("main");
|
|
8942
|
+
cleanupStaleWindows(activeBranches, `${PROJECT_DIR}__worktrees/`);
|
|
8943
|
+
const nonMainWorktrees = worktrees.filter((wt) => wtPaths.has(wt.branch));
|
|
8944
|
+
const merged = await Promise.all(nonMainWorktrees.map(async (wt) => {
|
|
8782
8945
|
const st = status.find((s) => s.worktree.includes(wt.branch) || s.worktree.startsWith(wt.branch));
|
|
8783
8946
|
const wtDir = wtPaths.get(wt.branch);
|
|
8784
8947
|
const env = wtDir ? await readEnvLocal(wtDir) : {};
|
|
@@ -8824,6 +8987,17 @@ async function apiCreateWorktree(req) {
|
|
|
8824
8987
|
const agent = typeof body.agent === "string" ? body.agent : "claude";
|
|
8825
8988
|
const isSandbox = config.profiles.sandbox !== undefined && profileName === config.profiles.sandbox.name;
|
|
8826
8989
|
const profileConfig = isSandbox ? config.profiles.sandbox : config.profiles.default;
|
|
8990
|
+
let envOverrides;
|
|
8991
|
+
if (body.envOverrides && typeof body.envOverrides === "object" && !Array.isArray(body.envOverrides)) {
|
|
8992
|
+
const raw2 = body.envOverrides;
|
|
8993
|
+
const parsed = {};
|
|
8994
|
+
for (const [k, v] of Object.entries(raw2)) {
|
|
8995
|
+
if (typeof v === "string")
|
|
8996
|
+
parsed[k] = v;
|
|
8997
|
+
}
|
|
8998
|
+
if (Object.keys(parsed).length > 0)
|
|
8999
|
+
envOverrides = parsed;
|
|
9000
|
+
}
|
|
8827
9001
|
log.info(`[worktree:add] agent=${agent} profile=${profileName}${branch ? ` branch=${branch}` : ""}${prompt ? ` prompt="${prompt.slice(0, 80)}"` : ""}`);
|
|
8828
9002
|
const result = await addWorktree(branch, {
|
|
8829
9003
|
prompt,
|
|
@@ -8834,7 +9008,8 @@ async function apiCreateWorktree(req) {
|
|
|
8834
9008
|
isSandbox,
|
|
8835
9009
|
sandboxConfig: isSandbox ? config.profiles.sandbox : undefined,
|
|
8836
9010
|
services: config.services,
|
|
8837
|
-
mainRepoDir: PROJECT_DIR
|
|
9011
|
+
mainRepoDir: PROJECT_DIR,
|
|
9012
|
+
envOverrides
|
|
8838
9013
|
});
|
|
8839
9014
|
if (!result.ok)
|
|
8840
9015
|
return errorResponse(result.error, 422);
|
|
@@ -8853,9 +9028,24 @@ async function apiDeleteWorktree(name) {
|
|
|
8853
9028
|
}
|
|
8854
9029
|
async function apiOpenWorktree(name) {
|
|
8855
9030
|
log.info(`[worktree:open] name=${name}`);
|
|
9031
|
+
const wtPaths = await getWorktreePaths();
|
|
9032
|
+
const wtDir = wtPaths.get(name);
|
|
9033
|
+
if (wtDir) {
|
|
9034
|
+
const env = await readEnvLocal(wtDir);
|
|
9035
|
+
if (!env.PROFILE) {
|
|
9036
|
+
log.info(`[worktree:open] initializing env for ${name}`);
|
|
9037
|
+
await initWorktreeEnv(name, {
|
|
9038
|
+
profile: config.profiles.default.name,
|
|
9039
|
+
agent: "claude",
|
|
9040
|
+
services: config.services
|
|
9041
|
+
});
|
|
9042
|
+
wtCache = null;
|
|
9043
|
+
}
|
|
9044
|
+
}
|
|
8856
9045
|
const result = await openWorktree(name);
|
|
8857
9046
|
if (!result.ok)
|
|
8858
9047
|
return errorResponse(result.error, 422);
|
|
9048
|
+
wtCache = null;
|
|
8859
9049
|
return jsonResponse({ message: result.output });
|
|
8860
9050
|
}
|
|
8861
9051
|
async function apiSendPrompt(name, req) {
|
|
@@ -9023,6 +9213,9 @@ Bun.serve({
|
|
|
9023
9213
|
case "input":
|
|
9024
9214
|
write(worktree, msg.data);
|
|
9025
9215
|
break;
|
|
9216
|
+
case "sendKeys":
|
|
9217
|
+
await sendKeys(worktree, msg.hexBytes);
|
|
9218
|
+
break;
|
|
9026
9219
|
case "selectPane":
|
|
9027
9220
|
if (ws.data.attached) {
|
|
9028
9221
|
log.debug(`[ws] selectPane pane=${msg.pane} worktree=${worktree}`);
|