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