webmux 0.32.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
@@ -2443,12 +2443,15 @@ integrations:
2443
2443
  linear:
2444
2444
  # Enable Linear issue lookup and linking in the UI.
2445
2445
  enabled: true
2446
- # Auto-create worktrees for assigned issues.
2446
+ # Auto-create worktrees for assigned issues labeled "webmux" or "webmux_oneshot".
2447
2447
  # autoCreateWorktrees: true
2448
- # Show a create-ticket action in the dashboard.
2448
+ # Show a create-ticket action in the dashboard. The team to file into is
2449
+ # picked in the dialog at creation time.
2449
2450
  # createTicketOption: true
2450
- # Restrict issue sync to a specific Linear team id.
2451
- # teamId: team-123
2451
+ # Restrict the auto-create watcher to issues from these teams. Useful when
2452
+ # the authenticated Linear user is in multiple teams or when running webmux
2453
+ # in several projects on the same machine that share a Linear account.
2454
+ # watchTeams: [ENG, OPS]
2452
2455
 
2453
2456
  # startupEnvs become runtime env vars for panes, agents, and hooks.
2454
2457
  startupEnvs:
@@ -2716,14 +2719,255 @@ ${result.stderr.trim()}` : ""
2716
2719
  console.log();
2717
2720
  });
2718
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
+
2719
2961
  // bin/src/service.ts
2720
2962
  var exports_service = {};
2721
2963
  __export(exports_service, {
2964
+ parseInstalledServiceConfig: () => parseInstalledServiceConfig,
2965
+ generateServiceFile: () => generateServiceFile,
2722
2966
  default: () => service
2723
2967
  });
2724
- import { existsSync as existsSync4, mkdirSync, unlinkSync } from "fs";
2725
- import { join as join5 } from "path";
2726
- 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";
2727
2971
  function getPlatform() {
2728
2972
  const plat = process.platform;
2729
2973
  if (plat === "linux" || plat === "darwin")
@@ -2752,10 +2996,10 @@ function printRunResult(result) {
2752
2996
  console.error(err);
2753
2997
  }
2754
2998
  function systemdUnitPath(serviceName) {
2755
- return join5(homedir(), ".config", "systemd", "user", `${serviceName}.service`);
2999
+ return join7(homedir3(), ".config", "systemd", "user", `${serviceName}.service`);
2756
3000
  }
2757
3001
  function launchdPlistPath(serviceName) {
2758
- return join5(homedir(), "Library", "LaunchAgents", `com.webmux.${serviceName}.plist`);
3002
+ return join7(homedir3(), "Library", "LaunchAgents", `com.webmux.${serviceName}.plist`);
2759
3003
  }
2760
3004
  function serviceFilePath(config) {
2761
3005
  if (config.platform === "linux")
@@ -2781,7 +3025,7 @@ WantedBy=default.target
2781
3025
  `;
2782
3026
  }
2783
3027
  function generateLaunchdPlist(config) {
2784
- const logPath = join5(homedir(), "Library", "Logs", `webmux-${config.serviceName}.log`);
3028
+ const logPath = join7(homedir3(), "Library", "Logs", `webmux-${config.serviceName}.log`);
2785
3029
  return `<?xml version="1.0" encoding="UTF-8"?>
2786
3030
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2787
3031
  <plist version="1.0">
@@ -2826,6 +3070,36 @@ function generateServiceFile(config) {
2826
3070
  return generateSystemdUnit(config);
2827
3071
  return generateLaunchdPlist(config);
2828
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
+ }
2829
3103
  function installCommands(config) {
2830
3104
  if (config.platform === "linux") {
2831
3105
  return [
@@ -2849,11 +3123,12 @@ function uninstallCommands(config) {
2849
3123
  ];
2850
3124
  }
2851
3125
  function isInstalled(config) {
2852
- return existsSync4(serviceFilePath(config));
3126
+ return existsSync5(serviceFilePath(config));
2853
3127
  }
2854
- async function install(config) {
3128
+ async function install(config, portExplicit) {
2855
3129
  const filePath = serviceFilePath(config);
2856
- if (isInstalled(config)) {
3130
+ const alreadyInstalled = isInstalled(config);
3131
+ if (alreadyInstalled) {
2857
3132
  const reinstall = await ue({ message: "Service is already installed. Reinstall?" });
2858
3133
  if (q(reinstall) || !reinstall) {
2859
3134
  R2.info("Aborted.");
@@ -2863,6 +3138,31 @@ async function install(config) {
2863
3138
  runCommand(cmd);
2864
3139
  }
2865
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 };
2866
3166
  const content = generateServiceFile(config);
2867
3167
  const commands = installCommands(config);
2868
3168
  Se([
@@ -2874,12 +3174,16 @@ async function install(config) {
2874
3174
  ...commands.map((c) => ` $ ${formatCommand(c)}`)
2875
3175
  ].join(`
2876
3176
  `), "Install service");
3177
+ if (portNote)
3178
+ R2.info(portNote);
3179
+ if (portWarning)
3180
+ R2.warn(portWarning);
2877
3181
  const ok = await ue({ message: "Proceed?" });
2878
3182
  if (q(ok) || !ok) {
2879
3183
  R2.info("Aborted.");
2880
3184
  return;
2881
3185
  }
2882
- mkdirSync(filePath.substring(0, filePath.lastIndexOf("/")), { recursive: true });
3186
+ mkdirSync2(filePath.substring(0, filePath.lastIndexOf("/")), { recursive: true });
2883
3187
  await Bun.write(filePath, content);
2884
3188
  R2.success(`Wrote ${filePath}`);
2885
3189
  for (const cmd of commands) {
@@ -2929,7 +3233,7 @@ ${result.stderr.toString()}`);
2929
3233
  R2.success(`$ ${formatCommand(cmd)}`);
