webmux 0.33.0 → 0.34.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/bin/webmux.js CHANGED
@@ -2719,14 +2719,255 @@ ${result.stderr.trim()}` : ""
2719
2719
  console.log();
2720
2720
  });
2721
2721
 
2722
+ // backend/src/lib/log.ts
2723
+ function ts() {
2724
+ return new Date().toISOString().slice(11, 23);
2725
+ }
2726
+ var DEBUG, log;
2727
+ var init_log = __esm(() => {
2728
+ DEBUG = Bun.env.WEBMUX_DEBUG === "1";
2729
+ log = {
2730
+ info(msg) {
2731
+ console.log(`[${ts()}] ${msg}`);
2732
+ },
2733
+ debug(msg) {
2734
+ if (DEBUG)
2735
+ console.log(`[${ts()}] ${msg}`);
2736
+ },
2737
+ warn(msg) {
2738
+ console.warn(`[${ts()}] ${msg}`);
2739
+ },
2740
+ error(msg, err) {
2741
+ err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
2742
+ }
2743
+ };
2744
+ });
2745
+
2746
+ // backend/src/domain/policies.ts
2747
+ function sanitizeBranchName(raw) {
2748
+ return raw.toLowerCase().replace(/\s+/g, "-").replace(INVALID_BRANCH_CHARS_RE, "").replace(/@\{/g, "").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/-{2,}/g, "-").replace(/^[.\-/]+|[.\-/]+$/g, "").replace(/\.lock$/i, "");
2749
+ }
2750
+ function isValidBranchName(raw) {
2751
+ return raw.length > 0 && sanitizeBranchName(raw) === raw;
2752
+ }
2753
+ function isValidWorktreeName(name) {
2754
+ return name.length > 0 && VALID_WORKTREE_NAME_RE.test(name) && !name.includes("..");
2755
+ }
2756
+ function isValidEnvKey(key) {
2757
+ return UNSAFE_ENV_KEY_RE.test(key);
2758
+ }
2759
+ function isValidInstancePrefix(value) {
2760
+ return VALID_INSTANCE_PREFIX_RE.test(value) && !RESERVED_INSTANCE_PREFIXES.has(value);
2761
+ }
2762
+ function allocateServicePorts(existingMetas, services) {
2763
+ const allocatable = services.filter((service) => service.portStart != null);
2764
+ if (allocatable.length === 0)
2765
+ return {};
2766
+ const reference = allocatable[0];
2767
+ const referenceStart = reference.portStart;
2768
+ const referenceStep = reference.portStep ?? 1;
2769
+ const occupiedSlots = new Set;
2770
+ for (const meta of existingMetas) {
2771
+ const port = meta.allocatedPorts[reference.portEnv];
2772
+ if (!Number.isInteger(port) || port < referenceStart)
2773
+ continue;
2774
+ const diff = port - referenceStart;
2775
+ if (diff % referenceStep !== 0)
2776
+ continue;
2777
+ occupiedSlots.add(diff / referenceStep);
2778
+ }
2779
+ let slot = 1;
2780
+ while (occupiedSlots.has(slot))
2781
+ slot += 1;
2782
+ const result = {};
2783
+ for (const service of allocatable) {
2784
+ const start = service.portStart;
2785
+ const step = service.portStep ?? 1;
2786
+ result[service.portEnv] = start + slot * step;
2787
+ }
2788
+ return result;
2789
+ }
2790
+ var INVALID_BRANCH_CHARS_RE, UNSAFE_ENV_KEY_RE, VALID_WORKTREE_NAME_RE, VALID_INSTANCE_PREFIX_RE, RESERVED_INSTANCE_PREFIXES;
2791
+ var init_policies = __esm(() => {
2792
+ INVALID_BRANCH_CHARS_RE = /[~^:?*\[\]\\]+/g;
2793
+ UNSAFE_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
2794
+ VALID_WORKTREE_NAME_RE = /^[a-z0-9][a-z0-9\-_./]*$/;
2795
+ VALID_INSTANCE_PREFIX_RE = /^[a-z0-9][a-z0-9\-]*$/;
2796
+ RESERVED_INSTANCE_PREFIXES = new Set(["api", "ws", "assets"]);
2797
+ });
2798
+
2799
+ // backend/src/adapters/instance-registry.ts
2800
+ import { mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync } from "fs";
2801
+ import { homedir } from "os";
2802
+ import { join as join5 } from "path";
2803
+ function defaultRegistryDir() {
2804
+ return join5(homedir(), ".webmux", "instances");
2805
+ }
2806
+ function isAlive(pid) {
2807
+ try {
2808
+ process.kill(pid, 0);
2809
+ return true;
2810
+ } catch (err) {
2811
+ return err?.code !== "ESRCH";
2812
+ }
2813
+ }
2814
+ function isInstanceEntry(value) {
2815
+ if (typeof value !== "object" || value === null)
2816
+ return false;
2817
+ const v2 = value;
2818
+ return typeof v2.prefix === "string" && isValidInstancePrefix(v2.prefix) && typeof v2.port === "number" && typeof v2.projectDir === "string" && typeof v2.pid === "number" && typeof v2.startedAt === "number";
2819
+ }
2820
+ function createInstanceRegistry(dir = defaultRegistryDir()) {
2821
+ function ensureDir() {
2822
+ mkdirSync(dir, { recursive: true });
2823
+ }
2824
+ function entryPath(port) {
2825
+ return join5(dir, `${port}.json`);
2826
+ }
2827
+ function readEntry(filename) {
2828
+ try {
2829
+ const raw = readFileSync3(join5(dir, filename), "utf8");
2830
+ const parsed = JSON.parse(raw);
2831
+ return isInstanceEntry(parsed) ? parsed : null;
2832
+ } catch {
2833
+ return null;
2834
+ }
2835
+ }
2836
+ return {
2837
+ register(entry) {
2838
+ ensureDir();
2839
+ const finalPath = entryPath(entry.port);
2840
+ const tmpPath = `${finalPath}.${process.pid}.${Date.now()}.tmp`;
2841
+ const text = `${JSON.stringify(entry, null, 2)}
2842
+ `;
2843
+ writeFileSync(tmpPath, text);
2844
+ renameSync(tmpPath, finalPath);
2845
+ },
2846
+ deregister(port, expectedPid) {
2847
+ if (expectedPid !== undefined) {
2848
+ const filename = `${port}.json`;
2849
+ const entry = readEntry(filename);
2850
+ if (entry && entry.pid !== expectedPid) {
2851
+ return;
2852
+ }
2853
+ }
2854
+ try {
2855
+ unlinkSync(entryPath(port));
2856
+ } catch (err) {
2857
+ const code = err?.code;
2858
+ if (code !== "ENOENT") {
2859
+ log.debug(`[instance-registry] deregister(${port}) failed: ${String(err)}`);
2860
+ }
2861
+ }
2862
+ },
2863
+ listLive() {
2864
+ let filenames;
2865
+ try {
2866
+ filenames = readdirSync2(dir).filter((name) => name.endsWith(".json"));
2867
+ } catch {
2868
+ return [];
2869
+ }
2870
+ const live = [];
2871
+ for (const filename of filenames) {
2872
+ const entry = readEntry(filename);
2873
+ if (!entry)
2874
+ continue;
2875
+ if (!isAlive(entry.pid)) {
2876
+ try {
2877
+ unlinkSync(join5(dir, filename));
2878
+ } catch {}
2879
+ continue;
2880
+ }
2881
+ live.push(entry);
2882
+ }
2883
+ return live;
2884
+ }
2885
+ };
2886
+ }
2887
+ var init_instance_registry = __esm(() => {
2888
+ init_log();
2889
+ init_policies();
2890
+ });
2891
+
2892
+ // bin/src/install-ports.ts
2893
+ import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4 } from "fs";
2894
+ import { homedir as homedir2 } from "os";
2895
+ import { join as join6 } from "path";
2896
+ function pickFreePort(start, taken) {
2897
+ const set = new Set(taken);
2898
+ let port = start;
2899
+ while (set.has(port))
2900
+ port += 1;
2901
+ return port;
2902
+ }
2903
+ function readInstalledServicePorts(opts = {}) {
2904
+ const systemdDir = opts.systemdDir ?? DEFAULT_SYSTEMD_DIR;
2905
+ const launchdDir = opts.launchdDir ?? DEFAULT_LAUNCHD_DIR;
2906
+ const ports = [];
2907
+ function collect(dir, namePredicate) {
2908
+ if (!existsSync4(dir))
2909
+ return;
2910
+ let names;
2911
+ try {
2912
+ names = readdirSync3(dir);
2913
+ } catch {
2914
+ return;
2915
+ }
2916
+ for (const name of names) {
2917
+ if (!namePredicate(name))
2918
+ continue;
2919
+ const full = join6(dir, name);
2920
+ if (opts.excludePath && full === opts.excludePath)
2921
+ continue;
2922
+ const port = readPortFromUnit(full);
2923
+ if (port !== null)
2924
+ ports.push(port);
2925
+ }
2926
+ }
2927
+ collect(systemdDir, (n) => n.startsWith("webmux-") && n.endsWith(".service"));
2928
+ collect(launchdDir, (n) => n.startsWith("com.webmux.") && n.endsWith(".plist"));
2929
+ return ports;
2930
+ }
2931
+ function readPortFromUnit(filePath) {
2932
+ let text;
2933
+ try {
2934
+ text = readFileSync4(filePath, "utf8");
2935
+ } catch {
2936
+ return null;
2937
+ }
2938
+ const regex = filePath.endsWith(".plist") ? LAUNCHD_PORT_RE : SYSTEMD_PORT_RE;
2939
+ const match = regex.exec(text);
2940
+ return match ? parseInt(match[1], 10) : null;
2941
+ }
2942
+ function discoverTakenPorts(opts = {}) {
2943
+ const registry = createInstanceRegistry(opts.registryDir);
2944
+ const live = registry.listLive().map((entry) => entry.port);
2945
+ const installed = readInstalledServicePorts({
2946
+ systemdDir: opts.systemdDir,
2947
+ launchdDir: opts.launchdDir,
2948
+ excludePath: opts.excludeUnitPath
2949
+ });
2950
+ return new Set([...live, ...installed]);
2951
+ }
2952
+ var DEFAULT_SYSTEMD_DIR, DEFAULT_LAUNCHD_DIR, SYSTEMD_PORT_RE, LAUNCHD_PORT_RE;
2953
+ var init_install_ports = __esm(() => {
2954
+ init_instance_registry();
2955
+ DEFAULT_SYSTEMD_DIR = join6(homedir2(), ".config", "systemd", "user");
2956
+ DEFAULT_LAUNCHD_DIR = join6(homedir2(), "Library", "LaunchAgents");
2957
+ SYSTEMD_PORT_RE = /--port\s+(\d+)/;
2958
+ LAUNCHD_PORT_RE = /<string>--port<\/string>\s*<string>(\d+)<\/string>/;
2959
+ });
2960
+
2722
2961
  // bin/src/service.ts
2723
2962
  var exports_service = {};
2724
2963
  __export(exports_service, {
2964
+ parseInstalledServiceConfig: () => parseInstalledServiceConfig,
2965
+ generateServiceFile: () => generateServiceFile,
2725
2966
  default: () => service
2726
2967
  });
2727
- import { existsSync as existsSync4, mkdirSync, unlinkSync } from "fs";
2728
- import { join as join5 } from "path";
2729
- import { homedir } from "os";
2968
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2 } from "fs";
2969
+ import { basename as basename3, join as join7 } from "path";
2970
+ import { homedir as homedir3 } from "os";
2730
2971
  function getPlatform() {
2731
2972
  const plat = process.platform;
2732
2973
  if (plat === "linux" || plat === "darwin")
@@ -2755,10 +2996,10 @@ function printRunResult(result) {
2755
2996
  console.error(err);
2756
2997
  }
2757
2998
  function systemdUnitPath(serviceName) {
2758
- return join5(homedir(), ".config", "systemd", "user", `${serviceName}.service`);
2999
+ return join7(homedir3(), ".config", "systemd", "user", `${serviceName}.service`);
2759
3000
  }
2760
3001
  function launchdPlistPath(serviceName) {
2761
- return join5(homedir(), "Library", "LaunchAgents", `com.webmux.${serviceName}.plist`);
3002
+ return join7(homedir3(), "Library", "LaunchAgents", `com.webmux.${serviceName}.plist`);
2762
3003
  }
2763
3004
  function serviceFilePath(config) {
2764
3005
  if (config.platform === "linux")
@@ -2784,7 +3025,7 @@ WantedBy=default.target
2784
3025
  `;
