webmux 0.20.0 → 0.21.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.
@@ -8886,7 +8886,7 @@ function listRemoteGitBranches(cwd) {
8886
8886
  } catch {}
8887
8887
  const output = runGit(["for-each-ref", "--format=%(refname:short)", "refs/remotes/origin"], cwd);
8888
8888
  return output.split(`
8889
- `).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => line.replace(/^origin\//, "")).filter((name) => name !== "HEAD");
8889
+ `).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => line.replace(/^origin\//, "")).filter((name) => name !== "HEAD" && name !== "origin");
8890
8890
  }
8891
8891
  function readGitWorktreeStatus(cwd) {
8892
8892
  const dirtyOutput = runGit(["status", "--porcelain"], cwd);
@@ -8948,7 +8948,11 @@ class BunGitGateway {
8948
8948
  if (opts.baseBranch)
8949
8949
  args.push(opts.baseBranch);
8950
8950
  } else {
8951
- args.push(opts.worktreePath, opts.branch);
8951
+ if (opts.startPoint) {
8952
+ args.push("-b", opts.branch, opts.worktreePath, opts.startPoint);
8953
+ } else {
8954
+ args.push(opts.worktreePath, opts.branch);
8955
+ }
8952
8956
  }
8953
8957
  runGit(args, opts.repoRoot);
8954
8958
  }
@@ -9114,7 +9118,8 @@ async function createManagedWorktree(opts, deps = {}) {
9114
9118
  worktreePath: opts.worktreePath,
9115
9119
  branch: opts.branch,
9116
9120
  mode: opts.mode,
9117
- baseBranch: opts.baseBranch
9121
+ baseBranch: opts.baseBranch,
9122
+ startPoint: opts.startPoint
9118
9123
  });
9119
9124
  worktreeCreated = true;
9120
9125
  const gitDir = git.resolveWorktreeGitDir(opts.worktreePath);
@@ -9207,7 +9212,7 @@ class LifecycleService {
9207
9212
  throw new LifecycleError("Base branch must differ from branch name", 400);
9208
9213
  }
9209
9214
  const baseBranch = mode === "new" ? requestedBaseBranch || this.deps.config.workspace.mainBranch : undefined;
9210
- this.ensureBranchAvailable(branch, mode);
9215
+ const branchAvailability = this.resolveBranchAvailability(branch, mode);
9211
9216
  const { profileName, profile } = this.resolveProfile(input.profile);
9212
9217
  const agent = this.resolveAgent(input.agent);
9213
9218
  const worktreePath = this.resolveWorktreePath(branch);
@@ -9218,7 +9223,7 @@ class LifecycleService {
9218
9223
  profile: profileName,
9219
9224
  agent
9220
9225
  };
9221
- const deleteBranchOnRollback = mode === "new";
9226
+ const deleteBranchOnRollback = mode === "new" || branchAvailability.deleteBranchOnRollback;
9222
9227
  let initialized = null;
9223
9228
  try {
9224
9229
  await this.reportCreateProgress({
@@ -9232,6 +9237,7 @@ class LifecycleService {
9232
9237
  branch,
9233
9238
  mode,
9234
9239
  ...baseBranch ? { baseBranch } : {},
9240
+ ...branchAvailability.startPoint ? { startPoint: branchAvailability.startPoint } : {},
9235
9241
  profile: profileName,
9236
9242
  agent,
9237
9243
  runtime: profile.runtime,
@@ -9377,9 +9383,9 @@ class LifecycleService {
9377
9383
  throw this.wrapOperationError(error);
9378
9384
  }
9379
9385
  }
9380
- listAvailableBranches() {
9386
+ listAvailableBranches(options = {}) {
9381
9387
  const localBranches = this.listLocalBranches().filter((branch) => isValidBranchName(branch));
9382
- const remoteBranches = this.listRemoteBranches().filter((branch) => isValidBranchName(branch));
9388
+ const remoteBranches = options.includeRemote ? this.listRemoteBranches().filter((branch) => isValidBranchName(branch)) : [];
9383
9389
  const checkedOutBranches = this.listCheckedOutBranches();
9384
9390
  const allBranches = [...new Set([...localBranches, ...remoteBranches])];
9385
9391
  return allBranches.filter((branch) => !checkedOutBranches.has(branch)).sort((left, right) => left.localeCompare(right)).map((name) => ({ name }));
@@ -9404,20 +9410,28 @@ class LifecycleService {
9404
9410
  }
9405
9411
  return await this.deps.autoName.generateBranchName(this.deps.config.autoName, prompt);
9406
9412
  }
9407
- ensureBranchAvailable(branch, mode) {
9413
+ resolveBranchAvailability(branch, mode) {
9408
9414
  const localBranches = new Set(this.listLocalBranches());
9409
9415
  if (mode === "new") {
9410
9416
  if (localBranches.has(branch)) {
9411
9417
  throw new LifecycleError(`Branch already exists: ${branch}`, 409);
9412
9418
  }
9413
- return;
9419
+ return { deleteBranchOnRollback: false };
9414
9420
  }
9415
- if (!localBranches.has(branch)) {
9416
- throw new LifecycleError(`Branch not found: ${branch}`, 404);
9421
+ if (localBranches.has(branch)) {
9422
+ if (this.listCheckedOutBranches().has(branch)) {
9423
+ throw new LifecycleError(`Branch already has a worktree: ${branch}`, 409);
9424
+ }
9425
+ return { deleteBranchOnRollback: false };
9417
9426
  }
9418
- if (this.listCheckedOutBranches().has(branch)) {
9419
- throw new LifecycleError(`Branch already has a worktree: ${branch}`, 409);
9427
+ const remoteBranches = new Set(this.listRemoteBranches());
9428
+ if (!remoteBranches.has(branch)) {
9429
+ throw new LifecycleError(`Branch not found: ${branch}`, 404);
9420
9430
  }
9431
+ return {
9432
+ startPoint: `origin/${branch}`,
9433
+ deleteBranchOnRollback: true
9434
+ };
9421
9435
  }
9422
9436
  resolveProfile(profileName) {
9423
9437
  const name = profileName ?? getDefaultProfileName(this.deps.config);
@@ -11696,9 +11710,10 @@ async function apiRuntimeEvent(req) {
11696
11710
  ...notification ? { notification } : {}
11697
11711
  });
11698
11712
  }
11699
- async function apiListBranches() {
11713
+ async function apiListBranches(req) {
11714
+ const includeRemote = new URL(req.url).searchParams.get("includeRemote") === "true";
11700
11715
  return jsonResponse({
11701
- branches: lifecycleService.listAvailableBranches()
11716
+ branches: lifecycleService.listAvailableBranches({ includeRemote })
11702
11717
  });
11703
11718
  }
11704
11719
  async function apiListBaseBranches() {
@@ -11984,7 +11999,7 @@ Bun.serve({
11984
11999
  GET: () => jsonResponse(getFrontendConfig())
11985
12000
  },
11986
12001
  "/api/branches": {
11987
- GET: () => catching("GET /api/branches", () => apiListBranches())
12002
+ GET: (req) => catching("GET /api/branches", () => apiListBranches(req))
11988
12003
  },
11989
12004
  "/api/base-branches": {
11990
12005
  GET: () => catching("GET /api/base-branches", () => apiListBaseBranches())
package/bin/webmux.js CHANGED
@@ -175,7 +175,7 @@ function listRemoteGitBranches(cwd) {
175
175
  } catch {}
176
176
  const output = runGit(["for-each-ref", "--format=%(refname:short)", "refs/remotes/origin"], cwd);
177
177
  return output.split(`
178
- `).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => line.replace(/^origin\//, "")).filter((name) => name !== "HEAD");
178
+ `).map((line) => line.trim()).filter((line) => line.length > 0).map((line) => line.replace(/^origin\//, "")).filter((name) => name !== "HEAD" && name !== "origin");
179
179
  }
180
180
  function readGitWorktreeStatus(cwd) {
181
181
  const dirtyOutput = runGit(["status", "--porcelain"], cwd);
@@ -237,7 +237,11 @@ class BunGitGateway {
237
237
  if (opts.baseBranch)
238
238
  args.push(opts.baseBranch);
239
239
  } else {
240
- args.push(opts.worktreePath, opts.branch);
240
+ if (opts.startPoint) {
241
+ args.push("-b", opts.branch, opts.worktreePath, opts.startPoint);
242
+ } else {
243
+ args.push(opts.worktreePath, opts.branch);
244
+ }
241
245
  }
242
246
  runGit(args, opts.repoRoot);
243
247
  }
@@ -1529,7 +1533,9 @@ function buildInitPromptSpec(context) {
1529
1533
  "Do not modify any other file.",
1530
1534
  "Do not ask the user questions. Infer the config from the repository contents.",
1531
1535
  "Be efficient: inspect only the files needed to determine the project name, main branch, service layout, dev commands, and ports.",
1532
- "The YAML must be valid and minimal.",
1536
+ "The active, uncommented YAML must be valid and minimal.",
1537
+ "Do not remove other starter sections or their explanatory comments just because they are unused.",
1538
+ "Keep optional examples and comments in place so the user can uncomment and use them later.",
1533
1539
  `Set workspace.defaultAgent to ${context.defaultAgent}.`,
1534
1540
  "Use this config shape:",
1535
1541
  "name: infer from the repository",
@@ -1544,6 +1550,7 @@ function buildInitPromptSpec(context) {
1544
1550
  "Include integrations.github.linkedRepos as an empty list, integrations.linear.enabled as true, and startupEnvs as an empty object.",
1545
1551
  "Only include optional sections like auto_name, lifecycleHooks, sandbox/docker config, mounts, or systemPrompt if the repository gives clear evidence they are needed.",
1546
1552
  "Prefer editing the existing keys over replacing the file with a completely different shape.",
1553
+ "Preserve the existing template structure and comments unless a specific change requires updating them.",
1547
1554
  "Before finishing, verify that `.webmux.yaml` exists and contains the final YAML."
1548
1555
  ].join(`
1549
1556
  `);
@@ -1875,44 +1882,179 @@ function buildStarterTemplate(input) {
1875
1882
  const defaultAgent = input.defaultAgent ?? "claude";
1876
1883
  const packageManager = input.packageManager ?? "npm";
1877
1884
  const devCommand = buildRunScriptCommand(packageManager, "dev");
1878
- return `# Project display name in the dashboard
1885
+ return `# Starter config for webmux.
1886
+ # Keep the active keys below as a minimal working setup, then uncomment
1887
+ # the examples to enable more services, profiles, integrations, or hooks.
1888
+
1889
+ # Project display name shown in the dashboard and browser title.
1879
1890
  name: ${input.projectName}
1880
1891
 
1881
1892
  workspace:
1893
+ # Git branch new worktrees start from.
1882
1894
  mainBranch: ${input.mainBranch}
1895
+ # Relative or absolute directory where managed worktrees are created.
1883
1896
  worktreeRoot: ../worktrees
1897
+ # Agent new worktrees use by default.
1884
1898
  defaultAgent: ${defaultAgent}
1885
-
1886
- # Each service defines a port env var that webmux injects into pane and agent
1887
- # process environments when creating a worktree. Ports are auto-assigned:
1888
- # base + (slot x step).
1899
+ # Example background pull settings for keeping the main branch fresh.
1900
+ # autoPull:
1901
+ # # Turn automatic pulls on or off.
1902
+ # enabled: false
1903
+ # # Seconds between pull attempts.
1904
+ # intervalSeconds: 300
1905
+
1906
+ # Services define the ports webmux allocates and tracks per worktree.
1889
1907
  services:
1890
- - name: app
1891
- portEnv: PORT
1892
- portStart: 3000
1893
- portStep: 10
1894
-
1908
+ # Example app service with a predictable per-worktree port.
1909
+ # - name: app
1910
+ # # Env var name injected into panes and hooks.
1911
+ # portEnv: PORT
1912
+ # # Starting port for the first worktree slot.
1913
+ # portStart: 3000
1914
+ # # Port increment between worktree slots.
1915
+ # portStep: 10
1916
+ # # Link shown in the dashboard when the service is running.
1917
+ # urlTemplate: http://localhost:\${PORT}
1918
+
1919
+ # Profiles define runtime, permissions, and tmux pane layout.
1895
1920
  profiles:
1896
1921
  default:
1922
+ # Run panes directly on the host machine.
1897
1923
  runtime: host
1898
- yolo: false
1899
- envPassthrough: []
1924
+ # Forward selected host env vars into the agent process.
1925
+ envPassthrough:
1926
+ # - ANTHROPIC_API_KEY
1927
+ # - OPENAI_API_KEY
1928
+ # Extra system instructions for the agent in this profile.
1929
+ # systemPrompt: >
1930
+ # You are working in \${WEBMUX_WORKTREE_PATH}
1931
+ # Skip agent permission prompts in this profile.
1932
+ # yolo: true
1933
+ # Panes define the tmux layout created for each worktree session.
1900
1934
  panes:
1935
+ # Main AI coding pane.
1901
1936
  - id: agent
1937
+ # Pane type: agent, command, or shell.
1902
1938
  kind: agent
1939
+ # Focus this pane when the session opens.
1903
1940
  focus: true
1904
- - id: app
1905
- kind: command
1906
- split: right
1907
- command: PORT=$PORT ${devCommand}
1908
-
1941
+ # Place this pane to the right of the existing layout.
1942
+ # split: right
1943
+ # Percent of the available space this pane should take.
1944
+ # sizePct: 50
1945
+ # Start this pane in the repo root or managed worktree.
1946
+ # cwd: worktree
1947
+ # Example dev server pane.
1948
+ # - id: app
1949
+ # # Pane type: agent, command, or shell.
1950
+ # kind: command
1951
+ # # Place this pane to the right of the existing layout.
1952
+ # split: right
1953
+ # # Percent of the available space this pane should take.
1954
+ # sizePct: 50
1955
+ # # Start this pane in the repo root or managed worktree.
1956
+ # cwd: worktree
1957
+ # # Change into a subdirectory before running the command.
1958
+ # workingDir: frontend
1959
+ # # Command run when the pane starts. webmux injects $PORT.
1960
+ # command: PORT=$PORT ${devCommand}
1961
+ # Example shell pane for manual commands.
1962
+ # - id: shell
1963
+ # # Pane type: agent, command, or shell.
1964
+ # kind: shell
1965
+ # # Place this pane below the existing layout.
1966
+ # split: bottom
1967
+ # # Percent of the available space this pane should take.
1968
+ # sizePct: 30
1969
+ # # Start this pane in the repo root or managed worktree.
1970
+ # cwd: repo
1971
+
1972
+ # Example sandbox profile that runs panes inside Docker.
1973
+ # sandbox:
1974
+ # # Run panes inside a container instead of on the host.
1975
+ # runtime: docker
1976
+ # # Docker image used for the sandbox container.
1977
+ # image: ghcr.io/your-org/your-image:latest
1978
+ # # Forward selected host env vars into the container.
1979
+ # envPassthrough:
1980
+ # - ANTHROPIC_API_KEY
1981
+ # - OPENAI_API_KEY
1982
+ # # Extra system instructions for the agent in this profile.
1983
+ # systemPrompt: >
1984
+ # Extra instructions for the sandbox profile.
1985
+ # # Skip agent permission prompts in this profile.
1986
+ # yolo: true
1987
+ # # Extra host paths to mount into the container.
1988
+ # mounts:
1989
+ # # Host path mounted into the sandbox.
1990
+ # - hostPath: ~/.codex
1991
+ # # Path inside the container.
1992
+ # guestPath: /root/.codex
1993
+ # # Allow writes through this mount.
1994
+ # writable: true
1995
+ # # Panes define the tmux layout created for sandbox sessions.
1996
+ # panes:
1997
+ # # Main AI coding pane.
1998
+ # - id: agent
1999
+ # # Pane type: agent, command, or shell.
2000
+ # kind: agent
2001
+ # # Focus this pane when the session opens.
2002
+ # focus: true
2003
+ # # Example shell pane for manual commands.
2004
+ # - id: shell
2005
+ # # Pane type: agent, command, or shell.
2006
+ # kind: shell
2007
+ # # Place this pane to the right of the existing layout.
2008
+ # split: right
2009
+ # # Start this pane in the repo root or managed worktree.
2010
+ # cwd: repo
2011
+
2012
+ # Integrations connect webmux to external systems.
1909
2013
  integrations:
1910
2014
  github:
1911
- linkedRepos: []
2015
+ # Additional local repos webmux should consider alongside the main repo.
2016
+ linkedRepos:
2017
+ # GitHub slug for a related repo.
2018
+ # - repo: your-org/your-repo
2019
+ # # Short label shown in the UI.
2020
+ # alias: repo
2021
+ # # Relative or absolute path to that local checkout.
2022
+ # dir: ../your-repo
2023
+ # Remove managed worktrees automatically when their PR merges.
2024
+ # autoRemoveOnMerge: true
1912
2025
  linear:
2026
+ # Enable Linear issue lookup and linking in the UI.
1913
2027
  enabled: true
1914
-
1915
- startupEnvs: {}
2028
+ # Auto-create worktrees for assigned issues.
2029
+ # autoCreateWorktrees: true
2030
+ # Show a create-ticket action in the dashboard.
2031
+ # createTicketOption: true
2032
+ # Restrict issue sync to a specific Linear team id.
2033
+ # teamId: team-123
2034
+
2035
+ # startupEnvs become runtime env vars for panes, agents, and hooks.
2036
+ startupEnvs:
2037
+ # Example feature flag available in every worktree session.
2038
+ # FEATURE_FLAG: true
2039
+ # Example service URL built from allocated ports.
2040
+ # API_BASE_URL: http://localhost:\${PORT}
2041
+
2042
+ # lifecycleHooks run custom shell commands during worktree lifecycle events.
2043
+ # lifecycleHooks:
2044
+ # # Runs after env setup and before panes start.
2045
+ # postCreate: bun install
2046
+ # # Runs before the worktree directory is removed.
2047
+ # preRemove: tmux kill-session -t "$WEBMUX_WORKTREE_ID" || true
2048
+
2049
+ # auto_name lets webmux generate a branch name when one is not provided.
2050
+ # auto_name:
2051
+ # # Provider used for automatic branch naming.
2052
+ # provider: ${defaultAgent}
2053
+ # # Model used for automatic branch naming.
2054
+ # model: ${defaultAgent === "codex" ? "gpt-5.1-codex" : "claude-3-5-haiku-latest"}
2055
+ # # Prompt that tells the model how to name branches.
2056
+ # system_prompt: >
2057
+ # Generate a short kebab-case git branch name.
1916
2058
  `;
1917
2059
  }
1918
2060
  var FAST_CLAUDE_MODEL = "haiku", FAST_CLAUDE_EFFORT = "low", FAST_CODEX_MODEL = "gpt-5.1-codex", FAST_CODEX_REASONING = "low";
@@ -11182,7 +11324,8 @@ async function createManagedWorktree(opts, deps2 = {}) {
11182
11324
  worktreePath: opts.worktreePath,
11183
11325
  branch: opts.branch,
11184
11326
  mode: opts.mode,
11185
- baseBranch: opts.baseBranch
11327
+ baseBranch: opts.baseBranch,
11328
+ startPoint: opts.startPoint
11186
11329
  });
11187
11330
  worktreeCreated = true;
11188
11331
  const gitDir = git.resolveWorktreeGitDir(opts.worktreePath);
@@ -11275,7 +11418,7 @@ class LifecycleService {
11275
11418
  throw new LifecycleError("Base branch must differ from branch name", 400);
11276
11419
  }
11277
11420
  const baseBranch = mode === "new" ? requestedBaseBranch || this.deps.config.workspace.mainBranch : undefined;
11278
- this.ensureBranchAvailable(branch, mode);
11421
+ const branchAvailability = this.resolveBranchAvailability(branch, mode);
11279
11422
  const { profileName, profile } = this.resolveProfile(input.profile);
11280
11423
  const agent = this.resolveAgent(input.agent);
11281
11424
  const worktreePath = this.resolveWorktreePath(branch);
@@ -11286,7 +11429,7 @@ class LifecycleService {
11286
11429
  profile: profileName,
11287
11430
  agent
11288
11431
  };
11289
- const deleteBranchOnRollback = mode === "new";
11432
+ const deleteBranchOnRollback = mode === "new" || branchAvailability.deleteBranchOnRollback;
11290
11433
  let initialized = null;
11291
11434
  try {
11292
11435
  await this.reportCreateProgress({
@@ -11300,6 +11443,7 @@ class LifecycleService {
11300
11443
  branch,
11301
11444
  mode,
11302
11445
  ...baseBranch ? { baseBranch } : {},
11446
+ ...branchAvailability.startPoint ? { startPoint: branchAvailability.startPoint } : {},
11303
11447
  profile: profileName,
11304
11448
  agent,
11305
11449
  runtime: profile.runtime,
@@ -11445,9 +11589,9 @@ class LifecycleService {
11445
11589
  throw this.wrapOperationError(error);
11446
11590
  }
11447
11591
  }
11448
- listAvailableBranches() {
11592
+ listAvailableBranches(options = {}) {
11449
11593
  const localBranches = this.listLocalBranches().filter((branch) => isValidBranchName(branch));
11450
- const remoteBranches = this.listRemoteBranches().filter((branch) => isValidBranchName(branch));
11594
+ const remoteBranches = options.includeRemote ? this.listRemoteBranches().filter((branch) => isValidBranchName(branch)) : [];
11451
11595
  const checkedOutBranches = this.listCheckedOutBranches();
11452
11596
  const allBranches = [...new Set([...localBranches, ...remoteBranches])];
11453
11597
  return allBranches.filter((branch) => !checkedOutBranches.has(branch)).sort((left, right) => left.localeCompare(right)).map((name) => ({ name }));
@@ -11472,20 +11616,28 @@ class LifecycleService {
11472
11616
  }
11473
11617
  return await this.deps.autoName.generateBranchName(this.deps.config.autoName, prompt);
11474
11618
  }
11475
- ensureBranchAvailable(branch, mode) {
11619
+ resolveBranchAvailability(branch, mode) {
11476
11620
  const localBranches = new Set(this.listLocalBranches());
11477
11621
  if (mode === "new") {
11478
11622
  if (localBranches.has(branch)) {
11479
11623
  throw new LifecycleError(`Branch already exists: ${branch}`, 409);
11480
11624
  }
11481
- return;
11625
+ return { deleteBranchOnRollback: false };
11482
11626
  }
11483
- if (!localBranches.has(branch)) {
11484
- throw new LifecycleError(`Branch not found: ${branch}`, 404);
11627
+ if (localBranches.has(branch)) {
11628
+ if (this.listCheckedOutBranches().has(branch)) {
11629
+ throw new LifecycleError(`Branch already has a worktree: ${branch}`, 409);
11630
+ }
11631
+ return { deleteBranchOnRollback: false };
11485
11632
  }
11486
- if (this.listCheckedOutBranches().has(branch)) {
11487
- throw new LifecycleError(`Branch already has a worktree: ${branch}`, 409);
11633
+ const remoteBranches = new Set(this.listRemoteBranches());
11634
+ if (!remoteBranches.has(branch)) {
11635
+ throw new LifecycleError(`Branch not found: ${branch}`, 404);
11488
11636
  }
11637
+ return {
11638
+ startPoint: `origin/${branch}`,
11639
+ deleteBranchOnRollback: true
11640
+ };
11489
11641
  }
11490
11642
  resolveProfile(profileName) {
11491
11643
  const name = profileName ?? getDefaultProfileName(this.deps.config);
@@ -12764,7 +12916,7 @@ import { fileURLToPath } from "url";
12764
12916
  // package.json
12765
12917
  var package_default = {
12766
12918
  name: "webmux",
12767
- version: "0.20.0",
12919
+ version: "0.21.0",
12768
12920
  description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
12769
12921
  type: "module",
12770
12922
  repository: {