webmux 0.11.0 → 0.13.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
@@ -47,6 +47,401 @@ var __export = (target, all) => {
47
47
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
48
  var __require = import.meta.require;
49
49
 
50
+ // backend/src/adapters/git.ts
51
+ import { rmSync } from "fs";
52
+ import { resolve } from "path";
53
+ function runGit(args, cwd) {
54
+ const result = Bun.spawnSync(["git", ...args], {
55
+ cwd,
56
+ stdout: "pipe",
57
+ stderr: "pipe"
58
+ });
59
+ if (result.exitCode !== 0) {
60
+ const stderr = new TextDecoder().decode(result.stderr).trim();
61
+ throw new Error(`git ${args.join(" ")} failed: ${stderr || `exit ${result.exitCode}`}`);
62
+ }
63
+ return new TextDecoder().decode(result.stdout).trim();
64
+ }
65
+ function tryRunGit(args, cwd) {
66
+ const result = Bun.spawnSync(["git", ...args], {
67
+ cwd,
68
+ stdout: "pipe",
69
+ stderr: "pipe"
70
+ });
71
+ if (result.exitCode !== 0) {
72
+ return {
73
+ ok: false,
74
+ stderr: new TextDecoder().decode(result.stderr).trim()
75
+ };
76
+ }
77
+ return {
78
+ ok: true,
79
+ stdout: new TextDecoder().decode(result.stdout).trim()
80
+ };
81
+ }
82
+ function errorMessage(error) {
83
+ return error instanceof Error ? error.message : String(error);
84
+ }
85
+ function isRegisteredWorktree(entries, worktreePath) {
86
+ const resolvedPath = resolve(worktreePath);
87
+ return entries.some((entry) => resolve(entry.path) === resolvedPath);
88
+ }
89
+ function removeDirectory(path) {
90
+ rmSync(path, {
91
+ recursive: true,
92
+ force: true
93
+ });
94
+ }
95
+ function currentCheckoutRef(cwd) {
96
+ const symbolicRef = tryRunGit(["symbolic-ref", "--quiet", "--short", "HEAD"], cwd);
97
+ if (symbolicRef.ok && symbolicRef.stdout.length > 0) {
98
+ return {
99
+ ref: symbolicRef.stdout,
100
+ branch: symbolicRef.stdout
101
+ };
102
+ }
103
+ return {
104
+ ref: runGit(["rev-parse", "--verify", "HEAD"], cwd),
105
+ branch: null
106
+ };
107
+ }
108
+ function resolveWorktreeRoot(cwd) {
109
+ const output = runGit(["rev-parse", "--show-toplevel"], cwd);
110
+ return resolve(cwd, output);
111
+ }
112
+ function resolveWorktreeGitDir(cwd) {
113
+ const output = runGit(["rev-parse", "--git-dir"], cwd);
114
+ return resolve(cwd, output);
115
+ }
116
+ function parseGitWorktreePorcelain(output) {
117
+ const entries = [];
118
+ let current = null;
119
+ const flush = () => {
120
+ if (current?.path)
121
+ entries.push(current);
122
+ current = null;
123
+ };
124
+ for (const rawLine of output.split(`
125
+ `)) {
126
+ const line = rawLine.trimEnd();
127
+ if (!line) {
128
+ flush();
129
+ continue;
130
+ }
131
+ if (line.startsWith("worktree ")) {
132
+ flush();
133
+ current = {
134
+ path: line.slice("worktree ".length),
135
+ branch: null,
136
+ head: null,
137
+ detached: false,
138
+ bare: false
139
+ };
140
+ continue;
141
+ }
142
+ if (!current)
143
+ continue;
144
+ if (line.startsWith("branch ")) {
145
+ current.branch = line.slice("branch ".length).replace(/^refs\/heads\//, "");
146
+ continue;
147
+ }
148
+ if (line.startsWith("HEAD ")) {
149
+ current.head = line.slice("HEAD ".length);
150
+ continue;
151
+ }
152
+ if (line === "detached") {
153
+ current.detached = true;
154
+ continue;
155
+ }
156
+ if (line === "bare") {
157
+ current.bare = true;
158
+ }
159
+ }
160
+ flush();
161
+ return entries;
162
+ }
163
+ function listGitWorktrees(cwd) {
164
+ const output = runGit(["worktree", "list", "--porcelain"], cwd);
165
+ return parseGitWorktreePorcelain(output);
166
+ }
167
+ function listLocalGitBranches(cwd) {
168
+ const output = runGit(["for-each-ref", "--format=%(refname:short)", "refs/heads"], cwd);
169
+ return output.split(`
170
+ `).map((line) => line.trim()).filter((line) => line.length > 0);
171
+ }
172
+ function readGitWorktreeStatus(cwd) {
173
+ const dirtyOutput = runGit(["status", "--porcelain"], cwd);
174
+ const commit = tryRunGit(["rev-parse", "HEAD"], cwd);
175
+ const ahead = tryRunGit(["rev-list", "--count", "@{upstream}..HEAD"], cwd);
176
+ return {
177
+ dirty: dirtyOutput.length > 0,
178
+ aheadCount: ahead.ok ? parseInt(ahead.stdout, 10) || 0 : 0,
179
+ currentCommit: commit.ok && commit.stdout.length > 0 ? commit.stdout : null
180
+ };
181
+ }
182
+ function removeGitWorktree(opts, deps = {}) {
183
+ const args = ["worktree", "remove"];
184
+ if (opts.force)
185
+ args.push("--force");
186
+ args.push(opts.worktreePath);
187
+ const result = (deps.tryRunGit ?? tryRunGit)(args, opts.repoRoot);
188
+ if (result.ok) {
189
+ return;
190
+ }
191
+ const failure = `git ${args.join(" ")} failed: ${result.stderr || "exit 1"}`;
192
+ const remainingWorktrees = (deps.listWorktrees ?? listGitWorktrees)(opts.repoRoot);
193
+ if (isRegisteredWorktree(remainingWorktrees, opts.worktreePath)) {
194
+ throw new Error(failure);
195
+ }
196
+ try {
197
+ (deps.removeDirectory ?? removeDirectory)(opts.worktreePath);
198
+ } catch (error) {
199
+ throw new Error(`${failure}; cleanup failed: ${errorMessage(error)}`);
200
+ }
201
+ }
202
+
203
+ class BunGitGateway {
204
+ resolveWorktreeRoot(cwd) {
205
+ return resolveWorktreeRoot(cwd);
206
+ }
207
+ resolveWorktreeGitDir(cwd) {
208
+ return resolveWorktreeGitDir(cwd);
209
+ }
210
+ listWorktrees(cwd) {
211
+ return listGitWorktrees(cwd);
212
+ }
213
+ listLocalBranches(cwd) {
214
+ return listLocalGitBranches(cwd);
215
+ }
216
+ readWorktreeStatus(cwd) {
217
+ return readGitWorktreeStatus(cwd);
218
+ }
219
+ createWorktree(opts) {
220
+ const args = ["worktree", "add"];
221
+ if (opts.mode === "new") {
222
+ args.push("-b", opts.branch, opts.worktreePath);
223
+ if (opts.baseBranch)
224
+ args.push(opts.baseBranch);
225
+ } else {
226
+ args.push(opts.worktreePath, opts.branch);
227
+ }
228
+ runGit(args, opts.repoRoot);
229
+ }
230
+ removeWorktree(opts) {
231
+ removeGitWorktree(opts);
232
+ }
233
+ deleteBranch(repoRoot, branch, force = false) {
234
+ runGit(["branch", force ? "-D" : "-d", branch], repoRoot);
235
+ }
236
+ mergeBranch(opts) {
237
+ const current = currentCheckoutRef(opts.repoRoot);
238
+ const shouldRestore = current.branch !== opts.targetBranch;
239
+ if (shouldRestore) {
240
+ runGit(["checkout", opts.targetBranch], opts.repoRoot);
241
+ }
242
+ let mergeError = null;
243
+ const cleanupErrors = [];
244
+ try {
245
+ runGit(["merge", "--no-ff", "--no-edit", opts.sourceBranch], opts.repoRoot);
246
+ } catch (error) {
247
+ mergeError = errorMessage(error);
248
+ const abort = tryRunGit(["merge", "--abort"], opts.repoRoot);
249
+ if (!abort.ok && abort.stderr.length > 0 && !abort.stderr.includes("MERGE_HEAD missing")) {
250
+ cleanupErrors.push(`merge abort failed: ${abort.stderr}`);
251
+ }
252
+ }
253
+ if (shouldRestore) {
254
+ const restore = tryRunGit(["checkout", current.ref], opts.repoRoot);
255
+ if (!restore.ok) {
256
+ cleanupErrors.push(`restore checkout failed: ${restore.stderr}`);
257
+ }
258
+ }
259
+ if (mergeError) {
260
+ const suffix = cleanupErrors.length > 0 ? `; ${cleanupErrors.join("; ")}` : "";
261
+ throw new Error(`${mergeError}${suffix}`);
262
+ }
263
+ if (cleanupErrors.length > 0) {
264
+ throw new Error(cleanupErrors.join("; "));
265
+ }
266
+ }
267
+ currentBranch(repoRoot) {
268
+ return runGit(["branch", "--show-current"], repoRoot);
269
+ }
270
+ }
271
+ var init_git = () => {};
272
+
273
+ // bin/src/completions.ts
274
+ var exports_completions = {};
275
+ __export(exports_completions, {
276
+ runCompletionCommand: () => runCompletionCommand,
277
+ listWorktreeBranches: () => listWorktreeBranches,
278
+ handleCompletions: () => handleCompletions,
279
+ extractBranches: () => extractBranches
280
+ });
281
+ import { basename, dirname, resolve as resolve2 } from "path";
282
+ function extractBranches(porcelainOutput, mainWorktreePath) {
283
+ const entries = parseGitWorktreePorcelain(porcelainOutput);
284
+ const resolvedMain = mainWorktreePath ? resolve2(mainWorktreePath) : null;
285
+ return entries.filter((e) => !e.bare && (!resolvedMain || resolve2(e.path) !== resolvedMain)).map((e) => e.branch ?? basename(e.path));
286
+ }
287
+ function defaultRunGit(args) {
288
+ const result = Bun.spawnSync(["git", ...args], {
289
+ stdout: "pipe",
290
+ stderr: "pipe"
291
+ });
292
+ return {
293
+ exitCode: result.exitCode,
294
+ stdout: new TextDecoder().decode(result.stdout).trim()
295
+ };
296
+ }
297
+ function listWorktreeBranches(deps = { runGit: defaultRunGit }) {
298
+ const worktreeResult = deps.runGit(["worktree", "list", "--porcelain"]);
299
+ if (worktreeResult.exitCode !== 0)
300
+ return [];
301
+ const commonDirResult = deps.runGit(["rev-parse", "--git-common-dir"]);
302
+ const mainPath = commonDirResult.exitCode === 0 ? dirname(resolve2(commonDirResult.stdout)) : null;
303
+ return extractBranches(worktreeResult.stdout, mainPath);
304
+ }
305
+ function handleCompletions(args) {
306
+ const subcommand = args[0];
307
+ if (!subcommand || !BRANCH_SUBCOMMANDS.has(subcommand)) {
308
+ return;
309
+ }
310
+ const branches = listWorktreeBranches();
311
+ for (const branch of branches) {
312
+ console.log(branch);
313
+ }
314
+ }
315
+ function isCompletionShell(value) {
316
+ return value === "bash" || value === "zsh";
317
+ }
318
+ function runCompletionCommand(args) {
319
+ const shell = args[0];
320
+ if (!shell || shell === "--help" || shell === "-h") {
321
+ console.log([
322
+ "Usage:",
323
+ " webmux completion <bash|zsh>",
324
+ "",
325
+ "Add this to your shell config to enable autocompletion:",
326
+ "",
327
+ " # ~/.zshrc",
328
+ ' eval "$(webmux completion zsh)"',
329
+ "",
330
+ " # ~/.bashrc",
331
+ ' eval "$(webmux completion bash)"'
332
+ ].join(`
333
+ `));
334
+ return 0;
335
+ }
336
+ if (!isCompletionShell(shell)) {
337
+ console.error(`Unknown shell: ${shell}. Supported: bash, zsh`);
338
+ return 1;
339
+ }
340
+ console.log(generateCompletionScript(shell));
341
+ return 0;
342
+ }
343
+ function generateCompletionScript(shell) {
344
+ switch (shell) {
345
+ case "zsh":
346
+ return ZSH_SCRIPT;
347
+ case "bash":
348
+ return BASH_SCRIPT;
349
+ }
350
+ }
351
+ var BRANCH_SUBCOMMANDS, ZSH_SCRIPT = `#compdef webmux
352
+
353
+ _webmux() {
354
+ local -a commands
355
+ commands=(
356
+ 'serve:Start the dashboard server'
357
+ 'init:Interactive project setup'
358
+ 'service:Manage webmux as a system service'
359
+ 'update:Update webmux to the latest version'
360
+ 'add:Create a worktree'
361
+ 'list:List worktrees and their status'
362
+ 'open:Open an existing worktree session'
363
+ 'close:Close a worktree session'
364
+ 'remove:Remove a worktree'
365
+ 'merge:Merge a worktree into main'
366
+ 'prune:Remove all worktrees in the current project'
367
+ 'completion:Generate shell completion script'
368
+ )
369
+
370
+ if (( CURRENT == 2 )); then
371
+ _describe 'command' commands
372
+ return
373
+ fi
374
+
375
+ case "\${words[2]}" in
376
+ open|close|remove|merge)
377
+ if (( CURRENT == 3 )); then
378
+ local -a branches
379
+ branches=(\${(f)"$(webmux --completions "\${words[2]}" 2>/dev/null)"})
380
+ if (( \${#branches} )); then
381
+ _describe 'worktree' branches
382
+ fi
383
+ fi
384
+ ;;
385
+ completion)
386
+ if (( CURRENT == 3 )); then
387
+ local -a shells
388
+ shells=('bash:Bash completion script' 'zsh:Zsh completion script')
389
+ _describe 'shell' shells
390
+ fi
391
+ ;;
392
+ service)
393
+ if (( CURRENT == 3 )); then
394
+ local -a actions
395
+ actions=(
396
+ 'install:Install webmux as a system service'
397
+ 'uninstall:Remove the system service'
398
+ 'status:Show service status'
399
+ 'logs:Show service logs'
400
+ )
401
+ _describe 'action' actions
402
+ fi
403
+ ;;
404
+ esac
405
+ }
406
+
407
+ compdef _webmux webmux`, BASH_SCRIPT = `_webmux() {
408
+ local cur prev
409
+ COMPREPLY=()
410
+ cur="\${COMP_WORDS[COMP_CWORD]}"
411
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
412
+
413
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
414
+ COMPREPLY=($(compgen -W "serve init service update add list open close remove merge prune completion" -- "\${cur}"))
415
+ return
416
+ fi
417
+
418
+ case "\${COMP_WORDS[1]}" in
419
+ open|close|remove|merge)
420
+ if [[ \${COMP_CWORD} -eq 2 ]]; then
421
+ local branches
422
+ branches=$(webmux --completions "\${COMP_WORDS[1]}" 2>/dev/null)
423
+ COMPREPLY=($(compgen -W "\${branches}" -- "\${cur}"))
424
+ fi
425
+ ;;
426
+ completion)
427
+ if [[ \${COMP_CWORD} -eq 2 ]]; then
428
+ COMPREPLY=($(compgen -W "bash zsh" -- "\${cur}"))
429
+ fi
430
+ ;;
431
+ service)
432
+ if [[ \${COMP_CWORD} -eq 2 ]]; then
433
+ COMPREPLY=($(compgen -W "install uninstall status logs" -- "\${cur}"))
434
+ fi
435
+ ;;
436
+ esac
437
+ }
438
+
439
+ complete -F _webmux webmux`;
440
+ var init_completions = __esm(() => {
441
+ init_git();
442
+ BRANCH_SUBCOMMANDS = new Set(["open", "close", "remove", "merge"]);
443
+ });
444
+
50
445
  // node_modules/.bun/sisteransi@1.0.5/node_modules/sisteransi/src/index.js
51
446
  var require_src = __commonJS((exports, module) => {
52
447
  var ESC = "\x1B";
@@ -993,7 +1388,7 @@ var init_dist2 = __esm(() => {
993
1388
 
994
1389
  // bin/src/shared.ts
995
1390
  import { existsSync, readFileSync } from "fs";
996
- import { basename, join } from "path";
1391
+ import { basename as basename2, join } from "path";
997
1392
  function run(cmd, args, opts) {
998
1393
  const result = Bun.spawnSync([cmd, ...args], { stdout: "pipe", stderr: "pipe", ...opts });
999
1394
  return {
@@ -1020,12 +1415,12 @@ function detectProjectName(gitRoot) {
1020
1415
  return pkg.name;
1021
1416
  } catch {}
1022
1417
  }
1023
- return basename(gitRoot);
1418
+ return basename2(gitRoot);
1024
1419
  }
1025
1420
  var init_shared = () => {};
1026
1421
 
1027
1422
  // bin/src/init-helpers.ts
1028
- import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync } from "fs";
1423
+ import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
1029
1424
  import { tmpdir } from "os";
1030
1425
  import { join as join2 } from "path";
1031
1426
  function isRecord(value) {
@@ -1427,7 +1822,7 @@ async function runInitAgentCommand(spec, cwd, handlers = {}) {
1427
1822
  try {
1428
1823
  summary = readFileSync2(spec.summaryPath, "utf8").trim() || summary;
1429
1824
  } finally {
1430
- rmSync(spec.summaryPath, { force: true });
1825
+ rmSync2(spec.summaryPath, { force: true });
1431
1826
  }
1432
1827
  }
1433
1828
  return { exitCode, stdout: stdoutResult.raw, stderr, summary };
@@ -1713,6 +2108,7 @@ ${result.stderr.trim()}` : ""
1713
2108
  console.log();
1714
2109
  console.log(" 1. Review .webmux.yaml and adjust panes, ports, and profiles if needed");
1715
2110
  console.log(" 2. Run: webmux");
2111
+ console.log(' 3. Enable tab completion: eval "$(webmux completion zsh)" (or bash)');
1716
2112
  console.log();
1717
2113
  });
1718
2114
 
@@ -1772,7 +2168,7 @@ ExecStart=${config.webmuxPath} serve --port ${config.port}
1772
2168
  WorkingDirectory=${config.projectDir}
1773
2169
  Restart=on-failure
1774
2170
  RestartSec=5
1775
- Environment=BACKEND_PORT=${config.port}
2171
+ Environment=PORT=${config.port}
1776
2172
  Environment=WEBMUX_PROJECT_DIR=${config.projectDir}
1777
2173
  Environment=PATH=${process.env.PATH}
1778
2174
 
@@ -1810,7 +2206,7 @@ function generateLaunchdPlist(config) {
1810
2206
  <string>${logPath}</string>
1811
2207
  <key>EnvironmentVariables</key>
1812
2208
  <dict>
1813
- <key>BACKEND_PORT</key>
2209
+ <key>PORT</key>
1814
2210
  <string>${config.port}</string>
1815
2211
  <key>WEBMUX_PROJECT_DIR</key>
1816
2212
  <string>${config.projectDir}</string>
@@ -2009,7 +2405,7 @@ async function service(args) {
2009
2405
  R2.error("Could not find webmux in PATH.");
2010
2406
  return;
2011
2407
  }
2012
- let port = parseInt(process.env.BACKEND_PORT || "5111");
2408
+ let port = parseInt(process.env.PORT || "5111");
2013
2409
  for (let i = 1;i < args.length; i++) {
2014
2410
  if (args[i] === "--port" && args[i + 1]) {
2015
2411
  const parsed = parseInt(args[++i]);
@@ -2189,7 +2585,7 @@ var init_fs = __esm(() => {
2189
2585
 
2190
2586
  // backend/src/adapters/tmux.ts
2191
2587
  import { createHash } from "crypto";
2192
- import { basename as basename2, resolve } from "path";
2588
+ import { basename as basename3, resolve as resolve3 } from "path";
2193
2589
  function runTmux(args) {
2194
2590
  const result = Bun.spawnSync(["tmux", ...args], {
2195
2591
  stdout: "pipe",
@@ -2214,8 +2610,8 @@ function sanitizeTmuxNameSegment(value, maxLength = 24) {
2214
2610
  return trimmed || "x";
2215
2611
  }
2216
2612
  function buildProjectSessionName(projectRoot) {
2217
- const resolved = resolve(projectRoot);
2218
- const base = sanitizeTmuxNameSegment(basename2(resolved), 18);
2613
+ const resolved = resolve3(projectRoot);
2614
+ const base = sanitizeTmuxNameSegment(basename3(resolved), 18);
2219
2615
  const hash = createHash("sha1").update(resolved).digest("hex").slice(0, 8);
2220
2616
  return `wm-${base}-${hash}`;
2221
2617
  }
@@ -2240,9 +2636,9 @@ class BunTmuxGateway {
2240
2636
  }
2241
2637
  ensureSession(sessionName, cwd) {
2242
2638
  const check = runTmux(["has-session", "-t", sessionName]);
2243
- if (check.exitCode === 0)
2244
- return;
2245
- assertTmuxOk(["new-session", "-d", "-s", sessionName, "-c", cwd], `create tmux session ${sessionName}`);
2639
+ if (check.exitCode !== 0) {
2640
+ assertTmuxOk(["new-session", "-d", "-s", sessionName, "-c", cwd], `create tmux session ${sessionName}`);
2641
+ }
2246
2642
  }
2247
2643
  hasWindow(sessionName, windowName) {
2248
2644
  const result = runTmux(["list-windows", "-t", sessionName, "-F", "#{window_name}"]);
@@ -9431,7 +9827,8 @@ function parseLinkedRepos(raw) {
9431
9827
  return [];
9432
9828
  return raw.filter(isRecord3).filter((entry) => typeof entry.repo === "string").map((entry) => ({
9433
9829
  repo: entry.repo,
9434
- alias: typeof entry.alias === "string" ? entry.alias : entry.repo.split("/").pop() ?? "repo"
9830
+ alias: typeof entry.alias === "string" ? entry.alias : entry.repo.split("/").pop() ?? "repo",
9831
+ ...typeof entry.dir === "string" && entry.dir.trim() ? { dir: entry.dir.trim() } : {}
9435
9832
  }));
9436
9833
  }
9437
9834
  function isDockerProfile(profile) {
@@ -9471,7 +9868,7 @@ function loadConfig(dir) {
9471
9868
  startupEnvs: parseStartupEnvs(parsed.startupEnvs),
9472
9869
  integrations: {
9473
9870
  github: {
9474
- linkedRepos: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github.linkedRepos) : []
9871
+ linkedRepos: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github.linkedRepos) : isRecord3(parsed.integrations) && Array.isArray(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github) : []
9475
9872
  },
9476
9873
  linear: {
9477
9874
  enabled: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.linear) && typeof parsed.integrations.linear.enabled === "boolean" ? parsed.integrations.linear.enabled : DEFAULT_CONFIG.integrations.linear.enabled
@@ -9521,7 +9918,7 @@ var init_config = __esm(() => {
9521
9918
 
9522
9919
  // backend/src/adapters/control-token.ts
9523
9920
  import { chmod, mkdir as mkdir2 } from "fs/promises";
9524
- import { dirname } from "path";
9921
+ import { dirname as dirname2 } from "path";
9525
9922
  async function loadControlToken() {
9526
9923
  if (cachedToken)
9527
9924
  return cachedToken;
@@ -9531,7 +9928,7 @@ async function loadControlToken() {
9531
9928
  return cachedToken;
9532
9929
  }
9533
9930
  const controlToken = crypto.randomUUID();
9534
- await mkdir2(dirname(CONTROL_TOKEN_PATH), { recursive: true });
9931
+ await mkdir2(dirname2(CONTROL_TOKEN_PATH), { recursive: true });
9535
9932
  await Bun.write(CONTROL_TOKEN_PATH, controlToken);
9536
9933
  await chmod(CONTROL_TOKEN_PATH, 384);
9537
9934
  cachedToken = controlToken;
@@ -9822,224 +10219,14 @@ var init_docker = __esm(() => {
9822
10219
  init_log();
9823
10220
  });
9824
10221
 
9825
- // backend/src/adapters/git.ts
9826
- import { rmSync as rmSync2 } from "fs";
9827
- import { resolve as resolve2 } from "path";
9828
- function runGit(args, cwd) {
9829
- const result = Bun.spawnSync(["git", ...args], {
9830
- cwd,
9831
- stdout: "pipe",
9832
- stderr: "pipe"
9833
- });
9834
- if (result.exitCode !== 0) {
9835
- const stderr = new TextDecoder().decode(result.stderr).trim();
9836
- throw new Error(`git ${args.join(" ")} failed: ${stderr || `exit ${result.exitCode}`}`);
10222
+ // backend/src/adapters/hooks.ts
10223
+ import { join as join7 } from "path";
10224
+ function buildErrorMessage(name, exitCode, stdout, stderr) {
10225
+ const output = stderr.trim() || stdout.trim();
10226
+ if (output) {
10227
+ return `${name} hook failed (exit ${exitCode}): ${output}`;
9837
10228
  }
9838
- return new TextDecoder().decode(result.stdout).trim();
9839
- }
9840
- function tryRunGit(args, cwd) {
9841
- const result = Bun.spawnSync(["git", ...args], {
9842
- cwd,
9843
- stdout: "pipe",
9844
- stderr: "pipe"
9845
- });
9846
- if (result.exitCode !== 0) {
9847
- return {
9848
- ok: false,
9849
- stderr: new TextDecoder().decode(result.stderr).trim()
9850
- };
9851
- }
9852
- return {
9853
- ok: true,
9854
- stdout: new TextDecoder().decode(result.stdout).trim()
9855
- };
9856
- }
9857
- function errorMessage(error) {
9858
- return error instanceof Error ? error.message : String(error);
9859
- }
9860
- function isRegisteredWorktree(entries, worktreePath) {
9861
- const resolvedPath = resolve2(worktreePath);
9862
- return entries.some((entry) => resolve2(entry.path) === resolvedPath);
9863
- }
9864
- function removeDirectory(path) {
9865
- rmSync2(path, {
9866
- recursive: true,
9867
- force: true
9868
- });
9869
- }
9870
- function currentCheckoutRef(cwd) {
9871
- const symbolicRef = tryRunGit(["symbolic-ref", "--quiet", "--short", "HEAD"], cwd);
9872
- if (symbolicRef.ok && symbolicRef.stdout.length > 0) {
9873
- return {
9874
- ref: symbolicRef.stdout,
9875
- branch: symbolicRef.stdout
9876
- };
9877
- }
9878
- return {
9879
- ref: runGit(["rev-parse", "--verify", "HEAD"], cwd),
9880
- branch: null
9881
- };
9882
- }
9883
- function resolveWorktreeRoot(cwd) {
9884
- const output = runGit(["rev-parse", "--show-toplevel"], cwd);
9885
- return resolve2(cwd, output);
9886
- }
9887
- function resolveWorktreeGitDir(cwd) {
9888
- const output = runGit(["rev-parse", "--git-dir"], cwd);
9889
- return resolve2(cwd, output);
9890
- }
9891
- function parseGitWorktreePorcelain(output) {
9892
- const entries = [];
9893
- let current = null;
9894
- const flush = () => {
9895
- if (current?.path)
9896
- entries.push(current);
9897
- current = null;
9898
- };
9899
- for (const rawLine of output.split(`
9900
- `)) {
9901
- const line = rawLine.trimEnd();
9902
- if (!line) {
9903
- flush();
9904
- continue;
9905
- }
9906
- if (line.startsWith("worktree ")) {
9907
- flush();
9908
- current = {
9909
- path: line.slice("worktree ".length),
9910
- branch: null,
9911
- head: null,
9912
- detached: false,
9913
- bare: false
9914
- };
9915
- continue;
9916
- }
9917
- if (!current)
9918
- continue;
9919
- if (line.startsWith("branch ")) {
9920
- current.branch = line.slice("branch ".length).replace(/^refs\/heads\//, "");
9921
- continue;
9922
- }
9923
- if (line.startsWith("HEAD ")) {
9924
- current.head = line.slice("HEAD ".length);
9925
- continue;
9926
- }
9927
- if (line === "detached") {
9928
- current.detached = true;
9929
- continue;
9930
- }
9931
- if (line === "bare") {
9932
- current.bare = true;
9933
- }
9934
- }
9935
- flush();
9936
- return entries;
9937
- }
9938
- function listGitWorktrees(cwd) {
9939
- const output = runGit(["worktree", "list", "--porcelain"], cwd);
9940
- return parseGitWorktreePorcelain(output);
9941
- }
9942
- function readGitWorktreeStatus(cwd) {
9943
- const dirtyOutput = runGit(["status", "--porcelain"], cwd);
9944
- const commit = tryRunGit(["rev-parse", "HEAD"], cwd);
9945
- const ahead = tryRunGit(["rev-list", "--count", "@{upstream}..HEAD"], cwd);
9946
- return {
9947
- dirty: dirtyOutput.length > 0,
9948
- aheadCount: ahead.ok ? parseInt(ahead.stdout, 10) || 0 : 0,
9949
- currentCommit: commit.ok && commit.stdout.length > 0 ? commit.stdout : null
9950
- };
9951
- }
9952
- function removeGitWorktree(opts, deps2 = {}) {
9953
- const args = ["worktree", "remove"];
9954
- if (opts.force)
9955
- args.push("--force");
9956
- args.push(opts.worktreePath);
9957
- const result = (deps2.tryRunGit ?? tryRunGit)(args, opts.repoRoot);
9958
- if (result.ok) {
9959
- return;
9960
- }
9961
- const failure = `git ${args.join(" ")} failed: ${result.stderr || "exit 1"}`;
9962
- const remainingWorktrees = (deps2.listWorktrees ?? listGitWorktrees)(opts.repoRoot);
9963
- if (isRegisteredWorktree(remainingWorktrees, opts.worktreePath)) {
9964
- throw new Error(failure);
9965
- }
9966
- try {
9967
- (deps2.removeDirectory ?? removeDirectory)(opts.worktreePath);
9968
- } catch (error) {
9969
- throw new Error(`${failure}; cleanup failed: ${errorMessage(error)}`);
9970
- }
9971
- }
9972
-
9973
- class BunGitGateway {
9974
- resolveWorktreeRoot(cwd) {
9975
- return resolveWorktreeRoot(cwd);
9976
- }
9977
- resolveWorktreeGitDir(cwd) {
9978
- return resolveWorktreeGitDir(cwd);
9979
- }
9980
- listWorktrees(cwd) {
9981
- return listGitWorktrees(cwd);
9982
- }
9983
- readWorktreeStatus(cwd) {
9984
- return readGitWorktreeStatus(cwd);
9985
- }
9986
- createWorktree(opts) {
9987
- const args = ["worktree", "add", "-b", opts.branch, opts.worktreePath];
9988
- if (opts.baseBranch)
9989
- args.push(opts.baseBranch);
9990
- runGit(args, opts.repoRoot);
9991
- }
9992
- removeWorktree(opts) {
9993
- removeGitWorktree(opts);
9994
- }
9995
- deleteBranch(repoRoot, branch, force = false) {
9996
- runGit(["branch", force ? "-D" : "-d", branch], repoRoot);
9997
- }
9998
- mergeBranch(opts) {
9999
- const current = currentCheckoutRef(opts.repoRoot);
10000
- const shouldRestore = current.branch !== opts.targetBranch;
10001
- if (shouldRestore) {
10002
- runGit(["checkout", opts.targetBranch], opts.repoRoot);
10003
- }
10004
- let mergeError = null;
10005
- const cleanupErrors = [];
10006
- try {
10007
- runGit(["merge", "--no-ff", "--no-edit", opts.sourceBranch], opts.repoRoot);
10008
- } catch (error) {
10009
- mergeError = errorMessage(error);
10010
- const abort = tryRunGit(["merge", "--abort"], opts.repoRoot);
10011
- if (!abort.ok && abort.stderr.length > 0 && !abort.stderr.includes("MERGE_HEAD missing")) {
10012
- cleanupErrors.push(`merge abort failed: ${abort.stderr}`);
10013
- }
10014
- }
10015
- if (shouldRestore) {
10016
- const restore = tryRunGit(["checkout", current.ref], opts.repoRoot);
10017
- if (!restore.ok) {
10018
- cleanupErrors.push(`restore checkout failed: ${restore.stderr}`);
10019
- }
10020
- }
10021
- if (mergeError) {
10022
- const suffix = cleanupErrors.length > 0 ? `; ${cleanupErrors.join("; ")}` : "";
10023
- throw new Error(`${mergeError}${suffix}`);
10024
- }
10025
- if (cleanupErrors.length > 0) {
10026
- throw new Error(cleanupErrors.join("; "));
10027
- }
10028
- }
10029
- currentBranch(repoRoot) {
10030
- return runGit(["branch", "--show-current"], repoRoot);
10031
- }
10032
- }
10033
- var init_git = () => {};
10034
-
10035
- // backend/src/adapters/hooks.ts
10036
- import { join as join7 } from "path";
10037
- function buildErrorMessage(name, exitCode, stdout, stderr) {
10038
- const output = stderr.trim() || stdout.trim();
10039
- if (output) {
10040
- return `${name} hook failed (exit ${exitCode}): ${output}`;
10041
- }
10042
- return `${name} hook failed (exit ${exitCode})`;
10229
+ return `${name} hook failed (exit ${exitCode})`;
10043
10230
  }
10044
10231
  function hasDirenv() {
10045
10232
  try {
@@ -10103,7 +10290,7 @@ class BunPortProbe {
10103
10290
  this.hostnames = hostnames;
10104
10291
  }
10105
10292
  isListening(port) {
10106
- return new Promise((resolve3) => {
10293
+ return new Promise((resolve4) => {
10107
10294
  let settled = false;
10108
10295
  let pending = this.hostnames.length;
10109
10296
  const settle = (result) => {
@@ -10112,20 +10299,20 @@ class BunPortProbe {
10112
10299
  if (result) {
10113
10300
  settled = true;
10114
10301
  clearTimeout(timer);
10115
- resolve3(true);
10302
+ resolve4(true);
10116
10303
  return;
10117
10304
  }
10118
10305
  pending--;
10119
10306
  if (pending === 0) {
10120
10307
  settled = true;
10121
10308
  clearTimeout(timer);
10122
- resolve3(false);
10309
+ resolve4(false);
10123
10310
  }
10124
10311
  };
10125
10312
  const timer = setTimeout(() => {
10126
10313
  if (!settled) {
10127
10314
  settled = true;
10128
- resolve3(false);
10315
+ resolve4(false);
10129
10316
  }
10130
10317
  }, this.timeoutMs);
10131
10318
  for (const hostname of this.hostnames) {
@@ -10149,10 +10336,6 @@ class BunPortProbe {
10149
10336
  }
10150
10337
 
10151
10338
  // backend/src/services/auto-name-service.ts
10152
- function buildPrompt(task) {
10153
- return `Task description:
10154
- ${task.trim()}`;
10155
- }
10156
10339
  function normalizeGeneratedBranchName(raw) {
10157
10340
  let branch = raw.trim();
10158
10341
  branch = branch.replace(/^```[\w-]*\s*/, "").replace(/\s*```$/, "");
@@ -10164,6 +10347,7 @@ function normalizeGeneratedBranchName(raw) {
10164
10347
  branch = branch.replace(/[/.]+/g, "-");
10165
10348
  branch = branch.replace(/-+/g, "-");
10166
10349
  branch = branch.replace(/^-+|-+$/g, "");
10350
+ branch = branch.slice(0, MAX_BRANCH_LENGTH).replace(/-+$/, "");
10167
10351
  if (!branch) {
10168
10352
  throw new Error("Auto-name model returned an empty branch name");
10169
10353
  }
@@ -10206,6 +10390,9 @@ function buildClaudeArgs(model, systemPrompt, prompt) {
10206
10390
  function escapeTomlString(s) {
10207
10391
  return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
10208
10392
  }
10393
+ function buildPrompt(prompt) {
10394
+ return `Here is the task description: ${prompt}. You MUST return the branch name only, no other text or comments. Be fast, make it simple, and concise.`;
10395
+ }
10209
10396
  function buildCodexArgs(model, systemPrompt, prompt) {
10210
10397
  const args = [
10211
10398
  "codex",
@@ -10252,20 +10439,21 @@ class AutoNameService {
10252
10439
  return normalizeGeneratedBranchName(output);
10253
10440
  }
10254
10441
  }
10255
- var DEFAULT_SYSTEM_PROMPT;
10442
+ var MAX_BRANCH_LENGTH = 40, DEFAULT_SYSTEM_PROMPT;
10256
10443
  var init_auto_name_service = __esm(() => {
10257
10444
  init_policies();
10258
10445
  DEFAULT_SYSTEM_PROMPT = [
10259
10446
  "Generate a concise git branch name from the task description.",
10260
10447
  "Return only the branch name.",
10261
10448
  "Use lowercase kebab-case.",
10449
+ `Maximum ${MAX_BRANCH_LENGTH} characters.`,
10262
10450
  "Do not include quotes, code fences, or prefixes like feature/ or fix/."
10263
10451
  ].join(" ");
10264
10452
  });
10265
10453
 
10266
10454
  // backend/src/adapters/agent-runtime.ts
10267
10455
  import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
10268
- import { dirname as dirname2, join as join8 } from "path";
10456
+ import { dirname as dirname3, join as join8 } from "path";
10269
10457
  function shellQuote(value) {
10270
10458
  return `'${value.replaceAll("'", "'\\''")}'`;
10271
10459
  }
@@ -10532,7 +10720,7 @@ async function ensureAgentRuntimeArtifacts(input) {
10532
10720
  agentCtlPath: join8(storagePaths.webmuxDir, "webmux-agentctl"),
10533
10721
  claudeSettingsPath: join8(input.worktreePath, ".claude", "settings.local.json")
10534
10722
  };
10535
- await mkdir3(dirname2(artifacts.claudeSettingsPath), { recursive: true });
10723
+ await mkdir3(dirname3(artifacts.claudeSettingsPath), { recursive: true });
10536
10724
  await Bun.write(artifacts.agentCtlPath, buildAgentCtlScript());
10537
10725
  await chmod2(artifacts.agentCtlPath, 493);
10538
10726
  const hookSettings = buildClaudeHookSettings(artifacts);
@@ -10555,15 +10743,22 @@ function buildRuntimeBootstrap(runtimeEnvPath) {
10555
10743
  return `set -a; . ${quoteShell(runtimeEnvPath)}; set +a`;
10556
10744
  }
10557
10745
  function buildAgentInvocation(input) {
10558
- const promptSuffix = input.prompt ? ` ${quoteShell(input.prompt)}` : "";
10559
10746
  if (input.agent === "codex") {
10560
10747
  const yoloFlag2 = input.yolo ? " --yolo" : "";
10748
+ if (input.launchMode === "resume") {
10749
+ return `codex${yoloFlag2} resume --last`;
10750
+ }
10751
+ const promptSuffix2 = input.prompt ? ` ${quoteShell(input.prompt)}` : "";
10561
10752
  if (input.systemPrompt) {
10562
- return `codex${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix}`;
10753
+ return `codex${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix2}`;
10563
10754
  }
10564
- return `codex${yoloFlag2}${promptSuffix}`;
10755
+ return `codex${yoloFlag2}${promptSuffix2}`;
10565
10756
  }
10566
10757
  const yoloFlag = input.yolo ? " --dangerously-skip-permissions" : "";
10758
+ if (input.launchMode === "resume") {
10759
+ return `claude${yoloFlag} --continue`;
10760
+ }
10761
+ const promptSuffix = input.prompt ? ` ${quoteShell(input.prompt)}` : "";
10567
10762
  if (input.systemPrompt) {
10568
10763
  return `claude${yoloFlag} --append-system-prompt ${quoteShell(input.systemPrompt)}${promptSuffix}`;
10569
10764
  }
@@ -10646,6 +10841,7 @@ function ensureSessionLayout(tmux, plan) {
10646
10841
  cwd: rootPane.cwd,
10647
10842
  command: plan.shellCommand
10648
10843
  });
10844
+ tmux.setWindowOption(plan.sessionName, plan.windowName, "pane-base-index", "0");
10649
10845
  tmux.setWindowOption(plan.sessionName, plan.windowName, "automatic-rename", "off");
10650
10846
  tmux.setWindowOption(plan.sessionName, plan.windowName, "allow-rename", "off");
10651
10847
  for (const pane of plan.panes.slice(1)) {
@@ -10704,10 +10900,12 @@ function rollbackManagedWorktreeCreation(opts, sessionLayoutPlan, git, deps2) {
10704
10900
  } catch (error) {
10705
10901
  cleanupErrors.push(`worktree rollback failed: ${toErrorMessage(error)}`);
10706
10902
  }
10707
- try {
10708
- git.deleteBranch(opts.repoRoot, opts.branch, true);
10709
- } catch (error) {
10710
- cleanupErrors.push(`branch rollback failed: ${toErrorMessage(error)}`);
10903
+ if (opts.deleteBranchOnRollback ?? true) {
10904
+ try {
10905
+ git.deleteBranch(opts.repoRoot, opts.branch, true);
10906
+ } catch (error) {
10907
+ cleanupErrors.push(`branch rollback failed: ${toErrorMessage(error)}`);
10908
+ }
10711
10909
  }
10712
10910
  return cleanupErrors.length > 0 ? joinErrorMessages(cleanupErrors) : null;
10713
10911
  }
@@ -10757,6 +10955,7 @@ async function createManagedWorktree(opts, deps2 = {}) {
10757
10955
  repoRoot: opts.repoRoot,
10758
10956
  worktreePath: opts.worktreePath,
10759
10957
  branch: opts.branch,
10958
+ mode: opts.mode,
10760
10959
  baseBranch: opts.baseBranch
10761
10960
  });
10762
10961
  worktreeCreated = true;
@@ -10819,7 +11018,7 @@ var init_worktree_service = __esm(() => {
10819
11018
  // backend/src/services/lifecycle-service.ts
10820
11019
  import { randomUUID as randomUUID2 } from "crypto";
10821
11020
  import { mkdir as mkdir4 } from "fs/promises";
10822
- import { dirname as dirname3, resolve as resolve3 } from "path";
11021
+ import { dirname as dirname4, resolve as resolve4 } from "path";
10823
11022
  function generateBranchName() {
10824
11023
  return `change-${randomUUID2().slice(0, 8)}`;
10825
11024
  }
@@ -10836,19 +11035,29 @@ class LifecycleService {
10836
11035
  this.deps = deps2;
10837
11036
  }
10838
11037
  async createWorktree(input) {
10839
- const branch = await this.resolveBranch(input.branch, input.prompt);
10840
- this.ensureBranchAvailable(branch);
11038
+ const mode = input.mode ?? "new";
11039
+ const branch = await this.resolveBranch(input.branch, input.prompt, mode);
11040
+ this.ensureBranchAvailable(branch, mode);
10841
11041
  const { profileName, profile } = this.resolveProfile(input.profile);
10842
11042
  const agent = this.resolveAgent(input.agent);
10843
11043
  const worktreePath = this.resolveWorktreePath(branch);
11044
+ const deleteBranchOnRollback = mode === "new";
10844
11045
  let initialized = null;
10845
11046
  try {
10846
- await mkdir4(dirname3(worktreePath), { recursive: true });
11047
+ await this.reportCreateProgress({
11048
+ branch,
11049
+ path: worktreePath,
11050
+ profile: profileName,
11051
+ agent,
11052
+ phase: "creating_worktree"
11053
+ });
11054
+ await mkdir4(dirname4(worktreePath), { recursive: true });
10847
11055
  initialized = await createManagedWorktree({
10848
11056
  repoRoot: this.deps.projectRoot,
10849
11057
  worktreePath,
10850
11058
  branch,
10851
- baseBranch: this.deps.config.workspace.mainBranch,
11059
+ mode,
11060
+ ...mode === "new" ? { baseBranch: this.deps.config.workspace.mainBranch } : {},
10852
11061
  profile: profileName,
10853
11062
  agent,
10854
11063
  runtime: profile.runtime,
@@ -10856,13 +11065,17 @@ class LifecycleService {
10856
11065
  allocatedPorts: await this.allocatePorts(),
10857
11066
  runtimeEnvExtras: { WEBMUX_WORKTREE_PATH: worktreePath },
10858
11067
  controlUrl: this.controlUrl(),
10859
- controlToken: await this.deps.getControlToken()
11068
+ controlToken: await this.deps.getControlToken(),
11069
+ deleteBranchOnRollback
10860
11070
  }, {
10861
11071
  git: this.deps.git
10862
11072
  });
10863
- await ensureAgentRuntimeArtifacts({
10864
- gitDir: initialized.paths.gitDir,
10865
- worktreePath
11073
+ await this.reportCreateProgress({
11074
+ branch,
11075
+ path: worktreePath,
11076
+ profile: profileName,
11077
+ agent,
11078
+ phase: "running_post_create_hook"
10866
11079
  });
10867
11080
  await this.runLifecycleHook({
10868
11081
  name: "postCreate",
@@ -10870,13 +11083,44 @@ class LifecycleService {
10870
11083
  meta: initialized.meta,
10871
11084
  worktreePath
10872
11085
  });
11086
+ initialized = await this.refreshManagedArtifactsFromMeta({
11087
+ gitDir: initialized.paths.gitDir,
11088
+ meta: initialized.meta,
11089
+ worktreePath
11090
+ });
11091
+ await this.reportCreateProgress({
11092
+ branch,
11093
+ path: worktreePath,
11094
+ profile: profileName,
11095
+ agent,
11096
+ phase: "preparing_runtime"
11097
+ });
11098
+ await ensureAgentRuntimeArtifacts({
11099
+ gitDir: initialized.paths.gitDir,
11100
+ worktreePath
11101
+ });
11102
+ await this.reportCreateProgress({
11103
+ branch,
11104
+ path: worktreePath,
11105
+ profile: profileName,
11106
+ agent,
11107
+ phase: "starting_session"
11108
+ });
10873
11109
  await this.materializeRuntimeSession({
10874
11110
  branch,
10875
11111
  profile,
10876
11112
  agent,
10877
11113
  initialized,
10878
11114
  worktreePath,
10879
- prompt: input.prompt
11115
+ prompt: input.prompt,
11116
+ launchMode: "fresh"
11117
+ });
11118
+ await this.reportCreateProgress({
11119
+ branch,
11120
+ path: worktreePath,
11121
+ profile: profileName,
11122
+ agent,
11123
+ phase: "reconciling"
10880
11124
  });
10881
11125
  await this.deps.reconciliation.reconcile(this.deps.projectRoot);
10882
11126
  return {
@@ -10885,17 +11129,20 @@ class LifecycleService {
10885
11129
  };
10886
11130
  } catch (error) {
10887
11131
  if (initialized) {
10888
- const cleanupError = await this.cleanupFailedCreate(branch, worktreePath, profile.runtime);
11132
+ const cleanupError = await this.cleanupFailedCreate(branch, worktreePath, profile.runtime, deleteBranchOnRollback);
10889
11133
  if (cleanupError) {
10890
11134
  throw this.wrapOperationError(new Error(`${toErrorMessage2(error)}; ${cleanupError}`));
10891
11135
  }
10892
11136
  }
10893
11137
  throw this.wrapOperationError(error);
11138
+ } finally {
11139
+ await this.finishCreateProgress(branch);
10894
11140
  }
10895
11141
  }
10896
11142
  async openWorktree(branch) {
10897
11143
  try {
10898
11144
  const resolved = await this.resolveExistingWorktree(branch);
11145
+ const launchMode = resolved.meta ? "resume" : "fresh";
10899
11146
  const initialized = resolved.meta ? await this.refreshManagedArtifacts(resolved) : await this.initializeUnmanagedWorktree(resolved);
10900
11147
  const { profile } = this.resolveProfile(initialized.meta.profile);
10901
11148
  await ensureAgentRuntimeArtifacts({
@@ -10907,7 +11154,8 @@ class LifecycleService {
10907
11154
  profile,
10908
11155
  agent: initialized.meta.agent,
10909
11156
  initialized,
10910
- worktreePath: resolved.entry.path
11157
+ worktreePath: resolved.entry.path,
11158
+ launchMode
10911
11159
  });
10912
11160
  await this.deps.reconciliation.reconcile(this.deps.projectRoot);
10913
11161
  return {
@@ -10935,6 +11183,20 @@ class LifecycleService {
10935
11183
  throw this.wrapOperationError(error);
10936
11184
  }
10937
11185
  }
11186
+ async pruneWorktrees() {
11187
+ try {
11188
+ const resolvedWorktrees = await this.resolveAllWorktrees();
11189
+ const removedBranches = [];
11190
+ for (const resolved of resolvedWorktrees) {
11191
+ const branch = resolved.entry.branch ?? resolved.entry.path;
11192
+ await this.removeResolvedWorktree(resolved);
11193
+ removedBranches.push(branch);
11194
+ }
11195
+ return { removedBranches };
11196
+ } catch (error) {
11197
+ throw this.wrapOperationError(error);
11198
+ }
11199
+ }
10938
11200
  async mergeWorktree(branch) {
10939
11201
  try {
10940
11202
  const resolved = await this.resolveExistingWorktree(branch);
@@ -10953,9 +11215,17 @@ class LifecycleService {
10953
11215
  throw this.wrapOperationError(error);
10954
11216
  }
10955
11217
  }
10956
- async resolveBranch(rawBranch, prompt) {
11218
+ listAvailableBranches() {
11219
+ const localBranches = this.listLocalBranches().filter((branch) => isValidBranchName(branch));
11220
+ const checkedOutBranches = this.listCheckedOutBranches();
11221
+ return localBranches.filter((branch) => !checkedOutBranches.has(branch)).sort((left, right) => left.localeCompare(right)).map((name) => ({ name }));
11222
+ }
11223
+ async resolveBranch(rawBranch, prompt, mode) {
10957
11224
  const explicitBranch = rawBranch?.trim();
10958
- const branch = explicitBranch || await this.generateAutoName(prompt) || generateBranchName();
11225
+ const branch = mode === "existing" ? explicitBranch : explicitBranch || await this.generateAutoName(prompt) || generateBranchName();
11226
+ if (!branch) {
11227
+ throw new LifecycleError("Existing branch is required", 400);
11228
+ }
10959
11229
  if (!isValidBranchName(branch)) {
10960
11230
  throw new LifecycleError(`Invalid branch name: ${branch}`, 400);
10961
11231
  }
@@ -10967,10 +11237,19 @@ class LifecycleService {
10967
11237
  }
10968
11238
  return await this.deps.autoName.generateBranchName(this.deps.config.autoName, prompt);
10969
11239
  }
10970
- ensureBranchAvailable(branch) {
10971
- const exists = this.listProjectWorktrees().some((entry) => entry.branch === branch);
10972
- if (exists) {
10973
- throw new LifecycleError(`Worktree already exists: ${branch}`, 409);
11240
+ ensureBranchAvailable(branch, mode) {
11241
+ const localBranches = new Set(this.listLocalBranches());
11242
+ if (mode === "new") {
11243
+ if (localBranches.has(branch)) {
11244
+ throw new LifecycleError(`Branch already exists: ${branch}`, 409);
11245
+ }
11246
+ return;
11247
+ }
11248
+ if (!localBranches.has(branch)) {
11249
+ throw new LifecycleError(`Branch not found: ${branch}`, 404);
11250
+ }
11251
+ if (this.listCheckedOutBranches().has(branch)) {
11252
+ throw new LifecycleError(`Branch already has a worktree: ${branch}`, 409);
10974
11253
  }
10975
11254
  }
10976
11255
  resolveProfile(profileName) {
@@ -11007,11 +11286,17 @@ class LifecycleService {
11007
11286
  return allocateServicePorts(metas, this.deps.config.services);
11008
11287
  }
11009
11288
  resolveWorktreePath(branch) {
11010
- return resolve3(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
11289
+ return resolve4(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
11290
+ }
11291
+ listLocalBranches() {
11292
+ return this.deps.git.listLocalBranches(resolve4(this.deps.projectRoot));
11293
+ }
11294
+ listCheckedOutBranches() {
11295
+ return new Set(this.deps.git.listWorktrees(resolve4(this.deps.projectRoot)).filter((entry) => !entry.bare && entry.branch !== null).map((entry) => entry.branch));
11011
11296
  }
11012
11297
  listProjectWorktrees() {
11013
- const projectRoot = resolve3(this.deps.projectRoot);
11014
- return this.deps.git.listWorktrees(projectRoot).filter((entry) => !entry.bare && resolve3(entry.path) !== projectRoot);
11298
+ const projectRoot = resolve4(this.deps.projectRoot);
11299
+ return this.deps.git.listWorktrees(projectRoot).filter((entry) => !entry.bare && resolve4(entry.path) !== projectRoot);
11015
11300
  }
11016
11301
  async readManagedMetas() {
11017
11302
  const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
@@ -11029,6 +11314,14 @@ class LifecycleService {
11029
11314
  const meta = await readWorktreeMeta(gitDir);
11030
11315
  return { entry, gitDir, meta };
11031
11316
  }
11317
+ async resolveAllWorktrees() {
11318
+ const entries = this.listProjectWorktrees().sort((left, right) => (left.branch ?? left.path).localeCompare(right.branch ?? right.path));
11319
+ return await Promise.all(entries.map(async (entry) => {
11320
+ const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
11321
+ const meta = await readWorktreeMeta(gitDir);
11322
+ return { entry, gitDir, meta };
11323
+ }));
11324
+ }
11032
11325
  async initializeUnmanagedWorktree(resolved) {
11033
11326
  const { profileName, profile } = this.resolveProfile(undefined);
11034
11327
  const dotenvValues = await loadDotenvLocal(resolved.entry.path);
@@ -11050,21 +11343,28 @@ class LifecycleService {
11050
11343
  if (!resolved.meta) {
11051
11344
  throw new Error("Missing managed metadata");
11052
11345
  }
11053
- const dotenvValues = await loadDotenvLocal(resolved.entry.path);
11054
- const runtimeEnv = buildRuntimeEnvMap(resolved.meta, {
11055
- WEBMUX_WORKTREE_PATH: resolved.entry.path
11346
+ return await this.refreshManagedArtifactsFromMeta({
11347
+ gitDir: resolved.gitDir,
11348
+ meta: resolved.meta,
11349
+ worktreePath: resolved.entry.path
11350
+ });
11351
+ }
11352
+ async refreshManagedArtifactsFromMeta(input) {
11353
+ const dotenvValues = await loadDotenvLocal(input.worktreePath);
11354
+ const runtimeEnv = buildRuntimeEnvMap(input.meta, {
11355
+ WEBMUX_WORKTREE_PATH: input.worktreePath
11056
11356
  }, dotenvValues);
11057
- await writeRuntimeEnv(resolved.gitDir, runtimeEnv);
11357
+ await writeRuntimeEnv(input.gitDir, runtimeEnv);
11058
11358
  const controlEnv = buildControlEnvMap({
11059
11359
  controlUrl: this.controlUrl(),
11060
11360
  controlToken: await this.deps.getControlToken(),
11061
- worktreeId: resolved.meta.worktreeId,
11062
- branch: resolved.meta.branch
11361
+ worktreeId: input.meta.worktreeId,
11362
+ branch: input.meta.branch
11063
11363
  });
11064
- await writeControlEnv(resolved.gitDir, controlEnv);
11364
+ await writeControlEnv(input.gitDir, controlEnv);
11065
11365
  return {
11066
- meta: resolved.meta,
11067
- paths: getWorktreeStoragePaths(resolved.gitDir),
11366
+ meta: input.meta,
11367
+ paths: getWorktreeStoragePaths(input.gitDir),
11068
11368
  runtimeEnv,
11069
11369
  controlEnv
11070
11370
  };
@@ -11087,6 +11387,7 @@ class LifecycleService {
11087
11387
  initialized: input.initialized,
11088
11388
  worktreePath: input.worktreePath,
11089
11389
  prompt: input.prompt,
11390
+ launchMode: input.launchMode,
11090
11391
  containerName: containerName2
11091
11392
  }));
11092
11393
  return;
@@ -11097,11 +11398,12 @@ class LifecycleService {
11097
11398
  agent: input.agent,
11098
11399
  initialized: input.initialized,
11099
11400
  worktreePath: input.worktreePath,
11100
- prompt: input.prompt
11401
+ prompt: input.prompt,
11402
+ launchMode: input.launchMode
11101
11403
  }));
11102
11404
  }
11103
11405
  buildSessionLayout(input) {
11104
- const systemPrompt = input.profile.systemPrompt ? expandTemplate(input.profile.systemPrompt, input.initialized.runtimeEnv) : undefined;
11406
+ const systemPrompt = input.launchMode === "fresh" && input.profile.systemPrompt ? expandTemplate(input.profile.systemPrompt, input.initialized.runtimeEnv) : undefined;
11105
11407
  const containerName2 = input.containerName;
11106
11408
  return planSessionLayout(this.deps.projectRoot, input.branch, input.profile.panes, {
11107
11409
  repoRoot: this.deps.projectRoot,
@@ -11114,7 +11416,8 @@ class LifecycleService {
11114
11416
  runtimeEnvPath: input.initialized.paths.runtimeEnvPath,
11115
11417
  yolo: input.profile.yolo === true,
11116
11418
  systemPrompt,
11117
- prompt: input.prompt
11419
+ prompt: input.launchMode === "fresh" ? input.prompt : undefined,
11420
+ launchMode: input.launchMode
11118
11421
  }),
11119
11422
  shell: buildDockerShellCommand(containerName2, input.worktreePath, input.initialized.paths.runtimeEnvPath)
11120
11423
  } : {
@@ -11123,7 +11426,8 @@ class LifecycleService {
11123
11426
  runtimeEnvPath: input.initialized.paths.runtimeEnvPath,
11124
11427
  yolo: input.profile.yolo === true,
11125
11428
  systemPrompt,
11126
- prompt: input.prompt
11429
+ prompt: input.launchMode === "fresh" ? input.prompt : undefined,
11430
+ launchMode: input.launchMode
11127
11431
  }),
11128
11432
  shell: buildManagedShellCommand(input.initialized.paths.runtimeEnvPath)
11129
11433
  }
@@ -11135,7 +11439,7 @@ class LifecycleService {
11135
11439
  }
11136
11440
  return profile;
11137
11441
  }
11138
- async cleanupFailedCreate(branch, worktreePath, runtime) {
11442
+ async cleanupFailedCreate(branch, worktreePath, runtime, deleteBranch) {
11139
11443
  const cleanupErrors = [];
11140
11444
  if (runtime === "docker") {
11141
11445
  try {
@@ -11155,8 +11459,8 @@ class LifecycleService {
11155
11459
  worktreePath,
11156
11460
  branch,
11157
11461
  force: true,
11158
- deleteBranch: true,
11159
- deleteBranchForce: true
11462
+ deleteBranch,
11463
+ deleteBranchForce: deleteBranch
11160
11464
  }, this.deps.git);
11161
11465
  } catch (error) {
11162
11466
  cleanupErrors.push(`worktree cleanup failed: ${toErrorMessage2(error)}`);
@@ -11212,6 +11516,12 @@ class LifecycleService {
11212
11516
  });
11213
11517
  console.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
11214
11518
  }
11519
+ async reportCreateProgress(progress) {
11520
+ await this.deps.onCreateProgress?.(progress);
11521
+ }
11522
+ async finishCreateProgress(branch) {
11523
+ await this.deps.onCreateFinished?.(branch);
11524
+ }
11215
11525
  wrapOperationError(error) {
11216
11526
  if (error instanceof LifecycleError) {
11217
11527
  return error;
@@ -11509,9 +11819,9 @@ var init_project_runtime = __esm(() => {
11509
11819
  });
11510
11820
 
11511
11821
  // backend/src/services/reconciliation-service.ts
11512
- import { basename as basename3, resolve as resolve4 } from "path";
11822
+ import { basename as basename4, resolve as resolve5 } from "path";
11513
11823
  function makeUnmanagedWorktreeId(path) {
11514
- return `unmanaged:${resolve4(path)}`;
11824
+ return `unmanaged:${resolve5(path)}`;
11515
11825
  }
11516
11826
  function isValidPort2(port) {
11517
11827
  return port !== null && Number.isInteger(port) && port >= 1 && port <= 65535;
@@ -11544,7 +11854,7 @@ function findWindow(windows, sessionName, branch) {
11544
11854
  return windows.find((window) => window.sessionName === sessionName && window.windowName === windowName) ?? null;
11545
11855
  }
11546
11856
  function resolveBranch(entry, metaBranch) {
11547
- const fallback = basename3(entry.path);
11857
+ const fallback = basename4(entry.path);
11548
11858
  return entry.branch ?? metaBranch ?? (fallback.length > 0 ? fallback : "unknown");
11549
11859
  }
11550
11860
 
@@ -11554,7 +11864,7 @@ class ReconciliationService {
11554
11864
  this.deps = deps2;
11555
11865
  }
11556
11866
  async reconcile(repoRoot) {
11557
- const normalizedRepoRoot = resolve4(repoRoot);
11867
+ const normalizedRepoRoot = resolve5(repoRoot);
11558
11868
  const worktrees = this.deps.git.listWorktrees(normalizedRepoRoot);
11559
11869
  const sessionName = buildProjectSessionName(normalizedRepoRoot);
11560
11870
  let windows = [];
@@ -11567,7 +11877,7 @@ class ReconciliationService {
11567
11877
  for (const entry of worktrees) {
11568
11878
  if (entry.bare)
11569
11879
  continue;
11570
- if (resolve4(entry.path) === normalizedRepoRoot)
11880
+ if (resolve5(entry.path) === normalizedRepoRoot)
11571
11881
  continue;
11572
11882
  const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
11573
11883
  const meta = await readWorktreeMeta(gitDir);
@@ -11624,9 +11934,33 @@ var init_reconciliation_service = __esm(() => {
11624
11934
  init_fs();
11625
11935
  });
11626
11936
 
11937
+ // backend/src/services/worktree-creation-service.ts
11938
+ class WorktreeCreationTracker {
11939
+ worktrees = new Map;
11940
+ set(progress) {
11941
+ const next = {
11942
+ branch: progress.branch,
11943
+ path: progress.path,
11944
+ profile: progress.profile,
11945
+ agentName: progress.agent,
11946
+ phase: progress.phase
11947
+ };
11948
+ this.worktrees.set(progress.branch, next);
11949
+ }
11950
+ clear(branch) {
11951
+ return this.worktrees.delete(branch);
11952
+ }
11953
+ has(branch) {
11954
+ return this.worktrees.has(branch);
11955
+ }
11956
+ list() {
11957
+ return [...this.worktrees.values()].sort((left, right) => left.branch.localeCompare(right.branch)).map((state) => ({ ...state }));
11958
+ }
11959
+ }
11960
+
11627
11961
  // backend/src/runtime.ts
11628
11962
  function createWebmuxRuntime(options = {}) {
11629
- const port = options.port ?? parseInt(Bun.env.BACKEND_PORT || "5111", 10);
11963
+ const port = options.port ?? parseInt(Bun.env.PORT || "5111", 10);
11630
11964
  const projectDir = gitRoot2(options.projectDir ?? Bun.env.WEBMUX_PROJECT_DIR ?? process.cwd());
11631
11965
  const config = loadConfig(projectDir);
11632
11966
  const git = new BunGitGateway;
@@ -11636,6 +11970,7 @@ function createWebmuxRuntime(options = {}) {
11636
11970
  const hooks = new BunLifecycleHookRunner;
11637
11971
  const autoName = new AutoNameService;
11638
11972
  const projectRuntime = new ProjectRuntime;
11973
+ const worktreeCreationTracker = new WorktreeCreationTracker;
11639
11974
  const runtimeNotifications = new NotificationService;
11640
11975
  const reconciliationService = new ReconciliationService({
11641
11976
  config,
@@ -11654,7 +11989,13 @@ function createWebmuxRuntime(options = {}) {
11654
11989
  docker,
11655
11990
  reconciliation: reconciliationService,
11656
11991
  hooks,
11657
- autoName
11992
+ autoName,
11993
+ onCreateProgress: (progress) => {
11994
+ worktreeCreationTracker.set(progress);
11995
+ },
11996
+ onCreateFinished: (branch) => {
11997
+ worktreeCreationTracker.clear(branch);
11998
+ }
11658
11999
  });
11659
12000
  return {
11660
12001
  port,
@@ -11667,6 +12008,7 @@ function createWebmuxRuntime(options = {}) {
11667
12008
  hooks,
11668
12009
  autoName,
11669
12010
  projectRuntime,
12011
+ worktreeCreationTracker,
11670
12012
  runtimeNotifications,
11671
12013
  reconciliationService,
11672
12014
  lifecycleService
@@ -11693,7 +12035,7 @@ __export(exports_worktree_commands, {
11693
12035
  parseAddCommandArgs: () => parseAddCommandArgs,
11694
12036
  getWorktreeCommandUsage: () => getWorktreeCommandUsage
11695
12037
  });
11696
- import { basename as basename4, resolve as resolve5 } from "path";
12038
+ import { basename as basename5, resolve as resolve6 } from "path";
11697
12039
  function getWorktreeCommandUsage(command) {
11698
12040
  switch (command) {
11699
12041
  case "add":
@@ -11724,6 +12066,9 @@ function getWorktreeCommandUsage(command) {
11724
12066
  case "merge":
11725
12067
  return `Usage:
11726
12068
  webmux merge <branch>`;
12069
+ case "prune":
12070
+ return `Usage:
12071
+ webmux prune`;
11727
12072
  }
11728
12073
  }
11729
12074
  function readOptionValue(args, index, flag) {
@@ -11826,8 +12171,31 @@ function parseBranchCommandArgs(args) {
11826
12171
  }
11827
12172
  return branch;
11828
12173
  }
12174
+ function parsePruneCommandArgs(args) {
12175
+ for (const arg of args) {
12176
+ if (arg === "--help" || arg === "-h") {
12177
+ return false;
12178
+ }
12179
+ if (arg.startsWith("-")) {
12180
+ throw new CommandUsageError(`Unknown option: ${arg}`);
12181
+ }
12182
+ throw new CommandUsageError(`Unexpected argument: ${arg}`);
12183
+ }
12184
+ return true;
12185
+ }
12186
+ function listProjectWorktrees(runtime) {
12187
+ const projectDir = resolve6(runtime.projectDir);
12188
+ return runtime.git.listWorktrees(projectDir).filter((entry) => !entry.bare && resolve6(entry.path) !== projectDir);
12189
+ }
12190
+ async function defaultConfirmPrune(worktreeCount) {
12191
+ const response = await Rt({
12192
+ message: `Prune all ${worktreeCount} worktree${worktreeCount === 1 ? "" : "s"}? This action cannot be undone.`,
12193
+ initialValue: false
12194
+ });
12195
+ return !Ct(response) && response;
12196
+ }
11829
12197
  function defaultSwitchToTmuxWindow(projectDir, branch) {
11830
- const sessionName = buildProjectSessionName(resolve5(projectDir));
12198
+ const sessionName = buildProjectSessionName(resolve6(projectDir));
11831
12199
  const windowName = buildWorktreeWindowName(branch);
11832
12200
  const target = `${sessionName}:${windowName}`;
11833
12201
  const selectResult = Bun.spawnSync(["tmux", "select-window", "-t", target], {
@@ -11856,8 +12224,8 @@ function defaultSwitchToTmuxWindow(projectDir, branch) {
11856
12224
  }
11857
12225
  }
11858
12226
  async function listWorktrees(runtime, stdout) {
11859
- const projectDir = resolve5(runtime.projectDir);
11860
- const entries = runtime.git.listWorktrees(projectDir).filter((entry) => !entry.bare && resolve5(entry.path) !== projectDir);
12227
+ const projectDir = resolve6(runtime.projectDir);
12228
+ const entries = listProjectWorktrees(runtime);
11861
12229
  if (entries.length === 0) {
11862
12230
  stdout("No worktrees found.");
11863
12231
  return;
@@ -11871,7 +12239,7 @@ async function listWorktrees(runtime, stdout) {
11871
12239
  }
11872
12240
  const openWindows = new Set(windows.filter((w) => w.sessionName === sessionName).map((w) => w.windowName));
11873
12241
  const rows = await Promise.all(entries.map(async (entry) => {
11874
- const branch = entry.branch ?? basename4(entry.path);
12242
+ const branch = entry.branch ?? basename5(entry.path);
11875
12243
  const isOpen = openWindows.has(buildWorktreeWindowName(branch));
11876
12244
  const gitDir = runtime.git.resolveWorktreeGitDir(entry.path);
11877
12245
  const meta = await readWorktreeMeta(gitDir);
@@ -11890,6 +12258,7 @@ async function runWorktreeCommand(context, deps2 = {}) {
11890
12258
  const stdout = deps2.stdout ?? ((message) => console.log(message));
11891
12259
  const stderr = deps2.stderr ?? ((message) => console.error(message));
11892
12260
  const switchToTmuxWindow = deps2.switchToTmuxWindow ?? defaultSwitchToTmuxWindow;
12261
+ const confirmPrune = deps2.confirmPrune ?? defaultConfirmPrune;
11893
12262
  try {
11894
12263
  if (context.command === "add") {
11895
12264
  const input = parseAddCommandArgs(context.args);
@@ -11918,6 +12287,32 @@ async function runWorktreeCommand(context, deps2 = {}) {
11918
12287
  await listWorktrees(runtime2, stdout);
11919
12288
  return 0;
11920
12289
  }
12290
+ if (context.command === "prune") {
12291
+ if (!parsePruneCommandArgs(context.args)) {
12292
+ stdout(getWorktreeCommandUsage("prune"));
12293
+ return 0;
12294
+ }
12295
+ const runtime2 = createRuntime({
12296
+ projectDir: context.projectDir,
12297
+ port: context.port
12298
+ });
12299
+ const worktrees = listProjectWorktrees(runtime2);
12300
+ if (worktrees.length === 0) {
12301
+ stdout("No worktrees found.");
12302
+ return 0;
12303
+ }
12304
+ if (!await confirmPrune(worktrees.length)) {
12305
+ stdout("Aborted.");
12306
+ return 0;
12307
+ }
12308
+ const result = await runtime2.lifecycleService.pruneWorktrees();
12309
+ if (result.removedBranches.length === 0) {
12310
+ stdout("No worktrees found.");
12311
+ return 0;
12312
+ }
12313
+ stdout(`Pruned ${result.removedBranches.length} worktree${result.removedBranches.length === 1 ? "" : "s"}: ${result.removedBranches.join(", ")}`);
12314
+ return 0;
12315
+ }
11921
12316
  const command = context.command;
11922
12317
  const branch = parseBranchCommandArgs(context.args);
11923
12318
  if (!branch) {
@@ -11953,6 +12348,7 @@ async function runWorktreeCommand(context, deps2 = {}) {
11953
12348
  }
11954
12349
  var CommandUsageError;
11955
12350
  var init_worktree_commands = __esm(() => {
12351
+ init_dist2();
11956
12352
  init_fs();
11957
12353
  init_tmux();
11958
12354
  init_policies();
@@ -11962,10 +12358,67 @@ var init_worktree_commands = __esm(() => {
11962
12358
  });
11963
12359
 
11964
12360
  // bin/src/webmux.ts
11965
- import { resolve as resolve6, dirname as dirname4, join as join10 } from "path";
12361
+ import { resolve as resolve7, dirname as dirname5, join as join10 } from "path";
11966
12362
  import { existsSync as existsSync5 } from "fs";
11967
12363
  import { fileURLToPath } from "url";
11968
- var PKG_ROOT = resolve6(dirname4(fileURLToPath(import.meta.url)), "..");
12364
+ // package.json
12365
+ var package_default = {
12366
+ name: "webmux",
12367
+ version: "0.13.0",
12368
+ description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
12369
+ type: "module",
12370
+ repository: {
12371
+ type: "git",
12372
+ url: "git+https://github.com/windmill-labs/workmux-web.git"
12373
+ },
12374
+ homepage: "https://github.com/windmill-labs/workmux-web",
12375
+ keywords: [
12376
+ "workmux",
12377
+ "git-worktree",
12378
+ "tmux",
12379
+ "dashboard",
12380
+ "terminal",
12381
+ "ai-agent"
12382
+ ],
12383
+ bin: {
12384
+ webmux: "bin/webmux.js"
12385
+ },
12386
+ workspaces: [
12387
+ "backend",
12388
+ "frontend"
12389
+ ],
12390
+ scripts: {
12391
+ dev: "bash dev.sh",
12392
+ start: "bun bin/webmux.js",
12393
+ build: "cd frontend && bun run build && cd .. && bun build backend/src/server.ts --target=bun --outfile=backend/dist/server.js && bun build bin/src/webmux.ts --target=bun --outfile=bin/webmux.js",
12394
+ prepublishOnly: "bun run build",
12395
+ test: "bun run --cwd backend test && bun test bin/src && bun run --cwd frontend test",
12396
+ "test:coverage": "bun run --cwd backend test --coverage && bun test --coverage bin/src && bun run --cwd frontend test:coverage"
12397
+ },
12398
+ files: [
12399
+ "bin/webmux.js",
12400
+ "backend/dist/",
12401
+ "frontend/dist/"
12402
+ ],
12403
+ devDependencies: {
12404
+ "@clack/prompts": "^1.1.0",
12405
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
12406
+ "@tailwindcss/vite": "^4.2.0",
12407
+ "@types/bun": "latest",
12408
+ "@xterm/addon-fit": "^0.10.0",
12409
+ "@xterm/addon-web-links": "^0.11.0",
12410
+ "@xterm/xterm": "^5.5.0",
12411
+ svelte: "^5.0.0",
12412
+ "svelte-check": "^4.0.0",
12413
+ tailwindcss: "^4.2.0",
12414
+ typescript: "^5.0.0",
12415
+ vite: "^6.0.0"
12416
+ },
12417
+ license: "MIT"
12418
+ };
12419
+
12420
+ // bin/src/webmux.ts
12421
+ var PKG_ROOT = resolve7(dirname5(fileURLToPath(import.meta.url)), "..");
11969
12422
  function usage2() {
11970
12423
  console.log(`
11971
12424
  webmux \u2014 Dev dashboard for managing Git worktrees
@@ -11981,21 +12434,27 @@ Usage:
11981
12434
  webmux close Close a worktree session without removing it
11982
12435
  webmux remove Remove a worktree
11983
12436
  webmux merge Merge a worktree into the main branch and remove it
12437
+ webmux prune Remove all worktrees in the current project
12438
+ webmux completion Generate shell completion script (bash, zsh)
11984
12439
 
11985
12440
  Options:
11986
12441
  --port N Set port (default: 5111)
11987
12442
  --debug Show debug-level logs
12443
+ --version Show version number
11988
12444
  --help Show this help message
11989
12445
 
11990
12446
  Environment:
11991
- BACKEND_PORT Same as --port (flag takes precedence)
12447
+ PORT Same as --port (flag takes precedence)
11992
12448
  `);
11993
12449
  }
11994
12450
  function isRootCommand(value) {
11995
- return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "list" || value === "open" || value === "close" || value === "remove" || value === "merge";
12451
+ return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "list" || value === "open" || value === "close" || value === "remove" || value === "merge" || value === "prune" || value === "completion";
12452
+ }
12453
+ function isServeRootOption(value) {
12454
+ return value === "--port" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
11996
12455
  }
11997
12456
  function parseRootArgs(args) {
11998
- let port = parseInt(process.env.BACKEND_PORT || "5111", 10);
12457
+ let port = parseInt(process.env.PORT || "5111", 10);
11999
12458
  let debug = false;
12000
12459
  let command = null;
12001
12460
  const commandArgs = [];
@@ -12003,7 +12462,7 @@ function parseRootArgs(args) {
12003
12462
  const arg = args[index];
12004
12463
  if (!arg)
12005
12464
  continue;
12006
- if (command) {
12465
+ if (command && (command !== "serve" || !isServeRootOption(arg))) {
12007
12466
  commandArgs.push(arg);
12008
12467
  continue;
12009
12468
  }
@@ -12023,6 +12482,11 @@ function parseRootArgs(args) {
12023
12482
  case "--debug":
12024
12483
  debug = true;
12025
12484
  break;
12485
+ case "--version":
12486
+ case "-V":
12487
+ console.log(package_default.version);
12488
+ process.exit(0);
12489
+ break;
12026
12490
  case "--help":
12027
12491
  case "-h":
12028
12492
  usage2();
@@ -12043,34 +12507,7 @@ Run webmux --help for usage.`);
12043
12507
  };
12044
12508
  }
12045
12509
  function isWorktreeCommand(command) {
12046
- return command === "add" || command === "list" || command === "open" || command === "close" || command === "remove" || command === "merge";
12047
- }
12048
- var args = process.argv.slice(2);
12049
- var parsed;
12050
- try {
12051
- parsed = parseRootArgs(args);
12052
- } catch (error) {
12053
- console.error(error instanceof Error ? error.message : String(error));
12054
- process.exit(1);
12055
- }
12056
- if (parsed.command === "init") {
12057
- await init_init().then(() => exports_init);
12058
- process.exit(0);
12059
- }
12060
- if (parsed.command === "service") {
12061
- const { default: service2 } = await Promise.resolve().then(() => (init_service(), exports_service));
12062
- await service2(parsed.commandArgs);
12063
- process.exit(0);
12064
- }
12065
- if (parsed.command === "update") {
12066
- console.log("Updating webmux to the latest version...");
12067
- const proc = Bun.spawn(["bun", "install", "--global", "webmux@latest"], {
12068
- stdin: "inherit",
12069
- stdout: "inherit",
12070
- stderr: "inherit"
12071
- });
12072
- const code = await proc.exited;
12073
- process.exit(code);
12510
+ return command === "add" || command === "list" || command === "open" || command === "close" || command === "remove" || command === "merge" || command === "prune";
12074
12511
  }
12075
12512
  async function loadEnvFile(path) {
12076
12513
  if (!existsSync5(path))
@@ -12091,32 +12528,6 @@ async function loadEnvFile(path) {
12091
12528
  }
12092
12529
  }
12093
12530
  }
12094
- await loadEnvFile(resolve6(process.cwd(), ".env.local"));
12095
- await loadEnvFile(resolve6(process.cwd(), ".env"));
12096
- if (isWorktreeCommand(parsed.command)) {
12097
- const { runWorktreeCommand: runWorktreeCommand2 } = await Promise.resolve().then(() => (init_worktree_commands(), exports_worktree_commands));
12098
- const exitCode = await runWorktreeCommand2({
12099
- command: parsed.command,
12100
- args: parsed.commandArgs,
12101
- projectDir: process.cwd(),
12102
- port: parsed.port
12103
- });
12104
- process.exit(exitCode);
12105
- }
12106
- if (parsed.command === null) {
12107
- usage2();
12108
- process.exit(0);
12109
- }
12110
- if (!existsSync5(resolve6(process.cwd(), ".webmux.yaml"))) {
12111
- console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
12112
- process.exit(1);
12113
- }
12114
- var baseEnv = {
12115
- ...process.env,
12116
- BACKEND_PORT: String(parsed.port),
12117
- WEBMUX_PROJECT_DIR: process.cwd(),
12118
- ...parsed.debug ? { WEBMUX_DEBUG: "1" } : {}
12119
- };
12120
12531
  function pipeWithPrefix(stream, prefix) {
12121
12532
  const reader = stream.getReader();
12122
12533
  const decoder = new TextDecoder;
@@ -12139,41 +12550,110 @@ function pipeWithPrefix(stream, prefix) {
12139
12550
  }
12140
12551
  })();
12141
12552
  }
