webmux 0.9.0 → 0.10.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.
@@ -7253,7 +7253,7 @@ var DEFAULT_CONFIG = {
7253
7253
  name: "Webmux",
7254
7254
  workspace: {
7255
7255
  mainBranch: "main",
7256
- worktreeRoot: "__worktrees",
7256
+ worktreeRoot: "../worktrees",
7257
7257
  defaultAgent: "claude"
7258
7258
  },
7259
7259
  profiles: {
@@ -7619,6 +7619,7 @@ import { dirname as dirname2, join as join3 } from "path";
7619
7619
  import { mkdir as mkdir2 } from "fs/promises";
7620
7620
  import { join as join2 } from "path";
7621
7621
  var SAFE_ENV_VALUE_RE = /^[A-Za-z0-9_./:@%+=,-]+$/;
7622
+ var DOTENV_LINE_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)/;
7622
7623
  function stringifyAllocatedPorts(ports) {
7623
7624
  const entries = Object.entries(ports).map(([key, value]) => [key, String(value)]);
7624
7625
  return Object.fromEntries(entries);
@@ -7628,6 +7629,34 @@ function quoteEnvValue(value) {
7628
7629
  return value;
7629
7630
  return `'${value.replaceAll("'", "'\\''")}'`;
7630
7631
  }
7632
+ function parseDotenv(content) {
7633
+ const env = {};
7634
+ for (const line of content.split(`
7635
+ `)) {
7636
+ if (line.trimStart().startsWith("#"))
7637
+ continue;
7638
+ const match = DOTENV_LINE_RE.exec(line);
7639
+ if (!match)
7640
+ continue;
7641
+ const key = match[1];
7642
+ let value = match[2];
7643
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
7644
+ value = value.slice(1, -1);
7645
+ } else {
7646
+ value = value.trimEnd();
7647
+ }
7648
+ env[key] = value;
7649
+ }
7650
+ return env;
7651
+ }
7652
+ async function loadDotenvLocal(worktreePath) {
7653
+ try {
7654
+ const content = await Bun.file(join2(worktreePath, ".env.local")).text();
7655
+ return parseDotenv(content);
7656
+ } catch {
7657
+ return {};
7658
+ }
7659
+ }
7631
7660
  function getWorktreeStoragePaths(gitDir) {
7632
7661
  const webmuxDir = join2(gitDir, "webmux");
7633
7662
  return {
@@ -7657,8 +7686,9 @@ async function writeWorktreeMeta(gitDir, meta) {
7657
7686
  await Bun.write(metaPath, JSON.stringify(meta, null, 2) + `
7658
7687
  `);
7659
7688
  }
7660
- function buildRuntimeEnvMap(meta, extraEnv = {}) {
7689
+ function buildRuntimeEnvMap(meta, extraEnv = {}, dotenvValues = {}) {
7661
7690
  return {
7691
+ ...dotenvValues,
7662
7692
  ...meta.startupEnvValues,
7663
7693
  ...stringifyAllocatedPorts(meta.allocatedPorts),
7664
7694
  ...extraEnv,
@@ -8538,7 +8568,7 @@ async function initializeManagedWorktree(opts) {
8538
8568
  };
8539
8569
  const paths = await ensureWorktreeStorageDirs(opts.gitDir);
8540
8570
  await writeWorktreeMeta(opts.gitDir, meta);
8541
- const runtimeEnv = buildRuntimeEnvMap(meta, opts.runtimeEnvExtras);
8571
+ const runtimeEnv = buildRuntimeEnvMap(meta, opts.runtimeEnvExtras, opts.dotenvValues);
8542
8572
  await writeRuntimeEnv(opts.gitDir, runtimeEnv);
8543
8573
  let controlEnv = null;
8544
8574
  if (opts.controlUrl && opts.controlToken) {
@@ -8570,6 +8600,7 @@ async function createManagedWorktree(opts, deps = {}) {
8570
8600
  });
8571
8601
  worktreeCreated = true;
8572
8602
  const gitDir = git.resolveWorktreeGitDir(opts.worktreePath);
8603
+ const dotenvValues = await loadDotenvLocal(opts.worktreePath);
8573
8604
  const initialized = await initializeManagedWorktree({
8574
8605
  gitDir,
8575
8606
  branch: opts.branch,
@@ -8579,6 +8610,7 @@ async function createManagedWorktree(opts, deps = {}) {
8579
8610
  startupEnvValues: opts.startupEnvValues,
8580
8611
  allocatedPorts: opts.allocatedPorts,
8581
8612
  runtimeEnvExtras: opts.runtimeEnvExtras,
8613
+ dotenvValues,
8582
8614
  controlUrl: opts.controlUrl,
8583
8615
  controlToken: opts.controlToken,
8584
8616
  now: opts.now,
@@ -8838,6 +8870,7 @@ class LifecycleService {
8838
8870
  }
8839
8871
  async initializeUnmanagedWorktree(resolved) {
8840
8872
  const { profileName, profile } = this.resolveProfile(undefined);
8873
+ const dotenvValues = await loadDotenvLocal(resolved.entry.path);
8841
8874
  return initializeManagedWorktree({
8842
8875
  gitDir: resolved.gitDir,
8843
8876
  branch: resolved.entry.branch ?? resolved.entry.path,
@@ -8847,6 +8880,7 @@ class LifecycleService {
8847
8880
  startupEnvValues: await this.buildStartupEnvValues(undefined),
8848
8881
  allocatedPorts: await this.allocatePorts(),
8849
8882
  runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: resolved.entry.path },
8883
+ dotenvValues,
8850
8884
  controlUrl: this.controlUrl(),
8851
8885
  controlToken: await this.deps.getControlToken()
8852
8886
  });
@@ -8855,9 +8889,10 @@ class LifecycleService {
8855
8889
  if (!resolved.meta) {
8856
8890
  throw new Error("Missing managed metadata");
8857
8891
  }
8892
+ const dotenvValues = await loadDotenvLocal(resolved.entry.path);
8858
8893
  const runtimeEnv = buildRuntimeEnvMap(resolved.meta, {
8859
8894
  WEBMUX_WORKTREE_PATH: resolved.entry.path
8860
- });
8895
+ }, dotenvValues);
8861
8896
  await writeRuntimeEnv(resolved.gitDir, runtimeEnv);
8862
8897
  const controlEnv = buildControlEnvMap({
8863
8898
  controlUrl: this.controlUrl(),
@@ -9005,13 +9040,14 @@ class LifecycleService {
9005
9040
  return;
9006
9041
  }
9007
9042
  console.debug(`[lifecycle-hook] RUNNING ${input.name}: ${input.command} in ${input.worktreePath}`);
9043
+ const dotenvValues = await loadDotenvLocal(input.worktreePath);
9008
9044
  await this.deps.hooks.run({
9009
9045
  name: input.name,
9010
9046
  command: input.command,
9011
9047
  cwd: input.worktreePath,
9012
9048
  env: buildRuntimeEnvMap(input.meta, {
9013
9049
  WEBMUX_WORKTREE_PATH: input.worktreePath
9014
- })
9050
+ }, dotenvValues)
9015
9051
  });
9016
9052
  console.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
9017
9053
  }
@@ -9765,36 +9801,53 @@ class BunLifecycleHookRunner {
9765
9801
  // backend/src/adapters/port-probe.ts
9766
9802
  class BunPortProbe {
9767
9803
  timeoutMs;
9768
- hostname;
9769
- constructor(timeoutMs = 300, hostname = "127.0.0.1") {
9804
+ hostnames;
9805
+ constructor(timeoutMs = 300, hostnames = ["127.0.0.1", "::1"]) {
9770
9806
  this.timeoutMs = timeoutMs;
9771
- this.hostname = hostname;
9807
+ this.hostnames = hostnames;
9772
9808
  }
9773
9809
  isListening(port) {
9774
9810
  return new Promise((resolve4) => {
9775
9811
  let settled = false;
9812
+ let pending = this.hostnames.length;
9776
9813
  const settle = (result) => {
9777
9814
  if (settled)
9778
9815
  return;
9779
- settled = true;
9780
- clearTimeout(timer);
9781
- resolve4(result);
9816
+ if (result) {
9817
+ settled = true;
9818
+ clearTimeout(timer);
9819
+ resolve4(true);
9820
+ return;
9821
+ }
9822
+ pending--;
9823
+ if (pending === 0) {
9824
+ settled = true;
9825
+ clearTimeout(timer);
9826
+ resolve4(false);
9827
+ }
9782
9828
  };
9783
- const timer = setTimeout(() => settle(false), this.timeoutMs);
9784
- Bun.connect({
9785
- hostname: this.hostname,
9786
- port,
9787
- socket: {
9788
- open(socket) {
9789
- socket.end();
9790
- settle(true);
9791
- },
9792
- error() {
9793
- settle(false);
9794
- },
9795
- data() {}
9829
+ const timer = setTimeout(() => {
9830
+ if (!settled) {
9831
+ settled = true;
9832
+ resolve4(false);
9796
9833
  }
9797
- }).catch(() => settle(false));
9834
+ }, this.timeoutMs);
9835
+ for (const hostname of this.hostnames) {
9836
+ Bun.connect({
9837
+ hostname,
9838
+ port,
9839
+ socket: {
9840
+ open(socket) {
9841
+ socket.end();
9842
+ settle(true);
9843
+ },
9844
+ error() {
9845
+ settle(false);
9846
+ },
9847
+ data() {}
9848
+ }
9849
+ }).catch(() => settle(false));
9850
+ }
9798
9851
  });
9799
9852
  }
9800
9853
  }
package/bin/webmux.js CHANGED
@@ -1095,7 +1095,7 @@ function buildInitPromptSpec(context) {
1095
1095
  "Use this config shape:",
1096
1096
  "name: infer from the repository",
1097
1097
  "workspace.mainBranch: infer from git",
1098
- "workspace.worktreeRoot: keep __worktrees unless there is clear evidence of an existing alternative",
1098
+ "workspace.worktreeRoot: keep ../worktrees unless there is clear evidence of an existing alternative",
1099
1099
  "services: one entry per real dev service with name, portEnv, and portStart when a default port is clear",
1100
1100
  "profiles.default.runtime: host",
1101
1101
  "profiles.default.envPassthrough: []",
@@ -1441,7 +1441,7 @@ name: ${input.projectName}
1441
1441
 
1442
1442
  workspace:
1443
1443
  mainBranch: ${input.mainBranch}
1444
- worktreeRoot: __worktrees
1444
+ worktreeRoot: ../worktrees
1445
1445
  defaultAgent: ${defaultAgent}
1446
1446
 
1447
1447
  # Each service defines a port env var that webmux injects into pane and agent
@@ -2061,6 +2061,34 @@ function quoteEnvValue(value) {
2061
2061
  return value;
2062
2062
  return `'${value.replaceAll("'", "'\\''")}'`;
2063
2063
  }
2064
+ function parseDotenv(content) {
2065
+ const env = {};
2066
+ for (const line of content.split(`
2067
+ `)) {
2068
+ if (line.trimStart().startsWith("#"))
2069
+ continue;
2070
+ const match = DOTENV_LINE_RE.exec(line);
2071
+ if (!match)
2072
+ continue;
2073
+ const key = match[1];
2074
+ let value = match[2];
2075
+ if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
2076
+ value = value.slice(1, -1);
2077
+ } else {
2078
+ value = value.trimEnd();
2079
+ }
2080
+ env[key] = value;
2081
+ }
2082
+ return env;
2083
+ }
2084
+ async function loadDotenvLocal(worktreePath) {
2085
+ try {
2086
+ const content = await Bun.file(join5(worktreePath, ".env.local")).text();
2087
+ return parseDotenv(content);
2088
+ } catch {
2089
+ return {};
2090
+ }
2091
+ }
2064
2092
  function getWorktreeStoragePaths(gitDir) {
2065
2093
  const webmuxDir = join5(gitDir, "webmux");
2066
2094
  return {
@@ -2090,8 +2118,9 @@ async function writeWorktreeMeta(gitDir, meta) {
2090
2118
  await Bun.write(metaPath, JSON.stringify(meta, null, 2) + `
2091
2119
  `);
2092
2120
  }
2093
- function buildRuntimeEnvMap(meta, extraEnv = {}) {
2121
+ function buildRuntimeEnvMap(meta, extraEnv = {}, dotenvValues = {}) {
2094
2122
  return {
2123
+ ...dotenvValues,
2095
2124
  ...meta.startupEnvValues,
2096
2125
  ...stringifyAllocatedPorts(meta.allocatedPorts),
2097
2126
  ...extraEnv,
@@ -2151,9 +2180,10 @@ async function readWorktreePrs(gitDir) {
2151
2180
  return [];
2152
2181
  }
2153
2182
  }
2154
- var SAFE_ENV_VALUE_RE;
2183
+ var SAFE_ENV_VALUE_RE, DOTENV_LINE_RE;
2155
2184
  var init_fs = __esm(() => {
2156
2185
  SAFE_ENV_VALUE_RE = /^[A-Za-z0-9_./:@%+=,-]+$/;
2186
+ DOTENV_LINE_RE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)/;
2157
2187
  });
2158
2188
 
2159
2189
  // backend/src/adapters/tmux.ts
@@ -9465,7 +9495,7 @@ var init_config = __esm(() => {
9465
9495
  name: "Webmux",
9466
9496
  workspace: {
9467
9497
  mainBranch: "main",
9468
- worktreeRoot: "__worktrees",
9498
+ worktreeRoot: "../worktrees",
9469
9499
  defaultAgent: "claude"
9470
9500
  },
9471
9501
  profiles: {
@@ -10060,36 +10090,53 @@ var init_hooks = () => {};
10060
10090
  // backend/src/adapters/port-probe.ts
10061
10091
  class BunPortProbe {
10062
10092
  timeoutMs;
10063
- hostname;
10064
- constructor(timeoutMs = 300, hostname = "127.0.0.1") {
10093
+ hostnames;
10094
+ constructor(timeoutMs = 300, hostnames = ["127.0.0.1", "::1"]) {
10065
10095
  this.timeoutMs = timeoutMs;
10066
- this.hostname = hostname;
10096
+ this.hostnames = hostnames;
10067
10097
  }
10068
10098
  isListening(port) {
10069
10099
  return new Promise((resolve3) => {
10070
10100
  let settled = false;
10101
+ let pending = this.hostnames.length;
10071
10102
  const settle = (result) => {
10072
10103
  if (settled)
10073
10104
  return;
10074
- settled = true;
10075
- clearTimeout(timer);
10076
- resolve3(result);
10105
+ if (result) {
10106
+ settled = true;
10107
+ clearTimeout(timer);
10108
+ resolve3(true);
10109
+ return;
10110
+ }
10111
+ pending--;
10112
+ if (pending === 0) {
10113
+ settled = true;
10114
+ clearTimeout(timer);
10115
+ resolve3(false);
10116
+ }
10077
10117
  };
10078
- const timer = setTimeout(() => settle(false), this.timeoutMs);
10079
- Bun.connect({
10080
- hostname: this.hostname,
10081
- port,
10082
- socket: {
10083
- open(socket) {
10084
- socket.end();
10085
- settle(true);
10086
- },
10087
- error() {
10088
- settle(false);
10089
- },
10090
- data() {}
10091
- }
10092
- }).catch(() => settle(false));
10118
+ const timer = setTimeout(() => {
10119
+ if (!settled) {
10120
+ settled = true;
10121
+ resolve3(false);
10122
+ }
10123
+ }, this.timeoutMs);
10124
+ for (const hostname of this.hostnames) {
10125
+ Bun.connect({
10126
+ hostname,
10127
+ port,
10128
+ socket: {
10129
+ open(socket) {
10130
+ socket.end();
10131
+ settle(true);
10132
+ },
10133
+ error() {
10134
+ settle(false);
10135
+ },
10136
+ data() {}
10137
+ }
10138
+ }).catch(() => settle(false));
10139
+ }
10093
10140
  });
10094
10141
  }
10095
10142
  }
@@ -10853,7 +10900,7 @@ async function initializeManagedWorktree(opts) {
10853
10900
  };
10854
10901
  const paths = await ensureWorktreeStorageDirs(opts.gitDir);
10855
10902
  await writeWorktreeMeta(opts.gitDir, meta);
10856
- const runtimeEnv = buildRuntimeEnvMap(meta, opts.runtimeEnvExtras);
10903
+ const runtimeEnv = buildRuntimeEnvMap(meta, opts.runtimeEnvExtras, opts.dotenvValues);
10857
10904
  await writeRuntimeEnv(opts.gitDir, runtimeEnv);
10858
10905
  let controlEnv = null;
10859
10906
  if (opts.controlUrl && opts.controlToken) {
@@ -10885,6 +10932,7 @@ async function createManagedWorktree(opts, deps2 = {}) {
10885
10932
  });
10886
10933
  worktreeCreated = true;
10887
10934
  const gitDir = git.resolveWorktreeGitDir(opts.worktreePath);
10935
+ const dotenvValues = await loadDotenvLocal(opts.worktreePath);
10888
10936
  const initialized = await initializeManagedWorktree({
10889
10937
  gitDir,
10890
10938
  branch: opts.branch,
@@ -10894,6 +10942,7 @@ async function createManagedWorktree(opts, deps2 = {}) {
10894
10942
  startupEnvValues: opts.startupEnvValues,
10895
10943
  allocatedPorts: opts.allocatedPorts,
10896
10944
  runtimeEnvExtras: opts.runtimeEnvExtras,
10945
+ dotenvValues,
10897
10946
  controlUrl: opts.controlUrl,
10898
10947
  controlToken: opts.controlToken,
10899
10948
  now: opts.now,
@@ -11153,6 +11202,7 @@ class LifecycleService {
11153
11202
  }
11154
11203
  async initializeUnmanagedWorktree(resolved) {
11155
11204
  const { profileName, profile } = this.resolveProfile(undefined);
11205
+ const dotenvValues = await loadDotenvLocal(resolved.entry.path);
11156
11206
  return initializeManagedWorktree({
11157
11207
  gitDir: resolved.gitDir,
11158
11208
  branch: resolved.entry.branch ?? resolved.entry.path,
@@ -11162,6 +11212,7 @@ class LifecycleService {
11162
11212
  startupEnvValues: await this.buildStartupEnvValues(undefined),
11163
11213
  allocatedPorts: await this.allocatePorts(),
11164
11214
  runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: resolved.entry.path },
11215
+ dotenvValues,
11165
11216
  controlUrl: this.controlUrl(),
11166
11217
  controlToken: await this.deps.getControlToken()
11167
11218
  });
@@ -11170,9 +11221,10 @@ class LifecycleService {
11170
11221
  if (!resolved.meta) {
11171
11222
  throw new Error("Missing managed metadata");
11172
11223
  }
11224
+ const dotenvValues = await loadDotenvLocal(resolved.entry.path);
11173
11225
  const runtimeEnv = buildRuntimeEnvMap(resolved.meta, {
11174
11226
  WEBMUX_WORKTREE_PATH: resolved.entry.path
11175
- });
11227
+ }, dotenvValues);
11176
11228
  await writeRuntimeEnv(resolved.gitDir, runtimeEnv);
11177
11229
  const controlEnv = buildControlEnvMap({
11178
11230
  controlUrl: this.controlUrl(),
@@ -11320,13 +11372,14 @@ class LifecycleService {
11320
11372
  return;
11321
11373
  }
11322
11374
  console.debug(`[lifecycle-hook] RUNNING ${input.name}: ${input.command} in ${input.worktreePath}`);
11375
+ const dotenvValues = await loadDotenvLocal(input.worktreePath);
11323
11376
  await this.deps.hooks.run({
11324
11377
  name: input.name,
11325
11378
  command: input.command,
11326
11379
  cwd: input.worktreePath,
11327
11380
  env: buildRuntimeEnvMap(input.meta, {
11328
11381
  WEBMUX_WORKTREE_PATH: input.worktreePath
11329
- })
11382
+ }, dotenvValues)
11330
11383
  });
11331
11384
  console.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
11332
11385
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webmux",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Web dashboard for workmux — browser UI with embedded terminals, PR monitoring, and CI integration",
5
5
  "type": "module",
6
6
  "repository": {