webmux 0.11.0 → 0.12.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,387 @@ 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 readGitWorktreeStatus(cwd) {
168
+ const dirtyOutput = runGit(["status", "--porcelain"], cwd);
169
+ const commit = tryRunGit(["rev-parse", "HEAD"], cwd);
170
+ const ahead = tryRunGit(["rev-list", "--count", "@{upstream}..HEAD"], cwd);
171
+ return {
172
+ dirty: dirtyOutput.length > 0,
173
+ aheadCount: ahead.ok ? parseInt(ahead.stdout, 10) || 0 : 0,
174
+ currentCommit: commit.ok && commit.stdout.length > 0 ? commit.stdout : null
175
+ };
176
+ }
177
+ function removeGitWorktree(opts, deps = {}) {
178
+ const args = ["worktree", "remove"];
179
+ if (opts.force)
180
+ args.push("--force");
181
+ args.push(opts.worktreePath);
182
+ const result = (deps.tryRunGit ?? tryRunGit)(args, opts.repoRoot);
183
+ if (result.ok) {
184
+ return;
185
+ }
186
+ const failure = `git ${args.join(" ")} failed: ${result.stderr || "exit 1"}`;
187
+ const remainingWorktrees = (deps.listWorktrees ?? listGitWorktrees)(opts.repoRoot);
188
+ if (isRegisteredWorktree(remainingWorktrees, opts.worktreePath)) {
189
+ throw new Error(failure);
190
+ }
191
+ try {
192
+ (deps.removeDirectory ?? removeDirectory)(opts.worktreePath);
193
+ } catch (error) {
194
+ throw new Error(`${failure}; cleanup failed: ${errorMessage(error)}`);
195
+ }
196
+ }
197
+
198
+ class BunGitGateway {
199
+ resolveWorktreeRoot(cwd) {
200
+ return resolveWorktreeRoot(cwd);
201
+ }
202
+ resolveWorktreeGitDir(cwd) {
203
+ return resolveWorktreeGitDir(cwd);
204
+ }
205
+ listWorktrees(cwd) {
206
+ return listGitWorktrees(cwd);
207
+ }
208
+ readWorktreeStatus(cwd) {
209
+ return readGitWorktreeStatus(cwd);
210
+ }
211
+ createWorktree(opts) {
212
+ const args = ["worktree", "add", "-b", opts.branch, opts.worktreePath];
213
+ if (opts.baseBranch)
214
+ args.push(opts.baseBranch);
215
+ runGit(args, opts.repoRoot);
216
+ }
217
+ removeWorktree(opts) {
218
+ removeGitWorktree(opts);
219
+ }
220
+ deleteBranch(repoRoot, branch, force = false) {
221
+ runGit(["branch", force ? "-D" : "-d", branch], repoRoot);
222
+ }
223
+ mergeBranch(opts) {
224
+ const current = currentCheckoutRef(opts.repoRoot);
225
+ const shouldRestore = current.branch !== opts.targetBranch;
226
+ if (shouldRestore) {
227
+ runGit(["checkout", opts.targetBranch], opts.repoRoot);
228
+ }
229
+ let mergeError = null;
230
+ const cleanupErrors = [];
231
+ try {
232
+ runGit(["merge", "--no-ff", "--no-edit", opts.sourceBranch], opts.repoRoot);
233
+ } catch (error) {
234
+ mergeError = errorMessage(error);
235
+ const abort = tryRunGit(["merge", "--abort"], opts.repoRoot);
236
+ if (!abort.ok && abort.stderr.length > 0 && !abort.stderr.includes("MERGE_HEAD missing")) {
237
+ cleanupErrors.push(`merge abort failed: ${abort.stderr}`);
238
+ }
239
+ }
240
+ if (shouldRestore) {
241
+ const restore = tryRunGit(["checkout", current.ref], opts.repoRoot);
242
+ if (!restore.ok) {
243
+ cleanupErrors.push(`restore checkout failed: ${restore.stderr}`);
244
+ }
245
+ }
246
+ if (mergeError) {
247
+ const suffix = cleanupErrors.length > 0 ? `; ${cleanupErrors.join("; ")}` : "";
248
+ throw new Error(`${mergeError}${suffix}`);
249
+ }
250
+ if (cleanupErrors.length > 0) {
251
+ throw new Error(cleanupErrors.join("; "));
252
+ }
253
+ }
254
+ currentBranch(repoRoot) {
255
+ return runGit(["branch", "--show-current"], repoRoot);
256
+ }
257
+ }
258
+ var init_git = () => {};
259
+
260
+ // bin/src/completions.ts
261
+ var exports_completions = {};
262
+ __export(exports_completions, {
263
+ runCompletionCommand: () => runCompletionCommand,
264
+ listWorktreeBranches: () => listWorktreeBranches,
265
+ handleCompletions: () => handleCompletions,
266
+ extractBranches: () => extractBranches
267
+ });
268
+ import { basename, dirname, resolve as resolve2 } from "path";
269
+ function extractBranches(porcelainOutput, mainWorktreePath) {
270
+ const entries = parseGitWorktreePorcelain(porcelainOutput);
271
+ const resolvedMain = mainWorktreePath ? resolve2(mainWorktreePath) : null;
272
+ return entries.filter((e) => !e.bare && (!resolvedMain || resolve2(e.path) !== resolvedMain)).map((e) => e.branch ?? basename(e.path));
273
+ }
274
+ function defaultRunGit(args) {
275
+ const result = Bun.spawnSync(["git", ...args], {
276
+ stdout: "pipe",
277
+ stderr: "pipe"
278
+ });
279
+ return {
280
+ exitCode: result.exitCode,
281
+ stdout: new TextDecoder().decode(result.stdout).trim()
282
+ };
283
+ }
284
+ function listWorktreeBranches(deps = { runGit: defaultRunGit }) {
285
+ const worktreeResult = deps.runGit(["worktree", "list", "--porcelain"]);
286
+ if (worktreeResult.exitCode !== 0)
287
+ return [];
288
+ const commonDirResult = deps.runGit(["rev-parse", "--git-common-dir"]);
289
+ const mainPath = commonDirResult.exitCode === 0 ? dirname(resolve2(commonDirResult.stdout)) : null;
290
+ return extractBranches(worktreeResult.stdout, mainPath);
291
+ }
292
+ function handleCompletions(args) {
293
+ const subcommand = args[0];
294
+ if (!subcommand || !BRANCH_SUBCOMMANDS.has(subcommand)) {
295
+ return;
296
+ }
297
+ const branches = listWorktreeBranches();
298
+ for (const branch of branches) {
299
+ console.log(branch);
300
+ }
301
+ }
302
+ function isCompletionShell(value) {
303
+ return value === "bash" || value === "zsh";
304
+ }
305
+ function runCompletionCommand(args) {
306
+ const shell = args[0];
307
+ if (!shell || shell === "--help" || shell === "-h") {
308
+ console.log([
309
+ "Usage:",
310
+ " webmux completion <bash|zsh>",
311
+ "",
312
+ "Add this to your shell config to enable autocompletion:",
313
+ "",
314
+ " # ~/.zshrc",
315
+ ' eval "$(webmux completion zsh)"',
316
+ "",
317
+ " # ~/.bashrc",
318
+ ' eval "$(webmux completion bash)"'
319
+ ].join(`
320
+ `));
321
+ return 0;
322
+ }
323
+ if (!isCompletionShell(shell)) {
324
+ console.error(`Unknown shell: ${shell}. Supported: bash, zsh`);
325
+ return 1;
326
+ }
327
+ console.log(generateCompletionScript(shell));
328
+ return 0;
329
+ }
330
+ function generateCompletionScript(shell) {
331
+ switch (shell) {
332
+ case "zsh":
333
+ return ZSH_SCRIPT;
334
+ case "bash":
335
+ return BASH_SCRIPT;
336
+ }
337
+ }
338
+ var BRANCH_SUBCOMMANDS, ZSH_SCRIPT = `#compdef webmux
339
+
340
+ _webmux() {
341
+ local -a commands
342
+ commands=(
343
+ 'serve:Start the dashboard server'
344
+ 'init:Interactive project setup'
345
+ 'service:Manage webmux as a system service'
346
+ 'update:Update webmux to the latest version'
347
+ 'add:Create a worktree'
348
+ 'list:List worktrees and their status'
349
+ 'open:Open an existing worktree session'
350
+ 'close:Close a worktree session'
351
+ 'remove:Remove a worktree'
352
+ 'merge:Merge a worktree into main'
353
+ 'completion:Generate shell completion script'
354
+ )
355
+
356
+ if (( CURRENT == 2 )); then
357
+ _describe 'command' commands
358
+ return
359
+ fi
360
+
361
+ case "\${words[2]}" in
362
+ open|close|remove|merge)
363
+ if (( CURRENT == 3 )); then
364
+ local -a branches
365
+ branches=(\${(f)"$(webmux --completions "\${words[2]}" 2>/dev/null)"})
366
+ if (( \${#branches} )); then
367
+ _describe 'worktree' branches
368
+ fi
369
+ fi
370
+ ;;
371
+ completion)
372
+ if (( CURRENT == 3 )); then
373
+ local -a shells
374
+ shells=('bash:Bash completion script' 'zsh:Zsh completion script')
375
+ _describe 'shell' shells
376
+ fi
377
+ ;;
378
+ service)
379
+ if (( CURRENT == 3 )); then
380
+ local -a actions
381
+ actions=(
382
+ 'install:Install webmux as a system service'
383
+ 'uninstall:Remove the system service'
384
+ 'status:Show service status'
385
+ 'logs:Show service logs'
386
+ )
387
+ _describe 'action' actions
388
+ fi
389
+ ;;
390
+ esac
391
+ }
392
+
393
+ compdef _webmux webmux`, BASH_SCRIPT = `_webmux() {
394
+ local cur prev
395
+ COMPREPLY=()
396
+ cur="\${COMP_WORDS[COMP_CWORD]}"
397
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
398
+
399
+ if [[ \${COMP_CWORD} -eq 1 ]]; then
400
+ COMPREPLY=($(compgen -W "serve init service update add list open close remove merge completion" -- "\${cur}"))
401
+ return
402
+ fi
403
+
404
+ case "\${COMP_WORDS[1]}" in
405
+ open|close|remove|merge)
406
+ if [[ \${COMP_CWORD} -eq 2 ]]; then
407
+ local branches
408
+ branches=$(webmux --completions "\${COMP_WORDS[1]}" 2>/dev/null)
409
+ COMPREPLY=($(compgen -W "\${branches}" -- "\${cur}"))
410
+ fi
411
+ ;;
412
+ completion)
413
+ if [[ \${COMP_CWORD} -eq 2 ]]; then
414
+ COMPREPLY=($(compgen -W "bash zsh" -- "\${cur}"))
415
+ fi
416
+ ;;
417
+ service)
418
+ if [[ \${COMP_CWORD} -eq 2 ]]; then
419
+ COMPREPLY=($(compgen -W "install uninstall status logs" -- "\${cur}"))
420
+ fi
421
+ ;;
422
+ esac
423
+ }
424
+
425
+ complete -F _webmux webmux`;
426
+ var init_completions = __esm(() => {
427
+ init_git();
428
+ BRANCH_SUBCOMMANDS = new Set(["open", "close", "remove", "merge"]);
429
+ });
430
+
50
431
  // node_modules/.bun/sisteransi@1.0.5/node_modules/sisteransi/src/index.js