12142
- var children = [];
12143
- var exiting = false;
12144
- function cleanup() {
12145
- if (exiting)
12553
+ async function main(args = process.argv.slice(2)) {
12554
+ if (args[0] === "--completions") {
12555
+ const { handleCompletions: handleCompletions2 } = await Promise.resolve().then(() => (init_completions(), exports_completions));
12556
+ handleCompletions2(args.slice(1));
12146
12557
  return;
12147
- exiting = true;
12148
- for (const child of children) {
12149
- try {
12150
- child.kill("SIGTERM");
12151
- } catch {}
12152
12558
  }
12153
- setTimeout(() => {
12559
+ let parsed;
12560
+ try {
12561
+ parsed = parseRootArgs(args);
12562
+ } catch (error) {
12563
+ console.error(error instanceof Error ? error.message : String(error));
12564
+ process.exit(1);
12565
+ }
12566
+ if (parsed.command === "completion") {
12567
+ const { runCompletionCommand: runCompletionCommand2 } = await Promise.resolve().then(() => (init_completions(), exports_completions));
12568
+ process.exit(runCompletionCommand2(parsed.commandArgs));
12569
+ }
12570
+ if (parsed.command === "init") {
12571
+ await init_init().then(() => exports_init);
12572
+ process.exit(0);
12573
+ }
12574
+ if (parsed.command === "service") {
12575
+ const { default: service2 } = await Promise.resolve().then(() => (init_service(), exports_service));
12576
+ await service2(parsed.commandArgs);
12577
+ process.exit(0);
12578
+ }
12579
+ if (parsed.command === "update") {
12580
+ console.log("Updating webmux to the latest version...");
12581
+ const proc = Bun.spawn(["bun", "install", "--global", "webmux@latest"], {
12582
+ stdin: "inherit",
12583
+ stdout: "inherit",
12584
+ stderr: "inherit"
12585
+ });
12586
+ const code = await proc.exited;
12587
+ process.exit(code);
12588
+ }
12589
+ await loadEnvFile(resolve7(process.cwd(), ".env.local"));
12590
+ await loadEnvFile(resolve7(process.cwd(), ".env"));
12591
+ if (isWorktreeCommand(parsed.command)) {
12592
+ const { runWorktreeCommand: runWorktreeCommand2 } = await Promise.resolve().then(() => (init_worktree_commands(), exports_worktree_commands));
12593
+ const exitCode = await runWorktreeCommand2({
12594
+ command: parsed.command,
12595
+ args: parsed.commandArgs,
12596
+ projectDir: process.cwd(),
12597
+ port: parsed.port
12598
+ });
12599
+ process.exit(exitCode);
12600
+ }
12601
+ if (parsed.command === null) {
12602
+ usage2();
12603
+ process.exit(0);
12604
+ }
12605
+ if (!existsSync5(resolve7(process.cwd(), ".webmux.yaml"))) {
12606
+ console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
12607
+ process.exit(1);
12608
+ }
12609
+ const baseEnv = {
12610
+ ...process.env,
12611
+ PORT: String(parsed.port),
12612
+ WEBMUX_PROJECT_DIR: process.cwd(),
12613
+ ...parsed.debug ? { WEBMUX_DEBUG: "1" } : {}
12614
+ };
12615
+ const children = [];
12616
+ let exiting = false;
12617
+ function cleanup() {
12618
+ if (exiting)
12619
+ return;
12620
+ exiting = true;
12154
12621
  for (const child of children) {
12155
12622
  try {
12156
- child.kill("SIGKILL");
12623
+ child.kill("SIGTERM");
12157
12624
  } catch {}
12158
12625
  }
12159
- process.exit(0);
12160
- }, 1000).unref();
12161
- }
12162
- process.on("SIGINT", cleanup);
12163
- process.on("SIGTERM", cleanup);
12164
- var backendEntry = join10(PKG_ROOT, "backend", "dist", "server.js");
12165
- var staticDir = join10(PKG_ROOT, "frontend", "dist");
12166
- if (!existsSync5(staticDir)) {
12167
- console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
12168
- process.exit(1);
12169
- }
12170
- console.log(`Starting webmux on port ${parsed.port}...`);
12171
- var be = Bun.spawn(["bun", backendEntry], {
12172
- env: { ...baseEnv, WEBMUX_STATIC_DIR: staticDir },
12173
- stdout: "pipe",
12174
- stderr: "pipe"
12175
- });
12176
- children.push(be);
12177
- pipeWithPrefix(be.stdout, "[BE]");
12178
- pipeWithPrefix(be.stderr, "[BE]");
12179
- await be.exited;
12626
+ setTimeout(() => {
12627
+ for (const child of children) {
12628
+ try {
12629
+ child.kill("SIGKILL");
12630
+ } catch {}
12631
+ }
12632
+ process.exit(0);
12633
+ }, 1000).unref();
12634
+ }
12635
+ process.on("SIGINT", cleanup);
12636
+ process.on("SIGTERM", cleanup);
12637
+ const backendEntry = join10(PKG_ROOT, "backend", "dist", "server.js");
12638
+ const staticDir = join10(PKG_ROOT, "frontend", "dist");
12639
+ if (!existsSync5(staticDir)) {
12640
+ console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
12641
+ process.exit(1);
12642
+ }
12643
+ console.log(`Starting webmux on port ${parsed.port}...`);
12644
+ const be = Bun.spawn(["bun", backendEntry], {
12645
+ env: { ...baseEnv, WEBMUX_STATIC_DIR: staticDir },
12646
+ stdout: "pipe",
12647
+ stderr: "pipe"
12648
+ });
12649
+ children.push(be);
12650
+ pipeWithPrefix(be.stdout, "[BE]");
12651
+ pipeWithPrefix(be.stderr, "[BE]");
12652
+ await be.exited;
12653
+ }
12654
+ if (import.meta.main) {
12655
+ await main();
12656
+ }
12657
+ export {
12658
+ parseRootArgs
12659
+ };