webmux 0.39.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
@@ -473,7 +473,8 @@ _webmux() {
473
473
  'remove:Remove a worktree'
474
474
  'merge:Merge a worktree into main'
475
475
  'send:Send a prompt to a running worktree agent'
476
- 'prune:Remove all worktrees in the current project'
476
+ 'prune:Remove all closed (not open) worktrees in the current project'
477
+ 'restore:Re-open all worktree sessions that were open before'
477
478
  'linear:Post a worktree conversation to a Linear issue/team'
478
479
  'project:List, add, or remove projects served by the dashboard'
479
480
  'completion:Generate shell completion script'
@@ -543,7 +544,7 @@ compdef _webmux webmux`, BASH_SCRIPT = `_webmux() {
543
544
  prev="\${COMP_WORDS[COMP_CWORD-1]}"
544
545
 
545
546
  if [[ \${COMP_CWORD} -eq 1 ]]; then
546
- COMPREPLY=($(compgen -W "serve init service update add oneshot list open close refresh archive unarchive label remove merge send prune linear project completion" -- "\${cur}"))
547
+ COMPREPLY=($(compgen -W "serve init service update add oneshot list open close refresh archive unarchive label remove merge send prune restore linear project completion" -- "\${cur}"))
547
548
  return
548
549
  fi
549
550
 
@@ -5996,7 +5997,7 @@ var init_zod = __esm(() => {
5996
5997
  init_external();
5997
5998
  });
5998
5999
 
5999
- // node_modules/.bun/@ts-rest+core@3.52.1+f5aac5c7d31a6dcb/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
6000
6001
  var isZodType = (obj) => {
6001
6002
  return typeof (obj === null || obj === undefined ? undefined : obj.safeParse) === "function";
6002
6003
  }, isZodObjectStrict = (obj) => {
@@ -8338,21 +8339,110 @@ ${result.stderr.trim()}` : ""
8338
8339
  console.log();
8339
8340
  });
8340
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
+
8341
8428
  // bin/src/service.ts
8342
8429
  var exports_service = {};
8343
8430
  __export(exports_service, {
8431
+ shouldPersistProject: () => shouldPersistProject,
8344
8432
  resolveEnvVars: () => resolveEnvVars,
8433
+ resolveConfirmDecision: () => resolveConfirmDecision,
8345
8434
  readPortFromUnit: () => readPortFromUnit,
8346
8435
  readEnvVarsFromUnit: () => readEnvVarsFromUnit,
8347
8436
  parseInstalledServiceConfig: () => parseInstalledServiceConfig,
8348
8437
  parseEnvCliArgs: () => parseEnvCliArgs,
8438
+ migrateServedRepoFromUnit: () => migrateServedRepoFromUnit,
8349
8439
  generateServiceFile: () => generateServiceFile,
8350
8440
  default: () => service,
8351
8441
  AUTO_PICKUP_ENV_VARS: () => AUTO_PICKUP_ENV_VARS
8352
8442
  });
8353
- import { chmodSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync3, unlinkSync } from "fs";
8354
- import { basename as basename3, join as join5 } from "path";
8355
- 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";
8356
8446
  function getPlatform() {
8357
8447
  const plat = process.platform;
8358
8448
  if (plat === "linux" || plat === "darwin")
@@ -8365,6 +8455,33 @@ function resolveWebmuxPath() {
8365
8455
  return null;
8366
8456
  return result.stdout.toString().trim();
8367
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
+ }
8368
8485
  function formatCommand([bin, args]) {
8369
8486
  return [bin, ...args].join(" ");
8370
8487
  }
@@ -8378,10 +8495,10 @@ function printRunResult(result) {
8378
8495
  console.error(err);
8379
8496
  }
8380
8497
  function systemdUnitPath(serviceName) {
8381
- return join5(homedir(), ".config", "systemd", "user", `${serviceName}.service`);
8498
+ return join6(homedir2(), ".config", "systemd", "user", `${serviceName}.service`);
8382
8499
  }
8383
8500
  function launchdPlistPath(serviceName) {
8384
- return join5(homedir(), "Library", "LaunchAgents", `com.webmux.${serviceName}.plist`);
8501
+ return join6(homedir2(), "Library", "LaunchAgents", `com.webmux.${serviceName}.plist`);
8385
8502
  }
8386
8503
  function serviceFilePath(config) {
8387
8504
  if (config.platform === "linux")
@@ -8392,16 +8509,15 @@ function generateSystemdUnit(config) {
8392
8509
  const extra = Object.keys(config.envVars).sort().map((key) => `Environment=${key}=${config.envVars[key]}`).join(`
8393
8510
  `);
8394
8511
  return `[Unit]
8395
- Description=webmux dashboard \u2014 ${config.projectName}
8512
+ Description=webmux dashboard
8396
8513
 
8397
8514
  [Service]
8398
8515
  Type=simple
8399
8516
  ExecStart=${config.webmuxPath} serve --port ${config.port}
8400
- WorkingDirectory=${config.projectDir}
8517
+ WorkingDirectory=${homedir2()}
8401
8518
  Restart=on-failure
8402
8519
  RestartSec=5
8403
8520
  Environment=PORT=${config.port}
8404
- Environment=WEBMUX_PROJECT_DIR=${config.projectDir}
8405
8521
  Environment=PATH=${process.env.PATH}${extra ? `
8406
8522
  ` + extra : ""}
8407
8523
 
@@ -8413,7 +8529,7 @@ function escapePlistText(value) {
8413
8529
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
8414
8530
  }
8415
8531
  function generateLaunchdPlist(config) {
8416
- const logPath = join5(homedir(), "Library", "Logs", `webmux-${config.serviceName}.log`);
8532
+ const logPath = join6(homedir2(), "Library", "Logs", `webmux-${config.serviceName}.log`);
8417
8533
  const extra = Object.keys(config.envVars).sort().map((key) => ` <key>${escapePlistText(key)}</key>
8418
8534
  <string>${escapePlistText(config.envVars[key])}</string>`).join(`
8419
8535
  `);
@@ -8431,7 +8547,7 @@ function generateLaunchdPlist(config) {
8431
8547
  <string>${config.port}</string>
8432
8548
  </array>
8433
8549
  <key>WorkingDirectory</key>
8434
- <string>${config.projectDir}</string>
8550
+ <string>${homedir2()}</string>
8435
8551
  <key>RunAtLoad</key>
8436
8552
  <true/>
8437
8553
  <key>KeepAlive</key>
@@ -8447,8 +8563,6 @@ function generateLaunchdPlist(config) {
8447
8563
  <dict>
8448
8564
  <key>PORT</key>
8449
8565
  <string>${config.port}</string>
8450
- <key>WEBMUX_PROJECT_DIR</key>
8451
- <string>${config.projectDir}</string>
8452
8566
  <key>PATH</key>
8453
8567
  <string>${process.env.PATH}</string>${extra ? `
8454
8568
  ` + extra : ""}
@@ -8465,7 +8579,7 @@ function generateServiceFile(config) {
8465
8579
  function readWorkingDirFromUnit(filePath, platform) {
8466
8580
  let text;
8467
8581
  try {
8468
- text = readFileSync3(filePath, "utf8");
8582
+ text = readFileSync4(filePath, "utf8");
8469
8583
  } catch {
8470
8584
  return null;
8471
8585
  }
@@ -8476,7 +8590,7 @@ function readWorkingDirFromUnit(filePath, platform) {
8476
8590
  function readPortFromUnit(filePath) {
8477
8591
  let text;
8478
8592
  try {
8479
- text = readFileSync3(filePath, "utf8");
8593
+ text = readFileSync4(filePath, "utf8");
8480
8594
  } catch {
8481
8595
  return null;
8482
8596
  }
@@ -8490,7 +8604,7 @@ function unescapePlistText(value) {
8490
8604
  function readEnvVarsFromUnit(filePath, platform) {
8491
8605
  let text;
8492
8606
  try {
8493
- text = readFileSync3(filePath, "utf8");
8607
+ text = readFileSync4(filePath, "utf8");
8494
8608
  } catch {
8495
8609
  return {};
8496
8610
  }
@@ -8519,19 +8633,13 @@ function parseInstalledServiceConfig(filePath, platform, webmuxPath) {
8519
8633
  const port = readPortFromUnit(filePath);
8520
8634
  if (port === null)
8521
8635
  return null;
8522
- const projectDir = readWorkingDirFromUnit(filePath, platform);
8523
- if (projectDir === null)
8524
- return null;
8525
8636
  const fileBase = basename3(filePath);
8526
8637
  const serviceName = platform === "linux" ? fileBase.replace(/\.service$/, "") : fileBase.replace(/^com\.webmux\./, "").replace(/\.plist$/, "");
8527
- const projectName = detectProjectName(projectDir);
8528
8638
  const envVars = readEnvVarsFromUnit(filePath, platform);
8529
8639
  return {
8530
8640
  platform,
8531
- projectName,
8532
8641
  serviceName,
8533
8642
  webmuxPath,
8534
- projectDir,
8535
8643
  port,
8536
8644
  envVars
8537
8645
  };
@@ -8626,19 +8734,19 @@ function redactSecretsInUnit(content, envVars) {
8626
8734
  }
8627
8735
  return out;
8628
8736
  }
8629
- 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) {
8630
8748
  const filePath = serviceFilePath(config);
8631
8749
  const alreadyInstalled = isInstalled(config);
8632
- if (alreadyInstalled) {
8633
- const reinstall = await confirm({ message: "Service is already installed. Reinstall?" });
8634
- if (isCancel(reinstall) || !reinstall) {
8635
- log.info("Aborted.");
8636
- return;
8637
- }
8638
- for (const cmd of uninstallCommands(config)) {
8639
- runCommand(cmd);
8640
- }
8641
- }
8642
8750
  const requestedPort = config.port;
8643
8751
  let chosenPort = requestedPort;
8644
8752
  let portNote = null;
@@ -8652,14 +8760,17 @@ async function install(config, portExplicit, envVarNotes) {
8652
8760
  config = { ...config, port: chosenPort };
8653
8761
  const content = generateServiceFile(config);
8654
8762
  const commands = installCommands(config);
8763
+ const persistPath = cwdProjectToPersist();
8655
8764
  const displayContent = redactSecretsInUnit(content, config.envVars);
8656
8765
  note([
8766
+ ...alreadyInstalled ? ["Service is already installed \u2014 this will reinstall it.", ""] : [],
8657
8767
  `File: ${filePath}`,
8658
8768
  "",
8659
8769
  "Contents:",
8660
8770
  displayContent,
8661
8771
  "Commands to run:",
8662
- ...commands.map((c4) => ` $ ${formatCommand(c4)}`)
8772
+ ...commands.map((c4) => ` $ ${formatCommand(c4)}`),
8773
+ ...persistPath ? ["", `Will also register this project: ${persistPath}`] : []
8663
8774
  ].join(`
8664
8775
  `), "Install service");
8665
8776
  if (Object.keys(config.envVars).length > 0) {
@@ -8669,12 +8780,28 @@ ${envVarNotes.join(`
8669
8780
  }
8670
8781
  if (portNote)
8671
8782
  log.info(portNote);
8672
- const ok = await confirm({ message: "Proceed?" });
8673
- if (isCancel(ok) || !ok) {
8674
- 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.");
8675
8786
  return;
8676
8787
  }
8677
- 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 });
8678
8805
  await Bun.write(filePath, content);
8679
8806
  if (Object.keys(config.envVars).length > 0) {
8680
8807
  try {
@@ -8684,6 +8811,14 @@ ${envVarNotes.join(`
8684
8811
  }
8685
8812
  }
8686
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
+ }
8687
8822
  for (const cmd of commands) {
8688
8823
  const result = runCommand(cmd);
8689
8824
  if (!result.success) {
@@ -8755,7 +8890,7 @@ function logs(config) {
8755
8890
  if (config.platform === "linux") {
8756
8891
  proc = Bun.spawn(["journalctl", "--user", "-u", config.serviceName, "-f", "--no-pager"], { stdout: "inherit", stderr: "inherit" });
8757
8892
  } else {
8758
- const logPath = join5(homedir(), "Library", "Logs", `webmux-${config.serviceName}.log`);
8893
+ const logPath = join6(homedir2(), "Library", "Logs", `webmux-${config.serviceName}.log`);
8759
8894
  if (!existsSync4(logPath)) {
8760
8895
  log.error(`Log file not found: ${logPath}`);
8761
8896
  return;
@@ -8784,6 +8919,9 @@ Usage:
8784
8919
  Options:
8785
8920
  --port N Pin the service to a port (default: 5111). On
8786
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.
8787
8925
  --env KEY=VALUE Bake an environment variable into the service
8788
8926
  unit (repeatable). Reserved keys PORT,
8789
8927
  WEBMUX_PROJECT_DIR, and PATH are rejected.
@@ -8812,11 +8950,6 @@ async function service(args) {
8812
8950
  log.error(`Unsupported platform: ${process.platform}. Only linux and macOS are supported.`);
8813
8951
  return;
8814
8952
  }
8815
- const gitRoot2 = getGitRoot();
8816
- if (!gitRoot2) {
8817
- log.error("Not inside a git repository.");
8818
- return;
8819
- }
8820
8953
  const serviceManager = platform === "linux" ? "systemctl" : "launchctl";
8821
8954
  const smResult = run("which", [serviceManager]);
8822
8955
  if (!smResult.success) {
@@ -8831,6 +8964,7 @@ async function service(args) {
8831
8964
  let port = parseInt(process.env.PORT || "5111");
8832
8965
  let portExplicit = false;
8833
8966
  let autoPickup = true;
8967
+ let autoConfirm = false;
8834
8968
  for (let i2 = 1;i2 < args.length; i2++) {
8835
8969
  if (args[i2] === "--port" && args[i2 + 1]) {
8836
8970
  const parsed = parseInt(args[++i2]);
@@ -8842,6 +8976,8 @@ async function service(args) {
8842
8976
  portExplicit = true;
8843
8977
  } else if (args[i2] === "--no-auto-env") {
8844
8978
  autoPickup = false;
8979
+ } else if (args[i2] === "--yes" || args[i2] === "-y") {
8980
+ autoConfirm = true;
8845
8981
  }
8846
8982
  }
8847
8983
  const cliEnv = parseEnvCliArgs(args.slice(1));
@@ -8850,7 +8986,6 @@ async function service(args) {
8850
8986
  log.error(err);
8851
8987
  return;
8852
8988
  }
8853
- const projectName = detectProjectName(gitRoot2);
8854
8989
  const serviceName = "webmux";
8855
8990
  let envVars = {};
8856
8991
  let envVarNotes = [];
@@ -8867,16 +9002,14 @@ async function service(args) {
8867
9002
  }
8868
9003
  const config = {
8869
9004
  platform,
8870
- projectName,
8871
9005
  serviceName,
8872
9006
  webmuxPath,
8873
- projectDir: gitRoot2,
8874
9007
  port,
8875
9008
  envVars
8876
9009
  };
8877
9010
  switch (action) {
8878
9011
  case "install":
8879
- await install(config, portExplicit, envVarNotes);
9012
+ await install(config, portExplicit, envVarNotes, autoConfirm);
8880
9013
  break;
8881
9014
  case "uninstall":
8882
9015
  await uninstall(config);
@@ -8897,6 +9030,7 @@ var AUTO_PICKUP_ENV_VARS, RESERVED_ENV_KEYS, SYSTEMD_WORKDIR_RE, LAUNCHD_WORKDIR
8897
9030
  var init_service = __esm(() => {
8898
9031
  init_dist4();
8899
9032
  init_shared();
9033
+ init_projects_registry();
8900
9034
  AUTO_PICKUP_ENV_VARS = ["LINEAR_API_KEY"];
8901
9035
  RESERVED_ENV_KEYS = new Set(["PORT", "WEBMUX_PROJECT_DIR", "PATH"]);
8902
9036
  SYSTEMD_WORKDIR_RE = /^WorkingDirectory=(.+)$/m;
@@ -8916,9 +9050,9 @@ __export(exports_service_restart, {
8916
9050
  restartCommand: () => restartCommand,
8917
9051
  listInstalledServices: () => listInstalledServices
8918
9052
  });
8919
- import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "fs";
8920
- import { homedir as homedir2 } from "os";
8921
- 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";
8922
9056
  function listInstalledServices(opts = {}) {
8923
9057
  const out = [];
8924
9058
  const systemdDir = opts.systemdDir ?? DEFAULT_SYSTEMD_DIR;
@@ -8930,7 +9064,7 @@ function listInstalledServices(opts = {}) {
8930
9064
  continue;
8931
9065
  out.push({
8932
9066
  name: name.slice(0, -".service".length),
8933
- filePath: join6(systemdDir, name),
9067
+ filePath: join7(systemdDir, name),
8934
9068
  platform: "linux"
8935
9069
  });
8936
9070
  }
@@ -8943,7 +9077,7 @@ function listInstalledServices(opts = {}) {
8943
9077
  continue;
8944
9078
  out.push({
8945
9079
  name: name.slice(0, -".plist".length),
8946
- filePath: join6(launchdDir, name),
9080
+ filePath: join7(launchdDir, name),
8947
9081
  platform: "darwin"
8948
9082
  });
8949
9083
  }
@@ -8986,17 +9120,19 @@ function reloadAfterRegenerate(service2, runner) {
8986
9120
  service is now unloaded \u2014 recover with: launchctl load -w "${service2.filePath}"`
8987
9121
  };
8988
9122
  }
8989
- async function updateInstalledService(service2, webmuxPath, runner = defaultRunner) {
9123
+ async function updateInstalledService(service2, webmuxPath, runner = defaultRunner, migrate = migrateServedRepoFromUnit) {
8990
9124
  const canRegenerate = webmuxPath.length > 0;
8991
9125
  const config = canRegenerate ? parseInstalledServiceConfig(service2.filePath, service2.platform, webmuxPath) : null;
8992
9126
  let regenerated = false;
9127
+ let migratedProject;
8993
9128
  if (config !== null) {
8994
9129
  let currentContent = "";
8995
9130
  try {
8996
- currentContent = readFileSync4(service2.filePath, "utf8");
9131
+ currentContent = readFileSync5(service2.filePath, "utf8");
8997
9132
  } catch {}
8998
9133
  const expected = generateServiceFile(config);
8999
9134
  if (currentContent !== expected) {
9135
+ migratedProject = migrate(service2.filePath, service2.platform) ?? undefined;
9000
9136
  try {
9001
9137
  await Bun.write(service2.filePath, expected);
9002
9138
  regenerated = true;
@@ -9005,6 +9141,7 @@ async function updateInstalledService(service2, webmuxPath, runner = defaultRunn
9005
9141
  service: service2,
9006
9142
  regenerated: false,
9007
9143
  restarted: false,
9144
+ migratedProject,
9008
9145
  error: `could not rewrite ${service2.filePath}: ${String(err)}`
9009
9146
  };
9010
9147
  }
@@ -9013,16 +9150,17 @@ async function updateInstalledService(service2, webmuxPath, runner = defaultRunn
9013
9150
  if (regenerated) {
9014
9151
  const reload = reloadAfterRegenerate(service2, runner);
9015
9152
  if (!reload.ok) {
9016
- return { service: service2, regenerated, restarted: false, error: reload.error };
9153
+ return { service: service2, regenerated, restarted: false, migratedProject, error: reload.error };
9017
9154
  }
9018
9155
  if (service2.platform === "darwin") {
9019
- return { service: service2, regenerated, restarted: true };
9156
+ return { service: service2, regenerated, restarted: true, migratedProject };
9020
9157
  }
9021
9158
  }
9022
9159
  const outcome = restartInstalledService(service2, runner);
9023
9160
  return {
9024
9161
  service: service2,
9025
9162
  regenerated,
9163
+ migratedProject,
9026
9164
  restarted: outcome.ok,
9027
9165
  error: outcome.error
9028
9166
  };
@@ -9032,40 +9170,16 @@ var init_service_restart = __esm(() => {
9032
9170
  init_shared();
9033
9171
  init_service();
9034
9172
  defaultRunner = { run };
9035
- DEFAULT_SYSTEMD_DIR = join6(homedir2(), ".config", "systemd", "user");
9036
- DEFAULT_LAUNCHD_DIR = join6(homedir2(), "Library", "LaunchAgents");
9037
- });
9038
-
9039
- // backend/src/lib/log.ts
9040
- function ts() {
9041
- return new Date().toISOString().slice(11, 23);
9042
- }
9043
- var DEBUG, log2;
9044
- var init_log = __esm(() => {
9045
- DEBUG = Bun.env.WEBMUX_DEBUG === "1";
9046
- log2 = {
9047
- info(msg) {
9048
- console.log(`[${ts()}] ${msg}`);
9049
- },
9050
- debug(msg) {
9051
- if (DEBUG)
9052
- console.log(`[${ts()}] ${msg}`);
9053
- },
9054
- warn(msg) {
9055
- console.warn(`[${ts()}] ${msg}`);
9056
- },
9057
- error(msg, err) {
9058
- err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
9059
- }
9060
- };
9173
+ DEFAULT_SYSTEMD_DIR = join7(homedir3(), ".config", "systemd", "user");
9174
+ DEFAULT_LAUNCHD_DIR = join7(homedir3(), "Library", "LaunchAgents");
9061
9175
  });
9062
9176
 
9063
9177
  // backend/src/adapters/instance-registry.ts
9064
- import { mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync5, renameSync, unlinkSync as unlinkSync2, writeFileSync } from "fs";
9065
- import { homedir as homedir3 } from "os";
9066
- 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";
9067
9181
  function defaultRegistryDir() {
9068
- return join7(homedir3(), ".webmux", "instances");
9182
+ return join8(homedir4(), ".webmux", "instances");
9069
9183
  }
9070
9184
  function isAlive(pid) {
9071
9185
  try {
@@ -9083,14 +9197,14 @@ function isInstanceEntry(value) {
9083
9197
  }
9084
9198
  function createInstanceRegistry(dir = defaultRegistryDir()) {
9085
9199
  function ensureDir() {
9086
- mkdirSync2(dir, { recursive: true });
9200
+ mkdirSync3(dir, { recursive: true });
9087
9201
  }
9088
9202
  function entryPath(port) {
9089
- return join7(dir, `${port}.json`);
9203
+ return join8(dir, `${port}.json`);
9090
9204
  }
9091
9205
  function readEntry(filename) {
9092
9206
  try {
9093
- const raw = readFileSync5(join7(dir, filename), "utf8");
9207
+ const raw = readFileSync6(join8(dir, filename), "utf8");
9094
9208
  const parsed = JSON.parse(raw);
9095
9209
  return isInstanceEntry(parsed) ? parsed : null;
9096
9210
  } catch {
@@ -9104,8 +9218,8 @@ function createInstanceRegistry(dir = defaultRegistryDir()) {
9104
9218
  const tmpPath = `${finalPath}.${process.pid}.${Date.now()}.tmp`;
9105
9219
  const text = `${JSON.stringify(entry, null, 2)}
9106
9220
  `;
9107
- writeFileSync(tmpPath, text);
9108
- renameSync(tmpPath, finalPath);
9221
+ writeFileSync2(tmpPath, text);
9222
+ renameSync2(tmpPath, finalPath);
9109
9223
  },
9110
9224
  deregister(port, expectedPid) {
9111
9225
  if (expectedPid !== undefined) {
@@ -9138,7 +9252,7 @@ function createInstanceRegistry(dir = defaultRegistryDir()) {
9138
9252
  continue;
9139
9253
  if (!isAlive(entry.pid)) {
9140
9254
  try {
9141
- unlinkSync2(join7(dir, filename));
9255
+ unlinkSync2(join8(dir, filename));
9142
9256
  } catch {}
9143
9257
  continue;
9144
9258
  }
@@ -11317,11 +11431,11 @@ function conversationSessionId(conversation) {
11317
11431
  return null;
11318
11432
  return conversation.provider === "codexAppServer" ? conversation.threadId : conversation.sessionId;
11319
11433
  }
11320
- var WORKTREE_META_SCHEMA_VERSION = 1, WORKTREE_ARCHIVE_STATE_VERSION = 1, ROOT_TAB_ID = "root";
11434
+ var WORKTREE_META_SCHEMA_VERSION = 1, WORKTREE_ARCHIVE_STATE_VERSION = 1, OPEN_SESSIONS_STATE_VERSION = 1, ROOT_TAB_ID = "root";
11321
11435
 
11322
11436
  // backend/src/adapters/fs.ts
11323
11437
  import { mkdir } from "fs/promises";
11324
- import { join as join8 } from "path";
11438
+ import { join as join9 } from "path";
11325
11439
  function stringifyAllocatedPorts(ports) {
11326
11440
  const entries = Object.entries(ports).map(([key, value]) => [key, String(value)]);
11327
11441
  return Object.fromEntries(entries);
@@ -11353,25 +11467,28 @@ function parseDotenv(content) {
11353
11467
  }
11354
11468
  async function loadDotenvLocal(worktreePath) {
11355
11469
  try {
11356
- const content = await Bun.file(join8(worktreePath, ".env.local")).text();
11470
+ const content = await Bun.file(join9(worktreePath, ".env.local")).text();
11357
11471
  return parseDotenv(content);
11358
11472
  } catch {
11359
11473
  return {};
11360
11474
  }
11361
11475
  }
11362
11476
  function getWorktreeStoragePaths(gitDir) {
11363
- const webmuxDir = join8(gitDir, "webmux");
11477
+ const webmuxDir = join9(gitDir, "webmux");
11364
11478
  return {
11365
11479
  gitDir,
11366
11480
  webmuxDir,
11367
- metaPath: join8(webmuxDir, "meta.json"),
11368
- runtimeEnvPath: join8(webmuxDir, "runtime.env"),
11369
- controlEnvPath: join8(webmuxDir, "control.env"),
11370
- 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")
11371
11485
  };
11372
11486
  }
11373
11487
  function getProjectArchiveStatePath(gitDir) {
11374
- return join8(gitDir, "webmux", "archive.json");
11488
+ return join9(gitDir, "webmux", "archive.json");
11489
+ }
11490
+ function getProjectOpenSessionsStatePath(gitDir) {
11491
+ return join9(gitDir, "webmux", "open-sessions.json");
11375
11492
  }
11376
11493
  async function ensureWorktreeStorageDirs(gitDir) {
11377
11494
  const paths = getWorktreeStoragePaths(gitDir);
@@ -11422,6 +11539,29 @@ async function writeWorktreeArchiveState(gitDir, state) {
11422
11539
  await Bun.write(archivePath, JSON.stringify(state, null, 2) + `
11423
11540
  `);
11424
11541
  }
11542
+ function emptyOpenSessionsState() {
11543
+ return {
11544
+ schemaVersion: OPEN_SESSIONS_STATE_VERSION,
11545
+ savedAt: "",
11546
+ branches: []
11547
+ };
11548
+ }
11549
+ function isOpenSessionsState(raw) {
11550
+ return isRecord3(raw) && typeof raw.schemaVersion === "number" && typeof raw.savedAt === "string" && Array.isArray(raw.branches) && raw.branches.every((branch) => typeof branch === "string");
11551
+ }
11552
+ async function readOpenSessionsState(gitDir) {
11553
+ const statePath = getProjectOpenSessionsStatePath(gitDir);
11554
+ try {
11555
+ const raw = await Bun.file(statePath).json();
11556
+ return isOpenSessionsState(raw) ? {
11557
+ schemaVersion: raw.schemaVersion,
11558
+ savedAt: raw.savedAt,
11559
+ branches: [...raw.branches]
11560
+ } : emptyOpenSessionsState();
11561
+ } catch {
11562
+ return emptyOpenSessionsState();
11563
+ }
11564
+ }
11425
11565
  function buildRuntimeEnvMap(meta, extraEnv = {}, dotenvValues = {}) {
11426
11566
  return {
11427
11567
  ...dotenvValues,
@@ -18756,8 +18896,8 @@ var init_dist5 = __esm(() => {
18756
18896
  });
18757
18897
 
18758
18898
  // backend/src/adapters/config.ts
18759
- import { readFileSync as readFileSync6 } from "fs";
18760
- 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";
18761
18901
  function DEFAULT_ONESHOT_SYSTEM_PROMPT() {
18762
18902
  return [
18763
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.",
@@ -18986,10 +19126,10 @@ function getDefaultProfileName(config) {
18986
19126
  return Object.keys(config.profiles)[0] ?? "default";
18987
19127
  }
18988
19128
  function readConfigFile(root) {
18989
- return readFileSync6(join9(root, ".webmux.yaml"), "utf8");
19129
+ return readFileSync7(join10(root, ".webmux.yaml"), "utf8");
18990
19130
  }
18991
19131
  function readLocalConfigFile(root) {
18992
- return readFileSync6(join9(root, ".webmux.local.yaml"), "utf8");
19132
+ return readFileSync7(join10(root, ".webmux.local.yaml"), "utf8");
18993
19133
  }
18994
19134
  function parseConfigDocument(text) {
18995
19135
  const parsed = $parse(text);
@@ -19137,7 +19277,7 @@ function projectRoot(dir) {
19137
19277
  if (result.exitCode !== 0)
19138
19278
  return gitRoot2(dir);
19139
19279
  const commonDir = new TextDecoder().decode(result.stdout).trim();
19140
- return commonDir ? dirname3(resolve7(dir, commonDir)) : gitRoot2(dir);
19280
+ return commonDir ? dirname4(resolve7(dir, commonDir)) : gitRoot2(dir);
19141
19281
  }
19142
19282
  function loadConfig(dir, options = {}) {
19143
19283
  const root = options.resolvedRoot ? dir : projectRoot(dir);
@@ -19216,7 +19356,7 @@ var init_config = __esm(() => {
19216
19356
 
19217
19357
  // backend/src/adapters/control-token.ts
19218
19358
  import { chmod, mkdir as mkdir2 } from "fs/promises";
19219
- import { dirname as dirname4 } from "path";
19359
+ import { dirname as dirname5 } from "path";
19220
19360
  async function loadControlToken() {
19221
19361
  if (cachedToken)
19222
19362
  return cachedToken;
@@ -19226,7 +19366,7 @@ async function loadControlToken() {
19226
19366
  return cachedToken;
19227
19367
  }
19228
19368
  const controlToken = crypto.randomUUID();
19229
- await mkdir2(dirname4(CONTROL_TOKEN_PATH), { recursive: true });
19369
+ await mkdir2(dirname5(CONTROL_TOKEN_PATH), { recursive: true });
19230
19370
  await Bun.write(CONTROL_TOKEN_PATH, controlToken);
19231
19371
  await chmod(CONTROL_TOKEN_PATH, 384);
19232
19372
  cachedToken = controlToken;
@@ -19494,7 +19634,7 @@ var init_docker = __esm(() => {
19494
19634
  });
19495
19635
 
19496
19636
  // backend/src/adapters/hooks.ts
19497
- import { join as join10 } from "path";
19637
+ import { join as join11 } from "path";
19498
19638
  function buildErrorMessage(name, exitCode, stdout2, stderr) {
19499
19639
  const output = stderr.trim() || stdout2.trim();
19500
19640
  if (output) {
@@ -19519,7 +19659,7 @@ class BunLifecycleHookRunner {
19519
19659
  return this.direnvAvailable;
19520
19660
  }
19521
19661
  async buildCommand(cwd, command) {
19522
- if (this.checkDirenv() && await Bun.file(join10(cwd, ".envrc")).exists()) {
19662
+ if (this.checkDirenv() && await Bun.file(join11(cwd, ".envrc")).exists()) {
19523
19663
  Bun.spawnSync(["direnv", "allow"], { cwd, stdout: "pipe", stderr: "pipe" });
19524
19664
  return ["direnv", "exec", cwd, "bash", "-c", command];
19525
19665
  }
@@ -19626,7 +19766,7 @@ var init_claude_cli = __esm(() => {
19626
19766
 
19627
19767
  // backend/src/adapters/session-discovery.ts
19628
19768
  import { readdir, stat as stat2 } from "fs/promises";
19629
- import { basename as basename5, join as join11 } from "path";
19769
+ import { basename as basename5, join as join12 } from "path";
19630
19770
  function home() {
19631
19771
  const value = Bun.env.HOME;
19632
19772
  if (!value)
@@ -19637,10 +19777,10 @@ function newestFirst(sessions) {
19637
19777
  return sessions.sort((left, right) => right.mtimeMs - left.mtimeMs).map((entry) => entry.sessionId);
19638
19778
  }
19639
19779
  async function listClaudeSessionIds(cwd) {
19640
- const dir = join11(home(), ".claude", "projects", encodeClaudeProjectDir(cwd));
19780
+ const dir = join12(home(), ".claude", "projects", encodeClaudeProjectDir(cwd));
19641
19781
  const names = await readdir(dir).catch(() => []);
19642
19782
  const stamped = await Promise.all(names.filter((name) => name.endsWith(".jsonl")).map(async (name) => {
19643
- const info = await stat2(join11(dir, name)).catch(() => null);
19783
+ const info = await stat2(join12(dir, name)).catch(() => null);
19644
19784
  return info ? { sessionId: basename5(name, ".jsonl"), mtimeMs: info.mtimeMs } : null;
19645
19785
  }));
19646
19786
  return newestFirst(stamped.filter((entry) => entry !== null));
@@ -19662,14 +19802,14 @@ async function readCodexSessionCwdId(path) {
19662
19802
  }
19663
19803
  }
19664
19804
  async function listCodexSessionIds(cwd) {
19665
- const root = join11(home(), ".codex", "sessions");
19805
+ const root = join12(home(), ".codex", "sessions");
19666
19806
  const relPaths = await readdir(root, { recursive: true }).catch(() => []);
19667
19807
  const rollouts = relPaths.filter((rel) => {
19668
19808
  const name = basename5(rel);
19669
19809
  return name.startsWith("rollout-") && name.endsWith(".jsonl");
19670
19810
  });
19671
19811
  const stamped = await Promise.all(rollouts.map(async (rel) => {
19672
- const path = join11(root, rel);
19812
+ const path = join12(root, rel);
19673
19813
  const meta = await readCodexSessionCwdId(path);
19674
19814
  if (!meta || meta.cwd !== cwd)
19675
19815
  return null;
@@ -19859,7 +19999,7 @@ var init_archive_state_service = __esm(() => {
19859
19999
 
19860
20000
  // backend/src/adapters/agent-runtime.ts
19861
20001
  import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
19862
- 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";
19863
20003
  function shellQuote(value) {
19864
20004
  return `'${value.replaceAll("'", "'\\''")}'`;
19865
20005
  }
@@ -20278,7 +20418,7 @@ async function mergeCodexHooksFile(hooksPath, hookSettings, agentCtlPath) {
20278
20418
  }
20279
20419
  async function resolveGitCommonDir(gitDir) {
20280
20420
  try {
20281
- const commonDir = (await Bun.file(join12(gitDir, "commondir")).text()).trim();
20421
+ const commonDir = (await Bun.file(join13(gitDir, "commondir")).text()).trim();
20282
20422
  if (!commonDir)
20283
20423
  return gitDir;
20284
20424
  return commonDir.startsWith("/") ? commonDir : resolve8(gitDir, commonDir);
@@ -20288,7 +20428,7 @@ async function resolveGitCommonDir(gitDir) {
20288
20428
  }
20289
20429
  async function ensureGeneratedCodexHooksIgnored(gitDir) {
20290
20430
  const commonDir = await resolveGitCommonDir(gitDir);
20291
- const excludePath = join12(commonDir, "info", "exclude");
20431
+ const excludePath = join13(commonDir, "info", "exclude");
20292
20432
  let existing = "";
20293
20433
  try {
20294
20434
  existing = await Bun.file(excludePath).text();
@@ -20298,7 +20438,7 @@ async function ensureGeneratedCodexHooksIgnored(gitDir) {
20298
20438
  const lines = existing.split(/\r?\n/).map((line) => line.trim());
20299
20439
  if (lines.includes(GENERATED_CODEX_HOOKS_EXCLUDE))
20300
20440
  return;
20301
- await mkdir3(dirname5(excludePath), { recursive: true });
20441
+ await mkdir3(dirname6(excludePath), { recursive: true });
20302
20442
  const separator = existing.length > 0 && !existing.endsWith(`
20303
20443
  `) ? `
20304
20444
  ` : "";
@@ -20308,12 +20448,12 @@ async function ensureGeneratedCodexHooksIgnored(gitDir) {
20308
20448
  async function ensureAgentRuntimeArtifacts(input) {
20309
20449
  const storagePaths = getWorktreeStoragePaths(input.gitDir);
20310
20450
  const artifacts = {
20311
- agentCtlPath: join12(storagePaths.webmuxDir, "webmux-agentctl"),
20312
- claudeSettingsPath: join12(input.worktreePath, ".claude", "settings.local.json"),
20313
- 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")
20314
20454
  };
20315
- await mkdir3(dirname5(artifacts.claudeSettingsPath), { recursive: true });
20316
- await mkdir3(dirname5(artifacts.codexHooksPath), { recursive: true });
20455
+ await mkdir3(dirname6(artifacts.claudeSettingsPath), { recursive: true });
20456
+ await mkdir3(dirname6(artifacts.codexHooksPath), { recursive: true });
20317
20457
  await Bun.write(artifacts.agentCtlPath, buildAgentCtlScript());
20318
20458
  await chmod2(artifacts.agentCtlPath, 493);
20319
20459
  const hookSettings = buildClaudeHookSettings(artifacts);
@@ -20845,7 +20985,7 @@ var init_worktree_service = __esm(() => {
20845
20985
  // backend/src/services/lifecycle-service.ts
20846
20986
  import { randomUUID as randomUUID3 } from "crypto";
20847
20987
  import { mkdir as mkdir4 } from "fs/promises";
20848
- import { dirname as dirname6, resolve as resolve10 } from "path";
20988
+ import { dirname as dirname7, resolve as resolve10 } from "path";
20849
20989
  function toErrorMessage2(error) {
20850
20990
  return error instanceof Error ? error.message : String(error);
20851
20991
  }
@@ -21289,9 +21429,13 @@ class LifecycleService {
21289
21429
  async pruneWorktrees() {
21290
21430
  try {
21291
21431
  const resolvedWorktrees = await this.resolveAllWorktrees();
21432
+ const sessionName = buildProjectSessionName(this.deps.projectRoot);
21292
21433
  const removedBranches = [];
21293
21434
  for (const resolved of resolvedWorktrees) {
21294
21435
  const branch = resolved.entry.branch ?? resolved.entry.path;
21436
+ if (this.deps.tmux.hasWindow(sessionName, buildWorktreeWindowName(branch))) {
21437
+ continue;
21438
+ }
21295
21439
  await this.removeResolvedWorktree(resolved);
21296
21440
  removedBranches.push(branch);
21297
21441
  }
@@ -21761,7 +21905,7 @@ ${oneshotPrompt}` : oneshotPrompt ?? baseSystemPrompt;
21761
21905
  ...createProgressBase,
21762
21906
  phase: "creating_worktree"
21763
21907
  });
21764
- await mkdir4(dirname6(worktreePath), { recursive: true });
21908
+ await mkdir4(dirname7(worktreePath), { recursive: true });
21765
21909
  initialized = await createManagedWorktree({
21766
21910
  repoRoot: this.deps.projectRoot,
21767
21911
  worktreePath,
@@ -22544,6 +22688,11 @@ function getWorktreeCommandUsage(command) {
22544
22688
  case "prune":
22545
22689
  return `Usage:
22546
22690
  webmux prune`;
22691
+ case "restore":
22692
+ return `Usage:
22693
+ webmux restore
22694
+
22695
+ Re-open every worktree session that was open the last time sessions were saved.`;
22547
22696
  case "tab":
22548
22697
  return [
22549
22698
  "Usage:",
@@ -22844,6 +22993,18 @@ function parsePruneCommandArgs(args) {
22844
22993
  }
22845
22994
  return true;
22846
22995
  }
22996
+ function parseRestoreCommandArgs(args) {
22997
+ for (const arg of args) {
22998
+ if (arg === "--help" || arg === "-h") {
22999
+ return false;
23000
+ }
23001
+ if (arg.startsWith("-")) {
23002
+ throw new CommandUsageError(`Unknown option: ${arg}`);
23003
+ }
23004
+ throw new CommandUsageError(`Unexpected argument: ${arg}`);
23005
+ }
23006
+ return true;
23007
+ }
22847
23008
  function parseListCommandArgs(args) {
22848
23009
  let mode = "active";
22849
23010
  let search = "";
@@ -22882,9 +23043,19 @@ function listProjectWorktrees(runtime) {
22882
23043
  const projectDir = resolve12(runtime.projectDir);
22883
23044
  return runtime.git.listWorktrees(projectDir).filter((entry) => !entry.bare && resolve12(entry.path) !== projectDir);
22884
23045
  }
23046
+ function buildOpenWorktreeWindowSet(runtime) {
23047
+ const sessionName = buildProjectSessionName(resolve12(runtime.projectDir));
23048
+ let windows = [];
23049
+ try {
23050
+ windows = runtime.tmux.listWindows();
23051
+ } catch {
23052
+ windows = [];
23053
+ }
23054
+ return new Set(windows.filter((w) => w.sessionName === sessionName).map((w) => w.windowName));
23055
+ }
22885
23056
  async function defaultConfirmPrune(worktreeCount) {
22886
23057
  const response = await confirm({
22887
- message: `Prune all ${worktreeCount} worktree${worktreeCount === 1 ? "" : "s"}? This action cannot be undone.`,
23058
+ message: `Prune ${worktreeCount} closed worktree${worktreeCount === 1 ? "" : "s"}? This action cannot be undone.`,
22888
23059
  initialValue: false
22889
23060
  });
22890
23061
  return !isCancel(response) && response;
@@ -22928,14 +23099,7 @@ async function listWorktrees(runtime, stdout2, options) {
22928
23099
  stdout2("No worktrees found.");
22929
23100
  return;
22930
23101
  }
22931
- const sessionName = buildProjectSessionName(projectDir);
22932
- let windows = [];
22933
- try {
22934
- windows = runtime.tmux.listWindows();
22935
- } catch {
22936
- windows = [];
22937
- }
22938
- const openWindows = new Set(windows.filter((w) => w.sessionName === sessionName).map((w) => w.windowName));
23102
+ const openWindows = buildOpenWorktreeWindowSet(runtime);
22939
23103
  const projectGitDir = runtime.git.resolveWorktreeGitDir(projectDir);
22940
23104
  const archivedPaths = buildArchivedWorktreePathSet(await readWorktreeArchiveState(projectGitDir));
22941
23105
  const rows = await Promise.all(entries.map(async (entry) => {
@@ -23000,6 +23164,7 @@ async function runWorktreeCommand(context, deps2 = {}) {
23000
23164
  const resolveBaseUrl = deps2.resolveBaseUrl ?? resolveProjectBaseUrl;
23001
23165
  const switchToTmuxWindow = deps2.switchToTmuxWindow ?? defaultSwitchToTmuxWindow;
23002
23166
  const confirmPrune = deps2.confirmPrune ?? defaultConfirmPrune;
23167
+ const readOpenSessions = deps2.readOpenSessions ?? readOpenSessionsState;
23003
23168
  try {
23004
23169
  if (context.command === "add") {
23005
23170
  const parsed = parseAddCommandArgs(context.args);
@@ -23078,18 +23243,85 @@ ${parsed.input.prompt}` : seed.data.conversationMarkdown;
23078
23243
  stdout2("No worktrees found.");
23079
23244
  return 0;
23080
23245
  }
23081
- if (!await confirmPrune(worktrees.length)) {
23246
+ const openWindows = buildOpenWorktreeWindowSet(runtime2);
23247
+ const closedWorktrees = worktrees.filter((entry) => !openWindows.has(buildWorktreeWindowName(entry.branch ?? basename7(entry.path))));
23248
+ if (closedWorktrees.length === 0) {
23249
+ stdout2("No closed worktrees to prune.");
23250
+ return 0;
23251
+ }
23252
+ if (!await confirmPrune(closedWorktrees.length)) {
23082
23253
  stdout2("Aborted.");
23083
23254
  return 0;
23084
23255
  }
23085
23256
  const result = await runtime2.lifecycleService.pruneWorktrees();
23086
23257
  if (result.removedBranches.length === 0) {
23087
- stdout2("No worktrees found.");
23258
+ stdout2("No closed worktrees to prune.");
23088
23259
  return 0;
23089
23260
  }
23090
23261
  stdout2(`Pruned ${result.removedBranches.length} worktree${result.removedBranches.length === 1 ? "" : "s"}: ${result.removedBranches.join(", ")}`);
23091
23262
  return 0;
23092
23263
  }
23264
+ if (context.command === "restore") {
23265
+ if (!parseRestoreCommandArgs(context.args)) {
23266
+ stdout2(getWorktreeCommandUsage("restore"));
23267
+ return 0;
23268
+ }
23269
+ const runtime2 = createRuntime({
23270
+ projectDir: context.projectDir,
23271
+ port: context.port
23272
+ });
23273
+ const projectDir = resolve12(runtime2.projectDir);
23274
+ const gitDir = runtime2.git.resolveWorktreeGitDir(projectDir);
23275
+ const state = await readOpenSessions(gitDir);
23276
+ if (state.branches.length === 0) {
23277
+ stdout2("No saved sessions to restore.");
23278
+ return 0;
23279
+ }
23280
+ const sessionName = buildProjectSessionName(projectDir);
23281
+ let openWindows = new Set;
23282
+ try {
23283
+ openWindows = new Set(runtime2.tmux.listWindows().filter((w) => w.sessionName === sessionName).map((w) => w.windowName));
23284
+ } catch {
23285
+ openWindows = new Set;
23286
+ }
23287
+ const existingBranches = new Set(listProjectWorktrees(runtime2).map((entry) => entry.branch ?? basename7(entry.path)));
23288
+ let restored = 0;
23289
+ let skipped = 0;
23290
+ let failed = 0;
23291
+ let firstRestored = null;
23292
+ for (const branch2 of state.branches) {
23293
+ if (openWindows.has(buildWorktreeWindowName(branch2))) {
23294
+ stdout2(`Already open: ${branch2}`);
23295
+ skipped++;
23296
+ continue;
23297
+ }
23298
+ if (!existingBranches.has(branch2)) {
23299
+ stderr(`Skipping ${branch2}: worktree no longer exists`);
23300
+ skipped++;
23301
+ continue;
23302
+ }
23303
+ try {
23304
+ await runtime2.lifecycleService.openWorktree(branch2);
23305
+ stdout2(`Restored ${branch2}`);
23306
+ restored++;
23307
+ if (!firstRestored)
23308
+ firstRestored = branch2;
23309
+ } catch (error) {
23310
+ stderr(`Failed to restore ${branch2}: ${error instanceof Error ? error.message : String(error)}`);
23311
+ failed++;
23312
+ }
23313
+ }
23314
+ const summaryParts = [`Restored ${restored} session${restored === 1 ? "" : "s"}`];
23315
+ if (skipped > 0)
23316
+ summaryParts.push(`skipped ${skipped}`);
23317
+ if (failed > 0)
23318
+ summaryParts.push(`${failed} failed`);
23319
+ stdout2(`${summaryParts.join(", ")}.`);
23320
+ if (firstRestored) {
23321
+ switchToTmuxWindow(runtime2.projectDir, firstRestored);
23322
+ }
23323
+ return failed > 0 ? 1 : 0;
23324
+ }
23093
23325
  if (context.command === "send") {
23094
23326
  const parsed = parseSendCommandArgs(context.args);
23095
23327
  if (!parsed) {
@@ -23227,13 +23459,13 @@ var init_worktree_commands = __esm(() => {
23227
23459
  });
23228
23460
 
23229
23461
  // bin/src/webmux.ts
23230
- 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";
23231
23463
  import { existsSync as existsSync6 } from "fs";
23232
23464
  import { fileURLToPath } from "url";
23233
23465
  // package.json
23234
23466
  var package_default = {
23235
23467
  name: "webmux",
23236
- version: "0.39.0",
23468
+ version: "0.41.0",
23237
23469
  description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
23238
23470
  type: "module",
23239
23471
  repository: {
@@ -23290,7 +23522,7 @@ var package_default = {
23290
23522
  };
23291
23523
 
23292
23524
  // bin/src/webmux.ts
23293
- var PKG_ROOT = resolve13(dirname7(fileURLToPath(import.meta.url)), "..");
23525
+ var PKG_ROOT = resolve13(dirname8(fileURLToPath(import.meta.url)), "..");
23294
23526
  function usage2() {
23295
23527
  console.log(`
23296
23528
  webmux \u2014 Dev dashboard for managing Git worktrees
@@ -23313,7 +23545,8 @@ Usage:
23313
23545
  webmux merge Merge a worktree into the main branch and remove it
23314
23546
  webmux send Send a prompt to a running worktree agent
23315
23547
  webmux tab List, create, switch, or close agent tabs in a worktree
23316
- webmux prune Remove all worktrees in the current project
23548
+ webmux prune Remove all closed (not open) worktrees in the current project
23549
+ webmux restore Re-open all worktree sessions that were open before
23317
23550
  webmux linear Post a worktree conversation to a Linear issue/team
23318
23551
  webmux project List, add, or remove projects served by the dashboard
23319
23552
  webmux completion Generate shell completion script (bash, zsh)
@@ -23331,7 +23564,7 @@ Environment:
23331
23564
  `);
23332
23565
  }
23333
23566
  function isRootCommand(value) {
23334
- return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "oneshot" || value === "list" || value === "open" || value === "close" || value === "refresh" || value === "archive" || value === "unarchive" || value === "label" || value === "remove" || value === "merge" || value === "send" || value === "tab" || value === "prune" || value === "linear" || value === "project" || value === "completion";
23567
+ return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "oneshot" || value === "list" || value === "open" || value === "close" || value === "refresh" || value === "archive" || value === "unarchive" || value === "label" || value === "remove" || value === "merge" || value === "send" || value === "tab" || value === "prune" || value === "restore" || value === "linear" || value === "project" || value === "completion";
23335
23568
  }
23336
23569
  function isServeRootOption(value) {
23337
23570
  return value === "--port" || value === "--app" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
@@ -23398,7 +23631,7 @@ Run webmux --help for usage.`);
23398
23631
  };
23399
23632
  }
23400
23633
  function isWorktreeCommand(command) {
23401
- return command === "add" || command === "list" || command === "open" || command === "close" || command === "refresh" || command === "archive" || command === "unarchive" || command === "label" || command === "remove" || command === "merge" || command === "send" || command === "tab" || command === "prune";
23634
+ return command === "add" || command === "list" || command === "open" || command === "close" || command === "refresh" || command === "archive" || command === "unarchive" || command === "label" || command === "remove" || command === "merge" || command === "send" || command === "tab" || command === "prune" || command === "restore";
23402
23635
  }
23403
23636
  async function loadEnvFile(path) {
23404
23637
  if (!existsSync6(path))
@@ -23526,6 +23759,8 @@ Refreshing ${services.length} installed webmux service(s) to pick up the new ver
23526
23759
  const parts = [];
23527
23760
  if (outcome.regenerated)
23528
23761
  parts.push("regenerated unit");
23762
+ if (outcome.migratedProject)
23763
+ parts.push(`migrated served project ${outcome.migratedProject}`);
23529
23764
  if (outcome.restarted)
23530
23765
  parts.push("restarted");
23531
23766
  if (!outcome.regenerated && !outcome.restarted && !outcome.error)
@@ -23613,8 +23848,8 @@ Refreshing ${services.length} installed webmux service(s) to pick up the new ver
23613
23848
  }
23614
23849
  process.on("SIGINT", cleanup);
23615
23850
  process.on("SIGTERM", cleanup);
23616
- const backendEntry = join13(PKG_ROOT, "backend", "dist", "server.js");
23617
- 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");
23618
23853
  if (!existsSync6(staticDir)) {
23619
23854
  console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
23620
23855
  process.exit(1);