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