2930
3234
  }
2931
3235
  }
2932
- unlinkSync(filePath);
3236
+ unlinkSync2(filePath);
2933
3237
  R2.success(`Removed ${filePath}`);
2934
3238
  R2.success("Service uninstalled.");
2935
3239
  }
@@ -2953,8 +3257,8 @@ function logs(config) {
2953
3257
  if (config.platform === "linux") {
2954
3258
  proc = Bun.spawn(["journalctl", "--user", "-u", config.serviceName, "-f", "--no-pager"], { stdout: "inherit", stderr: "inherit" });
2955
3259
  } else {
2956
- const logPath = join5(homedir(), "Library", "Logs", `webmux-${config.serviceName}.log`);
2957
- if (!existsSync4(logPath)) {
3260
+ const logPath = join7(homedir3(), "Library", "Logs", `webmux-${config.serviceName}.log`);
3261
+ if (!existsSync5(logPath)) {
2958
3262
  R2.error(`Log file not found: ${logPath}`);
2959
3263
  return;
2960
3264
  }
@@ -2975,6 +3279,12 @@ Usage:
2975
3279
  webmux service uninstall Stop, disable, and remove the service
2976
3280
  webmux service status Show service status
2977
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.
2978
3288
  `);
2979
3289
  }
2980
3290
  async function service(args) {
@@ -3010,6 +3320,7 @@ async function service(args) {
3010
3320
  return;
3011
3321
  }
3012
3322
  let port = parseInt(process.env.PORT || "5111");
3323
+ let portExplicit = false;
3013
3324
  for (let i = 1;i < args.length; i++) {
3014
3325
  if (args[i] === "--port" && args[i + 1]) {
3015
3326
  const parsed = parseInt(args[++i]);
@@ -3018,6 +3329,7 @@ async function service(args) {
3018
3329
  return;
3019
3330
  }
3020
3331
  port = parsed;
3332
+ portExplicit = true;
3021
3333
  }
3022
3334
  }
3023
3335
  const projectName = detectProjectName(gitRoot2);
@@ -3032,7 +3344,7 @@ async function service(args) {
3032
3344
  };
3033
3345
  switch (action) {
3034
3346
  case "install":
3035
- await install(config);
3347
+ await install(config, portExplicit);
3036
3348
  break;
3037
3349
  case "uninstall":
3038
3350
  await uninstall(config);
@@ -3045,9 +3357,141 @@ async function service(args) {
3045
3357
  break;
3046
3358
  }
3047
3359
  }
3360
+ var SYSTEMD_WORKDIR_RE, LAUNCHD_WORKDIR_RE;
3048
3361
  var init_service = __esm(() => {
3049
3362
  init_dist4();
3050
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");
3051
3495
  });
3052
3496
 
3053
3497
  // node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
@@ -7016,7 +7460,7 @@ var init_zod = __esm(() => {
7016
7460
  init_external();
7017
7461
  });
7018
7462
 
7019
- // node_modules/.bun/@ts-rest+core@3.52.1+596964f7fee2c930/node_modules/@ts-rest/core/index.esm.mjs
7463
+ // node_modules/.bun/@ts-rest+core@3.52.1+94e40505b11febf1/node_modules/@ts-rest/core/index.esm.mjs
7020
7464
  var isZodType = (obj) => {
7021
7465
  return typeof (obj === null || obj === undefined ? undefined : obj.safeParse) === "function";
7022
7466
  }, isZodObjectStrict = (obj) => {
@@ -7338,7 +7782,7 @@ function parseLinearTarget(raw) {
7338
7782
  return { kind: "team", teamKey: trimmed };
7339
7783
  return { kind: "invalid", raw: trimmed };
7340
7784
  }
7341
- var BooleanLikeSchema, ErrorResponseSchema, OkResponseSchema, EnabledResponseSchema, BuiltInAgentIdSchema, AgentIdSchema, WorktreeCreateModeSchema, LinearIssueIdSchema, LinearTeamKeySchema, PostWorktreeToLinearTargetSchema, PostWorktreeToLinearRequestSchema, PostWorktreeToLinearResponseSchema, FromLinearInputSchema, OneshotConfigSchema, AgentCapabilitiesSchema, AgentSummarySchema, AgentDetailsSchema, AgentListResponseSchema, UpsertCustomAgentRequestSchema, AgentResponseSchema, ValidateCustomAgentResponseSchema, WorktreeCreationPhaseSchema, AvailableBranchSchema, AvailableBranchesQuerySchema, NumberLikePathParamSchema, BranchListResponseSchema, WorktreeSourceSchema, CreateWorktreeRequestSchema, OpenWorktreeRequestSchema, CreateWorktreeResponseSchema, SetWorktreeArchivedRequestSchema, SetWorktreeArchivedResponseSchema, SetWorktreeLabelRequestSchema, SetWorktreeLabelResponseSchema, ToggleEnabledRequestSchema, SendWorktreePromptRequestSchema, AgentsSendMessageRequestSchema, PullMainRequestSchema, PullMainStatusSchema, PullMainResponseSchema, ServiceStatusSchema, PrCommentSchema, CiCheckSchema, PrEntrySchema, LinearIssueLabelSchema, LinearIssueStateSchema, LinkedLinearIssueSchema, LinearIssueSchema, LinearIssueAvailabilitySchema, LinearIssuesResponseSchema, WorktreeCreationStateSchema, AppNotificationSchema, ProjectWorktreeSnapshotSchema, ProjectSnapshotSchema, WorktreeConversationProviderSchema, CodexWorktreeConversationRefSchema, ClaudeWorktreeConversationRefSchema, WorktreeConversationRefSchema, AgentsUiWorktreeSummarySchema, AgentsUiConversationMessageRoleSchema, AgentsUiConversationMessageStatusSchema, AgentsUiConversationMessageKindSchema, AgentsUiConversationMessageSchema, AgentsUiConversationStateSchema, AgentsUiWorktreeConversationResponseSchema, AgentsUiSendMessageResponseSchema, AgentsUiInterruptResponseSchema, AgentsUiConversationSnapshotEventSchema, AgentsUiConversationMessageDeltaEventSchema, AgentsUiConversationErrorEventSchema, AgentsUiConversationEventSchema, WorktreeListResponseSchema, UnpushedCommitSchema, WorktreeDiffResponseSchema, ServiceConfigSchema, ProfileConfigSchema, LinkedRepoInfoSchema, AppConfigSchema, CiLogsResponseSchema, WorktreeNameParamsSchema, NotificationIdParamsSchema, AgentIdParamsSchema, RunIdParamsSchema;
7785
+ var BooleanLikeSchema, ErrorResponseSchema, OkResponseSchema, EnabledResponseSchema, BuiltInAgentIdSchema, AgentIdSchema, WorktreeCreateModeSchema, LinearIssueIdSchema, LinearTeamKeySchema, PostWorktreeToLinearTargetSchema, PostWorktreeToLinearRequestSchema, PostWorktreeToLinearResponseSchema, FromLinearInputSchema, OneshotConfigSchema, AgentCapabilitiesSchema, AgentSummarySchema, AgentDetailsSchema, AgentListResponseSchema, UpsertCustomAgentRequestSchema, AgentResponseSchema, ValidateCustomAgentResponseSchema, WorktreeCreationPhaseSchema, AvailableBranchSchema, AvailableBranchesQuerySchema, NumberLikePathParamSchema, BranchListResponseSchema, WorktreeSourceSchema, CreateWorktreeRequestSchema, OpenWorktreeRequestSchema, CreateWorktreeResponseSchema, SetWorktreeArchivedRequestSchema, SetWorktreeArchivedResponseSchema, SetWorktreeLabelRequestSchema, SetWorktreeLabelResponseSchema, ToggleEnabledRequestSchema, SendWorktreePromptRequestSchema, AgentsSendMessageRequestSchema, PullMainRequestSchema, PullMainStatusSchema, PullMainResponseSchema, ServiceStatusSchema, PrCommentSchema, CiCheckSchema, PrEntrySchema, LinearIssueLabelSchema, LinearIssueStateSchema, LinkedLinearIssueSchema, LinearIssueSchema, LinearIssueAvailabilitySchema, LinearIssuesResponseSchema, WorktreeCreationStateSchema, AppNotificationSchema, ProjectWorktreeSnapshotSchema, ProjectSnapshotSchema, WorktreeConversationProviderSchema, CodexWorktreeConversationRefSchema, ClaudeWorktreeConversationRefSchema, WorktreeConversationRefSchema, AgentsUiWorktreeSummarySchema, AgentsUiConversationMessageRoleSchema, AgentsUiConversationMessageStatusSchema, AgentsUiConversationMessageKindSchema, AgentsUiConversationMessageSchema, AgentsUiConversationStateSchema, AgentsUiWorktreeConversationResponseSchema, AgentsUiSendMessageResponseSchema, AgentsUiInterruptResponseSchema, AgentsUiConversationSnapshotEventSchema, AgentsUiConversationMessageDeltaEventSchema, AgentsUiConversationErrorEventSchema, AgentsUiConversationEventSchema, WorktreeListResponseSchema, UnpushedCommitSchema, WorktreeDiffResponseSchema, ServiceConfigSchema, ProfileConfigSchema, LinkedRepoInfoSchema, AppConfigSchema, CiLogsResponseSchema, WorktreeNameParamsSchema, NotificationIdParamsSchema, AgentIdParamsSchema, RunIdParamsSchema, InstanceSummarySchema, InstancesResponseSchema;
7342
7786
  var init_schemas = __esm(() => {
7343
7787
  init_zod();
7344
7788
  BooleanLikeSchema = exports_external.union([
@@ -7451,6 +7895,7 @@ var init_schemas = __esm(() => {
7451
7895
  envOverrides: exports_external.record(exports_external.string()).optional(),
7452
7896
  createLinearTicket: exports_external.literal(true).optional(),
7453
7897
  linearTitle: exports_external.string().optional(),
7898
+ linearTeamKey: exports_external.string().trim().transform((value) => value.toUpperCase()).pipe(LinearTeamKeySchema).optional(),
7454
7899
  fromLinear: FromLinearInputSchema.optional(),
7455
7900
  source: WorktreeSourceSchema.optional(),
7456
7901
  oneshot: OneshotConfigSchema.optional()
@@ -7763,6 +8208,15 @@ var init_schemas = __esm(() => {
7763
8208
  RunIdParamsSchema = exports_external.object({
7764
8209
  runId: NumberLikePathParamSchema
7765
8210
  });
8211
+ InstanceSummarySchema = exports_external.object({
8212
+ prefix: exports_external.string(),
8213
+ port: exports_external.number(),
8214
+ projectDir: exports_external.string(),
8215
+ startedAt: exports_external.number()
8216
+ });
8217
+ InstancesResponseSchema = exports_external.object({
8218
+ instances: exports_external.array(InstanceSummarySchema)
8219
+ });
7766
8220
  });
7767
8221
 
7768
8222
  // packages/api-contract/src/contract.ts
@@ -7803,7 +8257,8 @@ var init_contract = __esm(() => {
7803
8257
  setAutoRemoveOnMerge: "/api/github/auto-remove-on-merge",
7804
8258
  pullMain: "/api/pull-main",
7805
8259
  fetchCiLogs: "/api/ci-logs/:runId",
7806
- dismissNotification: "/api/notifications/:id/dismiss"
8260
+ dismissNotification: "/api/notifications/:id/dismiss",
8261
+ fetchInstances: "/api/instances"
7807
8262
  };
7808
8263
  commonErrorResponses = {
7809
8264
  400: ErrorResponseSchema,
@@ -8112,6 +8567,14 @@ var init_contract = __esm(() => {
8112
8567
  400: ErrorResponseSchema,
8113
8568
  404: ErrorResponseSchema
8114
8569
  }
8570
+ },
8571
+ fetchInstances: {
8572
+ method: "GET",
8573
+ path: apiPaths.fetchInstances,
8574
+ responses: {
8575
+ 200: InstancesResponseSchema,
8576
+ 500: ErrorResponseSchema
8577
+ }
8115
8578
  }
8116
8579
  }, {
8117
8580
  strictStatusCodes: true
@@ -8199,30 +8662,6 @@ var init_src = __esm(() => {
8199
8662
  init_schemas();
8200
8663
  });
8201
8664
 
8202
- // backend/src/lib/log.ts
8203
- function ts() {
8204
- return new Date().toISOString().slice(11, 23);
8205
- }
8206
- var DEBUG, log;
8207
- var init_log = __esm(() => {
8208
- DEBUG = Bun.env.WEBMUX_DEBUG === "1";
8209
- log = {
8210
- info(msg) {
8211
- console.log(`[${ts()}] ${msg}`);
8212
- },
8213
- debug(msg) {
8214
- if (DEBUG)
8215
- console.log(`[${ts()}] ${msg}`);
8216
- },
8217
- warn(msg) {
8218
- console.warn(`[${ts()}] ${msg}`);
8219
- },
8220
- error(msg, err) {
8221
- err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
8222
- }
8223
- };
8224
- });
8225
-
8226
8665
  // backend/src/services/linear-service.ts
8227
8666
  function gqlErrorMessage(raw) {
8228
8667
  return raw.errors && raw.errors.length > 0 ? raw.errors.map((error) => error.message).join("; ") : null;
@@ -9540,7 +9979,7 @@ var WORKTREE_META_SCHEMA_VERSION = 1, WORKTREE_ARCHIVE_STATE_VERSION = 1;
9540
9979
 
9541
9980
  // backend/src/adapters/fs.ts
9542
9981
  import { mkdir } from "fs/promises";
9543
- import { join as join6 } from "path";
9982
+ import { join as join9 } from "path";
9544
9983
  function stringifyAllocatedPorts(ports) {
9545
9984
  const entries = Object.entries(ports).map(([key, value]) => [key, String(value)]);
9546
9985
  return Object.fromEntries(entries);
@@ -9572,25 +10011,25 @@ function parseDotenv(content) {
9572
10011
  }
9573
10012
  async function loadDotenvLocal(worktreePath) {
9574
10013
  try {
9575
- const content = await Bun.file(join6(worktreePath, ".env.local")).text();
10014
+ const content = await Bun.file(join9(worktreePath, ".env.local")).text();
9576
10015
  return parseDotenv(content);
9577
10016
  } catch {
9578
10017
  return {};
9579
10018
  }
9580
10019
  }
9581
10020
  function getWorktreeStoragePaths(gitDir) {
9582
- const webmuxDir = join6(gitDir, "webmux");
10021
+ const webmuxDir = join9(gitDir, "webmux");
9583
10022
  return {
9584
10023
  gitDir,
9585
10024
  webmuxDir,
9586
- metaPath: join6(webmuxDir, "meta.json"),
9587
- runtimeEnvPath: join6(webmuxDir, "runtime.env"),
9588
- controlEnvPath: join6(webmuxDir, "control.env"),
9589
- 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")
9590
10029
  };
9591
10030
  }
9592
10031
  function getProjectArchiveStatePath(gitDir) {
9593
- return join6(gitDir, "webmux", "archive.json");
10032
+ return join9(gitDir, "webmux", "archive.json");
9594
10033
  }
9595
10034
  async function ensureWorktreeStorageDirs(gitDir) {
9596
10035
  const paths = getWorktreeStoragePaths(gitDir);
@@ -9759,7 +10198,7 @@ var init_fs = __esm(() => {
9759
10198
 
9760
10199
  // backend/src/adapters/tmux.ts
9761
10200
  import { createHash } from "crypto";
9762
- import { basename as basename3, resolve as resolve3 } from "path";
10201
+ import { basename as basename4, resolve as resolve3 } from "path";
9763
10202
  function runTmux(args) {
9764
10203
  const result = Bun.spawnSync(["tmux", ...args], {
9765
10204
  stdout: "pipe",
@@ -9788,7 +10227,7 @@ function sanitizeTmuxNameSegment(value, maxLength = 24) {
9788
10227
  }
9789
10228
  function buildProjectSessionName(projectRoot) {
9790
10229
  const resolved = resolve3(projectRoot);
9791
- const base = sanitizeTmuxNameSegment(basename3(resolved), 18);
10230
+ const base = sanitizeTmuxNameSegment(basename4(resolved), 18);
9792
10231
  const hash = createHash("sha1").update(resolved).digest("hex").slice(0, 8);
9793
10232
  return `wm-${base}-${hash}`;
9794
10233
  }
@@ -9863,54 +10302,6 @@ class BunTmuxGateway {
9863
10302
  }
9864
10303
  var init_tmux = () => {};
9865
10304
 
9866
- // backend/src/domain/policies.ts
9867
- function sanitizeBranchName(raw) {
9868
- 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, "");
9869
- }
9870
- function isValidBranchName(raw) {
9871
- return raw.length > 0 && sanitizeBranchName(raw) === raw;
9872
- }
9873
- function isValidWorktreeName(name) {
9874
- return name.length > 0 && VALID_WORKTREE_NAME_RE.test(name) && !name.includes("..");
9875
- }
9876
- function isValidEnvKey(key) {
9877
- return UNSAFE_ENV_KEY_RE.test(key);
9878
- }
9879
- function allocateServicePorts(existingMetas, services) {
9880
- const allocatable = services.filter((service2) => service2.portStart != null);
9881
- if (allocatable.length === 0)
9882
- return {};
9883
- const reference = allocatable[0];
9884
- const referenceStart = reference.portStart;
9885
- const referenceStep = reference.portStep ?? 1;
9886
- const occupiedSlots = new Set;
9887
- for (const meta of existingMetas) {
9888
- const port = meta.allocatedPorts[reference.portEnv];
9889
- if (!Number.isInteger(port) || port < referenceStart)
9890
- continue;
9891
- const diff = port - referenceStart;
9892
- if (diff % referenceStep !== 0)
9893
- continue;
9894
- occupiedSlots.add(diff / referenceStep);
9895
- }
9896
- let slot = 1;
9897
- while (occupiedSlots.has(slot))
9898
- slot += 1;
9899
- const result = {};
9900
- for (const service2 of allocatable) {
9901
- const start = service2.portStart;
9902
- const step = service2.portStep ?? 1;
9903
- result[service2.portEnv] = start + slot * step;
9904
- }
9905
- return result;
9906
- }
9907
- var INVALID_BRANCH_CHARS_RE, UNSAFE_ENV_KEY_RE, VALID_WORKTREE_NAME_RE;
9908
- var init_policies = __esm(() => {
9909
- INVALID_BRANCH_CHARS_RE = /[~^:?*\[\]\\]+/g;
9910
- UNSAFE_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
9911
- VALID_WORKTREE_NAME_RE = /^[a-z0-9][a-z0-9\-_./]*$/;
9912
- });
9913
-
9914
10305
  // backend/src/services/archive-service.ts
9915
10306
  import { resolve as resolve4 } from "path";
9916
10307
  function createArchiveState(entries) {
@@ -16934,8 +17325,8 @@ var init_dist5 = __esm(() => {
16934
17325
  });
16935
17326
 
16936
17327
  // backend/src/adapters/config.ts
16937
- import { readFileSync as readFileSync3 } from "fs";
16938
- 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";
16939
17330
  function DEFAULT_ONESHOT_SYSTEM_PROMPT() {
16940
17331
  return [
16941
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.",
@@ -17164,10 +17555,10 @@ function getDefaultProfileName(config) {
17164
17555
  return Object.keys(config.profiles)[0] ?? "default";
17165
17556
  }
17166
17557
  function readConfigFile(root) {
17167
- return readFileSync3(join7(root, ".webmux.yaml"), "utf8");
17558
+ return readFileSync7(join10(root, ".webmux.yaml"), "utf8");
17168
17559
  }
17169
17560
  function readLocalConfigFile(root) {
17170
- return readFileSync3(join7(root, ".webmux.local.yaml"), "utf8");
17561
+ return readFileSync7(join10(root, ".webmux.local.yaml"), "utf8");
17171
17562
  }
17172
17563
  function parseConfigDocument(text) {
17173
17564
  const parsed = $parse(text);
@@ -17191,12 +17582,7 @@ function parseProjectConfig(parsed) {
17191
17582
  linkedRepos: isRecord4(parsed.integrations) && isRecord4(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github.linkedRepos) : isRecord4(parsed.integrations) && Array.isArray(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github) : [],
17192
17583
  autoRemoveOnMerge: isRecord4(parsed.integrations) && isRecord4(parsed.integrations.github) && typeof parsed.integrations.github.autoRemoveOnMerge === "boolean" ? parsed.integrations.github.autoRemoveOnMerge : DEFAULT_CONFIG.integrations.github.autoRemoveOnMerge
17193
17584
  },
17194
- linear: {
17195
- enabled: isRecord4(parsed.integrations) && isRecord4(parsed.integrations.linear) && typeof parsed.integrations.linear.enabled === "boolean" ? parsed.integrations.linear.enabled : DEFAULT_CONFIG.integrations.linear.enabled,
17196
- autoCreateWorktrees: isRecord4(parsed.integrations) && isRecord4(parsed.integrations.linear) && typeof parsed.integrations.linear.autoCreateWorktrees === "boolean" ? parsed.integrations.linear.autoCreateWorktrees : DEFAULT_CONFIG.integrations.linear.autoCreateWorktrees,
17197
- createTicketOption: isRecord4(parsed.integrations) && isRecord4(parsed.integrations.linear) && typeof parsed.integrations.linear.createTicketOption === "boolean" ? parsed.integrations.linear.createTicketOption : DEFAULT_CONFIG.integrations.linear.createTicketOption,
17198
- ...isRecord4(parsed.integrations) && isRecord4(parsed.integrations.linear) && typeof parsed.integrations.linear.teamId === "string" && parsed.integrations.linear.teamId.trim() ? { teamId: parsed.integrations.linear.teamId.trim() } : {}
17199
- }
17585
+ linear: parseLinearIntegration(parsed)
17200
17586
  },
17201
17587
  lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
17202
17588
  autoName: parseAutoName(parsed.auto_name),
@@ -17206,6 +17592,29 @@ function parseProjectConfig(parsed) {
17206
17592
  function defaultConfig() {
17207
17593
  return parseProjectConfig({});
17208
17594
  }
17595
+ function parseTeamKeyList(raw) {
17596
+ if (!Array.isArray(raw))
17597
+ return;
17598
+ const keys = raw.filter((entry) => typeof entry === "string").map((entry) => entry.trim().toUpperCase()).filter((entry) => entry.length > 0);
17599
+ return keys.length > 0 ? Array.from(new Set(keys)) : undefined;
17600
+ }
17601
+ function parseLinearIntegration(parsed) {
17602
+ const defaults = DEFAULT_CONFIG.integrations.linear;
17603
+ const linear = isRecord4(parsed.integrations) && isRecord4(parsed.integrations.linear) ? parsed.integrations.linear : null;
17604
+ if (!linear)
17605
+ return { ...defaults };
17606
+ if (typeof linear.teamId === "string" && !warnedLegacyLinearTeamId) {
17607
+ warnedLegacyLinearTeamId = true;
17608
+ log.warn("[config] integrations.linear.teamId is no longer used \u2014 the ticket team is now picked at creation time in the dashboard");
17609
+ }
17610
+ const watchTeams = parseTeamKeyList(linear.watchTeams);
17611
+ return {
17612
+ enabled: typeof linear.enabled === "boolean" ? linear.enabled : defaults.enabled,
17613
+ autoCreateWorktrees: typeof linear.autoCreateWorktrees === "boolean" ? linear.autoCreateWorktrees : defaults.autoCreateWorktrees,
17614
+ createTicketOption: typeof linear.createTicketOption === "boolean" ? linear.createTicketOption : defaults.createTicketOption,
17615
+ ...watchTeams ? { watchTeams } : {}
17616
+ };
17617
+ }
17209
17618
  function parseLocalLinearOverlay(parsed) {
17210
17619
  if (!isRecord4(parsed.integrations))
17211
17620
  return null;
@@ -17219,8 +17628,9 @@ function parseLocalLinearOverlay(parsed) {
17219
17628
  overlay.autoCreateWorktrees = linear.autoCreateWorktrees;
17220
17629
  if (typeof linear.createTicketOption === "boolean")
17221
17630
  overlay.createTicketOption = linear.createTicketOption;
17222
- if (typeof linear.teamId === "string" && linear.teamId.trim())
17223
- overlay.teamId = linear.teamId.trim();
17631
+ const watchTeams = parseTeamKeyList(linear.watchTeams);
17632
+ if (watchTeams)
17633
+ overlay.watchTeams = watchTeams;
17224
17634
  return Object.keys(overlay).length > 0 ? overlay : null;
17225
17635
  }
17226
17636
  function parseLocalGitHubOverlay(parsed) {
@@ -17337,9 +17747,10 @@ function loadConfig(dir, options = {}) {
17337
17747
  function expandTemplate(template, env) {
17338
17748
  return template.replace(/\$\{(\w+)\}/g, (_, key) => env[key] ?? "");
17339
17749
  }
17340
- var DEFAULT_PANES, DEFAULT_CONFIG;
17750
+ var DEFAULT_PANES, DEFAULT_CONFIG, warnedLegacyLinearTeamId = false;
17341
17751
  var init_config = __esm(() => {
17342
17752
  init_dist5();
17753
+ init_log();
17343
17754
  DEFAULT_PANES = [
17344
17755
  { id: "agent", kind: "agent", focus: true },
17345
17756
  { id: "shell", kind: "shell", split: "right", sizePct: 25 }
@@ -17652,7 +18063,7 @@ var init_docker = __esm(() => {
17652
18063
  });
17653
18064
 
17654
18065
  // backend/src/adapters/hooks.ts
17655
- import { join as join8 } from "path";
18066
+ import { join as join11 } from "path";
17656
18067
  function buildErrorMessage(name, exitCode, stdout, stderr) {
17657
18068
  const output = stderr.trim() || stdout.trim();
17658
18069
  if (output) {
@@ -17677,7 +18088,7 @@ class BunLifecycleHookRunner {
17677
18088
  return this.direnvAvailable;
17678
18089
  }
17679
18090
  async buildCommand(cwd, command) {
17680
- if (this.checkDirenv() && await Bun.file(join8(cwd, ".envrc")).exists()) {
18091
+ if (this.checkDirenv() && await Bun.file(join11(cwd, ".envrc")).exists()) {
17681
18092
  Bun.spawnSync(["direnv", "allow"], { cwd, stdout: "pipe", stderr: "pipe" });
17682
18093
  return ["direnv", "exec", cwd, "bash", "-c", command];
17683
18094
  }
@@ -18005,7 +18416,7 @@ var init_archive_state_service = __esm(() => {
18005
18416
 
18006
18417
  // backend/src/adapters/agent-runtime.ts
18007
18418
  import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
18008
- 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";
18009
18420
  function shellQuote(value) {
18010
18421
  return `'${value.replaceAll("'", "'\\''")}'`;
18011
18422
  }
@@ -18424,7 +18835,7 @@ async function mergeCodexHooksFile(hooksPath, hookSettings, agentCtlPath) {
18424
18835
  }
18425
18836
  async function resolveGitCommonDir(gitDir) {
18426
18837
  try {
18427
- const commonDir = (await Bun.file(join9(gitDir, "commondir")).text()).trim();
18838
+ const commonDir = (await Bun.file(join12(gitDir, "commondir")).text()).trim();
18428
18839
  if (!commonDir)
18429
18840
  return gitDir;
18430
18841
  return commonDir.startsWith("/") ? commonDir : resolve6(gitDir, commonDir);
@@ -18434,7 +18845,7 @@ async function resolveGitCommonDir(gitDir) {
18434
18845
  }
18435
18846
  async function ensureGeneratedCodexHooksIgnored(gitDir) {
18436
18847
  const commonDir = await resolveGitCommonDir(gitDir);
18437
- const excludePath = join9(commonDir, "info", "exclude");
18848
+ const excludePath = join12(commonDir, "info", "exclude");
18438
18849
  let existing = "";
18439
18850
  try {
18440
18851
  existing = await Bun.file(excludePath).text();
@@ -18454,9 +18865,9 @@ async function ensureGeneratedCodexHooksIgnored(gitDir) {
18454
18865
  async function ensureAgentRuntimeArtifacts(input) {
18455
18866
  const storagePaths = getWorktreeStoragePaths(input.gitDir);
18456
18867
  const artifacts = {
18457
- agentCtlPath: join9(storagePaths.webmuxDir, "webmux-agentctl"),
18458
- claudeSettingsPath: join9(input.worktreePath, ".claude", "settings.local.json"),
18459
- 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")
18460
18871
  };
18461
18872
  await mkdir3(dirname4(artifacts.claudeSettingsPath), { recursive: true });
18462
18873
  await mkdir3(dirname4(artifacts.codexHooksPath), { recursive: true });
@@ -18490,7 +18901,7 @@ function buildDockerRuntimeBootstrap(runtimeEnvPath) {
18490
18901
  function buildBuiltInAgentInvocation(input) {
18491
18902
  const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
18492
18903
  if (input.agent === "codex") {
18493
- const hooksFlag = " --enable codex_hooks";
18904
+ const hooksFlag = " --enable hooks";
18494
18905
  const yoloFlag2 = input.yolo ? " --yolo" : "";
18495
18906
  if (input.launchMode === "resume") {
18496
18907
  return `codex${hooksFlag}${yoloFlag2} resume --last${promptSuffix}`;
@@ -19955,7 +20366,7 @@ async function mapWithConcurrency(items, limit, fn) {
19955
20366
  }
19956
20367
 
19957
20368
  // backend/src/services/reconciliation-service.ts
19958
- import { basename as basename4, resolve as resolve9 } from "path";
20369
+ import { basename as basename5, resolve as resolve9 } from "path";
19959
20370
  function makeUnmanagedWorktreeId(path) {
19960
20371
  return `unmanaged:${resolve9(path)}`;
19961
20372
  }
@@ -19990,7 +20401,7 @@ function findWindow(windows, sessionName, branch) {
19990
20401
  return windows.find((window) => window.sessionName === sessionName && window.windowName === windowName) ?? null;
19991
20402
  }
19992
20403
  function resolveBranch(entry, metaBranch) {
19993
- const fallback = basename4(entry.path);
20404
+ const fallback = basename5(entry.path);
19994
20405
  return entry.branch ?? metaBranch ?? (fallback.length > 0 ? fallback : "unknown");
19995
20406
  }
19996
20407
 
@@ -20227,7 +20638,7 @@ __export(exports_worktree_commands, {
20227
20638
  parseAddCommandArgs: () => parseAddCommandArgs,
20228
20639
  getWorktreeCommandUsage: () => getWorktreeCommandUsage
20229
20640
  });
20230
- import { basename as basename5, resolve as resolve10 } from "path";
20641
+ import { basename as basename6, resolve as resolve10 } from "path";
20231
20642
  function getWorktreeCommandUsage(command) {
20232
20643
  switch (command) {
20233
20644
  case "add":
@@ -20663,7 +21074,7 @@ async function listWorktrees(runtime, stdout, options) {
20663
21074
  const projectGitDir = runtime.git.resolveWorktreeGitDir(projectDir);
20664
21075
  const archivedPaths = buildArchivedWorktreePathSet(await readWorktreeArchiveState(projectGitDir));
20665
21076
  const rows = await Promise.all(entries.map(async (entry) => {
20666
- const branch = entry.branch ?? basename5(entry.path);
21077
+ const branch = entry.branch ?? basename6(entry.path);
20667
21078
  const isOpen = openWindows.has(buildWorktreeWindowName(branch));
20668
21079
  const gitDir = runtime.git.resolveWorktreeGitDir(entry.path);
20669
21080
  const meta = await readWorktreeMeta(gitDir);
@@ -20907,13 +21318,13 @@ var init_worktree_commands = __esm(() => {
20907
21318
  });
20908
21319
 
20909
21320
  // bin/src/webmux.ts
20910
- import { resolve as resolve11, dirname as dirname6, join as join10 } from "path";
20911
- 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";
20912
21323
  import { fileURLToPath } from "url";
20913
21324
  // package.json
20914
21325
  var package_default = {
20915
21326
  name: "webmux",
20916
- version: "0.32.0",
21327
+ version: "0.34.0",
20917
21328
  description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
20918
21329
  type: "module",
20919
21330
  repository: {
@@ -20996,7 +21407,9 @@ Usage:
20996
21407
  webmux completion Generate shell completion script (bash, zsh)
20997
21408
 
20998
21409
  Options:
20999
- --port N Set port (default: 5111)
21410
+ --port N Set port (default: 5111). Falls back to a free port when taken.
21411
+ --prefix NAME URL prefix this instance registers under (default: project dir basename).
21412
+ Other webmux instances on this machine will redirect /<NAME> to this port.
21000
21413
  --app Open dashboard in browser app mode (minimal window)
21001
21414
  --debug Show debug-level logs
21002
21415
  --version Show version number
@@ -21004,18 +21417,21 @@ Options:
21004
21417
 
21005
21418
  Environment:
21006
21419
  PORT Same as --port (flag takes precedence)
21420
+ WEBMUX_PREFIX Same as --prefix
21007
21421
  `);
21008
21422
  }
21009
21423
  function isRootCommand(value) {
21010
21424
  return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "oneshot" || value === "list" || value === "open" || value === "close" || value === "archive" || value === "unarchive" || value === "label" || value === "remove" || value === "merge" || value === "send" || value === "prune" || value === "linear" || value === "completion";
21011
21425
  }
21012
21426
  function isServeRootOption(value) {
21013
- return value === "--port" || value === "--app" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
21427
+ return value === "--port" || value === "--prefix" || value === "--app" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
21014
21428
  }
21015
21429
  function parseRootArgs(args) {
21016
21430
  let port = parseInt(process.env.PORT || "5111", 10);
21431
+ let portExplicit = process.env.PORT !== undefined;
21017
21432
  let debug = false;
21018
21433
  let app = false;
21434
+ let prefix = process.env.WEBMUX_PREFIX?.trim() || null;
21019
21435
  let command = null;
21020
21436
  const commandArgs = [];
21021
21437
  for (let index = 0;index < args.length; index++) {
@@ -21036,6 +21452,16 @@ function parseRootArgs(args) {
21036
21452
  if (Number.isNaN(port)) {
21037
21453
  throw new Error("Error: --port requires a numeric value");
21038
21454
  }
21455
+ portExplicit = true;
21456
+ index += 1;
21457
+ break;
21458
+ }
21459
+ case "--prefix": {
21460
+ const value = args[index + 1];
21461
+ if (!value) {
21462
+ throw new Error("Error: --prefix requires a value");
21463
+ }
21464
+ prefix = value.trim();
21039
21465
  index += 1;
21040
21466
  break;
21041
21467
  }
@@ -21064,8 +21490,10 @@ Run webmux --help for usage.`);
21064
21490
  }
21065
21491
  return {
21066
21492
  port,
21493
+ portExplicit,
21067
21494
  debug,
21068
21495
  app,
21496
+ prefix,
21069
21497
  command,
21070
21498
  commandArgs
21071
21499
  };
@@ -21074,7 +21502,7 @@ function isWorktreeCommand(command) {
21074
21502
  return command === "add" || command === "list" || command === "open" || command === "close" || command === "archive" || command === "unarchive" || command === "label" || command === "remove" || command === "merge" || command === "send" || command === "prune";
21075
21503
  }
21076
21504
  async function loadEnvFile(path) {
21077
- if (!existsSync5(path))
21505
+ if (!existsSync7(path))
21078
21506
  return;
21079
21507
  const lines = (await Bun.file(path).text()).split(`
21080
21508
  `);
@@ -21107,7 +21535,7 @@ function findBrowserBinary() {
21107
21535
  "brave-browser"
21108
21536
  ];
21109
21537
  for (const candidate of candidates) {
21110
- 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;
21111
21539
  if (found)
21112
21540
  return candidate;
21113
21541
  }
@@ -21143,7 +21571,7 @@ function pipeWithPrefix(stream, prefix, onTrigger) {
21143
21571
  console.log(`${prefix} ${line}`);
21144
21572
  if (onTrigger && !fired && line.includes(onTrigger.text)) {
21145
21573
  fired = true;
21146
- onTrigger.callback();
21574
+ onTrigger.callback(line);
21147
21575
  }
21148
21576
  }
21149
21577
  }
@@ -21186,6 +21614,28 @@ async function main(args = process.argv.slice(2)) {
21186
21614
  stderr: "inherit"
21187
21615
  });
21188
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
+ }
21189
21639
  process.exit(code);
21190
21640
  }
21191
21641
  await loadEnvFile(resolve11(process.cwd(), ".env.local"));
@@ -21214,7 +21664,7 @@ async function main(args = process.argv.slice(2)) {
21214
21664
  usage2();
21215
21665
  process.exit(0);
21216
21666
  }
21217
- if (!existsSync5(resolve11(process.cwd(), ".webmux.yaml"))) {
21667
+ if (!existsSync7(resolve11(process.cwd(), ".webmux.yaml"))) {
21218
21668
  console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
21219
21669
  process.exit(1);
21220
21670
  }
@@ -21222,6 +21672,8 @@ async function main(args = process.argv.slice(2)) {
21222
21672
  ...process.env,
21223
21673
  PORT: String(parsed.port),
21224
21674
  WEBMUX_PROJECT_DIR: process.cwd(),
21675
+ ...parsed.portExplicit ? { WEBMUX_PORT_STRICT: "1" } : {},
21676
+ ...parsed.prefix ? { WEBMUX_PREFIX: parsed.prefix } : {},
21225
21677
  ...parsed.debug ? { WEBMUX_DEBUG: "1" } : {}
21226
21678
  };
21227
21679
  const children = [];
@@ -21248,13 +21700,13 @@ async function main(args = process.argv.slice(2)) {
21248
21700
  }
21249
21701
  process.on("SIGINT", cleanup);
21250
21702
  process.on("SIGTERM", cleanup);
21251
- const backendEntry = join10(PKG_ROOT, "backend", "dist", "server.js");
21252
- const staticDir = join10(PKG_ROOT, "frontend", "dist");
21253
- 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)) {
21254
21706
  console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
21255
21707
  process.exit(1);
21256
21708
  }
21257
- console.log(`Starting webmux on port ${parsed.port}...`);
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)...`);
21258
21710
  const be2 = Bun.spawn(["bun", backendEntry], {
21259
21711
  env: { ...baseEnv, WEBMUX_STATIC_DIR: staticDir },
21260
21712
  stdout: "pipe",
@@ -21264,7 +21716,10 @@ async function main(args = process.argv.slice(2)) {
21264
21716
  if (parsed.app) {
21265
21717
  pipeWithPrefix(be2.stdout, "[BE]", {
21266
21718
  text: "Dev Dashboard API running at",
21267
- callback: () => openAppMode(`http://localhost:${parsed.port}`)
21719
+ callback: (line) => {
21720
+ const match = line.match(/https?:\/\/[^\s]+/);
21721
+ openAppMode(match?.[0] ?? `http://localhost:${parsed.port}`);
21722
+ }
21268
21723
  });
21269
21724
  } else {
21270
21725
  pipeWithPrefix(be2.stdout, "[BE]");