webmux 0.33.0 → 0.35.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 -0
- package/backend/dist/server.js +724 -423
- package/bin/webmux.js +1382 -329
- package/frontend/dist/assets/DiffDialog-BpDYIOfR.js +112 -0
- package/frontend/dist/assets/index-CU9BHgDe.js +89 -0
- package/frontend/dist/assets/index-DaGuNXKA.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/DiffDialog-BNK_hWb2.js +0 -112
- package/frontend/dist/assets/index-BvV7kChj.js +0 -34
- package/frontend/dist/assets/index-EO_hEDxL.css +0 -1
package/bin/webmux.js
CHANGED
|
@@ -5,43 +5,25 @@ var __getProtoOf = Object.getPrototypeOf;
|
|
|
5
5
|
var __defProp = Object.defineProperty;
|
|
6
6
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
function __accessProp(key) {
|
|
9
|
-
return this[key];
|
|
10
|
-
}
|
|
11
|
-
var __toESMCache_node;
|
|
12
|
-
var __toESMCache_esm;
|
|
13
8
|
var __toESM = (mod, isNodeMode, target) => {
|
|
14
|
-
var canCache = mod != null && typeof mod === "object";
|
|
15
|
-
if (canCache) {
|
|
16
|
-
var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
|
|
17
|
-
var cached = cache.get(mod);
|
|
18
|
-
if (cached)
|
|
19
|
-
return cached;
|
|
20
|
-
}
|
|
21
9
|
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
22
10
|
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
23
11
|
for (let key of __getOwnPropNames(mod))
|
|
24
12
|
if (!__hasOwnProp.call(to, key))
|
|
25
13
|
__defProp(to, key, {
|
|
26
|
-
get:
|
|
14
|
+
get: () => mod[key],
|
|
27
15
|
enumerable: true
|
|
28
16
|
});
|
|
29
|
-
if (canCache)
|
|
30
|
-
cache.set(mod, to);
|
|
31
17
|
return to;
|
|
32
18
|
};
|
|
33
19
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
34
|
-
var __returnValue = (v) => v;
|
|
35
|
-
function __exportSetter(name, newValue) {
|
|
36
|
-
this[name] = __returnValue.bind(null, newValue);
|
|
37
|
-
}
|
|
38
20
|
var __export = (target, all) => {
|
|
39
21
|
for (var name in all)
|
|
40
22
|
__defProp(target, name, {
|
|
41
23
|
get: all[name],
|
|
42
24
|
enumerable: true,
|
|
43
25
|
configurable: true,
|
|
44
|
-
set:
|
|
26
|
+
set: (newValue) => all[name] = () => newValue
|
|
45
27
|
});
|
|
46
28
|
};
|
|
47
29
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
@@ -466,6 +448,7 @@ _webmux() {
|
|
|
466
448
|
'list:List worktrees and their status'
|
|
467
449
|
'open:Open an existing worktree session'
|
|
468
450
|
'close:Close a worktree session'
|
|
451
|
+
'refresh:Refresh a Codex agent terminal'
|
|
469
452
|
'archive:Hide a worktree from the default list'
|
|
470
453
|
'unarchive:Show an archived worktree again'
|
|
471
454
|
'label:Set or clear a workspace label'
|
|
@@ -483,7 +466,7 @@ _webmux() {
|
|
|
483
466
|
fi
|
|
484
467
|
|
|
485
468
|
case "\${words[2]}" in
|
|
486
|
-
open|close|archive|unarchive|label|remove|merge|send)
|
|
469
|
+
open|close|refresh|archive|unarchive|label|remove|merge|send)
|
|
487
470
|
if (( CURRENT == 3 )); then
|
|
488
471
|
local -a branches
|
|
489
472
|
branches=(\${(f)"$(webmux --completions "\${words[2]}" 2>/dev/null)"})
|
|
@@ -534,12 +517,12 @@ compdef _webmux webmux`, BASH_SCRIPT = `_webmux() {
|
|
|
534
517
|
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
535
518
|
|
|
536
519
|
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
537
|
-
COMPREPLY=($(compgen -W "serve init service update add oneshot list open close archive unarchive label remove merge send prune linear completion" -- "\${cur}"))
|
|
520
|
+
COMPREPLY=($(compgen -W "serve init service update add oneshot list open close refresh archive unarchive label remove merge send prune linear completion" -- "\${cur}"))
|
|
538
521
|
return
|
|
539
522
|
fi
|
|
540
523
|
|
|
541
524
|
case "\${COMP_WORDS[1]}" in
|
|
542
|
-
open|close|archive|unarchive|label|remove|merge|send)
|
|
525
|
+
open|close|refresh|archive|unarchive|label|remove|merge|send)
|
|
543
526
|
if [[ \${COMP_CWORD} -eq 2 ]]; then
|
|
544
527
|
local branches
|
|
545
528
|
branches=$(webmux --completions "\${COMP_WORDS[1]}" 2>/dev/null)
|
|
@@ -571,7 +554,7 @@ compdef _webmux webmux`, BASH_SCRIPT = `_webmux() {
|
|
|
571
554
|
complete -F _webmux webmux`;
|
|
572
555
|
var init_completions = __esm(() => {
|
|
573
556
|
init_git();
|
|
574
|
-
BRANCH_SUBCOMMANDS = new Set(["open", "close", "archive", "unarchive", "label", "remove", "merge", "send"]);
|
|
557
|
+
BRANCH_SUBCOMMANDS = new Set(["open", "close", "refresh", "archive", "unarchive", "label", "remove", "merge", "send"]);
|
|
575
558
|
});
|
|
576
559
|
|
|
577
560
|
// node_modules/.bun/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/utils.js
|
|
@@ -710,7 +693,7 @@ var init_dist2 = __esm(() => {
|
|
|
710
693
|
dist_default2 = fastStringWidth;
|
|
711
694
|
});
|
|
712
695
|
|
|
713
|
-
// node_modules/.bun/fast-wrap-ansi@0.2.
|
|
696
|
+
// node_modules/.bun/fast-wrap-ansi@0.2.2/node_modules/fast-wrap-ansi/lib/main.js
|
|
714
697
|
function wrapAnsi(string, columns, options) {
|
|
715
698
|
return String(string).normalize().split(CRLF_OR_LF).map((line) => exec(line, columns, options)).join(`
|
|
716
699
|
`);
|
|
@@ -2719,14 +2702,259 @@ ${result.stderr.trim()}` : ""
|
|
|
2719
2702
|
console.log();
|
|
2720
2703
|
});
|
|
2721
2704
|
|
|
2705
|
+
// backend/src/lib/log.ts
|
|
2706
|
+
function ts() {
|
|
2707
|
+
return new Date().toISOString().slice(11, 23);
|
|
2708
|
+
}
|
|
2709
|
+
var DEBUG, log;
|
|
2710
|
+
var init_log = __esm(() => {
|
|
2711
|
+
DEBUG = Bun.env.WEBMUX_DEBUG === "1";
|
|
2712
|
+
log = {
|
|
2713
|
+
info(msg) {
|
|
2714
|
+
console.log(`[${ts()}] ${msg}`);
|
|
2715
|
+
},
|
|
2716
|
+
debug(msg) {
|
|
2717
|
+
if (DEBUG)
|
|
2718
|
+
console.log(`[${ts()}] ${msg}`);
|
|
2719
|
+
},
|
|
2720
|
+
warn(msg) {
|
|
2721
|
+
console.warn(`[${ts()}] ${msg}`);
|
|
2722
|
+
},
|
|
2723
|
+
error(msg, err) {
|
|
2724
|
+
err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
|
|
2725
|
+
}
|
|
2726
|
+
};
|
|
2727
|
+
});
|
|
2728
|
+
|
|
2729
|
+
// backend/src/domain/policies.ts
|
|
2730
|
+
function sanitizeBranchName(raw) {
|
|
2731
|
+
return raw.toLowerCase().replace(/\s+/g, "-").replace(INVALID_BRANCH_CHARS_RE, "").replace(/@\{/g, "").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/-{2,}/g, "-").replace(/^[.\-/]+|[.\-/]+$/g, "").replace(/\.lock$/i, "");
|
|
2732
|
+
}
|
|
2733
|
+
function isValidBranchName(raw) {
|
|
2734
|
+
return raw.length > 0 && sanitizeBranchName(raw) === raw;
|
|
2735
|
+
}
|
|
2736
|
+
function isValidWorktreeName(name) {
|
|
2737
|
+
return name.length > 0 && VALID_WORKTREE_NAME_RE.test(name) && !name.includes("..");
|
|
2738
|
+
}
|
|
2739
|
+
function isValidEnvKey(key) {
|
|
2740
|
+
return UNSAFE_ENV_KEY_RE.test(key);
|
|
2741
|
+
}
|
|
2742
|
+
function isValidInstancePrefix(value) {
|
|
2743
|
+
return VALID_INSTANCE_PREFIX_RE.test(value) && !RESERVED_INSTANCE_PREFIXES.has(value);
|
|
2744
|
+
}
|
|
2745
|
+
function allocateServicePorts(existingMetas, services) {
|
|
2746
|
+
const allocatable = services.filter((service) => service.portStart != null);
|
|
2747
|
+
if (allocatable.length === 0)
|
|
2748
|
+
return {};
|
|
2749
|
+
const reference = allocatable[0];
|
|
2750
|
+
const referenceStart = reference.portStart;
|
|
2751
|
+
const referenceStep = reference.portStep ?? 1;
|
|
2752
|
+
const occupiedSlots = new Set;
|
|
2753
|
+
for (const meta of existingMetas) {
|
|
2754
|
+
const port = meta.allocatedPorts[reference.portEnv];
|
|
2755
|
+
if (!Number.isInteger(port) || port < referenceStart)
|
|
2756
|
+
continue;
|
|
2757
|
+
const diff = port - referenceStart;
|
|
2758
|
+
if (diff % referenceStep !== 0)
|
|
2759
|
+
continue;
|
|
2760
|
+
occupiedSlots.add(diff / referenceStep);
|
|
2761
|
+
}
|
|
2762
|
+
let slot = 1;
|
|
2763
|
+
while (occupiedSlots.has(slot))
|
|
2764
|
+
slot += 1;
|
|
2765
|
+
const result = {};
|
|
2766
|
+
for (const service of allocatable) {
|
|
2767
|
+
const start = service.portStart;
|
|
2768
|
+
const step = service.portStep ?? 1;
|
|
2769
|
+
result[service.portEnv] = start + slot * step;
|
|
2770
|
+
}
|
|
2771
|
+
return result;
|
|
2772
|
+
}
|
|
2773
|
+
var INVALID_BRANCH_CHARS_RE, UNSAFE_ENV_KEY_RE, VALID_WORKTREE_NAME_RE, VALID_INSTANCE_PREFIX_RE, RESERVED_INSTANCE_PREFIXES;
|
|
2774
|
+
var init_policies = __esm(() => {
|
|
2775
|
+
INVALID_BRANCH_CHARS_RE = /[~^:?*\[\]\\]+/g;
|
|
2776
|
+
UNSAFE_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2777
|
+
VALID_WORKTREE_NAME_RE = /^[a-z0-9][a-z0-9\-_./]*$/;
|
|
2778
|
+
VALID_INSTANCE_PREFIX_RE = /^[a-z0-9][a-z0-9\-]*$/;
|
|
2779
|
+
RESERVED_INSTANCE_PREFIXES = new Set(["api", "ws", "assets"]);
|
|
2780
|
+
});
|
|
2781
|
+
|
|
2782
|
+
// backend/src/adapters/instance-registry.ts
|
|
2783
|
+
import { mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
2784
|
+
import { homedir } from "os";
|
|
2785
|
+
import { join as join5 } from "path";
|
|
2786
|
+
function defaultRegistryDir() {
|
|
2787
|
+
return join5(homedir(), ".webmux", "instances");
|
|
2788
|
+
}
|
|
2789
|
+
function isAlive(pid) {
|
|
2790
|
+
try {
|
|
2791
|
+
process.kill(pid, 0);
|
|
2792
|
+
return true;
|
|
2793
|
+
} catch (err) {
|
|
2794
|
+
return err?.code !== "ESRCH";
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
function isInstanceEntry(value) {
|
|
2798
|
+
if (typeof value !== "object" || value === null)
|
|
2799
|
+
return false;
|
|
2800
|
+
const v2 = value;
|
|
2801
|
+
return typeof v2.prefix === "string" && isValidInstancePrefix(v2.prefix) && typeof v2.port === "number" && typeof v2.projectDir === "string" && typeof v2.pid === "number" && typeof v2.startedAt === "number";
|
|
2802
|
+
}
|
|
2803
|
+
function createInstanceRegistry(dir = defaultRegistryDir()) {
|
|
2804
|
+
function ensureDir() {
|
|
2805
|
+
mkdirSync(dir, { recursive: true });
|
|
2806
|
+
}
|
|
2807
|
+
function entryPath(port) {
|
|
2808
|
+
return join5(dir, `${port}.json`);
|
|
2809
|
+
}
|
|
2810
|
+
function readEntry(filename) {
|
|
2811
|
+
try {
|
|
2812
|
+
const raw = readFileSync3(join5(dir, filename), "utf8");
|
|
2813
|
+
const parsed = JSON.parse(raw);
|
|
2814
|
+
return isInstanceEntry(parsed) ? parsed : null;
|
|
2815
|
+
} catch {
|
|
2816
|
+
return null;
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
return {
|
|
2820
|
+
register(entry) {
|
|
2821
|
+
ensureDir();
|
|
2822
|
+
const finalPath = entryPath(entry.port);
|
|
2823
|
+
const tmpPath = `${finalPath}.${process.pid}.${Date.now()}.tmp`;
|
|
2824
|
+
const text = `${JSON.stringify(entry, null, 2)}
|
|
2825
|
+
`;
|
|
2826
|
+
writeFileSync(tmpPath, text);
|
|
2827
|
+
renameSync(tmpPath, finalPath);
|
|
2828
|
+
},
|
|
2829
|
+
deregister(port, expectedPid) {
|
|
2830
|
+
if (expectedPid !== undefined) {
|
|
2831
|
+
const filename = `${port}.json`;
|
|
2832
|
+
const entry = readEntry(filename);
|
|
2833
|
+
if (entry && entry.pid !== expectedPid) {
|
|
2834
|
+
return;
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
try {
|
|
2838
|
+
unlinkSync(entryPath(port));
|
|
2839
|
+
} catch (err) {
|
|
2840
|
+
const code = err?.code;
|
|
2841
|
+
if (code !== "ENOENT") {
|
|
2842
|
+
log.debug(`[instance-registry] deregister(${port}) failed: ${String(err)}`);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
},
|
|
2846
|
+
listLive() {
|
|
2847
|
+
let filenames;
|
|
2848
|
+
try {
|
|
2849
|
+
filenames = readdirSync2(dir).filter((name) => name.endsWith(".json"));
|
|
2850
|
+
} catch {
|
|
2851
|
+
return [];
|
|
2852
|
+
}
|
|
2853
|
+
const live = [];
|
|
2854
|
+
for (const filename of filenames) {
|
|
2855
|
+
const entry = readEntry(filename);
|
|
2856
|
+
if (!entry)
|
|
2857
|
+
continue;
|
|
2858
|
+
if (!isAlive(entry.pid)) {
|
|
2859
|
+
try {
|
|
2860
|
+
unlinkSync(join5(dir, filename));
|
|
2861
|
+
} catch {}
|
|
2862
|
+
continue;
|
|
2863
|
+
}
|
|
2864
|
+
live.push(entry);
|
|
2865
|
+
}
|
|
2866
|
+
return live;
|
|
2867
|
+
}
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
var init_instance_registry = __esm(() => {
|
|
2871
|
+
init_log();
|
|
2872
|
+
init_policies();
|
|
2873
|
+
});
|
|
2874
|
+
|
|
2875
|
+
// bin/src/install-ports.ts
|
|
2876
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4 } from "fs";
|
|
2877
|
+
import { homedir as homedir2 } from "os";
|
|
2878
|
+
import { join as join6 } from "path";
|
|
2879
|
+
function pickFreePort(start, taken) {
|
|
2880
|
+
const set = new Set(taken);
|
|
2881
|
+
let port = start;
|
|
2882
|
+
while (set.has(port))
|
|
2883
|
+
port += 1;
|
|
2884
|
+
return port;
|
|
2885
|
+
}
|
|
2886
|
+
function readInstalledServicePorts(opts = {}) {
|
|
2887
|
+
const systemdDir = opts.systemdDir ?? DEFAULT_SYSTEMD_DIR;
|
|
2888
|
+
const launchdDir = opts.launchdDir ?? DEFAULT_LAUNCHD_DIR;
|
|
2889
|
+
const ports = [];
|
|
2890
|
+
function collect(dir, namePredicate) {
|
|
2891
|
+
if (!existsSync4(dir))
|
|
2892
|
+
return;
|
|
2893
|
+
let names;
|
|
2894
|
+
try {
|
|
2895
|
+
names = readdirSync3(dir);
|
|
2896
|
+
} catch {
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
for (const name of names) {
|
|
2900
|
+
if (!namePredicate(name))
|
|
2901
|
+
continue;
|
|
2902
|
+
const full = join6(dir, name);
|
|
2903
|
+
if (opts.excludePath && full === opts.excludePath)
|
|
2904
|
+
continue;
|
|
2905
|
+
const port = readPortFromUnit(full);
|
|
2906
|
+
if (port !== null)
|
|
2907
|
+
ports.push(port);
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
collect(systemdDir, (n) => n.startsWith("webmux-") && n.endsWith(".service"));
|
|
2911
|
+
collect(launchdDir, (n) => n.startsWith("com.webmux.") && n.endsWith(".plist"));
|
|
2912
|
+
return ports;
|
|
2913
|
+
}
|
|
2914
|
+
function readPortFromUnit(filePath) {
|
|
2915
|
+
let text;
|
|
2916
|
+
try {
|
|
2917
|
+
text = readFileSync4(filePath, "utf8");
|
|
2918
|
+
} catch {
|
|
2919
|
+
return null;
|
|
2920
|
+
}
|
|
2921
|
+
const regex = filePath.endsWith(".plist") ? LAUNCHD_PORT_RE : SYSTEMD_PORT_RE;
|
|
2922
|
+
const match = regex.exec(text);
|
|
2923
|
+
return match ? parseInt(match[1], 10) : null;
|
|
2924
|
+
}
|
|
2925
|
+
function discoverTakenPorts(opts = {}) {
|
|
2926
|
+
const registry = createInstanceRegistry(opts.registryDir);
|
|
2927
|
+
const live = registry.listLive().map((entry) => entry.port);
|
|
2928
|
+
const installed = readInstalledServicePorts({
|
|
2929
|
+
systemdDir: opts.systemdDir,
|
|
2930
|
+
launchdDir: opts.launchdDir,
|
|
2931
|
+
excludePath: opts.excludeUnitPath
|
|
2932
|
+
});
|
|
2933
|
+
return new Set([...live, ...installed]);
|
|
2934
|
+
}
|
|
2935
|
+
var DEFAULT_SYSTEMD_DIR, DEFAULT_LAUNCHD_DIR, SYSTEMD_PORT_RE, LAUNCHD_PORT_RE;
|
|
2936
|
+
var init_install_ports = __esm(() => {
|
|
2937
|
+
init_instance_registry();
|
|
2938
|
+
DEFAULT_SYSTEMD_DIR = join6(homedir2(), ".config", "systemd", "user");
|
|
2939
|
+
DEFAULT_LAUNCHD_DIR = join6(homedir2(), "Library", "LaunchAgents");
|
|
2940
|
+
SYSTEMD_PORT_RE = /--port\s+(\d+)/;
|
|
2941
|
+
LAUNCHD_PORT_RE = /<string>--port<\/string>\s*<string>(\d+)<\/string>/;
|
|
2942
|
+
});
|
|
2943
|
+
|
|
2722
2944
|
// bin/src/service.ts
|
|
2723
2945
|
var exports_service = {};
|
|
2724
2946
|
__export(exports_service, {
|
|
2725
|
-
|
|
2947
|
+
resolveEnvVars: () => resolveEnvVars,
|
|
2948
|
+
readEnvVarsFromUnit: () => readEnvVarsFromUnit,
|
|
2949
|
+
parseInstalledServiceConfig: () => parseInstalledServiceConfig,
|
|
2950
|
+
parseEnvCliArgs: () => parseEnvCliArgs,
|
|
2951
|
+
generateServiceFile: () => generateServiceFile,
|
|
2952
|
+
default: () => service,
|
|
2953
|
+
AUTO_PICKUP_ENV_VARS: () => AUTO_PICKUP_ENV_VARS
|
|
2726
2954
|
});
|
|
2727
|
-
import { existsSync as
|
|
2728
|
-
import { join as
|
|
2729
|
-
import { homedir } from "os";
|
|
2955
|
+
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
2956
|
+
import { basename as basename3, join as join7 } from "path";
|
|
2957
|
+
import { homedir as homedir3 } from "os";
|
|
2730
2958
|
function getPlatform() {
|
|
2731
2959
|
const plat = process.platform;
|
|
2732
2960
|
if (plat === "linux" || plat === "darwin")
|
|
@@ -2755,10 +2983,10 @@ function printRunResult(result) {
|
|
|
2755
2983
|
console.error(err);
|
|
2756
2984
|
}
|
|
2757
2985
|
function systemdUnitPath(serviceName) {
|
|
2758
|
-
return
|
|
2986
|
+
return join7(homedir3(), ".config", "systemd", "user", `${serviceName}.service`);
|
|
2759
2987
|
}
|
|
2760
2988
|
function launchdPlistPath(serviceName) {
|
|
2761
|
-
return
|
|
2989
|
+
return join7(homedir3(), "Library", "LaunchAgents", `com.webmux.${serviceName}.plist`);
|
|
2762
2990
|
}
|
|
2763
2991
|
function serviceFilePath(config) {
|
|
2764
2992
|
if (config.platform === "linux")
|
|
@@ -2766,6 +2994,8 @@ function serviceFilePath(config) {
|
|
|
2766
2994
|
return launchdPlistPath(config.serviceName);
|
|
2767
2995
|
}
|
|
2768
2996
|
function generateSystemdUnit(config) {
|
|
2997
|
+
const extra = Object.keys(config.envVars).sort().map((key) => `Environment=${key}=${config.envVars[key]}`).join(`
|
|
2998
|
+
`);
|
|
2769
2999
|
return `[Unit]
|
|
2770
3000
|
Description=webmux dashboard \u2014 ${config.projectName}
|
|
2771
3001
|
|
|
@@ -2777,14 +3007,21 @@ Restart=on-failure
|
|
|
2777
3007
|
RestartSec=5
|
|
2778
3008
|
Environment=PORT=${config.port}
|
|
2779
3009
|
Environment=WEBMUX_PROJECT_DIR=${config.projectDir}
|
|
2780
|
-
Environment=PATH=${process.env.PATH}
|
|
3010
|
+
Environment=PATH=${process.env.PATH}${extra ? `
|
|
3011
|
+
` + extra : ""}
|
|
2781
3012
|
|
|
2782
3013
|
[Install]
|
|
2783
3014
|
WantedBy=default.target
|
|
2784
3015
|
`;
|
|
2785
3016
|
}
|
|
3017
|
+
function escapePlistText(value) {
|
|
3018
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
3019
|
+
}
|
|
2786
3020
|
function generateLaunchdPlist(config) {
|
|
2787
|
-
const logPath =
|
|
3021
|
+
const logPath = join7(homedir3(), "Library", "Logs", `webmux-${config.serviceName}.log`);
|
|
3022
|
+
const extra = Object.keys(config.envVars).sort().map((key) => ` <key>${escapePlistText(key)}</key>
|
|
3023
|
+
<string>${escapePlistText(config.envVars[key])}</string>`).join(`
|
|
3024
|
+
`);
|
|
2788
3025
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2789
3026
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2790
3027
|
<plist version="1.0">
|
|
@@ -2818,7 +3055,8 @@ function generateLaunchdPlist(config) {
|
|
|
2818
3055
|
<key>WEBMUX_PROJECT_DIR</key>
|
|
2819
3056
|
<string>${config.projectDir}</string>
|
|
2820
3057
|
<key>PATH</key>
|
|
2821
|
-
<string>${process.env.PATH}</string
|
|
3058
|
+
<string>${process.env.PATH}</string>${extra ? `
|
|
3059
|
+
` + extra : ""}
|
|
2822
3060
|
</dict>
|
|
2823
3061
|
</dict>
|
|
2824
3062
|
</plist>
|
|
@@ -2829,6 +3067,69 @@ function generateServiceFile(config) {
|
|
|
2829
3067
|
return generateSystemdUnit(config);
|
|
2830
3068
|
return generateLaunchdPlist(config);
|
|
2831
3069
|
}
|
|
3070
|
+
function readWorkingDirFromUnit(filePath, platform) {
|
|
3071
|
+
let text;
|
|
3072
|
+
try {
|
|
3073
|
+
text = readFileSync5(filePath, "utf8");
|
|
3074
|
+
} catch {
|
|
3075
|
+
return null;
|
|
3076
|
+
}
|
|
3077
|
+
const regex = platform === "linux" ? SYSTEMD_WORKDIR_RE : LAUNCHD_WORKDIR_RE;
|
|
3078
|
+
const match = regex.exec(text);
|
|
3079
|
+
return match ? match[1].trim() : null;
|
|
3080
|
+
}
|
|
3081
|
+
function unescapePlistText(value) {
|
|
3082
|
+
return value.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
3083
|
+
}
|
|
3084
|
+
function readEnvVarsFromUnit(filePath, platform) {
|
|
3085
|
+
let text;
|
|
3086
|
+
try {
|
|
3087
|
+
text = readFileSync5(filePath, "utf8");
|
|
3088
|
+
} catch {
|
|
3089
|
+
return {};
|
|
3090
|
+
}
|
|
3091
|
+
const out = {};
|
|
3092
|
+
if (platform === "linux") {
|
|
3093
|
+
for (const match of text.matchAll(SYSTEMD_ENV_RE)) {
|
|
3094
|
+
const key = match[1];
|
|
3095
|
+
if (RESERVED_ENV_KEYS.has(key))
|
|
3096
|
+
continue;
|
|
3097
|
+
out[key] = match[2];
|
|
3098
|
+
}
|
|
3099
|
+
return out;
|
|
3100
|
+
}
|
|
3101
|
+
const dict = LAUNCHD_ENV_DICT_RE.exec(text);
|
|
3102
|
+
if (!dict)
|
|
3103
|
+
return out;
|
|
3104
|
+
for (const match of dict[1].matchAll(LAUNCHD_ENV_ENTRY_RE)) {
|
|
3105
|
+
const key = unescapePlistText(match[1]);
|
|
3106
|
+
if (RESERVED_ENV_KEYS.has(key))
|
|
3107
|
+
continue;
|
|
3108
|
+
out[key] = unescapePlistText(match[2]);
|
|
3109
|
+
}
|
|
3110
|
+
return out;
|
|
3111
|
+
}
|
|
3112
|
+
function parseInstalledServiceConfig(filePath, platform, webmuxPath) {
|
|
3113
|
+
const port = readPortFromUnit(filePath);
|
|
3114
|
+
if (port === null)
|
|
3115
|
+
return null;
|
|
3116
|
+
const projectDir = readWorkingDirFromUnit(filePath, platform);
|
|
3117
|
+
if (projectDir === null)
|
|
3118
|
+
return null;
|
|
3119
|
+
const fileBase = basename3(filePath);
|
|
3120
|
+
const serviceName = platform === "linux" ? fileBase.replace(/\.service$/, "") : fileBase.replace(/^com\.webmux\./, "").replace(/\.plist$/, "");
|
|
3121
|
+
const projectName = detectProjectName(projectDir);
|
|
3122
|
+
const envVars = readEnvVarsFromUnit(filePath, platform);
|
|
3123
|
+
return {
|
|
3124
|
+
platform,
|
|
3125
|
+
projectName,
|
|
3126
|
+
serviceName,
|
|
3127
|
+
webmuxPath,
|
|
3128
|
+
projectDir,
|
|
3129
|
+
port,
|
|
3130
|
+
envVars
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
2832
3133
|
function installCommands(config) {
|
|
2833
3134
|
if (config.platform === "linux") {
|
|
2834
3135
|
return [
|
|
@@ -2852,11 +3153,77 @@ function uninstallCommands(config) {
|
|
|
2852
3153
|
];
|
|
2853
3154
|
}
|
|
2854
3155
|
function isInstalled(config) {
|
|
2855
|
-
return
|
|
3156
|
+
return existsSync5(serviceFilePath(config));
|
|
3157
|
+
}
|
|
3158
|
+
function resolveEnvVars(opts) {
|
|
3159
|
+
const envVars = { ...opts.existing };
|
|
3160
|
+
const notes = [];
|
|
3161
|
+
for (const key of Object.keys(opts.existing).sort()) {
|
|
3162
|
+
notes.push(` ${key} (kept from existing unit)`);
|
|
3163
|
+
}
|
|
3164
|
+
if (opts.autoPickup) {
|
|
3165
|
+
for (const key of AUTO_PICKUP_ENV_VARS) {
|
|
3166
|
+
const value = opts.processEnv[key];
|
|
3167
|
+
if (value === undefined || value === "")
|
|
3168
|
+
continue;
|
|
3169
|
+
const prior = envVars[key];
|
|
3170
|
+
envVars[key] = value;
|
|
3171
|
+
notes.push(prior === undefined ? ` ${key} (auto-picked from shell environment)` : prior === value ? ` ${key} (auto-pick matched existing value)` : ` ${key} (auto-picked from shell environment, overrides existing)`);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
for (const [key, value] of Object.entries(opts.cliEnv)) {
|
|
3175
|
+
const prior = envVars[key];
|
|
3176
|
+
envVars[key] = value;
|
|
3177
|
+
notes.push(prior === undefined ? ` ${key} (from --env)` : ` ${key} (from --env, overrides previous value)`);
|
|
3178
|
+
}
|
|
3179
|
+
return { envVars, notes };
|
|
2856
3180
|
}
|
|
2857
|
-
|
|
3181
|
+
function parseEnvCliArgs(args) {
|
|
3182
|
+
const envVars = {};
|
|
3183
|
+
const errors = [];
|
|
3184
|
+
for (let i = 0;i < args.length; i++) {
|
|
3185
|
+
if (args[i] !== "--env")
|
|
3186
|
+
continue;
|
|
3187
|
+
const raw = args[i + 1];
|
|
3188
|
+
if (raw === undefined) {
|
|
3189
|
+
errors.push("--env requires a KEY=VALUE argument");
|
|
3190
|
+
break;
|
|
3191
|
+
}
|
|
3192
|
+
i++;
|
|
3193
|
+
const eq = raw.indexOf("=");
|
|
3194
|
+
if (eq <= 0) {
|
|
3195
|
+
errors.push(`--env expects KEY=VALUE (got: ${raw})`);
|
|
3196
|
+
continue;
|
|
3197
|
+
}
|
|
3198
|
+
const key = raw.slice(0, eq);
|
|
3199
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
3200
|
+
errors.push(`--env key is not a valid identifier: ${key}`);
|
|
3201
|
+
continue;
|
|
3202
|
+
}
|
|
3203
|
+
if (RESERVED_ENV_KEYS.has(key)) {
|
|
3204
|
+
errors.push(`--env cannot set ${key} \u2014 it is managed by the service unit`);
|
|
3205
|
+
continue;
|
|
3206
|
+
}
|
|
3207
|
+
envVars[key] = raw.slice(eq + 1);
|
|
3208
|
+
}
|
|
3209
|
+
return { envVars, errors };
|
|
3210
|
+
}
|
|
3211
|
+
function redactSecretsInUnit(content, envVars) {
|
|
3212
|
+
let out = content;
|
|
3213
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
3214
|
+
if (!value)
|
|
3215
|
+
continue;
|
|
3216
|
+
if (!/(?:TOKEN|KEY|PASSWORD|SECRET)$/i.test(key))
|
|
3217
|
+
continue;
|
|
3218
|
+
const masked = `\u2022\u2022\u2022 (${value.length} chars)`;
|
|
3219
|
+
out = out.split(value).join(masked);
|
|
3220
|
+
}
|
|
3221
|
+
return out;
|
|
3222
|
+
}
|
|
3223
|
+
async function install(config, portExplicit, envVarNotes) {
|
|
2858
3224
|
const filePath = serviceFilePath(config);
|
|
2859
|
-
|
|
3225
|
+
const alreadyInstalled = isInstalled(config);
|
|
3226
|
+
if (alreadyInstalled) {
|
|
2860
3227
|
const reinstall = await ue({ message: "Service is already installed. Reinstall?" });
|
|
2861
3228
|
if (q(reinstall) || !reinstall) {
|
|
2862
3229
|
R2.info("Aborted.");
|
|
@@ -2866,24 +3233,66 @@ async function install(config) {
|
|
|
2866
3233
|
runCommand(cmd);
|
|
2867
3234
|
}
|
|
2868
3235
|
}
|
|
3236
|
+
const requestedPort = config.port;
|
|
3237
|
+
let chosenPort = requestedPort;
|
|
3238
|
+
let portNote = null;
|
|
3239
|
+
let portWarning = null;
|
|
3240
|
+
if (!portExplicit) {
|
|
3241
|
+
const existingPort = alreadyInstalled ? readPortFromUnit(filePath) : null;
|
|
3242
|
+
if (existingPort !== null) {
|
|
3243
|
+
chosenPort = existingPort;
|
|
3244
|
+
if (existingPort !== requestedPort) {
|
|
3245
|
+
portNote = `Reusing port ${existingPort} from the existing service unit (pass --port to override).`;
|
|
3246
|
+
}
|
|
3247
|
+
} else {
|
|
3248
|
+
const taken = discoverTakenPorts({ excludeUnitPath: filePath });
|
|
3249
|
+
chosenPort = pickFreePort(requestedPort, taken);
|
|
3250
|
+
if (chosenPort !== requestedPort) {
|
|
3251
|
+
portNote = `Port ${requestedPort} is already used by another webmux instance \u2014 picked ${chosenPort} instead (pass --port to override).`;
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
} else {
|
|
3255
|
+
const taken = discoverTakenPorts({ excludeUnitPath: filePath });
|
|
3256
|
+
if (taken.has(requestedPort)) {
|
|
3257
|
+
portWarning = `Port ${requestedPort} is already claimed by another webmux instance. The service will fail to bind on start; omit --port to auto-pick a free port.`;
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
config = { ...config, port: chosenPort };
|
|
2869
3261
|
const content = generateServiceFile(config);
|
|
2870
3262
|
const commands = installCommands(config);
|
|
3263
|
+
const displayContent = redactSecretsInUnit(content, config.envVars);
|
|
2871
3264
|
Se([
|
|
2872
3265
|
`File: ${filePath}`,
|
|
2873
3266
|
"",
|
|
2874
3267
|
"Contents:",
|
|
2875
|
-
|
|
3268
|
+
displayContent,
|
|
2876
3269
|
"Commands to run:",
|
|
2877
3270
|
...commands.map((c) => ` $ ${formatCommand(c)}`)
|
|
2878
3271
|
].join(`
|
|
2879
3272
|
`), "Install service");
|
|
3273
|
+
if (Object.keys(config.envVars).length > 0) {
|
|
3274
|
+
R2.info(`Environment variables baked into the unit:
|
|
3275
|
+
${envVarNotes.join(`
|
|
3276
|
+
`)}`);
|
|
3277
|
+
}
|
|
3278
|
+
if (portNote)
|
|
3279
|
+
R2.info(portNote);
|
|
3280
|
+
if (portWarning)
|
|
3281
|
+
R2.warn(portWarning);
|
|
2880
3282
|
const ok = await ue({ message: "Proceed?" });
|
|
2881
3283
|
if (q(ok) || !ok) {
|
|
2882
3284
|
R2.info("Aborted.");
|
|
2883
3285
|
return;
|
|
2884
3286
|
}
|
|
2885
|
-
|
|
3287
|
+
mkdirSync2(filePath.substring(0, filePath.lastIndexOf("/")), { recursive: true });
|
|
2886
3288
|
await Bun.write(filePath, content);
|
|
3289
|
+
if (Object.keys(config.envVars).length > 0) {
|
|
3290
|
+
try {
|
|
3291
|
+
chmodSync(filePath, 384);
|
|
3292
|
+
} catch (err) {
|
|
3293
|
+
R2.warn(`Wrote ${filePath} but could not chmod 600: ${String(err)}`);
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
2887
3296
|
R2.success(`Wrote ${filePath}`);
|
|
2888
3297
|
for (const cmd of commands) {
|
|
2889
3298
|
const result = runCommand(cmd);
|
|
@@ -2932,7 +3341,7 @@ ${result.stderr.toString()}`);
|
|
|
2932
3341
|
R2.success(`$ ${formatCommand(cmd)}`);
|
|
2933
3342
|
}
|
|
2934
3343
|
}
|
|
2935
|
-
|
|
3344
|
+
unlinkSync2(filePath);
|
|
2936
3345
|
R2.success(`Removed ${filePath}`);
|
|
2937
3346
|
R2.success("Service uninstalled.");
|
|
2938
3347
|
}
|
|
@@ -2956,8 +3365,8 @@ function logs(config) {
|
|
|
2956
3365
|
if (config.platform === "linux") {
|
|
2957
3366
|
proc = Bun.spawn(["journalctl", "--user", "-u", config.serviceName, "-f", "--no-pager"], { stdout: "inherit", stderr: "inherit" });
|
|
2958
3367
|
} else {
|
|
2959
|
-
const logPath =
|
|
2960
|
-
if (!
|
|
3368
|
+
const logPath = join7(homedir3(), "Library", "Logs", `webmux-${config.serviceName}.log`);
|
|
3369
|
+
if (!existsSync5(logPath)) {
|
|
2961
3370
|
R2.error(`Log file not found: ${logPath}`);
|
|
2962
3371
|
return;
|
|
2963
3372
|
}
|
|
@@ -2978,6 +3387,22 @@ Usage:
|
|
|
2978
3387
|
webmux service uninstall Stop, disable, and remove the service
|
|
2979
3388
|
webmux service status Show service status
|
|
2980
3389
|
webmux service logs Tail service logs
|
|
3390
|
+
|
|
3391
|
+
Options:
|
|
3392
|
+
--port N Pin the service to a specific port. When omitted,
|
|
3393
|
+
a free port is picked automatically by scanning
|
|
3394
|
+
other webmux instances and installed services
|
|
3395
|
+
\u2014 second-project installs no longer collide on 5111.
|
|
3396
|
+
--env KEY=VALUE Bake an environment variable into the service
|
|
3397
|
+
unit (repeatable). Reserved keys PORT,
|
|
3398
|
+
WEBMUX_PROJECT_DIR, and PATH are rejected.
|
|
3399
|
+
--no-auto-env Skip auto-detection of webmux-relevant env vars
|
|
3400
|
+
from the current shell (default: detect
|
|
3401
|
+
${AUTO_PICKUP_ENV_VARS.join(", ")}).
|
|
3402
|
+
Useful in CI / non-interactive installs.
|
|
3403
|
+
|
|
3404
|
+
When any env var is set, the unit file is written with mode 0600 so
|
|
3405
|
+
secrets are readable only by the installing user.
|
|
2981
3406
|
`);
|
|
2982
3407
|
}
|
|
2983
3408
|
async function service(args) {
|
|
@@ -3013,6 +3438,8 @@ async function service(args) {
|
|
|
3013
3438
|
return;
|
|
3014
3439
|
}
|
|
3015
3440
|
let port = parseInt(process.env.PORT || "5111");
|
|
3441
|
+
let portExplicit = false;
|
|
3442
|
+
let autoPickup = true;
|
|
3016
3443
|
for (let i = 1;i < args.length; i++) {
|
|
3017
3444
|
if (args[i] === "--port" && args[i + 1]) {
|
|
3018
3445
|
const parsed = parseInt(args[++i]);
|
|
@@ -3021,21 +3448,44 @@ async function service(args) {
|
|
|
3021
3448
|
return;
|
|
3022
3449
|
}
|
|
3023
3450
|
port = parsed;
|
|
3451
|
+
portExplicit = true;
|
|
3452
|
+
} else if (args[i] === "--no-auto-env") {
|
|
3453
|
+
autoPickup = false;
|
|
3024
3454
|
}
|
|
3025
3455
|
}
|
|
3456
|
+
const cliEnv = parseEnvCliArgs(args.slice(1));
|
|
3457
|
+
if (cliEnv.errors.length > 0) {
|
|
3458
|
+
for (const err of cliEnv.errors)
|
|
3459
|
+
R2.error(err);
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3026
3462
|
const projectName = detectProjectName(gitRoot2);
|
|
3027
3463
|
const serviceName = `webmux-${sanitizeName(projectName)}`;
|
|
3464
|
+
let envVars = {};
|
|
3465
|
+
let envVarNotes = [];
|
|
3466
|
+
if (action === "install") {
|
|
3467
|
+
const existing = isInstalledAt(platform, serviceName) ? readEnvVarsFromUnit(platform === "linux" ? systemdUnitPath(serviceName) : launchdPlistPath(serviceName), platform) : {};
|
|
3468
|
+
const resolved = resolveEnvVars({
|
|
3469
|
+
cliEnv: cliEnv.envVars,
|
|
3470
|
+
processEnv: process.env,
|
|
3471
|
+
existing,
|
|
3472
|
+
autoPickup
|
|
3473
|
+
});
|
|
3474
|
+
envVars = resolved.envVars;
|
|
3475
|
+
envVarNotes = resolved.notes;
|
|
3476
|
+
}
|
|
3028
3477
|
const config = {
|
|
3029
3478
|
platform,
|
|
3030
3479
|
projectName,
|
|
3031
3480
|
serviceName,
|
|
3032
3481
|
webmuxPath,
|
|
3033
3482
|
projectDir: gitRoot2,
|
|
3034
|
-
port
|
|
3483
|
+
port,
|
|
3484
|
+
envVars
|
|
3035
3485
|
};
|
|
3036
3486
|
switch (action) {
|
|
3037
3487
|
case "install":
|
|
3038
|
-
await install(config);
|
|
3488
|
+
await install(config, portExplicit, envVarNotes);
|
|
3039
3489
|
break;
|
|
3040
3490
|
case "uninstall":
|
|
3041
3491
|
await uninstall(config);
|
|
@@ -3048,9 +3498,150 @@ async function service(args) {
|
|
|
3048
3498
|
break;
|
|
3049
3499
|
}
|
|
3050
3500
|
}
|
|
3501
|
+
function isInstalledAt(platform, serviceName) {
|
|
3502
|
+
const path = platform === "linux" ? systemdUnitPath(serviceName) : launchdPlistPath(serviceName);
|
|
3503
|
+
return existsSync5(path);
|
|
3504
|
+
}
|
|
3505
|
+
var AUTO_PICKUP_ENV_VARS, RESERVED_ENV_KEYS, SYSTEMD_WORKDIR_RE, LAUNCHD_WORKDIR_RE, SYSTEMD_ENV_RE, LAUNCHD_ENV_DICT_RE, LAUNCHD_ENV_ENTRY_RE;
|
|
3051
3506
|
var init_service = __esm(() => {
|
|
3052
3507
|
init_dist4();
|
|
3053
3508
|
init_shared();
|
|
3509
|
+
init_install_ports();
|
|
3510
|
+
AUTO_PICKUP_ENV_VARS = ["LINEAR_API_KEY"];
|
|
3511
|
+
RESERVED_ENV_KEYS = new Set(["PORT", "WEBMUX_PROJECT_DIR", "PATH"]);
|
|
3512
|
+
SYSTEMD_WORKDIR_RE = /^WorkingDirectory=(.+)$/m;
|
|
3513
|
+
LAUNCHD_WORKDIR_RE = /<key>WorkingDirectory<\/key>\s*<string>([^<]+)<\/string>/;
|
|
3514
|
+
SYSTEMD_ENV_RE = /^Environment=([A-Za-z_][A-Za-z0-9_]*)=(.*)$/gm;
|
|
3515
|
+
LAUNCHD_ENV_DICT_RE = /<key>EnvironmentVariables<\/key>\s*<dict>([\s\S]*?)<\/dict>/;
|
|
3516
|
+
LAUNCHD_ENV_ENTRY_RE = /<key>([^<]+)<\/key>\s*<string>([^<]*)<\/string>/g;
|
|
3517
|
+
});
|
|
3518
|
+
|
|
3519
|
+
// bin/src/service-restart.ts
|
|
3520
|
+
var exports_service_restart = {};
|
|
3521
|
+
__export(exports_service_restart, {
|
|
3522
|
+
updateInstalledService: () => updateInstalledService,
|
|
3523
|
+
restartInstalledService: () => restartInstalledService,
|
|
3524
|
+
restartCommand: () => restartCommand,
|
|
3525
|
+
listInstalledServices: () => listInstalledServices
|
|
3526
|
+
});
|
|
3527
|
+
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
|
|
3528
|
+
import { homedir as homedir4 } from "os";
|
|
3529
|
+
import { join as join8 } from "path";
|
|
3530
|
+
function listInstalledServices(opts = {}) {
|
|
3531
|
+
const out = [];
|
|
3532
|
+
const systemdDir = opts.systemdDir ?? DEFAULT_SYSTEMD_DIR2;
|
|
3533
|
+
const launchdDir = opts.launchdDir ?? DEFAULT_LAUNCHD_DIR2;
|
|
3534
|
+
if (existsSync6(systemdDir)) {
|
|
3535
|
+
try {
|
|
3536
|
+
for (const name of readdirSync4(systemdDir)) {
|
|
3537
|
+
if (!name.startsWith("webmux-") || !name.endsWith(".service"))
|
|
3538
|
+
continue;
|
|
3539
|
+
out.push({
|
|
3540
|
+
name: name.slice(0, -".service".length),
|
|
3541
|
+
filePath: join8(systemdDir, name),
|
|
3542
|
+
platform: "linux"
|
|
3543
|
+
});
|
|
3544
|
+
}
|
|
3545
|
+
} catch {}
|
|
3546
|
+
}
|
|
3547
|
+
if (existsSync6(launchdDir)) {
|
|
3548
|
+
try {
|
|
3549
|
+
for (const name of readdirSync4(launchdDir)) {
|
|
3550
|
+
if (!name.startsWith("com.webmux.") || !name.endsWith(".plist"))
|
|
3551
|
+
continue;
|
|
3552
|
+
out.push({
|
|
3553
|
+
name: name.slice(0, -".plist".length),
|
|
3554
|
+
filePath: join8(launchdDir, name),
|
|
3555
|
+
platform: "darwin"
|
|
3556
|
+
});
|
|
3557
|
+
}
|
|
3558
|
+
} catch {}
|
|
3559
|
+
}
|
|
3560
|
+
return out;
|
|
3561
|
+
}
|
|
3562
|
+
function restartCommand(service2, uid) {
|
|
3563
|
+
if (service2.platform === "linux") {
|
|
3564
|
+
return { bin: "systemctl", args: ["--user", "restart", service2.name] };
|
|
3565
|
+
}
|
|
3566
|
+
return { bin: "launchctl", args: ["kickstart", "-k", `gui/${uid}/${service2.name}`] };
|
|
3567
|
+
}
|
|
3568
|
+
function restartInstalledService(service2, runner = defaultRunner) {
|
|
3569
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
3570
|
+
const { bin, args } = restartCommand(service2, uid);
|
|
3571
|
+
const result = runner.run(bin, args);
|
|
3572
|
+
if (!result.success) {
|
|
3573
|
+
return {
|
|
3574
|
+
service: service2,
|
|
3575
|
+
ok: false,
|
|
3576
|
+
error: result.stderr.toString().trim() || `${bin} ${args.join(" ")} failed`
|
|
3577
|
+
};
|
|
3578
|
+
}
|
|
3579
|
+
return { service: service2, ok: true };
|
|
3580
|
+
}
|
|
3581
|
+
function reloadAfterRegenerate(service2, runner) {
|
|
3582
|
+
if (service2.platform === "linux") {
|
|
3583
|
+
const result = runner.run("systemctl", ["--user", "daemon-reload"]);
|
|
3584
|
+
return result.success ? { ok: true } : { ok: false, error: result.stderr.toString().trim() || "daemon-reload failed" };
|
|
3585
|
+
}
|
|
3586
|
+
runner.run("launchctl", ["unload", service2.filePath]);
|
|
3587
|
+
const loadResult = runner.run("launchctl", ["load", "-w", service2.filePath]);
|
|
3588
|
+
if (loadResult.success)
|
|
3589
|
+
return { ok: true };
|
|
3590
|
+
const stderr = loadResult.stderr.toString().trim() || "load failed";
|
|
3591
|
+
return {
|
|
3592
|
+
ok: false,
|
|
3593
|
+
error: `${stderr}
|
|
3594
|
+
service is now unloaded \u2014 recover with: launchctl load -w "${service2.filePath}"`
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
async function updateInstalledService(service2, webmuxPath, runner = defaultRunner) {
|
|
3598
|
+
const canRegenerate = webmuxPath.length > 0;
|
|
3599
|
+
const config = canRegenerate ? parseInstalledServiceConfig(service2.filePath, service2.platform, webmuxPath) : null;
|
|
3600
|
+
let regenerated = false;
|
|
3601
|
+
if (config !== null) {
|
|
3602
|
+
let currentContent = "";
|
|
3603
|
+
try {
|
|
3604
|
+
currentContent = readFileSync6(service2.filePath, "utf8");
|
|
3605
|
+
} catch {}
|
|
3606
|
+
const expected = generateServiceFile(config);
|
|
3607
|
+
if (currentContent !== expected) {
|
|
3608
|
+
try {
|
|
3609
|
+
await Bun.write(service2.filePath, expected);
|
|
3610
|
+
regenerated = true;
|
|
3611
|
+
} catch (err) {
|
|
3612
|
+
return {
|
|
3613
|
+
service: service2,
|
|
3614
|
+
regenerated: false,
|
|
3615
|
+
restarted: false,
|
|
3616
|
+
error: `could not rewrite ${service2.filePath}: ${String(err)}`
|
|
3617
|
+
};
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3621
|
+
if (regenerated) {
|
|
3622
|
+
const reload = reloadAfterRegenerate(service2, runner);
|
|
3623
|
+
if (!reload.ok) {
|
|
3624
|
+
return { service: service2, regenerated, restarted: false, error: reload.error };
|
|
3625
|
+
}
|
|
3626
|
+
if (service2.platform === "darwin") {
|
|
3627
|
+
return { service: service2, regenerated, restarted: true };
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
const outcome = restartInstalledService(service2, runner);
|
|
3631
|
+
return {
|
|
3632
|
+
service: service2,
|
|
3633
|
+
regenerated,
|
|
3634
|
+
restarted: outcome.ok,
|
|
3635
|
+
error: outcome.error
|
|
3636
|
+
};
|
|
3637
|
+
}
|
|
3638
|
+
var defaultRunner, DEFAULT_SYSTEMD_DIR2, DEFAULT_LAUNCHD_DIR2;
|
|
3639
|
+
var init_service_restart = __esm(() => {
|
|
3640
|
+
init_shared();
|
|
3641
|
+
init_service();
|
|
3642
|
+
defaultRunner = { run };
|
|
3643
|
+
DEFAULT_SYSTEMD_DIR2 = join8(homedir4(), ".config", "systemd", "user");
|
|
3644
|
+
DEFAULT_LAUNCHD_DIR2 = join8(homedir4(), "Library", "LaunchAgents");
|
|
3054
3645
|
});
|
|
3055
3646
|
|
|
3056
3647
|
// node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
|
|
@@ -7019,7 +7610,7 @@ var init_zod = __esm(() => {
|
|
|
7019
7610
|
init_external();
|
|
7020
7611
|
});
|
|
7021
7612
|
|
|
7022
|
-
// node_modules/.bun/@ts-rest+core@3.52.1+
|
|
7613
|
+
// node_modules/.bun/@ts-rest+core@3.52.1+c185e43edea803d3/node_modules/@ts-rest/core/index.esm.mjs
|
|
7023
7614
|
var isZodType = (obj) => {
|
|
7024
7615
|
return typeof (obj === null || obj === undefined ? undefined : obj.safeParse) === "function";
|
|
7025
7616
|
}, isZodObjectStrict = (obj) => {
|
|
@@ -7341,7 +7932,7 @@ function parseLinearTarget(raw) {
|
|
|
7341
7932
|
return { kind: "team", teamKey: trimmed };
|
|
7342
7933
|
return { kind: "invalid", raw: trimmed };
|
|
7343
7934
|
}
|
|
7344
|
-
var BooleanLikeSchema, ErrorResponseSchema, OkResponseSchema, EnabledResponseSchema, BuiltInAgentIdSchema, AgentIdSchema, WorktreeCreateModeSchema, LinearIssueIdSchema, LinearTeamKeySchema, PostWorktreeToLinearTargetSchema, PostWorktreeToLinearRequestSchema, PostWorktreeToLinearResponseSchema, FromLinearInputSchema, OneshotConfigSchema, AgentCapabilitiesSchema, AgentSummarySchema, AgentDetailsSchema, AgentListResponseSchema, UpsertCustomAgentRequestSchema, AgentResponseSchema, ValidateCustomAgentResponseSchema, WorktreeCreationPhaseSchema, AvailableBranchSchema, AvailableBranchesQuerySchema, NumberLikePathParamSchema, BranchListResponseSchema, WorktreeSourceSchema, CreateWorktreeRequestSchema, OpenWorktreeRequestSchema, CreateWorktreeResponseSchema, SetWorktreeArchivedRequestSchema, SetWorktreeArchivedResponseSchema, SetWorktreeLabelRequestSchema, SetWorktreeLabelResponseSchema, ToggleEnabledRequestSchema, SendWorktreePromptRequestSchema, AgentsSendMessageRequestSchema, PullMainRequestSchema, PullMainStatusSchema, PullMainResponseSchema, ServiceStatusSchema, PrCommentSchema, CiCheckSchema, PrEntrySchema, LinearIssueLabelSchema, LinearIssueStateSchema, LinkedLinearIssueSchema, LinearIssueSchema, LinearIssueAvailabilitySchema, LinearIssuesResponseSchema, WorktreeCreationStateSchema, AppNotificationSchema, ProjectWorktreeSnapshotSchema, ProjectSnapshotSchema, WorktreeConversationProviderSchema, CodexWorktreeConversationRefSchema, ClaudeWorktreeConversationRefSchema, WorktreeConversationRefSchema, AgentsUiWorktreeSummarySchema, AgentsUiConversationMessageRoleSchema, AgentsUiConversationMessageStatusSchema, AgentsUiConversationMessageKindSchema, AgentsUiConversationMessageSchema, AgentsUiConversationStateSchema, AgentsUiWorktreeConversationResponseSchema, AgentsUiSendMessageResponseSchema, AgentsUiInterruptResponseSchema, AgentsUiConversationSnapshotEventSchema, AgentsUiConversationMessageDeltaEventSchema, AgentsUiConversationErrorEventSchema, AgentsUiConversationEventSchema, WorktreeListResponseSchema, UnpushedCommitSchema, WorktreeDiffResponseSchema, ServiceConfigSchema, ProfileConfigSchema, LinkedRepoInfoSchema, AppConfigSchema, CiLogsResponseSchema, WorktreeNameParamsSchema, NotificationIdParamsSchema, AgentIdParamsSchema, RunIdParamsSchema, InstanceSummarySchema, InstancesResponseSchema;
|
|
7935
|
+
var BooleanLikeSchema, ErrorResponseSchema, OkResponseSchema, EnabledResponseSchema, BuiltInAgentIdSchema, AgentIdSchema, WorktreeCreateModeSchema, LinearIssueIdSchema, LinearTeamKeySchema, PostWorktreeToLinearTargetSchema, PostWorktreeToLinearRequestSchema, PostWorktreeToLinearResponseSchema, FromLinearInputSchema, OneshotConfigSchema, AgentCapabilitiesSchema, AgentSummarySchema, AgentDetailsSchema, AgentListResponseSchema, UpsertCustomAgentRequestSchema, AgentResponseSchema, ValidateCustomAgentResponseSchema, WorktreeCreationPhaseSchema, AvailableBranchSchema, AvailableBranchesQuerySchema, NumberLikePathParamSchema, BranchListResponseSchema, WorktreeSourceSchema, CreateWorktreeRequestSchema, OpenWorktreeRequestSchema, CreateWorktreeResponseSchema, SetWorktreeArchivedRequestSchema, SetWorktreeArchivedResponseSchema, SetWorktreeLabelRequestSchema, SetWorktreeLabelResponseSchema, ToggleEnabledRequestSchema, SendWorktreePromptRequestSchema, AgentsSendMessageRequestSchema, PullMainRequestSchema, PullMainStatusSchema, PullMainResponseSchema, ServiceStatusSchema, PrCommentSchema, CiCheckSchema, PrEntrySchema, LinearIssueLabelSchema, LinearIssueStateSchema, LinkedLinearIssueSchema, LinearIssueSchema, LinearIssueAvailabilitySchema, LinearIssuesResponseSchema, AutoNameProviderSchema, AutoNameConfigResponseSchema, WorktreeCreationStateSchema, AppNotificationSchema, ProjectWorktreeSnapshotSchema, ProjectSnapshotSchema, WorktreeConversationProviderSchema, CodexWorktreeConversationRefSchema, ClaudeWorktreeConversationRefSchema, WorktreeConversationRefSchema, AgentsUiWorktreeSummarySchema, AgentsUiConversationMessageRoleSchema, AgentsUiConversationMessageStatusSchema, AgentsUiConversationMessageKindSchema, AgentsUiConversationMessageSchema, AgentsUiConversationStateSchema, AgentsUiWorktreeConversationResponseSchema, AgentsUiSendMessageResponseSchema, AgentsUiInterruptResponseSchema, AgentsUiConversationSnapshotEventSchema, AgentsUiConversationMessageDeltaEventSchema, AgentsUiConversationErrorEventSchema, AgentsUiConversationEventSchema, WorktreeListResponseSchema, UnpushedCommitSchema, WorktreeDiffResponseSchema, ServiceConfigSchema, ProfileConfigSchema, LinkedRepoInfoSchema, AppConfigSchema, CiLogsResponseSchema, WorktreeNameParamsSchema, NotificationIdParamsSchema, AgentIdParamsSchema, RunIdParamsSchema, InstanceSummarySchema, InstancesResponseSchema;
|
|
7345
7936
|
var init_schemas = __esm(() => {
|
|
7346
7937
|
init_zod();
|
|
7347
7938
|
BooleanLikeSchema = exports_external.union([
|
|
@@ -7577,6 +8168,15 @@ var init_schemas = __esm(() => {
|
|
|
7577
8168
|
availability: LinearIssueAvailabilitySchema,
|
|
7578
8169
|
issues: exports_external.array(LinearIssueSchema)
|
|
7579
8170
|
});
|
|
8171
|
+
AutoNameProviderSchema = exports_external.enum(["claude", "codex"]);
|
|
8172
|
+
AutoNameConfigResponseSchema = exports_external.object({
|
|
8173
|
+
autoName: exports_external.object({
|
|
8174
|
+
provider: AutoNameProviderSchema,
|
|
8175
|
+
model: exports_external.string().optional(),
|
|
8176
|
+
systemPrompt: exports_external.string().optional()
|
|
8177
|
+
}).nullable(),
|
|
8178
|
+
linearAvailability: LinearIssueAvailabilitySchema
|
|
8179
|
+
});
|
|
7580
8180
|
WorktreeCreationStateSchema = exports_external.object({
|
|
7581
8181
|
phase: WorktreeCreationPhaseSchema
|
|
7582
8182
|
});
|
|
@@ -7598,6 +8198,7 @@ var init_schemas = __esm(() => {
|
|
|
7598
8198
|
profile: exports_external.string().nullable(),
|
|
7599
8199
|
agentName: AgentIdSchema.nullable(),
|
|
7600
8200
|
agentLabel: exports_external.string().nullable(),
|
|
8201
|
+
agentTerminalStale: exports_external.boolean(),
|
|
7601
8202
|
mux: exports_external.boolean(),
|
|
7602
8203
|
dirty: exports_external.boolean(),
|
|
7603
8204
|
unpushed: exports_external.boolean(),
|
|
@@ -7646,6 +8247,7 @@ var init_schemas = __esm(() => {
|
|
|
7646
8247
|
profile: exports_external.string().nullable(),
|
|
7647
8248
|
agentName: AgentIdSchema.nullable(),
|
|
7648
8249
|
agentLabel: exports_external.string().nullable(),
|
|
8250
|
+
agentTerminalStale: exports_external.boolean(),
|
|
7649
8251
|
mux: exports_external.boolean(),
|
|
7650
8252
|
status: exports_external.string(),
|
|
7651
8253
|
dirty: exports_external.boolean(),
|
|
@@ -7804,6 +8406,7 @@ var init_contract = __esm(() => {
|
|
|
7804
8406
|
removeWorktree: "/api/worktrees/:name",
|
|
7805
8407
|
openWorktree: "/api/worktrees/:name/open",
|
|
7806
8408
|
closeWorktree: "/api/worktrees/:name/close",
|
|
8409
|
+
refreshWorktreeAgentTerminal: "/api/worktrees/:name/agent-terminal/refresh",
|
|
7807
8410
|
setWorktreeArchived: "/api/worktrees/:name/archive",
|
|
7808
8411
|
syncWorktreePrs: "/api/worktrees/:name/sync-prs",
|
|
7809
8412
|
postWorktreeToLinear: "/api/worktrees/:name/linear/post",
|
|
@@ -7812,6 +8415,7 @@ var init_contract = __esm(() => {
|
|
|
7812
8415
|
mergeWorktree: "/api/worktrees/:name/merge",
|
|
7813
8416
|
fetchWorktreeDiff: "/api/worktrees/:name/diff",
|
|
7814
8417
|
fetchLinearIssues: "/api/linear/issues",
|
|
8418
|
+
fetchAutoNameConfig: "/api/project/auto-name",
|
|
7815
8419
|
setLinearAutoCreate: "/api/linear/auto-create",
|
|
7816
8420
|
setAutoRemoveOnMerge: "/api/github/auto-remove-on-merge",
|
|
7817
8421
|
pullMain: "/api/pull-main",
|
|
@@ -8002,6 +8606,16 @@ var init_contract = __esm(() => {
|
|
|
8002
8606
|
...commonErrorResponses
|
|
8003
8607
|
}
|
|
8004
8608
|
},
|
|
8609
|
+
refreshWorktreeAgentTerminal: {
|
|
8610
|
+
method: "POST",
|
|
8611
|
+
path: apiPaths.refreshWorktreeAgentTerminal,
|
|
8612
|
+
pathParams: WorktreeNameParamsSchema,
|
|
8613
|
+
body: c.noBody(),
|
|
8614
|
+
responses: {
|
|
8615
|
+
200: OkResponseSchema,
|
|
8616
|
+
...commonErrorResponses
|
|
8617
|
+
}
|
|
8618
|
+
},
|
|
8005
8619
|
setWorktreeArchived: {
|
|
8006
8620
|
method: "PUT",
|
|
8007
8621
|
path: apiPaths.setWorktreeArchived,
|
|
@@ -8080,6 +8694,14 @@ var init_contract = __esm(() => {
|
|
|
8080
8694
|
502: ErrorResponseSchema
|
|
8081
8695
|
}
|
|
8082
8696
|
},
|
|
8697
|
+
fetchAutoNameConfig: {
|
|
8698
|
+
method: "GET",
|
|
8699
|
+
path: apiPaths.fetchAutoNameConfig,
|
|
8700
|
+
responses: {
|
|
8701
|
+
200: AutoNameConfigResponseSchema,
|
|
8702
|
+
500: ErrorResponseSchema
|
|
8703
|
+
}
|
|
8704
|
+
},
|
|
8083
8705
|
setLinearAutoCreate: {
|
|
8084
8706
|
method: "PUT",
|
|
8085
8707
|
path: apiPaths.setLinearAutoCreate,
|
|
@@ -8221,30 +8843,6 @@ var init_src = __esm(() => {
|
|
|
8221
8843
|
init_schemas();
|
|
8222
8844
|
});
|
|
8223
8845
|
|
|
8224
|
-
// backend/src/lib/log.ts
|
|
8225
|
-
function ts() {
|
|
8226
|
-
return new Date().toISOString().slice(11, 23);
|
|
8227
|
-
}
|
|
8228
|
-
var DEBUG, log;
|
|
8229
|
-
var init_log = __esm(() => {
|
|
8230
|
-
DEBUG = Bun.env.WEBMUX_DEBUG === "1";
|
|
8231
|
-
log = {
|
|
8232
|
-
info(msg) {
|
|
8233
|
-
console.log(`[${ts()}] ${msg}`);
|
|
8234
|
-
},
|
|
8235
|
-
debug(msg) {
|
|
8236
|
-
if (DEBUG)
|
|
8237
|
-
console.log(`[${ts()}] ${msg}`);
|
|
8238
|
-
},
|
|
8239
|
-
warn(msg) {
|
|
8240
|
-
console.warn(`[${ts()}] ${msg}`);
|
|
8241
|
-
},
|
|
8242
|
-
error(msg, err) {
|
|
8243
|
-
err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
|
|
8244
|
-
}
|
|
8245
|
-
};
|
|
8246
|
-
});
|
|
8247
|
-
|
|
8248
8846
|
// backend/src/services/linear-service.ts
|
|
8249
8847
|
function gqlErrorMessage(raw) {
|
|
8250
8848
|
return raw.errors && raw.errors.length > 0 ? raw.errors.map((error) => error.message).join("; ") : null;
|
|
@@ -8367,6 +8965,39 @@ async function fetchInProgressStateId(teamId) {
|
|
|
8367
8965
|
inProgressStateIdCache.set(teamId, result.data);
|
|
8368
8966
|
return result;
|
|
8369
8967
|
}
|
|
8968
|
+
async function searchTeamIssuesByKeywords(input) {
|
|
8969
|
+
const keywords = input.keywords.map((k2) => k2.trim()).filter((k2) => k2.length > 0);
|
|
8970
|
+
if (keywords.length === 0) {
|
|
8971
|
+
return { ok: true, data: [] };
|
|
8972
|
+
}
|
|
8973
|
+
const titleFilters = keywords.map((keyword) => ({ title: { containsIgnoreCase: keyword } }));
|
|
8974
|
+
const response = await postLinearGraphql(TEAM_ISSUES_BY_KEYWORDS_QUERY, { teamId: input.teamId, titleFilters, first: input.limit ?? 10 });
|
|
8975
|
+
if (!response.ok)
|
|
8976
|
+
return { ok: false, error: response.error };
|
|
8977
|
+
const error = gqlErrorMessage(response.data);
|
|
8978
|
+
if (error)
|
|
8979
|
+
return { ok: false, error };
|
|
8980
|
+
const nodes = response.data.data?.issues.nodes ?? [];
|
|
8981
|
+
return {
|
|
8982
|
+
ok: true,
|
|
8983
|
+
data: nodes.map((node) => ({
|
|
8984
|
+
id: node.id,
|
|
8985
|
+
identifier: node.identifier,
|
|
8986
|
+
title: node.title,
|
|
8987
|
+
description: node.description,
|
|
8988
|
+
priority: node.priority,
|
|
8989
|
+
priorityLabel: node.priorityLabel,
|
|
8990
|
+
url: node.url,
|
|
8991
|
+
branchName: node.branchName,
|
|
8992
|
+
dueDate: node.dueDate,
|
|
8993
|
+
updatedAt: node.updatedAt,
|
|
8994
|
+
state: node.state,
|
|
8995
|
+
team: node.team,
|
|
8996
|
+
labels: node.labels.nodes,
|
|
8997
|
+
project: node.project?.name ?? null
|
|
8998
|
+
}))
|
|
8999
|
+
};
|
|
9000
|
+
}
|
|
8370
9001
|
function buildWebmuxAttachmentTitle(branch) {
|
|
8371
9002
|
return `${WEBMUX_ATTACHMENT_TITLE_PREFIX}${branch}`;
|
|
8372
9003
|
}
|
|
@@ -8567,6 +9198,35 @@ var VIEWER_QUERY = `
|
|
|
8567
9198
|
}
|
|
8568
9199
|
}
|
|
8569
9200
|
}
|
|
9201
|
+
`, TEAM_ISSUES_BY_KEYWORDS_QUERY = `
|
|
9202
|
+
query TeamIssuesByKeywords($teamId: ID!, $titleFilters: [IssueFilter!]!, $first: Int!) {
|
|
9203
|
+
issues(
|
|
9204
|
+
filter: {
|
|
9205
|
+
team: { id: { eq: $teamId } }
|
|
9206
|
+
state: { type: { in: ["triage", "backlog", "unstarted", "started"] } }
|
|
9207
|
+
or: $titleFilters
|
|
9208
|
+
}
|
|
9209
|
+
orderBy: updatedAt
|
|
9210
|
+
first: $first
|
|
9211
|
+
) {
|
|
9212
|
+
nodes {
|
|
9213
|
+
id
|
|
9214
|
+
identifier
|
|
9215
|
+
title
|
|
9216
|
+
description
|
|
9217
|
+
priority
|
|
9218
|
+
priorityLabel
|
|
9219
|
+
url
|
|
9220
|
+
branchName
|
|
9221
|
+
dueDate
|
|
9222
|
+
updatedAt
|
|
9223
|
+
state { name color type }
|
|
9224
|
+
team { name key }
|
|
9225
|
+
labels { nodes { name color } }
|
|
9226
|
+
project { name }
|
|
9227
|
+
}
|
|
9228
|
+
}
|
|
9229
|
+
}
|
|
8570
9230
|
`, WEBMUX_ATTACHMENT_TITLE_PREFIX = "webmux-state:", STATE_PRIORITY;
|
|
8571
9231
|
var init_linear_service = __esm(() => {
|
|
8572
9232
|
init_log();
|
|
@@ -8632,61 +9292,407 @@ async function buildSeedFromLinear(input, deps2) {
|
|
|
8632
9292
|
log.error(`[linear] webmux attachment download failed: ${payloadResult.error}`);
|
|
8633
9293
|
}
|
|
8634
9294
|
}
|
|
8635
|
-
const source = attachmentPayload ? "webmux-attachment" : pr ? "github-integration" : "none";
|
|
8636
|
-
const branch = attachmentPayload?.branch ?? pr?.branch ?? (issue.data.branchName || null);
|
|
8637
|
-
const baseBranch = attachmentPayload?.baseBranch ?? null;
|
|
8638
|
-
const conversationMarkdown = attachmentPayload ? `${issueHeader}${buildPriorConversationSection(attachmentPayload)}` : issueHeader;
|
|
8639
|
-
return {
|
|
8640
|
-
ok: true,
|
|
8641
|
-
data: {
|
|
8642
|
-
source,
|
|
8643
|
-
branch,
|
|
8644
|
-
baseBranch,
|
|
8645
|
-
prUrl: pr?.url ?? null,
|
|
8646
|
-
conversationMarkdown
|
|
8647
|
-
}
|
|
8648
|
-
};
|
|
9295
|
+
const source = attachmentPayload ? "webmux-attachment" : pr ? "github-integration" : "none";
|
|
9296
|
+
const branch = attachmentPayload?.branch ?? pr?.branch ?? (issue.data.branchName || null);
|
|
9297
|
+
const baseBranch = attachmentPayload?.baseBranch ?? null;
|
|
9298
|
+
const conversationMarkdown = attachmentPayload ? `${issueHeader}${buildPriorConversationSection(attachmentPayload)}` : issueHeader;
|
|
9299
|
+
return {
|
|
9300
|
+
ok: true,
|
|
9301
|
+
data: {
|
|
9302
|
+
source,
|
|
9303
|
+
branch,
|
|
9304
|
+
baseBranch,
|
|
9305
|
+
prUrl: pr?.url ?? null,
|
|
9306
|
+
conversationMarkdown
|
|
9307
|
+
}
|
|
9308
|
+
};
|
|
9309
|
+
}
|
|
9310
|
+
async function downloadWebmuxAttachmentDefault(url) {
|
|
9311
|
+
const apiKey = Bun.env.LINEAR_API_KEY;
|
|
9312
|
+
if (!apiKey)
|
|
9313
|
+
return { ok: false, error: "LINEAR_API_KEY not set" };
|
|
9314
|
+
try {
|
|
9315
|
+
const res = await fetch(url, {
|
|
9316
|
+
headers: { Authorization: apiKey }
|
|
9317
|
+
});
|
|
9318
|
+
if (!res.ok) {
|
|
9319
|
+
return { ok: false, error: `Asset download failed ${res.status}` };
|
|
9320
|
+
}
|
|
9321
|
+
const text = await res.text();
|
|
9322
|
+
const parsed = WebmuxConversationAttachmentPayloadSchema.safeParse(JSON.parse(text));
|
|
9323
|
+
if (!parsed.success) {
|
|
9324
|
+
return { ok: false, error: "Asset is not a webmux conversation payload" };
|
|
9325
|
+
}
|
|
9326
|
+
return { ok: true, data: parsed.data };
|
|
9327
|
+
} catch (err) {
|
|
9328
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9329
|
+
return { ok: false, error: msg };
|
|
9330
|
+
}
|
|
9331
|
+
}
|
|
9332
|
+
var WebmuxConversationAttachmentPayloadSchema, defaultSeedFromLinearDeps;
|
|
9333
|
+
var init_conversation_export_service = __esm(() => {
|
|
9334
|
+
init_src();
|
|
9335
|
+
init_zod();
|
|
9336
|
+
init_log();
|
|
9337
|
+
init_linear_service();
|
|
9338
|
+
WebmuxConversationAttachmentPayloadSchema = exports_external.object({
|
|
9339
|
+
webmux: exports_external.literal(1),
|
|
9340
|
+
branch: exports_external.string(),
|
|
9341
|
+
baseBranch: exports_external.string().nullable(),
|
|
9342
|
+
agent: AgentIdSchema.nullable(),
|
|
9343
|
+
createdAt: exports_external.string(),
|
|
9344
|
+
conversation: exports_external.array(AgentsUiConversationMessageSchema)
|
|
9345
|
+
});
|
|
9346
|
+
defaultSeedFromLinearDeps = {
|
|
9347
|
+
fetchIssueWithAttachments,
|
|
9348
|
+
downloadWebmuxAttachment: downloadWebmuxAttachmentDefault
|
|
9349
|
+
};
|
|
9350
|
+
});
|
|
9351
|
+
|
|
9352
|
+
// backend/src/services/llm-spawn.ts
|
|
9353
|
+
async function defaultLlmSpawn(args, options = {}) {
|
|
9354
|
+
const proc = Bun.spawn(args, {
|
|
9355
|
+
stdout: "pipe",
|
|
9356
|
+
stderr: "pipe"
|
|
9357
|
+
});
|
|
9358
|
+
const resultPromise = Promise.all([
|
|
9359
|
+
new Response(proc.stdout).text(),
|
|
9360
|
+
new Response(proc.stderr).text(),
|
|
9361
|
+
proc.exited
|
|
9362
|
+
]).then(([stdout, stderr, exitCode]) => ({ exitCode, stdout, stderr }));
|
|
9363
|
+
const timeoutMs = options.timeoutMs;
|
|
9364
|
+
if (timeoutMs === undefined) {
|
|
9365
|
+
return await resultPromise;
|
|
9366
|
+
}
|
|
9367
|
+
return await new Promise((resolve3, reject) => {
|
|
9368
|
+
let settled = false;
|
|
9369
|
+
const timeoutId = setTimeout(() => {
|
|
9370
|
+
if (settled)
|
|
9371
|
+
return;
|
|
9372
|
+
settled = true;
|
|
9373
|
+
try {
|
|
9374
|
+
proc.kill("SIGKILL");
|
|
9375
|
+
} catch {}
|
|
9376
|
+
reject(new LlmSpawnTimeoutError(timeoutMs));
|
|
9377
|
+
}, timeoutMs);
|
|
9378
|
+
resultPromise.then((result) => {
|
|
9379
|
+
if (settled)
|
|
9380
|
+
return;
|
|
9381
|
+
settled = true;
|
|
9382
|
+
clearTimeout(timeoutId);
|
|
9383
|
+
resolve3(result);
|
|
9384
|
+
}, (error) => {
|
|
9385
|
+
if (settled)
|
|
9386
|
+
return;
|
|
9387
|
+
settled = true;
|
|
9388
|
+
clearTimeout(timeoutId);
|
|
9389
|
+
reject(error);
|
|
9390
|
+
});
|
|
9391
|
+
});
|
|
9392
|
+
}
|
|
9393
|
+
function escapeTomlString(s) {
|
|
9394
|
+
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
|
|
9395
|
+
}
|
|
9396
|
+
function buildLlmArgs(config, systemPrompt, userPrompt) {
|
|
9397
|
+
if (config.provider === "claude") {
|
|
9398
|
+
return [
|
|
9399
|
+
"claude",
|
|
9400
|
+
"-p",
|
|
9401
|
+
"--system-prompt",
|
|
9402
|
+
systemPrompt,
|
|
9403
|
+
"--output-format",
|
|
9404
|
+
"text",
|
|
9405
|
+
"--no-session-persistence",
|
|
9406
|
+
"--model",
|
|
9407
|
+
config.model || DEFAULT_CLAUDE_MODEL,
|
|
9408
|
+
"--effort",
|
|
9409
|
+
"low",
|
|
9410
|
+
userPrompt
|
|
9411
|
+
];
|
|
9412
|
+
}
|
|
9413
|
+
const args = [
|
|
9414
|
+
"codex",
|
|
9415
|
+
"-c",
|
|
9416
|
+
`developer_instructions="${escapeTomlString(systemPrompt)}"`,
|
|
9417
|
+
"exec",
|
|
9418
|
+
"--ephemeral"
|
|
9419
|
+
];
|
|
9420
|
+
if (config.model) {
|
|
9421
|
+
args.push("-m", config.model);
|
|
9422
|
+
}
|
|
9423
|
+
args.push(userPrompt);
|
|
9424
|
+
return args;
|
|
9425
|
+
}
|
|
9426
|
+
async function runShortLlmTask(config, systemPrompt, userPrompt, options = {}) {
|
|
9427
|
+
const args = buildLlmArgs(config, systemPrompt, userPrompt);
|
|
9428
|
+
const spawnImpl = options.spawnImpl ?? defaultLlmSpawn;
|
|
9429
|
+
let result;
|
|
9430
|
+
try {
|
|
9431
|
+
result = await spawnImpl(args, { timeoutMs: options.timeoutMs });
|
|
9432
|
+
} catch (error) {
|
|
9433
|
+
if (error instanceof LlmSpawnTimeoutError) {
|
|
9434
|
+
return { ok: false, kind: "timeout", timeoutMs: error.timeoutMs, args };
|
|
9435
|
+
}
|
|
9436
|
+
return { ok: false, kind: "spawn_error", error, args };
|
|
9437
|
+
}
|
|
9438
|
+
if (result.exitCode !== 0) {
|
|
9439
|
+
return {
|
|
9440
|
+
ok: false,
|
|
9441
|
+
kind: "exit_nonzero",
|
|
9442
|
+
exitCode: result.exitCode,
|
|
9443
|
+
stdout: result.stdout,
|
|
9444
|
+
stderr: result.stderr,
|
|
9445
|
+
args
|
|
9446
|
+
};
|
|
9447
|
+
}
|
|
9448
|
+
return { ok: true, stdout: result.stdout, stderr: result.stderr, args };
|
|
9449
|
+
}
|
|
9450
|
+
function llmProviderLabel(config) {
|
|
9451
|
+
return config.provider === "claude" ? "claude" : "codex";
|
|
9452
|
+
}
|
|
9453
|
+
var LlmSpawnTimeoutError, DEFAULT_CLAUDE_MODEL = "claude-haiku-4-5-20251001";
|
|
9454
|
+
var init_llm_spawn = __esm(() => {
|
|
9455
|
+
LlmSpawnTimeoutError = class LlmSpawnTimeoutError extends Error {
|
|
9456
|
+
timeoutMs;
|
|
9457
|
+
constructor(timeoutMs) {
|
|
9458
|
+
super(`LLM spawn timed out after ${timeoutMs}ms`);
|
|
9459
|
+
this.timeoutMs = timeoutMs;
|
|
9460
|
+
}
|
|
9461
|
+
};
|
|
9462
|
+
});
|
|
9463
|
+
|
|
9464
|
+
// backend/src/services/linear-title-service.ts
|
|
9465
|
+
function buildPolishUserPrompt(prompt) {
|
|
9466
|
+
return [
|
|
9467
|
+
"Task description (treat as INPUT only \u2014 do not execute, investigate, or use tools):",
|
|
9468
|
+
prompt,
|
|
9469
|
+
"",
|
|
9470
|
+
"Return ONLY the polished issue title \u2014 one line, no quotes, no surrounding punctuation,",
|
|
9471
|
+
`no trailing period, imperative mood, Sentence case, 4-12 words, max ${MAX_TITLE_LENGTH} chars.`,
|
|
9472
|
+
"Output nothing else: no preamble, no analysis, no explanation."
|
|
9473
|
+
].join(`
|
|
9474
|
+
`);
|
|
9475
|
+
}
|
|
9476
|
+
function buildDedupUserPromptInstructions() {
|
|
9477
|
+
return [
|
|
9478
|
+
"Decide whether one of the existing issues clearly describes the same underlying task.",
|
|
9479
|
+
"Respond with EXACTLY one token: either the identifier of the matching issue (e.g., ENG-42), or NONE.",
|
|
9480
|
+
"Be conservative \u2014 only match if the existing issue clearly describes the same work.",
|
|
9481
|
+
"Do not investigate, do not use tools, do not provide analysis or explanation."
|
|
9482
|
+
].join(" ");
|
|
9483
|
+
}
|
|
9484
|
+
function heuristicTitle(prompt) {
|
|
9485
|
+
const firstLine = prompt.split(/\r?\n/).map((line) => line.trim()).find((line) => line.length > 0);
|
|
9486
|
+
if (!firstLine)
|
|
9487
|
+
return null;
|
|
9488
|
+
if (firstLine.length <= MAX_TITLE_LENGTH)
|
|
9489
|
+
return firstLine;
|
|
9490
|
+
return `${firstLine.slice(0, MAX_TITLE_LENGTH - 1).trimEnd()}\u2026`;
|
|
9491
|
+
}
|
|
9492
|
+
function normalizePolishedTitle(raw) {
|
|
9493
|
+
let title = raw.trim();
|
|
9494
|
+
title = title.replace(/^```[\w-]*\s*/, "").replace(/\s*```$/, "");
|
|
9495
|
+
title = title.split(/\r?\n/)[0]?.trim() ?? "";
|
|
9496
|
+
title = title.replace(/^title\s*:\s*/i, "");
|
|
9497
|
+
title = title.replace(/^["'`]+|["'`]+$/g, "");
|
|
9498
|
+
title = title.replace(/[.!?,;:]+$/, "");
|
|
9499
|
+
title = title.replace(/\s+/g, " ").trim();
|
|
9500
|
+
if (!title)
|
|
9501
|
+
return null;
|
|
9502
|
+
if (title.length > MAX_TITLE_LENGTH) {
|
|
9503
|
+
title = title.slice(0, MAX_TITLE_LENGTH).trimEnd();
|
|
9504
|
+
}
|
|
9505
|
+
return title;
|
|
9506
|
+
}
|
|
9507
|
+
async function polishLinearIssueTitle(input) {
|
|
9508
|
+
const heuristic = heuristicTitle(input.prompt);
|
|
9509
|
+
if (!input.autoName) {
|
|
9510
|
+
return heuristic ? { title: heuristic, source: "heuristic_no_config" } : null;
|
|
9511
|
+
}
|
|
9512
|
+
if (!heuristic)
|
|
9513
|
+
return null;
|
|
9514
|
+
const runLlm = input.runLlm ?? runShortLlmTask;
|
|
9515
|
+
let result;
|
|
9516
|
+
try {
|
|
9517
|
+
result = await runLlm(input.autoName, POLISH_SYSTEM_PROMPT, buildPolishUserPrompt(input.prompt.trim()), { timeoutMs: TITLE_TIMEOUT_MS });
|
|
9518
|
+
} catch (err) {
|
|
9519
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9520
|
+
log.warn(`[linear-title] polish call threw: ${msg}; falling back to heuristic`);
|
|
9521
|
+
return { title: heuristic, source: "heuristic_fallback" };
|
|
9522
|
+
}
|
|
9523
|
+
const cli = llmProviderLabel(input.autoName);
|
|
9524
|
+
if (!result.ok) {
|
|
9525
|
+
if (result.kind === "timeout") {
|
|
9526
|
+
log.warn(`[linear-title] ${cli} polish timed out after ${result.timeoutMs}ms; using heuristic`);
|
|
9527
|
+
} else if (result.kind === "spawn_error") {
|
|
9528
|
+
log.warn(`[linear-title] ${cli} not on PATH; using heuristic title`);
|
|
9529
|
+
} else {
|
|
9530
|
+
const stderr = result.stderr.trim() || `exit ${result.exitCode}`;
|
|
9531
|
+
log.warn(`[linear-title] ${cli} polish failed: ${stderr}; using heuristic`);
|
|
9532
|
+
}
|
|
9533
|
+
return { title: heuristic, source: "heuristic_fallback" };
|
|
9534
|
+
}
|
|
9535
|
+
const normalized = normalizePolishedTitle(result.stdout);
|
|
9536
|
+
if (!normalized) {
|
|
9537
|
+
log.warn(`[linear-title] ${cli} returned empty/unusable title; using heuristic`);
|
|
9538
|
+
return { title: heuristic, source: "heuristic_fallback" };
|
|
9539
|
+
}
|
|
9540
|
+
return { title: normalized, source: "llm" };
|
|
8649
9541
|
}
|
|
8650
|
-
|
|
8651
|
-
const
|
|
8652
|
-
|
|
8653
|
-
|
|
9542
|
+
function extractKeywords(title, max = 4) {
|
|
9543
|
+
const tokens = title.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2 && !STOPWORDS.has(t));
|
|
9544
|
+
const seen = new Set;
|
|
9545
|
+
const out = [];
|
|
9546
|
+
for (const token of tokens) {
|
|
9547
|
+
if (seen.has(token))
|
|
9548
|
+
continue;
|
|
9549
|
+
seen.add(token);
|
|
9550
|
+
out.push(token);
|
|
9551
|
+
if (out.length >= max)
|
|
9552
|
+
break;
|
|
9553
|
+
}
|
|
9554
|
+
return out;
|
|
9555
|
+
}
|
|
9556
|
+
async function findDuplicateLinearIssue(input) {
|
|
9557
|
+
const keywords = extractKeywords(input.polishedTitle);
|
|
9558
|
+
if (keywords.length === 0)
|
|
9559
|
+
return null;
|
|
9560
|
+
const search = input.search ?? searchTeamIssuesByKeywords;
|
|
9561
|
+
const searchResult = await search({
|
|
9562
|
+
teamId: input.teamId,
|
|
9563
|
+
keywords,
|
|
9564
|
+
limit: MAX_DEDUP_CANDIDATES
|
|
9565
|
+
});
|
|
9566
|
+
if (!searchResult.ok) {
|
|
9567
|
+
log.warn(`[linear-title] dedup search failed: ${searchResult.error}`);
|
|
9568
|
+
return null;
|
|
9569
|
+
}
|
|
9570
|
+
const candidates = searchResult.data;
|
|
9571
|
+
if (candidates.length === 0)
|
|
9572
|
+
return null;
|
|
9573
|
+
const userPrompt = buildDedupUserPrompt({
|
|
9574
|
+
polishedTitle: input.polishedTitle,
|
|
9575
|
+
prompt: input.prompt,
|
|
9576
|
+
candidates
|
|
9577
|
+
});
|
|
9578
|
+
const runLlm = input.runLlm ?? runShortLlmTask;
|
|
9579
|
+
let result;
|
|
8654
9580
|
try {
|
|
8655
|
-
|
|
8656
|
-
headers: { Authorization: apiKey }
|
|
8657
|
-
});
|
|
8658
|
-
if (!res.ok) {
|
|
8659
|
-
return { ok: false, error: `Asset download failed ${res.status}` };
|
|
8660
|
-
}
|
|
8661
|
-
const text = await res.text();
|
|
8662
|
-
const parsed = WebmuxConversationAttachmentPayloadSchema.safeParse(JSON.parse(text));
|
|
8663
|
-
if (!parsed.success) {
|
|
8664
|
-
return { ok: false, error: "Asset is not a webmux conversation payload" };
|
|
8665
|
-
}
|
|
8666
|
-
return { ok: true, data: parsed.data };
|
|
9581
|
+
result = await runLlm(input.autoName, DEDUP_SYSTEM_PROMPT, userPrompt, { timeoutMs: DEDUP_TIMEOUT_MS });
|
|
8667
9582
|
} catch (err) {
|
|
8668
9583
|
const msg = err instanceof Error ? err.message : String(err);
|
|
8669
|
-
|
|
9584
|
+
log.warn(`[linear-title] dedup call threw: ${msg}`);
|
|
9585
|
+
return null;
|
|
9586
|
+
}
|
|
9587
|
+
if (!result.ok) {
|
|
9588
|
+
const cli = llmProviderLabel(input.autoName);
|
|
9589
|
+
if (result.kind === "timeout") {
|
|
9590
|
+
log.warn(`[linear-title] ${cli} dedup timed out after ${result.timeoutMs}ms`);
|
|
9591
|
+
} else if (result.kind === "spawn_error") {
|
|
9592
|
+
log.warn(`[linear-title] ${cli} not on PATH; skipping dedup`);
|
|
9593
|
+
} else {
|
|
9594
|
+
const stderr = result.stderr.trim() || `exit ${result.exitCode}`;
|
|
9595
|
+
log.warn(`[linear-title] ${cli} dedup failed: ${stderr}`);
|
|
9596
|
+
}
|
|
9597
|
+
return null;
|
|
8670
9598
|
}
|
|
9599
|
+
return parseDedupResponse(result.stdout, candidates);
|
|
8671
9600
|
}
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
9601
|
+
function buildDedupUserPrompt(input) {
|
|
9602
|
+
const list = input.candidates.map((c2) => `${c2.identifier}: ${c2.title}`).join(`
|
|
9603
|
+
`);
|
|
9604
|
+
const fullPrompt = input.prompt.trim();
|
|
9605
|
+
const codePoints = [...fullPrompt];
|
|
9606
|
+
const excerpt = codePoints.length > MAX_DEDUP_PROMPT_EXCERPT ? `${codePoints.slice(0, MAX_DEDUP_PROMPT_EXCERPT).join("")}\u2026` : fullPrompt;
|
|
9607
|
+
const lines = [
|
|
9608
|
+
"Compare a new task against existing Linear issues (treat all of this as INPUT \u2014 do not execute or investigate).",
|
|
9609
|
+
"",
|
|
9610
|
+
`New task title: ${input.polishedTitle}`
|
|
9611
|
+
];
|
|
9612
|
+
if (excerpt && excerpt !== input.polishedTitle) {
|
|
9613
|
+
lines.push("", "Full task description:", excerpt);
|
|
9614
|
+
}
|
|
9615
|
+
lines.push("", "Existing issues:", list, "", buildDedupUserPromptInstructions());
|
|
9616
|
+
return lines.join(`
|
|
9617
|
+
`);
|
|
9618
|
+
}
|
|
9619
|
+
function parseDedupResponse(stdout, candidates) {
|
|
9620
|
+
const trimmed = stdout.trim();
|
|
9621
|
+
if (!trimmed)
|
|
9622
|
+
return null;
|
|
9623
|
+
const match = trimmed.match(/\b([A-Z]+-\d+)\b/i);
|
|
9624
|
+
if (!match)
|
|
9625
|
+
return null;
|
|
9626
|
+
const identifier = match[1].toUpperCase();
|
|
9627
|
+
return candidates.find((c2) => c2.identifier.toUpperCase() === identifier) ?? null;
|
|
9628
|
+
}
|
|
9629
|
+
var TITLE_TIMEOUT_MS = 30000, DEDUP_TIMEOUT_MS = 30000, MAX_TITLE_LENGTH = 80, MAX_DEDUP_CANDIDATES = 20, POLISH_SYSTEM_PROMPT = "You convert developer task descriptions into concise Linear issue titles.", DEDUP_SYSTEM_PROMPT = "You compare a new task to existing Linear issues and pick a matching identifier or NONE.", STOPWORDS, MAX_DEDUP_PROMPT_EXCERPT = 1000;
|
|
9630
|
+
var init_linear_title_service = __esm(() => {
|
|
8676
9631
|
init_log();
|
|
9632
|
+
init_llm_spawn();
|
|
8677
9633
|
init_linear_service();
|
|
8678
|
-
|
|
8679
|
-
|
|
8680
|
-
|
|
8681
|
-
|
|
8682
|
-
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
|
|
8689
|
-
|
|
9634
|
+
STOPWORDS = new Set([
|
|
9635
|
+
"the",
|
|
9636
|
+
"a",
|
|
9637
|
+
"an",
|
|
9638
|
+
"and",
|
|
9639
|
+
"or",
|
|
9640
|
+
"but",
|
|
9641
|
+
"is",
|
|
9642
|
+
"are",
|
|
9643
|
+
"was",
|
|
9644
|
+
"were",
|
|
9645
|
+
"be",
|
|
9646
|
+
"been",
|
|
9647
|
+
"being",
|
|
9648
|
+
"to",
|
|
9649
|
+
"of",
|
|
9650
|
+
"in",
|
|
9651
|
+
"on",
|
|
9652
|
+
"at",
|
|
9653
|
+
"for",
|
|
9654
|
+
"with",
|
|
9655
|
+
"by",
|
|
9656
|
+
"from",
|
|
9657
|
+
"as",
|
|
9658
|
+
"into",
|
|
9659
|
+
"this",
|
|
9660
|
+
"that",
|
|
9661
|
+
"these",
|
|
9662
|
+
"those",
|
|
9663
|
+
"it",
|
|
9664
|
+
"its",
|
|
9665
|
+
"we",
|
|
9666
|
+
"our",
|
|
9667
|
+
"you",
|
|
9668
|
+
"your",
|
|
9669
|
+
"can",
|
|
9670
|
+
"should",
|
|
9671
|
+
"would",
|
|
9672
|
+
"could",
|
|
9673
|
+
"will",
|
|
9674
|
+
"do",
|
|
9675
|
+
"does",
|
|
9676
|
+
"did",
|
|
9677
|
+
"have",
|
|
9678
|
+
"has",
|
|
9679
|
+
"had",
|
|
9680
|
+
"not",
|
|
9681
|
+
"no",
|
|
9682
|
+
"if",
|
|
9683
|
+
"then",
|
|
9684
|
+
"than",
|
|
9685
|
+
"when",
|
|
9686
|
+
"where",
|
|
9687
|
+
"why",
|
|
9688
|
+
"how",
|
|
9689
|
+
"i",
|
|
9690
|
+
"me",
|
|
9691
|
+
"my",
|
|
9692
|
+
"us",
|
|
9693
|
+
"them",
|
|
9694
|
+
"their"
|
|
9695
|
+
]);
|
|
8690
9696
|
});
|
|
8691
9697
|
|
|
8692
9698
|
// bin/src/oneshot.ts
|
|
@@ -8724,7 +9730,9 @@ function getOneshotUsage() {
|
|
|
8724
9730
|
" --keep-open Don't auto-close the worktree session when the agent finishes",
|
|
8725
9731
|
" --linear ID|TEAM Tie this oneshot to Linear:",
|
|
8726
9732
|
" ENG-123 \u2014 load the issue body as context, post results back",
|
|
8727
|
-
" ENG \u2014 create a new issue in that team when done",
|
|
9733
|
+
" ENG \u2014 create a new issue in that team when done.",
|
|
9734
|
+
" When autoName is configured, the title is polished",
|
|
9735
|
+
" and likely duplicates are surfaced before creation.",
|
|
8728
9736
|
" --branch <name> Override the branch when --linear resolves to one",
|
|
8729
9737
|
" --help Show this help message"
|
|
8730
9738
|
].join(`
|
|
@@ -9176,13 +10184,38 @@ function pollConversationHistory(branch, port, state) {
|
|
|
9176
10184
|
}
|
|
9177
10185
|
};
|
|
9178
10186
|
}
|
|
9179
|
-
function
|
|
9180
|
-
if (!
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
10187
|
+
async function promptDuplicateChoice(candidate, polishedTitle) {
|
|
10188
|
+
if (!process.stdin.isTTY) {
|
|
10189
|
+
process.stderr.write(`[${timestamp()}] [warn] non-interactive shell; ignoring possible duplicate ${candidate.identifier}: "${candidate.title}" (${candidate.url})
|
|
10190
|
+
`);
|
|
10191
|
+
return "create_new";
|
|
10192
|
+
}
|
|
10193
|
+
Se(`${candidate.identifier}: ${candidate.title}
|
|
10194
|
+
${candidate.url}`, "Possible existing match");
|
|
10195
|
+
const choice = await xe({
|
|
10196
|
+
message: "Found a possible existing match. What should webmux do?",
|
|
10197
|
+
initialValue: "use_existing",
|
|
10198
|
+
options: [
|
|
10199
|
+
{
|
|
10200
|
+
value: "use_existing",
|
|
10201
|
+
label: `Use existing (${candidate.identifier})`,
|
|
10202
|
+
hint: "Treat this oneshot as resuming the existing issue"
|
|
10203
|
+
},
|
|
10204
|
+
{
|
|
10205
|
+
value: "create_new",
|
|
10206
|
+
label: "Create new issue",
|
|
10207
|
+
hint: `Title: "${polishedTitle}"`
|
|
10208
|
+
},
|
|
10209
|
+
{
|
|
10210
|
+
value: "cancel",
|
|
10211
|
+
label: "Cancel",
|
|
10212
|
+
hint: "Don't start the oneshot"
|
|
10213
|
+
}
|
|
10214
|
+
]
|
|
10215
|
+
});
|
|
10216
|
+
if (q(choice))
|
|
10217
|
+
return "cancel";
|
|
10218
|
+
return choice;
|
|
9186
10219
|
}
|
|
9187
10220
|
async function runOneshot(parsed, port) {
|
|
9188
10221
|
const stdout = (line) => {
|
|
@@ -9199,44 +10232,78 @@ async function runOneshot(parsed, port) {
|
|
|
9199
10232
|
let fromLinearIssueId = parsed.fromLinearIssueId;
|
|
9200
10233
|
let postToLinearTarget = parsed.postToLinearTarget;
|
|
9201
10234
|
try {
|
|
10235
|
+
let autoName = null;
|
|
9202
10236
|
if (postToLinearTarget) {
|
|
9203
|
-
const
|
|
9204
|
-
if (
|
|
10237
|
+
const projectAutoName = await api.fetchAutoNameConfig();
|
|
10238
|
+
if (projectAutoName.linearAvailability === "missing_api_key") {
|
|
9205
10239
|
stderr(`[${timestamp()}] [error] server has no LINEAR_API_KEY \u2014 the post-back to Linear at the end of the run will fail. Set the env var on the webmux server and restart it.`);
|
|
9206
10240
|
return 1;
|
|
9207
10241
|
}
|
|
9208
|
-
if (
|
|
10242
|
+
if (projectAutoName.linearAvailability === "disabled") {
|
|
9209
10243
|
stderr(`[${timestamp()}] [error] Linear integration is disabled on the webmux server.`);
|
|
9210
10244
|
return 1;
|
|
9211
10245
|
}
|
|
10246
|
+
autoName = projectAutoName.autoName;
|
|
9212
10247
|
}
|
|
9213
10248
|
if (postToLinearTarget?.kind === "team") {
|
|
9214
|
-
|
|
9215
|
-
if (!title) {
|
|
10249
|
+
if (!parsed.prompt) {
|
|
9216
10250
|
stderr(`[${timestamp()}] [error] --linear ${postToLinearTarget.teamKey} requires --prompt to derive an issue title`);
|
|
9217
10251
|
return 1;
|
|
9218
10252
|
}
|
|
10253
|
+
const polished = await polishLinearIssueTitle({ prompt: parsed.prompt, autoName });
|
|
10254
|
+
if (!polished) {
|
|
10255
|
+
stderr(`[${timestamp()}] [error] could not derive a title from --prompt`);
|
|
10256
|
+
return 1;
|
|
10257
|
+
}
|
|
10258
|
+
if (polished.source === "llm") {
|
|
10259
|
+
stdout(`[${timestamp()}] [event] polished title: "${polished.title}"`);
|
|
10260
|
+
}
|
|
9219
10261
|
if (parsed.resume) {
|
|
9220
10262
|
stdout(`[${timestamp()}] [event] no Linear issue for this resume; creating a fresh ${postToLinearTarget.teamKey}-N for the post-back`);
|
|
9221
10263
|
}
|
|
9222
|
-
stdout(`[${timestamp()}] [event] creating Linear issue in team ${postToLinearTarget.teamKey}...`);
|
|
9223
10264
|
const team = await fetchTeamByKey(postToLinearTarget.teamKey);
|
|
9224
10265
|
if (!team.ok) {
|
|
9225
10266
|
stderr(`[${timestamp()}] [error] Linear team lookup failed: ${team.error}`);
|
|
9226
10267
|
return 1;
|
|
9227
10268
|
}
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
|
|
9235
|
-
|
|
10269
|
+
let duplicate = null;
|
|
10270
|
+
if (autoName) {
|
|
10271
|
+
duplicate = await findDuplicateLinearIssue({
|
|
10272
|
+
polishedTitle: polished.title,
|
|
10273
|
+
prompt: parsed.prompt,
|
|
10274
|
+
teamId: team.data.id,
|
|
10275
|
+
autoName
|
|
10276
|
+
});
|
|
10277
|
+
}
|
|
10278
|
+
if (duplicate) {
|
|
10279
|
+
const choice = await promptDuplicateChoice(duplicate, polished.title);
|
|
10280
|
+
if (choice === "cancel") {
|
|
10281
|
+
stdout(`[${timestamp()}] [event] cancelled by user`);
|
|
10282
|
+
return 0;
|
|
10283
|
+
}
|
|
10284
|
+
if (choice === "use_existing") {
|
|
10285
|
+
stdout(`[${timestamp()}] [event] using existing Linear issue ${duplicate.identifier} \u2192 ${duplicate.url}`);
|
|
10286
|
+
fromLinearIssueId = duplicate.identifier;
|
|
10287
|
+
postToLinearTarget = { kind: "issue", issueId: duplicate.identifier };
|
|
10288
|
+
} else {
|
|
10289
|
+
stdout(`[${timestamp()}] [event] user chose to create a new issue despite candidate ${duplicate.identifier}`);
|
|
10290
|
+
}
|
|
10291
|
+
}
|
|
10292
|
+
if (postToLinearTarget.kind === "team") {
|
|
10293
|
+
stdout(`[${timestamp()}] [event] creating Linear issue in team ${postToLinearTarget.teamKey}...`);
|
|
10294
|
+
const created = await createLinearIssue({
|
|
10295
|
+
teamId: team.data.id,
|
|
10296
|
+
title: polished.title,
|
|
10297
|
+
description: ""
|
|
10298
|
+
});
|
|
10299
|
+
if (!created.ok) {
|
|
10300
|
+
stderr(`[${timestamp()}] [error] Linear issue creation failed: ${created.error}`);
|
|
10301
|
+
return 1;
|
|
10302
|
+
}
|
|
10303
|
+
stdout(`[${timestamp()}] [event] created Linear issue ${created.data.identifier} \u2192 ${created.data.url}`);
|
|
10304
|
+
fromLinearIssueId = created.data.identifier;
|
|
10305
|
+
postToLinearTarget = { kind: "issue", issueId: created.data.identifier };
|
|
9236
10306
|
}
|
|
9237
|
-
stdout(`[${timestamp()}] [event] created Linear issue ${created.data.identifier} \u2192 ${created.data.url}`);
|
|
9238
|
-
fromLinearIssueId = created.data.identifier;
|
|
9239
|
-
postToLinearTarget = { kind: "issue", issueId: created.data.identifier };
|
|
9240
10307
|
}
|
|
9241
10308
|
if (fromLinearIssueId) {
|
|
9242
10309
|
stdout(`[${timestamp()}] [event] resolving Linear issue ${fromLinearIssueId}...`);
|
|
@@ -9403,9 +10470,11 @@ async function runOneshotCommand(args, port) {
|
|
|
9403
10470
|
}
|
|
9404
10471
|
var TOOL_PRIMARY_KEY, MAX_CONSECUTIVE_RECONNECTS = 30, RECONNECT_WARN_AT, IDLE_GRACE_MS = 15000;
|
|
9405
10472
|
var init_oneshot = __esm(() => {
|
|
10473
|
+
init_dist4();
|
|
9406
10474
|
init_src();
|
|
9407
10475
|
init_linear_service();
|
|
9408
10476
|
init_conversation_export_service();
|
|
10477
|
+
init_linear_title_service();
|
|
9409
10478
|
init_shared();
|
|
9410
10479
|
TOOL_PRIMARY_KEY = {
|
|
9411
10480
|
bash: ["command"],
|
|
@@ -9562,7 +10631,7 @@ var WORKTREE_META_SCHEMA_VERSION = 1, WORKTREE_ARCHIVE_STATE_VERSION = 1;
|
|
|
9562
10631
|
|
|
9563
10632
|
// backend/src/adapters/fs.ts
|
|
9564
10633
|
import { mkdir } from "fs/promises";
|
|
9565
|
-
import { join as
|
|
10634
|
+
import { join as join9 } from "path";
|
|
9566
10635
|
function stringifyAllocatedPorts(ports) {
|
|
9567
10636
|
const entries = Object.entries(ports).map(([key, value]) => [key, String(value)]);
|
|
9568
10637
|
return Object.fromEntries(entries);
|
|
@@ -9594,25 +10663,25 @@ function parseDotenv(content) {
|
|
|
9594
10663
|
}
|
|
9595
10664
|
async function loadDotenvLocal(worktreePath) {
|
|
9596
10665
|
try {
|
|
9597
|
-
const content = await Bun.file(
|
|
10666
|
+
const content = await Bun.file(join9(worktreePath, ".env.local")).text();
|
|
9598
10667
|
return parseDotenv(content);
|
|
9599
10668
|
} catch {
|
|
9600
10669
|
return {};
|
|
9601
10670
|
}
|
|
9602
10671
|
}
|
|
9603
10672
|
function getWorktreeStoragePaths(gitDir) {
|
|
9604
|
-
const webmuxDir =
|
|
10673
|
+
const webmuxDir = join9(gitDir, "webmux");
|
|
9605
10674
|
return {
|
|
9606
10675
|
gitDir,
|
|
9607
10676
|
webmuxDir,
|
|
9608
|
-
metaPath:
|
|
9609
|
-
runtimeEnvPath:
|
|
9610
|
-
controlEnvPath:
|
|
9611
|
-
prsPath:
|
|
10677
|
+
metaPath: join9(webmuxDir, "meta.json"),
|
|
10678
|
+
runtimeEnvPath: join9(webmuxDir, "runtime.env"),
|
|
10679
|
+
controlEnvPath: join9(webmuxDir, "control.env"),
|
|
10680
|
+
prsPath: join9(webmuxDir, "prs.json")
|
|
9612
10681
|
};
|
|
9613
10682
|
}
|
|
9614
10683
|
function getProjectArchiveStatePath(gitDir) {
|
|
9615
|
-
return
|
|
10684
|
+
return join9(gitDir, "webmux", "archive.json");
|
|
9616
10685
|
}
|
|
9617
10686
|
async function ensureWorktreeStorageDirs(gitDir) {
|
|
9618
10687
|
const paths = getWorktreeStoragePaths(gitDir);
|
|
@@ -9781,7 +10850,7 @@ var init_fs = __esm(() => {
|
|
|
9781
10850
|
|
|
9782
10851
|
// backend/src/adapters/tmux.ts
|
|
9783
10852
|
import { createHash } from "crypto";
|
|
9784
|
-
import { basename as
|
|
10853
|
+
import { basename as basename4, resolve as resolve3 } from "path";
|
|
9785
10854
|
function runTmux(args) {
|
|
9786
10855
|
const result = Bun.spawnSync(["tmux", ...args], {
|
|
9787
10856
|
stdout: "pipe",
|
|
@@ -9810,7 +10879,7 @@ function sanitizeTmuxNameSegment(value, maxLength = 24) {
|
|
|
9810
10879
|
}
|
|
9811
10880
|
function buildProjectSessionName(projectRoot) {
|
|
9812
10881
|
const resolved = resolve3(projectRoot);
|
|
9813
|
-
const base = sanitizeTmuxNameSegment(
|
|
10882
|
+
const base = sanitizeTmuxNameSegment(basename4(resolved), 18);
|
|
9814
10883
|
const hash = createHash("sha1").update(resolved).digest("hex").slice(0, 8);
|
|
9815
10884
|
return `wm-${base}-${hash}`;
|
|
9816
10885
|
}
|
|
@@ -9885,55 +10954,6 @@ class BunTmuxGateway {
|
|
|
9885
10954
|
}
|
|
9886
10955
|
var init_tmux = () => {};
|
|
9887
10956
|
|
|
9888
|
-
// backend/src/domain/policies.ts
|
|
9889
|
-
function sanitizeBranchName(raw) {
|
|
9890
|
-
return raw.toLowerCase().replace(/\s+/g, "-").replace(INVALID_BRANCH_CHARS_RE, "").replace(/@\{/g, "").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/-{2,}/g, "-").replace(/^[.\-/]+|[.\-/]+$/g, "").replace(/\.lock$/i, "");
|
|
9891
|
-
}
|
|
9892
|
-
function isValidBranchName(raw) {
|
|
9893
|
-
return raw.length > 0 && sanitizeBranchName(raw) === raw;
|
|
9894
|
-
}
|
|
9895
|
-
function isValidWorktreeName(name) {
|
|
9896
|
-
return name.length > 0 && VALID_WORKTREE_NAME_RE.test(name) && !name.includes("..");
|
|
9897
|
-
}
|
|
9898
|
-
function isValidEnvKey(key) {
|
|
9899
|
-
return UNSAFE_ENV_KEY_RE.test(key);
|
|
9900
|
-
}
|
|
9901
|
-
function allocateServicePorts(existingMetas, services) {
|
|
9902
|
-
const allocatable = services.filter((service2) => service2.portStart != null);
|
|
9903
|
-
if (allocatable.length === 0)
|
|
9904
|
-
return {};
|
|
9905
|
-
const reference = allocatable[0];
|
|
9906
|
-
const referenceStart = reference.portStart;
|
|
9907
|
-
const referenceStep = reference.portStep ?? 1;
|
|
9908
|
-
const occupiedSlots = new Set;
|
|
9909
|
-
for (const meta of existingMetas) {
|
|
9910
|
-
const port = meta.allocatedPorts[reference.portEnv];
|
|
9911
|
-
if (!Number.isInteger(port) || port < referenceStart)
|
|
9912
|
-
continue;
|
|
9913
|
-
const diff = port - referenceStart;
|
|
9914
|
-
if (diff % referenceStep !== 0)
|
|
9915
|
-
continue;
|
|
9916
|
-
occupiedSlots.add(diff / referenceStep);
|
|
9917
|
-
}
|
|
9918
|
-
let slot = 1;
|
|
9919
|
-
while (occupiedSlots.has(slot))
|
|
9920
|
-
slot += 1;
|
|
9921
|
-
const result = {};
|
|
9922
|
-
for (const service2 of allocatable) {
|
|
9923
|
-
const start = service2.portStart;
|
|
9924
|
-
const step = service2.portStep ?? 1;
|
|
9925
|
-
result[service2.portEnv] = start + slot * step;
|
|
9926
|
-
}
|
|
9927
|
-
return result;
|
|
9928
|
-
}
|
|
9929
|
-
var INVALID_BRANCH_CHARS_RE, UNSAFE_ENV_KEY_RE, VALID_WORKTREE_NAME_RE, RESERVED_INSTANCE_PREFIXES;
|
|
9930
|
-
var init_policies = __esm(() => {
|
|
9931
|
-
INVALID_BRANCH_CHARS_RE = /[~^:?*\[\]\\]+/g;
|
|
9932
|
-
UNSAFE_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9933
|
-
VALID_WORKTREE_NAME_RE = /^[a-z0-9][a-z0-9\-_./]*$/;
|
|
9934
|
-
RESERVED_INSTANCE_PREFIXES = new Set(["api", "ws", "assets"]);
|
|
9935
|
-
});
|
|
9936
|
-
|
|
9937
10957
|
// backend/src/services/archive-service.ts
|
|
9938
10958
|
import { resolve as resolve4 } from "path";
|
|
9939
10959
|
function createArchiveState(entries) {
|
|
@@ -16957,8 +17977,8 @@ var init_dist5 = __esm(() => {
|
|
|
16957
17977
|
});
|
|
16958
17978
|
|
|
16959
17979
|
// backend/src/adapters/config.ts
|
|
16960
|
-
import { readFileSync as
|
|
16961
|
-
import { dirname as dirname2, join as
|
|
17980
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
17981
|
+
import { dirname as dirname2, join as join10, resolve as resolve5 } from "path";
|
|
16962
17982
|
function DEFAULT_ONESHOT_SYSTEM_PROMPT() {
|
|
16963
17983
|
return [
|
|
16964
17984
|
"You are running in webmux ONESHOT mode. There is NO interactive user \u2014 nobody is watching the chat or will respond to questions, approvals, or status checks. Any message asking the user to review, approve, confirm, take a look, or 'let you know' is wasted output: it will not be answered.",
|
|
@@ -17187,10 +18207,10 @@ function getDefaultProfileName(config) {
|
|
|
17187
18207
|
return Object.keys(config.profiles)[0] ?? "default";
|
|
17188
18208
|
}
|
|
17189
18209
|
function readConfigFile(root) {
|
|
17190
|
-
return
|
|
18210
|
+
return readFileSync7(join10(root, ".webmux.yaml"), "utf8");
|
|
17191
18211
|
}
|
|
17192
18212
|
function readLocalConfigFile(root) {
|
|
17193
|
-
return
|
|
18213
|
+
return readFileSync7(join10(root, ".webmux.local.yaml"), "utf8");
|
|
17194
18214
|
}
|
|
17195
18215
|
function parseConfigDocument(text) {
|
|
17196
18216
|
const parsed = $parse(text);
|
|
@@ -17695,7 +18715,7 @@ var init_docker = __esm(() => {
|
|
|
17695
18715
|
});
|
|
17696
18716
|
|
|
17697
18717
|
// backend/src/adapters/hooks.ts
|
|
17698
|
-
import { join as
|
|
18718
|
+
import { join as join11 } from "path";
|
|
17699
18719
|
function buildErrorMessage(name, exitCode, stdout, stderr) {
|
|
17700
18720
|
const output = stderr.trim() || stdout.trim();
|
|
17701
18721
|
if (output) {
|
|
@@ -17720,7 +18740,7 @@ class BunLifecycleHookRunner {
|
|
|
17720
18740
|
return this.direnvAvailable;
|
|
17721
18741
|
}
|
|
17722
18742
|
async buildCommand(cwd, command) {
|
|
17723
|
-
if (this.checkDirenv() && await Bun.file(
|
|
18743
|
+
if (this.checkDirenv() && await Bun.file(join11(cwd, ".envrc")).exists()) {
|
|
17724
18744
|
Bun.spawnSync(["direnv", "allow"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
17725
18745
|
return ["direnv", "exec", cwd, "bash", "-c", command];
|
|
17726
18746
|
}
|
|
@@ -17843,88 +18863,15 @@ function normalizeGeneratedBranchName(raw) {
|
|
|
17843
18863
|
function getSystemPrompt(config) {
|
|
17844
18864
|
return config.systemPrompt?.trim() || DEFAULT_SYSTEM_PROMPT;
|
|
17845
18865
|
}
|
|
17846
|
-
async function defaultSpawn(args, options = {}) {
|
|
17847
|
-
const proc = Bun.spawn(args, {
|
|
17848
|
-
stdout: "pipe",
|
|
17849
|
-
stderr: "pipe"
|
|
17850
|
-
});
|
|
17851
|
-
const resultPromise = Promise.all([
|
|
17852
|
-
new Response(proc.stdout).text(),
|
|
17853
|
-
new Response(proc.stderr).text(),
|
|
17854
|
-
proc.exited
|
|
17855
|
-
]).then(([stdout, stderr, exitCode]) => ({ exitCode, stdout, stderr }));
|
|
17856
|
-
if (options.timeoutMs === undefined) {
|
|
17857
|
-
return await resultPromise;
|
|
17858
|
-
}
|
|
17859
|
-
return await new Promise((resolve6, reject) => {
|
|
17860
|
-
let settled = false;
|
|
17861
|
-
const timeoutId = setTimeout(() => {
|
|
17862
|
-
if (settled)
|
|
17863
|
-
return;
|
|
17864
|
-
settled = true;
|
|
17865
|
-
try {
|
|
17866
|
-
proc.kill("SIGKILL");
|
|
17867
|
-
} catch {}
|
|
17868
|
-
reject(new AutoNameTimeoutError(options.timeoutMs));
|
|
17869
|
-
}, options.timeoutMs);
|
|
17870
|
-
resultPromise.then((result) => {
|
|
17871
|
-
if (settled)
|
|
17872
|
-
return;
|
|
17873
|
-
settled = true;
|
|
17874
|
-
clearTimeout(timeoutId);
|
|
17875
|
-
resolve6(result);
|
|
17876
|
-
}, (error) => {
|
|
17877
|
-
if (settled)
|
|
17878
|
-
return;
|
|
17879
|
-
settled = true;
|
|
17880
|
-
clearTimeout(timeoutId);
|
|
17881
|
-
reject(error);
|
|
17882
|
-
});
|
|
17883
|
-
});
|
|
17884
|
-
}
|
|
17885
|
-
function buildClaudeArgs(model, systemPrompt, prompt) {
|
|
17886
|
-
const args = [
|
|
17887
|
-
"claude",
|
|
17888
|
-
"-p",
|
|
17889
|
-
"--system-prompt",
|
|
17890
|
-
systemPrompt,
|
|
17891
|
-
"--output-format",
|
|
17892
|
-
"text",
|
|
17893
|
-
"--no-session-persistence",
|
|
17894
|
-
"--model",
|
|
17895
|
-
model || DEFAULT_AUTO_NAME_MODEL,
|
|
17896
|
-
"--effort",
|
|
17897
|
-
"low"
|
|
17898
|
-
];
|
|
17899
|
-
args.push(prompt);
|
|
17900
|
-
return args;
|
|
17901
|
-
}
|
|
17902
|
-
function escapeTomlString(s) {
|
|
17903
|
-
return s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
|
|
17904
|
-
}
|
|
17905
18866
|
function buildPrompt(prompt) {
|
|
17906
18867
|
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.`;
|
|
17907
18868
|
}
|
|
17908
|
-
function buildCodexArgs(model, systemPrompt, prompt) {
|
|
17909
|
-
const args = [
|
|
17910
|
-
"codex",
|
|
17911
|
-
"-c",
|
|
17912
|
-
`developer_instructions="${escapeTomlString(systemPrompt)}"`,
|
|
17913
|
-
"exec",
|
|
17914
|
-
"--ephemeral"
|
|
17915
|
-
];
|
|
17916
|
-
if (model) {
|
|
17917
|
-
args.push("-m", model);
|
|
17918
|
-
}
|
|
17919
|
-
args.push(prompt);
|
|
17920
|
-
return args;
|
|
17921
|
-
}
|
|
17922
18869
|
|
|
17923
18870
|
class AutoNameService {
|
|
17924
18871
|
spawnImpl;
|
|
17925
18872
|
timeoutMs;
|
|
17926
18873
|
constructor(deps2 = {}) {
|
|
17927
|
-
this.spawnImpl = deps2.spawnImpl
|
|
18874
|
+
this.spawnImpl = deps2.spawnImpl;
|
|
17928
18875
|
this.timeoutMs = deps2.timeoutMs ?? AUTO_NAME_TIMEOUT_MS;
|
|
17929
18876
|
}
|
|
17930
18877
|
async generateBranchName(config, task) {
|
|
@@ -17934,24 +18881,24 @@ class AutoNameService {
|
|
|
17934
18881
|
}
|
|
17935
18882
|
const systemPrompt = getSystemPrompt(config);
|
|
17936
18883
|
const userPrompt = buildPrompt(prompt);
|
|
17937
|
-
const
|
|
17938
|
-
const
|
|
17939
|
-
|
|
17940
|
-
|
|
17941
|
-
|
|
17942
|
-
|
|
17943
|
-
if (
|
|
18884
|
+
const cli = llmProviderLabel(config);
|
|
18885
|
+
const runOptions = { timeoutMs: this.timeoutMs };
|
|
18886
|
+
if (this.spawnImpl)
|
|
18887
|
+
runOptions.spawnImpl = this.spawnImpl;
|
|
18888
|
+
const result = await runShortLlmTask(config, systemPrompt, userPrompt, runOptions);
|
|
18889
|
+
if (!result.ok) {
|
|
18890
|
+
if (result.kind === "timeout") {
|
|
17944
18891
|
const fallback = generateFallbackBranchName();
|
|
17945
18892
|
log.warn(`[auto-name] ${cli} timed out after ${this.timeoutMs}ms; using fallback branch ${fallback}`);
|
|
17946
18893
|
return fallback;
|
|
17947
18894
|
}
|
|
17948
|
-
|
|
17949
|
-
|
|
17950
|
-
|
|
18895
|
+
if (result.kind === "spawn_error") {
|
|
18896
|
+
throw new Error(`'${cli}' CLI not found. Install it or check your PATH.`);
|
|
18897
|
+
}
|
|
17951
18898
|
const stderr = result.stderr.trim();
|
|
17952
18899
|
const stdout = result.stdout.trim();
|
|
17953
18900
|
const output2 = stderr || stdout || `exit ${result.exitCode}`;
|
|
17954
|
-
const command = args.join(" ");
|
|
18901
|
+
const command = result.args.join(" ");
|
|
17955
18902
|
throw new Error(`${cli} failed (command: ${command}): ${output2}`);
|
|
17956
18903
|
}
|
|
17957
18904
|
const output = result.stdout.trim();
|
|
@@ -17961,11 +18908,12 @@ class AutoNameService {
|
|
|
17961
18908
|
return normalizeGeneratedBranchName(output);
|
|
17962
18909
|
}
|
|
17963
18910
|
}
|
|
17964
|
-
var MAX_BRANCH_LENGTH = 40,
|
|
18911
|
+
var MAX_BRANCH_LENGTH = 40, AUTO_NAME_TIMEOUT_MS = 15000, DEFAULT_SYSTEM_PROMPT;
|
|
17965
18912
|
var init_auto_name_service = __esm(() => {
|
|
17966
18913
|
init_policies();
|
|
17967
18914
|
init_branch_name();
|
|
17968
18915
|
init_log();
|
|
18916
|
+
init_llm_spawn();
|
|
17969
18917
|
DEFAULT_SYSTEM_PROMPT = [
|
|
17970
18918
|
"Generate a concise git branch name from the task description.",
|
|
17971
18919
|
"Return only the branch name.",
|
|
@@ -17973,13 +18921,6 @@ var init_auto_name_service = __esm(() => {
|
|
|
17973
18921
|
`Maximum ${MAX_BRANCH_LENGTH} characters.`,
|
|
17974
18922
|
"Do not include quotes, code fences, or prefixes like feature/ or fix/."
|
|
17975
18923
|
].join(" ");
|
|
17976
|
-
AutoNameTimeoutError = class AutoNameTimeoutError extends Error {
|
|
17977
|
-
timeoutMs;
|
|
17978
|
-
constructor(timeoutMs) {
|
|
17979
|
-
super(`Auto-name timed out after ${timeoutMs}ms`);
|
|
17980
|
-
this.timeoutMs = timeoutMs;
|
|
17981
|
-
}
|
|
17982
|
-
};
|
|
17983
18924
|
});
|
|
17984
18925
|
|
|
17985
18926
|
// backend/src/services/archive-state-service.ts
|
|
@@ -18048,7 +18989,7 @@ var init_archive_state_service = __esm(() => {
|
|
|
18048
18989
|
|
|
18049
18990
|
// backend/src/adapters/agent-runtime.ts
|
|
18050
18991
|
import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
|
|
18051
|
-
import { dirname as dirname4, join as
|
|
18992
|
+
import { dirname as dirname4, join as join12, resolve as resolve6 } from "path";
|
|
18052
18993
|
function shellQuote(value) {
|
|
18053
18994
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
18054
18995
|
}
|
|
@@ -18467,7 +19408,7 @@ async function mergeCodexHooksFile(hooksPath, hookSettings, agentCtlPath) {
|
|
|
18467
19408
|
}
|
|
18468
19409
|
async function resolveGitCommonDir(gitDir) {
|
|
18469
19410
|
try {
|
|
18470
|
-
const commonDir = (await Bun.file(
|
|
19411
|
+
const commonDir = (await Bun.file(join12(gitDir, "commondir")).text()).trim();
|
|
18471
19412
|
if (!commonDir)
|
|
18472
19413
|
return gitDir;
|
|
18473
19414
|
return commonDir.startsWith("/") ? commonDir : resolve6(gitDir, commonDir);
|
|
@@ -18477,7 +19418,7 @@ async function resolveGitCommonDir(gitDir) {
|
|
|
18477
19418
|
}
|
|
18478
19419
|
async function ensureGeneratedCodexHooksIgnored(gitDir) {
|
|
18479
19420
|
const commonDir = await resolveGitCommonDir(gitDir);
|
|
18480
|
-
const excludePath =
|
|
19421
|
+
const excludePath = join12(commonDir, "info", "exclude");
|
|
18481
19422
|
let existing = "";
|
|
18482
19423
|
try {
|
|
18483
19424
|
existing = await Bun.file(excludePath).text();
|
|
@@ -18497,9 +19438,9 @@ async function ensureGeneratedCodexHooksIgnored(gitDir) {
|
|
|
18497
19438
|
async function ensureAgentRuntimeArtifacts(input) {
|
|
18498
19439
|
const storagePaths = getWorktreeStoragePaths(input.gitDir);
|
|
18499
19440
|
const artifacts = {
|
|
18500
|
-
agentCtlPath:
|
|
18501
|
-
claudeSettingsPath:
|
|
18502
|
-
codexHooksPath:
|
|
19441
|
+
agentCtlPath: join12(storagePaths.webmuxDir, "webmux-agentctl"),
|
|
19442
|
+
claudeSettingsPath: join12(input.worktreePath, ".claude", "settings.local.json"),
|
|
19443
|
+
codexHooksPath: join12(input.worktreePath, ".codex", "hooks.json")
|
|
18503
19444
|
};
|
|
18504
19445
|
await mkdir3(dirname4(artifacts.claudeSettingsPath), { recursive: true });
|
|
18505
19446
|
await mkdir3(dirname4(artifacts.codexHooksPath), { recursive: true });
|
|
@@ -18533,10 +19474,11 @@ function buildDockerRuntimeBootstrap(runtimeEnvPath) {
|
|
|
18533
19474
|
function buildBuiltInAgentInvocation(input) {
|
|
18534
19475
|
const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
|
|
18535
19476
|
if (input.agent === "codex") {
|
|
18536
|
-
const hooksFlag = " --enable
|
|
19477
|
+
const hooksFlag = " --enable hooks";
|
|
18537
19478
|
const yoloFlag2 = input.yolo ? " --yolo" : "";
|
|
18538
19479
|
if (input.launchMode === "resume") {
|
|
18539
|
-
|
|
19480
|
+
const resumeTarget = input.resumeConversationId ? ` ${quoteShell(input.resumeConversationId)}` : " --last";
|
|
19481
|
+
return `codex${hooksFlag}${yoloFlag2} resume${resumeTarget}${promptSuffix}`;
|
|
18540
19482
|
}
|
|
18541
19483
|
if (input.systemPrompt) {
|
|
18542
19484
|
return `codex${hooksFlag}${yoloFlag2} -c ${quoteShell(`developer_instructions=${input.systemPrompt}`)}${promptSuffix}`;
|
|
@@ -18579,7 +19521,8 @@ function buildAgentInvocation(input) {
|
|
|
18579
19521
|
yolo: input.yolo,
|
|
18580
19522
|
systemPrompt: input.systemPrompt,
|
|
18581
19523
|
prompt: input.prompt,
|
|
18582
|
-
launchMode: input.launchMode
|
|
19524
|
+
launchMode: input.launchMode,
|
|
19525
|
+
resumeConversationId: input.resumeConversationId
|
|
18583
19526
|
});
|
|
18584
19527
|
}
|
|
18585
19528
|
return buildCustomAgentInvocation({
|
|
@@ -19000,6 +19943,17 @@ function buildRuntimeControlBaseUrl(controlBaseUrl, runtime) {
|
|
|
19000
19943
|
return trimmed;
|
|
19001
19944
|
}
|
|
19002
19945
|
}
|
|
19946
|
+
function resolveCodexResumeConversationId(meta, agent, launchMode) {
|
|
19947
|
+
if (launchMode !== "resume")
|
|
19948
|
+
return;
|
|
19949
|
+
if (meta.agentTerminalStale !== true)
|
|
19950
|
+
return;
|
|
19951
|
+
if (agent.kind !== "builtin" || agent.implementation.agent !== "codex")
|
|
19952
|
+
return;
|
|
19953
|
+
if (meta.conversation?.provider !== "codexAppServer")
|
|
19954
|
+
return;
|
|
19955
|
+
return meta.conversation.threadId;
|
|
19956
|
+
}
|
|
19003
19957
|
function prefixAgentBranch(agent, branch) {
|
|
19004
19958
|
return `${agent}-${branch}`;
|
|
19005
19959
|
}
|
|
@@ -19073,6 +20027,7 @@ class LifecycleService {
|
|
|
19073
20027
|
const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
|
|
19074
20028
|
const agent = this.resolveAgentDefinition(initialized.meta.agent);
|
|
19075
20029
|
const launchMode = resolved.meta && agent.capabilities.resume ? "resume" : "fresh";
|
|
20030
|
+
const resumeConversationId = resolveCodexResumeConversationId(initialized.meta, agent, launchMode);
|
|
19076
20031
|
await ensureAgentRuntimeArtifacts({
|
|
19077
20032
|
gitDir: initialized.paths.gitDir,
|
|
19078
20033
|
worktreePath: resolved.entry.path
|
|
@@ -19085,7 +20040,57 @@ class LifecycleService {
|
|
|
19085
20040
|
initialized,
|
|
19086
20041
|
worktreePath: resolved.entry.path,
|
|
19087
20042
|
launchMode,
|
|
19088
|
-
followUpPrompt: options.prompt
|
|
20043
|
+
followUpPrompt: options.prompt,
|
|
20044
|
+
resumeConversationId
|
|
20045
|
+
});
|
|
20046
|
+
if (initialized.meta.agentTerminalStale === true) {
|
|
20047
|
+
await writeWorktreeMeta(resolved.gitDir, {
|
|
20048
|
+
...initialized.meta,
|
|
20049
|
+
agentTerminalStale: false
|
|
20050
|
+
});
|
|
20051
|
+
}
|
|
20052
|
+
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
20053
|
+
return {
|
|
20054
|
+
branch,
|
|
20055
|
+
worktreeId: initialized.meta.worktreeId
|
|
20056
|
+
};
|
|
20057
|
+
} catch (error) {
|
|
20058
|
+
throw this.wrapOperationError(error);
|
|
20059
|
+
}
|
|
20060
|
+
}
|
|
20061
|
+
async refreshAgentTerminal(branch) {
|
|
20062
|
+
try {
|
|
20063
|
+
const resolved = await this.resolveExistingWorktree(branch);
|
|
20064
|
+
if (!resolved.meta) {
|
|
20065
|
+
throw new LifecycleError(`Worktree ${branch} has no managed metadata to refresh`, 409);
|
|
20066
|
+
}
|
|
20067
|
+
const initialized = await this.refreshManagedArtifacts(resolved);
|
|
20068
|
+
const { profileName, profile } = this.resolveProfile(initialized.meta.profile);
|
|
20069
|
+
const agent = this.resolveAgentDefinition(initialized.meta.agent);
|
|
20070
|
+
if (agent.kind !== "builtin" || agent.implementation.agent !== "codex") {
|
|
20071
|
+
throw new LifecycleError("Refreshing the agent terminal is only available for Codex worktrees", 409);
|
|
20072
|
+
}
|
|
20073
|
+
const conversation = initialized.meta.conversation;
|
|
20074
|
+
if (conversation?.provider !== "codexAppServer") {
|
|
20075
|
+
throw new LifecycleError("No Codex conversation is available to refresh", 409);
|
|
20076
|
+
}
|
|
20077
|
+
await ensureAgentRuntimeArtifacts({
|
|
20078
|
+
gitDir: initialized.paths.gitDir,
|
|
20079
|
+
worktreePath: resolved.entry.path
|
|
20080
|
+
});
|
|
20081
|
+
await this.materializeRuntimeSession({
|
|
20082
|
+
branch,
|
|
20083
|
+
profileName,
|
|
20084
|
+
profile,
|
|
20085
|
+
agent,
|
|
20086
|
+
initialized,
|
|
20087
|
+
worktreePath: resolved.entry.path,
|
|
20088
|
+
launchMode: "resume",
|
|
20089
|
+
resumeConversationId: conversation.threadId
|
|
20090
|
+
});
|
|
20091
|
+
await writeWorktreeMeta(resolved.gitDir, {
|
|
20092
|
+
...initialized.meta,
|
|
20093
|
+
agentTerminalStale: false
|
|
19089
20094
|
});
|
|
19090
20095
|
await this.deps.reconciliation.reconcile(this.deps.projectRoot, { force: true });
|
|
19091
20096
|
return {
|
|
@@ -19400,6 +20405,7 @@ class LifecycleService {
|
|
|
19400
20405
|
followUpPrompt: input.followUpPrompt,
|
|
19401
20406
|
launchMode: input.launchMode,
|
|
19402
20407
|
source: input.source,
|
|
20408
|
+
resumeConversationId: input.resumeConversationId,
|
|
19403
20409
|
containerName: containerName2
|
|
19404
20410
|
}));
|
|
19405
20411
|
return;
|
|
@@ -19414,7 +20420,8 @@ class LifecycleService {
|
|
|
19414
20420
|
creationPrompt: input.creationPrompt,
|
|
19415
20421
|
followUpPrompt: input.followUpPrompt,
|
|
19416
20422
|
launchMode: input.launchMode,
|
|
19417
|
-
source: input.source
|
|
20423
|
+
source: input.source,
|
|
20424
|
+
resumeConversationId: input.resumeConversationId
|
|
19418
20425
|
}));
|
|
19419
20426
|
}
|
|
19420
20427
|
buildSessionLayout(input) {
|
|
@@ -19439,7 +20446,8 @@ ${oneshotPrompt}` : oneshotPrompt ?? baseSystemPrompt;
|
|
|
19439
20446
|
yolo: input.profile.yolo === true,
|
|
19440
20447
|
systemPrompt,
|
|
19441
20448
|
prompt,
|
|
19442
|
-
launchMode: input.launchMode
|
|
20449
|
+
launchMode: input.launchMode,
|
|
20450
|
+
resumeConversationId: input.resumeConversationId
|
|
19443
20451
|
}),
|
|
19444
20452
|
shell: buildDockerShellCommand(containerName2, input.worktreePath, input.initialized.paths.runtimeEnvPath)
|
|
19445
20453
|
} : {
|
|
@@ -19453,7 +20461,8 @@ ${oneshotPrompt}` : oneshotPrompt ?? baseSystemPrompt;
|
|
|
19453
20461
|
yolo: input.profile.yolo === true,
|
|
19454
20462
|
systemPrompt,
|
|
19455
20463
|
prompt,
|
|
19456
|
-
launchMode: input.launchMode
|
|
20464
|
+
launchMode: input.launchMode,
|
|
20465
|
+
resumeConversationId: input.resumeConversationId
|
|
19457
20466
|
}),
|
|
19458
20467
|
shell: buildManagedShellCommand(input.initialized.paths.runtimeEnvPath)
|
|
19459
20468
|
}
|
|
@@ -19814,6 +20823,7 @@ function makeDefaultState(input) {
|
|
|
19814
20823
|
agentName: input.agentName ?? null,
|
|
19815
20824
|
source: input.source ?? "ui",
|
|
19816
20825
|
oneshot: input.oneshot ?? null,
|
|
20826
|
+
agentTerminalStale: input.agentTerminalStale === true,
|
|
19817
20827
|
git: {
|
|
19818
20828
|
exists: true,
|
|
19819
20829
|
branch: input.branch,
|
|
@@ -19861,6 +20871,8 @@ class ProjectRuntime {
|
|
|
19861
20871
|
existing.baseBranch = input.baseBranch;
|
|
19862
20872
|
existing.profile = input.profile ?? existing.profile;
|
|
19863
20873
|
existing.agentName = input.agentName ?? existing.agentName;
|
|
20874
|
+
if (input.agentTerminalStale !== undefined)
|
|
20875
|
+
existing.agentTerminalStale = input.agentTerminalStale;
|
|
19864
20876
|
if (input.runtime)
|
|
19865
20877
|
existing.agent.runtime = input.runtime;
|
|
19866
20878
|
if (input.source !== undefined)
|
|
@@ -19922,6 +20934,11 @@ class ProjectRuntime {
|
|
|
19922
20934
|
state.prs = prs.map((pr) => clonePrEntry(pr));
|
|
19923
20935
|
return state;
|
|
19924
20936
|
}
|
|
20937
|
+
setAgentTerminalStale(worktreeId, stale) {
|
|
20938
|
+
const state = this.requireWorktree(worktreeId);
|
|
20939
|
+
state.agentTerminalStale = stale;
|
|
20940
|
+
return state;
|
|
20941
|
+
}
|
|
19925
20942
|
applyEvent(event, now) {
|
|
19926
20943
|
const state = this.requireWorktree(event.worktreeId);
|
|
19927
20944
|
if (event.branch !== state.branch) {
|
|
@@ -19998,7 +21015,7 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
19998
21015
|
}
|
|
19999
21016
|
|
|
20000
21017
|
// backend/src/services/reconciliation-service.ts
|
|
20001
|
-
import { basename as
|
|
21018
|
+
import { basename as basename5, resolve as resolve9 } from "path";
|
|
20002
21019
|
function makeUnmanagedWorktreeId(path) {
|
|
20003
21020
|
return `unmanaged:${resolve9(path)}`;
|
|
20004
21021
|
}
|
|
@@ -20033,7 +21050,7 @@ function findWindow(windows, sessionName, branch) {
|
|
|
20033
21050
|
return windows.find((window) => window.sessionName === sessionName && window.windowName === windowName) ?? null;
|
|
20034
21051
|
}
|
|
20035
21052
|
function resolveBranch(entry, metaBranch) {
|
|
20036
|
-
const fallback =
|
|
21053
|
+
const fallback = basename5(entry.path);
|
|
20037
21054
|
return entry.branch ?? metaBranch ?? (fallback.length > 0 ? fallback : "unknown");
|
|
20038
21055
|
}
|
|
20039
21056
|
|
|
@@ -20092,6 +21109,7 @@ class ReconciliationService {
|
|
|
20092
21109
|
path: entry.path,
|
|
20093
21110
|
profile: meta?.profile ?? null,
|
|
20094
21111
|
agentName: meta?.agent ?? null,
|
|
21112
|
+
agentTerminalStale: meta?.agentTerminalStale === true,
|
|
20095
21113
|
runtime: meta?.runtime ?? "host",
|
|
20096
21114
|
source: meta?.source ?? "ui",
|
|
20097
21115
|
oneshot: meta?.oneshot ?? null,
|
|
@@ -20127,6 +21145,7 @@ class ReconciliationService {
|
|
|
20127
21145
|
path: state.path,
|
|
20128
21146
|
profile: state.profile,
|
|
20129
21147
|
agentName: state.agentName,
|
|
21148
|
+
agentTerminalStale: state.agentTerminalStale,
|
|
20130
21149
|
runtime: state.runtime,
|
|
20131
21150
|
source: state.source,
|
|
20132
21151
|
oneshot: state.oneshot
|
|
@@ -20270,7 +21289,7 @@ __export(exports_worktree_commands, {
|
|
|
20270
21289
|
parseAddCommandArgs: () => parseAddCommandArgs,
|
|
20271
21290
|
getWorktreeCommandUsage: () => getWorktreeCommandUsage
|
|
20272
21291
|
});
|
|
20273
|
-
import { basename as
|
|
21292
|
+
import { basename as basename6, resolve as resolve10 } from "path";
|
|
20274
21293
|
function getWorktreeCommandUsage(command) {
|
|
20275
21294
|
switch (command) {
|
|
20276
21295
|
case "add":
|
|
@@ -20309,6 +21328,9 @@ function getWorktreeCommandUsage(command) {
|
|
|
20309
21328
|
case "close":
|
|
20310
21329
|
return `Usage:
|
|
20311
21330
|
webmux close <branch>`;
|
|
21331
|
+
case "refresh":
|
|
21332
|
+
return `Usage:
|
|
21333
|
+
webmux refresh <branch>`;
|
|
20312
21334
|
case "archive":
|
|
20313
21335
|
return `Usage:
|
|
20314
21336
|
webmux archive <branch>`;
|
|
@@ -20706,7 +21728,7 @@ async function listWorktrees(runtime, stdout, options) {
|
|
|
20706
21728
|
const projectGitDir = runtime.git.resolveWorktreeGitDir(projectDir);
|
|
20707
21729
|
const archivedPaths = buildArchivedWorktreePathSet(await readWorktreeArchiveState(projectGitDir));
|
|
20708
21730
|
const rows = await Promise.all(entries.map(async (entry) => {
|
|
20709
|
-
const branch = entry.branch ??
|
|
21731
|
+
const branch = entry.branch ?? basename6(entry.path);
|
|
20710
21732
|
const isOpen = openWindows.has(buildWorktreeWindowName(branch));
|
|
20711
21733
|
const gitDir = runtime.git.resolveWorktreeGitDir(entry.path);
|
|
20712
21734
|
const meta = await readWorktreeMeta(gitDir);
|
|
@@ -20907,6 +21929,10 @@ ${parsed.input.prompt}` : seed.data.conversationMarkdown;
|
|
|
20907
21929
|
await runtime.lifecycleService.closeWorktree(branch);
|
|
20908
21930
|
stdout(`Closed worktree ${branch}`);
|
|
20909
21931
|
return 0;
|
|
21932
|
+
case "refresh":
|
|
21933
|
+
await runtime.lifecycleService.refreshAgentTerminal(branch);
|
|
21934
|
+
stdout(`Refreshed agent terminal for ${branch}`);
|
|
21935
|
+
return 0;
|
|
20910
21936
|
case "archive":
|
|
20911
21937
|
await runtime.lifecycleService.setWorktreeArchived(branch, true);
|
|
20912
21938
|
stdout(`Archived worktree ${branch}`);
|
|
@@ -20950,13 +21976,13 @@ var init_worktree_commands = __esm(() => {
|
|
|
20950
21976
|
});
|
|
20951
21977
|
|
|
20952
21978
|
// bin/src/webmux.ts
|
|
20953
|
-
import { resolve as resolve11, dirname as dirname6, join as
|
|
20954
|
-
import { existsSync as
|
|
21979
|
+
import { resolve as resolve11, dirname as dirname6, join as join13 } from "path";
|
|
21980
|
+
import { existsSync as existsSync7 } from "fs";
|
|
20955
21981
|
import { fileURLToPath } from "url";
|
|
20956
21982
|
// package.json
|
|
20957
21983
|
var package_default = {
|
|
20958
21984
|
name: "webmux",
|
|
20959
|
-
version: "0.
|
|
21985
|
+
version: "0.35.0",
|
|
20960
21986
|
description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
|
|
20961
21987
|
type: "module",
|
|
20962
21988
|
repository: {
|
|
@@ -21028,6 +22054,7 @@ Usage:
|
|
|
21028
22054
|
webmux list List worktrees and their status
|
|
21029
22055
|
webmux open Open an existing worktree session
|
|
21030
22056
|
webmux close Close a worktree session without removing it
|
|
22057
|
+
webmux refresh Refresh a Codex agent terminal from saved chat
|
|
21031
22058
|
webmux archive Hide a worktree from the default list
|
|
21032
22059
|
webmux unarchive Show an archived worktree again
|
|
21033
22060
|
webmux label Set or clear a workspace label
|
|
@@ -21053,13 +22080,14 @@ Environment:
|
|
|
21053
22080
|
`);
|
|
21054
22081
|
}
|
|
21055
22082
|
function isRootCommand(value) {
|
|
21056
|
-
return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "oneshot" || value === "list" || value === "open" || value === "close" || value === "archive" || value === "unarchive" || value === "label" || value === "remove" || value === "merge" || value === "send" || value === "prune" || value === "linear" || value === "completion";
|
|
22083
|
+
return value === "serve" || value === "init" || value === "service" || value === "update" || value === "add" || value === "oneshot" || value === "list" || value === "open" || value === "close" || value === "refresh" || value === "archive" || value === "unarchive" || value === "label" || value === "remove" || value === "merge" || value === "send" || value === "prune" || value === "linear" || value === "completion";
|
|
21057
22084
|
}
|
|
21058
22085
|
function isServeRootOption(value) {
|
|
21059
22086
|
return value === "--port" || value === "--prefix" || value === "--app" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
|
|
21060
22087
|
}
|
|
21061
22088
|
function parseRootArgs(args) {
|
|
21062
22089
|
let port = parseInt(process.env.PORT || "5111", 10);
|
|
22090
|
+
let portExplicit = process.env.PORT !== undefined;
|
|
21063
22091
|
let debug = false;
|
|
21064
22092
|
let app = false;
|
|
21065
22093
|
let prefix = process.env.WEBMUX_PREFIX?.trim() || null;
|
|
@@ -21083,6 +22111,7 @@ function parseRootArgs(args) {
|
|
|
21083
22111
|
if (Number.isNaN(port)) {
|
|
21084
22112
|
throw new Error("Error: --port requires a numeric value");
|
|
21085
22113
|
}
|
|
22114
|
+
portExplicit = true;
|
|
21086
22115
|
index += 1;
|
|
21087
22116
|
break;
|
|
21088
22117
|
}
|
|
@@ -21120,6 +22149,7 @@ Run webmux --help for usage.`);
|
|
|
21120
22149
|
}
|
|
21121
22150
|
return {
|
|
21122
22151
|
port,
|
|
22152
|
+
portExplicit,
|
|
21123
22153
|
debug,
|
|
21124
22154
|
app,
|
|
21125
22155
|
prefix,
|
|
@@ -21128,10 +22158,10 @@ Run webmux --help for usage.`);
|
|
|
21128
22158
|
};
|
|
21129
22159
|
}
|
|
21130
22160
|
function isWorktreeCommand(command) {
|
|
21131
|
-
return command === "add" || command === "list" || command === "open" || command === "close" || command === "archive" || command === "unarchive" || command === "label" || command === "remove" || command === "merge" || command === "send" || command === "prune";
|
|
22161
|
+
return command === "add" || command === "list" || command === "open" || command === "close" || command === "refresh" || command === "archive" || command === "unarchive" || command === "label" || command === "remove" || command === "merge" || command === "send" || command === "prune";
|
|
21132
22162
|
}
|
|
21133
22163
|
async function loadEnvFile(path) {
|
|
21134
|
-
if (!
|
|
22164
|
+
if (!existsSync7(path))
|
|
21135
22165
|
return;
|
|
21136
22166
|
const lines = (await Bun.file(path).text()).split(`
|
|
21137
22167
|
`);
|
|
@@ -21164,7 +22194,7 @@ function findBrowserBinary() {
|
|
|
21164
22194
|
"brave-browser"
|
|
21165
22195
|
];
|
|
21166
22196
|
for (const candidate of candidates) {
|
|
21167
|
-
const found = candidate.startsWith("/") ?
|
|
22197
|
+
const found = candidate.startsWith("/") ? existsSync7(candidate) : Bun.spawnSync(["which", candidate], { stdout: "pipe", stderr: "pipe" }).success;
|
|
21168
22198
|
if (found)
|
|
21169
22199
|
return candidate;
|
|
21170
22200
|
}
|
|
@@ -21243,6 +22273,28 @@ async function main(args = process.argv.slice(2)) {
|
|
|
21243
22273
|
stderr: "inherit"
|
|
21244
22274
|
});
|
|
21245
22275
|
const code = await proc.exited;
|
|
22276
|
+
if (code === 0) {
|
|
22277
|
+
const { listInstalledServices: listInstalledServices2, updateInstalledService: updateInstalledService2 } = await Promise.resolve().then(() => (init_service_restart(), exports_service_restart));
|
|
22278
|
+
const services = listInstalledServices2();
|
|
22279
|
+
if (services.length > 0) {
|
|
22280
|
+
const whichResult = Bun.spawnSync(["which", "webmux"], { stdout: "pipe", stderr: "pipe" });
|
|
22281
|
+
const webmuxPath = whichResult.success ? whichResult.stdout.toString().trim() : "";
|
|
22282
|
+
console.log(`
|
|
22283
|
+
Refreshing ${services.length} installed webmux service(s) to pick up the new version...`);
|
|
22284
|
+
for (const svc of services) {
|
|
22285
|
+
const outcome = await updateInstalledService2(svc, webmuxPath);
|
|
22286
|
+
const parts = [];
|
|
22287
|
+
if (outcome.regenerated)
|
|
22288
|
+
parts.push("regenerated unit");
|
|
22289
|
+
if (outcome.restarted)
|
|
22290
|
+
parts.push("restarted");
|
|
22291
|
+
if (!outcome.regenerated && !outcome.restarted && !outcome.error)
|
|
22292
|
+
parts.push("no change");
|
|
22293
|
+
const status2 = outcome.error ? `failed \u2014 ${outcome.error}` : parts.join(", ");
|
|
22294
|
+
console.log(` ${svc.name}: ${status2}`);
|
|
22295
|
+
}
|
|
22296
|
+
}
|
|
22297
|
+
}
|
|
21246
22298
|
process.exit(code);
|
|
21247
22299
|
}
|
|
21248
22300
|
await loadEnvFile(resolve11(process.cwd(), ".env.local"));
|
|
@@ -21271,7 +22323,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
21271
22323
|
usage2();
|
|
21272
22324
|
process.exit(0);
|
|
21273
22325
|
}
|
|
21274
|
-
if (!
|
|
22326
|
+
if (!existsSync7(resolve11(process.cwd(), ".webmux.yaml"))) {
|
|
21275
22327
|
console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
|
|
21276
22328
|
process.exit(1);
|
|
21277
22329
|
}
|
|
@@ -21279,6 +22331,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
21279
22331
|
...process.env,
|
|
21280
22332
|
PORT: String(parsed.port),
|
|
21281
22333
|
WEBMUX_PROJECT_DIR: process.cwd(),
|
|
22334
|
+
...parsed.portExplicit ? { WEBMUX_PORT_STRICT: "1" } : {},
|
|
21282
22335
|
...parsed.prefix ? { WEBMUX_PREFIX: parsed.prefix } : {},
|
|
21283
22336
|
...parsed.debug ? { WEBMUX_DEBUG: "1" } : {}
|
|
21284
22337
|
};
|
|
@@ -21306,13 +22359,13 @@ async function main(args = process.argv.slice(2)) {
|
|
|
21306
22359
|
}
|
|
21307
22360
|
process.on("SIGINT", cleanup);
|
|
21308
22361
|
process.on("SIGTERM", cleanup);
|
|
21309
|
-
const backendEntry =
|
|
21310
|
-
const staticDir =
|
|
21311
|
-
if (!
|
|
22362
|
+
const backendEntry = join13(PKG_ROOT, "backend", "dist", "server.js");
|
|
22363
|
+
const staticDir = join13(PKG_ROOT, "frontend", "dist");
|
|
22364
|
+
if (!existsSync7(staticDir)) {
|
|
21312
22365
|
console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
|
|
21313
22366
|
process.exit(1);
|
|
21314
22367
|
}
|
|
21315
|
-
console.log(`Starting webmux on port ${parsed.port} (falls back to a free port if taken)...`);
|
|
22368
|
+
console.log(parsed.portExplicit ? `Starting webmux on port ${parsed.port}...` : `Starting webmux on port ${parsed.port} (falls back to a free port if taken)...`);
|
|
21316
22369
|
const be2 = Bun.spawn(["bun", backendEntry], {
|
|
21317
22370
|
env: { ...baseEnv, WEBMUX_STATIC_DIR: staticDir },
|
|
21318
22371
|
stdout: "pipe",
|