webmux 0.14.0 → 0.16.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 +2 -1
- package/backend/dist/server.js +152 -71
- package/bin/webmux.js +147 -67
- package/frontend/dist/assets/index-BgwdjtQn.js +35 -0
- package/frontend/dist/assets/index-Bt0UjBTn.css +32 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-Bi9DHlpD.js +0 -34
- package/frontend/dist/assets/index-FwEUWC9Q.css +0 -32
package/README.md
CHANGED
|
@@ -72,9 +72,10 @@ webmux # opens on http://localhost:5111
|
|
|
72
72
|
|
|
73
73
|
## Configuration
|
|
74
74
|
|
|
75
|
-
webmux uses a
|
|
75
|
+
webmux uses a project config file in the project root, plus an optional local overlay:
|
|
76
76
|
|
|
77
77
|
- **`.webmux.yaml`** — Worktree root, pane layout, service ports, profiles, linked repos, and Docker sandbox settings.
|
|
78
|
+
- **`.webmux.local.yaml`** — Optional local-only overlay for additional `profiles` and `lifecycleHooks`. Profiles are additive, conflicting profile names are replaced by the local definition, and local lifecycle hook commands run after the project-level command for the same hook.
|
|
78
79
|
|
|
79
80
|
<details>
|
|
80
81
|
<summary><strong>.webmux.yaml example</strong></summary>
|
package/backend/dist/server.js
CHANGED
|
@@ -6905,7 +6905,7 @@ var require_public_api = __commonJS((exports) => {
|
|
|
6905
6905
|
});
|
|
6906
6906
|
|
|
6907
6907
|
// backend/src/server.ts
|
|
6908
|
-
import { join as join6, resolve as
|
|
6908
|
+
import { join as join6, resolve as resolve6 } from "path";
|
|
6909
6909
|
import { networkInterfaces } from "os";
|
|
6910
6910
|
|
|
6911
6911
|
// backend/src/lib/log.ts
|
|
@@ -7196,7 +7196,7 @@ async function loadControlToken() {
|
|
|
7196
7196
|
|
|
7197
7197
|
// backend/src/adapters/config.ts
|
|
7198
7198
|
import { readFileSync } from "fs";
|
|
7199
|
-
import { join } from "path";
|
|
7199
|
+
import { dirname as dirname2, join, resolve } from "path";
|
|
7200
7200
|
|
|
7201
7201
|
// node_modules/.bun/yaml@2.8.2/node_modules/yaml/dist/index.js
|
|
7202
7202
|
var composer = require_composer();
|
|
@@ -7275,6 +7275,23 @@ var DEFAULT_CONFIG = {
|
|
|
7275
7275
|
function clonePanes(panes) {
|
|
7276
7276
|
return panes.map((pane) => ({ ...pane }));
|
|
7277
7277
|
}
|
|
7278
|
+
function cloneMounts(mounts) {
|
|
7279
|
+
return mounts?.map((mount) => ({ ...mount }));
|
|
7280
|
+
}
|
|
7281
|
+
function cloneProfile(profile) {
|
|
7282
|
+
return {
|
|
7283
|
+
...profile,
|
|
7284
|
+
envPassthrough: [...profile.envPassthrough],
|
|
7285
|
+
panes: clonePanes(profile.panes),
|
|
7286
|
+
...profile.mounts ? { mounts: cloneMounts(profile.mounts) } : {}
|
|
7287
|
+
};
|
|
7288
|
+
}
|
|
7289
|
+
function cloneProfiles(profiles) {
|
|
7290
|
+
return Object.fromEntries(Object.entries(profiles).map(([name, profile]) => [name, cloneProfile(profile)]));
|
|
7291
|
+
}
|
|
7292
|
+
function defaultProfiles() {
|
|
7293
|
+
return { default: cloneProfile(DEFAULT_CONFIG.profiles.default) };
|
|
7294
|
+
}
|
|
7278
7295
|
function isRecord(value) {
|
|
7279
7296
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7280
7297
|
}
|
|
@@ -7347,16 +7364,16 @@ function parseProfile(raw, fallbackRuntime) {
|
|
|
7347
7364
|
...mounts ? { mounts } : {}
|
|
7348
7365
|
};
|
|
7349
7366
|
}
|
|
7350
|
-
function parseProfiles(raw) {
|
|
7367
|
+
function parseProfiles(raw, includeDefaultProfile) {
|
|
7351
7368
|
if (!isRecord(raw))
|
|
7352
|
-
return
|
|
7369
|
+
return includeDefaultProfile ? defaultProfiles() : {};
|
|
7353
7370
|
const profiles = Object.entries(raw).reduce((acc, [name, value]) => {
|
|
7354
7371
|
const fallbackRuntime = name === "sandbox" ? "docker" : "host";
|
|
7355
7372
|
acc[name] = parseProfile(value, fallbackRuntime);
|
|
7356
7373
|
return acc;
|
|
7357
7374
|
}, {});
|
|
7358
7375
|
if (Object.keys(profiles).length === 0) {
|
|
7359
|
-
return
|
|
7376
|
+
return includeDefaultProfile ? defaultProfiles() : {};
|
|
7360
7377
|
}
|
|
7361
7378
|
return profiles;
|
|
7362
7379
|
}
|
|
@@ -7428,6 +7445,69 @@ function getDefaultProfileName(config) {
|
|
|
7428
7445
|
function readConfigFile(root) {
|
|
7429
7446
|
return readFileSync(join(root, ".webmux.yaml"), "utf8");
|
|
7430
7447
|
}
|
|
7448
|
+
function readLocalConfigFile(root) {
|
|
7449
|
+
return readFileSync(join(root, ".webmux.local.yaml"), "utf8");
|
|
7450
|
+
}
|
|
7451
|
+
function parseConfigDocument(text) {
|
|
7452
|
+
const parsed = $parse(text);
|
|
7453
|
+
return isRecord(parsed) ? parsed : {};
|
|
7454
|
+
}
|
|
7455
|
+
function parseProjectConfig(parsed) {
|
|
7456
|
+
return {
|
|
7457
|
+
name: typeof parsed.name === "string" && parsed.name.trim() ? parsed.name.trim() : DEFAULT_CONFIG.name,
|
|
7458
|
+
workspace: {
|
|
7459
|
+
mainBranch: isRecord(parsed.workspace) && typeof parsed.workspace.mainBranch === "string" ? parsed.workspace.mainBranch : DEFAULT_CONFIG.workspace.mainBranch,
|
|
7460
|
+
worktreeRoot: isRecord(parsed.workspace) && typeof parsed.workspace.worktreeRoot === "string" ? parsed.workspace.worktreeRoot : DEFAULT_CONFIG.workspace.worktreeRoot,
|
|
7461
|
+
defaultAgent: isRecord(parsed.workspace) ? parseAgentKind(parsed.workspace.defaultAgent) : DEFAULT_CONFIG.workspace.defaultAgent
|
|
7462
|
+
},
|
|
7463
|
+
profiles: parseProfiles(parsed.profiles, true),
|
|
7464
|
+
services: parseServices(parsed.services),
|
|
7465
|
+
startupEnvs: parseStartupEnvs(parsed.startupEnvs),
|
|
7466
|
+
integrations: {
|
|
7467
|
+
github: {
|
|
7468
|
+
linkedRepos: isRecord(parsed.integrations) && isRecord(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github.linkedRepos) : isRecord(parsed.integrations) && Array.isArray(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github) : []
|
|
7469
|
+
},
|
|
7470
|
+
linear: {
|
|
7471
|
+
enabled: isRecord(parsed.integrations) && isRecord(parsed.integrations.linear) && typeof parsed.integrations.linear.enabled === "boolean" ? parsed.integrations.linear.enabled : DEFAULT_CONFIG.integrations.linear.enabled
|
|
7472
|
+
}
|
|
7473
|
+
},
|
|
7474
|
+
lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
|
|
7475
|
+
autoName: parseAutoName(parsed.auto_name)
|
|
7476
|
+
};
|
|
7477
|
+
}
|
|
7478
|
+
function defaultConfig() {
|
|
7479
|
+
return parseProjectConfig({});
|
|
7480
|
+
}
|
|
7481
|
+
function loadLocalProjectConfigOverlay(root) {
|
|
7482
|
+
try {
|
|
7483
|
+
const text = readLocalConfigFile(root).trim();
|
|
7484
|
+
if (!text) {
|
|
7485
|
+
return { profiles: {}, lifecycleHooks: {} };
|
|
7486
|
+
}
|
|
7487
|
+
const parsed = parseConfigDocument(text);
|
|
7488
|
+
return {
|
|
7489
|
+
profiles: parseProfiles(parsed.profiles, false),
|
|
7490
|
+
lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks)
|
|
7491
|
+
};
|
|
7492
|
+
} catch {
|
|
7493
|
+
return { profiles: {}, lifecycleHooks: {} };
|
|
7494
|
+
}
|
|
7495
|
+
}
|
|
7496
|
+
function mergeHookCommand(projectCommand, localCommand) {
|
|
7497
|
+
if (projectCommand && localCommand) {
|
|
7498
|
+
return ["set -e", projectCommand, localCommand].join(`
|
|
7499
|
+
`);
|
|
7500
|
+
}
|
|
7501
|
+
return localCommand ?? projectCommand;
|
|
7502
|
+
}
|
|
7503
|
+
function mergeLifecycleHooks(projectHooks, localHooks) {
|
|
7504
|
+
const postCreate = mergeHookCommand(projectHooks.postCreate, localHooks.postCreate);
|
|
7505
|
+
const preRemove = mergeHookCommand(projectHooks.preRemove, localHooks.preRemove);
|
|
7506
|
+
return {
|
|
7507
|
+
...postCreate ? { postCreate } : {},
|
|
7508
|
+
...preRemove ? { preRemove } : {}
|
|
7509
|
+
};
|
|
7510
|
+
}
|
|
7431
7511
|
function gitRoot(dir) {
|
|
7432
7512
|
const result = Bun.spawnSync(["git", "rev-parse", "--show-toplevel"], { stdout: "pipe", stderr: "pipe", cwd: dir });
|
|
7433
7513
|
if (result.exitCode !== 0)
|
|
@@ -7435,37 +7515,31 @@ function gitRoot(dir) {
|
|
|
7435
7515
|
const root = new TextDecoder().decode(result.stdout).trim();
|
|
7436
7516
|
return root || dir;
|
|
7437
7517
|
}
|
|
7438
|
-
function
|
|
7518
|
+
function projectRoot(dir) {
|
|
7519
|
+
const result = Bun.spawnSync(["git", "rev-parse", "--git-common-dir"], { stdout: "pipe", stderr: "pipe", cwd: dir });
|
|
7520
|
+
if (result.exitCode !== 0)
|
|
7521
|
+
return gitRoot(dir);
|
|
7522
|
+
const commonDir = new TextDecoder().decode(result.stdout).trim();
|
|
7523
|
+
return commonDir ? dirname2(resolve(dir, commonDir)) : gitRoot(dir);
|
|
7524
|
+
}
|
|
7525
|
+
function loadConfig(dir, options = {}) {
|
|
7526
|
+
const root = options.resolvedRoot ? dir : projectRoot(dir);
|
|
7527
|
+
let projectConfig;
|
|
7439
7528
|
try {
|
|
7440
|
-
const root = gitRoot(dir);
|
|
7441
7529
|
const text = readConfigFile(root).trim();
|
|
7442
|
-
|
|
7443
|
-
return DEFAULT_CONFIG;
|
|
7444
|
-
const parsed = $parse(text);
|
|
7445
|
-
return {
|
|
7446
|
-
name: typeof parsed.name === "string" && parsed.name.trim() ? parsed.name.trim() : DEFAULT_CONFIG.name,
|
|
7447
|
-
workspace: {
|
|
7448
|
-
mainBranch: isRecord(parsed.workspace) && typeof parsed.workspace.mainBranch === "string" ? parsed.workspace.mainBranch : DEFAULT_CONFIG.workspace.mainBranch,
|
|
7449
|
-
worktreeRoot: isRecord(parsed.workspace) && typeof parsed.workspace.worktreeRoot === "string" ? parsed.workspace.worktreeRoot : DEFAULT_CONFIG.workspace.worktreeRoot,
|
|
7450
|
-
defaultAgent: isRecord(parsed.workspace) ? parseAgentKind(parsed.workspace.defaultAgent) : DEFAULT_CONFIG.workspace.defaultAgent
|
|
7451
|
-
},
|
|
7452
|
-
profiles: parseProfiles(parsed.profiles),
|
|
7453
|
-
services: parseServices(parsed.services),
|
|
7454
|
-
startupEnvs: parseStartupEnvs(parsed.startupEnvs),
|
|
7455
|
-
integrations: {
|
|
7456
|
-
github: {
|
|
7457
|
-
linkedRepos: isRecord(parsed.integrations) && isRecord(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github.linkedRepos) : isRecord(parsed.integrations) && Array.isArray(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github) : []
|
|
7458
|
-
},
|
|
7459
|
-
linear: {
|
|
7460
|
-
enabled: isRecord(parsed.integrations) && isRecord(parsed.integrations.linear) && typeof parsed.integrations.linear.enabled === "boolean" ? parsed.integrations.linear.enabled : DEFAULT_CONFIG.integrations.linear.enabled
|
|
7461
|
-
}
|
|
7462
|
-
},
|
|
7463
|
-
lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
|
|
7464
|
-
autoName: parseAutoName(parsed.auto_name)
|
|
7465
|
-
};
|
|
7530
|
+
projectConfig = text ? parseProjectConfig(parseConfigDocument(text)) : defaultConfig();
|
|
7466
7531
|
} catch {
|
|
7467
|
-
|
|
7532
|
+
projectConfig = defaultConfig();
|
|
7468
7533
|
}
|
|
7534
|
+
const localOverlay = loadLocalProjectConfigOverlay(root);
|
|
7535
|
+
return {
|
|
7536
|
+
...projectConfig,
|
|
7537
|
+
profiles: {
|
|
7538
|
+
...cloneProfiles(projectConfig.profiles),
|
|
7539
|
+
...cloneProfiles(localOverlay.profiles)
|
|
7540
|
+
},
|
|
7541
|
+
lifecycleHooks: mergeLifecycleHooks(projectConfig.lifecycleHooks, localOverlay.lifecycleHooks)
|
|
7542
|
+
};
|
|
7469
7543
|
}
|
|
7470
7544
|
function expandTemplate(template, env) {
|
|
7471
7545
|
return template.replace(/\$\{(\w+)\}/g, (_, key) => env[key] ?? "");
|
|
@@ -7612,11 +7686,11 @@ async function fetchAssignedIssues() {
|
|
|
7612
7686
|
// backend/src/services/lifecycle-service.ts
|
|
7613
7687
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
7614
7688
|
import { mkdir as mkdir4 } from "fs/promises";
|
|
7615
|
-
import { dirname as
|
|
7689
|
+
import { dirname as dirname4, resolve as resolve4 } from "path";
|
|
7616
7690
|
|
|
7617
7691
|
// backend/src/adapters/agent-runtime.ts
|
|
7618
7692
|
import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
|
|
7619
|
-
import { dirname as
|
|
7693
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
7620
7694
|
|
|
7621
7695
|
// backend/src/adapters/fs.ts
|
|
7622
7696
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
@@ -8024,7 +8098,7 @@ async function ensureAgentRuntimeArtifacts(input) {
|
|
|
8024
8098
|
agentCtlPath: join3(storagePaths.webmuxDir, "webmux-agentctl"),
|
|
8025
8099
|
claudeSettingsPath: join3(input.worktreePath, ".claude", "settings.local.json")
|
|
8026
8100
|
};
|
|
8027
|
-
await mkdir3(
|
|
8101
|
+
await mkdir3(dirname3(artifacts.claudeSettingsPath), { recursive: true });
|
|
8028
8102
|
await Bun.write(artifacts.agentCtlPath, buildAgentCtlScript());
|
|
8029
8103
|
await chmod2(artifacts.agentCtlPath, 493);
|
|
8030
8104
|
const hookSettings = buildClaudeHookSettings(artifacts);
|
|
@@ -8038,7 +8112,7 @@ async function ensureAgentRuntimeArtifacts(input) {
|
|
|
8038
8112
|
|
|
8039
8113
|
// backend/src/adapters/tmux.ts
|
|
8040
8114
|
import { createHash } from "crypto";
|
|
8041
|
-
import { basename, resolve } from "path";
|
|
8115
|
+
import { basename, resolve as resolve2 } from "path";
|
|
8042
8116
|
function runTmux(args) {
|
|
8043
8117
|
const result = Bun.spawnSync(["tmux", ...args], {
|
|
8044
8118
|
stdout: "pipe",
|
|
@@ -8057,13 +8131,16 @@ function assertTmuxOk(args, action) {
|
|
|
8057
8131
|
}
|
|
8058
8132
|
return result.stdout;
|
|
8059
8133
|
}
|
|
8134
|
+
function isIgnorableKillWindowError(stderr) {
|
|
8135
|
+
return stderr.includes("can't find window") || stderr.includes("can't find session") || stderr.includes("no server running") || stderr.includes("error connecting to") && stderr.includes("No such file or directory");
|
|
8136
|
+
}
|
|
8060
8137
|
function sanitizeTmuxNameSegment(value, maxLength = 24) {
|
|
8061
8138
|
const sanitized = value.toLowerCase().replace(/[^a-z0-9_.-]+/g, "-").replace(/-{2,}/g, "-").replace(/^[.-]+|[.-]+$/g, "");
|
|
8062
8139
|
const trimmed = sanitized.slice(0, maxLength);
|
|
8063
8140
|
return trimmed || "x";
|
|
8064
8141
|
}
|
|
8065
|
-
function buildProjectSessionName(
|
|
8066
|
-
const resolved =
|
|
8142
|
+
function buildProjectSessionName(projectRoot2) {
|
|
8143
|
+
const resolved = resolve2(projectRoot2);
|
|
8067
8144
|
const base = sanitizeTmuxNameSegment(basename(resolved), 18);
|
|
8068
8145
|
const hash = createHash("sha1").update(resolved).digest("hex").slice(0, 8);
|
|
8069
8146
|
return `wm-${base}-${hash}`;
|
|
@@ -8090,8 +8167,10 @@ class BunTmuxGateway {
|
|
|
8090
8167
|
ensureSession(sessionName, cwd) {
|
|
8091
8168
|
const check = runTmux(["has-session", "-t", sessionName]);
|
|
8092
8169
|
if (check.exitCode !== 0) {
|
|
8093
|
-
assertTmuxOk(["new-session", "-d", "-s", sessionName, "-c", cwd], `create tmux session ${sessionName}`);
|
|
8170
|
+
assertTmuxOk(["new-session", "-d", "-s", sessionName, "-c", cwd, ";", "set-option", "-t", sessionName, "destroy-unattached", "off"], `create tmux session ${sessionName}`);
|
|
8171
|
+
return;
|
|
8094
8172
|
}
|
|
8173
|
+
assertTmuxOk(["set-option", "-t", sessionName, "destroy-unattached", "off"], `set destroy-unattached off for ${sessionName}`);
|
|
8095
8174
|
}
|
|
8096
8175
|
hasWindow(sessionName, windowName) {
|
|
8097
8176
|
const result = runTmux(["list-windows", "-t", sessionName, "-F", "#{window_name}"]);
|
|
@@ -8102,7 +8181,7 @@ class BunTmuxGateway {
|
|
|
8102
8181
|
}
|
|
8103
8182
|
killWindow(sessionName, windowName) {
|
|
8104
8183
|
const result = runTmux(["kill-window", "-t", `${sessionName}:${windowName}`]);
|
|
8105
|
-
if (result.exitCode !== 0 && !result.stderr
|
|
8184
|
+
if (result.exitCode !== 0 && !isIgnorableKillWindowError(result.stderr)) {
|
|
8106
8185
|
throw new Error(`kill tmux window ${sessionName}:${windowName} failed: ${result.stderr}`);
|
|
8107
8186
|
}
|
|
8108
8187
|
}
|
|
@@ -8246,7 +8325,7 @@ function resolvePaneStartupCommand(template, commands) {
|
|
|
8246
8325
|
return template.command;
|
|
8247
8326
|
}
|
|
8248
8327
|
}
|
|
8249
|
-
function planSessionLayout(
|
|
8328
|
+
function planSessionLayout(projectRoot2, branch, templates, ctx) {
|
|
8250
8329
|
if (templates.length === 0) {
|
|
8251
8330
|
throw new Error("At least one pane template is required");
|
|
8252
8331
|
}
|
|
@@ -8267,7 +8346,7 @@ function planSessionLayout(projectRoot, branch, templates, ctx) {
|
|
|
8267
8346
|
});
|
|
8268
8347
|
const focusPaneIndex = panes.find((pane) => pane.focus)?.index ?? 0;
|
|
8269
8348
|
return {
|
|
8270
|
-
sessionName: buildProjectSessionName(
|
|
8349
|
+
sessionName: buildProjectSessionName(projectRoot2),
|
|
8271
8350
|
windowName: buildWorktreeWindowName(branch),
|
|
8272
8351
|
shellCommand: ctx.paneCommands.shell,
|
|
8273
8352
|
panes,
|
|
@@ -8313,7 +8392,7 @@ import { randomUUID } from "crypto";
|
|
|
8313
8392
|
|
|
8314
8393
|
// backend/src/adapters/git.ts
|
|
8315
8394
|
import { rmSync } from "fs";
|
|
8316
|
-
import { resolve as
|
|
8395
|
+
import { resolve as resolve3 } from "path";
|
|
8317
8396
|
function runGit(args, cwd) {
|
|
8318
8397
|
const result = Bun.spawnSync(["git", ...args], {
|
|
8319
8398
|
cwd,
|
|
@@ -8347,8 +8426,8 @@ function errorMessage(error) {
|
|
|
8347
8426
|
return error instanceof Error ? error.message : String(error);
|
|
8348
8427
|
}
|
|
8349
8428
|
function isRegisteredWorktree(entries, worktreePath) {
|
|
8350
|
-
const resolvedPath =
|
|
8351
|
-
return entries.some((entry) =>
|
|
8429
|
+
const resolvedPath = resolve3(worktreePath);
|
|
8430
|
+
return entries.some((entry) => resolve3(entry.path) === resolvedPath);
|
|
8352
8431
|
}
|
|
8353
8432
|
function removeDirectory(path) {
|
|
8354
8433
|
rmSync(path, {
|
|
@@ -8371,11 +8450,11 @@ function currentCheckoutRef(cwd) {
|
|
|
8371
8450
|
}
|
|
8372
8451
|
function resolveWorktreeRoot(cwd) {
|
|
8373
8452
|
const output = runGit(["rev-parse", "--show-toplevel"], cwd);
|
|
8374
|
-
return
|
|
8453
|
+
return resolve3(cwd, output);
|
|
8375
8454
|
}
|
|
8376
8455
|
function resolveWorktreeGitDir(cwd) {
|
|
8377
8456
|
const output = runGit(["rev-parse", "--git-dir"], cwd);
|
|
8378
|
-
return
|
|
8457
|
+
return resolve3(cwd, output);
|
|
8379
8458
|
}
|
|
8380
8459
|
function parseGitWorktreePorcelain(output) {
|
|
8381
8460
|
const entries = [];
|
|
@@ -8718,7 +8797,7 @@ class LifecycleService {
|
|
|
8718
8797
|
agent,
|
|
8719
8798
|
phase: "creating_worktree"
|
|
8720
8799
|
});
|
|
8721
|
-
await mkdir4(
|
|
8800
|
+
await mkdir4(dirname4(worktreePath), { recursive: true });
|
|
8722
8801
|
initialized = await createManagedWorktree({
|
|
8723
8802
|
repoRoot: this.deps.projectRoot,
|
|
8724
8803
|
worktreePath,
|
|
@@ -8953,17 +9032,17 @@ class LifecycleService {
|
|
|
8953
9032
|
return allocateServicePorts(metas, this.deps.config.services);
|
|
8954
9033
|
}
|
|
8955
9034
|
resolveWorktreePath(branch) {
|
|
8956
|
-
return
|
|
9035
|
+
return resolve4(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
|
|
8957
9036
|
}
|
|
8958
9037
|
listLocalBranches() {
|
|
8959
|
-
return this.deps.git.listLocalBranches(
|
|
9038
|
+
return this.deps.git.listLocalBranches(resolve4(this.deps.projectRoot));
|
|
8960
9039
|
}
|
|
8961
9040
|
listCheckedOutBranches() {
|
|
8962
|
-
return new Set(this.deps.git.listWorktrees(
|
|
9041
|
+
return new Set(this.deps.git.listWorktrees(resolve4(this.deps.projectRoot)).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch));
|
|
8963
9042
|
}
|
|
8964
9043
|
listProjectWorktrees() {
|
|
8965
|
-
const
|
|
8966
|
-
return this.deps.git.listWorktrees(
|
|
9044
|
+
const projectRoot2 = resolve4(this.deps.projectRoot);
|
|
9045
|
+
return this.deps.git.listWorktrees(projectRoot2).filter((entry) => !entry.bare && resolve4(entry.path) !== projectRoot2);
|
|
8967
9046
|
}
|
|
8968
9047
|
async readManagedMetas() {
|
|
8969
9048
|
const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
|
|
@@ -9984,7 +10063,7 @@ class BunPortProbe {
|
|
|
9984
10063
|
this.hostnames = hostnames;
|
|
9985
10064
|
}
|
|
9986
10065
|
isListening(port) {
|
|
9987
|
-
return new Promise((
|
|
10066
|
+
return new Promise((resolve5) => {
|
|
9988
10067
|
let settled = false;
|
|
9989
10068
|
let pending = this.hostnames.length;
|
|
9990
10069
|
const settle = (result) => {
|
|
@@ -9993,20 +10072,20 @@ class BunPortProbe {
|
|
|
9993
10072
|
if (result) {
|
|
9994
10073
|
settled = true;
|
|
9995
10074
|
clearTimeout(timer);
|
|
9996
|
-
|
|
10075
|
+
resolve5(true);
|
|
9997
10076
|
return;
|
|
9998
10077
|
}
|
|
9999
10078
|
pending--;
|
|
10000
10079
|
if (pending === 0) {
|
|
10001
10080
|
settled = true;
|
|
10002
10081
|
clearTimeout(timer);
|
|
10003
|
-
|
|
10082
|
+
resolve5(false);
|
|
10004
10083
|
}
|
|
10005
10084
|
};
|
|
10006
10085
|
const timer = setTimeout(() => {
|
|
10007
10086
|
if (!settled) {
|
|
10008
10087
|
settled = true;
|
|
10009
|
-
|
|
10088
|
+
resolve5(false);
|
|
10010
10089
|
}
|
|
10011
10090
|
}, this.timeoutMs);
|
|
10012
10091
|
for (const hostname of this.hostnames) {
|
|
@@ -10411,9 +10490,9 @@ class ProjectRuntime {
|
|
|
10411
10490
|
}
|
|
10412
10491
|
|
|
10413
10492
|
// backend/src/services/reconciliation-service.ts
|
|
10414
|
-
import { basename as basename2, resolve as
|
|
10493
|
+
import { basename as basename2, resolve as resolve5 } from "path";
|
|
10415
10494
|
function makeUnmanagedWorktreeId(path) {
|
|
10416
|
-
return `unmanaged:${
|
|
10495
|
+
return `unmanaged:${resolve5(path)}`;
|
|
10417
10496
|
}
|
|
10418
10497
|
function isValidPort2(port) {
|
|
10419
10498
|
return port !== null && Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
@@ -10456,7 +10535,7 @@ class ReconciliationService {
|
|
|
10456
10535
|
this.deps = deps;
|
|
10457
10536
|
}
|
|
10458
10537
|
async reconcile(repoRoot) {
|
|
10459
|
-
const normalizedRepoRoot =
|
|
10538
|
+
const normalizedRepoRoot = resolve5(repoRoot);
|
|
10460
10539
|
const worktrees = this.deps.git.listWorktrees(normalizedRepoRoot);
|
|
10461
10540
|
const sessionName = buildProjectSessionName(normalizedRepoRoot);
|
|
10462
10541
|
let windows = [];
|
|
@@ -10469,7 +10548,7 @@ class ReconciliationService {
|
|
|
10469
10548
|
for (const entry of worktrees) {
|
|
10470
10549
|
if (entry.bare)
|
|
10471
10550
|
continue;
|
|
10472
|
-
if (
|
|
10551
|
+
if (resolve5(entry.path) === normalizedRepoRoot)
|
|
10473
10552
|
continue;
|
|
10474
10553
|
const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
|
|
10475
10554
|
const meta = await readWorktreeMeta(gitDir);
|
|
@@ -10548,8 +10627,8 @@ class WorktreeCreationTracker {
|
|
|
10548
10627
|
// backend/src/runtime.ts
|
|
10549
10628
|
function createWebmuxRuntime(options = {}) {
|
|
10550
10629
|
const port = options.port ?? parseInt(Bun.env.PORT || "5111", 10);
|
|
10551
|
-
const projectDir =
|
|
10552
|
-
const config = loadConfig(projectDir);
|
|
10630
|
+
const projectDir = projectRoot(options.projectDir ?? Bun.env.WEBMUX_PROJECT_DIR ?? process.cwd());
|
|
10631
|
+
const config = loadConfig(projectDir, { resolvedRoot: true });
|
|
10553
10632
|
const git = new BunGitGateway;
|
|
10554
10633
|
const portProbe = new BunPortProbe;
|
|
10555
10634
|
const tmux = new BunTmuxGateway;
|
|
@@ -10640,7 +10719,7 @@ function getFrontendConfig() {
|
|
|
10640
10719
|
startupEnvs: config.startupEnvs,
|
|
10641
10720
|
linkedRepos: config.integrations.github.linkedRepos.map((lr) => ({
|
|
10642
10721
|
alias: lr.alias,
|
|
10643
|
-
...lr.dir ? { dir:
|
|
10722
|
+
...lr.dir ? { dir: resolve6(PROJECT_DIR, lr.dir) } : {}
|
|
10644
10723
|
}))
|
|
10645
10724
|
};
|
|
10646
10725
|
}
|
|
@@ -10749,9 +10828,9 @@ async function hasValidControlToken(req) {
|
|
|
10749
10828
|
}
|
|
10750
10829
|
async function getWorktreeGitDirs() {
|
|
10751
10830
|
const gitDirs = new Map;
|
|
10752
|
-
const
|
|
10753
|
-
for (const entry of git.listWorktrees(
|
|
10754
|
-
if (entry.bare ||
|
|
10831
|
+
const projectRoot2 = resolve6(PROJECT_DIR);
|
|
10832
|
+
for (const entry of git.listWorktrees(projectRoot2)) {
|
|
10833
|
+
if (entry.bare || resolve6(entry.path) === projectRoot2 || !entry.branch)
|
|
10755
10834
|
continue;
|
|
10756
10835
|
gitDirs.set(entry.branch, git.resolveWorktreeGitDir(entry.path));
|
|
10757
10836
|
}
|
|
@@ -11023,8 +11102,8 @@ Bun.serve({
|
|
|
11023
11102
|
const url = new URL(req.url);
|
|
11024
11103
|
const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
|
|
11025
11104
|
const filePath = join6(STATIC_DIR, rawPath);
|
|
11026
|
-
const staticRoot =
|
|
11027
|
-
if (!
|
|
11105
|
+
const staticRoot = resolve6(STATIC_DIR);
|
|
11106
|
+
if (!resolve6(filePath).startsWith(staticRoot + "/")) {
|
|
11028
11107
|
return new Response("Forbidden", { status: 403 });
|
|
11029
11108
|
}
|
|
11030
11109
|
const file = Bun.file(filePath);
|
|
@@ -11039,6 +11118,8 @@ Bun.serve({
|
|
|
11039
11118
|
return new Response("Not Found", { status: 404 });
|
|
11040
11119
|
},
|
|
11041
11120
|
websocket: {
|
|
11121
|
+
idleTimeout: 255,
|
|
11122
|
+
sendPings: true,
|
|
11042
11123
|
data: {},
|
|
11043
11124
|
open(ws) {
|
|
11044
11125
|
log.debug(`[ws] open branch=${ws.data.branch}`);
|
|
@@ -11109,8 +11190,8 @@ Bun.serve({
|
|
|
11109
11190
|
break;
|
|
11110
11191
|
}
|
|
11111
11192
|
},
|
|
11112
|
-
async close(ws) {
|
|
11113
|
-
log.debug(`[ws] close branch=${ws.data.branch} attached=${ws.data.attached} worktreeId=${ws.data.worktreeId}`);
|
|
11193
|
+
async close(ws, code, reason) {
|
|
11194
|
+
log.debug(`[ws] close branch=${ws.data.branch} code=${code} reason=${reason} attached=${ws.data.attached} worktreeId=${ws.data.worktreeId}`);
|
|
11114
11195
|
if (ws.data.worktreeId) {
|
|
11115
11196
|
clearCallbacks(ws.data.worktreeId);
|
|
11116
11197
|
await detach(ws.data.worktreeId);
|