2785
3026
  }
2786
3027
  function generateLaunchdPlist(config) {
2787
- const logPath = join5(homedir(), "Library", "Logs", `webmux-${config.serviceName}.log`);
3028
+ const logPath = join7(homedir3(), "Library", "Logs", `webmux-${config.serviceName}.log`);
2788
3029
  return `<?xml version="1.0" encoding="UTF-8"?>
2789
3030
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2790
3031
  <plist version="1.0">
@@ -2829,6 +3070,36 @@ function generateServiceFile(config) {
2829
3070
  return generateSystemdUnit(config);
2830
3071
  return generateLaunchdPlist(config);
2831
3072
  }
3073
+ function readWorkingDirFromUnit(filePath, platform) {
3074
+ let text;
3075
+ try {
3076
+ text = readFileSync5(filePath, "utf8");
3077
+ } catch {
3078
+ return null;
3079
+ }
3080
+ const regex = platform === "linux" ? SYSTEMD_WORKDIR_RE : LAUNCHD_WORKDIR_RE;
3081
+ const match = regex.exec(text);
3082
+ return match ? match[1].trim() : null;
3083
+ }
3084
+ function parseInstalledServiceConfig(filePath, platform, webmuxPath) {
3085
+ const port = readPortFromUnit(filePath);
3086
+ if (port === null)
3087
+ return null;
3088
+ const projectDir = readWorkingDirFromUnit(filePath, platform);
3089
+ if (projectDir === null)
3090
+ return null;
3091
+ const fileBase = basename3(filePath);
3092
+ const serviceName = platform === "linux" ? fileBase.replace(/\.service$/, "") : fileBase.replace(/^com\.webmux\./, "").replace(/\.plist$/, "");
3093
+ const projectName = detectProjectName(projectDir);
3094
+ return {
3095
+ platform,
3096
+ projectName,
3097
+ serviceName,
3098
+ webmuxPath,
3099
+ projectDir,
3100
+ port
3101
+ };
3102
+ }
2832
3103
  function installCommands(config) {
2833
3104
  if (config.platform === "linux") {
2834
3105
  return [
@@ -2852,11 +3123,12 @@ function uninstallCommands(config) {
2852
3123
  ];
2853
3124
  }
2854
3125
  function isInstalled(config) {
2855
- return existsSync4(serviceFilePath(config));
3126
+ return existsSync5(serviceFilePath(config));
2856
3127
  }
2857
- async function install(config) {
3128
+ async function install(config, portExplicit) {
2858
3129
  const filePath = serviceFilePath(config);
2859
- if (isInstalled(config)) {
3130
+ const alreadyInstalled = isInstalled(config);
3131
+ if (alreadyInstalled) {
2860
3132
  const reinstall = await ue({ message: "Service is already installed. Reinstall?" });
2861
3133
  if (q(reinstall) || !reinstall) {
2862
3134
  R2.info("Aborted.");
@@ -2866,6 +3138,31 @@ async function install(config) {
2866
3138
  runCommand(cmd);
2867
3139
  }
2868
3140
  }
3141
+ const requestedPort = config.port;
3142
+ let chosenPort = requestedPort;
3143
+ let portNote = null;
3144
+ let portWarning = null;
3145
+ if (!portExplicit) {
3146
+ const existingPort = alreadyInstalled ? readPortFromUnit(filePath) : null;
3147
+ if (existingPort !== null) {
3148
+ chosenPort = existingPort;
3149
+ if (existingPort !== requestedPort) {
3150
+ portNote = `Reusing port ${existingPort} from the existing service unit (pass --port to override).`;
3151
+ }
3152
+ } else {
3153
+ const taken = discoverTakenPorts({ excludeUnitPath: filePath });
3154
+ chosenPort = pickFreePort(requestedPort, taken);
3155
+ if (chosenPort !== requestedPort) {
3156
+ portNote = `Port ${requestedPort} is already used by another webmux instance \u2014 picked ${chosenPort} instead (pass --port to override).`;
3157
+ }
3158
+ }
3159
+ } else {
3160
+ const taken = discoverTakenPorts({ excludeUnitPath: filePath });
3161
+ if (taken.has(requestedPort)) {
3162
+ portWarning = `Port ${requestedPort} is already claimed by another webmux instance. The service will fail to bind on start; omit --port to auto-pick a free port.`;
3163
+ }
3164
+ }
3165
+ config = { ...config, port: chosenPort };
2869
3166
  const content = generateServiceFile(config);
2870
3167
  const commands = installCommands(config);
2871
3168
  Se([
@@ -2877,12 +3174,16 @@ async function install(config) {
2877
3174
  ...commands.map((c) => ` $ ${formatCommand(c)}`)
2878
3175
  ].join(`
2879
3176
  `), "Install service");
3177
+ if (portNote)
3178
+ R2.info(portNote);
3179
+ if (portWarning)
3180
+ R2.warn(portWarning);
2880
3181
  const ok = await ue({ message: "Proceed?" });
2881
3182
  if (q(ok) || !ok) {
2882
3183
  R2.info("Aborted.");
2883
3184
  return;
2884
3185
  }
2885
- mkdirSync(filePath.substring(0, filePath.lastIndexOf("/")), { recursive: true });
3186
+ mkdirSync2(filePath.substring(0, filePath.lastIndexOf("/")), { recursive: true });
2886
3187
  await Bun.write(filePath, content);
2887
3188
  R2.success(`Wrote ${filePath}`);
2888
3189
  for (const cmd of commands) {
@@ -2932,7 +3233,7 @@ ${result.stderr.toString()}`);
2932
3233
  R2.success(`$ ${formatCommand(cmd)}`);
