webmux 0.14.0 → 0.15.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 CHANGED
@@ -72,9 +72,10 @@ webmux # opens on http://localhost:5111
72
72
 
73
73
  ## Configuration
74
74
 
75
- webmux uses a single config file in the project root:
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>
@@ -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 resolve5 } from "path";
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 { default: { ...DEFAULT_CONFIG.profiles.default, panes: clonePanes(DEFAULT_CONFIG.profiles.default.panes) } };
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 { default: { ...DEFAULT_CONFIG.profiles.default, panes: clonePanes(DEFAULT_CONFIG.profiles.default.panes) } };
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 loadConfig(dir) {
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
- if (!text)
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
- return DEFAULT_CONFIG;
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 dirname3, resolve as resolve3 } from "path";
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 dirname2, join as join3 } from "path";
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(dirname2(artifacts.claudeSettingsPath), { recursive: true });
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(projectRoot) {
8066
- const resolved = resolve(projectRoot);
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.includes("can't find window")) {
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(projectRoot, branch, templates, ctx) {
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(projectRoot),
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 resolve2 } from "path";
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 = resolve2(worktreePath);
8351
- return entries.some((entry) => resolve2(entry.path) === resolvedPath);
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 resolve2(cwd, output);
8453
+ return resolve3(cwd, output);
8375
8454
  }
8376
8455
  function resolveWorktreeGitDir(cwd) {
8377
8456
  const output = runGit(["rev-parse", "--git-dir"], cwd);
8378
- return resolve2(cwd, output);
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(dirname3(worktreePath), { recursive: true });
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 resolve3(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
9035
+ return resolve4(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
8957
9036
  }
8958
9037
  listLocalBranches() {
8959
- return this.deps.git.listLocalBranches(resolve3(this.deps.projectRoot));
9038
+ return this.deps.git.listLocalBranches(resolve4(this.deps.projectRoot));
8960
9039
  }
8961
9040
  listCheckedOutBranches() {
8962
- return new Set(this.deps.git.listWorktrees(resolve3(this.deps.projectRoot)).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch));
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 projectRoot = resolve3(this.deps.projectRoot);
8966
- return this.deps.git.listWorktrees(projectRoot).filter((entry) => !entry.bare && resolve3(entry.path) !== projectRoot);
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((resolve4) => {
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
- resolve4(true);
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
- resolve4(false);
10082
+ resolve5(false);
10004
10083
  }
10005
10084
  };
10006
10085
  const timer = setTimeout(() => {
10007
10086
  if (!settled) {
10008
10087
  settled = true;
10009
- resolve4(false);
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 resolve4 } from "path";
10493
+ import { basename as basename2, resolve as resolve5 } from "path";
10415
10494
  function makeUnmanagedWorktreeId(path) {
10416
- return `unmanaged:${resolve4(path)}`;
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 = resolve4(repoRoot);
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 (resolve4(entry.path) === normalizedRepoRoot)
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 = gitRoot(options.projectDir ?? Bun.env.WEBMUX_PROJECT_DIR ?? process.cwd());
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: resolve5(PROJECT_DIR, lr.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 projectRoot = resolve5(PROJECT_DIR);
10753
- for (const entry of git.listWorktrees(projectRoot)) {
10754
- if (entry.bare || resolve5(entry.path) === projectRoot || !entry.branch)
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 = resolve5(STATIC_DIR);
11027
- if (!resolve5(filePath).startsWith(staticRoot + "/")) {
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);