webmux 0.32.0 → 0.34.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/backend/dist/server.js +642 -372
- package/bin/webmux.js +598 -143
- package/frontend/dist/assets/{DiffDialog-CtwnOqjo.js → DiffDialog-Dv77nDf4.js} +1 -1
- package/frontend/dist/assets/index-BgvCuf9J.js +34 -0
- package/frontend/dist/assets/index-EO_hEDxL.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-CvURkZrd.css +0 -1
- package/frontend/dist/assets/index-EqF9CRFa.js +0 -34
package/bin/webmux.js
CHANGED
|
@@ -2443,12 +2443,15 @@ integrations:
|
|
|
2443
2443
|
linear:
|
|
2444
2444
|
# Enable Linear issue lookup and linking in the UI.
|
|
2445
2445
|
enabled: true
|
|
2446
|
-
# Auto-create worktrees for assigned issues.
|
|
2446
|
+
# Auto-create worktrees for assigned issues labeled "webmux" or "webmux_oneshot".
|
|
2447
2447
|
# autoCreateWorktrees: true
|
|
2448
|
-
# Show a create-ticket action in the dashboard.
|
|
2448
|
+
# Show a create-ticket action in the dashboard. The team to file into is
|
|
2449
|
+
# picked in the dialog at creation time.
|
|
2449
2450
|
# createTicketOption: true
|
|
2450
|
-
# Restrict
|
|
2451
|
-
#
|
|
2451
|
+
# Restrict the auto-create watcher to issues from these teams. Useful when
|
|
2452
|
+
# the authenticated Linear user is in multiple teams or when running webmux
|
|
2453
|
+
# in several projects on the same machine that share a Linear account.
|
|
2454
|
+
# watchTeams: [ENG, OPS]
|
|
2452
2455
|
|
|
2453
2456
|
# startupEnvs become runtime env vars for panes, agents, and hooks.
|
|
2454
2457
|
startupEnvs:
|
|
@@ -2716,14 +2719,255 @@ ${result.stderr.trim()}` : ""
|
|
|
2716
2719
|
console.log();
|
|
2717
2720
|
});
|
|
2718
2721
|
|
|
2722
|
+
// backend/src/lib/log.ts
|
|
2723
|
+
function ts() {
|
|
2724
|
+
return new Date().toISOString().slice(11, 23);
|
|
2725
|
+
}
|
|
2726
|
+
var DEBUG, log;
|
|
2727
|
+
var init_log = __esm(() => {
|
|
2728
|
+
DEBUG = Bun.env.WEBMUX_DEBUG === "1";
|
|
2729
|
+
log = {
|
|
2730
|
+
info(msg) {
|
|
2731
|
+
console.log(`[${ts()}] ${msg}`);
|
|
2732
|
+
},
|
|
2733
|
+
debug(msg) {
|
|
2734
|
+
if (DEBUG)
|
|
2735
|
+
console.log(`[${ts()}] ${msg}`);
|
|
2736
|
+
},
|
|
2737
|
+
warn(msg) {
|
|
2738
|
+
console.warn(`[${ts()}] ${msg}`);
|
|
2739
|
+
},
|
|
2740
|
+
error(msg, err) {
|
|
2741
|
+
err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
|
|
2742
|
+
}
|
|
2743
|
+
};
|
|
2744
|
+
});
|
|
2745
|
+
|
|
2746
|
+
// backend/src/domain/policies.ts
|
|
2747
|
+
function sanitizeBranchName(raw) {
|
|
2748
|
+
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, "");
|
|
2749
|
+
}
|
|
2750
|
+
function isValidBranchName(raw) {
|
|
2751
|
+
return raw.length > 0 && sanitizeBranchName(raw) === raw;
|
|
2752
|
+
}
|
|
2753
|
+
function isValidWorktreeName(name) {
|
|
2754
|
+
return name.length > 0 && VALID_WORKTREE_NAME_RE.test(name) && !name.includes("..");
|
|
2755
|
+
}
|
|
2756
|
+
function isValidEnvKey(key) {
|
|
2757
|
+
return UNSAFE_ENV_KEY_RE.test(key);
|
|
2758
|
+
}
|
|
2759
|
+
function isValidInstancePrefix(value) {
|
|
2760
|
+
return VALID_INSTANCE_PREFIX_RE.test(value) && !RESERVED_INSTANCE_PREFIXES.has(value);
|
|
2761
|
+
}
|
|
2762
|
+
function allocateServicePorts(existingMetas, services) {
|
|
2763
|
+
const allocatable = services.filter((service) => service.portStart != null);
|
|
2764
|
+
if (allocatable.length === 0)
|
|
2765
|
+
return {};
|
|
2766
|
+
const reference = allocatable[0];
|
|
2767
|
+
const referenceStart = reference.portStart;
|
|
2768
|
+
const referenceStep = reference.portStep ?? 1;
|
|
2769
|
+
const occupiedSlots = new Set;
|
|
2770
|
+
for (const meta of existingMetas) {
|
|
2771
|
+
const port = meta.allocatedPorts[reference.portEnv];
|
|
2772
|
+
if (!Number.isInteger(port) || port < referenceStart)
|
|
2773
|
+
continue;
|
|
2774
|
+
const diff = port - referenceStart;
|
|
2775
|
+
if (diff % referenceStep !== 0)
|
|
2776
|
+
continue;
|
|
2777
|
+
occupiedSlots.add(diff / referenceStep);
|
|
2778
|
+
}
|
|
2779
|
+
let slot = 1;
|
|
2780
|
+
while (occupiedSlots.has(slot))
|
|
2781
|
+
slot += 1;
|
|
2782
|
+
const result = {};
|
|
2783
|
+
for (const service of allocatable) {
|
|
2784
|
+
const start = service.portStart;
|
|
2785
|
+
const step = service.portStep ?? 1;
|
|
2786
|
+
result[service.portEnv] = start + slot * step;
|
|
2787
|
+
}
|
|
2788
|
+
return result;
|
|
2789
|
+
}
|
|
2790
|
+
var INVALID_BRANCH_CHARS_RE, UNSAFE_ENV_KEY_RE, VALID_WORKTREE_NAME_RE, VALID_INSTANCE_PREFIX_RE, RESERVED_INSTANCE_PREFIXES;
|
|
2791
|
+
var init_policies = __esm(() => {
|
|
2792
|
+
INVALID_BRANCH_CHARS_RE = /[~^:?*\[\]\\]+/g;
|
|
2793
|
+
UNSAFE_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2794
|
+
VALID_WORKTREE_NAME_RE = /^[a-z0-9][a-z0-9\-_./]*$/;
|
|
2795
|
+
VALID_INSTANCE_PREFIX_RE = /^[a-z0-9][a-z0-9\-]*$/;
|
|
2796
|
+
RESERVED_INSTANCE_PREFIXES = new Set(["api", "ws", "assets"]);
|
|
2797
|
+
});
|
|
2798
|
+
|
|
2799
|
+
// backend/src/adapters/instance-registry.ts
|
|
2800
|
+
import { mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync3, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
2801
|
+
import { homedir } from "os";
|
|
2802
|
+
import { join as join5 } from "path";
|
|
2803
|
+
function defaultRegistryDir() {
|
|
2804
|
+
return join5(homedir(), ".webmux", "instances");
|
|
2805
|
+
}
|
|
2806
|
+
function isAlive(pid) {
|
|
2807
|
+
try {
|
|
2808
|
+
process.kill(pid, 0);
|
|
2809
|
+
return true;
|
|
2810
|
+
} catch (err) {
|
|
2811
|
+
return err?.code !== "ESRCH";
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
function isInstanceEntry(value) {
|
|
2815
|
+
if (typeof value !== "object" || value === null)
|
|
2816
|
+
return false;
|
|
2817
|
+
const v2 = value;
|
|
2818
|
+
return typeof v2.prefix === "string" && isValidInstancePrefix(v2.prefix) && typeof v2.port === "number" && typeof v2.projectDir === "string" && typeof v2.pid === "number" && typeof v2.startedAt === "number";
|
|
2819
|
+
}
|
|
2820
|
+
function createInstanceRegistry(dir = defaultRegistryDir()) {
|
|
2821
|
+
function ensureDir() {
|
|
2822
|
+
mkdirSync(dir, { recursive: true });
|
|
2823
|
+
}
|
|
2824
|
+
function entryPath(port) {
|
|
2825
|
+
return join5(dir, `${port}.json`);
|
|
2826
|
+
}
|
|
2827
|
+
function readEntry(filename) {
|
|
2828
|
+
try {
|
|
2829
|
+
const raw = readFileSync3(join5(dir, filename), "utf8");
|
|
2830
|
+
const parsed = JSON.parse(raw);
|
|
2831
|
+
return isInstanceEntry(parsed) ? parsed : null;
|
|
2832
|
+
} catch {
|
|
2833
|
+
return null;
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
return {
|
|
2837
|
+
register(entry) {
|
|
2838
|
+
ensureDir();
|
|
2839
|
+
const finalPath = entryPath(entry.port);
|
|
2840
|
+
const tmpPath = `${finalPath}.${process.pid}.${Date.now()}.tmp`;
|
|
2841
|
+
const text = `${JSON.stringify(entry, null, 2)}
|
|
2842
|
+
`;
|
|
2843
|
+
writeFileSync(tmpPath, text);
|
|
2844
|
+
renameSync(tmpPath, finalPath);
|
|
2845
|
+
},
|
|
2846
|
+
deregister(port, expectedPid) {
|
|
2847
|
+
if (expectedPid !== undefined) {
|
|
2848
|
+
const filename = `${port}.json`;
|
|
2849
|
+
const entry = readEntry(filename);
|
|
2850
|
+
if (entry && entry.pid !== expectedPid) {
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2854
|
+
try {
|
|
2855
|
+
unlinkSync(entryPath(port));
|
|
2856
|
+
} catch (err) {
|
|
2857
|
+
const code = err?.code;
|
|
2858
|
+
if (code !== "ENOENT") {
|
|
2859
|
+
log.debug(`[instance-registry] deregister(${port}) failed: ${String(err)}`);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
},
|
|
2863
|
+
listLive() {
|
|
2864
|
+
let filenames;
|
|
2865
|
+
try {
|
|
2866
|
+
filenames = readdirSync2(dir).filter((name) => name.endsWith(".json"));
|
|
2867
|
+
} catch {
|
|
2868
|
+
return [];
|
|
2869
|
+
}
|
|
2870
|
+
const live = [];
|
|
2871
|
+
for (const filename of filenames) {
|
|
2872
|
+
const entry = readEntry(filename);
|
|
2873
|
+
if (!entry)
|
|
2874
|
+
continue;
|
|
2875
|
+
if (!isAlive(entry.pid)) {
|
|
2876
|
+
try {
|
|
2877
|
+
unlinkSync(join5(dir, filename));
|
|
2878
|
+
} catch {}
|
|
2879
|
+
continue;
|
|
2880
|
+
}
|
|
2881
|
+
live.push(entry);
|
|
2882
|
+
}
|
|
2883
|
+
return live;
|
|
2884
|
+
}
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
var init_instance_registry = __esm(() => {
|
|
2888
|
+
init_log();
|
|
2889
|
+
init_policies();
|
|
2890
|
+
});
|
|
2891
|
+
|
|
2892
|
+
// bin/src/install-ports.ts
|
|
2893
|
+
import { existsSync as existsSync4, readdirSync as readdirSync3, readFileSync as readFileSync4 } from "fs";
|
|
2894
|
+
import { homedir as homedir2 } from "os";
|
|
2895
|
+
import { join as join6 } from "path";
|
|
2896
|
+
function pickFreePort(start, taken) {
|
|
2897
|
+
const set = new Set(taken);
|
|
2898
|
+
let port = start;
|
|
2899
|
+
while (set.has(port))
|
|
2900
|
+
port += 1;
|
|
2901
|
+
return port;
|
|
2902
|
+
}
|
|
2903
|
+
function readInstalledServicePorts(opts = {}) {
|
|
2904
|
+
const systemdDir = opts.systemdDir ?? DEFAULT_SYSTEMD_DIR;
|
|
2905
|
+
const launchdDir = opts.launchdDir ?? DEFAULT_LAUNCHD_DIR;
|
|
2906
|
+
const ports = [];
|
|
2907
|
+
function collect(dir, namePredicate) {
|
|
2908
|
+
if (!existsSync4(dir))
|
|
2909
|
+
return;
|
|
2910
|
+
let names;
|
|
2911
|
+
try {
|
|
2912
|
+
names = readdirSync3(dir);
|
|
2913
|
+
} catch {
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
for (const name of names) {
|
|
2917
|
+
if (!namePredicate(name))
|
|
2918
|
+
continue;
|
|
2919
|
+
const full = join6(dir, name);
|
|
2920
|
+
if (opts.excludePath && full === opts.excludePath)
|
|
2921
|
+
continue;
|
|
2922
|
+
const port = readPortFromUnit(full);
|
|
2923
|
+
if (port !== null)
|
|
2924
|
+
ports.push(port);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
collect(systemdDir, (n) => n.startsWith("webmux-") && n.endsWith(".service"));
|
|
2928
|
+
collect(launchdDir, (n) => n.startsWith("com.webmux.") && n.endsWith(".plist"));
|
|
2929
|
+
return ports;
|
|
2930
|
+
}
|
|
2931
|
+
function readPortFromUnit(filePath) {
|
|
2932
|
+
let text;
|
|
2933
|
+
try {
|
|
2934
|
+
text = readFileSync4(filePath, "utf8");
|
|
2935
|
+
} catch {
|
|
2936
|
+
return null;
|
|
2937
|
+
}
|
|
2938
|
+
const regex = filePath.endsWith(".plist") ? LAUNCHD_PORT_RE : SYSTEMD_PORT_RE;
|
|
2939
|
+
const match = regex.exec(text);
|
|
2940
|
+
return match ? parseInt(match[1], 10) : null;
|
|
2941
|
+
}
|
|
2942
|
+
function discoverTakenPorts(opts = {}) {
|
|
2943
|
+
const registry = createInstanceRegistry(opts.registryDir);
|
|
2944
|
+
const live = registry.listLive().map((entry) => entry.port);
|
|
2945
|
+
const installed = readInstalledServicePorts({
|
|
2946
|
+
systemdDir: opts.systemdDir,
|
|
2947
|
+
launchdDir: opts.launchdDir,
|
|
2948
|
+
excludePath: opts.excludeUnitPath
|
|
2949
|
+
});
|
|
2950
|
+
return new Set([...live, ...installed]);
|
|
2951
|
+
}
|
|
2952
|
+
var DEFAULT_SYSTEMD_DIR, DEFAULT_LAUNCHD_DIR, SYSTEMD_PORT_RE, LAUNCHD_PORT_RE;
|
|
2953
|
+
var init_install_ports = __esm(() => {
|
|
2954
|
+
init_instance_registry();
|
|
2955
|
+
DEFAULT_SYSTEMD_DIR = join6(homedir2(), ".config", "systemd", "user");
|
|
2956
|
+
DEFAULT_LAUNCHD_DIR = join6(homedir2(), "Library", "LaunchAgents");
|
|
2957
|
+
SYSTEMD_PORT_RE = /--port\s+(\d+)/;
|
|
2958
|
+
LAUNCHD_PORT_RE = /<string>--port<\/string>\s*<string>(\d+)<\/string>/;
|
|
2959
|
+
});
|
|
2960
|
+
|
|
2719
2961
|
// bin/src/service.ts
|
|
2720
2962
|
var exports_service = {};
|
|
2721
2963
|
__export(exports_service, {
|
|
2964
|
+
parseInstalledServiceConfig: () => parseInstalledServiceConfig,
|
|
2965
|
+
generateServiceFile: () => generateServiceFile,
|
|
2722
2966
|
default: () => service
|
|
2723
2967
|
});
|
|
2724
|
-
import { existsSync as
|
|
2725
|
-
import { join as
|
|
2726
|
-
import { homedir } from "os";
|
|
2968
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, unlinkSync as unlinkSync2 } from "fs";
|
|
2969
|
+
import { basename as basename3, join as join7 } from "path";
|
|
2970
|
+
import { homedir as homedir3 } from "os";
|
|
2727
2971
|
function getPlatform() {
|
|
2728
2972
|
const plat = process.platform;
|
|
2729
2973
|
if (plat === "linux" || plat === "darwin")
|
|
@@ -2752,10 +2996,10 @@ function printRunResult(result) {
|
|
|
2752
2996
|
console.error(err);
|
|
2753
2997
|
}
|
|
2754
2998
|
function systemdUnitPath(serviceName) {
|
|
2755
|
-
return
|
|
2999
|
+
return join7(homedir3(), ".config", "systemd", "user", `${serviceName}.service`);
|
|
2756
3000
|
}
|
|
2757
3001
|
function launchdPlistPath(serviceName) {
|
|
2758
|
-
return
|
|
3002
|
+
return join7(homedir3(), "Library", "LaunchAgents", `com.webmux.${serviceName}.plist`);
|
|
2759
3003
|
}
|
|
2760
3004
|
function serviceFilePath(config) {
|
|
2761
3005
|
if (config.platform === "linux")
|
|
@@ -2781,7 +3025,7 @@ WantedBy=default.target
|
|
|
2781
3025
|
`;
|
|
2782
3026
|
}
|
|
2783
3027
|
function generateLaunchdPlist(config) {
|
|
2784
|
-
const logPath =
|
|
3028
|
+
const logPath = join7(homedir3(), "Library", "Logs", `webmux-${config.serviceName}.log`);
|
|
2785
3029
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2786
3030
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2787
3031
|
<plist version="1.0">
|
|
@@ -2826,6 +3070,36 @@ function generateServiceFile(config) {
|
|
|
2826
3070
|
return generateSystemdUnit(config);
|
|
2827
3071
|
return generateLaunchdPlist(config);
|
|
2828
3072
|
}
|
|
3073
|
+
function readWorkingDirFromUnit(filePath, platform) {
|
|
3074
|
+
let text;
|
|
3075
|
+
try {
|
|
3076
|
+
text = readFileSync5(filePath, "utf8");
|
|
3077
|
+
} catch {
|
|
3078
|
+
return null;
|
|
3079
|
+
}
|
|
3080
|
+
const regex = platform === "linux" ? SYSTEMD_WORKDIR_RE : LAUNCHD_WORKDIR_RE;
|
|
3081
|
+
const match = regex.exec(text);
|
|
3082
|
+
return match ? match[1].trim() : null;
|
|
3083
|
+
}
|
|
3084
|
+
function parseInstalledServiceConfig(filePath, platform, webmuxPath) {
|
|
3085
|
+
const port = readPortFromUnit(filePath);
|
|
3086
|
+
if (port === null)
|
|
3087
|
+
return null;
|
|
3088
|
+
const projectDir = readWorkingDirFromUnit(filePath, platform);
|
|
3089
|
+
if (projectDir === null)
|
|
3090
|
+
return null;
|
|
3091
|
+
const fileBase = basename3(filePath);
|
|
3092
|
+
const serviceName = platform === "linux" ? fileBase.replace(/\.service$/, "") : fileBase.replace(/^com\.webmux\./, "").replace(/\.plist$/, "");
|
|
3093
|
+
const projectName = detectProjectName(projectDir);
|
|
3094
|
+
return {
|
|
3095
|
+
platform,
|
|
3096
|
+
projectName,
|
|
3097
|
+
serviceName,
|
|
3098
|
+
webmuxPath,
|
|
3099
|
+
projectDir,
|
|
3100
|
+
port
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
2829
3103
|
function installCommands(config) {
|
|
2830
3104
|
if (config.platform === "linux") {
|
|
2831
3105
|
return [
|
|
@@ -2849,11 +3123,12 @@ function uninstallCommands(config) {
|
|
|
2849
3123
|
];
|
|
2850
3124
|
}
|
|
2851
3125
|
function isInstalled(config) {
|
|
2852
|
-
return
|
|
3126
|
+
return existsSync5(serviceFilePath(config));
|
|
2853
3127
|
}
|
|
2854
|
-
async function install(config) {
|
|
3128
|
+
async function install(config, portExplicit) {
|
|
2855
3129
|
const filePath = serviceFilePath(config);
|
|
2856
|
-
|
|
3130
|
+
const alreadyInstalled = isInstalled(config);
|
|
3131
|
+
if (alreadyInstalled) {
|
|
2857
3132
|
const reinstall = await ue({ message: "Service is already installed. Reinstall?" });
|
|
2858
3133
|
if (q(reinstall) || !reinstall) {
|
|
2859
3134
|
R2.info("Aborted.");
|
|
@@ -2863,6 +3138,31 @@ async function install(config) {
|
|
|
2863
3138
|
runCommand(cmd);
|
|
2864
3139
|
}
|
|
2865
3140
|
}
|
|
3141
|
+
const requestedPort = config.port;
|
|
3142
|
+
let chosenPort = requestedPort;
|
|
3143
|
+
let portNote = null;
|
|
3144
|
+
let portWarning = null;
|
|
3145
|
+
if (!portExplicit) {
|
|
3146
|
+
const existingPort = alreadyInstalled ? readPortFromUnit(filePath) : null;
|
|
3147
|
+
if (existingPort !== null) {
|
|
3148
|
+
chosenPort = existingPort;
|
|
3149
|
+
if (existingPort !== requestedPort) {
|
|
3150
|
+
portNote = `Reusing port ${existingPort} from the existing service unit (pass --port to override).`;
|
|
3151
|
+
}
|
|
3152
|
+
} else {
|
|
3153
|
+
const taken = discoverTakenPorts({ excludeUnitPath: filePath });
|
|
3154
|
+
chosenPort = pickFreePort(requestedPort, taken);
|
|
3155
|
+
if (chosenPort !== requestedPort) {
|
|
3156
|
+
portNote = `Port ${requestedPort} is already used by another webmux instance \u2014 picked ${chosenPort} instead (pass --port to override).`;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
} else {
|
|
3160
|
+
const taken = discoverTakenPorts({ excludeUnitPath: filePath });
|
|
3161
|
+
if (taken.has(requestedPort)) {
|
|
3162
|
+
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.`;
|
|
3163
|
+
}
|
|
3164
|
+
}
|
|
3165
|
+
config = { ...config, port: chosenPort };
|
|
2866
3166
|
const content = generateServiceFile(config);
|
|
2867
3167
|
const commands = installCommands(config);
|
|
2868
3168
|
Se([
|
|
@@ -2874,12 +3174,16 @@ async function install(config) {
|
|
|
2874
3174
|
...commands.map((c) => ` $ ${formatCommand(c)}`)
|
|
2875
3175
|
].join(`
|
|
2876
3176
|
`), "Install service");
|
|
3177
|
+
if (portNote)
|
|
3178
|
+
R2.info(portNote);
|
|
3179
|
+
if (portWarning)
|
|
3180
|
+
R2.warn(portWarning);
|
|
2877
3181
|
const ok = await ue({ message: "Proceed?" });
|
|
2878
3182
|
if (q(ok) || !ok) {
|
|
2879
3183
|
R2.info("Aborted.");
|
|
2880
3184
|
return;
|
|
2881
3185
|
}
|
|
2882
|
-
|
|
3186
|
+
mkdirSync2(filePath.substring(0, filePath.lastIndexOf("/")), { recursive: true });
|
|
2883
3187
|
await Bun.write(filePath, content);
|
|
2884
3188
|
R2.success(`Wrote ${filePath}`);
|
|
2885
3189
|
for (const cmd of commands) {
|
|
@@ -2929,7 +3233,7 @@ ${result.stderr.toString()}`);
|
|
|
2929
3233
|
R2.success(`$ ${formatCommand(cmd)}`);
|
|
2930
3234
|
}
|
|
2931
3235
|
}
|
|
2932
|
-
|
|
3236
|
+
unlinkSync2(filePath);
|
|
2933
3237
|
R2.success(`Removed ${filePath}`);
|
|
2934
3238
|
R2.success("Service uninstalled.");
|
|
2935
3239
|
}
|
|
@@ -2953,8 +3257,8 @@ function logs(config) {
|
|
|
2953
3257
|
if (config.platform === "linux") {
|
|
2954
3258
|
proc = Bun.spawn(["journalctl", "--user", "-u", config.serviceName, "-f", "--no-pager"], { stdout: "inherit", stderr: "inherit" });
|
|
2955
3259
|
} else {
|
|
2956
|
-
const logPath =
|
|
2957
|
-
if (!
|
|
3260
|
+
const logPath = join7(homedir3(), "Library", "Logs", `webmux-${config.serviceName}.log`);
|
|
3261
|
+
if (!existsSync5(logPath)) {
|
|
2958
3262
|
R2.error(`Log file not found: ${logPath}`);
|
|
2959
3263
|
return;
|
|
2960
3264
|
}
|
|
@@ -2975,6 +3279,12 @@ Usage:
|
|
|
2975
3279
|
webmux service uninstall Stop, disable, and remove the service
|
|
2976
3280
|
webmux service status Show service status
|
|
2977
3281
|
webmux service logs Tail service logs
|
|
3282
|
+
|
|
3283
|
+
Options:
|
|
3284
|
+
--port N Pin the service to a specific port. When omitted,
|
|
3285
|
+
a free port is picked automatically by scanning
|
|
3286
|
+
other webmux instances and installed services
|
|
3287
|
+
\u2014 second-project installs no longer collide on 5111.
|
|
2978
3288
|
`);
|
|
2979
3289
|
}
|
|
2980
3290
|
async function service(args) {
|
|
@@ -3010,6 +3320,7 @@ async function service(args) {
|
|
|
3010
3320
|
return;
|
|
3011
3321
|
}
|
|
3012
3322
|
let port = parseInt(process.env.PORT || "5111");
|
|
3323
|
+
let portExplicit = false;
|
|
3013
3324
|
for (let i = 1;i < args.length; i++) {
|
|
3014
3325
|
if (args[i] === "--port" && args[i + 1]) {
|
|
3015
3326
|
const parsed = parseInt(args[++i]);
|
|
@@ -3018,6 +3329,7 @@ async function service(args) {
|
|
|
3018
3329
|
return;
|
|
3019
3330
|
}
|
|
3020
3331
|
port = parsed;
|
|
3332
|
+
portExplicit = true;
|
|
3021
3333
|
}
|
|
3022
3334
|
}
|
|
3023
3335
|
const projectName = detectProjectName(gitRoot2);
|
|
@@ -3032,7 +3344,7 @@ async function service(args) {
|
|
|
3032
3344
|
};
|
|
3033
3345
|
switch (action) {
|
|
3034
3346
|
case "install":
|
|
3035
|
-
await install(config);
|
|
3347
|
+
await install(config, portExplicit);
|
|
3036
3348
|
break;
|
|
3037
3349
|
case "uninstall":
|
|
3038
3350
|
await uninstall(config);
|
|
@@ -3045,9 +3357,141 @@ async function service(args) {
|
|
|
3045
3357
|
break;
|
|
3046
3358
|
}
|
|
3047
3359
|
}
|
|
3360
|
+
var SYSTEMD_WORKDIR_RE, LAUNCHD_WORKDIR_RE;
|
|
3048
3361
|
var init_service = __esm(() => {
|
|
3049
3362
|
init_dist4();
|
|
3050
3363
|
init_shared();
|
|
3364
|
+
init_install_ports();
|
|
3365
|
+
SYSTEMD_WORKDIR_RE = /^WorkingDirectory=(.+)$/m;
|
|
3366
|
+
LAUNCHD_WORKDIR_RE = /<key>WorkingDirectory<\/key>\s*<string>([^<]+)<\/string>/;
|
|
3367
|
+
});
|
|
3368
|
+
|
|
3369
|
+
// bin/src/service-restart.ts
|
|
3370
|
+
var exports_service_restart = {};
|
|
3371
|
+
__export(exports_service_restart, {
|
|
3372
|
+
updateInstalledService: () => updateInstalledService,
|
|
3373
|
+
restartInstalledService: () => restartInstalledService,
|
|
3374
|
+
restartCommand: () => restartCommand,
|
|
3375
|
+
listInstalledServices: () => listInstalledServices
|
|
3376
|
+
});
|
|
3377
|
+
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
|
|
3378
|
+
import { homedir as homedir4 } from "os";
|
|
3379
|
+
import { join as join8 } from "path";
|
|
3380
|
+
function listInstalledServices(opts = {}) {
|
|
3381
|
+
const out = [];
|
|
3382
|
+
const systemdDir = opts.systemdDir ?? DEFAULT_SYSTEMD_DIR2;
|
|
3383
|
+
const launchdDir = opts.launchdDir ?? DEFAULT_LAUNCHD_DIR2;
|
|
3384
|
+
if (existsSync6(systemdDir)) {
|
|
3385
|
+
try {
|
|
3386
|
+
for (const name of readdirSync4(systemdDir)) {
|
|
3387
|
+
if (!name.startsWith("webmux-") || !name.endsWith(".service"))
|
|
3388
|
+
continue;
|
|
3389
|
+
out.push({
|
|
3390
|
+
name: name.slice(0, -".service".length),
|
|
3391
|
+
filePath: join8(systemdDir, name),
|
|
3392
|
+
platform: "linux"
|
|
3393
|
+
});
|
|
3394
|
+
}
|
|
3395
|
+
} catch {}
|
|
3396
|
+
}
|
|
3397
|
+
if (existsSync6(launchdDir)) {
|
|
3398
|
+
try {
|
|
3399
|
+
for (const name of readdirSync4(launchdDir)) {
|
|
3400
|
+
if (!name.startsWith("com.webmux.") || !name.endsWith(".plist"))
|
|
3401
|
+
continue;
|
|
3402
|
+
out.push({
|
|
3403
|
+
name: name.slice(0, -".plist".length),
|
|
3404
|
+
filePath: join8(launchdDir, name),
|
|
3405
|
+
platform: "darwin"
|
|
3406
|
+
});
|
|
3407
|
+
}
|
|
3408
|
+
} catch {}
|
|
3409
|
+
}
|
|
3410
|
+
return out;
|
|
3411
|
+
}
|
|
3412
|
+
function restartCommand(service2, uid) {
|
|
3413
|
+
if (service2.platform === "linux") {
|
|
3414
|
+
return { bin: "systemctl", args: ["--user", "restart", service2.name] };
|
|
3415
|
+
}
|
|
3416
|
+
return { bin: "launchctl", args: ["kickstart", "-k", `gui/${uid}/${service2.name}`] };
|
|
3417
|
+
}
|
|
3418
|
+
function restartInstalledService(service2, runner = defaultRunner) {
|
|
3419
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
|
3420
|
+
const { bin, args } = restartCommand(service2, uid);
|
|
3421
|
+
const result = runner.run(bin, args);
|
|
3422
|
+
if (!result.success) {
|
|
3423
|
+
return {
|
|
3424
|
+
service: service2,
|
|
3425
|
+
ok: false,
|
|
3426
|
+
error: result.stderr.toString().trim() || `${bin} ${args.join(" ")} failed`
|
|
3427
|
+
};
|
|
3428
|
+
}
|
|
3429
|
+
return { service: service2, ok: true };
|
|
3430
|
+
}
|
|
3431
|
+
function reloadAfterRegenerate(service2, runner) {
|
|
3432
|
+
if (service2.platform === "linux") {
|
|
3433
|
+
const result = runner.run("systemctl", ["--user", "daemon-reload"]);
|
|
3434
|
+
return result.success ? { ok: true } : { ok: false, error: result.stderr.toString().trim() || "daemon-reload failed" };
|
|
3435
|
+
}
|
|
3436
|
+
runner.run("launchctl", ["unload", service2.filePath]);
|
|
3437
|
+
const loadResult = runner.run("launchctl", ["load", "-w", service2.filePath]);
|
|
3438
|
+
if (loadResult.success)
|
|
3439
|
+
return { ok: true };
|
|
3440
|
+
const stderr = loadResult.stderr.toString().trim() || "load failed";
|
|
3441
|
+
return {
|
|
3442
|
+
ok: false,
|
|
3443
|
+
error: `${stderr}
|
|
3444
|
+
service is now unloaded \u2014 recover with: launchctl load -w "${service2.filePath}"`
|
|
3445
|
+
};
|
|
3446
|
+
}
|
|
3447
|
+
async function updateInstalledService(service2, webmuxPath, runner = defaultRunner) {
|
|
3448
|
+
const canRegenerate = webmuxPath.length > 0;
|
|
3449
|
+
const config = canRegenerate ? parseInstalledServiceConfig(service2.filePath, service2.platform, webmuxPath) : null;
|
|
3450
|
+
let regenerated = false;
|
|
3451
|
+
if (config !== null) {
|
|
3452
|
+
let currentContent = "";
|
|
3453
|
+
try {
|
|
3454
|
+
currentContent = readFileSync6(service2.filePath, "utf8");
|
|
3455
|
+
} catch {}
|
|
3456
|
+
const expected = generateServiceFile(config);
|
|
3457
|
+
if (currentContent !== expected) {
|
|
3458
|
+
try {
|
|
3459
|
+
await Bun.write(service2.filePath, expected);
|
|
3460
|
+
regenerated = true;
|
|
3461
|
+
} catch (err) {
|
|
3462
|
+
return {
|
|
3463
|
+
service: service2,
|
|
3464
|
+
regenerated: false,
|
|
3465
|
+
restarted: false,
|
|
3466
|
+
error: `could not rewrite ${service2.filePath}: ${String(err)}`
|
|
3467
|
+
};
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
if (regenerated) {
|
|
3472
|
+
const reload = reloadAfterRegenerate(service2, runner);
|
|
3473
|
+
if (!reload.ok) {
|
|
3474
|
+
return { service: service2, regenerated, restarted: false, error: reload.error };
|
|
3475
|
+
}
|
|
3476
|
+
if (service2.platform === "darwin") {
|
|
3477
|
+
return { service: service2, regenerated, restarted: true };
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
const outcome = restartInstalledService(service2, runner);
|
|
3481
|
+
return {
|
|
3482
|
+
service: service2,
|
|
3483
|
+
regenerated,
|
|
3484
|
+
restarted: outcome.ok,
|
|
3485
|
+
error: outcome.error
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3488
|
+
var defaultRunner, DEFAULT_SYSTEMD_DIR2, DEFAULT_LAUNCHD_DIR2;
|
|
3489
|
+
var init_service_restart = __esm(() => {
|
|
3490
|
+
init_shared();
|
|
3491
|
+
init_service();
|
|
3492
|
+
defaultRunner = { run };
|
|
3493
|
+
DEFAULT_SYSTEMD_DIR2 = join8(homedir4(), ".config", "systemd", "user");
|
|
3494
|
+
DEFAULT_LAUNCHD_DIR2 = join8(homedir4(), "Library", "LaunchAgents");
|
|
3051
3495
|
});
|
|
3052
3496
|
|
|
3053
3497
|
// node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
|
|
@@ -7016,7 +7460,7 @@ var init_zod = __esm(() => {
|
|
|
7016
7460
|
init_external();
|
|
7017
7461
|
});
|
|
7018
7462
|
|
|
7019
|
-
// node_modules/.bun/@ts-rest+core@3.52.1+
|
|
7463
|
+
// node_modules/.bun/@ts-rest+core@3.52.1+94e40505b11febf1/node_modules/@ts-rest/core/index.esm.mjs
|
|
7020
7464
|
var isZodType = (obj) => {
|
|
7021
7465
|
return typeof (obj === null || obj === undefined ? undefined : obj.safeParse) === "function";
|
|
7022
7466
|
}, isZodObjectStrict = (obj) => {
|
|
@@ -7338,7 +7782,7 @@ function parseLinearTarget(raw) {
|
|
|
7338
7782
|
return { kind: "team", teamKey: trimmed };
|
|
7339
7783
|
return { kind: "invalid", raw: trimmed };
|
|
7340
7784
|
}
|
|
7341
|
-
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;
|
|
7785
|
+
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;
|
|
7342
7786
|
var init_schemas = __esm(() => {
|
|
7343
7787
|
init_zod();
|
|
7344
7788
|
BooleanLikeSchema = exports_external.union([
|
|
@@ -7451,6 +7895,7 @@ var init_schemas = __esm(() => {
|
|
|
7451
7895
|
envOverrides: exports_external.record(exports_external.string()).optional(),
|
|
7452
7896
|
createLinearTicket: exports_external.literal(true).optional(),
|
|
7453
7897
|
linearTitle: exports_external.string().optional(),
|
|
7898
|
+
linearTeamKey: exports_external.string().trim().transform((value) => value.toUpperCase()).pipe(LinearTeamKeySchema).optional(),
|
|
7454
7899
|
fromLinear: FromLinearInputSchema.optional(),
|
|
7455
7900
|
source: WorktreeSourceSchema.optional(),
|
|
7456
7901
|
oneshot: OneshotConfigSchema.optional()
|
|
@@ -7763,6 +8208,15 @@ var init_schemas = __esm(() => {
|
|
|
7763
8208
|
RunIdParamsSchema = exports_external.object({
|
|
7764
8209
|
runId: NumberLikePathParamSchema
|
|
7765
8210
|
});
|
|
8211
|
+
InstanceSummarySchema = exports_external.object({
|
|
8212
|
+
prefix: exports_external.string(),
|
|
8213
|
+
port: exports_external.number(),
|
|
8214
|
+
projectDir: exports_external.string(),
|
|
8215
|
+
startedAt: exports_external.number()
|
|
8216
|
+
});
|
|
8217
|
+
InstancesResponseSchema = exports_external.object({
|
|
8218
|
+
instances: exports_external.array(InstanceSummarySchema)
|
|
8219
|
+
});
|
|
7766
8220
|
});
|
|
7767
8221
|
|
|
7768
8222
|
// packages/api-contract/src/contract.ts
|
|
@@ -7803,7 +8257,8 @@ var init_contract = __esm(() => {
|
|
|
7803
8257
|
setAutoRemoveOnMerge: "/api/github/auto-remove-on-merge",
|
|
7804
8258
|
pullMain: "/api/pull-main",
|
|
7805
8259
|
fetchCiLogs: "/api/ci-logs/:runId",
|
|
7806
|
-
dismissNotification: "/api/notifications/:id/dismiss"
|
|
8260
|
+
dismissNotification: "/api/notifications/:id/dismiss",
|
|
8261
|
+
fetchInstances: "/api/instances"
|
|
7807
8262
|
};
|
|
7808
8263
|
commonErrorResponses = {
|
|
7809
8264
|
400: ErrorResponseSchema,
|
|
@@ -8112,6 +8567,14 @@ var init_contract = __esm(() => {
|
|
|
8112
8567
|
400: ErrorResponseSchema,
|
|
8113
8568
|
404: ErrorResponseSchema
|
|
8114
8569
|
}
|
|
8570
|
+
},
|
|
8571
|
+
fetchInstances: {
|
|
8572
|
+
method: "GET",
|
|
8573
|
+
path: apiPaths.fetchInstances,
|
|
8574
|
+
responses: {
|
|
8575
|
+
200: InstancesResponseSchema,
|
|
8576
|
+
500: ErrorResponseSchema
|
|
8577
|
+
}
|
|
8115
8578
|
}
|
|
8116
8579
|
}, {
|
|
8117
8580
|
strictStatusCodes: true
|
|
@@ -8199,30 +8662,6 @@ var init_src = __esm(() => {
|
|
|
8199
8662
|
init_schemas();
|
|
8200
8663
|
});
|
|
8201
8664
|
|
|
8202
|
-
// backend/src/lib/log.ts
|
|
8203
|
-
function ts() {
|
|
8204
|
-
return new Date().toISOString().slice(11, 23);
|
|
8205
|
-
}
|
|
8206
|
-
var DEBUG, log;
|
|
8207
|
-
var init_log = __esm(() => {
|
|
8208
|
-
DEBUG = Bun.env.WEBMUX_DEBUG === "1";
|
|
8209
|
-
log = {
|
|
8210
|
-
info(msg) {
|
|
8211
|
-
console.log(`[${ts()}] ${msg}`);
|
|
8212
|
-
},
|
|
8213
|
-
debug(msg) {
|
|
8214
|
-
if (DEBUG)
|
|
8215
|
-
console.log(`[${ts()}] ${msg}`);
|
|
8216
|
-
},
|
|
8217
|
-
warn(msg) {
|
|
8218
|
-
console.warn(`[${ts()}] ${msg}`);
|
|
8219
|
-
},
|
|
8220
|
-
error(msg, err) {
|
|
8221
|
-
err !== undefined ? console.error(`[${ts()}] ${msg}`, err) : console.error(`[${ts()}] ${msg}`);
|
|
8222
|
-
}
|
|
8223
|
-
};
|
|
8224
|
-
});
|
|
8225
|
-
|
|
8226
8665
|
// backend/src/services/linear-service.ts
|
|
8227
8666
|
function gqlErrorMessage(raw) {
|
|
8228
8667
|
return raw.errors && raw.errors.length > 0 ? raw.errors.map((error) => error.message).join("; ") : null;
|
|
@@ -9540,7 +9979,7 @@ var WORKTREE_META_SCHEMA_VERSION = 1, WORKTREE_ARCHIVE_STATE_VERSION = 1;
|
|
|
9540
9979
|
|
|
9541
9980
|
// backend/src/adapters/fs.ts
|
|
9542
9981
|
import { mkdir } from "fs/promises";
|
|
9543
|
-
import { join as
|
|
9982
|
+
import { join as join9 } from "path";
|
|
9544
9983
|
function stringifyAllocatedPorts(ports) {
|
|
9545
9984
|
const entries = Object.entries(ports).map(([key, value]) => [key, String(value)]);
|
|
9546
9985
|
return Object.fromEntries(entries);
|
|
@@ -9572,25 +10011,25 @@ function parseDotenv(content) {
|
|
|
9572
10011
|
}
|
|
9573
10012
|
async function loadDotenvLocal(worktreePath) {
|
|
9574
10013
|
try {
|
|
9575
|
-
const content = await Bun.file(
|
|
10014
|
+
const content = await Bun.file(join9(worktreePath, ".env.local")).text();
|
|
9576
10015
|
return parseDotenv(content);
|
|
9577
10016
|
} catch {
|
|
9578
10017
|
return {};
|
|
9579
10018
|
}
|
|
9580
10019
|
}
|
|
9581
10020
|
function getWorktreeStoragePaths(gitDir) {
|
|
9582
|
-
const webmuxDir =
|
|
10021
|
+
const webmuxDir = join9(gitDir, "webmux");
|
|
9583
10022
|
return {
|
|
9584
10023
|
gitDir,
|
|
9585
10024
|
webmuxDir,
|
|
9586
|
-
metaPath:
|
|
9587
|
-
runtimeEnvPath:
|
|
9588
|
-
controlEnvPath:
|
|
9589
|
-
prsPath:
|
|
10025
|
+
metaPath: join9(webmuxDir, "meta.json"),
|
|
10026
|
+
runtimeEnvPath: join9(webmuxDir, "runtime.env"),
|
|
10027
|
+
controlEnvPath: join9(webmuxDir, "control.env"),
|
|
10028
|
+
prsPath: join9(webmuxDir, "prs.json")
|
|
9590
10029
|
};
|
|
9591
10030
|
}
|
|
9592
10031
|
function getProjectArchiveStatePath(gitDir) {
|
|
9593
|
-
return
|
|
10032
|
+
return join9(gitDir, "webmux", "archive.json");
|
|
9594
10033
|
}
|
|
9595
10034
|
async function ensureWorktreeStorageDirs(gitDir) {
|
|
9596
10035
|
const paths = getWorktreeStoragePaths(gitDir);
|
|
@@ -9759,7 +10198,7 @@ var init_fs = __esm(() => {
|
|
|
9759
10198
|
|
|
9760
10199
|
// backend/src/adapters/tmux.ts
|
|
9761
10200
|
import { createHash } from "crypto";
|
|
9762
|
-
import { basename as
|
|
10201
|
+
import { basename as basename4, resolve as resolve3 } from "path";
|
|
9763
10202
|
function runTmux(args) {
|
|
9764
10203
|
const result = Bun.spawnSync(["tmux", ...args], {
|
|
9765
10204
|
stdout: "pipe",
|
|
@@ -9788,7 +10227,7 @@ function sanitizeTmuxNameSegment(value, maxLength = 24) {
|
|
|
9788
10227
|
}
|
|
9789
10228
|
function buildProjectSessionName(projectRoot) {
|
|
9790
10229
|
const resolved = resolve3(projectRoot);
|
|
9791
|
-
const base = sanitizeTmuxNameSegment(
|
|
10230
|
+
const base = sanitizeTmuxNameSegment(basename4(resolved), 18);
|
|
9792
10231
|
const hash = createHash("sha1").update(resolved).digest("hex").slice(0, 8);
|
|
9793
10232
|
return `wm-${base}-${hash}`;
|
|
9794
10233
|
}
|
|
@@ -9863,54 +10302,6 @@ class BunTmuxGateway {
|
|
|
9863
10302
|
}
|
|
9864
10303
|
var init_tmux = () => {};
|
|
9865
10304
|
|
|
9866
|
-
// backend/src/domain/policies.ts
|
|
9867
|
-
function sanitizeBranchName(raw) {
|
|
9868
|
-
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, "");
|
|
9869
|
-
}
|
|
9870
|
-
function isValidBranchName(raw) {
|
|
9871
|
-
return raw.length > 0 && sanitizeBranchName(raw) === raw;
|
|
9872
|
-
}
|
|
9873
|
-
function isValidWorktreeName(name) {
|
|
9874
|
-
return name.length > 0 && VALID_WORKTREE_NAME_RE.test(name) && !name.includes("..");
|
|
9875
|
-
}
|
|
9876
|
-
function isValidEnvKey(key) {
|
|
9877
|
-
return UNSAFE_ENV_KEY_RE.test(key);
|
|
9878
|
-
}
|
|
9879
|
-
function allocateServicePorts(existingMetas, services) {
|
|
9880
|
-
const allocatable = services.filter((service2) => service2.portStart != null);
|
|
9881
|
-
if (allocatable.length === 0)
|
|
9882
|
-
return {};
|
|
9883
|
-
const reference = allocatable[0];
|
|
9884
|
-
const referenceStart = reference.portStart;
|
|
9885
|
-
const referenceStep = reference.portStep ?? 1;
|
|
9886
|
-
const occupiedSlots = new Set;
|
|
9887
|
-
for (const meta of existingMetas) {
|
|
9888
|
-
const port = meta.allocatedPorts[reference.portEnv];
|
|
9889
|
-
if (!Number.isInteger(port) || port < referenceStart)
|
|
9890
|
-
continue;
|
|
9891
|
-
const diff = port - referenceStart;
|
|
9892
|
-
if (diff % referenceStep !== 0)
|
|
9893
|
-
continue;
|
|
9894
|
-
occupiedSlots.add(diff / referenceStep);
|
|
9895
|
-
}
|
|
9896
|
-
let slot = 1;
|
|
9897
|
-
while (occupiedSlots.has(slot))
|
|
9898
|
-
slot += 1;
|
|
9899
|
-
const result = {};
|
|
9900
|
-
for (const service2 of allocatable) {
|
|
9901
|
-
const start = service2.portStart;
|
|
9902
|
-
const step = service2.portStep ?? 1;
|
|
9903
|
-
result[service2.portEnv] = start + slot * step;
|
|
9904
|
-
}
|
|
9905
|
-
return result;
|
|
9906
|
-
}
|
|
9907
|
-
var INVALID_BRANCH_CHARS_RE, UNSAFE_ENV_KEY_RE, VALID_WORKTREE_NAME_RE;
|
|
9908
|
-
var init_policies = __esm(() => {
|
|
9909
|
-
INVALID_BRANCH_CHARS_RE = /[~^:?*\[\]\\]+/g;
|
|
9910
|
-
UNSAFE_ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
9911
|
-
VALID_WORKTREE_NAME_RE = /^[a-z0-9][a-z0-9\-_./]*$/;
|
|
9912
|
-
});
|
|
9913
|
-
|
|
9914
10305
|
// backend/src/services/archive-service.ts
|
|
9915
10306
|
import { resolve as resolve4 } from "path";
|
|
9916
10307
|
function createArchiveState(entries) {
|
|
@@ -16934,8 +17325,8 @@ var init_dist5 = __esm(() => {
|
|
|
16934
17325
|
});
|
|
16935
17326
|
|
|
16936
17327
|
// backend/src/adapters/config.ts
|
|
16937
|
-
import { readFileSync as
|
|
16938
|
-
import { dirname as dirname2, join as
|
|
17328
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
17329
|
+
import { dirname as dirname2, join as join10, resolve as resolve5 } from "path";
|
|
16939
17330
|
function DEFAULT_ONESHOT_SYSTEM_PROMPT() {
|
|
16940
17331
|
return [
|
|
16941
17332
|
"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.",
|
|
@@ -17164,10 +17555,10 @@ function getDefaultProfileName(config) {
|
|
|
17164
17555
|
return Object.keys(config.profiles)[0] ?? "default";
|
|
17165
17556
|
}
|
|
17166
17557
|
function readConfigFile(root) {
|
|
17167
|
-
return
|
|
17558
|
+
return readFileSync7(join10(root, ".webmux.yaml"), "utf8");
|
|
17168
17559
|
}
|
|
17169
17560
|
function readLocalConfigFile(root) {
|
|
17170
|
-
return
|
|
17561
|
+
return readFileSync7(join10(root, ".webmux.local.yaml"), "utf8");
|
|
17171
17562
|
}
|
|
17172
17563
|
function parseConfigDocument(text) {
|
|
17173
17564
|
const parsed = $parse(text);
|
|
@@ -17191,12 +17582,7 @@ function parseProjectConfig(parsed) {
|
|
|
17191
17582
|
linkedRepos: isRecord4(parsed.integrations) && isRecord4(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github.linkedRepos) : isRecord4(parsed.integrations) && Array.isArray(parsed.integrations.github) ? parseLinkedRepos(parsed.integrations.github) : [],
|
|
17192
17583
|
autoRemoveOnMerge: isRecord4(parsed.integrations) && isRecord4(parsed.integrations.github) && typeof parsed.integrations.github.autoRemoveOnMerge === "boolean" ? parsed.integrations.github.autoRemoveOnMerge : DEFAULT_CONFIG.integrations.github.autoRemoveOnMerge
|
|
17193
17584
|
},
|
|
17194
|
-
linear:
|
|
17195
|
-
enabled: isRecord4(parsed.integrations) && isRecord4(parsed.integrations.linear) && typeof parsed.integrations.linear.enabled === "boolean" ? parsed.integrations.linear.enabled : DEFAULT_CONFIG.integrations.linear.enabled,
|
|
17196
|
-
autoCreateWorktrees: isRecord4(parsed.integrations) && isRecord4(parsed.integrations.linear) && typeof parsed.integrations.linear.autoCreateWorktrees === "boolean" ? parsed.integrations.linear.autoCreateWorktrees : DEFAULT_CONFIG.integrations.linear.autoCreateWorktrees,
|
|
17197
|
-
createTicketOption: isRecord4(parsed.integrations) && isRecord4(parsed.integrations.linear) && typeof parsed.integrations.linear.createTicketOption === "boolean" ? parsed.integrations.linear.createTicketOption : DEFAULT_CONFIG.integrations.linear.createTicketOption,
|
|
17198
|
-
...isRecord4(parsed.integrations) && isRecord4(parsed.integrations.linear) && typeof parsed.integrations.linear.teamId === "string" && parsed.integrations.linear.teamId.trim() ? { teamId: parsed.integrations.linear.teamId.trim() } : {}
|
|
17199
|
-
}
|
|
17585
|
+
linear: parseLinearIntegration(parsed)
|
|
17200
17586
|
},
|
|
17201
17587
|
lifecycleHooks: parseLifecycleHooks(parsed.lifecycleHooks),
|
|
17202
17588
|
autoName: parseAutoName(parsed.auto_name),
|
|
@@ -17206,6 +17592,29 @@ function parseProjectConfig(parsed) {
|
|
|
17206
17592
|
function defaultConfig() {
|
|
17207
17593
|
return parseProjectConfig({});
|
|
17208
17594
|
}
|
|
17595
|
+
function parseTeamKeyList(raw) {
|
|
17596
|
+
if (!Array.isArray(raw))
|
|
17597
|
+
return;
|
|
17598
|
+
const keys = raw.filter((entry) => typeof entry === "string").map((entry) => entry.trim().toUpperCase()).filter((entry) => entry.length > 0);
|
|
17599
|
+
return keys.length > 0 ? Array.from(new Set(keys)) : undefined;
|
|
17600
|
+
}
|
|
17601
|
+
function parseLinearIntegration(parsed) {
|
|
17602
|
+
const defaults = DEFAULT_CONFIG.integrations.linear;
|
|
17603
|
+
const linear = isRecord4(parsed.integrations) && isRecord4(parsed.integrations.linear) ? parsed.integrations.linear : null;
|
|
17604
|
+
if (!linear)
|
|
17605
|
+
return { ...defaults };
|
|
17606
|
+
if (typeof linear.teamId === "string" && !warnedLegacyLinearTeamId) {
|
|
17607
|
+
warnedLegacyLinearTeamId = true;
|
|
17608
|
+
log.warn("[config] integrations.linear.teamId is no longer used \u2014 the ticket team is now picked at creation time in the dashboard");
|
|
17609
|
+
}
|
|
17610
|
+
const watchTeams = parseTeamKeyList(linear.watchTeams);
|
|
17611
|
+
return {
|
|
17612
|
+
enabled: typeof linear.enabled === "boolean" ? linear.enabled : defaults.enabled,
|
|
17613
|
+
autoCreateWorktrees: typeof linear.autoCreateWorktrees === "boolean" ? linear.autoCreateWorktrees : defaults.autoCreateWorktrees,
|
|
17614
|
+
createTicketOption: typeof linear.createTicketOption === "boolean" ? linear.createTicketOption : defaults.createTicketOption,
|
|
17615
|
+
...watchTeams ? { watchTeams } : {}
|
|
17616
|
+
};
|
|
17617
|
+
}
|
|
17209
17618
|
function parseLocalLinearOverlay(parsed) {
|
|
17210
17619
|
if (!isRecord4(parsed.integrations))
|
|
17211
17620
|
return null;
|
|
@@ -17219,8 +17628,9 @@ function parseLocalLinearOverlay(parsed) {
|
|
|
17219
17628
|
overlay.autoCreateWorktrees = linear.autoCreateWorktrees;
|
|
17220
17629
|
if (typeof linear.createTicketOption === "boolean")
|
|
17221
17630
|
overlay.createTicketOption = linear.createTicketOption;
|
|
17222
|
-
|
|
17223
|
-
|
|
17631
|
+
const watchTeams = parseTeamKeyList(linear.watchTeams);
|
|
17632
|
+
if (watchTeams)
|
|
17633
|
+
overlay.watchTeams = watchTeams;
|
|
17224
17634
|
return Object.keys(overlay).length > 0 ? overlay : null;
|
|
17225
17635
|
}
|
|
17226
17636
|
function parseLocalGitHubOverlay(parsed) {
|
|
@@ -17337,9 +17747,10 @@ function loadConfig(dir, options = {}) {
|
|
|
17337
17747
|
function expandTemplate(template, env) {
|
|
17338
17748
|
return template.replace(/\$\{(\w+)\}/g, (_, key) => env[key] ?? "");
|
|
17339
17749
|
}
|
|
17340
|
-
var DEFAULT_PANES, DEFAULT_CONFIG;
|
|
17750
|
+
var DEFAULT_PANES, DEFAULT_CONFIG, warnedLegacyLinearTeamId = false;
|
|
17341
17751
|
var init_config = __esm(() => {
|
|
17342
17752
|
init_dist5();
|
|
17753
|
+
init_log();
|
|
17343
17754
|
DEFAULT_PANES = [
|
|
17344
17755
|
{ id: "agent", kind: "agent", focus: true },
|
|
17345
17756
|
{ id: "shell", kind: "shell", split: "right", sizePct: 25 }
|
|
@@ -17652,7 +18063,7 @@ var init_docker = __esm(() => {
|
|
|
17652
18063
|
});
|
|
17653
18064
|
|
|
17654
18065
|
// backend/src/adapters/hooks.ts
|
|
17655
|
-
import { join as
|
|
18066
|
+
import { join as join11 } from "path";
|
|
17656
18067
|
function buildErrorMessage(name, exitCode, stdout, stderr) {
|
|
17657
18068
|
const output = stderr.trim() || stdout.trim();
|
|
17658
18069
|
if (output) {
|
|
@@ -17677,7 +18088,7 @@ class BunLifecycleHookRunner {
|
|
|
17677
18088
|
return this.direnvAvailable;
|
|
17678
18089
|
}
|
|
17679
18090
|
async buildCommand(cwd, command) {
|
|
17680
|
-
if (this.checkDirenv() && await Bun.file(
|
|
18091
|
+
if (this.checkDirenv() && await Bun.file(join11(cwd, ".envrc")).exists()) {
|
|
17681
18092
|
Bun.spawnSync(["direnv", "allow"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
17682
18093
|
return ["direnv", "exec", cwd, "bash", "-c", command];
|
|
17683
18094
|
}
|
|
@@ -18005,7 +18416,7 @@ var init_archive_state_service = __esm(() => {
|
|
|
18005
18416
|
|
|
18006
18417
|
// backend/src/adapters/agent-runtime.ts
|
|
18007
18418
|
import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
|
|
18008
|
-
import { dirname as dirname4, join as
|
|
18419
|
+
import { dirname as dirname4, join as join12, resolve as resolve6 } from "path";
|
|
18009
18420
|
function shellQuote(value) {
|
|
18010
18421
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
18011
18422
|
}
|
|
@@ -18424,7 +18835,7 @@ async function mergeCodexHooksFile(hooksPath, hookSettings, agentCtlPath) {
|
|
|
18424
18835
|
}
|
|
18425
18836
|
async function resolveGitCommonDir(gitDir) {
|
|
18426
18837
|
try {
|
|
18427
|
-
const commonDir = (await Bun.file(
|
|
18838
|
+
const commonDir = (await Bun.file(join12(gitDir, "commondir")).text()).trim();
|
|
18428
18839
|
if (!commonDir)
|
|
18429
18840
|
return gitDir;
|
|
18430
18841
|
return commonDir.startsWith("/") ? commonDir : resolve6(gitDir, commonDir);
|
|
@@ -18434,7 +18845,7 @@ async function resolveGitCommonDir(gitDir) {
|
|
|
18434
18845
|
}
|
|
18435
18846
|
async function ensureGeneratedCodexHooksIgnored(gitDir) {
|
|
18436
18847
|
const commonDir = await resolveGitCommonDir(gitDir);
|
|
18437
|
-
const excludePath =
|
|
18848
|
+
const excludePath = join12(commonDir, "info", "exclude");
|
|
18438
18849
|
let existing = "";
|
|
18439
18850
|
try {
|
|
18440
18851
|
existing = await Bun.file(excludePath).text();
|
|
@@ -18454,9 +18865,9 @@ async function ensureGeneratedCodexHooksIgnored(gitDir) {
|
|
|
18454
18865
|
async function ensureAgentRuntimeArtifacts(input) {
|
|
18455
18866
|
const storagePaths = getWorktreeStoragePaths(input.gitDir);
|
|
18456
18867
|
const artifacts = {
|
|
18457
|
-
agentCtlPath:
|
|
18458
|
-
claudeSettingsPath:
|
|
18459
|
-
codexHooksPath:
|
|
18868
|
+
agentCtlPath: join12(storagePaths.webmuxDir, "webmux-agentctl"),
|
|
18869
|
+
claudeSettingsPath: join12(input.worktreePath, ".claude", "settings.local.json"),
|
|
18870
|
+
codexHooksPath: join12(input.worktreePath, ".codex", "hooks.json")
|
|
18460
18871
|
};
|
|
18461
18872
|
await mkdir3(dirname4(artifacts.claudeSettingsPath), { recursive: true });
|
|
18462
18873
|
await mkdir3(dirname4(artifacts.codexHooksPath), { recursive: true });
|
|
@@ -18490,7 +18901,7 @@ function buildDockerRuntimeBootstrap(runtimeEnvPath) {
|
|
|
18490
18901
|
function buildBuiltInAgentInvocation(input) {
|
|
18491
18902
|
const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
|
|
18492
18903
|
if (input.agent === "codex") {
|
|
18493
|
-
const hooksFlag = " --enable
|
|
18904
|
+
const hooksFlag = " --enable hooks";
|
|
18494
18905
|
const yoloFlag2 = input.yolo ? " --yolo" : "";
|
|
18495
18906
|
if (input.launchMode === "resume") {
|
|
18496
18907
|
return `codex${hooksFlag}${yoloFlag2} resume --last${promptSuffix}`;
|
|
@@ -19955,7 +20366,7 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
19955
20366
|
}
|
|
19956
20367
|
|
|
19957
20368
|
// backend/src/services/reconciliation-service.ts
|
|
19958
|
-
import { basename as
|
|
20369
|
+
import { basename as basename5, resolve as resolve9 } from "path";
|
|
19959
20370
|
function makeUnmanagedWorktreeId(path) {
|
|
19960
20371
|
return `unmanaged:${resolve9(path)}`;
|
|
19961
20372
|
}
|
|
@@ -19990,7 +20401,7 @@ function findWindow(windows, sessionName, branch) {
|
|
|
19990
20401
|
return windows.find((window) => window.sessionName === sessionName && window.windowName === windowName) ?? null;
|
|
19991
20402
|
}
|
|
19992
20403
|
function resolveBranch(entry, metaBranch) {
|
|
19993
|
-
const fallback =
|
|
20404
|
+
const fallback = basename5(entry.path);
|
|
19994
20405
|
return entry.branch ?? metaBranch ?? (fallback.length > 0 ? fallback : "unknown");
|
|
19995
20406
|
}
|
|
19996
20407
|
|
|
@@ -20227,7 +20638,7 @@ __export(exports_worktree_commands, {
|
|
|
20227
20638
|
parseAddCommandArgs: () => parseAddCommandArgs,
|
|
20228
20639
|
getWorktreeCommandUsage: () => getWorktreeCommandUsage
|
|
20229
20640
|
});
|
|
20230
|
-
import { basename as
|
|
20641
|
+
import { basename as basename6, resolve as resolve10 } from "path";
|
|
20231
20642
|
function getWorktreeCommandUsage(command) {
|
|
20232
20643
|
switch (command) {
|
|
20233
20644
|
case "add":
|
|
@@ -20663,7 +21074,7 @@ async function listWorktrees(runtime, stdout, options) {
|
|
|
20663
21074
|
const projectGitDir = runtime.git.resolveWorktreeGitDir(projectDir);
|
|
20664
21075
|
const archivedPaths = buildArchivedWorktreePathSet(await readWorktreeArchiveState(projectGitDir));
|
|
20665
21076
|
const rows = await Promise.all(entries.map(async (entry) => {
|
|
20666
|
-
const branch = entry.branch ??
|
|
21077
|
+
const branch = entry.branch ?? basename6(entry.path);
|
|
20667
21078
|
const isOpen = openWindows.has(buildWorktreeWindowName(branch));
|
|
20668
21079
|
const gitDir = runtime.git.resolveWorktreeGitDir(entry.path);
|
|
20669
21080
|
const meta = await readWorktreeMeta(gitDir);
|
|
@@ -20907,13 +21318,13 @@ var init_worktree_commands = __esm(() => {
|
|
|
20907
21318
|
});
|
|
20908
21319
|
|
|
20909
21320
|
// bin/src/webmux.ts
|
|
20910
|
-
import { resolve as resolve11, dirname as dirname6, join as
|
|
20911
|
-
import { existsSync as
|
|
21321
|
+
import { resolve as resolve11, dirname as dirname6, join as join13 } from "path";
|
|
21322
|
+
import { existsSync as existsSync7 } from "fs";
|
|
20912
21323
|
import { fileURLToPath } from "url";
|
|
20913
21324
|
// package.json
|
|
20914
21325
|
var package_default = {
|
|
20915
21326
|
name: "webmux",
|
|
20916
|
-
version: "0.
|
|
21327
|
+
version: "0.34.0",
|
|
20917
21328
|
description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
|
|
20918
21329
|
type: "module",
|
|
20919
21330
|
repository: {
|
|
@@ -20996,7 +21407,9 @@ Usage:
|
|
|
20996
21407
|
webmux completion Generate shell completion script (bash, zsh)
|
|
20997
21408
|
|
|
20998
21409
|
Options:
|
|
20999
|
-
--port N Set port (default: 5111)
|
|
21410
|
+
--port N Set port (default: 5111). Falls back to a free port when taken.
|
|
21411
|
+
--prefix NAME URL prefix this instance registers under (default: project dir basename).
|
|
21412
|
+
Other webmux instances on this machine will redirect /<NAME> to this port.
|
|
21000
21413
|
--app Open dashboard in browser app mode (minimal window)
|
|
21001
21414
|
--debug Show debug-level logs
|
|
21002
21415
|
--version Show version number
|
|
@@ -21004,18 +21417,21 @@ Options:
|
|
|
21004
21417
|
|
|
21005
21418
|
Environment:
|
|
21006
21419
|
PORT Same as --port (flag takes precedence)
|
|
21420
|
+
WEBMUX_PREFIX Same as --prefix
|
|
21007
21421
|
`);
|
|
21008
21422
|
}
|
|
21009
21423
|
function isRootCommand(value) {
|
|
21010
21424
|
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";
|
|
21011
21425
|
}
|
|
21012
21426
|
function isServeRootOption(value) {
|
|
21013
|
-
return value === "--port" || value === "--app" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
|
|
21427
|
+
return value === "--port" || value === "--prefix" || value === "--app" || value === "--debug" || value === "--help" || value === "-h" || value === "--version" || value === "-V";
|
|
21014
21428
|
}
|
|
21015
21429
|
function parseRootArgs(args) {
|
|
21016
21430
|
let port = parseInt(process.env.PORT || "5111", 10);
|
|
21431
|
+
let portExplicit = process.env.PORT !== undefined;
|
|
21017
21432
|
let debug = false;
|
|
21018
21433
|
let app = false;
|
|
21434
|
+
let prefix = process.env.WEBMUX_PREFIX?.trim() || null;
|
|
21019
21435
|
let command = null;
|
|
21020
21436
|
const commandArgs = [];
|
|
21021
21437
|
for (let index = 0;index < args.length; index++) {
|
|
@@ -21036,6 +21452,16 @@ function parseRootArgs(args) {
|
|
|
21036
21452
|
if (Number.isNaN(port)) {
|
|
21037
21453
|
throw new Error("Error: --port requires a numeric value");
|
|
21038
21454
|
}
|
|
21455
|
+
portExplicit = true;
|
|
21456
|
+
index += 1;
|
|
21457
|
+
break;
|
|
21458
|
+
}
|
|
21459
|
+
case "--prefix": {
|
|
21460
|
+
const value = args[index + 1];
|
|
21461
|
+
if (!value) {
|
|
21462
|
+
throw new Error("Error: --prefix requires a value");
|
|
21463
|
+
}
|
|
21464
|
+
prefix = value.trim();
|
|
21039
21465
|
index += 1;
|
|
21040
21466
|
break;
|
|
21041
21467
|
}
|
|
@@ -21064,8 +21490,10 @@ Run webmux --help for usage.`);
|
|
|
21064
21490
|
}
|
|
21065
21491
|
return {
|
|
21066
21492
|
port,
|
|
21493
|
+
portExplicit,
|
|
21067
21494
|
debug,
|
|
21068
21495
|
app,
|
|
21496
|
+
prefix,
|
|
21069
21497
|
command,
|
|
21070
21498
|
commandArgs
|
|
21071
21499
|
};
|
|
@@ -21074,7 +21502,7 @@ function isWorktreeCommand(command) {
|
|
|
21074
21502
|
return command === "add" || command === "list" || command === "open" || command === "close" || command === "archive" || command === "unarchive" || command === "label" || command === "remove" || command === "merge" || command === "send" || command === "prune";
|
|
21075
21503
|
}
|
|
21076
21504
|
async function loadEnvFile(path) {
|
|
21077
|
-
if (!
|
|
21505
|
+
if (!existsSync7(path))
|
|
21078
21506
|
return;
|
|
21079
21507
|
const lines = (await Bun.file(path).text()).split(`
|
|
21080
21508
|
`);
|
|
@@ -21107,7 +21535,7 @@ function findBrowserBinary() {
|
|
|
21107
21535
|
"brave-browser"
|
|
21108
21536
|
];
|
|
21109
21537
|
for (const candidate of candidates) {
|
|
21110
|
-
const found = candidate.startsWith("/") ?
|
|
21538
|
+
const found = candidate.startsWith("/") ? existsSync7(candidate) : Bun.spawnSync(["which", candidate], { stdout: "pipe", stderr: "pipe" }).success;
|
|
21111
21539
|
if (found)
|
|
21112
21540
|
return candidate;
|
|
21113
21541
|
}
|
|
@@ -21143,7 +21571,7 @@ function pipeWithPrefix(stream, prefix, onTrigger) {
|
|
|
21143
21571
|
console.log(`${prefix} ${line}`);
|
|
21144
21572
|
if (onTrigger && !fired && line.includes(onTrigger.text)) {
|
|
21145
21573
|
fired = true;
|
|
21146
|
-
onTrigger.callback();
|
|
21574
|
+
onTrigger.callback(line);
|
|
21147
21575
|
}
|
|
21148
21576
|
}
|
|
21149
21577
|
}
|
|
@@ -21186,6 +21614,28 @@ async function main(args = process.argv.slice(2)) {
|
|
|
21186
21614
|
stderr: "inherit"
|
|
21187
21615
|
});
|
|
21188
21616
|
const code = await proc.exited;
|
|
21617
|
+
if (code === 0) {
|
|
21618
|
+
const { listInstalledServices: listInstalledServices2, updateInstalledService: updateInstalledService2 } = await Promise.resolve().then(() => (init_service_restart(), exports_service_restart));
|
|
21619
|
+
const services = listInstalledServices2();
|
|
21620
|
+
if (services.length > 0) {
|
|
21621
|
+
const whichResult = Bun.spawnSync(["which", "webmux"], { stdout: "pipe", stderr: "pipe" });
|
|
21622
|
+
const webmuxPath = whichResult.success ? whichResult.stdout.toString().trim() : "";
|
|
21623
|
+
console.log(`
|
|
21624
|
+
Refreshing ${services.length} installed webmux service(s) to pick up the new version...`);
|
|
21625
|
+
for (const svc of services) {
|
|
21626
|
+
const outcome = await updateInstalledService2(svc, webmuxPath);
|
|
21627
|
+
const parts = [];
|
|
21628
|
+
if (outcome.regenerated)
|
|
21629
|
+
parts.push("regenerated unit");
|
|
21630
|
+
if (outcome.restarted)
|
|
21631
|
+
parts.push("restarted");
|
|
21632
|
+
if (!outcome.regenerated && !outcome.restarted && !outcome.error)
|
|
21633
|
+
parts.push("no change");
|
|
21634
|
+
const status2 = outcome.error ? `failed \u2014 ${outcome.error}` : parts.join(", ");
|
|
21635
|
+
console.log(` ${svc.name}: ${status2}`);
|
|
21636
|
+
}
|
|
21637
|
+
}
|
|
21638
|
+
}
|
|
21189
21639
|
process.exit(code);
|
|
21190
21640
|
}
|
|
21191
21641
|
await loadEnvFile(resolve11(process.cwd(), ".env.local"));
|
|
@@ -21214,7 +21664,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
21214
21664
|
usage2();
|
|
21215
21665
|
process.exit(0);
|
|
21216
21666
|
}
|
|
21217
|
-
if (!
|
|
21667
|
+
if (!existsSync7(resolve11(process.cwd(), ".webmux.yaml"))) {
|
|
21218
21668
|
console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
|
|
21219
21669
|
process.exit(1);
|
|
21220
21670
|
}
|
|
@@ -21222,6 +21672,8 @@ async function main(args = process.argv.slice(2)) {
|
|
|
21222
21672
|
...process.env,
|
|
21223
21673
|
PORT: String(parsed.port),
|
|
21224
21674
|
WEBMUX_PROJECT_DIR: process.cwd(),
|
|
21675
|
+
...parsed.portExplicit ? { WEBMUX_PORT_STRICT: "1" } : {},
|
|
21676
|
+
...parsed.prefix ? { WEBMUX_PREFIX: parsed.prefix } : {},
|
|
21225
21677
|
...parsed.debug ? { WEBMUX_DEBUG: "1" } : {}
|
|
21226
21678
|
};
|
|
21227
21679
|
const children = [];
|
|
@@ -21248,13 +21700,13 @@ async function main(args = process.argv.slice(2)) {
|
|
|
21248
21700
|
}
|
|
21249
21701
|
process.on("SIGINT", cleanup);
|
|
21250
21702
|
process.on("SIGTERM", cleanup);
|
|
21251
|
-
const backendEntry =
|
|
21252
|
-
const staticDir =
|
|
21253
|
-
if (!
|
|
21703
|
+
const backendEntry = join13(PKG_ROOT, "backend", "dist", "server.js");
|
|
21704
|
+
const staticDir = join13(PKG_ROOT, "frontend", "dist");
|
|
21705
|
+
if (!existsSync7(staticDir)) {
|
|
21254
21706
|
console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
|
|
21255
21707
|
process.exit(1);
|
|
21256
21708
|
}
|
|
21257
|
-
console.log(`Starting webmux on port ${parsed.port}...`);
|
|
21709
|
+
console.log(parsed.portExplicit ? `Starting webmux on port ${parsed.port}...` : `Starting webmux on port ${parsed.port} (falls back to a free port if taken)...`);
|
|
21258
21710
|
const be2 = Bun.spawn(["bun", backendEntry], {
|
|
21259
21711
|
env: { ...baseEnv, WEBMUX_STATIC_DIR: staticDir },
|
|
21260
21712
|
stdout: "pipe",
|
|
@@ -21264,7 +21716,10 @@ async function main(args = process.argv.slice(2)) {
|
|
|
21264
21716
|
if (parsed.app) {
|
|
21265
21717
|
pipeWithPrefix(be2.stdout, "[BE]", {
|
|
21266
21718
|
text: "Dev Dashboard API running at",
|
|
21267
|
-
callback: () =>
|
|
21719
|
+
callback: (line) => {
|
|
21720
|
+
const match = line.match(/https?:\/\/[^\s]+/);
|
|
21721
|
+
openAppMode(match?.[0] ?? `http://localhost:${parsed.port}`);
|
|
21722
|
+
}
|
|
21268
21723
|
});
|
|
21269
21724
|
} else {
|
|
21270
21725
|
pipeWithPrefix(be2.stdout, "[BE]");
|