2933
3234
  }
2934
3235
  }
2935
- unlinkSync(filePath);
3236
+ unlinkSync2(filePath);
2936
3237
  R2.success(`Removed ${filePath}`);
2937
3238
  R2.success("Service uninstalled.");
2938
3239
  }
@@ -2956,8 +3257,8 @@ function logs(config) {
2956
3257
  if (config.platform === "linux") {
2957
3258
  proc = Bun.spawn(["journalctl", "--user", "-u", config.serviceName, "-f", "--no-pager"], { stdout: "inherit", stderr: "inherit" });
2958
3259
  } else {
2959
- const logPath = join5(homedir(), "Library", "Logs", `webmux-${config.serviceName}.log`);
2960
- if (!existsSync4(logPath)) {
3260
+ const logPath = join7(homedir3(), "Library", "Logs", `webmux-${config.serviceName}.log`);
3261
+ if (!existsSync5(logPath)) {
2961
3262
  R2.error(`Log file not found: ${logPath}`);
2962
3263
  return;
2963
3264
  }
@@ -2978,6 +3279,12 @@ Usage:
2978
3279
  webmux service uninstall Stop, disable, and remove the service
2979
3280
  webmux service status Show service status
2980
3281
  webmux service logs Tail service logs
3282
+
3283
+ Options:
3284
+ --port N Pin the service to a specific port. When omitted,
3285
+ a free port is picked automatically by scanning
3286
+ other webmux instances and installed services
3287
+ \u2014 second-project installs no longer collide on 5111.
2981
3288
  `);
2982
3289
  }
2983
3290
  async function service(args) {
@@ -3013,6 +3320,7 @@ async function service(args) {
3013
3320
  return;
3014
3321
  }
3015
3322
  let port = parseInt(process.env.PORT || "5111");
3323
+ let portExplicit = false;
3016
3324
  for (let i = 1;i < args.length; i++) {
3017
3325
  if (args[i] === "--port" && args[i + 1]) {
3018
3326
  const parsed = parseInt(args[++i]);
@@ -3021,6 +3329,7 @@ async function service(args) {
3021
3329
  return;
3022
3330
  }
3023
3331
  port = parsed;
3332
+ portExplicit = true;
3024
3333
  }
3025
3334
  }
3026
3335
  const projectName = detectProjectName(gitRoot2);
@@ -3035,7 +3344,7 @@ async function service(args) {
3035
3344
  };
3036
3345
  switch (action) {
3037
3346
  case "install":
3038
- await install(config);
3347
+ await install(config, portExplicit);
3039
3348
  break;
3040
3349
  case "uninstall":
3041
3350
  await uninstall(config);
@@ -3048,9 +3357,141 @@ async function service(args) {
3048
3357
  break;
3049
3358
  }
3050
3359
  }
3360
+ var SYSTEMD_WORKDIR_RE, LAUNCHD_WORKDIR_RE;
3051
3361
  var init_service = __esm(() => {
3052
3362
  init_dist4();
3053
3363
  init_shared();
3364
+ init_install_ports();
3365
+ SYSTEMD_WORKDIR_RE = /^WorkingDirectory=(.+)$/m;
3366
+ LAUNCHD_WORKDIR_RE = /<key>WorkingDirectory<\/key>\s*<string>([^<]+)<\/string>/;
3367
+ });
3368
+
3369
+ // bin/src/service-restart.ts
3370
+ var exports_service_restart = {};
3371
+ __export(exports_service_restart, {
3372
+ updateInstalledService: () => updateInstalledService,
3373
+ restartInstalledService: () => restartInstalledService,
3374
+ restartCommand: () => restartCommand,
3375
+ listInstalledServices: () => listInstalledServices
3376
+ });
3377
+ import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
3378
+ import { homedir as homedir4 } from "os";
3379
+ import { join as join8 } from "path";
3380
+ function listInstalledServices(opts = {}) {
3381
+ const out = [];
3382
+ const systemdDir = opts.systemdDir ?? DEFAULT_SYSTEMD_DIR2;
3383
+ const launchdDir = opts.launchdDir ?? DEFAULT_LAUNCHD_DIR2;
3384
+ if (existsSync6(systemdDir)) {
3385
+ try {
3386
+ for (const name of readdirSync4(systemdDir)) {
3387
+ if (!name.startsWith("webmux-") || !name.endsWith(".service"))
3388
+ continue;
3389
+ out.push({
3390
+ name: name.slice(0, -".service".length),
3391
+ filePath: join8(systemdDir, name),
3392
+ platform: "linux"
3393
+ });
3394
+ }
3395
+ } catch {}
3396
+ }
3397
+ if (existsSync6(launchdDir)) {
3398
+ try {
3399
+ for (const name of readdirSync4(launchdDir)) {
3400
+ if (!name.startsWith("com.webmux.") || !name.endsWith(".plist"))
3401
+ continue;
3402
+ out.push({
3403
+ name: name.slice(0, -".plist".length),
3404
+ filePath: join8(launchdDir, name),
3405
+ platform: "darwin"
3406
+ });
3407
+ }
3408
+ } catch {}
3409
+ }
3410
+ return out;
3411
+ }
3412
+ function restartCommand(service2, uid) {
3413
+ if (service2.platform === "linux") {
3414
+ return { bin: "systemctl", args: ["--user", "restart", service2.name] };
3415
+ }
3416
+ return { bin: "launchctl", args: ["kickstart", "-k", `gui/${uid}/${service2.name}`] };
3417
+ }
3418
+ function restartInstalledService(service2, runner = defaultRunner) {
3419
+ const uid = typeof process.getuid === "function" ? process.getuid() : 0;
3420
+ const { bin, args } = restartCommand(service2, uid);
3421
+ const result = runner.run(bin, args);
3422
+ if (!result.success) {
3423
+ return {
3424
+ service: service2,
3425
+ ok: false,
3426
+ error: result.stderr.toString().trim() || `${bin} ${args.join(" ")} failed`
3427
+ };
3428
+ }
3429
+ return { service: service2, ok: true };
3430
+ }
3431
+ function reloadAfterRegenerate(service2, runner) {
3432
+ if (service2.platform === "linux") {
3433
+ const result = runner.run("systemctl", ["--user", "daemon-reload"]);
3434
+ return result.success ? { ok: true } : { ok: false, error: result.stderr.toString().trim() || "daemon-reload failed" };
3435
+ }
3436
+ runner.run("launchctl", ["unload", service2.filePath]);
3437
+ const loadResult = runner.run("launchctl", ["load", "-w", service2.filePath]);
3438
+ if (loadResult.success)
3439
+ return { ok: true };
3440
+ const stderr = loadResult.stderr.toString().trim() || "load failed";
3441
+ return {
3442
+ ok: false,
3443
+ error: `${stderr}
3444
+ service is now unloaded \u2014 recover with: launchctl load -w "${service2.filePath}"`
3445
+ };
3446
+ }
3447
+ async function updateInstalledService(service2, webmuxPath, runner = defaultRunner) {
3448
+ const canRegenerate = webmuxPath.length > 0;
3449
+ const config = canRegenerate ? parseInstalledServiceConfig(service2.filePath, service2.platform, webmuxPath) : null;
3450
+ let regenerated = false;
3451
+ if (config !== null) {
3452
+ let currentContent = "";
3453
+ try {
3454
+ currentContent = readFileSync6(service2.filePath, "utf8");
3455
+ } catch {}
3456
+ const expected = generateServiceFile(config);
3457
+ if (currentContent !== expected) {
3458
+ try {
3459
+ await Bun.write(service2.filePath, expected);
3460
+ regenerated = true;
3461
+ } catch (err) {
3462
+ return {
3463
+ service: service2,
3464
+ regenerated: false,
3465
+ restarted: false,
3466
+ error: `could not rewrite ${service2.filePath}: ${String(err)}`
3467
+ };
3468
+ }
3469
+ }
3470
+ }
3471
+ if (regenerated) {
3472
+ const reload = reloadAfterRegenerate(service2, runner);
3473
+ if (!reload.ok) {
3474
+ return { service: service2, regenerated, restarted: false, error: reload.error };
3475
+ }
3476
+ if (service2.platform === "darwin") {
3477
+ return { service: service2, regenerated, restarted: true };
3478
+ }
3479
+ }
3480
+ const outcome = restartInstalledService(service2, runner);
3481
+ return {
3482
+ service: service2,
3483
+ regenerated,
3484
+ restarted: outcome.ok,
3485
+ error: outcome.error
3486
+ };
3487
+ }
3488
+ var defaultRunner, DEFAULT_SYSTEMD_DIR2, DEFAULT_LAUNCHD_DIR2;
3489
+ var init_service_restart = __esm(() => {
3490
+ init_shared();
3491
+ init_service();
3492
+ defaultRunner = { run };
3493
+ DEFAULT_SYSTEMD_DIR2 = join8(homedir4(), ".config", "systemd", "user");
3494
+ DEFAULT_LAUNCHD_DIR2 = join8(homedir4(), "Library", "LaunchAgents");
3054
3495
  });
3055
3496
 
3056
3497
  // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
@@ -8221,30 +8662,6 @@ var init_src = __esm(() => {
8221
8662
  init_schemas();
8222
8663
  });
8223
8664
 
8224
- // backend/src/lib/log.ts
8225
- function ts() {
8226
- return new Date().toISOString().slice(11, 23);
8227
- }
8228
- var DEBUG, log;
8229
- var init_log = __esm(() => {
8230
- DEBUG = Bun.env.WEBMUX_DEBUG === "1";
8231
- log = {
8232
- info(msg) {
8233
- console.log(`[${ts()}] ${msg}`);
8234
- },
8235
- debug(msg) {
8236
- if (DEBUG)
8237
- console.log(`[${ts()}] ${msg}`);
8238
- },
8239
- warn(msg) {
8240
- console.warn(`[${ts()}] ${msg}`);
8241
- },
8242
- error(msg, err) {
8243
- err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
8244
- }
8245
- };
8246
- });
8247
-
8248
8665
  // backend/src/services/linear-service.ts
8249
8666
  function gqlErrorMessage(raw) {
8250
8667
  return raw.errors && raw.errors.length > 0 ? raw.errors.map((error) => error.message).join("; ") : null;
@@ -9562,7 +9979,7 @@ var WORKTREE_META_SCHEMA_VERSION = 1, WORKTREE_ARCHIVE_STATE_VERSION = 1;
9562
9979
 
9563
9980
  // backend/src/adapters/fs.ts
9564
9981
  import { mkdir } from "fs/promises";
9565
- import { join as join6 } from "path";
9982
+ import { join as join9 } from "path";
9566
9983
  function stringifyAllocatedPorts(ports) {
9567
9984
  const entries = Object.entries(ports).map(([key, value]) => [key, String(value)]);
9568
9985
  return Object.fromEntries(entries);
@@ -9594,25 +10011,25 @@ function parseDotenv(content) {
9594
10011
  }
9595
10012
  async function loadDotenvLocal(worktreePath) {
9596
10013
  try {
9597
- const content = await Bun.file(join6(worktreePath, ".env.local")).text();
10014
+ const content = await Bun.file(join9(worktreePath, ".env.local")).text();
9598
10015
  return parseDotenv(content);
9599
10016
  } catch {
9600
10017
  return {};
9601
10018
  }
9602
10019
  }
9603
10020
  function getWorktreeStoragePaths(gitDir) {
9604
- const webmuxDir = join6(gitDir, "webmux");
10021
+ const webmuxDir = join9(gitDir, "webmux");
9605
10022
  return {
9606
10023
  gitDir,
9607
10024
  webmuxDir,
9608
- metaPath: join6(webmuxDir, "meta.json"),
9609
- runtimeEnvPath: join6(webmuxDir, "runtime.env"),
9610
- controlEnvPath: join6(webmuxDir, "control.env"),
9611
- prsPath: join6(webmuxDir, "prs.json")
10025
+ metaPath: join9(webmuxDir, "meta.json"),
10026
+ runtimeEnvPath: join9(webmuxDir, "runtime.env"),
10027
+ controlEnvPath: join9(webmuxDir, "control.env"),
10028
+ prsPath: join9(webmuxDir, "prs.json")
9612
10029
  };
9613
10030
  }
9614
10031
  function getProjectArchiveStatePath(gitDir) {
9615
- return join6(gitDir, "webmux", "archive.json");
10032
+ return join9(gitDir, "webmux", "archive.json");
9616
10033
  }
9617
10034
  async function ensureWorktreeStorageDirs(gitDir) {
9618
10035
  const paths = getWorktreeStoragePaths(gitDir);
@@ -9781,7 +10198,7 @@ var init_fs = __esm(() => {
9781
10198
 
9782
10199
  // backend/src/adapters/tmux.ts
9783
10200
  import { createHash } from "crypto";
9784
- import { basename as basename3, resolve as resolve3 } from "path";
10201
+ import { basename as basename4, resolve as resolve3 } from "path";
9785
10202
  function runTmux(args) {
9786
10203
  const result = Bun.spawnSync(["tmux", ...args], {
9787
10204
  stdout: "pipe",
@@ -9810,7 +10227,7 @@ function sanitizeTmuxNameSegment(value, maxLength = 24) {
9810
10227
  }
9811
10228
  function buildProjectSessionName(projectRoot) {
9812
10229
  const resolved = resolve3(projectRoot);
9813
- const base = sanitizeTmuxNameSegment(basename3(resolved), 18);
10230
+ const base = sanitizeTmuxNameSegment(basename4(resolved), 18);
9814
10231
  const hash = createHash("sha1").update(resolved).digest("hex").slice(0, 8);
9815
10232
  return `wm-${base}-${hash}`;
9816
10233
  }
@@ -9885,55 +10302,6 @@ class BunTmuxGateway {
9885
10302
  }
9886
10303
  var init_tmux = () => {};
9887
10304
 
9888
- // backend/src/domain/policies.ts
9889
- function sanitizeBranchName(raw) {
9890
- return raw.toLowerCase().replace(/\s+/g, "-").replace(INVALID_BRANCH_CHARS_RE, "").replace(/@\{/g, "").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/-{2,}/g, "-").replace(/^[.\-/]+|[.\-/]+$/g, "").replace(/\.lock$/i, "");
9891
- }
9892
- function isValidBranchName(raw) {
9893
- return raw.length > 0 && sanitizeBranchName(raw) === raw;
9894
- }
9895
- function isValidWorktreeName(name) {
9896
- return name.length > 0 && VALID_WORKTREE_NAME_RE.test(name) && !name.includes("..");
9897
- }
9898
- function isValidEnvKey(key) {
9899
- return UNSAFE_ENV_KEY_RE.test(key);
9900
- }
9901
- function allocateServicePorts(existingMetas, services) {
9902
- const allocatable = services.filter((service2) => service2.portStart != null);
9903
- if (allocatable.length === 0)
9904
- return {};
9905
- const reference = allocatable[0];
9906
- const referenceStart = reference.portStart;
9907
- const referenceStep = reference.portStep ?? 1;
9908
- const occupiedSlots = new Set;
9909
- for (const meta of existingMetas) {
9910
- const port = meta.allocatedPorts[reference.portEnv];
9911
- if (!Number.isInteger(port) || port < referenceStart)
9912
- continue;
9913
- const diff = port - referenceStart;
9914
- if (diff % referenceStep !== 0)
9915
- continue;
9916
- occupiedSlots.add(diff / referenceStep);
9917
- }
9918
- let slot = 1;
9919
- while (occupiedSlots.has(slot))
9920
- slot += 1;
9921
- const result = {};
9922
- for (const service2 of allocatable) {
9923
- const start = service2.portStart;
9924
- const step = service2.portStep ?? 1;
9925
- result[service2.portEnv] = start + slot * step;
9926
- }
9927
- return result;
9928
- }
9929
- var INVALID_BRANCH_CHARS_RE, UNSAFE_ENV_KEY_RE, VALID_WORKTREE_NAME_RE, RESERVED_INSTANCE_PREFIXES;
9930
- var init_policies = __esm(() => {
9931
- INVALID_BRANCH_CHARS_RE = /[~^:?*\[\]\\]+/g;
9932
- UNSAFE_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
9933
- VALID_WORKTREE_NAME_RE = /^[a-z0-9][a-z0-9\-_./]*$/;
9934
- RESERVED_INSTANCE_PREFIXES = new Set(["api", "ws", "assets"]);
9935
- });
9936
-
9937
10305
  // backend/src/services/archive-service.ts
9938
10306
  import { resolve as resolve4 } from "path";
9939
10307
  function createArchiveState(entries) {
@@ -16957,8 +17325,8 @@ var init_dist5 = __esm(() => {
16957
17325
  });
16958
17326
 
16959
17327
  // backend/src/adapters/config.ts
16960
- import { readFileSync as readFileSync3 } from "fs";
16961
- import { dirname as dirname2, join as join7, resolve as resolve5 } from "path";
17328
+ import { readFileSync as readFileSync7 } from "fs";
17329
+ import { dirname as dirname2, join as join10, resolve as resolve5 } from "path";
16962
17330
  function DEFAULT_ONESHOT_SYSTEM_PROMPT() {
16963
17331
  return [
16964
17332
  "You are running in webmux ONESHOT mode. There is NO interactive user \u2014 nobody is watching the chat or will respond to questions, approvals, or status checks. Any message asking the user to review, approve, confirm, take a look, or 'let you know' is wasted output: it will not be answered.",
@@ -17187,10 +17555,10 @@ function getDefaultProfileName(config) {
17187
17555
  return Object.keys(config.profiles)[0] ?? "default";
17188
17556
  }
17189
17557
  function readConfigFile(root) {
17190
- return readFileSync3(join7(root, ".webmux.yaml"), "utf8");
17558
+ return readFileSync7(join10(root, ".webmux.yaml"), "utf8");
17191
17559
  }
17192
17560
  function readLocalConfigFile(root) {
17193
- return readFileSync3(join7(root, ".webmux.local.yaml"), "utf8");
17561
+ return readFileSync7(join10(root, ".webmux.local.yaml"), "utf8");
17194
17562
  }
17195
17563
  function parseConfigDocument(text) {
17196
17564
  const parsed = $parse(text);
@@ -17695,7 +18063,7 @@ var init_docker = __esm(() => {
17695
18063
  });
17696
18064
 
17697
18065
  // backend/src/adapters/hooks.ts
17698
- import { join as join8 } from "path";
18066
+ import { join as join11 } from "path";
17699
18067
  function buildErrorMessage(name, exitCode, stdout, stderr) {
17700
18068
  const output = stderr.trim() || stdout.trim();
17701
18069
  if (output) {
@@ -17720,7 +18088,7 @@ class BunLifecycleHookRunner {
17720
18088
  return this.direnvAvailable;
17721
18089
  }
17722
18090
  async buildCommand(cwd, command) {
17723
- if (this.checkDirenv() && await Bun.file(join8(cwd, ".envrc")).exists()) {
18091
+ if (this.checkDirenv() && await Bun.file(join11(cwd, ".envrc")).exists()) {
17724
18092
  Bun.spawnSync(["direnv", "allow"], { cwd, stdout: "pipe", stderr: "pipe" });
17725
18093
  return ["direnv", "exec", cwd, "bash", "-c", command];
17726
18094
  }
@@ -18048,7 +18416,7 @@ var init_archive_state_service = __esm(() => {
18048
18416
 
18049
18417
  // backend/src/adapters/agent-runtime.ts
18050
18418
  import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
18051
- import { dirname as dirname4, join as join9, resolve as resolve6 } from "path";
18419
+ import { dirname as dirname4, join as join12, resolve as resolve6 } from "path";
18052
18420
  function shellQuote(value) {
18053
18421
  return `'${value.replaceAll("'", "'\\''")}'`;
18054
18422
  }
@@ -18467,7 +18835,7 @@ async function mergeCodexHooksFile(hooksPath, hookSettings, agentCtlPath) {
18467
18835
  }
18468
18836
  async function resolveGitCommonDir(gitDir) {
18469
18837
  try {
18470
- const commonDir = (await Bun.file(join9(gitDir, "commondir")).text()).trim();
18838
+ const commonDir = (await Bun.file(join12(gitDir, "commondir")).text()).trim();
18471
18839
  if (!commonDir)
18472
18840
  return gitDir;
18473
18841
  return commonDir.startsWith("/") ? commonDir : resolve6(gitDir, commonDir);
@@ -18477,7 +18845,7 @@ async function resolveGitCommonDir(gitDir) {
18477
18845
  }
18478
18846
  async function ensureGeneratedCodexHooksIgnored(gitDir) {
18479
18847
  const commonDir = await resolveGitCommonDir(gitDir);
18480
- const excludePath = join9(commonDir, "info", "exclude");
18848
+ const excludePath = join12(commonDir, "info", "exclude");
18481
18849
  let existing = "";
18482
18850
  try {
18483
18851
  existing = await Bun.file(excludePath).text();
@@ -18497,9 +18865,9 @@ async function ensureGeneratedCodexHooksIgnored(gitDir) {
18497
18865
  async function ensureAgentRuntimeArtifacts(input) {
18498
18866
  const storagePaths = getWorktreeStoragePaths(input.gitDir);
18499
18867
  const artifacts = {
18500
- agentCtlPath: join9(storagePaths.webmuxDir, "webmux-agentctl"),
18501
- claudeSettingsPath: join9(input.worktreePath, ".claude", "settings.local.json"),
18502
- codexHooksPath: join9(input.worktreePath, ".codex", "hooks.json")
18868
+ agentCtlPath: join12(storagePaths.webmuxDir, "webmux-agentctl"),
18869
+ claudeSettingsPath: join12(input.worktreePath, ".claude", "settings.local.json"),
18870
+ codexHooksPath: join12(input.worktreePath, ".codex", "hooks.json")
18503
18871
  };
18504
18872
  await mkdir3(dirname4(artifacts.claudeSettingsPath), { recursive: true });
18505
18873
  await mkdir3(dirname4(artifacts.codexHooksPath), { recursive: true });
@@ -18533,7 +18901,7 @@ function buildDockerRuntimeBootstrap(runtimeEnvPath) {
18533
18901
  function buildBuiltInAgentInvocation(input) {
18534
18902
  const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
18535
18903
  if (input.agent === "codex") {
18536
- const hooksFlag = " --enable codex_hooks";
18904
+ const hooksFlag = " --enable hooks";
18537
18905
  const yoloFlag2 = input.yolo ? " --yolo" : "";
18538
18906
  if (input.launchMode === "resume") {
18539
18907
  return `codex${hooksFlag}${yoloFlag2} resume --last${promptSuffix}`;
@@ -19998,7 +20366,7 @@ async function mapWithConcurrency(items, limit, fn) {
19998
20366
  }
19999
20367
 
20000
20368
  // backend/src/services/reconciliation-service.ts
20001
- import { basename as basename4, resolve as resolve9 } from "path";
20369
+ import { basename as basename5, resolve as resolve9 } from "path";
20002
20370
  function makeUnmanagedWorktreeId(path) {
20003
20371
  return `unmanaged:${resolve9(path)}`;
20004
20372
  }
@@ -20033,7 +20401,7 @@ function findWindow(windows, sessionName, branch) {
20033
20401
  return windows.find((window) => window.sessionName === sessionName && window.windowName === windowName) ?? null;
20034
20402
  }
20035
20403
  function resolveBranch(entry, metaBranch) {
20036
- const fallback = basename4(entry.path);
20404
+ const fallback = basename5(entry.path);
20037
20405
  return entry.branch ?? metaBranch ?? (fallback.length > 0 ? fallback : "unknown");
20038
20406
  }
20039
20407
 
@@ -20270,7 +20638,7 @@ __export(exports_worktree_commands, {
20270
20638
  parseAddCommandArgs: () => parseAddCommandArgs,
20271
20639
  getWorktreeCommandUsage: () => getWorktreeCommandUsage
20272
20640
  });
20273
- import { basename as basename5, resolve as resolve10 } from "path";
20641
+ import { basename as basename6, resolve as resolve10 } from "path";
20274
20642
  function getWorktreeCommandUsage(command) {
20275
20643
  switch (command) {
20276
20644
  case "add":
@@ -20706,7 +21074,7 @@ async function listWorktrees(runtime, stdout, options) {
20706
21074
  const projectGitDir = runtime.git.resolveWorktreeGitDir(projectDir);
20707
21075
  const archivedPaths = buildArchivedWorktreePathSet(await readWorktreeArchiveState(projectGitDir));
20708
21076
  const rows = await Promise.all(entries.map(async (entry) => {
20709
- const branch = entry.branch ?? basename5(entry.path);
21077
+ const branch = entry.branch ?? basename6(entry.path);
20710
21078
  const isOpen = openWindows.has(buildWorktreeWindowName(branch));
20711
21079
  const gitDir = runtime.git.resolveWorktreeGitDir(entry.path);
20712
21080
  const meta = await readWorktreeMeta(gitDir);
@@ -20950,13 +21318,13 @@ var init_worktree_commands = __esm(() => {
20950
21318
  });
20951
21319
 
20952
21320
  // bin/src/webmux.ts
20953
- import { resolve as resolve11, dirname as dirname6, join as join10 } from "path";
20954
- import { existsSync as existsSync5 } from "fs";
21321
+ import { resolve as resolve11, dirname as dirname6, join as join13 } from "path";
21322
+ import { existsSync as existsSync7 } from "fs";
20955
21323
  import { fileURLToPath } from "url";
20956
21324
  // package.json
20957
21325
  var package_default = {
20958
21326
  name: "webmux",
20959
- version: "0.33.0",
21327
+ version: "0.34.0",
20960
21328
  description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
20961
21329
  type: "module",
20962
21330
  repository: {
@@ -21060,6 +21428,7 @@ function isServeRootOption(value) {
21060
21428
  }
21061
21429
  function parseRootArgs(args) {
21062
21430
  let port = parseInt(process.env.PORT || "5111", 10);
21431
+ let portExplicit = process.env.PORT !== undefined;
21063
21432
  let debug = false;
21064
21433
  let app = false;
21065
21434
  let prefix = process.env.WEBMUX_PREFIX?.trim() || null;
@@ -21083,6 +21452,7 @@ function parseRootArgs(args) {
21083
21452
  if (Number.isNaN(port)) {
21084
21453
  throw new Error("Error: --port requires a numeric value");
21085
21454
  }
21455
+ portExplicit = true;
21086
21456
  index += 1;
21087
21457
  break;
21088
21458
  }
@@ -21120,6 +21490,7 @@ Run webmux --help for usage.`);
21120
21490
  }
21121
21491
  return {
21122
21492
  port,
21493
+ portExplicit,
21123
21494
  debug,
21124
21495
  app,
21125
21496
  prefix,
@@ -21131,7 +21502,7 @@ function isWorktreeCommand(command) {
21131
21502
  return command === "add" || command === "list" || command === "open" || command === "close" || command === "archive" || command === "unarchive" || command === "label" || command === "remove" || command === "merge" || command === "send" || command === "prune";
21132
21503
  }
21133
21504
  async function loadEnvFile(path) {
21134
- if (!existsSync5(path))
21505
+ if (!existsSync7(path))
21135
21506
  return;
21136
21507
  const lines = (await Bun.file(path).text()).split(`
21137
21508
  `);
@@ -21164,7 +21535,7 @@ function findBrowserBinary() {
21164
21535
  "brave-browser"
21165
21536
  ];
21166
21537
  for (const candidate of candidates) {
21167
- const found = candidate.startsWith("/") ? existsSync5(candidate) : Bun.spawnSync(["which", candidate], { stdout: "pipe", stderr: "pipe" }).success;
21538
+ const found = candidate.startsWith("/") ? existsSync7(candidate) : Bun.spawnSync(["which", candidate], { stdout: "pipe", stderr: "pipe" }).success;
21168
21539
  if (found)
21169
21540
  return candidate;
21170
21541
  }
@@ -21243,6 +21614,28 @@ async function main(args = process.argv.slice(2)) {
21243
21614
  stderr: "inherit"
21244
21615
  });
21245
21616
  const code = await proc.exited;
21617
+ if (code === 0) {
21618
+ const { listInstalledServices: listInstalledServices2, updateInstalledService: updateInstalledService2 } = await Promise.resolve().then(() => (init_service_restart(), exports_service_restart));
21619
+ const services = listInstalledServices2();
21620
+ if (services.length > 0) {
21621
+ const whichResult = Bun.spawnSync(["which", "webmux"], { stdout: "pipe", stderr: "pipe" });
21622
+ const webmuxPath = whichResult.success ? whichResult.stdout.toString().trim() : "";
21623
+ console.log(`
21624
+ Refreshing ${services.length} installed webmux service(s) to pick up the new version...`);
21625
+ for (const svc of services) {
21626
+ const outcome = await updateInstalledService2(svc, webmuxPath);
21627
+ const parts = [];
21628
+ if (outcome.regenerated)
21629
+ parts.push("regenerated unit");
21630
+ if (outcome.restarted)
21631
+ parts.push("restarted");
21632
+ if (!outcome.regenerated && !outcome.restarted && !outcome.error)
21633
+ parts.push("no change");
21634
+ const status2 = outcome.error ? `failed \u2014 ${outcome.error}` : parts.join(", ");
21635
+ console.log(` ${svc.name}: ${status2}`);
21636
+ }
21637
+ }
21638
+ }
21246
21639
  process.exit(code);
21247
21640
  }
21248
21641
  await loadEnvFile(resolve11(process.cwd(), ".env.local"));
@@ -21271,7 +21664,7 @@ async function main(args = process.argv.slice(2)) {
21271
21664
  usage2();
21272
21665
  process.exit(0);
21273
21666
  }
21274
- if (!existsSync5(resolve11(process.cwd(), ".webmux.yaml"))) {
21667
+ if (!existsSync7(resolve11(process.cwd(), ".webmux.yaml"))) {
21275
21668
  console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
21276
21669
  process.exit(1);
21277
21670
  }
@@ -21279,6 +21672,7 @@ async function main(args = process.argv.slice(2)) {
21279
21672
  ...process.env,
21280
21673
  PORT: String(parsed.port),
21281
21674
  WEBMUX_PROJECT_DIR: process.cwd(),
21675
+ ...parsed.portExplicit ? { WEBMUX_PORT_STRICT: "1" } : {},
21282
21676
  ...parsed.prefix ? { WEBMUX_PREFIX: parsed.prefix } : {},
21283
21677
  ...parsed.debug ? { WEBMUX_DEBUG: "1" } : {}
21284
21678
  };
@@ -21306,13 +21700,13 @@ async function main(args = process.argv.slice(2)) {
21306
21700
  }
21307
21701
  process.on("SIGINT", cleanup);
21308
21702
  process.on("SIGTERM", cleanup);
21309
- const backendEntry = join10(PKG_ROOT, "backend", "dist", "server.js");
21310
- const staticDir = join10(PKG_ROOT, "frontend", "dist");
21311
- if (!existsSync5(staticDir)) {
21703
+ const backendEntry = join13(PKG_ROOT, "backend", "dist", "server.js");
21704
+ const staticDir = join13(PKG_ROOT, "frontend", "dist");
21705
+ if (!existsSync7(staticDir)) {
21312
21706
  console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
21313
21707
  process.exit(1);
21314
21708
  }
21315
- console.log(`Starting webmux on port ${parsed.port} (falls back to a free port if taken)...`);
21709
+ console.log(parsed.portExplicit ? `Starting webmux on port ${parsed.port}...` : `Starting webmux on port ${parsed.port} (falls back to a free port if taken)...`);
21316
21710
  const be2 = Bun.spawn(["bun", backendEntry], {
21317
21711
  env: { ...baseEnv, WEBMUX_STATIC_DIR: staticDir },
21318
21712
  stdout: "pipe",