webmux 0.10.1 → 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
 
@@ -1768,11 +2150,11 @@ Description=webmux dashboard \u2014 ${config.projectName}
1768
2150
 
1769
2151
  [Service]
1770
2152
  Type=simple
1771
- ExecStart=${config.webmuxPath} --port ${config.port}
2153
+ 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
 
@@ -1791,6 +2173,7 @@ function generateLaunchdPlist(config) {
1791
2173
  <key>ProgramArguments</key>
1792
2174
  <array>
1793
2175
  <string>${config.webmuxPath}</string>
2176
+ <string>serve</string>
1794
2177
  <string>--port</string>
1795
2178
  <string>${config.port}</string>
1796
2179
  </array>
@@ -1809,7 +2192,7 @@ function generateLaunchdPlist(config) {
1809
2192
  <string>${logPath}</string>
1810
2193
  <key>EnvironmentVariables</key>
1811
2194
  <dict>
1812
- <key>BACKEND_PORT</key>
2195
+ <key>PORT</key>
1813
2196
  <string>${config.port}</string>
1814
2197
  <key>WEBMUX_PROJECT_DIR</key>
1815
2198
  <string>${config.projectDir}</string>
@@ -2008,7 +2391,7 @@ async function service(args) {
2008
2391
  R2.error("Could not find webmux in PATH.");
2009
2392
  return;
2010
2393
  }
2011
- let port = parseInt(process.env.BACKEND_PORT || "5111");
2394
+ let port = parseInt(process.env.PORT || "5111");
2012
2395
  for (let i = 1;i < args.length; i++) {
2013
2396
  if (args[i] === "--port" && args[i + 1]) {
2014
2397
  const parsed = parseInt(args[++i]);
@@ -2188,7 +2571,7 @@ var init_fs = __esm(() => {
2188
2571
 
2189
2572
  // backend/src/adapters/tmux.ts
2190
2573
  import { createHash } from "crypto";
2191
- import { basename as basename2, resolve } from "path";
2574
+ import { basename as basename3, resolve as resolve3 } from "path";
2192
2575
  function runTmux(args) {
2193
2576
  const result = Bun.spawnSync(["tmux", ...args], {
2194
2577
  stdout: "pipe",
@@ -2213,8 +2596,8 @@ function sanitizeTmuxNameSegment(value, maxLength = 24) {
2213
2596
  return trimmed || "x";
2214
2597
  }
2215
2598
  function buildProjectSessionName(projectRoot) {
2216
- const resolved = resolve(projectRoot);
2217
- const base = sanitizeTmuxNameSegment(basename2(resolved), 18);
2599
+ const resolved = resolve3(projectRoot);
2600
+ const base = sanitizeTmuxNameSegment(basename3(resolved), 18);
2218
2601
  const hash = createHash("sha1").update(resolved).digest("hex").slice(0, 8);
2219
2602
  return `wm-${base}-${hash}`;
2220
2603
  }
@@ -2239,9 +2622,10 @@ class BunTmuxGateway {
2239
2622
  }
2240
2623
  ensureSession(sessionName, cwd) {
2241
2624
  const check = runTmux(["has-session", "-t", sessionName]);
2242
- if (check.exitCode === 0)
2243
- return;
2244
- 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}`);
2245
2629
  }
2246
2630
  hasWindow(sessionName, windowName) {
2247
2631
  const result = runTmux(["list-windows", "-t", sessionName, "-F", "#{window_name}"]);
@@ -9416,10 +9800,12 @@ function parseLifecycleHooks(raw) {
9416
9800
  function parseAutoName(raw) {
9417
9801
  if (!isRecord3(raw))
9418
9802
  return null;
9419
- if (typeof raw.model !== "string" || !raw.model.trim())
9803
+ const provider = raw.provider;
9804
+ if (provider !== "claude" && provider !== "codex")
9420
9805
  return null;
9421
9806
  return {
9422
- model: raw.model.trim(),
9807
+ provider,
9808
+ ...typeof raw.model === "string" && raw.model.trim() ? { model: raw.model.trim() } : {},
9423
9809
  ...typeof raw.system_prompt === "string" && raw.system_prompt.trim() ? { systemPrompt: raw.system_prompt.trim() } : {}
9424
9810
  };
9425
9811
  }
@@ -9428,7 +9814,8 @@ function parseLinkedRepos(raw) {
9428
9814
  return [];
9429
9815
  return raw.filter(isRecord3).filter((entry) => typeof entry.repo === "string").map((entry) => ({
9430
9816
  repo: entry.repo,
9431
- 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() } : {}
9432
9819
  }));
9433
9820
  }
9434
9821
  function isDockerProfile(profile) {
@@ -9468,7 +9855,7 @@ function loadConfig(dir) {
9468
9855
  startupEnvs: parseStartupEnvs(parsed.startupEnvs),
9469
9856
  integrations: {
9470
9857
  github: {
9471
- 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) : []
9472
9859
  },
9473
9860
  linear: {
9474
9861
  enabled: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.linear) && typeof parsed.integrations.linear.enabled === "boolean" ? parsed.integrations.linear.enabled : DEFAULT_CONFIG.integrations.linear.enabled
@@ -9518,7 +9905,7 @@ var init_config = __esm(() => {
9518
9905
 
9519
9906
  // backend/src/adapters/control-token.ts
9520
9907
  import { chmod, mkdir as mkdir2 } from "fs/promises";
9521
- import { dirname } from "path";
9908
+ import { dirname as dirname2 } from "path";
9522
9909
  async function loadControlToken() {
9523
9910
  if (cachedToken)
9524
9911
  return cachedToken;
@@ -9528,7 +9915,7 @@ async function loadControlToken() {
9528
9915
  return cachedToken;
9529
9916
  }
9530
9917
  const controlToken = crypto.randomUUID();
9531
- await mkdir2(dirname(CONTROL_TOKEN_PATH), { recursive: true });
9918
+ await mkdir2(dirname2(CONTROL_TOKEN_PATH), { recursive: true });
9532
9919
  await Bun.write(CONTROL_TOKEN_PATH, controlToken);
9533
9920
  await chmod(CONTROL_TOKEN_PATH, 384);
9534
9921
  cachedToken = controlToken;
@@ -9819,230 +10206,20 @@ var init_docker = __esm(() => {
9819
10206
  init_log();
9820
10207
  });
9821
10208
 
9822
- // backend/src/adapters/git.ts
9823
- import { rmSync as rmSync2 } from "fs";
9824
- import { resolve as resolve2 } from "path";
9825
- function runGit(args, cwd) {
9826
- const result = Bun.spawnSync(["git", ...args], {
9827
- cwd,
9828
- stdout: "pipe",
9829
- stderr: "pipe"
9830
- });
9831
- if (result.exitCode !== 0) {
9832
- const stderr = new TextDecoder().decode(result.stderr).trim();
9833
- 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}`;
9834
10215
  }
9835
- return new TextDecoder().decode(result.stdout).trim();
10216
+ return `${name} hook failed (exit ${exitCode})`;
9836
10217
  }
9837
- function tryRunGit(args, cwd) {
9838
- const result = Bun.spawnSync(["git", ...args], {
9839
- cwd,
9840
- stdout: "pipe",
9841
- stderr: "pipe"
9842
- });
9843
- if (result.exitCode !== 0) {
9844
- return {
9845
- ok: false,
9846
- stderr: new TextDecoder().decode(result.stderr).trim()
9847
- };
9848
- }
9849
- return {
9850
- ok: true,
9851
- stdout: new TextDecoder().decode(result.stdout).trim()
9852
- };
9853
- }
9854
- function errorMessage(error) {
9855
- return error instanceof Error ? error.message : String(error);
9856
- }
9857
- function isRegisteredWorktree(entries, worktreePath) {
9858
- const resolvedPath = resolve2(worktreePath);
9859
- return entries.some((entry) => resolve2(entry.path) === resolvedPath);
9860
- }
9861
- function removeDirectory(path) {
9862
- rmSync2(path, {
9863
- recursive: true,
9864
- force: true
9865
- });
9866
- }
9867
- function currentCheckoutRef(cwd) {
9868
- const symbolicRef = tryRunGit(["symbolic-ref", "--quiet", "--short", "HEAD"], cwd);
9869
- if (symbolicRef.ok && symbolicRef.stdout.length > 0) {
9870
- return {
9871
- ref: symbolicRef.stdout,
9872
- branch: symbolicRef.stdout
9873
- };
9874
- }
9875
- return {
9876
- ref: runGit(["rev-parse", "--verify", "HEAD"], cwd),
9877
- branch: null
9878
- };
9879
- }
9880
- function resolveWorktreeRoot(cwd) {
9881
- const output = runGit(["rev-parse", "--show-toplevel"], cwd);
9882
- return resolve2(cwd, output);
9883
- }
9884
- function resolveWorktreeGitDir(cwd) {
9885
- const output = runGit(["rev-parse", "--git-dir"], cwd);
9886
- return resolve2(cwd, output);
9887
- }
9888
- function parseGitWorktreePorcelain(output) {
9889
- const entries = [];
9890
- let current = null;
9891
- const flush = () => {
9892
- if (current?.path)
9893
- entries.push(current);
9894
- current = null;
9895
- };
9896
- for (const rawLine of output.split(`
9897
- `)) {
9898
- const line = rawLine.trimEnd();
9899
- if (!line) {
9900
- flush();
9901
- continue;
9902
- }
9903
- if (line.startsWith("worktree ")) {
9904
- flush();
9905
- current = {
9906
- path: line.slice("worktree ".length),
9907
- branch: null,
9908
- head: null,
9909
- detached: false,
9910
- bare: false
9911
- };
9912
- continue;
9913
- }
9914
- if (!current)
9915
- continue;
9916
- if (line.startsWith("branch ")) {
9917
- current.branch = line.slice("branch ".length).replace(/^refs\/heads\//, "");
9918
- continue;
9919
- }
9920
- if (line.startsWith("HEAD ")) {
9921
- current.head = line.slice("HEAD ".length);
9922
- continue;
9923
- }
9924
- if (line === "detached") {
9925
- current.detached = true;
9926
- continue;
9927
- }
9928
- if (line === "bare") {
9929
- current.bare = true;
9930
- }
9931
- }
9932
- flush();
9933
- return entries;
9934
- }
9935
- function listGitWorktrees(cwd) {
9936
- const output = runGit(["worktree", "list", "--porcelain"], cwd);
9937
- return parseGitWorktreePorcelain(output);
9938
- }
9939
- function readGitWorktreeStatus(cwd) {
9940
- const dirtyOutput = runGit(["status", "--porcelain"], cwd);
9941
- const commit = tryRunGit(["rev-parse", "HEAD"], cwd);
9942
- const ahead = tryRunGit(["rev-list", "--count", "@{upstream}..HEAD"], cwd);
9943
- return {
9944
- dirty: dirtyOutput.length > 0,
9945
- aheadCount: ahead.ok ? parseInt(ahead.stdout, 10) || 0 : 0,
9946
- currentCommit: commit.ok && commit.stdout.length > 0 ? commit.stdout : null
9947
- };
9948
- }
9949
- function removeGitWorktree(opts, deps2 = {}) {
9950
- const args = ["worktree", "remove"];
9951
- if (opts.force)
9952
- args.push("--force");
9953
- args.push(opts.worktreePath);
9954
- const result = (deps2.tryRunGit ?? tryRunGit)(args, opts.repoRoot);
9955
- if (result.ok) {
9956
- return;
9957
- }
9958
- const failure = `git ${args.join(" ")} failed: ${result.stderr || "exit 1"}`;
9959
- const remainingWorktrees = (deps2.listWorktrees ?? listGitWorktrees)(opts.repoRoot);
9960
- if (isRegisteredWorktree(remainingWorktrees, opts.worktreePath)) {
9961
- throw new Error(failure);
9962
- }
9963
- try {
9964
- (deps2.removeDirectory ?? removeDirectory)(opts.worktreePath);
9965
- } catch (error) {
9966
- throw new Error(`${failure}; cleanup failed: ${errorMessage(error)}`);
9967
- }
9968
- }
9969
-
9970
- class BunGitGateway {
9971
- resolveWorktreeRoot(cwd) {
9972
- return resolveWorktreeRoot(cwd);
9973
- }
9974
- resolveWorktreeGitDir(cwd) {
9975
- return resolveWorktreeGitDir(cwd);
9976
- }
9977
- listWorktrees(cwd) {
9978
- return listGitWorktrees(cwd);
9979
- }
9980
- readWorktreeStatus(cwd) {
9981
- return readGitWorktreeStatus(cwd);
9982
- }
9983
- createWorktree(opts) {
9984
- const args = ["worktree", "add", "-b", opts.branch, opts.worktreePath];
9985
- if (opts.baseBranch)
9986
- args.push(opts.baseBranch);
9987
- runGit(args, opts.repoRoot);
9988
- }
9989
- removeWorktree(opts) {
9990
- removeGitWorktree(opts);
9991
- }
9992
- deleteBranch(repoRoot, branch, force = false) {
9993
- runGit(["branch", force ? "-D" : "-d", branch], repoRoot);
9994
- }
9995
- mergeBranch(opts) {
9996
- const current = currentCheckoutRef(opts.repoRoot);
9997
- const shouldRestore = current.branch !== opts.targetBranch;
9998
- if (shouldRestore) {
9999
- runGit(["checkout", opts.targetBranch], opts.repoRoot);
10000
- }
10001
- let mergeError = null;
10002
- const cleanupErrors = [];
10003
- try {
10004
- runGit(["merge", "--no-ff", "--no-edit", opts.sourceBranch], opts.repoRoot);
10005
- } catch (error) {
10006
- mergeError = errorMessage(error);
10007
- const abort = tryRunGit(["merge", "--abort"], opts.repoRoot);
10008
- if (!abort.ok && abort.stderr.length > 0 && !abort.stderr.includes("MERGE_HEAD missing")) {
10009
- cleanupErrors.push(`merge abort failed: ${abort.stderr}`);
10010
- }
10011
- }
10012
- if (shouldRestore) {
10013
- const restore = tryRunGit(["checkout", current.ref], opts.repoRoot);
10014
- if (!restore.ok) {
10015
- cleanupErrors.push(`restore checkout failed: ${restore.stderr}`);
10016
- }
10017
- }
10018
- if (mergeError) {
10019
- const suffix = cleanupErrors.length > 0 ? `; ${cleanupErrors.join("; ")}` : "";
10020
- throw new Error(`${mergeError}${suffix}`);
10021
- }
10022
- if (cleanupErrors.length > 0) {
10023
- throw new Error(cleanupErrors.join("; "));
10024
- }
10025
- }
10026
- currentBranch(repoRoot) {
10027
- return runGit(["branch", "--show-current"], repoRoot);
10028
- }
10029
- }
10030
- var init_git = () => {};
10031
-
10032
- // backend/src/adapters/hooks.ts
10033
- import { join as join7 } from "path";
10034
- function buildErrorMessage(name, exitCode, stdout, stderr) {
10035
- const output = stderr.trim() || stdout.trim();
10036
- if (output) {
10037
- return `${name} hook failed (exit ${exitCode}): ${output}`;
10038
- }
10039
- return `${name} hook failed (exit ${exitCode})`;
10040
- }
10041
- function hasDirenv() {
10042
- try {
10043
- return Bun.spawnSync(["direnv", "version"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0;
10044
- } catch {
10045
- return false;
10218
+ function hasDirenv() {
10219
+ try {
10220
+ return Bun.spawnSync(["direnv", "version"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0;
10221
+ } catch {
10222
+ return false;
10046
10223
  }
10047
10224
  }
10048
10225
 
@@ -10100,7 +10277,7 @@ class BunPortProbe {
10100
10277
  this.hostnames = hostnames;
10101
10278
  }
10102
10279
  isListening(port) {
10103
- return new Promise((resolve3) => {
10280
+ return new Promise((resolve4) => {
10104
10281
  let settled = false;
10105
10282
  let pending = this.hostnames.length;
10106
10283
  const settle = (result) => {
@@ -10109,20 +10286,20 @@ class BunPortProbe {
10109
10286
  if (result) {
10110
10287
  settled = true;
10111
10288
  clearTimeout(timer);
10112
- resolve3(true);
10289
+ resolve4(true);
10113
10290
  return;
10114
10291
  }
10115
10292
  pending--;
10116
10293
  if (pending === 0) {
10117
10294
  settled = true;
10118
10295
  clearTimeout(timer);
10119
- resolve3(false);
10296
+ resolve4(false);
10120
10297
  }
10121
10298
  };
10122
10299
  const timer = setTimeout(() => {
10123
10300
  if (!settled) {
10124
10301
  settled = true;
10125
- resolve3(false);
10302
+ resolve4(false);
10126
10303
  }
10127
10304
  }, this.timeoutMs);
10128
10305
  for (const hostname of this.hostnames) {
@@ -10146,26 +10323,6 @@ class BunPortProbe {
10146
10323
  }
10147
10324
 
10148
10325
  // backend/src/services/auto-name-service.ts
10149
- function isRecord4(value) {
10150
- return typeof value === "object" && value !== null && !Array.isArray(value);
10151
- }
10152
- function buildPrompt(task) {
10153
- return `Task description:
10154
- ${task.trim()}`;
10155
- }
10156
- function parseBranchNamePayload(raw) {
10157
- if (!isRecord4(raw) || typeof raw.branch_name !== "string") {
10158
- throw new Error("Auto-name response did not include branch_name");
10159
- }
10160
- return raw.branch_name;
10161
- }
10162
- function parseJsonText(text) {
10163
- try {
10164
- return JSON.parse(text);
10165
- } catch {
10166
- throw new Error(`Auto-name response was not valid JSON: ${text}`);
10167
- }
10168
- }
10169
10326
  function normalizeGeneratedBranchName(raw) {
10170
10327
  let branch = raw.trim();
10171
10328
  branch = branch.replace(/^```[\w-]*\s*/, "").replace(/\s*```$/, "");
@@ -10177,6 +10334,7 @@ function normalizeGeneratedBranchName(raw) {
10177
10334
  branch = branch.replace(/[/.]+/g, "-");
10178
10335
  branch = branch.replace(/-+/g, "-");
10179
10336
  branch = branch.replace(/^-+|-+$/g, "");
10337
+ branch = branch.slice(0, MAX_BRANCH_LENGTH).replace(/-+$/, "");
10180
10338
  if (!branch) {
10181
10339
  throw new Error("Auto-name model returned an empty branch name");
10182
10340
  }
@@ -10185,266 +10343,108 @@ function normalizeGeneratedBranchName(raw) {
10185
10343
  }
10186
10344
  return branch;
10187
10345
  }
10188
- function resolveAutoNameModel(modelSpec) {
10189
- const trimmed = modelSpec.trim();
10190
- const slashIndex = trimmed.indexOf("/");
10191
- if (slashIndex > 0) {
10192
- const provider = trimmed.slice(0, slashIndex);
10193
- const model = trimmed.slice(slashIndex + 1).trim().replace(/^models\//, "");
10194
- if (!model) {
10195
- throw new Error(`Invalid auto_name model: ${modelSpec}`);
10196
- }
10197
- if (provider === "anthropic" || provider === "google" || provider === "openai") {
10198
- return { provider, model };
10199
- }
10200
- if (provider === "gemini") {
10201
- return { provider: "google", model };
10202
- }
10203
- }
10204
- if (trimmed.startsWith("claude-")) {
10205
- return { provider: "anthropic", model: trimmed };
10206
- }
10207
- if (trimmed.startsWith("gemini-") || trimmed.startsWith("models/gemini-")) {
10208
- return { provider: "google", model: trimmed.replace(/^models\//, "") };
10209
- }
10210
- if (/^(gpt-|chatgpt-|o\d)/.test(trimmed)) {
10211
- return { provider: "openai", model: trimmed };
10212
- }
10213
- throw new Error(`Unsupported auto_name model provider for ${modelSpec}. Use an anthropic/, gemini/, google/, or openai/ prefix, or a known model name.`);
10214
- }
10215
10346
  function getSystemPrompt(config) {
10216
10347
  return config.systemPrompt?.trim() || DEFAULT_SYSTEM_PROMPT;
10217
10348
  }
10218
- function extractAnthropicText(raw) {
10219
- if (!isRecord4(raw) || !Array.isArray(raw.content))
10220
- return null;
10221
- for (const item of raw.content) {
10222
- if (!isRecord4(item))
10223
- continue;
10224
- if (item.type === "text" && typeof item.text === "string" && item.text.trim()) {
10225
- return item.text;
10226
- }
10227
- }
10228
- return null;
10349
+ async function defaultSpawn(args) {
10350
+ const proc = Bun.spawn(args, {
10351
+ stdout: "pipe",
10352
+ stderr: "pipe"
10353
+ });
10354
+ const [stdout, stderr, exitCode] = await Promise.all([
10355
+ new Response(proc.stdout).text(),
10356
+ new Response(proc.stderr).text(),
10357
+ proc.exited
10358
+ ]);
10359
+ return { exitCode, stdout, stderr };
10229
10360
  }
10230
- function extractGoogleText(raw) {
10231
- if (!isRecord4(raw) || !Array.isArray(raw.candidates))
10232
- return null;
10233
- for (const candidate of raw.candidates) {
10234
- if (!isRecord4(candidate) || !isRecord4(candidate.content) || !Array.isArray(candidate.content.parts))
10235
- continue;
10236
- for (const part of candidate.content.parts) {
10237
- if (isRecord4(part) && typeof part.text === "string" && part.text.trim()) {
10238
- return part.text;
10239
- }
10240
- }
10361
+ function buildClaudeArgs(model, systemPrompt, prompt) {
10362
+ const args = [
10363
+ "claude",
10364
+ "-p",
10365
+ "--system-prompt",
10366
+ systemPrompt,
10367
+ "--output-format",
10368
+ "text",
10369
+ "--no-session-persistence"
10370
+ ];
10371
+ if (model) {
10372
+ args.push("--model", model);
10241
10373
  }
10242
- return null;
10374
+ args.push(prompt);
10375
+ return args;
10243
10376
  }
10244
- function extractOpenAiText(raw) {
10245
- if (!isRecord4(raw))
10246
- return null;
10247
- if (typeof raw.output_text === "string" && raw.output_text.trim()) {
10248
- return raw.output_text;
10249
- }
10250
- if (!Array.isArray(raw.output))
10251
- return null;
10252
- for (const item of raw.output) {
10253
- if (!isRecord4(item) || !Array.isArray(item.content))
10254
- continue;
10255
- for (const content of item.content) {
10256
- if (!isRecord4(content))
10257
- continue;
10258
- if (typeof content.text === "string" && content.text.trim()) {
10259
- return content.text;
10260
- }
10261
- }
10262
- }
10263
- return null;
10377
+ function escapeTomlString(s) {
10378
+ return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
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.`;
10264
10382
  }
10265
- async function readErrorBody(response) {
10266
- const text = (await response.text()).trim();
10267
- return text || `HTTP ${response.status}`;
10383
+ function buildCodexArgs(model, systemPrompt, prompt) {
10384
+ const args = [
10385
+ "codex",
10386
+ "-c",
10387
+ `developer_instructions="${escapeTomlString(systemPrompt)}"`,
10388
+ "exec",
10389
+ "--ephemeral"
10390
+ ];
10391
+ if (model) {
10392
+ args.push("-m", model);
10393
+ }
10394
+ args.push(prompt);
10395
+ return args;
10268
10396
  }
10269
10397
 
10270
10398
  class AutoNameService {
10271
- fetchImpl;
10272
- anthropicApiKey;
10273
- geminiApiKey;
10274
- openaiApiKey;
10399
+ spawnImpl;
10275
10400
  constructor(deps2 = {}) {
10276
- this.fetchImpl = deps2.fetchImpl ?? fetch;
10277
- this.anthropicApiKey = deps2.anthropicApiKey ?? Bun.env.ANTHROPIC_API_KEY;
10278
- this.geminiApiKey = deps2.geminiApiKey ?? Bun.env.GEMINI_API_KEY;
10279
- this.openaiApiKey = deps2.openaiApiKey ?? Bun.env.OPENAI_API_KEY;
10401
+ this.spawnImpl = deps2.spawnImpl ?? defaultSpawn;
10280
10402
  }
10281
10403
  async generateBranchName(config, task) {
10282
10404
  const prompt = task.trim();
10283
10405
  if (!prompt) {
10284
10406
  throw new Error("Auto-name requires a prompt");
10285
10407
  }
10286
- const resolved = resolveAutoNameModel(config.model);
10287
- const branchName = resolved.provider === "anthropic" ? await this.generateWithAnthropic(resolved.model, getSystemPrompt(config), prompt) : resolved.provider === "google" ? await this.generateWithGoogle(resolved.model, getSystemPrompt(config), prompt) : await this.generateWithOpenAI(resolved.model, getSystemPrompt(config), prompt);
10288
- return normalizeGeneratedBranchName(branchName);
10289
- }
10290
- async generateWithAnthropic(model, systemPrompt, task) {
10291
- if (!this.anthropicApiKey) {
10292
- throw new Error("ANTHROPIC_API_KEY is required for auto_name with Anthropic models");
10293
- }
10294
- const response = await this.fetchImpl("https://api.anthropic.com/v1/messages", {
10295
- method: "POST",
10296
- headers: {
10297
- "content-type": "application/json",
10298
- "x-api-key": this.anthropicApiKey,
10299
- "anthropic-version": "2023-06-01"
10300
- },
10301
- body: JSON.stringify({
10302
- model,
10303
- system: systemPrompt,
10304
- max_tokens: 64,
10305
- messages: [{ role: "user", content: buildPrompt(task) }],
10306
- output_config: {
10307
- format: {
10308
- type: "json_schema",
10309
- schema: BRANCH_NAME_SCHEMA
10310
- }
10311
- }
10312
- })
10313
- });
10314
- if (!response.ok) {
10315
- throw new Error(`Anthropic auto-name request failed: ${await readErrorBody(response)}`);
10316
- }
10317
- const json = await response.json();
10318
- if (isRecord4(json) && json.stop_reason === "refusal") {
10319
- throw new Error("Anthropic auto-name request was refused");
10320
- }
10321
- if (isRecord4(json) && json.stop_reason === "max_tokens") {
10322
- throw new Error("Anthropic auto-name response hit max_tokens before completing");
10323
- }
10324
- const text = extractAnthropicText(json);
10325
- if (!text) {
10326
- throw new Error("Anthropic auto-name response did not include text");
10327
- }
10328
- return parseBranchNamePayload(parseJsonText(text));
10329
- }
10330
- async generateWithGoogle(model, systemPrompt, task) {
10331
- if (!this.geminiApiKey) {
10332
- throw new Error("GEMINI_API_KEY is required for auto_name with Gemini models");
10333
- }
10334
- const response = await this.fetchImpl(`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`, {
10335
- method: "POST",
10336
- headers: {
10337
- "content-type": "application/json",
10338
- "x-goog-api-key": this.geminiApiKey
10339
- },
10340
- body: JSON.stringify({
10341
- systemInstruction: {
10342
- parts: [{ text: systemPrompt }]
10343
- },
10344
- contents: [
10345
- {
10346
- role: "user",
10347
- parts: [{ text: buildPrompt(task) }]
10348
- }
10349
- ],
10350
- generationConfig: {
10351
- responseMimeType: "application/json",
10352
- responseJsonSchema: GEMINI_BRANCH_NAME_SCHEMA
10353
- }
10354
- })
10355
- });
10356
- if (!response.ok) {
10357
- throw new Error(`Google auto-name request failed: ${await readErrorBody(response)}`);
10358
- }
10359
- const json = await response.json();
10360
- const text = extractGoogleText(json);
10361
- if (!text) {
10362
- throw new Error("Google auto-name response did not include text");
10363
- }
10364
- return parseBranchNamePayload(parseJsonText(text));
10365
- }
10366
- async generateWithOpenAI(model, systemPrompt, task) {
10367
- if (!this.openaiApiKey) {
10368
- throw new Error("OPENAI_API_KEY is required for auto_name with OpenAI models");
10369
- }
10370
- const response = await this.fetchImpl("https://api.openai.com/v1/responses", {
10371
- method: "POST",
10372
- headers: {
10373
- "content-type": "application/json",
10374
- authorization: `Bearer ${this.openaiApiKey}`
10375
- },
10376
- body: JSON.stringify({
10377
- model,
10378
- input: [
10379
- { role: "system", content: systemPrompt },
10380
- { role: "user", content: buildPrompt(task) }
10381
- ],
10382
- max_output_tokens: 64,
10383
- text: {
10384
- format: {
10385
- type: "json_schema",
10386
- name: "branch_name_response",
10387
- strict: true,
10388
- schema: BRANCH_NAME_SCHEMA
10389
- }
10390
- }
10391
- })
10392
- });
10393
- if (!response.ok) {
10394
- throw new Error(`OpenAI auto-name request failed: ${await readErrorBody(response)}`);
10408
+ const systemPrompt = getSystemPrompt(config);
10409
+ const userPrompt = buildPrompt(prompt);
10410
+ const args = config.provider === "claude" ? buildClaudeArgs(config.model, systemPrompt, userPrompt) : buildCodexArgs(config.model, systemPrompt, userPrompt);
10411
+ const cli = config.provider === "claude" ? "claude" : "codex";
10412
+ let result;
10413
+ try {
10414
+ result = await this.spawnImpl(args);
10415
+ } catch {
10416
+ throw new Error(`'${cli}' CLI not found. Install it or check your PATH.`);
10395
10417
  }
10396
- const json = await response.json();
10397
- if (isRecord4(json) && Array.isArray(json.output)) {
10398
- for (const item of json.output) {
10399
- if (!isRecord4(item) || !Array.isArray(item.content))
10400
- continue;
10401
- for (const content of item.content) {
10402
- if (isRecord4(content) && content.type === "refusal" && typeof content.refusal === "string") {
10403
- throw new Error(`OpenAI auto-name request was refused: ${content.refusal}`);
10404
- }
10405
- }
10406
- }
10418
+ if (result.exitCode !== 0) {
10419
+ const detail = result.stderr.trim() || `exit ${result.exitCode}`;
10420
+ throw new Error(`${cli} failed: ${detail}`);
10407
10421
  }
10408
- const text = extractOpenAiText(json);
10409
- if (!text) {
10410
- throw new Error("OpenAI auto-name response did not include text");
10422
+ const output = result.stdout.trim();
10423
+ if (!output) {
10424
+ throw new Error(`${cli} returned empty output`);
10411
10425
  }
10412
- return parseBranchNamePayload(parseJsonText(text));
10426
+ return normalizeGeneratedBranchName(output);
10413
10427
  }
10414
10428
  }
10415
- var BRANCH_NAME_SCHEMA, GEMINI_BRANCH_NAME_SCHEMA, DEFAULT_SYSTEM_PROMPT;
10429
+ var MAX_BRANCH_LENGTH = 40, DEFAULT_SYSTEM_PROMPT;
10416
10430
  var init_auto_name_service = __esm(() => {
10417
10431
  init_policies();
10418
- BRANCH_NAME_SCHEMA = {
10419
- type: "object",
10420
- properties: {
10421
- branch_name: {
10422
- type: "string",
10423
- description: "A lowercase kebab-case git branch name with no prefix"
10424
- }
10425
- },
10426
- required: ["branch_name"],
10427
- additionalProperties: false
10428
- };
10429
- GEMINI_BRANCH_NAME_SCHEMA = {
10430
- ...BRANCH_NAME_SCHEMA,
10431
- propertyOrdering: ["branch_name"]
10432
- };
10433
10432
  DEFAULT_SYSTEM_PROMPT = [
10434
10433
  "Generate a concise git branch name from the task description.",
10435
10434
  "Return only the branch name.",
10436
10435
  "Use lowercase kebab-case.",
10436
+ `Maximum ${MAX_BRANCH_LENGTH} characters.`,
10437
10437
  "Do not include quotes, code fences, or prefixes like feature/ or fix/."
10438
10438
  ].join(" ");
10439
10439
  });
10440
10440
 
10441
10441
  // backend/src/adapters/agent-runtime.ts
10442
10442
  import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
10443
- import { dirname as dirname2, join as join8 } from "path";
10443
+ import { dirname as dirname3, join as join8 } from "path";
10444
10444
  function shellQuote(value) {
10445
10445
  return `'${value.replaceAll("'", "'\\''")}'`;
10446
10446
  }
10447
- function isRecord5(value) {
10447
+ function isRecord4(value) {
10448
10448
  return typeof value === "object" && value !== null && !Array.isArray(value);
10449
10449
  }
10450
10450
  function buildAgentCtlScript() {
@@ -10707,12 +10707,12 @@ async function ensureAgentRuntimeArtifacts(input) {
10707
10707
  agentCtlPath: join8(storagePaths.webmuxDir, "webmux-agentctl"),
10708
10708
  claudeSettingsPath: join8(input.worktreePath, ".claude", "settings.local.json")
10709
10709
  };
10710
- await mkdir3(dirname2(artifacts.claudeSettingsPath), { recursive: true });
10710
+ await mkdir3(dirname3(artifacts.claudeSettingsPath), { recursive: true });
10711
10711
  await Bun.write(artifacts.agentCtlPath, buildAgentCtlScript());
10712
10712
  await chmod2(artifacts.agentCtlPath, 493);
10713
10713
  const hookSettings = buildClaudeHookSettings(artifacts);
10714
10714
  const hooks = hookSettings.hooks;
10715
- if (!isRecord5(hooks)) {
10715
+ if (!isRecord4(hooks)) {
10716
10716
  throw new Error("Invalid Claude hook settings");
10717
10717
  }
10718
10718
  await mergeClaudeSettings(artifacts.claudeSettingsPath, hooks);
@@ -10994,7 +10994,7 @@ var init_worktree_service = __esm(() => {
10994
10994
  // backend/src/services/lifecycle-service.ts
10995
10995
  import { randomUUID as randomUUID2 } from "crypto";
10996
10996
  import { mkdir as mkdir4 } from "fs/promises";
10997
- import { dirname as dirname3, resolve as resolve3 } from "path";
10997
+ import { dirname as dirname4, resolve as resolve4 } from "path";
10998
10998
  function generateBranchName() {
10999
10999
  return `change-${randomUUID2().slice(0, 8)}`;
11000
11000
  }
@@ -11018,7 +11018,14 @@ class LifecycleService {
11018
11018
  const worktreePath = this.resolveWorktreePath(branch);
11019
11019
  let initialized = null;
11020
11020
  try {
11021
- 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 });
11022
11029
  initialized = await createManagedWorktree({
11023
11030
  repoRoot: this.deps.projectRoot,
11024
11031
  worktreePath,
@@ -11035,9 +11042,12 @@ class LifecycleService {
11035
11042
  }, {
11036
11043
  git: this.deps.git
11037
11044
  });
11038
- await ensureAgentRuntimeArtifacts({
11039
- gitDir: initialized.paths.gitDir,
11040
- worktreePath
11045
+ await this.reportCreateProgress({
11046
+ branch,
11047
+ path: worktreePath,
11048
+ profile: profileName,
11049
+ agent,
11050
+ phase: "running_post_create_hook"
11041
11051
  });
11042
11052
  await this.runLifecycleHook({
11043
11053
  name: "postCreate",
@@ -11045,6 +11055,29 @@ class LifecycleService {
11045
11055
  meta: initialized.meta,
11046
11056
  worktreePath
11047
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
+ });
11048
11081
  await this.materializeRuntimeSession({
11049
11082
  branch,
11050
11083
  profile,
@@ -11053,6 +11086,13 @@ class LifecycleService {
11053
11086
  worktreePath,
11054
11087
  prompt: input.prompt
11055
11088
  });
11089
+ await this.reportCreateProgress({
11090
+ branch,
11091
+ path: worktreePath,
11092
+ profile: profileName,
11093
+ agent,
11094
+ phase: "reconciling"
11095
+ });
11056
11096
  await this.deps.reconciliation.reconcile(this.deps.projectRoot);
11057
11097
  return {
11058
11098
  branch,
@@ -11066,6 +11106,8 @@ class LifecycleService {
11066
11106
  }
11067
11107
  }
11068
11108
  throw this.wrapOperationError(error);
11109
+ } finally {
11110
+ await this.finishCreateProgress(branch);
11069
11111
  }
11070
11112
  }
11071
11113
  async openWorktree(branch) {
@@ -11182,11 +11224,11 @@ class LifecycleService {
11182
11224
  return allocateServicePorts(metas, this.deps.config.services);
11183
11225
  }
11184
11226
  resolveWorktreePath(branch) {
11185
- return resolve3(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
11227
+ return resolve4(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
11186
11228
  }
11187
11229
  listProjectWorktrees() {
11188
- const projectRoot = resolve3(this.deps.projectRoot);
11189
- 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);
11190
11232
  }
11191
11233
  async readManagedMetas() {
11192
11234
  const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
@@ -11225,21 +11267,28 @@ class LifecycleService {
11225
11267
  if (!resolved.meta) {
11226
11268
  throw new Error("Missing managed metadata");
11227
11269
  }
11228
- const dotenvValues = await loadDotenvLocal(resolved.entry.path);
11229
- const runtimeEnv = buildRuntimeEnvMap(resolved.meta, {
11230
- 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
11231
11280
  }, dotenvValues);
11232
- await writeRuntimeEnv(resolved.gitDir, runtimeEnv);
11281
+ await writeRuntimeEnv(input.gitDir, runtimeEnv);
11233
11282
  const controlEnv = buildControlEnvMap({
11234
11283
  controlUrl: this.controlUrl(),
11235
11284
  controlToken: await this.deps.getControlToken(),
11236
- worktreeId: resolved.meta.worktreeId,
11237
- branch: resolved.meta.branch
11285
+ worktreeId: input.meta.worktreeId,
11286
+ branch: input.meta.branch
11238
11287
  });
11239
- await writeControlEnv(resolved.gitDir, controlEnv);
11288
+ await writeControlEnv(input.gitDir, controlEnv);
11240
11289
  return {
11241
- meta: resolved.meta,
11242
- paths: getWorktreeStoragePaths(resolved.gitDir),
11290
+ meta: input.meta,
11291
+ paths: getWorktreeStoragePaths(input.gitDir),
11243
11292
  runtimeEnv,
11244
11293
  controlEnv
11245
11294
  };
@@ -11387,6 +11436,12 @@ class LifecycleService {
11387
11436
  });
11388
11437
  console.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
11389
11438
  }
11439
+ async reportCreateProgress(progress) {
11440
+ await this.deps.onCreateProgress?.(progress);
11441
+ }
11442
+ async finishCreateProgress(branch) {
11443
+ await this.deps.onCreateFinished?.(branch);
11444
+ }
11390
11445
  wrapOperationError(error) {
11391
11446
  if (error instanceof LifecycleError) {
11392
11447
  return error;
@@ -11684,9 +11739,9 @@ var init_project_runtime = __esm(() => {
11684
11739
  });
11685
11740
 
11686
11741
  // backend/src/services/reconciliation-service.ts
11687
- import { basename as basename3, resolve as resolve4 } from "path";
11742
+ import { basename as basename4, resolve as resolve5 } from "path";
11688
11743
  function makeUnmanagedWorktreeId(path) {
11689
- return `unmanaged:${resolve4(path)}`;
11744
+ return `unmanaged:${resolve5(path)}`;
11690
11745
  }
11691
11746
  function isValidPort2(port) {
11692
11747
  return port !== null && Number.isInteger(port) && port >= 1 && port <= 65535;
@@ -11719,7 +11774,7 @@ function findWindow(windows, sessionName, branch) {
11719
11774
  return windows.find((window) => window.sessionName === sessionName && window.windowName === windowName) ?? null;
11720
11775
  }
11721
11776
  function resolveBranch(entry, metaBranch) {
11722
- const fallback = basename3(entry.path);
11777
+ const fallback = basename4(entry.path);
11723
11778
  return entry.branch ?? metaBranch ?? (fallback.length > 0 ? fallback : "unknown");
11724
11779
  }
11725
11780
 
@@ -11729,7 +11784,7 @@ class ReconciliationService {
11729
11784
  this.deps = deps2;
11730
11785
  }
11731
11786
  async reconcile(repoRoot) {
11732
- const normalizedRepoRoot = resolve4(repoRoot);
11787
+ const normalizedRepoRoot = resolve5(repoRoot);
11733
11788
  const worktrees = this.deps.git.listWorktrees(normalizedRepoRoot);
11734
11789
  const sessionName = buildProjectSessionName(normalizedRepoRoot);
11735
11790
  let windows = [];
@@ -11742,7 +11797,7 @@ class ReconciliationService {
11742
11797
  for (const entry of worktrees) {
11743
11798
  if (entry.bare)
11744
11799
  continue;
11745
- if (resolve4(entry.path) === normalizedRepoRoot)
11800
+ if (resolve5(entry.path) === normalizedRepoRoot)
11746
11801
  continue;
11747
11802
  const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
11748
11803
  const meta = await readWorktreeMeta(gitDir);
@@ -11799,9 +11854,33 @@ var init_reconciliation_service = __esm(() => {
11799
11854
  init_fs();
11800
11855
  });
11801
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
+
11802
11881
  // backend/src/runtime.ts
11803
11882
  function createWebmuxRuntime(options = {}) {
11804
- const port = options.port ?? parseInt(Bun.env.BACKEND_PORT || "5111", 10);
11883
+ const port = options.port ?? parseInt(Bun.env.PORT || "5111", 10);
11805
11884
  const projectDir = gitRoot2(options.projectDir ?? Bun.env.WEBMUX_PROJECT_DIR ?? process.cwd());
11806
11885
  const config = loadConfig(projectDir);
11807
11886
  const git = new BunGitGateway;
@@ -11811,6 +11890,7 @@ function createWebmuxRuntime(options = {}) {
11811
11890
  const hooks = new BunLifecycleHookRunner;
11812
11891
  const autoName = new AutoNameService;
11813
11892
  const projectRuntime = new ProjectRuntime;
11893
+ const worktreeCreationTracker = new WorktreeCreationTracker;
11814
11894
  const runtimeNotifications = new NotificationService;
11815
11895
  const reconciliationService = new ReconciliationService({
11816
11896
  config,
@@ -11829,7 +11909,13 @@ function createWebmuxRuntime(options = {}) {
11829
11909
  docker,
11830
11910
  reconciliation: reconciliationService,
11831
11911
  hooks,
11832
- autoName
11912
+ autoName,
11913
+ onCreateProgress: (progress) => {
11914
+ worktreeCreationTracker.set(progress);
11915
+ },
11916
+ onCreateFinished: (branch) => {
11917
+ worktreeCreationTracker.clear(branch);
11918
+ }
11833
11919
  });
11834
11920
  return {
11835
11921
  port,
@@ -11842,6 +11928,7 @@ function createWebmuxRuntime(options = {}) {
11842
11928
  hooks,
11843
11929
  autoName,
11844
11930
  projectRuntime,
11931
+ worktreeCreationTracker,
11845
11932
  runtimeNotifications,
11846
11933
  reconciliationService,
11847
11934
  lifecycleService
@@ -11868,7 +11955,7 @@ __export(exports_worktree_commands, {
11868
11955
  parseAddCommandArgs: () => parseAddCommandArgs,
11869
11956
  getWorktreeCommandUsage: () => getWorktreeCommandUsage
11870
11957
  });
11871
- import { basename as basename4, resolve as resolve5 } from "path";
11958
+ import { basename as basename5, resolve as resolve6 } from "path";
11872
11959
  function getWorktreeCommandUsage(command) {
11873
11960
  switch (command) {
11874
11961
  case "add":
@@ -12002,7 +12089,7 @@ function parseBranchCommandArgs(args) {
12002
12089
  return branch;
12003
12090
  }
12004
12091
  function defaultSwitchToTmuxWindow(projectDir, branch) {
12005
- const sessionName = buildProjectSessionName(resolve5(projectDir));
12092
+ const sessionName = buildProjectSessionName(resolve6(projectDir));
12006
12093
  const windowName = buildWorktreeWindowName(branch);
12007
12094
  const target = `${sessionName}:${windowName}`;
12008
12095
  const selectResult = Bun.spawnSync(["tmux", "select-window", "-t", target], {
@@ -12031,8 +12118,8 @@ function defaultSwitchToTmuxWindow(projectDir, branch) {
12031
12118
  }
12032
12119
  }
12033
12120
  async function listWorktrees(runtime, stdout) {
12034
- const projectDir = resolve5(runtime.projectDir);
12035
- 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);
12036
12123
  if (entries.length === 0) {
12037
12124
  stdout("No worktrees found.");
12038
12125
  return;
@@ -12046,7 +12133,7 @@ async function listWorktrees(runtime, stdout) {
12046
12133
  }
12047
12134
  const openWindows = new Set(windows.filter((w) => w.sessionName === sessionName).map((w) => w.windowName));
12048
12135
  const rows = await Promise.all(entries.map(async (entry) => {
12049
- const branch = entry.branch ?? basename4(entry.path);
12136
+ const branch = entry.branch ?? basename5(entry.path);
12050
12137
  const isOpen = openWindows.has(buildWorktreeWindowName(branch));
12051
12138
  const gitDir = runtime.git.resolveWorktreeGitDir(entry.path);
12052
12139
  const meta = await readWorktreeMeta(gitDir);
@@ -12137,37 +12224,102 @@ var init_worktree_commands = __esm(() => {
12137
12224
  });
12138
12225
 
12139
12226
  // bin/src/webmux.ts
12140
- 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";
12141
12228
  import { existsSync as existsSync5 } from "fs";
12142
12229
  import { fileURLToPath } from "url";
12143
- 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)), "..");
12144
12288
  function usage2() {
12145
12289
  console.log(`
12146
12290
  webmux \u2014 Dev dashboard for managing Git worktrees
12147
12291
 
12148
12292
  Usage:
12149
- webmux Start the dashboard
12293
+ webmux serve Start the dashboard server
12150
12294
  webmux init Interactive project setup
12151
12295
  webmux service Manage webmux as a system service
12296
+ webmux update Update webmux to the latest version
12152
12297
  webmux add Create a worktree using the dashboard lifecycle
12153
12298
  webmux list List worktrees and their status
12154
12299
  webmux open Open an existing worktree session
12155
12300
  webmux close Close a worktree session without removing it
12156
12301
  webmux remove Remove a worktree
12157
12302
  webmux merge Merge a worktree into the main branch and remove it
12158
- webmux --port N Set port (default: 5111)
12159
- webmux --debug Show debug-level logs
12160
- webmux --help Show this help message
12303
+ webmux completion Generate shell completion script (bash, zsh)
12304
+
12305
+ Options:
12306
+ --port N Set port (default: 5111)
12307
+ --debug Show debug-level logs
12308
+ --version Show version number
12309
+ --help Show this help message
12161
12310
 
12162
12311
  Environment:
12163
- BACKEND_PORT Same as --port (flag takes precedence)
12312
+ PORT Same as --port (flag takes precedence)
12164
12313
  `);
12165
12314
  }
12166
12315
  function isRootCommand(value) {
12167
- return value === "init" || value === "service" || 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";
12168
12320
  }
12169
12321
  function parseRootArgs(args) {
12170
- let port = parseInt(process.env.BACKEND_PORT || "5111", 10);
12322
+ let port = parseInt(process.env.PORT || "5111", 10);
12171
12323
  let debug = false;
12172
12324
  let command = null;
12173
12325
  const commandArgs = [];
@@ -12175,7 +12327,7 @@ function parseRootArgs(args) {
12175
12327
  const arg = args[index];
12176
12328
  if (!arg)
12177
12329
  continue;
12178
- if (command) {
12330
+ if (command && (command !== "serve" || !isServeRootOption(arg))) {
12179
12331
  commandArgs.push(arg);
12180
12332
  continue;
12181
12333
  }
@@ -12195,6 +12347,11 @@ function parseRootArgs(args) {
12195
12347
  case "--debug":
12196
12348
  debug = true;
12197
12349
  break;
12350
+ case "--version":
12351
+ case "-V":
12352
+ console.log(package_default.version);
12353
+ process.exit(0);
12354
+ break;
12198
12355
  case "--help":
12199
12356
  case "-h":
12200
12357
  usage2();
@@ -12217,23 +12374,6 @@ Run webmux --help for usage.`);
12217
12374
  function isWorktreeCommand(command) {
12218
12375
  return command === "add" || command === "list" || command === "open" || command === "close" || command === "remove" || command === "merge";
12219
12376
  }
12220
- var args = process.argv.slice(2);
12221
- var parsed;
12222
- try {
12223
- parsed = parseRootArgs(args);
12224
- } catch (error) {
12225
- console.error(error instanceof Error ? error.message : String(error));
12226
- process.exit(1);
12227
- }
12228
- if (parsed.command === "init") {
12229
- await init_init().then(() => exports_init);
12230
- process.exit(0);
12231
- }
12232
- if (parsed.command === "service") {
12233
- const { default: service2 } = await Promise.resolve().then(() => (init_service(), exports_service));
12234
- await service2(parsed.commandArgs);
12235
- process.exit(0);
12236
- }
12237
12377
  async function loadEnvFile(path) {
12238
12378
  if (!existsSync5(path))
12239
12379
  return;
@@ -12253,28 +12393,6 @@ async function loadEnvFile(path) {
12253
12393
  }
12254
12394
  }
12255
12395
  }
12256
- await loadEnvFile(resolve6(process.cwd(), ".env.local"));
12257
- await loadEnvFile(resolve6(process.cwd(), ".env"));
12258
- if (isWorktreeCommand(parsed.command)) {
12259
- const { runWorktreeCommand: runWorktreeCommand2 } = await Promise.resolve().then(() => (init_worktree_commands(), exports_worktree_commands));
12260
- const exitCode = await runWorktreeCommand2({
12261
- command: parsed.command,
12262
- args: parsed.commandArgs,
12263
- projectDir: process.cwd(),
12264
- port: parsed.port
12265
- });
12266
- process.exit(exitCode);
12267
- }
12268
- if (!existsSync5(resolve6(process.cwd(), ".webmux.yaml"))) {
12269
- console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
12270
- process.exit(1);
12271
- }
12272
- var baseEnv = {
12273
- ...process.env,
12274
- BACKEND_PORT: String(parsed.port),
12275
- WEBMUX_PROJECT_DIR: process.cwd(),
12276
- ...parsed.debug ? { WEBMUX_DEBUG: "1" } : {}
12277
- };
12278
12396
  function pipeWithPrefix(stream, prefix) {
12279
12397
  const reader = stream.getReader();
12280
12398
  const decoder = new TextDecoder;
@@ -12297,41 +12415,110 @@ function pipeWithPrefix(stream, prefix) {
12297
12415
  }
12298
12416
  })();
12299
12417
  }
12300
- var children = [];
12301
- var exiting = false;
12302
- function cleanup() {
12303
- 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));
12304
12422
  return;
12305
- exiting = true;
12306
- for (const child of children) {
12307
- try {
12308
- child.kill("SIGTERM");
12309
- } catch {}
12310
12423
  }
12311
- 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;
12312
12486
  for (const child of children) {
12313
12487
  try {
12314
- child.kill("SIGKILL");
12488
+ child.kill("SIGTERM");
12315
12489
  } catch {}
12316
12490
  }
12317
- process.exit(0);
12318
- }, 1000).unref();
12319
- }
12320
- process.on("SIGINT", cleanup);
12321
- process.on("SIGTERM", cleanup);
12322
- var backendEntry = join10(PKG_ROOT, "backend", "dist", "server.js");
12323
- var staticDir = join10(PKG_ROOT, "frontend", "dist");
12324
- if (!existsSync5(staticDir)) {
12325
- console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
12326
- process.exit(1);
12327
- }
12328
- console.log(`Starting webmux on port ${parsed.port}...`);
12329
- var be = Bun.spawn(["bun", backendEntry], {
12330
- env: { ...baseEnv, WEBMUX_STATIC_DIR: staticDir },
12331
- stdout: "pipe",
12332
- stderr: "pipe"
12333
- });
12334
- children.push(be);
12335
- pipeWithPrefix(be.stdout, "[BE]");
12336
- pipeWithPrefix(be.stderr, "[BE]");
12337
- 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
+ };