webmux 0.40.0 → 0.41.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
@@ -5997,7 +5997,7 @@ var init_zod = __esm(() => {
5997
5997
  init_external();
5998
5998
  });
5999
5999
 
6000
- // node_modules/.bun/@ts-rest+core@3.52.1+aea371d0c1c4fd29/node_modules/@ts-rest/core/index.esm.mjs
6000
+ // node_modules/.bun/@ts-rest+core@3.52.1+38b2c5aebf202339/node_modules/@ts-rest/core/index.esm.mjs
6001
6001
  var isZodType = (obj) => {
6002
6002
  return typeof (obj === null || obj === undefined ? undefined : obj.safeParse) === "function";
6003
6003
  }, isZodObjectStrict = (obj) => {
@@ -8339,21 +8339,110 @@ ${result.stderr.trim()}` : ""
8339
8339
  console.log();
8340
8340
  });
8341
8341
 
8342
+ // backend/src/domain/projects.ts
8343
+ function isProjectEntry(value) {
8344
+ if (typeof value !== "object" || value === null)
8345
+ return false;
8346
+ const v = value;
8347
+ return typeof v.path === "string" && v.path.length > 0 && typeof v.name === "string" && typeof v.addedAt === "number";
8348
+ }
8349
+
8350
+ // backend/src/lib/log.ts
8351
+ function ts() {
8352
+ return new Date().toISOString().slice(11, 23);
8353
+ }
8354
+ var DEBUG, log2;
8355
+ var init_log = __esm(() => {
8356
+ DEBUG = Bun.env.WEBMUX_DEBUG === "1";
8357
+ log2 = {
8358
+ info(msg) {
8359
+ console.log(`[${ts()}] ${msg}`);
8360
+ },
8361
+ debug(msg) {
8362
+ if (DEBUG)
8363
+ console.log(`[${ts()}] ${msg}`);
8364
+ },
8365
+ warn(msg) {
8366
+ console.warn(`[${ts()}] ${msg}`);
8367
+ },
8368
+ error(msg, err) {
8369
+ err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
8370
+ }
8371
+ };
8372
+ });
8373
+
8374
+ // backend/src/adapters/projects-registry.ts
8375
+ import { mkdirSync, readFileSync as readFileSync3, renameSync, writeFileSync } from "fs";
8376
+ import { homedir } from "os";
8377
+ import { dirname as dirname3, join as join5 } from "path";
8378
+ function defaultRegistryFile() {
8379
+ return join5(homedir(), ".webmux", "projects.json");
8380
+ }
8381
+ function createProjectsRegistry(file = defaultRegistryFile()) {
8382
+ function read() {
8383
+ let raw;
8384
+ try {
8385
+ raw = readFileSync3(file, "utf8");
8386
+ } catch {
8387
+ return [];
8388
+ }
8389
+ let parsed;
8390
+ try {
8391
+ parsed = JSON.parse(raw);
8392
+ } catch {
8393
+ log2.debug(`[projects-registry] ignoring malformed ${file}`);
8394
+ return [];
8395
+ }
8396
+ if (!Array.isArray(parsed))
8397
+ return [];
8398
+ return parsed.filter(isProjectEntry);
8399
+ }
8400
+ function write(entries) {
8401
+ mkdirSync(dirname3(file), { recursive: true });
8402
+ const tmpPath = `${file}.${process.pid}.${Date.now()}.tmp`;
8403
+ writeFileSync(tmpPath, `${JSON.stringify(entries, null, 2)}
8404
+ `);
8405
+ renameSync(tmpPath, file);
8406
+ }
8407
+ return {
8408
+ list() {
8409
+ return read();
8410
+ },
8411
+ add(entry) {
8412
+ const entries = read().filter((existing) => existing.path !== entry.path);
8413
+ entries.push(entry);
8414
+ write(entries);
8415
+ },
8416
+ remove(path) {
8417
+ const entries = read();
8418
+ const next = entries.filter((entry) => entry.path !== path);
8419
+ if (next.length !== entries.length)
8420
+ write(next);
8421
+ }
8422
+ };
8423
+ }
8424
+ var init_projects_registry = __esm(() => {
8425
+ init_log();
8426
+ });
8427
+
8342
8428
  // bin/src/service.ts
8343
8429
  var exports_service = {};
8344
8430
  __export(exports_service, {
8431
+ shouldPersistProject: () => shouldPersistProject,
8345
8432
  resolveEnvVars: () => resolveEnvVars,
8433
+ resolveConfirmDecision: () => resolveConfirmDecision,
8346
8434
  readPortFromUnit: () => readPortFromUnit,
8347
8435
  readEnvVarsFromUnit: () => readEnvVarsFromUnit,
8348
8436
  parseInstalledServiceConfig: () => parseInstalledServiceConfig,
8349
8437
  parseEnvCliArgs: () => parseEnvCliArgs,
8438
+ migrateServedRepoFromUnit: () => migrateServedRepoFromUnit,
8350
8439
  generateServiceFile: () => generateServiceFile,
8351
8440
  default: () => service,
8352
8441
  AUTO_PICKUP_ENV_VARS: () => AUTO_PICKUP_ENV_VARS
8353
8442
  });
8354
- import { chmodSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, unlinkSync } from "fs";
8355
- import { basename as basename3, join as join5 } from "path";
8356
- import { homedir } from "os";
8443
+ import { chmodSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, unlinkSync } from "fs";
8444
+ import { basename as basename3, join as join6 } from "path";
8445
+ import { homedir as homedir2 } from "os";
8357
8446
  function getPlatform() {
8358
8447
  const plat = process.platform;
8359
8448
  if (plat === "linux" || plat === "darwin")
@@ -8366,6 +8455,33 @@ function resolveWebmuxPath() {
8366
8455
  return null;
8367
8456
  return result.stdout.toString().trim();
8368
8457
  }
8458
+ function shouldPersistProject(root, hasWebmuxConfig, existingPaths) {
8459
+ if (!root || !hasWebmuxConfig)
8460
+ return false;
8461
+ return !existingPaths.includes(root);
8462
+ }
8463
+ function hasWebmuxConfig(root) {
8464
+ return existsSync4(join6(root, ".webmux.yaml")) || existsSync4(join6(root, ".webmux.local.yaml"));
8465
+ }
8466
+ function persistProject(root, registry) {
8467
+ if (!root)
8468
+ return null;
8469
+ const existingPaths = registry.list().map((entry) => entry.path);
8470
+ if (!shouldPersistProject(root, hasWebmuxConfig(root), existingPaths))
8471
+ return null;
8472
+ registry.add({ path: root, name: detectProjectName(root), addedAt: Date.now() });
8473
+ return root;
8474
+ }
8475
+ function cwdProjectToPersist() {
8476
+ const gitRoot2 = getGitRoot();
8477
+ if (!gitRoot2)
8478
+ return null;
8479
+ const existingPaths = createProjectsRegistry().list().map((entry) => entry.path);
8480
+ return shouldPersistProject(gitRoot2, hasWebmuxConfig(gitRoot2), existingPaths) ? gitRoot2 : null;
8481
+ }
8482
+ function migrateServedRepoFromUnit(filePath, platform, registry = createProjectsRegistry()) {
8483
+ return persistProject(readWorkingDirFromUnit(filePath, platform), registry);
8484
+ }
8369
8485
  function formatCommand([bin, args]) {
8370
8486
  return [bin, ...args].join(" ");
8371
8487
  }
@@ -8379,10 +8495,10 @@ function printRunResult(result) {
8379
8495
  console.error(err);
8380
8496
  }
8381
8497
  function systemdUnitPath(serviceName) {
8382
- return join5(homedir(), ".config", "systemd", "user", `${serviceName}.service`);
8498
+ return join6(homedir2(), ".config", "systemd", "user", `${serviceName}.service`);
8383
8499
  }
8384
8500
  function launchdPlistPath(serviceName) {
8385
- return join5(homedir(), "Library", "LaunchAgents", `com.webmux.${serviceName}.plist`);
8501
+ return join6(homedir2(), "Library", "LaunchAgents", `com.webmux.${serviceName}.plist`);
8386
8502
  }
8387
8503
  function serviceFilePath(config) {
8388
8504
  if (config.platform === "linux")
@@ -8393,16 +8509,15 @@ function generateSystemdUnit(config) {
8393
8509
  const extra = Object.keys(config.envVars).sort().map((key) => `Environment=${key}=${config.envVars[key]}`).join(`
8394
8510
  `);
8395
8511
  return `[Unit]
8396
- Description=webmux dashboard \u2014 ${config.projectName}
8512
+ Description=webmux dashboard
8397
8513
 
8398
8514
  [Service]
8399
8515
  Type=simple
8400
8516
  ExecStart=${config.webmuxPath} serve --port ${config.port}
8401
- WorkingDirectory=${config.projectDir}
8517
+ WorkingDirectory=${homedir2()}
8402
8518
  Restart=on-failure
8403
8519
  RestartSec=5
8404
8520
  Environment=PORT=${config.port}
8405
- Environment=WEBMUX_PROJECT_DIR=${config.projectDir}
8406
8521
  Environment=PATH=${process.env.PATH}${extra ? `
8407
8522
  ` + extra : ""}
8408
8523
 
@@ -8414,7 +8529,7 @@ function escapePlistText(value) {
8414
8529
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
8415
8530
  }
8416
8531
  function generateLaunchdPlist(config) {
8417
- const logPath = join5(homedir(), "Library", "Logs", `webmux-${config.serviceName}.log`);
8532
+ const logPath = join6(homedir2(), "Library", "Logs", `webmux-${config.serviceName}.log`);
8418
8533
  const extra = Object.keys(config.envVars).sort().map((key) => ` <key>${escapePlistText(key)}</key>
8419
8534
  <string>${escapePlistText(config.envVars[key])}</string>`).join(`
8420
8535
  `);
@@ -8432,7 +8547,7 @@ function generateLaunchdPlist(config) {
8432
8547
  <string>${config.port}</string>
8433
8548
  </array>
8434
8549
  <key>WorkingDirectory</key>
8435
- <string>${config.projectDir}</string>
8550
+ <string>${homedir2()}</string>
8436
8551
  <key>RunAtLoad</key>
8437
8552
  <true/>
8438
8553
  <key>KeepAlive</key>
@@ -8448,8 +8563,6 @@ function generateLaunchdPlist(config) {
8448
8563
  <dict>
8449
8564
  <key>PORT</key>
8450
8565
  <string>${config.port}</string>
8451
- <key>WEBMUX_PROJECT_DIR</key>
8452
- <string>${config.projectDir}</string>
8453
8566
  <key>PATH</key>
8454
8567
  <string>${process.env.PATH}</string>${extra ? `
8455
8568
  ` + extra : ""}
@@ -8466,7 +8579,7 @@ function generateServiceFile(config) {
8466
8579
  function readWorkingDirFromUnit(filePath, platform) {
8467
8580
  let text;
8468
8581
  try {
8469
- text = readFileSync3(filePath, "utf8");
8582
+ text = readFileSync4(filePath, "utf8");
8470
8583
  } catch {
8471
8584
  return null;
8472
8585
  }
@@ -8477,7 +8590,7 @@ function readWorkingDirFromUnit(filePath, platform) {
8477
8590
  function readPortFromUnit(filePath) {
8478
8591
  let text;
8479
8592
  try {
8480
- text = readFileSync3(filePath, "utf8");
8593
+ text = readFileSync4(filePath, "utf8");
8481
8594
  } catch {
8482
8595
  return null;
8483
8596
  }
@@ -8491,7 +8604,7 @@ function unescapePlistText(value) {
8491
8604
  function readEnvVarsFromUnit(filePath, platform) {
8492
8605
  let text;
8493
8606
  try {
8494
- text = readFileSync3(filePath, "utf8");
8607
+ text = readFileSync4(filePath, "utf8");
8495
8608
  } catch {
8496
8609
  return {};
8497
8610
  }
@@ -8520,19 +8633,13 @@ function parseInstalledServiceConfig(filePath, platform, webmuxPath) {
8520
8633
  const port = readPortFromUnit(filePath);
8521
8634
  if (port === null)
8522
8635
  return null;
8523
- const projectDir = readWorkingDirFromUnit(filePath, platform);
8524
- if (projectDir === null)
8525
- return null;
8526
8636
  const fileBase = basename3(filePath);
8527
8637
  const serviceName = platform === "linux" ? fileBase.replace(/\.service$/, "") : fileBase.replace(/^com\.webmux\./, "").replace(/\.plist$/, "");
8528
- const projectName = detectProjectName(projectDir);
8529
8638
  const envVars = readEnvVarsFromUnit(filePath, platform);
8530
8639
  return {
8531
8640
  platform,
8532
- projectName,
8533
8641
  serviceName,
8534
8642
  webmuxPath,
8535
- projectDir,
8536
8643
  port,
8537
8644
  envVars
8538
8645
  };
@@ -8627,19 +8734,19 @@ function redactSecretsInUnit(content, envVars) {
8627
8734
  }
8628
8735
  return out;
8629
8736
  }
8630
- async function install(config, portExplicit, envVarNotes) {
8737
+ function isInteractive() {
8738
+ return Boolean(process.stdin.isTTY);
8739
+ }
8740
+ function resolveConfirmDecision(autoConfirm, interactive) {
8741
+ if (autoConfirm)
8742
+ return "proceed";
8743
+ if (!interactive)
8744
+ return "abort-noninteractive";
8745
+ return "prompt";
8746
+ }
8747
+ async function install(config, portExplicit, envVarNotes, autoConfirm) {
8631
8748
  const filePath = serviceFilePath(config);
8632
8749
  const alreadyInstalled = isInstalled(config);
8633
- if (alreadyInstalled) {
8634
- const reinstall = await confirm({ message: "Service is already installed. Reinstall?" });
8635
- if (isCancel(reinstall) || !reinstall) {
8636
- log.info("Aborted.");
8637
- return;
8638
- }
8639
- for (const cmd of uninstallCommands(config)) {
8640
- runCommand(cmd);
8641
- }
8642
- }
8643
8750
  const requestedPort = config.port;
8644
8751
  let chosenPort = requestedPort;
8645
8752
  let portNote = null;
@@ -8653,14 +8760,17 @@ async function install(config, portExplicit, envVarNotes) {
8653
8760
  config = { ...config, port: chosenPort };
8654
8761
  const content = generateServiceFile(config);
8655
8762
  const commands = installCommands(config);
8763
+ const persistPath = cwdProjectToPersist();
8656
8764
  const displayContent = redactSecretsInUnit(content, config.envVars);
8657
8765
  note([
8766
+ ...alreadyInstalled ? ["Service is already installed \u2014 this will reinstall it.", ""] : [],
8658
8767
  `File: ${filePath}`,
8659
8768
  "",
8660
8769
  "Contents:",
8661
8770
  displayContent,
8662
8771
  "Commands to run:",
8663
- ...commands.map((c4) => ` $ ${formatCommand(c4)}`)
8772
+ ...commands.map((c4) => ` $ ${formatCommand(c4)}`),
8773
+ ...persistPath ? ["", `Will also register this project: ${persistPath}`] : []
8664
8774
  ].join(`
8665
8775
  `), "Install service");
8666
8776
  if (Object.keys(config.envVars).length > 0) {
@@ -8670,12 +8780,28 @@ ${envVarNotes.join(`
8670
8780
  }
8671
8781
  if (portNote)
8672
8782
  log.info(portNote);
8673
- const ok = await confirm({ message: "Proceed?" });
8674
- if (isCancel(ok) || !ok) {
8675
- log.info("Aborted.");
8783
+ const decision = resolveConfirmDecision(autoConfirm, isInteractive());
8784
+ if (decision === "abort-noninteractive") {
8785
+ log.info(`Non-interactive environment \u2014 not ${alreadyInstalled ? "reinstalling" : "installing"}. ` + "Re-run with --yes to confirm and apply the plan above.");
8676
8786
  return;
8677
8787
  }
8678
- mkdirSync(filePath.substring(0, filePath.lastIndexOf("/")), { recursive: true });
8788
+ if (decision === "prompt") {
8789
+ const ok = await confirm({ message: alreadyInstalled ? "Reinstall?" : "Proceed?" });
8790
+ if (isCancel(ok) || !ok) {
8791
+ log.info("Aborted.");
8792
+ return;
8793
+ }
8794
+ }
8795
+ if (alreadyInstalled) {
8796
+ const migrated = migrateServedRepoFromUnit(filePath, config.platform);
8797
+ if (migrated) {
8798
+ log.success(`Migrated previously-served project ${detectProjectName(migrated)} (${migrated})`);
8799
+ }
8800
+ for (const cmd of uninstallCommands(config)) {
8801
+ runCommand(cmd);
8802
+ }
8803
+ }
8804
+ mkdirSync2(filePath.substring(0, filePath.lastIndexOf("/")), { recursive: true });
8679
8805
  await Bun.write(filePath, content);
8680
8806
  if (Object.keys(config.envVars).length > 0) {
8681
8807
  try {
@@ -8685,6 +8811,14 @@ ${envVarNotes.join(`
8685
8811
  }
8686
8812
  }
8687
8813
  log.success(`Wrote ${filePath}`);
8814
+ if (persistPath) {
8815
+ createProjectsRegistry().add({
8816
+ path: persistPath,
8817
+ name: detectProjectName(persistPath),
8818
+ addedAt: Date.now()
8819
+ });
8820
+ log.success(`Registered project ${detectProjectName(persistPath)} (${persistPath})`);
8821
+ }
8688
8822
  for (const cmd of commands) {
8689
8823
  const result = runCommand(cmd);
8690
8824
  if (!result.success) {
@@ -8756,7 +8890,7 @@ function logs(config) {
8756
8890
  if (config.platform === "linux") {
8757
8891
  proc = Bun.spawn(["journalctl", "--user", "-u", config.serviceName, "-f", "--no-pager"], { stdout: "inherit", stderr: "inherit" });
8758
8892
  } else {
8759
- const logPath = join5(homedir(), "Library", "Logs", `webmux-${config.serviceName}.log`);
8893
+ const logPath = join6(homedir2(), "Library", "Logs", `webmux-${config.serviceName}.log`);
8760
8894
  if (!existsSync4(logPath)) {
8761
8895
  log.error(`Log file not found: ${logPath}`);
8762
8896
  return;
@@ -8785,6 +8919,9 @@ Usage:
8785
8919
  Options:
8786
8920
  --port N Pin the service to a port (default: 5111). On
8787
8921
  reinstall without --port the existing port is kept.
8922
+ --yes, -y Skip the confirmation prompt and install. In a
8923
+ non-interactive shell (CI, pipe) install prints the
8924
+ plan and stops unless --yes is passed.
8788
8925
  --env KEY=VALUE Bake an environment variable into the service
8789
8926
  unit (repeatable). Reserved keys PORT,
8790
8927
  WEBMUX_PROJECT_DIR, and PATH are rejected.
@@ -8813,11 +8950,6 @@ async function service(args) {
8813
8950
  log.error(`Unsupported platform: ${process.platform}. Only linux and macOS are supported.`);
8814
8951
  return;
8815
8952
  }
8816
- const gitRoot2 = getGitRoot();
8817
- if (!gitRoot2) {
8818
- log.error("Not inside a git repository.");
8819
- return;
8820
- }
8821
8953
  const serviceManager = platform === "linux" ? "systemctl" : "launchctl";
8822
8954
  const smResult = run("which", [serviceManager]);
8823
8955
  if (!smResult.success) {
@@ -8832,6 +8964,7 @@ async function service(args) {
8832
8964
  let port = parseInt(process.env.PORT || "5111");
8833
8965
  let portExplicit = false;
8834
8966
  let autoPickup = true;
8967
+ let autoConfirm = false;
8835
8968
  for (let i2 = 1;i2 < args.length; i2++) {
8836
8969
  if (args[i2] === "--port" && args[i2 + 1]) {
8837
8970
  const parsed = parseInt(args[++i2]);
@@ -8843,6 +8976,8 @@ async function service(args) {
8843
8976
  portExplicit = true;
8844
8977
  } else if (args[i2] === "--no-auto-env") {
8845
8978
  autoPickup = false;
8979
+ } else if (args[i2] === "--yes" || args[i2] === "-y") {
8980
+ autoConfirm = true;
8846
8981
  }
8847
8982
  }
8848
8983
  const cliEnv = parseEnvCliArgs(args.slice(1));
@@ -8851,7 +8986,6 @@ async function service(args) {
8851
8986
  log.error(err);
8852
8987
  return;
8853
8988
  }
8854
- const projectName = detectProjectName(gitRoot2);
8855
8989
  const serviceName = "webmux";
8856
8990
  let envVars = {};
8857
8991
  let envVarNotes = [];
@@ -8868,16 +9002,14 @@ async function service(args) {
8868
9002
  }
8869
9003
  const config = {
8870
9004
  platform,
8871
- projectName,
8872
9005
  serviceName,
8873
9006
  webmuxPath,
8874
- projectDir: gitRoot2,
8875
9007
  port,
8876
9008
  envVars
8877
9009
  };
8878
9010
  switch (action) {
8879
9011
  case "install":
8880
- await install(config, portExplicit, envVarNotes);
9012
+ await install(config, portExplicit, envVarNotes, autoConfirm);
8881
9013
  break;
8882
9014
  case "uninstall":
8883
9015
  await uninstall(config);
@@ -8898,6 +9030,7 @@ var AUTO_PICKUP_ENV_VARS, RESERVED_ENV_KEYS, SYSTEMD_WORKDIR_RE, LAUNCHD_WORKDIR
8898
9030
  var init_service = __esm(() => {
8899
9031
  init_dist4();
8900
9032
  init_shared();
9033
+ init_projects_registry();
8901
9034
  AUTO_PICKUP_ENV_VARS = ["LINEAR_API_KEY"];
8902
9035
  RESERVED_ENV_KEYS = new Set(["PORT", "WEBMUX_PROJECT_DIR", "PATH"]);
8903
9036
  SYSTEMD_WORKDIR_RE = /^WorkingDirectory=(.+)$/m;
@@ -8917,9 +9050,9 @@ __export(exports_service_restart, {
8917
9050
  restartCommand: () => restartCommand,
8918
9051
  listInstalledServices: () => listInstalledServices
8919
9052
  });
8920
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
8921
- import { homedir as homedir2 } from "os";
8922
- import { join as join6 } from "path";
9053
+ import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "fs";
9054
+ import { homedir as homedir3 } from "os";
9055
+ import { join as join7 } from "path";
8923
9056
  function listInstalledServices(opts = {}) {
8924
9057
  const out = [];
8925
9058
  const systemdDir = opts.systemdDir ?? DEFAULT_SYSTEMD_DIR;
@@ -8931,7 +9064,7 @@ function listInstalledServices(opts = {}) {
8931
9064
  continue;
8932
9065
  out.push({
8933
9066
  name: name.slice(0, -".service".length),
8934
- filePath: join6(systemdDir, name),
9067
+ filePath: join7(systemdDir, name),
8935
9068
  platform: "linux"
8936
9069
  });
8937
9070
  }
@@ -8944,7 +9077,7 @@ function listInstalledServices(opts = {}) {
8944
9077
  continue;
8945
9078
  out.push({
8946
9079
  name: name.slice(0, -".plist".length),
8947
- filePath: join6(launchdDir, name),
9080
+ filePath: join7(launchdDir, name),
8948
9081
  platform: "darwin"
8949
9082
  });
8950
9083
  }
@@ -8987,17 +9120,19 @@ function reloadAfterRegenerate(service2, runner) {
8987
9120
  service is now unloaded \u2014 recover with: launchctl load -w "${service2.filePath}"`
8988
9121
  };
8989
9122
  }
8990
- async function updateInstalledService(service2, webmuxPath, runner = defaultRunner) {
9123
+ async function updateInstalledService(service2, webmuxPath, runner = defaultRunner, migrate = migrateServedRepoFromUnit) {
8991
9124
  const canRegenerate = webmuxPath.length > 0;
8992
9125
  const config = canRegenerate ? parseInstalledServiceConfig(service2.filePath, service2.platform, webmuxPath) : null;
8993
9126
  let regenerated = false;
9127
+ let migratedProject;
8994
9128
  if (config !== null) {
8995
9129
  let currentContent = "";
8996
9130
  try {
8997
- currentContent = readFileSync4(service2.filePath, "utf8");
9131
+ currentContent = readFileSync5(service2.filePath, "utf8");
8998
9132
  } catch {}
8999
9133
  const expected = generateServiceFile(config);
9000
9134
  if (currentContent !== expected) {
9135
+ migratedProject = migrate(service2.filePath, service2.platform) ?? undefined;
9001
9136
  try {
9002
9137
  await Bun.write(service2.filePath, expected);
9003
9138
  regenerated = true;
@@ -9006,6 +9141,7 @@ async function updateInstalledService(service2, webmuxPath, runner = defaultRunn
9006
9141
  service: service2,
9007
9142
  regenerated: false,
9008
9143
  restarted: false,
9144
+ migratedProject,
9009
9145
  error: `could not rewrite ${service2.filePath}: ${String(err)}`
9010
9146
  };
9011
9147
  }
@@ -9014,16 +9150,17 @@ async function updateInstalledService(service2, webmuxPath, runner = defaultRunn
9014
9150
  if (regenerated) {
9015
9151
  const reload = reloadAfterRegenerate(service2, runner);
9016
9152
  if (!reload.ok) {
9017
- return { service: service2, regenerated, restarted: false, error: reload.error };
9153
+ return { service: service2, regenerated, restarted: false, migratedProject, error: reload.error };
9018
9154
  }
9019
9155
  if (service2.platform === "darwin") {
9020
- return { service: service2, regenerated, restarted: true };
9156
+ return { service: service2, regenerated, restarted: true, migratedProject };
9021
9157
  }
9022
9158
  }
9023
9159
  const outcome = restartInstalledService(service2, runner);
9024
9160
  return {
9025
9161
  service: service2,
9026
9162
  regenerated,
9163
+ migratedProject,
9027
9164
  restarted: outcome.ok,
9028
9165
  error: outcome.error
9029
9166
  };
@@ -9033,40 +9170,16 @@ var init_service_restart = __esm(() => {
9033
9170
  init_shared();
9034
9171
  init_service();
9035
9172
  defaultRunner = { run };
9036
- DEFAULT_SYSTEMD_DIR = join6(homedir2(), ".config", "systemd", "user");
9037
- DEFAULT_LAUNCHD_DIR = join6(homedir2(), "Library", "LaunchAgents");
9038
- });
9039
-
9040
- // backend/src/lib/log.ts
9041
- function ts() {
9042
- return new Date().toISOString().slice(11, 23);
9043
- }
9044
- var DEBUG, log2;
9045
- var init_log = __esm(() => {
9046
- DEBUG = Bun.env.WEBMUX_DEBUG === "1";
9047
- log2 = {
9048
- info(msg) {
9049
- console.log(`[${ts()}] ${msg}`);
9050
- },
9051
- debug(msg) {
9052
- if (DEBUG)
9053
- console.log(`[${ts()}] ${msg}`);
9054
- },
9055
- warn(msg) {
9056
- console.warn(`[${ts()}] ${msg}`);
9057
- },
9058
- error(msg, err) {
9059
- err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
9060
- }
9061
- };
9173
+ DEFAULT_SYSTEMD_DIR = join7(homedir3(), ".config", "systemd", "user");
9174
+ DEFAULT_LAUNCHD_DIR = join7(homedir3(), "Library", "LaunchAgents");
9062
9175
  });
9063
9176
 
9064
9177
  // backend/src/adapters/instance-registry.ts
9065
- import { mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync5, renameSync, unlinkSync as unlinkSync2, writeFileSync } from "fs";
9066
- import { homedir as homedir3 } from "os";
9067
- import { join as join7 } from "path";
9178
+ import { mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync6, renameSync as renameSync2, unlinkSync as unlinkSync2, writeFileSync as writeFileSync2 } from "fs";
9179
+ import { homedir as homedir4 } from "os";
9180
+ import { join as join8 } from "path";
9068
9181
  function defaultRegistryDir() {
9069
- return join7(homedir3(), ".webmux", "instances");
9182
+ return join8(homedir4(), ".webmux", "instances");
9070
9183
  }
9071
9184
  function isAlive(pid) {
9072
9185
  try {
@@ -9084,14 +9197,14 @@ function isInstanceEntry(value) {
9084
9197
  }
9085
9198
  function createInstanceRegistry(dir = defaultRegistryDir()) {
9086
9199
  function ensureDir() {
9087
- mkdirSync2(dir, { recursive: true });
9200
+ mkdirSync3(dir, { recursive: true });
9088
9201
  }
9089
9202
  function entryPath(port) {
9090
- return join7(dir, `${port}.json`);
9203
+ return join8(dir, `${port}.json`);
9091
9204
  }
9092
9205
  function readEntry(filename) {
9093
9206
  try {
9094
- const raw = readFileSync5(join7(dir, filename), "utf8");
9207
+ const raw = readFileSync6(join8(dir, filename), "utf8");
9095
9208
  const parsed = JSON.parse(raw);
9096
9209
  return isInstanceEntry(parsed) ? parsed : null;
9097
9210
  } catch {
@@ -9105,8 +9218,8 @@ function createInstanceRegistry(dir = defaultRegistryDir()) {
9105
9218
  const tmpPath = `${finalPath}.${process.pid}.${Date.now()}.tmp`;
9106
9219
  const text = `${JSON.stringify(entry, null, 2)}
9107
9220
  `;
9108
- writeFileSync(tmpPath, text);
9109
- renameSync(tmpPath, finalPath);
9221
+ writeFileSync2(tmpPath, text);
9222
+ renameSync2(tmpPath, finalPath);
9110
9223
  },
9111
9224
  deregister(port, expectedPid) {
9112
9225
  if (expectedPid !== undefined) {
@@ -9139,7 +9252,7 @@ function createInstanceRegistry(dir = defaultRegistryDir()) {
9139
9252
  continue;
9140
9253
  if (!isAlive(entry.pid)) {
9141
9254
  try {
9142
- unlinkSync2(join7(dir, filename));
9255
+ unlinkSync2(join8(dir, filename));
9143
9256
  } catch {}
9144
9257
  continue;
9145
9258
  }
@@ -11322,7 +11435,7 @@ var WORKTREE_META_SCHEMA_VERSION = 1, WORKTREE_ARCHIVE_STATE_VERSION = 1, OPEN_S
11322
11435
 
11323
11436
  // backend/src/adapters/fs.ts
11324
11437
  import { mkdir } from "fs/promises";
11325
- import { join as join8 } from "path";
11438
+ import { join as join9 } from "path";
11326
11439
  function stringifyAllocatedPorts(ports) {
11327
11440
  const entries = Object.entries(ports).map(([key, value]) => [key, String(value)]);
11328
11441
  return Object.fromEntries(entries);
@@ -11354,28 +11467,28 @@ function parseDotenv(content) {
11354
11467
  }
11355
11468
  async function loadDotenvLocal(worktreePath) {
11356
11469
  try {
11357
- const content = await Bun.file(join8(worktreePath, ".env.local")).text();
11470
+ const content = await Bun.file(join9(worktreePath, ".env.local")).text();
11358
11471
  return parseDotenv(content);
11359
11472
  } catch {
11360
11473
  return {};
11361
11474
  }
11362
11475
  }
11363
11476
  function getWorktreeStoragePaths(gitDir) {
11364
- const webmuxDir = join8(gitDir, "webmux");
11477
+ const webmuxDir = join9(gitDir, "webmux");
11365
11478
  return {
11366
11479
  gitDir,
11367
11480
  webmuxDir,
11368
- metaPath: join8(webmuxDir, "meta.json"),
11369
- runtimeEnvPath: join8(webmuxDir, "runtime.env"),
11370
- controlEnvPath: join8(webmuxDir, "control.env"),
11371
- prsPath: join8(webmuxDir, "prs.json")
11481
+ metaPath: join9(webmuxDir, "meta.json"),
11482
+ runtimeEnvPath: join9(webmuxDir, "runtime.env"),
11483
+ controlEnvPath: join9(webmuxDir, "control.env"),
11484
+ prsPath: join9(webmuxDir, "prs.json")
11372
11485
  };
11373
11486
  }
11374
11487
  function getProjectArchiveStatePath(gitDir) {
11375
- return join8(gitDir, "webmux", "archive.json");
11488
+ return join9(gitDir, "webmux", "archive.json");
11376
11489
  }
11377
11490
  function getProjectOpenSessionsStatePath(gitDir) {
11378
- return join8(gitDir, "webmux", "open-sessions.json");
11491
+ return join9(gitDir, "webmux", "open-sessions.json");
11379
11492
  }
11380
11493
  async function ensureWorktreeStorageDirs(gitDir) {
11381
11494
  const paths = getWorktreeStoragePaths(gitDir);
@@ -18783,8 +18896,8 @@ var init_dist5 = __esm(() => {
18783
18896
  });
18784
18897
 
18785
18898
  // backend/src/adapters/config.ts
18786
- import { readFileSync as readFileSync6 } from "fs";
18787
- import { dirname as dirname3, join as join9, resolve as resolve7 } from "path";
18899
+ import { readFileSync as readFileSync7 } from "fs";
18900
+ import { dirname as dirname4, join as join10, resolve as resolve7 } from "path";
18788
18901
  function DEFAULT_ONESHOT_SYSTEM_PROMPT() {
18789
18902
  return [
18790
18903
  "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.",
@@ -19013,10 +19126,10 @@ function getDefaultProfileName(config) {
19013
19126
  return Object.keys(config.profiles)[0] ?? "default";
19014
19127
  }
19015
19128
  function readConfigFile(root) {
19016
- return readFileSync6(join9(root, ".webmux.yaml"), "utf8");
19129
+ return readFileSync7(join10(root, ".webmux.yaml"), "utf8");
19017
19130
  }
19018
19131
  function readLocalConfigFile(root) {
19019
- return readFileSync6(join9(root, ".webmux.local.yaml"), "utf8");
19132
+ return readFileSync7(join10(root, ".webmux.local.yaml"), "utf8");
19020
19133
  }
19021
19134
  function parseConfigDocument(text) {
19022
19135
  const parsed = $parse(text);
@@ -19164,7 +19277,7 @@ function projectRoot(dir) {
19164
19277
  if (result.exitCode !== 0)
19165
19278
  return gitRoot2(dir);
19166
19279
  const commonDir = new TextDecoder().decode(result.stdout).trim();
19167
- return commonDir ? dirname3(resolve7(dir, commonDir)) : gitRoot2(dir);
19280
+ return commonDir ? dirname4(resolve7(dir, commonDir)) : gitRoot2(dir);
19168
19281
  }
19169
19282
  function loadConfig(dir, options = {}) {
19170
19283
  const root = options.resolvedRoot ? dir : projectRoot(dir);
@@ -19243,7 +19356,7 @@ var init_config = __esm(() => {
19243
19356
 
19244
19357
  // backend/src/adapters/control-token.ts
19245
19358
  import { chmod, mkdir as mkdir2 } from "fs/promises";
19246
- import { dirname as dirname4 } from "path";
19359
+ import { dirname as dirname5 } from "path";
19247
19360
  async function loadControlToken() {
19248
19361
  if (cachedToken)
19249
19362
  return cachedToken;
@@ -19253,7 +19366,7 @@ async function loadControlToken() {
19253
19366
  return cachedToken;
19254
19367
  }
19255
19368
  const controlToken = crypto.randomUUID();
19256
- await mkdir2(dirname4(CONTROL_TOKEN_PATH), { recursive: true });
19369
+ await mkdir2(dirname5(CONTROL_TOKEN_PATH), { recursive: true });
19257
19370
  await Bun.write(CONTROL_TOKEN_PATH, controlToken);
19258
19371
  await chmod(CONTROL_TOKEN_PATH, 384);
19259
19372
  cachedToken = controlToken;
@@ -19521,7 +19634,7 @@ var init_docker = __esm(() => {
19521
19634
  });
19522
19635
 
19523
19636
  // backend/src/adapters/hooks.ts
19524
- import { join as join10 } from "path";
19637
+ import { join as join11 } from "path";
19525
19638
  function buildErrorMessage(name, exitCode, stdout2, stderr) {
19526
19639
  const output = stderr.trim() || stdout2.trim();
19527
19640
  if (output) {
@@ -19546,7 +19659,7 @@ class BunLifecycleHookRunner {
19546
19659
  return this.direnvAvailable;
19547
19660
  }
19548
19661
  async buildCommand(cwd, command) {
19549
- if (this.checkDirenv() && await Bun.file(join10(cwd, ".envrc")).exists()) {
19662
+ if (this.checkDirenv() && await Bun.file(join11(cwd, ".envrc")).exists()) {
19550
19663
  Bun.spawnSync(["direnv", "allow"], { cwd, stdout: "pipe", stderr: "pipe" });
19551
19664
  return ["direnv", "exec", cwd, "bash", "-c", command];
19552
19665
  }
@@ -19653,7 +19766,7 @@ var init_claude_cli = __esm(() => {
19653
19766
 
19654
19767
  // backend/src/adapters/session-discovery.ts
19655
19768
  import { readdir, stat as stat2 } from "fs/promises";
19656
- import { basename as basename5, join as join11 } from "path";
19769
+ import { basename as basename5, join as join12 } from "path";
19657
19770
  function home() {
19658
19771
  const value = Bun.env.HOME;
19659
19772
  if (!value)
@@ -19664,10 +19777,10 @@ function newestFirst(sessions) {
19664
19777
  return sessions.sort((left, right) => right.mtimeMs - left.mtimeMs).map((entry) => entry.sessionId);
19665
19778
  }
19666
19779
  async function listClaudeSessionIds(cwd) {
19667
- const dir = join11(home(), ".claude", "projects", encodeClaudeProjectDir(cwd));
19780
+ const dir = join12(home(), ".claude", "projects", encodeClaudeProjectDir(cwd));
19668
19781
  const names = await readdir(dir).catch(() => []);
19669
19782
  const stamped = await Promise.all(names.filter((name) => name.endsWith(".jsonl")).map(async (name) => {
19670
- const info = await stat2(join11(dir, name)).catch(() => null);
19783
+ const info = await stat2(join12(dir, name)).catch(() => null);
19671
19784
  return info ? { sessionId: basename5(name, ".jsonl"), mtimeMs: info.mtimeMs } : null;
19672
19785
  }));
19673
19786
  return newestFirst(stamped.filter((entry) => entry !== null));
@@ -19689,14 +19802,14 @@ async function readCodexSessionCwdId(path) {
19689
19802
  }
19690
19803
  }
19691
19804
  async function listCodexSessionIds(cwd) {
19692
- const root = join11(home(), ".codex", "sessions");
19805
+ const root = join12(home(), ".codex", "sessions");
19693
19806
  const relPaths = await readdir(root, { recursive: true }).catch(() => []);
19694
19807
  const rollouts = relPaths.filter((rel) => {
19695
19808
  const name = basename5(rel);
19696
19809
  return name.startsWith("rollout-") && name.endsWith(".jsonl");
19697
19810
  });
19698
19811
  const stamped = await Promise.all(rollouts.map(async (rel) => {
19699
- const path = join11(root, rel);
19812
+ const path = join12(root, rel);
19700
19813
  const meta = await readCodexSessionCwdId(path);
19701
19814
  if (!meta || meta.cwd !== cwd)
19702
19815
  return null;
@@ -19886,7 +19999,7 @@ var init_archive_state_service = __esm(() => {
19886
19999
 
19887
20000
  // backend/src/adapters/agent-runtime.ts
19888
20001
  import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
19889
- import { dirname as dirname5, join as join12, resolve as resolve8 } from "path";
20002
+ import { dirname as dirname6, join as join13, resolve as resolve8 } from "path";
19890
20003
  function shellQuote(value) {
19891
20004
  return `'${value.replaceAll("'", "'\\''")}'`;
19892
20005
  }
@@ -20305,7 +20418,7 @@ async function mergeCodexHooksFile(hooksPath, hookSettings, agentCtlPath) {
20305
20418
  }
20306
20419
  async function resolveGitCommonDir(gitDir) {
20307
20420
  try {
20308
- const commonDir = (await Bun.file(join12(gitDir, "commondir")).text()).trim();
20421
+ const commonDir = (await Bun.file(join13(gitDir, "commondir")).text()).trim();
20309
20422
  if (!commonDir)
20310
20423
  return gitDir;
20311
20424
  return commonDir.startsWith("/") ? commonDir : resolve8(gitDir, commonDir);
@@ -20315,7 +20428,7 @@ async function resolveGitCommonDir(gitDir) {
20315
20428
  }
20316
20429
  async function ensureGeneratedCodexHooksIgnored(gitDir) {
20317
20430
  const commonDir = await resolveGitCommonDir(gitDir);
20318
- const excludePath = join12(commonDir, "info", "exclude");
20431
+ const excludePath = join13(commonDir, "info", "exclude");
20319
20432
  let existing = "";
20320
20433
  try {
20321
20434
  existing = await Bun.file(excludePath).text();
@@ -20325,7 +20438,7 @@ async function ensureGeneratedCodexHooksIgnored(gitDir) {
20325
20438
  const lines = existing.split(/\r?\n/).map((line) => line.trim());
20326
20439
  if (lines.includes(GENERATED_CODEX_HOOKS_EXCLUDE))
20327
20440
  return;
20328
- await mkdir3(dirname5(excludePath), { recursive: true });
20441
+ await mkdir3(dirname6(excludePath), { recursive: true });
20329
20442
  const separator = existing.length > 0 && !existing.endsWith(`
20330
20443
  `) ? `
20331
20444
  ` : "";
@@ -20335,12 +20448,12 @@ async function ensureGeneratedCodexHooksIgnored(gitDir) {
20335
20448
  async function ensureAgentRuntimeArtifacts(input) {
20336
20449
  const storagePaths = getWorktreeStoragePaths(input.gitDir);
20337
20450
  const artifacts = {
20338
- agentCtlPath: join12(storagePaths.webmuxDir, "webmux-agentctl"),
20339
- claudeSettingsPath: join12(input.worktreePath, ".claude", "settings.local.json"),
20340
- codexHooksPath: join12(input.worktreePath, ".codex", "hooks.json")
20451
+ agentCtlPath: join13(storagePaths.webmuxDir, "webmux-agentctl"),
20452
+ claudeSettingsPath: join13(input.worktreePath, ".claude", "settings.local.json"),
20453
+ codexHooksPath: join13(input.worktreePath, ".codex", "hooks.json")
20341
20454
  };
20342
- await mkdir3(dirname5(artifacts.claudeSettingsPath), { recursive: true });
20343
- await mkdir3(dirname5(artifacts.codexHooksPath), { recursive: true });
20455
+ await mkdir3(dirname6(artifacts.claudeSettingsPath), { recursive: true });
20456
+ await mkdir3(dirname6(artifacts.codexHooksPath), { recursive: true });
20344
20457
  await Bun.write(artifacts.agentCtlPath, buildAgentCtlScript());
20345
20458
  await chmod2(artifacts.agentCtlPath, 493);
20346
20459
  const hookSettings = buildClaudeHookSettings(artifacts);
@@ -20872,7 +20985,7 @@ var init_worktree_service = __esm(() => {
20872
20985
  // backend/src/services/lifecycle-service.ts
20873
20986
  import { randomUUID as randomUUID3 } from "crypto";
20874
20987
  import { mkdir as mkdir4 } from "fs/promises";
20875
- import { dirname as dirname6, resolve as resolve10 } from "path";
20988
+ import { dirname as dirname7, resolve as resolve10 } from "path";
20876
20989
  function toErrorMessage2(error) {
20877
20990
  return error instanceof Error ? error.message : String(error);
20878
20991
  }
@@ -21792,7 +21905,7 @@ ${oneshotPrompt}` : oneshotPrompt ?? baseSystemPrompt;
21792
21905
  ...createProgressBase,
21793
21906
  phase: "creating_worktree"
21794
21907
  });
21795
- await mkdir4(dirname6(worktreePath), { recursive: true });
21908
+ await mkdir4(dirname7(worktreePath), { recursive: true });
21796
21909
  initialized = await createManagedWorktree({
21797
21910
  repoRoot: this.deps.projectRoot,
21798
21911
  worktreePath,
@@ -23346,13 +23459,13 @@ var init_worktree_commands = __esm(() => {
23346
23459
  });
23347
23460
 
23348
23461
  // bin/src/webmux.ts
23349
- import { resolve as resolve13, dirname as dirname7, join as join13 } from "path";
23462
+ import { resolve as resolve13, dirname as dirname8, join as join14 } from "path";
23350
23463
  import { existsSync as existsSync6 } from "fs";
23351
23464
  import { fileURLToPath } from "url";
23352
23465
  // package.json
23353
23466
  var package_default = {
23354
23467
  name: "webmux",
23355
- version: "0.40.0",
23468
+ version: "0.41.0",
23356
23469
  description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
23357
23470
  type: "module",
23358
23471
  repository: {
@@ -23409,7 +23522,7 @@ var package_default = {
23409
23522
  };
23410
23523
 
23411
23524
  // bin/src/webmux.ts
23412
- var PKG_ROOT = resolve13(dirname7(fileURLToPath(import.meta.url)), "..");
23525
+ var PKG_ROOT = resolve13(dirname8(fileURLToPath(import.meta.url)), "..");
23413
23526
  function usage2() {
23414
23527
  console.log(`
23415
23528
  webmux \u2014 Dev dashboard for managing Git worktrees
@@ -23646,6 +23759,8 @@ Refreshing ${services.length} installed webmux service(s) to pick up the new ver
23646
23759
  const parts = [];
23647
23760
  if (outcome.regenerated)
23648
23761
  parts.push("regenerated unit");
23762
+ if (outcome.migratedProject)
23763
+ parts.push(`migrated served project ${outcome.migratedProject}`);
23649
23764
  if (outcome.restarted)
23650
23765
  parts.push("restarted");
23651
23766
  if (!outcome.regenerated && !outcome.restarted && !outcome.error)
@@ -23733,8 +23848,8 @@ Refreshing ${services.length} installed webmux service(s) to pick up the new ver
23733
23848
  }
23734
23849
  process.on("SIGINT", cleanup);
23735
23850
  process.on("SIGTERM", cleanup);
23736
- const backendEntry = join13(PKG_ROOT, "backend", "dist", "server.js");
23737
- const staticDir = join13(PKG_ROOT, "frontend", "dist");
23851
+ const backendEntry = join14(PKG_ROOT, "backend", "dist", "server.js");
23852
+ const staticDir = join14(PKG_ROOT, "frontend", "dist");
23738
23853
  if (!existsSync6(staticDir)) {
23739
23854
  console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
23740
23855
  process.exit(1);