webmux 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/backend/dist/server.js +180 -33
- package/bin/webmux.js +715 -370
- package/frontend/dist/assets/index-DIWwx16E.css +32 -0
- package/frontend/dist/assets/index-Pz_SK2_d.js +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,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
|
|
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
|
-
|
|
1811
|
+
rmSync2(spec.summaryPath, { force: true });
|
|
1431
1812
|
}
|
|
1432
1813
|
}
|
|
1433
1814
|
return { exitCode, stdout: stdoutResult.raw, stderr, summary };
|
|
@@ -1713,6 +2094,7 @@ ${result.stderr.trim()}` : ""
|
|
|
1713
2094
|
console.log();
|
|
1714
2095
|
console.log(" 1. Review .webmux.yaml and adjust panes, ports, and profiles if needed");
|
|
1715
2096
|
console.log(" 2. Run: webmux");
|
|
2097
|
+
console.log(' 3. Enable tab completion: eval "$(webmux completion zsh)" (or bash)');
|
|
1716
2098
|
console.log();
|
|
1717
2099
|
});
|
|
1718
2100
|
|
|
@@ -1772,7 +2154,7 @@ ExecStart=${config.webmuxPath} serve --port ${config.port}
|
|
|
1772
2154
|
WorkingDirectory=${config.projectDir}
|
|
1773
2155
|
Restart=on-failure
|
|
1774
2156
|
RestartSec=5
|
|
1775
|
-
Environment=
|
|
2157
|
+
Environment=PORT=${config.port}
|
|
1776
2158
|
Environment=WEBMUX_PROJECT_DIR=${config.projectDir}
|
|
1777
2159
|
Environment=PATH=${process.env.PATH}
|
|
1778
2160
|
|
|
@@ -1810,7 +2192,7 @@ function generateLaunchdPlist(config) {
|
|
|
1810
2192
|
<string>${logPath}</string>
|
|
1811
2193
|
<key>EnvironmentVariables</key>
|
|
1812
2194
|
<dict>
|
|
1813
|
-
<key>
|
|
2195
|
+
<key>PORT</key>
|
|
1814
2196
|
<string>${config.port}</string>
|
|
1815
2197
|
<key>WEBMUX_PROJECT_DIR</key>
|
|
1816
2198
|
<string>${config.projectDir}</string>
|
|
@@ -2009,7 +2391,7 @@ async function service(args) {
|
|
|
2009
2391
|
R2.error("Could not find webmux in PATH.");
|
|
2010
2392
|
return;
|
|
2011
2393
|
}
|
|
2012
|
-
let port = parseInt(process.env.
|
|
2394
|
+
let port = parseInt(process.env.PORT || "5111");
|
|
2013
2395
|
for (let i = 1;i < args.length; i++) {
|
|
2014
2396
|
if (args[i] === "--port" && args[i + 1]) {
|
|
2015
2397
|
const parsed = parseInt(args[++i]);
|
|
@@ -2189,7 +2571,7 @@ var init_fs = __esm(() => {
|
|
|
2189
2571
|
|
|
2190
2572
|
// backend/src/adapters/tmux.ts
|
|
2191
2573
|
import { createHash } from "crypto";
|
|
2192
|
-
import { basename as
|
|
2574
|
+
import { basename as basename3, resolve as resolve3 } from "path";
|
|
2193
2575
|
function runTmux(args) {
|
|
2194
2576
|
const result = Bun.spawnSync(["tmux", ...args], {
|
|
2195
2577
|
stdout: "pipe",
|
|
@@ -2214,8 +2596,8 @@ function sanitizeTmuxNameSegment(value, maxLength = 24) {
|
|
|
2214
2596
|
return trimmed || "x";
|
|
2215
2597
|
}
|
|
2216
2598
|
function buildProjectSessionName(projectRoot) {
|
|
2217
|
-
const resolved =
|
|
2218
|
-
const base = sanitizeTmuxNameSegment(
|
|
2599
|
+
const resolved = resolve3(projectRoot);
|
|
2600
|
+
const base = sanitizeTmuxNameSegment(basename3(resolved), 18);
|
|
2219
2601
|
const hash = createHash("sha1").update(resolved).digest("hex").slice(0, 8);
|
|
2220
2602
|
return `wm-${base}-${hash}`;
|
|
2221
2603
|
}
|
|
@@ -2240,9 +2622,10 @@ class BunTmuxGateway {
|
|
|
2240
2622
|
}
|
|
2241
2623
|
ensureSession(sessionName, cwd) {
|
|
2242
2624
|
const check = runTmux(["has-session", "-t", sessionName]);
|
|
2243
|
-
if (check.exitCode
|
|
2244
|
-
|
|
2245
|
-
|
|
2625
|
+
if (check.exitCode !== 0) {
|
|
2626
|
+
assertTmuxOk(["new-session", "-d", "-s", sessionName, "-c", cwd], `create tmux session ${sessionName}`);
|
|
2627
|
+
}
|
|
2628
|
+
assertTmuxOk(["set-option", "-t", sessionName, "pane-base-index", "0"], `set pane-base-index on ${sessionName}`);
|
|
2246
2629
|
}
|
|
2247
2630
|
hasWindow(sessionName, windowName) {
|
|
2248
2631
|
const result = runTmux(["list-windows", "-t", sessionName, "-F", "#{window_name}"]);
|
|
@@ -9431,7 +9814,8 @@ function parseLinkedRepos(raw) {
|
|
|
9431
9814
|
return [];
|
|
9432
9815
|
return raw.filter(isRecord3).filter((entry) => typeof entry.repo === "string").map((entry) => ({
|
|
9433
9816
|
repo: entry.repo,
|
|
9434
|
-
alias: typeof entry.alias === "string" ? entry.alias : entry.repo.split("/").pop() ?? "repo"
|
|
9817
|
+
alias: typeof entry.alias === "string" ? entry.alias : entry.repo.split("/").pop() ?? "repo",
|
|
9818
|
+
...typeof entry.dir === "string" && entry.dir.trim() ? { dir: entry.dir.trim() } : {}
|
|
9435
9819
|
}));
|
|
9436
9820
|
}
|
|
9437
9821
|
function isDockerProfile(profile) {
|
|
@@ -9471,7 +9855,7 @@ function loadConfig(dir) {
|
|
|
9471
9855
|
startupEnvs: parseStartupEnvs(parsed.startupEnvs),
|
|
9472
9856
|
integrations: {
|
|
9473
9857
|
github: {
|
|
9474
|
-
linkedRepos: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github.linkedRepos) : []
|
|
9858
|
+
linkedRepos: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github.linkedRepos) : isRecord3(parsed.integrations) && Array.isArray(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github) : []
|
|
9475
9859
|
},
|
|
9476
9860
|
linear: {
|
|
9477
9861
|
enabled: isRecord3(parsed.integrations) && isRecord3(parsed.integrations.linear) && typeof parsed.integrations.linear.enabled === "boolean" ? parsed.integrations.linear.enabled : DEFAULT_CONFIG.integrations.linear.enabled
|
|
@@ -9521,7 +9905,7 @@ var init_config = __esm(() => {
|
|
|
9521
9905
|
|
|
9522
9906
|
// backend/src/adapters/control-token.ts
|
|
9523
9907
|
import { chmod, mkdir as mkdir2 } from "fs/promises";
|
|
9524
|
-
import { dirname } from "path";
|
|
9908
|
+
import { dirname as dirname2 } from "path";
|
|
9525
9909
|
async function loadControlToken() {
|
|
9526
9910
|
if (cachedToken)
|
|
9527
9911
|
return cachedToken;
|
|
@@ -9531,7 +9915,7 @@ async function loadControlToken() {
|
|
|
9531
9915
|
return cachedToken;
|
|
9532
9916
|
}
|
|
9533
9917
|
const controlToken = crypto.randomUUID();
|
|
9534
|
-
await mkdir2(
|
|
9918
|
+
await mkdir2(dirname2(CONTROL_TOKEN_PATH), { recursive: true });
|
|
9535
9919
|
await Bun.write(CONTROL_TOKEN_PATH, controlToken);
|
|
9536
9920
|
await chmod(CONTROL_TOKEN_PATH, 384);
|
|
9537
9921
|
cachedToken = controlToken;
|
|
@@ -9822,230 +10206,20 @@ var init_docker = __esm(() => {
|
|
|
9822
10206
|
init_log();
|
|
9823
10207
|
});
|
|
9824
10208
|
|
|
9825
|
-
// backend/src/adapters/
|
|
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}`}`);
|
|
10209
|
+
// backend/src/adapters/hooks.ts
|
|
10210
|
+
import { join as join7 } from "path";
|
|
10211
|
+
function buildErrorMessage(name, exitCode, stdout, stderr) {
|
|
10212
|
+
const output = stderr.trim() || stdout.trim();
|
|
10213
|
+
if (output) {
|
|
10214
|
+
return `${name} hook failed (exit ${exitCode}): ${output}`;
|
|
9837
10215
|
}
|
|
9838
|
-
return
|
|
10216
|
+
return `${name} hook failed (exit ${exitCode})`;
|
|
9839
10217
|
}
|
|
9840
|
-
function
|
|
9841
|
-
|
|
9842
|
-
|
|
9843
|
-
|
|
9844
|
-
|
|
9845
|
-
});
|
|
9846
|
-
if (result.exitCode !== 0) {
|
|
9847
|
-
return {
|
|
9848
|
-
ok: false,
|
|
9849
|
-
stderr: new TextDecoder().decode(result.stderr).trim()
|
|
9850
|
-
};
|
|
9851
|
-
}
|
|
9852
|
-
return {
|
|
9853
|
-
ok: true,
|
|
9854
|
-
stdout: new TextDecoder().decode(result.stdout).trim()
|
|
9855
|
-
};
|
|
9856
|
-
}
|
|
9857
|
-
function errorMessage(error) {
|
|
9858
|
-
return error instanceof Error ? error.message : String(error);
|
|
9859
|
-
}
|
|
9860
|
-
function isRegisteredWorktree(entries, worktreePath) {
|
|
9861
|
-
const resolvedPath = resolve2(worktreePath);
|
|
9862
|
-
return entries.some((entry) => resolve2(entry.path) === resolvedPath);
|
|
9863
|
-
}
|
|
9864
|
-
function removeDirectory(path) {
|
|
9865
|
-
rmSync2(path, {
|
|
9866
|
-
recursive: true,
|
|
9867
|
-
force: true
|
|
9868
|
-
});
|
|
9869
|
-
}
|
|
9870
|
-
function currentCheckoutRef(cwd) {
|
|
9871
|
-
const symbolicRef = tryRunGit(["symbolic-ref", "--quiet", "--short", "HEAD"], cwd);
|
|
9872
|
-
if (symbolicRef.ok && symbolicRef.stdout.length > 0) {
|
|
9873
|
-
return {
|
|
9874
|
-
ref: symbolicRef.stdout,
|
|
9875
|
-
branch: symbolicRef.stdout
|
|
9876
|
-
};
|
|
9877
|
-
}
|
|
9878
|
-
return {
|
|
9879
|
-
ref: runGit(["rev-parse", "--verify", "HEAD"], cwd),
|
|
9880
|
-
branch: null
|
|
9881
|
-
};
|
|
9882
|
-
}
|
|
9883
|
-
function resolveWorktreeRoot(cwd) {
|
|
9884
|
-
const output = runGit(["rev-parse", "--show-toplevel"], cwd);
|
|
9885
|
-
return resolve2(cwd, output);
|
|
9886
|
-
}
|
|
9887
|
-
function resolveWorktreeGitDir(cwd) {
|
|
9888
|
-
const output = runGit(["rev-parse", "--git-dir"], cwd);
|
|
9889
|
-
return resolve2(cwd, output);
|
|
9890
|
-
}
|
|
9891
|
-
function parseGitWorktreePorcelain(output) {
|
|
9892
|
-
const entries = [];
|
|
9893
|
-
let current = null;
|
|
9894
|
-
const flush = () => {
|
|
9895
|
-
if (current?.path)
|
|
9896
|
-
entries.push(current);
|
|
9897
|
-
current = null;
|
|
9898
|
-
};
|
|
9899
|
-
for (const rawLine of output.split(`
|
|
9900
|
-
`)) {
|
|
9901
|
-
const line = rawLine.trimEnd();
|
|
9902
|
-
if (!line) {
|
|
9903
|
-
flush();
|
|
9904
|
-
continue;
|
|
9905
|
-
}
|
|
9906
|
-
if (line.startsWith("worktree ")) {
|
|
9907
|
-
flush();
|
|
9908
|
-
current = {
|
|
9909
|
-
path: line.slice("worktree ".length),
|
|
9910
|
-
branch: null,
|
|
9911
|
-
head: null,
|
|
9912
|
-
detached: false,
|
|
9913
|
-
bare: false
|
|
9914
|
-
};
|
|
9915
|
-
continue;
|
|
9916
|
-
}
|
|
9917
|
-
if (!current)
|
|
9918
|
-
continue;
|
|
9919
|
-
if (line.startsWith("branch ")) {
|
|
9920
|
-
current.branch = line.slice("branch ".length).replace(/^refs\/heads\//, "");
|
|
9921
|
-
continue;
|
|
9922
|
-
}
|
|
9923
|
-
if (line.startsWith("HEAD ")) {
|
|
9924
|
-
current.head = line.slice("HEAD ".length);
|
|
9925
|
-
continue;
|
|
9926
|
-
}
|
|
9927
|
-
if (line === "detached") {
|
|
9928
|
-
current.detached = true;
|
|
9929
|
-
continue;
|
|
9930
|
-
}
|
|
9931
|
-
if (line === "bare") {
|
|
9932
|
-
current.bare = true;
|
|
9933
|
-
}
|
|
9934
|
-
}
|
|
9935
|
-
flush();
|
|
9936
|
-
return entries;
|
|
9937
|
-
}
|
|
9938
|
-
function listGitWorktrees(cwd) {
|
|
9939
|
-
const output = runGit(["worktree", "list", "--porcelain"], cwd);
|
|
9940
|
-
return parseGitWorktreePorcelain(output);
|
|
9941
|
-
}
|
|
9942
|
-
function readGitWorktreeStatus(cwd) {
|
|
9943
|
-
const dirtyOutput = runGit(["status", "--porcelain"], cwd);
|
|
9944
|
-
const commit = tryRunGit(["rev-parse", "HEAD"], cwd);
|
|
9945
|
-
const ahead = tryRunGit(["rev-list", "--count", "@{upstream}..HEAD"], cwd);
|
|
9946
|
-
return {
|
|
9947
|
-
dirty: dirtyOutput.length > 0,
|
|
9948
|
-
aheadCount: ahead.ok ? parseInt(ahead.stdout, 10) || 0 : 0,
|
|
9949
|
-
currentCommit: commit.ok && commit.stdout.length > 0 ? commit.stdout : null
|
|
9950
|
-
};
|
|
9951
|
-
}
|
|
9952
|
-
function removeGitWorktree(opts, deps2 = {}) {
|
|
9953
|
-
const args = ["worktree", "remove"];
|
|
9954
|
-
if (opts.force)
|
|
9955
|
-
args.push("--force");
|
|
9956
|
-
args.push(opts.worktreePath);
|
|
9957
|
-
const result = (deps2.tryRunGit ?? tryRunGit)(args, opts.repoRoot);
|
|
9958
|
-
if (result.ok) {
|
|
9959
|
-
return;
|
|
9960
|
-
}
|
|
9961
|
-
const failure = `git ${args.join(" ")} failed: ${result.stderr || "exit 1"}`;
|
|
9962
|
-
const remainingWorktrees = (deps2.listWorktrees ?? listGitWorktrees)(opts.repoRoot);
|
|
9963
|
-
if (isRegisteredWorktree(remainingWorktrees, opts.worktreePath)) {
|
|
9964
|
-
throw new Error(failure);
|
|
9965
|
-
}
|
|
9966
|
-
try {
|
|
9967
|
-
(deps2.removeDirectory ?? removeDirectory)(opts.worktreePath);
|
|
9968
|
-
} catch (error) {
|
|
9969
|
-
throw new Error(`${failure}; cleanup failed: ${errorMessage(error)}`);
|
|
9970
|
-
}
|
|
9971
|
-
}
|
|
9972
|
-
|
|
9973
|
-
class BunGitGateway {
|
|
9974
|
-
resolveWorktreeRoot(cwd) {
|
|
9975
|
-
return resolveWorktreeRoot(cwd);
|
|
9976
|
-
}
|
|
9977
|
-
resolveWorktreeGitDir(cwd) {
|
|
9978
|
-
return resolveWorktreeGitDir(cwd);
|
|
9979
|
-
}
|
|
9980
|
-
listWorktrees(cwd) {
|
|
9981
|
-
return listGitWorktrees(cwd);
|
|
9982
|
-
}
|
|
9983
|
-
readWorktreeStatus(cwd) {
|
|
9984
|
-
return readGitWorktreeStatus(cwd);
|
|
9985
|
-
}
|
|
9986
|
-
createWorktree(opts) {
|
|
9987
|
-
const args = ["worktree", "add", "-b", opts.branch, opts.worktreePath];
|
|
9988
|
-
if (opts.baseBranch)
|
|
9989
|
-
args.push(opts.baseBranch);
|
|
9990
|
-
runGit(args, opts.repoRoot);
|
|
9991
|
-
}
|
|
9992
|
-
removeWorktree(opts) {
|
|
9993
|
-
removeGitWorktree(opts);
|
|
9994
|
-
}
|
|
9995
|
-
deleteBranch(repoRoot, branch, force = false) {
|
|
9996
|
-
runGit(["branch", force ? "-D" : "-d", branch], repoRoot);
|
|
9997
|
-
}
|
|
9998
|
-
mergeBranch(opts) {
|
|
9999
|
-
const current = currentCheckoutRef(opts.repoRoot);
|
|
10000
|
-
const shouldRestore = current.branch !== opts.targetBranch;
|
|
10001
|
-
if (shouldRestore) {
|
|
10002
|
-
runGit(["checkout", opts.targetBranch], opts.repoRoot);
|
|
10003
|
-
}
|
|
10004
|
-
let mergeError = null;
|
|
10005
|
-
const cleanupErrors = [];
|
|
10006
|
-
try {
|
|
10007
|
-
runGit(["merge", "--no-ff", "--no-edit", opts.sourceBranch], opts.repoRoot);
|
|
10008
|
-
} catch (error) {
|
|
10009
|
-
mergeError = errorMessage(error);
|
|
10010
|
-
const abort = tryRunGit(["merge", "--abort"], opts.repoRoot);
|
|
10011
|
-
if (!abort.ok && abort.stderr.length > 0 && !abort.stderr.includes("MERGE_HEAD missing")) {
|
|
10012
|
-
cleanupErrors.push(`merge abort failed: ${abort.stderr}`);
|
|
10013
|
-
}
|
|
10014
|
-
}
|
|
10015
|
-
if (shouldRestore) {
|
|
10016
|
-
const restore = tryRunGit(["checkout", current.ref], opts.repoRoot);
|
|
10017
|
-
if (!restore.ok) {
|
|
10018
|
-
cleanupErrors.push(`restore checkout failed: ${restore.stderr}`);
|
|
10019
|
-
}
|
|
10020
|
-
}
|
|
10021
|
-
if (mergeError) {
|
|
10022
|
-
const suffix = cleanupErrors.length > 0 ? `; ${cleanupErrors.join("; ")}` : "";
|
|
10023
|
-
throw new Error(`${mergeError}${suffix}`);
|
|
10024
|
-
}
|
|
10025
|
-
if (cleanupErrors.length > 0) {
|
|
10026
|
-
throw new Error(cleanupErrors.join("; "));
|
|
10027
|
-
}
|
|
10028
|
-
}
|
|
10029
|
-
currentBranch(repoRoot) {
|
|
10030
|
-
return runGit(["branch", "--show-current"], repoRoot);
|
|
10031
|
-
}
|
|
10032
|
-
}
|
|
10033
|
-
var init_git = () => {};
|
|
10034
|
-
|
|
10035
|
-
// backend/src/adapters/hooks.ts
|
|
10036
|
-
import { join as join7 } from "path";
|
|
10037
|
-
function buildErrorMessage(name, exitCode, stdout, stderr) {
|
|
10038
|
-
const output = stderr.trim() || stdout.trim();
|
|
10039
|
-
if (output) {
|
|
10040
|
-
return `${name} hook failed (exit ${exitCode}): ${output}`;
|
|
10041
|
-
}
|
|
10042
|
-
return `${name} hook failed (exit ${exitCode})`;
|
|
10043
|
-
}
|
|
10044
|
-
function hasDirenv() {
|
|
10045
|
-
try {
|
|
10046
|
-
return Bun.spawnSync(["direnv", "version"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0;
|
|
10047
|
-
} catch {
|
|
10048
|
-
return false;
|
|
10218
|
+
function hasDirenv() {
|
|
10219
|
+
try {
|
|
10220
|
+
return Bun.spawnSync(["direnv", "version"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0;
|
|
10221
|
+
} catch {
|
|
10222
|
+
return false;
|
|
10049
10223
|
}
|
|
10050
10224
|
}
|
|
10051
10225
|
|
|
@@ -10103,7 +10277,7 @@ class BunPortProbe {
|
|
|
10103
10277
|
this.hostnames = hostnames;
|
|
10104
10278
|
}
|
|
10105
10279
|
isListening(port) {
|
|
10106
|
-
return new Promise((
|
|
10280
|
+
return new Promise((resolve4) => {
|
|
10107
10281
|
let settled = false;
|
|
10108
10282
|
let pending = this.hostnames.length;
|
|
10109
10283
|
const settle = (result) => {
|
|
@@ -10112,20 +10286,20 @@ class BunPortProbe {
|
|
|
10112
10286
|
if (result) {
|
|
10113
10287
|
settled = true;
|
|
10114
10288
|
clearTimeout(timer);
|
|
10115
|
-
|
|
10289
|
+
resolve4(true);
|
|
10116
10290
|
return;
|
|
10117
10291
|
}
|
|
10118
10292
|
pending--;
|
|
10119
10293
|
if (pending === 0) {
|
|
10120
10294
|
settled = true;
|
|
10121
10295
|
clearTimeout(timer);
|
|
10122
|
-
|
|
10296
|
+
resolve4(false);
|
|
10123
10297
|
}
|
|
10124
10298
|
};
|
|
10125
10299
|
const timer = setTimeout(() => {
|
|
10126
10300
|
if (!settled) {
|
|
10127
10301
|
settled = true;
|
|
10128
|
-
|
|
10302
|
+
resolve4(false);
|
|
10129
10303
|
}
|
|
10130
10304
|
}, this.timeoutMs);
|
|
10131
10305
|
for (const hostname of this.hostnames) {
|
|
@@ -10149,10 +10323,6 @@ class BunPortProbe {
|
|
|
10149
10323
|
}
|
|
10150
10324
|
|
|
10151
10325
|
// backend/src/services/auto-name-service.ts
|
|
10152
|
-
function buildPrompt(task) {
|
|
10153
|
-
return `Task description:
|
|
10154
|
-
${task.trim()}`;
|
|
10155
|
-
}
|
|
10156
10326
|
function normalizeGeneratedBranchName(raw) {
|
|
10157
10327
|
let branch = raw.trim();
|
|
10158
10328
|
branch = branch.replace(/^```[\w-]*\s*/, "").replace(/\s*```$/, "");
|
|
@@ -10164,6 +10334,7 @@ function normalizeGeneratedBranchName(raw) {
|
|
|
10164
10334
|
branch = branch.replace(/[/.]+/g, "-");
|
|
10165
10335
|
branch = branch.replace(/-+/g, "-");
|
|
10166
10336
|
branch = branch.replace(/^-+|-+$/g, "");
|
|
10337
|
+
branch = branch.slice(0, MAX_BRANCH_LENGTH).replace(/-+$/, "");
|
|
10167
10338
|
if (!branch) {
|
|
10168
10339
|
throw new Error("Auto-name model returned an empty branch name");
|
|
10169
10340
|
}
|
|
@@ -10206,6 +10377,9 @@ function buildClaudeArgs(model, systemPrompt, prompt) {
|
|
|
10206
10377
|
function escapeTomlString(s) {
|
|
10207
10378
|
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
|
|
10208
10379
|
}
|
|
10380
|
+
function buildPrompt(prompt) {
|
|
10381
|
+
return `Here is the task description: ${prompt}. You MUST return the branch name only, no other text or comments. Be fast, make it simple, and concise.`;
|
|
10382
|
+
}
|
|
10209
10383
|
function buildCodexArgs(model, systemPrompt, prompt) {
|
|
10210
10384
|
const args = [
|
|
10211
10385
|
"codex",
|
|
@@ -10252,20 +10426,21 @@ class AutoNameService {
|
|
|
10252
10426
|
return normalizeGeneratedBranchName(output);
|
|
10253
10427
|
}
|
|
10254
10428
|
}
|
|
10255
|
-
var DEFAULT_SYSTEM_PROMPT;
|
|
10429
|
+
var MAX_BRANCH_LENGTH = 40, DEFAULT_SYSTEM_PROMPT;
|
|
10256
10430
|
var init_auto_name_service = __esm(() => {
|
|
10257
10431
|
init_policies();
|
|
10258
10432
|
DEFAULT_SYSTEM_PROMPT = [
|
|
10259
10433
|
"Generate a concise git branch name from the task description.",
|
|
10260
10434
|
"Return only the branch name.",
|
|
10261
10435
|
"Use lowercase kebab-case.",
|
|
10436
|
+
`Maximum ${MAX_BRANCH_LENGTH} characters.`,
|
|
10262
10437
|
"Do not include quotes, code fences, or prefixes like feature/ or fix/."
|
|
10263
10438
|
].join(" ");
|
|
10264
10439
|
});
|
|
10265
10440
|
|
|
10266
10441
|
// backend/src/adapters/agent-runtime.ts
|
|
10267
10442
|
import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
|
|
10268
|
-
import { dirname as
|
|
10443
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
10269
10444
|
function shellQuote(value) {
|
|
10270
10445
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
10271
10446
|
}
|
|
@@ -10532,7 +10707,7 @@ async function ensureAgentRuntimeArtifacts(input) {
|
|
|
10532
10707
|
agentCtlPath: join8(storagePaths.webmuxDir, "webmux-agentctl"),
|
|
10533
10708
|
claudeSettingsPath: join8(input.worktreePath, ".claude", "settings.local.json")
|
|
10534
10709
|
};
|
|
10535
|
-
await mkdir3(
|
|
10710
|
+
await mkdir3(dirname3(artifacts.claudeSettingsPath), { recursive: true });
|
|
10536
10711
|
await Bun.write(artifacts.agentCtlPath, buildAgentCtlScript());
|
|
10537
10712
|
await chmod2(artifacts.agentCtlPath, 493);
|
|
10538
10713
|
const hookSettings = buildClaudeHookSettings(artifacts);
|
|
@@ -10819,7 +10994,7 @@ var init_worktree_service = __esm(() => {
|
|
|
10819
10994
|
// backend/src/services/lifecycle-service.ts
|
|
10820
10995
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
10821
10996
|
import { mkdir as mkdir4 } from "fs/promises";
|
|
10822
|
-
import { dirname as
|
|
10997
|
+
import { dirname as dirname4, resolve as resolve4 } from "path";
|
|
10823
10998
|
function generateBranchName() {
|
|
10824
10999
|
return `change-${randomUUID2().slice(0, 8)}`;
|
|
10825
11000
|
}
|
|
@@ -10843,7 +11018,14 @@ class LifecycleService {
|
|
|
10843
11018
|
const worktreePath = this.resolveWorktreePath(branch);
|
|
10844
11019
|
let initialized = null;
|
|
10845
11020
|
try {
|
|
10846
|
-
await
|
|
11021
|
+
await this.reportCreateProgress({
|
|
11022
|
+
branch,
|
|
11023
|
+
path: worktreePath,
|
|
11024
|
+
profile: profileName,
|
|
11025
|
+
agent,
|
|
11026
|
+
phase: "creating_worktree"
|
|
11027
|
+
});
|
|
11028
|
+
await mkdir4(dirname4(worktreePath), { recursive: true });
|
|
10847
11029
|
initialized = await createManagedWorktree({
|
|
10848
11030
|
repoRoot: this.deps.projectRoot,
|
|
10849
11031
|
worktreePath,
|
|
@@ -10860,9 +11042,12 @@ class LifecycleService {
|
|
|
10860
11042
|
}, {
|
|
10861
11043
|
git: this.deps.git
|
|
10862
11044
|
});
|
|
10863
|
-
await
|
|
10864
|
-
|
|
10865
|
-
worktreePath
|
|
11045
|
+
await this.reportCreateProgress({
|
|
11046
|
+
branch,
|
|
11047
|
+
path: worktreePath,
|
|
11048
|
+
profile: profileName,
|
|
11049
|
+
agent,
|
|
11050
|
+
phase: "running_post_create_hook"
|
|
10866
11051
|
});
|
|
10867
11052
|
await this.runLifecycleHook({
|
|
10868
11053
|
name: "postCreate",
|
|
@@ -10870,6 +11055,29 @@ class LifecycleService {
|
|
|
10870
11055
|
meta: initialized.meta,
|
|
10871
11056
|
worktreePath
|
|
10872
11057
|
});
|
|
11058
|
+
initialized = await this.refreshManagedArtifactsFromMeta({
|
|
11059
|
+
gitDir: initialized.paths.gitDir,
|
|
11060
|
+
meta: initialized.meta,
|
|
11061
|
+
worktreePath
|
|
11062
|
+
});
|
|
11063
|
+
await this.reportCreateProgress({
|
|
11064
|
+
branch,
|
|
11065
|
+
path: worktreePath,
|
|
11066
|
+
profile: profileName,
|
|
11067
|
+
agent,
|
|
11068
|
+
phase: "preparing_runtime"
|
|
11069
|
+
});
|
|
11070
|
+
await ensureAgentRuntimeArtifacts({
|
|
11071
|
+
gitDir: initialized.paths.gitDir,
|
|
11072
|
+
worktreePath
|
|
11073
|
+
});
|
|
11074
|
+
await this.reportCreateProgress({
|
|
11075
|
+
branch,
|
|
11076
|
+
path: worktreePath,
|
|
11077
|
+
profile: profileName,
|
|
11078
|
+
agent,
|
|
11079
|
+
phase: "starting_session"
|
|
11080
|
+
});
|
|
10873
11081
|
await this.materializeRuntimeSession({
|
|
10874
11082
|
branch,
|
|
10875
11083
|
profile,
|
|
@@ -10878,6 +11086,13 @@ class LifecycleService {
|
|
|
10878
11086
|
worktreePath,
|
|
10879
11087
|
prompt: input.prompt
|
|
10880
11088
|
});
|
|
11089
|
+
await this.reportCreateProgress({
|
|
11090
|
+
branch,
|
|
11091
|
+
path: worktreePath,
|
|
11092
|
+
profile: profileName,
|
|
11093
|
+
agent,
|
|
11094
|
+
phase: "reconciling"
|
|
11095
|
+
});
|
|
10881
11096
|
await this.deps.reconciliation.reconcile(this.deps.projectRoot);
|
|
10882
11097
|
return {
|
|
10883
11098
|
branch,
|
|
@@ -10891,6 +11106,8 @@ class LifecycleService {
|
|
|
10891
11106
|
}
|
|
10892
11107
|
}
|
|
10893
11108
|
throw this.wrapOperationError(error);
|
|
11109
|
+
} finally {
|
|
11110
|
+
await this.finishCreateProgress(branch);
|
|
10894
11111
|
}
|
|
10895
11112
|
}
|
|
10896
11113
|
async openWorktree(branch) {
|
|
@@ -11007,11 +11224,11 @@ class LifecycleService {
|
|
|
11007
11224
|
return allocateServicePorts(metas, this.deps.config.services);
|
|
11008
11225
|
}
|
|
11009
11226
|
resolveWorktreePath(branch) {
|
|
11010
|
-
return
|
|
11227
|
+
return resolve4(this.deps.projectRoot, this.deps.config.workspace.worktreeRoot, branch);
|
|
11011
11228
|
}
|
|
11012
11229
|
listProjectWorktrees() {
|
|
11013
|
-
const projectRoot =
|
|
11014
|
-
return this.deps.git.listWorktrees(projectRoot).filter((entry) => !entry.bare &&
|
|
11230
|
+
const projectRoot = resolve4(this.deps.projectRoot);
|
|
11231
|
+
return this.deps.git.listWorktrees(projectRoot).filter((entry) => !entry.bare && resolve4(entry.path) !== projectRoot);
|
|
11015
11232
|
}
|
|
11016
11233
|
async readManagedMetas() {
|
|
11017
11234
|
const metas = await Promise.all(this.listProjectWorktrees().map(async (entry) => {
|
|
@@ -11050,21 +11267,28 @@ class LifecycleService {
|
|
|
11050
11267
|
if (!resolved.meta) {
|
|
11051
11268
|
throw new Error("Missing managed metadata");
|
|
11052
11269
|
}
|
|
11053
|
-
|
|
11054
|
-
|
|
11055
|
-
|
|
11270
|
+
return await this.refreshManagedArtifactsFromMeta({
|
|
11271
|
+
gitDir: resolved.gitDir,
|
|
11272
|
+
meta: resolved.meta,
|
|
11273
|
+
worktreePath: resolved.entry.path
|
|
11274
|
+
});
|
|
11275
|
+
}
|
|
11276
|
+
async refreshManagedArtifactsFromMeta(input) {
|
|
11277
|
+
const dotenvValues = await loadDotenvLocal(input.worktreePath);
|
|
11278
|
+
const runtimeEnv = buildRuntimeEnvMap(input.meta, {
|
|
11279
|
+
WEBMUX_WORKTREE_PATH: input.worktreePath
|
|
11056
11280
|
}, dotenvValues);
|
|
11057
|
-
await writeRuntimeEnv(
|
|
11281
|
+
await writeRuntimeEnv(input.gitDir, runtimeEnv);
|
|
11058
11282
|
const controlEnv = buildControlEnvMap({
|
|
11059
11283
|
controlUrl: this.controlUrl(),
|
|
11060
11284
|
controlToken: await this.deps.getControlToken(),
|
|
11061
|
-
worktreeId:
|
|
11062
|
-
branch:
|
|
11285
|
+
worktreeId: input.meta.worktreeId,
|
|
11286
|
+
branch: input.meta.branch
|
|
11063
11287
|
});
|
|
11064
|
-
await writeControlEnv(
|
|
11288
|
+
await writeControlEnv(input.gitDir, controlEnv);
|
|
11065
11289
|
return {
|
|
11066
|
-
meta:
|
|
11067
|
-
paths: getWorktreeStoragePaths(
|
|
11290
|
+
meta: input.meta,
|
|
11291
|
+
paths: getWorktreeStoragePaths(input.gitDir),
|
|
11068
11292
|
runtimeEnv,
|
|
11069
11293
|
controlEnv
|
|
11070
11294
|
};
|
|
@@ -11212,6 +11436,12 @@ class LifecycleService {
|
|
|
11212
11436
|
});
|
|
11213
11437
|
console.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
|
|
11214
11438
|
}
|
|
11439
|
+
async reportCreateProgress(progress) {
|
|
11440
|
+
await this.deps.onCreateProgress?.(progress);
|
|
11441
|
+
}
|
|
11442
|
+
async finishCreateProgress(branch) {
|
|
11443
|
+
await this.deps.onCreateFinished?.(branch);
|
|
11444
|
+
}
|
|
11215
11445
|
wrapOperationError(error) {
|
|
11216
11446
|
if (error instanceof LifecycleError) {
|
|
11217
11447
|
return error;
|
|
@@ -11509,9 +11739,9 @@ var init_project_runtime = __esm(() => {
|
|
|
11509
11739
|
});
|
|
11510
11740
|
|
|
11511
11741
|
// backend/src/services/reconciliation-service.ts
|
|
11512
|
-
import { basename as
|
|
11742
|
+
import { basename as basename4, resolve as resolve5 } from "path";
|
|
11513
11743
|
function makeUnmanagedWorktreeId(path) {
|
|
11514
|
-
return `unmanaged:${
|
|
11744
|
+
return `unmanaged:${resolve5(path)}`;
|
|
11515
11745
|
}
|
|
11516
11746
|
function isValidPort2(port) {
|
|
11517
11747
|
return port !== null && Number.isInteger(port) && port >= 1 && port <= 65535;
|
|
@@ -11544,7 +11774,7 @@ function findWindow(windows, sessionName, branch) {
|
|
|
11544
11774
|
return windows.find((window) => window.sessionName === sessionName && window.windowName === windowName) ?? null;
|
|
11545
11775
|
}
|
|
11546
11776
|
function resolveBranch(entry, metaBranch) {
|
|
11547
|
-
const fallback =
|
|
11777
|
+
const fallback = basename4(entry.path);
|
|
11548
11778
|
return entry.branch ?? metaBranch ?? (fallback.length > 0 ? fallback : "unknown");
|
|
11549
11779
|
}
|
|
11550
11780
|
|
|
@@ -11554,7 +11784,7 @@ class ReconciliationService {
|
|
|
11554
11784
|
this.deps = deps2;
|
|
11555
11785
|
}
|
|
11556
11786
|
async reconcile(repoRoot) {
|
|
11557
|
-
const normalizedRepoRoot =
|
|
11787
|
+
const normalizedRepoRoot = resolve5(repoRoot);
|
|
11558
11788
|
const worktrees = this.deps.git.listWorktrees(normalizedRepoRoot);
|
|
11559
11789
|
const sessionName = buildProjectSessionName(normalizedRepoRoot);
|
|
11560
11790
|
let windows = [];
|
|
@@ -11567,7 +11797,7 @@ class ReconciliationService {
|
|
|
11567
11797
|
for (const entry of worktrees) {
|
|
11568
11798
|
if (entry.bare)
|
|
11569
11799
|
continue;
|
|
11570
|
-
if (
|
|
11800
|
+
if (resolve5(entry.path) === normalizedRepoRoot)
|
|
11571
11801
|
continue;
|
|
11572
11802
|
const gitDir = this.deps.git.resolveWorktreeGitDir(entry.path);
|
|
11573
11803
|
const meta = await readWorktreeMeta(gitDir);
|
|
@@ -11624,9 +11854,33 @@ var init_reconciliation_service = __esm(() => {
|
|
|
11624
11854
|
init_fs();
|
|
11625
11855
|
});
|
|
11626
11856
|
|
|
11857
|
+
// backend/src/services/worktree-creation-service.ts
|
|
11858
|
+
class WorktreeCreationTracker {
|
|
11859
|
+
worktrees = new Map;
|
|
11860
|
+
set(progress) {
|
|
11861
|
+
const next = {
|
|
11862
|
+
branch: progress.branch,
|
|
11863
|
+
path: progress.path,
|
|
11864
|
+
profile: progress.profile,
|
|
11865
|
+
agentName: progress.agent,
|
|
11866
|
+
phase: progress.phase
|
|
11867
|
+
};
|
|
11868
|
+
this.worktrees.set(progress.branch, next);
|
|
11869
|
+
}
|
|
11870
|
+
clear(branch) {
|
|
11871
|
+
return this.worktrees.delete(branch);
|
|
11872
|
+
}
|
|
11873
|
+
has(branch) {
|
|
11874
|
+
return this.worktrees.has(branch);
|
|
11875
|
+
}
|
|
11876
|
+
list() {
|
|
11877
|
+
return [...this.worktrees.values()].sort((left, right) => left.branch.localeCompare(right.branch)).map((state) => ({ ...state }));
|
|
11878
|
+
}
|
|
11879
|
+
}
|
|
11880
|
+
|
|
11627
11881
|
// backend/src/runtime.ts
|
|
11628
11882
|
function createWebmuxRuntime(options = {}) {
|
|
11629
|
-
const port = options.port ?? parseInt(Bun.env.
|
|
11883
|
+
const port = options.port ?? parseInt(Bun.env.PORT || "5111", 10);
|
|
11630
11884
|
const projectDir = gitRoot2(options.projectDir ?? Bun.env.WEBMUX_PROJECT_DIR ?? process.cwd());
|
|
11631
11885
|
const config = loadConfig(projectDir);
|
|
11632
11886
|
const git = new BunGitGateway;
|
|
@@ -11636,6 +11890,7 @@ function createWebmuxRuntime(options = {}) {
|
|
|
11636
11890
|
const hooks = new BunLifecycleHookRunner;
|
|
11637
11891
|
const autoName = new AutoNameService;
|
|
11638
11892
|
const projectRuntime = new ProjectRuntime;
|
|
11893
|
+
const worktreeCreationTracker = new WorktreeCreationTracker;
|
|
11639
11894
|
const runtimeNotifications = new NotificationService;
|
|
11640
11895
|
const reconciliationService = new ReconciliationService({
|
|
11641
11896
|
config,
|
|
@@ -11654,7 +11909,13 @@ function createWebmuxRuntime(options = {}) {
|
|
|
11654
11909
|
docker,
|
|
11655
11910
|
reconciliation: reconciliationService,
|
|
11656
11911
|
hooks,
|
|
11657
|
-
autoName
|
|
11912
|
+
autoName,
|
|
11913
|
+
onCreateProgress: (progress) => {
|
|
11914
|
+
worktreeCreationTracker.set(progress);
|
|
11915
|
+
},
|
|
11916
|
+
onCreateFinished: (branch) => {
|
|
11917
|
+
worktreeCreationTracker.clear(branch);
|
|
11918
|
+
}
|
|
11658
11919
|
});
|
|
11659
11920
|
return {
|
|
11660
11921
|
port,
|
|
@@ -11667,6 +11928,7 @@ function createWebmuxRuntime(options = {}) {
|
|
|
11667
11928
|
hooks,
|
|
11668
11929
|
autoName,
|
|
11669
11930
|
projectRuntime,
|
|
11931
|
+
worktreeCreationTracker,
|
|
11670
11932
|
runtimeNotifications,
|
|
11671
11933
|
reconciliationService,
|
|
11672
11934
|
lifecycleService
|
|
@@ -11693,7 +11955,7 @@ __export(exports_worktree_commands, {
|
|
|
11693
11955
|
parseAddCommandArgs: () => parseAddCommandArgs,
|
|
11694
11956
|
getWorktreeCommandUsage: () => getWorktreeCommandUsage
|
|
11695
11957
|
});
|
|
11696
|
-
import { basename as
|
|
11958
|
+
import { basename as basename5, resolve as resolve6 } from "path";
|
|
11697
11959
|
function getWorktreeCommandUsage(command) {
|
|
11698
11960
|
switch (command) {
|
|
11699
11961
|
case "add":
|
|
@@ -11827,7 +12089,7 @@ function parseBranchCommandArgs(args) {
|
|
|
11827
12089
|
return branch;
|
|
11828
12090
|
}
|
|
11829
12091
|
function defaultSwitchToTmuxWindow(projectDir, branch) {
|
|
11830
|
-
const sessionName = buildProjectSessionName(
|
|
12092
|
+
const sessionName = buildProjectSessionName(resolve6(projectDir));
|
|
11831
12093
|
const windowName = buildWorktreeWindowName(branch);
|
|
11832
12094
|
const target = `${sessionName}:${windowName}`;
|
|
11833
12095
|
const selectResult = Bun.spawnSync(["tmux", "select-window", "-t", target], {
|
|
@@ -11856,8 +12118,8 @@ function defaultSwitchToTmuxWindow(projectDir, branch) {
|
|
|
11856
12118
|
}
|
|
11857
12119
|
}
|
|
11858
12120
|
async function listWorktrees(runtime, stdout) {
|
|
11859
|
-
const projectDir =
|
|
11860
|
-
const entries = runtime.git.listWorktrees(projectDir).filter((entry) => !entry.bare &&
|
|
12121
|
+
const projectDir = resolve6(runtime.projectDir);
|
|
12122
|
+
const entries = runtime.git.listWorktrees(projectDir).filter((entry) => !entry.bare && resolve6(entry.path) !== projectDir);
|
|
11861
12123
|
if (entries.length === 0) {
|
|
11862
12124
|
stdout("No worktrees found.");
|
|
11863
12125
|
return;
|
|
@@ -11871,7 +12133,7 @@ async function listWorktrees(runtime, stdout) {
|
|
|
11871
12133
|
}
|
|
11872
12134
|
const openWindows = new Set(windows.filter((w) => w.sessionName === sessionName).map((w) => w.windowName));
|
|
11873
12135
|
const rows = await Promise.all(entries.map(async (entry) => {
|
|
11874
|
-
const branch = entry.branch ??
|
|
12136
|
+
const branch = entry.branch ?? basename5(entry.path);
|
|
11875
12137
|
const isOpen = openWindows.has(buildWorktreeWindowName(branch));
|
|
11876
12138
|
const gitDir = runtime.git.resolveWorktreeGitDir(entry.path);
|
|
11877
12139
|
const meta = await readWorktreeMeta(gitDir);
|
|
@@ -11962,10 +12224,67 @@ var init_worktree_commands = __esm(() => {
|
|
|
11962
12224
|
});
|
|
11963
12225
|
|
|
11964
12226
|
// bin/src/webmux.ts
|
|
11965
|
-
import { resolve as
|
|
12227
|
+
import { resolve as resolve7, dirname as dirname5, join as join10 } from "path";
|
|
11966
12228
|
import { existsSync as existsSync5 } from "fs";
|
|
11967
12229
|
import { fileURLToPath } from "url";
|
|
11968
|
-
|
|
12230
|
+
// package.json
|
|
12231
|
+
var package_default = {
|
|
12232
|
+
name: "webmux",
|
|
12233
|
+
version: "0.12.0",
|
|
12234
|
+
description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
|
|
12235
|
+
type: "module",
|
|
12236
|
+
repository: {
|
|
12237
|
+
type: "git",
|
|
12238
|
+
url: "git+https://github.com/windmill-labs/workmux-web.git"
|
|
12239
|
+
},
|
|
12240
|
+
homepage: "https://github.com/windmill-labs/workmux-web",
|
|
12241
|
+
keywords: [
|
|
12242
|
+
"workmux",
|
|
12243
|
+
"git-worktree",
|
|
12244
|
+
"tmux",
|
|
12245
|
+
"dashboard",
|
|
12246
|
+
"terminal",
|
|
12247
|
+
"ai-agent"
|
|
12248
|
+
],
|
|
12249
|
+
bin: {
|
|
12250
|
+
webmux: "bin/webmux.js"
|
|
12251
|
+
},
|
|
12252
|
+
workspaces: [
|
|
12253
|
+
"backend",
|
|
12254
|
+
"frontend"
|
|
12255
|
+
],
|
|
12256
|
+
scripts: {
|
|
12257
|
+
dev: "bash dev.sh",
|
|
12258
|
+
start: "bun bin/webmux.js",
|
|
12259
|
+
build: "cd frontend && bun run build && cd .. && bun build backend/src/server.ts --target=bun --outfile=backend/dist/server.js && bun build bin/src/webmux.ts --target=bun --outfile=bin/webmux.js",
|
|
12260
|
+
prepublishOnly: "bun run build",
|
|
12261
|
+
test: "bun run --cwd backend test && bun test bin/src && bun run --cwd frontend test",
|
|
12262
|
+
"test:coverage": "bun run --cwd backend test --coverage && bun test --coverage bin/src && bun run --cwd frontend test:coverage"
|
|
12263
|
+
},
|
|
12264
|
+
files: [
|
|
12265
|
+
"bin/webmux.js",
|
|
12266
|
+
"backend/dist/",
|
|
12267
|
+
"frontend/dist/"
|
|
12268
|
+
],
|
|
12269
|
+
devDependencies: {
|
|
12270
|
+
"@clack/prompts": "^1.1.0",
|
|
12271
|
+
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
|
12272
|
+
"@tailwindcss/vite": "^4.2.0",
|
|
12273
|
+
"@types/bun": "latest",
|
|
12274
|
+
"@xterm/addon-fit": "^0.10.0",
|
|
12275
|
+
"@xterm/addon-web-links": "^0.11.0",
|
|
12276
|
+
"@xterm/xterm": "^5.5.0",
|
|
12277
|
+
svelte: "^5.0.0",
|
|
12278
|
+
"svelte-check": "^4.0.0",
|
|
12279
|
+
tailwindcss: "^4.2.0",
|
|
12280
|
+
typescript: "^5.0.0",
|
|
12281
|
+
vite: "^6.0.0"
|
|
12282
|
+
},
|
|
12283
|
+
license: "MIT"
|
|
12284
|
+
};
|
|
12285
|
+
|
|
12286
|
+
// bin/src/webmux.ts
|
|
12287
|
+
var PKG_ROOT = resolve7(dirname5(fileURLToPath(import.meta.url)), "..");
|
|
11969
12288
|
function usage2() {
|
|
11970
12289
|
console.log(`
|
|
11971
12290
|
webmux \u2014 Dev dashboard for managing Git worktrees
|
|
@@ -11981,21 +12300,26 @@ Usage:
|
|
|
11981
12300
|
webmux close Close a worktree session without removing it
|
|
11982
12301
|
webmux remove Remove a worktree
|
|
11983
12302
|
webmux merge Merge a worktree into the main branch and remove it
|
|
12303
|
+
webmux completion Generate shell completion script (bash, zsh)
|
|
11984
12304
|
|
|
11985
12305
|
Options:
|
|
11986
12306
|
--port N Set port (default: 5111)
|
|
11987
12307
|
--debug Show debug-level logs
|
|
12308
|
+
--version Show version number
|
|
11988
12309
|
--help Show this help message
|
|
11989
12310
|
|
|
11990
12311
|
Environment:
|
|
11991
|
-
|
|
12312
|
+
PORT Same as --port (flag takes precedence)
|
|
11992
12313
|
`);
|
|
11993
12314
|
}
|
|
11994
12315
|
function isRootCommand(value) {
|
|
11995
|
-
return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "list" || value === "open" || value === "close" || value === "remove" || value === "merge";
|
|
12316
|
+
return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "list" || value === "open" || value === "close" || value === "remove" || value === "merge" || value === "completion";
|
|
12317
|
+
}
|
|
12318
|
+
function isServeRootOption(value) {
|
|
12319
|
+
return value === "--port" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
|
|
11996
12320
|
}
|
|
11997
12321
|
function parseRootArgs(args) {
|
|
11998
|
-
let port = parseInt(process.env.
|
|
12322
|
+
let port = parseInt(process.env.PORT || "5111", 10);
|
|
11999
12323
|
let debug = false;
|
|
12000
12324
|
let command = null;
|
|
12001
12325
|
const commandArgs = [];
|
|
@@ -12003,7 +12327,7 @@ function parseRootArgs(args) {
|
|
|
12003
12327
|
const arg = args[index];
|
|
12004
12328
|
if (!arg)
|
|
12005
12329
|
continue;
|
|
12006
|
-
if (command) {
|
|
12330
|
+
if (command && (command !== "serve" || !isServeRootOption(arg))) {
|
|
12007
12331
|
commandArgs.push(arg);
|
|
12008
12332
|
continue;
|
|
12009
12333
|
}
|
|
@@ -12023,6 +12347,11 @@ function parseRootArgs(args) {
|
|
|
12023
12347
|
case "--debug":
|
|
12024
12348
|
debug = true;
|
|
12025
12349
|
break;
|
|
12350
|
+
case "--version":
|
|
12351
|
+
case "-V":
|
|
12352
|
+
console.log(package_default.version);
|
|
12353
|
+
process.exit(0);
|
|
12354
|
+
break;
|
|
12026
12355
|
case "--help":
|
|
12027
12356
|
case "-h":
|
|
12028
12357
|
usage2();
|
|
@@ -12045,33 +12374,6 @@ Run webmux --help for usage.`);
|
|
|
12045
12374
|
function isWorktreeCommand(command) {
|
|
12046
12375
|
return command === "add" || command === "list" || command === "open" || command === "close" || command === "remove" || command === "merge";
|
|
12047
12376
|
}
|
|
12048
|
-
var args = process.argv.slice(2);
|
|
12049
|
-
var parsed;
|
|
12050
|
-
try {
|
|
12051
|
-
parsed = parseRootArgs(args);
|
|
12052
|
-
} catch (error) {
|
|
12053
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
12054
|
-
process.exit(1);
|
|
12055
|
-
}
|
|
12056
|
-
if (parsed.command === "init") {
|
|
12057
|
-
await init_init().then(() => exports_init);
|
|
12058
|
-
process.exit(0);
|
|
12059
|
-
}
|
|
12060
|
-
if (parsed.command === "service") {
|
|
12061
|
-
const { default: service2 } = await Promise.resolve().then(() => (init_service(), exports_service));
|
|
12062
|
-
await service2(parsed.commandArgs);
|
|
12063
|
-
process.exit(0);
|
|
12064
|
-
}
|
|
12065
|
-
if (parsed.command === "update") {
|
|
12066
|
-
console.log("Updating webmux to the latest version...");
|
|
12067
|
-
const proc = Bun.spawn(["bun", "install", "--global", "webmux@latest"], {
|
|
12068
|
-
stdin: "inherit",
|
|
12069
|
-
stdout: "inherit",
|
|
12070
|
-
stderr: "inherit"
|
|
12071
|
-
});
|
|
12072
|
-
const code = await proc.exited;
|
|
12073
|
-
process.exit(code);
|
|
12074
|
-
}
|
|
12075
12377
|
async function loadEnvFile(path) {
|
|
12076
12378
|
if (!existsSync5(path))
|
|
12077
12379
|
return;
|
|
@@ -12091,32 +12393,6 @@ async function loadEnvFile(path) {
|
|
|
12091
12393
|
}
|
|
12092
12394
|
}
|
|
12093
12395
|
}
|
|
12094
|
-
await loadEnvFile(resolve6(process.cwd(), ".env.local"));
|
|
12095
|
-
await loadEnvFile(resolve6(process.cwd(), ".env"));
|
|
12096
|
-
if (isWorktreeCommand(parsed.command)) {
|
|
12097
|
-
const { runWorktreeCommand: runWorktreeCommand2 } = await Promise.resolve().then(() => (init_worktree_commands(), exports_worktree_commands));
|
|
12098
|
-
const exitCode = await runWorktreeCommand2({
|
|
12099
|
-
command: parsed.command,
|
|
12100
|
-
args: parsed.commandArgs,
|
|
12101
|
-
projectDir: process.cwd(),
|
|
12102
|
-
port: parsed.port
|
|
12103
|
-
});
|
|
12104
|
-
process.exit(exitCode);
|
|
12105
|
-
}
|
|
12106
|
-
if (parsed.command === null) {
|
|
12107
|
-
usage2();
|
|
12108
|
-
process.exit(0);
|
|
12109
|
-
}
|
|
12110
|
-
if (!existsSync5(resolve6(process.cwd(), ".webmux.yaml"))) {
|
|
12111
|
-
console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
|
|
12112
|
-
process.exit(1);
|
|
12113
|
-
}
|
|
12114
|
-
var baseEnv = {
|
|
12115
|
-
...process.env,
|
|
12116
|
-
BACKEND_PORT: String(parsed.port),
|
|
12117
|
-
WEBMUX_PROJECT_DIR: process.cwd(),
|
|
12118
|
-
...parsed.debug ? { WEBMUX_DEBUG: "1" } : {}
|
|
12119
|
-
};
|
|
12120
12396
|
function pipeWithPrefix(stream, prefix) {
|
|
12121
12397
|
const reader = stream.getReader();
|
|
12122
12398
|
const decoder = new TextDecoder;
|
|
@@ -12139,41 +12415,110 @@ function pipeWithPrefix(stream, prefix) {
|
|
|
12139
12415
|
}
|
|
12140
12416
|
})();
|
|
12141
12417
|
}
|
|
12142
|
-
|
|
12143
|
-
|
|
12144
|
-
|
|
12145
|
-
|
|
12418
|
+
async function main(args = process.argv.slice(2)) {
|
|
12419
|
+
if (args[0] === "--completions") {
|
|
12420
|
+
const { handleCompletions: handleCompletions2 } = await Promise.resolve().then(() => (init_completions(), exports_completions));
|
|
12421
|
+
handleCompletions2(args.slice(1));
|
|
12146
12422
|
return;
|
|
12147
|
-
exiting = true;
|
|
12148
|
-
for (const child of children) {
|
|
12149
|
-
try {
|
|
12150
|
-
child.kill("SIGTERM");
|
|
12151
|
-
} catch {}
|
|
12152
12423
|
}
|
|
12153
|
-
|
|
12424
|
+
let parsed;
|
|
12425
|
+
try {
|
|
12426
|
+
parsed = parseRootArgs(args);
|
|
12427
|
+
} catch (error) {
|
|
12428
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
12429
|
+
process.exit(1);
|
|
12430
|
+
}
|
|
12431
|
+
if (parsed.command === "completion") {
|
|
12432
|
+
const { runCompletionCommand: runCompletionCommand2 } = await Promise.resolve().then(() => (init_completions(), exports_completions));
|
|
12433
|
+
process.exit(runCompletionCommand2(parsed.commandArgs));
|
|
12434
|
+
}
|
|
12435
|
+
if (parsed.command === "init") {
|
|
12436
|
+
await init_init().then(() => exports_init);
|
|
12437
|
+
process.exit(0);
|
|
12438
|
+
}
|
|
12439
|
+
if (parsed.command === "service") {
|
|
12440
|
+
const { default: service2 } = await Promise.resolve().then(() => (init_service(), exports_service));
|
|
12441
|
+
await service2(parsed.commandArgs);
|
|
12442
|
+
process.exit(0);
|
|
12443
|
+
}
|
|
12444
|
+
if (parsed.command === "update") {
|
|
12445
|
+
console.log("Updating webmux to the latest version...");
|
|
12446
|
+
const proc = Bun.spawn(["bun", "install", "--global", "webmux@latest"], {
|
|
12447
|
+
stdin: "inherit",
|
|
12448
|
+
stdout: "inherit",
|
|
12449
|
+
stderr: "inherit"
|
|
12450
|
+
});
|
|
12451
|
+
const code = await proc.exited;
|
|
12452
|
+
process.exit(code);
|
|
12453
|
+
}
|
|
12454
|
+
await loadEnvFile(resolve7(process.cwd(), ".env.local"));
|
|
12455
|
+
await loadEnvFile(resolve7(process.cwd(), ".env"));
|
|
12456
|
+
if (isWorktreeCommand(parsed.command)) {
|
|
12457
|
+
const { runWorktreeCommand: runWorktreeCommand2 } = await Promise.resolve().then(() => (init_worktree_commands(), exports_worktree_commands));
|
|
12458
|
+
const exitCode = await runWorktreeCommand2({
|
|
12459
|
+
command: parsed.command,
|
|
12460
|
+
args: parsed.commandArgs,
|
|
12461
|
+
projectDir: process.cwd(),
|
|
12462
|
+
port: parsed.port
|
|
12463
|
+
});
|
|
12464
|
+
process.exit(exitCode);
|
|
12465
|
+
}
|
|
12466
|
+
if (parsed.command === null) {
|
|
12467
|
+
usage2();
|
|
12468
|
+
process.exit(0);
|
|
12469
|
+
}
|
|
12470
|
+
if (!existsSync5(resolve7(process.cwd(), ".webmux.yaml"))) {
|
|
12471
|
+
console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
|
|
12472
|
+
process.exit(1);
|
|
12473
|
+
}
|
|
12474
|
+
const baseEnv = {
|
|
12475
|
+
...process.env,
|
|
12476
|
+
PORT: String(parsed.port),
|
|
12477
|
+
WEBMUX_PROJECT_DIR: process.cwd(),
|
|
12478
|
+
...parsed.debug ? { WEBMUX_DEBUG: "1" } : {}
|
|
12479
|
+
};
|
|
12480
|
+
const children = [];
|
|
12481
|
+
let exiting = false;
|
|
12482
|
+
function cleanup() {
|
|
12483
|
+
if (exiting)
|
|
12484
|
+
return;
|
|
12485
|
+
exiting = true;
|
|
12154
12486
|
for (const child of children) {
|
|
12155
12487
|
try {
|
|
12156
|
-
child.kill("
|
|
12488
|
+
child.kill("SIGTERM");
|
|
12157
12489
|
} catch {}
|
|
12158
12490
|
}
|
|
12159
|
-
|
|
12160
|
-
|
|
12161
|
-
|
|
12162
|
-
|
|
12163
|
-
|
|
12164
|
-
|
|
12165
|
-
|
|
12166
|
-
|
|
12167
|
-
|
|
12168
|
-
process.
|
|
12169
|
-
|
|
12170
|
-
|
|
12171
|
-
|
|
12172
|
-
|
|
12173
|
-
|
|
12174
|
-
|
|
12175
|
-
}
|
|
12176
|
-
|
|
12177
|
-
|
|
12178
|
-
|
|
12179
|
-
|
|
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
|
+
};
|