51
432
  var require_src = __commonJS((exports, module) => {
52
433
  var ESC = "\x1B";
@@ -993,7 +1374,7 @@ var init_dist2 = __esm(() => {
993
1374
 
994
1375
  // bin/src/shared.ts
995
1376
  import { existsSync, readFileSync } from "fs";
996
- import { basename, join } from "path";
1377
+ import { basename as basename2, join } from "path";
997
1378
  function run(cmd, args, opts) {
998
1379
  const result = Bun.spawnSync([cmd, ...args], { stdout: "pipe", stderr: "pipe", ...opts });
999
1380
  return {
@@ -1020,12 +1401,12 @@ function detectProjectName(gitRoot) {
1020
1401
  return pkg.name;
1021
1402
  } catch {}
1022
1403
  }
1023
- return basename(gitRoot);
1404
+ return basename2(gitRoot);
1024
1405
  }
1025
1406
  var init_shared = () => {};
1026
1407
 
1027
1408
  // bin/src/init-helpers.ts
1028
- import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync } from "fs";
1409
+ import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "fs";
1029
1410
  import { tmpdir } from "os";
1030
1411
  import { join as join2 } from "path";
1031
1412
  function isRecord(value) {
@@ -1427,7 +1808,7 @@ async function runInitAgentCommand(spec, cwd, handlers = {}) {
1427
1808
  try {
1428
1809
  summary = readFileSync2(spec.summaryPath, "utf8").trim() || summary;
1429
1810
  } finally {
1430
- rmSync(spec.summaryPath, { force: true });
1811
+ rmSync2(spec.summaryPath, { force: true });
1431
1812
  }
1432
1813
  }
1433
1814
  return { exitCode, stdout: stdoutResult.raw, stderr, summary };
@@ -1713,6 +2094,7 @@ ${result.stderr.trim()}` : ""
1713
2094
  console.log();
1714
2095
  console.log(" 1. Review .webmux.yaml and adjust panes, ports, and profiles if needed");
1715
2096
  console.log(" 2. Run: webmux");
2097
+ console.log(' 3. Enable tab completion: eval "$(webmux completion zsh)" (or bash)');
1716
2098
  console.log();
1717
2099
  });
1718
2100
 
@@ -1772,7 +2154,7 @@ ExecStart=${config.webmuxPath} serve --port ${config.port}
1772
2154
  WorkingDirectory=${config.projectDir}
1773
2155
  Restart=on-failure
1774
2156
  RestartSec=5
1775
- Environment=BACKEND_PORT=${config.port}
2157
+ Environment=PORT=${config.port}
1776
2158
  Environment=WEBMUX_PROJECT_DIR=${config.projectDir}
1777
2159
  Environment=PATH=${process.env.PATH}
1778
2160
 
@@ -1810,7 +2192,7 @@ function generateLaunchdPlist(config) {
1810
2192
  <string>${logPath}</string>
1811
2193
  <key>EnvironmentVariables</key>
1812
2194
  <dict>
1813
- <key>BACKEND_PORT</key>
2195
+ <key>PORT</key>
1814
2196
  <string>${config.port}</string>
1815
2197
  <key>WEBMUX_PROJECT_DIR</key>
1816
2198
  <string>${config.projectDir}</string>
@@ -2009,7 +2391,7 @@ async function service(args) {
2009
2391
  R2.error("Could not find webmux in PATH.");
2010
2392
  return;
2011
2393
  }
2012
- let port = parseInt(process.env.BACKEND_PORT || "5111");
2394
+ let port = parseInt(process.env.PORT || "5111");
2013
2395
  for (let i = 1;i < args.length; i++) {
2014
2396
  if (args[i] === "--port" && args[i + 1]) {
2015
2397
  const parsed = parseInt(args[++i]);
@@ -2189,7 +2571,7 @@ var init_fs = __esm(() => {
2189
2571
 
2190
2572
  // backend/src/adapters/tmux.ts
2191
2573
  import { createHash } from "crypto";
2192
- import { basename as basename2, resolve } from "path";
2574
+ import { basename as basename3, resolve as resolve3 } from "path";
2193
2575
  function runTmux(args) {
2194
2576
  const result = Bun.spawnSync(["tmux", ...args], {
2195
2577
  stdout: "pipe",
@@ -2214,8 +2596,8 @@ function sanitizeTmuxNameSegment(value, maxLength = 24) {
2214
2596
  return trimmed || "x";
2215
2597
  }
2216
2598
  function buildProjectSessionName(projectRoot) {
2217
- const resolved = resolve(projectRoot);
2218
- const base = sanitizeTmuxNameSegment(basename2(resolved), 18);
2599
+ const resolved = resolve3(projectRoot);
2600
+ const base = sanitizeTmuxNameSegment(basename3(resolved), 18);
2219
2601
  const hash = createHash("sha1").update(resolved).digest("hex").slice(0, 8);
2220
2602
  return `wm-${base}-${hash}`;
2221
2603
  }
@@ -2240,9 +2622,10 @@ class BunTmuxGateway {
2240
2622
  }
2241
2623
  ensureSession(sessionName, cwd) {
2242
2624
  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}`);
2625
+ if (check.exitCode !== 0) {
2626
+ assertTmuxOk(["new-session", "-d", "-s", sessionName, "-c", cwd], `create tmux session ${sessionName}`);
2627
+ }
2628
+ assertTmuxOk(["set-option", "-t", sessionName, "pane-base-index", "0"], `set pane-base-index on ${sessionName}`);
2246
2629
  }
2247
2630
  hasWindow(sessionName, windowName) {
2248
2631
  const result = runTmux(["list-windows", "-t", sessionName, "-F", "#{window_name}"]);
@@ -9431,7 +9814,8 @@ function parseLinkedRepos(raw) {
9431
9814
  return [];
9432
9815
  return raw.filter(isRecord3).filter((entry) => typeof entry.repo === "string").map((entry) => ({
9433
9816
  repo: entry.repo,
9434
- alias: typeof entry.alias === "string" ? entry.alias : entry.repo.split("/").pop() ?? "repo"
9817
+ alias: typeof entry.alias === "string" ? entry.alias : entry.repo.split("/").pop() ?? "repo",
9818
+ ...typeof entry.dir === "string" && entry.dir.trim() ? { dir: entry.dir.trim() } : {}
9435
9819
  }));
9436
9820
  }
9437
9821
  function isDockerProfile(profile) {
@@ -9471,7 +9855,7 @@ function loadConfig(dir) {
9471
9855
  startupEnvs: parseStartupEnvs(parsed.startupEnvs),
9472
9856
  integrations: {
9473
9857
  github: {
9474
- linkedRepos: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github.linkedRepos) : []
9858
+ 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
9859
  },
9476
9860
  linear: {
9477
9861
  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 +9905,7 @@ var init_config = __esm(() => {
9521
9905
 
9522
9906
  // backend/src/adapters/control-token.ts
9523
9907
  import { chmod, mkdir as mkdir2 } from "fs/promises";
9524
- import { dirname } from "path";
9908
+ import { dirname as dirname2 } from "path";
9525
9909
  async function loadControlToken() {
9526
9910
  if (cachedToken)
9527
9911
  return cachedToken;
@@ -9531,7 +9915,7 @@ async function loadControlToken() {
9531
9915
  return cachedToken;
9532
9916
  }
9533
9917
  const controlToken = crypto.randomUUID();
9534
- await mkdir2(dirname(CONTROL_TOKEN_PATH), { recursive: true });
9918
+ await mkdir2(dirname2(CONTROL_TOKEN_PATH), { recursive: true });
9535
9919
  await Bun.write(CONTROL_TOKEN_PATH, controlToken);
9536
9920
  await chmod(CONTROL_TOKEN_PATH, 384);
9537
9921
  cachedToken = controlToken;
@@ -9822,230 +10206,20 @@ var init_docker = __esm(() => {
9822
10206
  init_log();
9823
10207
  });
9824
10208
 
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}`}`);
10209
+ // backend/src/adapters/hooks.ts
10210
+ import { join as join7 } from "path";
10211
+ function buildErrorMessage(name, exitCode, stdout, stderr) {
10212
+ const output = stderr.trim() || stdout.trim();
10213
+ if (output) {
10214
+ return `${name} hook failed (exit ${exitCode}): ${output}`;
9837
10215
  }
9838
- return new TextDecoder().decode(result.stdout).trim();
10216
+ return `${name} hook failed (exit ${exitCode})`;
9839
10217
  }
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})`;
10043
- }
10044
- function hasDirenv() {
10045
- try {
10046
- return Bun.spawnSync(["direnv", "version"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0;
10047
- } catch {
10048
- return false;
10218
+ function hasDirenv() {
10219
+ try {
10220
+ return Bun.spawnSync(["direnv", "version"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0;
10221
+ } catch {
10222
+ return false;
10049
10223
  }
10050
10224
  }
10051
10225
 
@@ -10103,7 +10277,7 @@ class BunPortProbe {
10103
10277
  this.hostnames = hostnames;
10104
10278
  }
10105
10279
  isListening(port) {
10106
- return new Promise((resolve3) => {
10280
+ return new Promise((resolve4) => {
10107
10281
  let settled = false;
10108
10282
  let pending = this.hostnames.length;
10109
10283
  const settle = (result) => {
@@ -10112,20 +10286,20 @@ class BunPortProbe {
10112
10286
  if (result) {
10113
10287
  settled = true;
10114
10288
  clearTimeout(timer);
10115
- resolve3(true);
10289
+ resolve4(true);
10116
10290
  return;
10117
10291
  }
10118
10292
  pending--;
10119
10293
  if (pending === 0) {
10120
10294
  settled = true;
10121
10295
  clearTimeout(timer);
10122
- resolve3(false);
10296
+ resolve4(false);
10123
10297
  }
10124
10298
  };
10125
10299
  const timer = setTimeout(() => {
10126
10300
  if (!settled) {
10127
10301
  settled = true;
10128
- resolve3(false);
10302
+ resolve4(false);
10129
10303
  }
10130
10304
  }, this.timeoutMs);
10131
10305
  for (const hostname of this.hostnames) {
@@ -10149,10 +10323,6 @@ class BunPortProbe {
10149
10323
  }
10150
10324
 
10151
10325
  // backend/src/services/auto-name-service.ts
10152
- function buildPrompt(task) {
10153
- return `Task description:
10154
- ${task.trim()}`;
10155
- }
10156
10326
  function normalizeGeneratedBranchName(raw) {
10157
10327
  let branch = raw.trim();
10158
10328
  branch = branch.replace(/^```[\w-]*\s*/, "").replace(/\s*```$/, "");
@@ -10164,6 +10334,7 @@ function normalizeGeneratedBranchName(raw) {
10164
10334
  branch = branch.replace(/[/.]+/g, "-");
10165
10335
  branch = branch.replace(/-+/g, "-");
10166
10336
  branch = branch.replace(/^-+|-+$/g, "");
10337
+ branch = branch.slice(0, MAX_BRANCH_LENGTH).replace(/-+$/, "");
10167
10338
  if (!branch) {
10168
10339
  throw new Error("Auto-name model returned an empty branch name");
10169
10340
  }
@@ -10206,6 +10377,9 @@ function buildClaudeArgs(model, systemPrompt, prompt) {
10206
10377
  function escapeTomlString(s) {
10207
10378
  return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
10208
10379
  }
10380
+ function buildPrompt(prompt) {
10381
+ 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.`;
10382
+ }
10209
10383
  function buildCodexArgs(model, systemPrompt, prompt) {
10210
10384
  const args = [
10211
10385
  "codex",
@@ -10252,20 +10426,21 @@ class AutoNameService {
10252
10426
  return normalizeGeneratedBranchName(output);
10253
10427
  }
10254
10428
  }
10255
- var DEFAULT_SYSTEM_PROMPT;
10429
+ var MAX_BRANCH_LENGTH = 40, DEFAULT_SYSTEM_PROMPT;
10256
10430
  var init_auto_name_service = __esm(() => {
10257
10431
  init_policies();
10258
10432
  DEFAULT_SYSTEM_PROMPT = [
10259
10433
  "Generate a concise git branch name from the task description.",
10260
10434
  "Return only the branch name.",
10261
10435
  "Use lowercase kebab-case.",
10436
+ `Maximum ${MAX_BRANCH_LENGTH} characters.`,
10262
10437
  "Do not include quotes, code fences, or prefixes like feature/ or fix/."
10263
10438
  ].join(" ");
10264
10439
  });
10265
10440
 
10266
10441
  // backend/src/adapters/agent-runtime.ts
10267
10442
  import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
10268
- import { dirname as dirname2, join as join8 } from "path";
10443
+ import { dirname as dirname3, join as join8 } from "path";
10269
10444
  function shellQuote(value) {
10270
10445
  return `'${value.replaceAll("'", "'\\''")}'`;
10271
10446
  }
@@ -10532,7 +10707,7 @@ async function ensureAgentRuntimeArtifacts(input) {
10532
10707
  agentCtlPath: join8(storagePaths.webmuxDir, "webmux-agentctl"),
10533
10708
  claudeSettingsPath: join8(input.worktreePath, ".claude", "settings.local.json")
10534
10709
  };
10535
- await mkdir3(dirname2(artifacts.claudeSettingsPath), { recursive: true });
10710
+ await mkdir3(dirname3(artifacts.claudeSettingsPath), { recursive: true });
10536
10711
  await Bun.write(artifacts.agentCtlPath, buildAgentCtlScript());
10537
10712
  await chmod2(artifacts.agentCtlPath, 493);
10538
10713
  const hookSettings = buildClaudeHookSettings(artifacts);
@@ -10819,7 +10994,7 @@ var init_worktree_service = __esm(() => {
10819
10994
  // backend/src/services/lifecycle-service.ts
10820
10995
  import { randomUUID as randomUUID2 } from "crypto";
10821
10996
  import { mkdir as mkdir4 } from "fs/promises";
10822
- import { dirname as dirname3, resolve as resolve3 } from "path";
10997
+ import { dirname as dirname4, resolve as resolve4 } from "path";
10823
10998
  function generateBranchName() {
10824
10999
  return `change-${randomUUID2().slice(0, 8)}`;
10825
11000
  }
@@ -10843,7 +11018,14 @@ class LifecycleService {
10843
11018
  const worktreePath = this.resolveWorktreePath(branch);
10844
11019
  let initialized = null;
10845
11020
  try {
10846
- await mkdir4(dirname3(worktreePath), { recursive: true });
11021
+ await this.reportCreateProgress({
11022
+ branch,
11023
+ path: worktreePath,
11024
+ profile: profileName,
11025
+ agent,
11026
+ phase: "creating_worktree"
11027
+ });
11028
+ await mkdir4(dirname4(worktreePath), { recursive: true });
10847
11029
  initialized = await createManagedWorktree({
10848
11030
  repoRoot: this.deps.projectRoot,
10849
11031
  worktreePath,
@@ -10860,9 +11042,12 @@ class LifecycleService {
10860
11042
  }, {
10861
11043
  git: this.deps.git
10862
11044
  });
10863
- await ensureAgentRuntimeArtifacts({
10864
- gitDir: initialized.paths.gitDir,
10865
- worktreePath
11045
+ await this.reportCreateProgress({
11046
+ branch,
11047
+ path: worktreePath,
11048
+ profile: profileName,
11049
+ agent,
11050
+ phase: "running_post_create_hook"
10866
11051
  });
10867
11052
  await this.runLifecycleHook({
10868
11053
  name: "postCreate",
@@ -10870,6 +11055,29 @@ class LifecycleService {
10870
11055
  meta: initialized.meta,
10871
11056
  worktreePath
10872
11057
  });
11058
+ initialized = await this.refreshManagedArtifactsFromMeta({
11059
+ gitDir: initialized.paths.gitDir,
11060
+ meta: initialized.meta,
11061
+ worktreePath
11062
+ });
11063
+ await this.reportCreateProgress({
11064
+ branch,
11065
+ path: worktreePath,
11066
+ profile: profileName,
11067
+ agent,
11068
+ phase: "preparing_runtime"
11069
+ });
11070
+ await ensureAgentRuntimeArtifacts({
11071
+ gitDir: initialized.paths.gitDir,
11072
+ worktreePath
11073
+ });
11074
+ await this.reportCreateProgress({
11075
+ branch,
11076
+ path: worktreePath,
11077
+ profile: profileName,
11078
+ agent,
11079
+ phase: "starting_session"
11080
+ });
10873
11081
  await this.materializeRuntimeSession({
10874
11082
  branch,
10875
11083
  profile,
@@ -10878,6 +11086,13 @@ class LifecycleService {
10878
11086
  worktreePath,
10879
11087
  prompt: input.prompt
10880
11088
  });
11089
+ await this.reportCreateProgress({
11090
+ branch,
11091
+ path: worktreePath,
11092
+ profile: profileName,
11093
+ agent,
11094
+ phase: "reconciling"
11095
+ });
10881
11096
  await this.deps.reconciliation.reconcile(this.deps.projectRoot);
10882
11097
  return {
10883
11098
  branch,
@@ -10891,6 +11106,8 @@ class LifecycleService {
10891
11106
  }
10892
11107
  }
10893
11108
  throw this.wrapOperationError(error);
11109
+ } finally {
11110
+ await this.finishCreateProgress(branch);
10894
11111
  }
10895
11112
  }
10896
11113
  async openWorktree(branch) {
@@ -11007,11 +11224,11 @@ class LifecycleService {
11007
11224
  return allocateServicePorts(metas, this.deps.config.services);
11008
11225
  }
11009
11226
  resolveWorktreePath(branch) {
11010
- return resolve3(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
11227
+ return resolve4(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
11011
11228
  }
11012
11229
  listProjectWorktrees() {
11013
- const projectRoot = resolve3(this.deps.projectRoot);
11014
- return this.deps.git.listWorktrees(projectRoot).filter((entry) => !entry.bare && resolve3(entry.path) !== projectRoot);
11230
+ const projectRoot = resolve4(this.deps.projectRoot);
11231
+ return this.deps.git.listWorktrees(projectRoot).filter((entry) => !entry.bare && resolve4(entry.path) !== projectRoot);
11015
11232
  }
11016
11233
  async readManagedMetas() {
11017
11234
  const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
@@ -11050,21 +11267,28 @@ class LifecycleService {
11050
11267
  if (!resolved.meta) {
11051
11268
  throw new Error("Missing managed metadata");
11052
11269
  }
11053
- const dotenvValues = await loadDotenvLocal(resolved.entry.path);
11054
- const runtimeEnv = buildRuntimeEnvMap(resolved.meta, {
11055
- WEBMUX_WORKTREE_PATH: resolved.entry.path
11270
+ return await this.refreshManagedArtifactsFromMeta({
11271
+ gitDir: resolved.gitDir,
11272
+ meta: resolved.meta,
11273
+ worktreePath: resolved.entry.path
11274
+ });
11275
+ }
11276
+ async refreshManagedArtifactsFromMeta(input) {
11277
+ const dotenvValues = await loadDotenvLocal(input.worktreePath);
11278
+ const runtimeEnv = buildRuntimeEnvMap(input.meta, {
11279
+ WEBMUX_WORKTREE_PATH: input.worktreePath
11056
11280
  }, dotenvValues);
11057
- await writeRuntimeEnv(resolved.gitDir, runtimeEnv);
11281
+ await writeRuntimeEnv(input.gitDir, runtimeEnv);
11058
11282
  const controlEnv = buildControlEnvMap({
11059
11283
  controlUrl: this.controlUrl(),
11060
11284
  controlToken: await this.deps.getControlToken(),
11061
- worktreeId: resolved.meta.worktreeId,
11062
- branch: resolved.meta.branch
11285
+ worktreeId: input.meta.worktreeId,
11286
+ branch: input.meta.branch
11063
11287
  });
11064
- await writeControlEnv(resolved.gitDir, controlEnv);
11288
+ await writeControlEnv(input.gitDir, controlEnv);
11065
11289
  return {
11066
- meta: resolved.meta,
11067
- paths: getWorktreeStoragePaths(resolved.gitDir),
11290
+ meta: input.meta,
11291
+ paths: getWorktreeStoragePaths(input.gitDir),
11068
11292
  runtimeEnv,
11069
11293
  controlEnv
11070
11294
  };
@@ -11212,6 +11436,12 @@ class LifecycleService {
11212
11436
  });
11213
11437
  console.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
11214
11438
  }
11439
+ async reportCreateProgress(progress) {
11440
+ await this.deps.onCreateProgress?.(progress);
11441
+ }
11442
+ async finishCreateProgress(branch) {
11443
+ await this.deps.onCreateFinished?.(branch);
11444
+ }
11215
11445
  wrapOperationError(error) {
11216
11446
  if (error instanceof LifecycleError) {
11217
11447
  return error;
@@ -11509,9 +11739,9 @@ var init_project_runtime = __esm(() => {
11509
11739
  });
11510
11740
 
11511
11741
  // backend/src/services/reconciliation-service.ts
11512
- import { basename as basename3, resolve as resolve4 } from "path";
11742
+ import { basename as basename4, resolve as resolve5 } from "path";
11513
11743
  function makeUnmanagedWorktreeId(path) {
11514
- return `unmanaged:${resolve4(path)}`;
11744
+ return `unmanaged:${resolve5(path)}`;
11515
11745
  }
11516
11746
  function isValidPort2(port) {
11517
11747
  return port !== null && Number.isInteger(port) && port >= 1 && port <= 65535;
@@ -11544,7 +11774,7 @@ function findWindow(windows, sessionName, branch) {
11544
11774
  return windows.find((window) => window.sessionName === sessionName && window.windowName === windowName) ?? null;
11545
11775
  }
11546
11776
  function resolveBranch(entry, metaBranch) {
11547
- const fallback = basename3(entry.path);
11777
+ const fallback = basename4(entry.path);
11548
11778
  return entry.branch ?? metaBranch ?? (fallback.length > 0 ? fallback : "unknown");
11549
11779
  }
11550
11780
 
@@ -11554,7 +11784,7 @@ class ReconciliationService {
11554
11784
  this.deps = deps2;
11555
11785
  }
11556
11786
  async reconcile(repoRoot) {
11557
- const normalizedRepoRoot = resolve4(repoRoot);
11787
+ const normalizedRepoRoot = resolve5(repoRoot);
11558
11788
  const worktrees = this.deps.git.listWorktrees(normalizedRepoRoot);
11559
11789
  const sessionName = buildProjectSessionName(normalizedRepoRoot);
11560
11790
  let windows = [];
@@ -11567,7 +11797,7 @@ class ReconciliationService {
11567
11797
  for (const entry of worktrees) {
11568
11798
  if (entry.bare)
11569
11799
  continue;
11570
- if (resolve4(entry.path) === normalizedRepoRoot)
11800
+ if (resolve5(entry.path) === normalizedRepoRoot)
11571
11801
  continue;
11572
11802
  const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
11573
11803
  const meta = await readWorktreeMeta(gitDir);
@@ -11624,9 +11854,33 @@ var init_reconciliation_service = __esm(() => {
11624
11854
  init_fs();
11625
11855
  });
11626
11856
 
11857
+ // backend/src/services/worktree-creation-service.ts
11858
+ class WorktreeCreationTracker {
11859
+ worktrees = new Map;
11860
+ set(progress) {
11861
+ const next = {
11862
+ branch: progress.branch,
11863
+ path: progress.path,
11864
+ profile: progress.profile,
11865
+ agentName: progress.agent,
11866
+ phase: progress.phase
11867
+ };
11868
+ this.worktrees.set(progress.branch, next);
11869
+ }
11870
+ clear(branch) {
11871
+ return this.worktrees.delete(branch);
11872
+ }
11873
+ has(branch) {
11874
+ return this.worktrees.has(branch);
11875
+ }
11876
+ list() {
11877
+ return [...this.worktrees.values()].sort((left, right) => left.branch.localeCompare(right.branch)).map((state) => ({ ...state }));
11878
+ }
11879
+ }
11880
+
11627
11881
  // backend/src/runtime.ts
11628
11882
  function createWebmuxRuntime(options = {}) {
11629
- const port = options.port ?? parseInt(Bun.env.BACKEND_PORT || "5111", 10);
11883
+ const port = options.port ?? parseInt(Bun.env.PORT || "5111", 10);
11630
11884
  const projectDir = gitRoot2(options.projectDir ?? Bun.env.WEBMUX_PROJECT_DIR ?? process.cwd());
11631
11885
  const config = loadConfig(projectDir);
11632
11886
  const git = new BunGitGateway;
@@ -11636,6 +11890,7 @@ function createWebmuxRuntime(options = {}) {
11636
11890
  const hooks = new BunLifecycleHookRunner;
11637
11891
  const autoName = new AutoNameService;
11638
11892
  const projectRuntime = new ProjectRuntime;
11893
+ const worktreeCreationTracker = new WorktreeCreationTracker;
11639
11894
  const runtimeNotifications = new NotificationService;
11640
11895
  const reconciliationService = new ReconciliationService({
11641
11896
  config,
@@ -11654,7 +11909,13 @@ function createWebmuxRuntime(options = {}) {
11654
11909
  docker,
11655
11910
  reconciliation: reconciliationService,
11656
11911
  hooks,
11657
- autoName
11912
+ autoName,
11913
+ onCreateProgress: (progress) => {
11914
+ worktreeCreationTracker.set(progress);
11915
+ },
11916
+ onCreateFinished: (branch) => {
11917
+ worktreeCreationTracker.clear(branch);
11918
+ }
11658
11919
  });
11659
11920
  return {
11660
11921
  port,
@@ -11667,6 +11928,7 @@ function createWebmuxRuntime(options = {}) {
11667
11928
  hooks,
11668
11929
  autoName,
11669
11930
  projectRuntime,
11931
+ worktreeCreationTracker,
11670
11932
  runtimeNotifications,
11671
11933
  reconciliationService,
11672
11934
  lifecycleService
@@ -11693,7 +11955,7 @@ __export(exports_worktree_commands, {
11693
11955
  parseAddCommandArgs: () => parseAddCommandArgs,
11694
11956
  getWorktreeCommandUsage: () => getWorktreeCommandUsage
11695
11957
  });
11696
- import { basename as basename4, resolve as resolve5 } from "path";
11958
+ import { basename as basename5, resolve as resolve6 } from "path";
11697
11959
  function getWorktreeCommandUsage(command) {
11698
11960
  switch (command) {
11699
11961
  case "add":
@@ -11827,7 +12089,7 @@ function parseBranchCommandArgs(args) {
11827
12089
  return branch;
11828
12090
  }
11829
12091
  function defaultSwitchToTmuxWindow(projectDir, branch) {
11830
- const sessionName = buildProjectSessionName(resolve5(projectDir));
12092
+ const sessionName = buildProjectSessionName(resolve6(projectDir));
11831
12093
  const windowName = buildWorktreeWindowName(branch);
11832
12094
  const target = `${sessionName}:${windowName}`;
11833
12095
  const selectResult = Bun.spawnSync(["tmux", "select-window", "-t", target], {
@@ -11856,8 +12118,8 @@ function defaultSwitchToTmuxWindow(projectDir, branch) {
11856
12118
  }
11857
12119
  }
11858
12120
  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);
12121
+ const projectDir = resolve6(runtime.projectDir);
12122
+ const entries = runtime.git.listWorktrees(projectDir).filter((entry) => !entry.bare && resolve6(entry.path) !== projectDir);
11861
12123
  if (entries.length === 0) {
11862
12124
  stdout("No worktrees found.");
11863
12125
  return;
@@ -11871,7 +12133,7 @@ async function listWorktrees(runtime, stdout) {
11871
12133
  }
11872
12134
  const openWindows = new Set(windows.filter((w) => w.sessionName === sessionName).map((w) => w.windowName));
11873
12135
  const rows = await Promise.all(entries.map(async (entry) => {
11874
- const branch = entry.branch ?? basename4(entry.path);
12136
+ const branch = entry.branch ?? basename5(entry.path);
11875
12137
  const isOpen = openWindows.has(buildWorktreeWindowName(branch));
11876
12138
  const gitDir = runtime.git.resolveWorktreeGitDir(entry.path);
11877
12139
  const meta = await readWorktreeMeta(gitDir);
@@ -11962,10 +12224,67 @@ var init_worktree_commands = __esm(() => {
11962
12224
  });
11963
12225
 
11964
12226
  // bin/src/webmux.ts
11965
- import { resolve as resolve6, dirname as dirname4, join as join10 } from "path";
12227
+ import { resolve as resolve7, dirname as dirname5, join as join10 } from "path";
11966
12228
  import { existsSync as existsSync5 } from "fs";
11967
12229
  import { fileURLToPath } from "url";
11968
- var PKG_ROOT = resolve6(dirname4(fileURLToPath(import.meta.url)), "..");
12230
+ // package.json
12231
+ var package_default = {
12232
+ name: "webmux",
12233
+ version: "0.12.0",
12234
+ description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
12235
+ type: "module",
12236
+ repository: {
12237
+ type: "git",
12238
+ url: "git+https://github.com/windmill-labs/workmux-web.git"
12239
+ },
12240
+ homepage: "https://github.com/windmill-labs/workmux-web",
12241
+ keywords: [
12242
+ "workmux",
12243
+ "git-worktree",
12244
+ "tmux",
12245
+ "dashboard",
12246
+ "terminal",
12247
+ "ai-agent"
12248
+ ],
12249
+ bin: {
12250
+ webmux: "bin/webmux.js"
12251
+ },
12252
+ workspaces: [
12253
+ "backend",
12254
+ "frontend"
12255
+ ],
12256
+ scripts: {
12257
+ dev: "bash dev.sh",
12258
+ start: "bun bin/webmux.js",
12259
+ 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",
12260
+ prepublishOnly: "bun run build",
12261
+ test: "bun run --cwd backend test && bun test bin/src && bun run --cwd frontend test",
12262
+ "test:coverage": "bun run --cwd backend test --coverage && bun test --coverage bin/src && bun run --cwd frontend test:coverage"
12263
+ },
12264
+ files: [
12265
+ "bin/webmux.js",
12266
+ "backend/dist/",
12267
+ "frontend/dist/"
12268
+ ],
12269
+ devDependencies: {
12270
+ "@clack/prompts": "^1.1.0",
12271
+ "@sveltejs/vite-plugin-svelte": "^5.0.0",
12272
+ "@tailwindcss/vite": "^4.2.0",
12273
+ "@types/bun": "latest",
12274
+ "@xterm/addon-fit": "^0.10.0",
12275
+ "@xterm/addon-web-links": "^0.11.0",
12276
+ "@xterm/xterm": "^5.5.0",
12277
+ svelte: "^5.0.0",
12278
+ "svelte-check": "^4.0.0",
12279
+ tailwindcss: "^4.2.0",
12280
+ typescript: "^5.0.0",
12281
+ vite: "^6.0.0"
12282
+ },
12283
+ license: "MIT"
12284
+ };
12285
+
12286
+ // bin/src/webmux.ts
12287
+ var PKG_ROOT = resolve7(dirname5(fileURLToPath(import.meta.url)), "..");
11969
12288
  function usage2() {
11970
12289
  console.log(`
11971
12290
  webmux \u2014 Dev dashboard for managing Git worktrees
@@ -11981,21 +12300,26 @@ Usage:
11981
12300
  webmux close Close a worktree session without removing it
11982
12301
  webmux remove Remove a worktree
11983
12302
  webmux merge Merge a worktree into the main branch and remove it
12303
+ webmux completion Generate shell completion script (bash, zsh)
11984
12304
 
11985
12305
  Options:
11986
12306
  --port N Set port (default: 5111)
11987
12307
  --debug Show debug-level logs
12308
+ --version Show version number
11988
12309
  --help Show this help message
11989
12310
 
11990
12311
  Environment:
11991
- BACKEND_PORT Same as --port (flag takes precedence)
12312
+ PORT Same as --port (flag takes precedence)
11992
12313
  `);
11993
12314
  }
11994
12315
  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";
12316
+ return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "list" || value === "open" || value === "close" || value === "remove" || value === "merge" || value === "completion";
12317
+ }
12318
+ function isServeRootOption(value) {
12319
+ return value === "--port" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
11996
12320
  }
11997
12321
  function parseRootArgs(args) {
11998
- let port = parseInt(process.env.BACKEND_PORT || "5111", 10);
12322
+ let port = parseInt(process.env.PORT || "5111", 10);
11999
12323
  let debug = false;
12000
12324
  let command = null;
12001
12325
  const commandArgs = [];
@@ -12003,7 +12327,7 @@ function parseRootArgs(args) {
12003
12327
  const arg = args[index];
12004
12328
  if (!arg)
12005
12329
  continue;
12006
- if (command) {
12330
+ if (command && (command !== "serve" || !isServeRootOption(arg))) {
12007
12331
  commandArgs.push(arg);
12008
12332
  continue;
12009
12333
  }
@@ -12023,6 +12347,11 @@ function parseRootArgs(args) {
12023
12347
  case "--debug":
12024
12348
  debug = true;
12025
12349
  break;
12350
+ case "--version":
12351
+ case "-V":
12352
+ console.log(package_default.version);
12353
+ process.exit(0);
12354
+ break;
12026
12355
  case "--help":
12027
12356
  case "-h":
12028
12357
  usage2();
@@ -12045,33 +12374,6 @@ Run webmux --help for usage.`);
12045
12374
  function isWorktreeCommand(command) {
12046
12375
  return command === "add" || command === "list" || command === "open" || command === "close" || command === "remove" || command === "merge";
12047
12376
  }
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);
12074
- }
12075
12377
  async function loadEnvFile(path) {
12076
12378
  if (!existsSync5(path))
12077
12379
  return;
@@ -12091,32 +12393,6 @@ async function loadEnvFile(path) {
12091
12393
  }
12092
12394
  }
12093
12395
  }
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
12396
  function pipeWithPrefix(stream, prefix) {
12121
12397
  const reader = stream.getReader();
12122
12398
  const decoder = new TextDecoder;
@@ -12139,41 +12415,110 @@ function pipeWithPrefix(stream, prefix) {
12139
12415
  }
12140
12416
  })();
12141
12417
  }
12142
- var children = [];
12143
- var exiting = false;
12144
- function cleanup() {
12145
- if (exiting)
12418
+ async function main(args = process.argv.slice(2)) {
12419
+ if (args[0] === "--completions") {
12420
+ const { handleCompletions: handleCompletions2 } = await Promise.resolve().then(() => (init_completions(), exports_completions));
12421
+ handleCompletions2(args.slice(1));
12146
12422
  return;
12147
- exiting = true;
12148
- for (const child of children) {
12149
- try {
12150
- child.kill("SIGTERM");
12151
- } catch {}
12152
12423
  }
12153
- setTimeout(() => {
12424
+ let parsed;
12425
+ try {
12426
+ parsed = parseRootArgs(args);
12427
+ } catch (error) {
12428
+ console.error(error instanceof Error ? error.message : String(error));
12429
+ process.exit(1);
12430
+ }
12431
+ if (parsed.command === "completion") {
12432
+ const { runCompletionCommand: runCompletionCommand2 } = await Promise.resolve().then(() => (init_completions(), exports_completions));
12433
+ process.exit(runCompletionCommand2(parsed.commandArgs));
12434
+ }
12435
+ if (parsed.command === "init") {
12436
+ await init_init().then(() => exports_init);
12437
+ process.exit(0);
12438
+ }
12439
+ if (parsed.command === "service") {
12440
+ const { default: service2 } = await Promise.resolve().then(() => (init_service(), exports_service));
12441
+ await service2(parsed.commandArgs);
12442
+ process.exit(0);
12443
+ }
12444
+ if (parsed.command === "update") {
12445
+ console.log("Updating webmux to the latest version...");
12446
+ const proc = Bun.spawn(["bun", "install", "--global", "webmux@latest"], {
12447
+ stdin: "inherit",
12448
+ stdout: "inherit",
12449
+ stderr: "inherit"
12450
+ });
12451
+ const code = await proc.exited;
12452
+ process.exit(code);
12453
+ }
12454
+ await loadEnvFile(resolve7(process.cwd(), ".env.local"));
12455
+ await loadEnvFile(resolve7(process.cwd(), ".env"));
12456
+ if (isWorktreeCommand(parsed.command)) {
12457
+ const { runWorktreeCommand: runWorktreeCommand2 } = await Promise.resolve().then(() => (init_worktree_commands(), exports_worktree_commands));
12458
+ const exitCode = await runWorktreeCommand2({
12459
+ command: parsed.command,
12460
+ args: parsed.commandArgs,
12461
+ projectDir: process.cwd(),
12462
+ port: parsed.port
12463
+ });
12464
+ process.exit(exitCode);
12465
+ }
12466
+ if (parsed.command === null) {
12467
+ usage2();
12468
+ process.exit(0);
12469
+ }
12470
+ if (!existsSync5(resolve7(process.cwd(), ".webmux.yaml"))) {
12471
+ console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
12472
+ process.exit(1);
12473
+ }
12474
+ const baseEnv = {
12475
+ ...process.env,
12476
+ PORT: String(parsed.port),
12477
+ WEBMUX_PROJECT_DIR: process.cwd(),
12478
+ ...parsed.debug ? { WEBMUX_DEBUG: "1" } : {}
12479
+ };
12480
+ const children = [];
12481
+ let exiting = false;
12482
+ function cleanup() {
12483
+ if (exiting)
12484
+ return;
12485
+ exiting = true;
12154
12486
  for (const child of children) {
12155
12487
  try {
12156
- child.kill("SIGKILL");
12488
+ child.kill("SIGTERM");
12157
12489
  } catch {}
12158
12490
  }
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;
12491
+ setTimeout(() => {
12492
+ for (const child of children) {
12493
+ try {
12494
+ child.kill("SIGKILL");
12495
+ } catch {}
12496
+ }
12497
+ process.exit(0);
12498
+ }, 1000).unref();
12499
+ }
12500
+ process.on("SIGINT", cleanup);
12501
+ process.on("SIGTERM", cleanup);
12502
+ const backendEntry = join10(PKG_ROOT, "backend", "dist", "server.js");
12503
+ const staticDir = join10(PKG_ROOT, "frontend", "dist");
12504
+ if (!existsSync5(staticDir)) {
12505
+ console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
12506
+ process.exit(1);
12507
+ }
12508
+ console.log(`Starting webmux on port ${parsed.port}...`);
12509
+ const be = Bun.spawn(["bun", backendEntry], {
12510
+ env: { ...baseEnv, WEBMUX_STATIC_DIR: staticDir },
12511
+ stdout: "pipe",
12512
+ stderr: "pipe"
12513
+ });
12514
+ children.push(be);
12515
+ pipeWithPrefix(be.stdout, "[BE]");
12516
+ pipeWithPrefix(be.stderr, "[BE]");
12517
+ await be.exited;
12518
+ }
12519
+ if (import.meta.main) {
12520
+ await main();
12521
+ }
12522
+ export {
12523
+ parseRootArgs
12524
+ };