webmux 0.33.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/bin/webmux.js
CHANGED
|
@@ -2719,14 +2719,255 @@ ${result.stderr.trim()}` : ""
|
|
|
2719
2719
|
console.log();
|
|
2720
2720
|
});
|
|
2721
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
|
+
|
|
2722
2961
|
// bin/src/service.ts
|
|
2723
2962
|
var exports_service = {};
|
|
2724
2963
|
__export(exports_service, {
|
|
2964
|
+
parseInstalledServiceConfig: () => parseInstalledServiceConfig,
|
|
2965
|
+
generateServiceFile: () => generateServiceFile,
|
|
2725
2966
|
default: () => service
|
|
2726
2967
|
});
|
|
2727
|
-
import { existsSync as
|
|
2728
|
-
import { join as
|
|
2729
|
-
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";
|
|
2730
2971
|
function getPlatform() {
|
|
2731
2972
|
const plat = process.platform;
|
|
2732
2973
|
if (plat === "linux" || plat === "darwin")
|
|
@@ -2755,10 +2996,10 @@ function printRunResult(result) {
|
|
|
2755
2996
|
console.error(err);
|
|
2756
2997
|
}
|
|
2757
2998
|
function systemdUnitPath(serviceName) {
|
|
2758
|
-
return
|
|
2999
|
+
return join7(homedir3(), ".config", "systemd", "user", `${serviceName}.service`);
|
|
2759
3000
|
}
|
|
2760
3001
|
function launchdPlistPath(serviceName) {
|
|
2761
|
-
return
|
|
3002
|
+
return join7(homedir3(), "Library", "LaunchAgents", `com.webmux.${serviceName}.plist`);
|
|
2762
3003
|
}
|
|
2763
3004
|
function serviceFilePath(config) {
|
|
2764
3005
|
if (config.platform === "linux")
|
|
@@ -2784,7 +3025,7 @@ WantedBy=default.target
|
|
|
2784
3025
|
`;
|
|
2785
3026
|
}
|
|
2786
3027
|
function generateLaunchdPlist(config) {
|
|
2787
|
-
const logPath =
|
|
3028
|
+
const logPath = join7(homedir3(), "Library", "Logs", `webmux-${config.serviceName}.log`);
|
|
2788
3029
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
2789
3030
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2790
3031
|
<plist version="1.0">
|
|
@@ -2829,6 +3070,36 @@ function generateServiceFile(config) {
|
|
|
2829
3070
|
return generateSystemdUnit(config);
|
|
2830
3071
|
return generateLaunchdPlist(config);
|
|
2831
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
|
+
}
|
|
2832
3103
|
function installCommands(config) {
|
|
2833
3104
|
if (config.platform === "linux") {
|
|
2834
3105
|
return [
|
|
@@ -2852,11 +3123,12 @@ function uninstallCommands(config) {
|
|
|
2852
3123
|
];
|
|
2853
3124
|
}
|
|
2854
3125
|
function isInstalled(config) {
|
|
2855
|
-
return
|
|
3126
|
+
return existsSync5(serviceFilePath(config));
|
|
2856
3127
|
}
|
|
2857
|
-
async function install(config) {
|
|
3128
|
+
async function install(config, portExplicit) {
|
|
2858
3129
|
const filePath = serviceFilePath(config);
|
|
2859
|
-
|
|
3130
|
+
const alreadyInstalled = isInstalled(config);
|
|
3131
|
+
if (alreadyInstalled) {
|
|
2860
3132
|
const reinstall = await ue({ message: "Service is already installed. Reinstall?" });
|
|
2861
3133
|
if (q(reinstall) || !reinstall) {
|
|
2862
3134
|
R2.info("Aborted.");
|
|
@@ -2866,6 +3138,31 @@ async function install(config) {
|
|
|
2866
3138
|
runCommand(cmd);
|
|
2867
3139
|
}
|
|
2868
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 };
|
|
2869
3166
|
const content = generateServiceFile(config);
|
|
2870
3167
|
const commands = installCommands(config);
|
|
2871
3168
|
Se([
|
|
@@ -2877,12 +3174,16 @@ async function install(config) {
|
|
|
2877
3174
|
...commands.map((c) => ` $ ${formatCommand(c)}`)
|
|
2878
3175
|
].join(`
|
|
2879
3176
|
`), "Install service");
|
|
3177
|
+
if (portNote)
|
|
3178
|
+
R2.info(portNote);
|
|
3179
|
+
if (portWarning)
|
|
3180
|
+
R2.warn(portWarning);
|
|
2880
3181
|
const ok = await ue({ message: "Proceed?" });
|
|
2881
3182
|
if (q(ok) || !ok) {
|
|
2882
3183
|
R2.info("Aborted.");
|
|
2883
3184
|
return;
|
|
2884
3185
|
}
|
|
2885
|
-
|
|
3186
|
+
mkdirSync2(filePath.substring(0, filePath.lastIndexOf("/")), { recursive: true });
|
|
2886
3187
|
await Bun.write(filePath, content);
|
|
2887
3188
|
R2.success(`Wrote ${filePath}`);
|
|
2888
3189
|
for (const cmd of commands) {
|
|
@@ -2932,7 +3233,7 @@ ${result.stderr.toString()}`);
|
|
|
2932
3233
|
R2.success(`$ ${formatCommand(cmd)}`);
|
|
2933
3234
|
}
|
|
2934
3235
|
}
|
|
2935
|
-
|
|
3236
|
+
unlinkSync2(filePath);
|
|
2936
3237
|
R2.success(`Removed ${filePath}`);
|
|
2937
3238
|
R2.success("Service uninstalled.");
|
|
2938
3239
|
}
|
|
@@ -2956,8 +3257,8 @@ function logs(config) {
|
|
|
2956
3257
|
if (config.platform === "linux") {
|
|
2957
3258
|
proc = Bun.spawn(["journalctl", "--user", "-u", config.serviceName, "-f", "--no-pager"], { stdout: "inherit", stderr: "inherit" });
|
|
2958
3259
|
} else {
|
|
2959
|
-
const logPath =
|
|
2960
|
-
if (!
|
|
3260
|
+
const logPath = join7(homedir3(), "Library", "Logs", `webmux-${config.serviceName}.log`);
|
|
3261
|
+
if (!existsSync5(logPath)) {
|
|
2961
3262
|
R2.error(`Log file not found: ${logPath}`);
|
|
2962
3263
|
return;
|
|
2963
3264
|
}
|
|
@@ -2978,6 +3279,12 @@ Usage:
|
|
|
2978
3279
|
webmux service uninstall Stop, disable, and remove the service
|
|
2979
3280
|
webmux service status Show service status
|
|
2980
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.
|
|
2981
3288
|
`);
|
|
2982
3289
|
}
|
|
2983
3290
|
async function service(args) {
|
|
@@ -3013,6 +3320,7 @@ async function service(args) {
|
|
|
3013
3320
|
return;
|
|
3014
3321
|
}
|
|
3015
3322
|
let port = parseInt(process.env.PORT || "5111");
|
|
3323
|
+
let portExplicit = false;
|
|
3016
3324
|
for (let i = 1;i < args.length; i++) {
|
|
3017
3325
|
if (args[i] === "--port" && args[i + 1]) {
|
|
3018
3326
|
const parsed = parseInt(args[++i]);
|
|
@@ -3021,6 +3329,7 @@ async function service(args) {
|
|
|
3021
3329
|
return;
|
|
3022
3330
|
}
|
|
3023
3331
|
port = parsed;
|
|
3332
|
+
portExplicit = true;
|
|
3024
3333
|
}
|
|
3025
3334
|
}
|
|
3026
3335
|
const projectName = detectProjectName(gitRoot2);
|
|
@@ -3035,7 +3344,7 @@ async function service(args) {
|
|
|
3035
3344
|
};
|
|
3036
3345
|
switch (action) {
|
|
3037
3346
|
case "install":
|
|
3038
|
-
await install(config);
|
|
3347
|
+
await install(config, portExplicit);
|
|
3039
3348
|
break;
|
|
3040
3349
|
case "uninstall":
|
|
3041
3350
|
await uninstall(config);
|
|
@@ -3048,9 +3357,141 @@ async function service(args) {
|
|
|
3048
3357
|
break;
|
|
3049
3358
|
}
|
|
3050
3359
|
}
|
|
3360
|
+
var SYSTEMD_WORKDIR_RE, LAUNCHD_WORKDIR_RE;
|
|
3051
3361
|
var init_service = __esm(() => {
|
|
3052
3362
|
init_dist4();
|
|
3053
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");
|
|
3054
3495
|
});
|
|
3055
3496
|
|
|
3056
3497
|
// node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
|
|
@@ -8221,30 +8662,6 @@ var init_src = __esm(() => {
|
|
|
8221
8662
|
init_schemas();
|
|
8222
8663
|
});
|
|
8223
8664
|
|
|
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
8665
|
// backend/src/services/linear-service.ts
|
|
8249
8666
|
function gqlErrorMessage(raw) {
|
|
8250
8667
|
return raw.errors && raw.errors.length > 0 ? raw.errors.map((error) => error.message).join("; ") : null;
|
|
@@ -9562,7 +9979,7 @@ var WORKTREE_META_SCHEMA_VERSION = 1, WORKTREE_ARCHIVE_STATE_VERSION = 1;
|
|
|
9562
9979
|
|
|
9563
9980
|
// backend/src/adapters/fs.ts
|
|
9564
9981
|
import { mkdir } from "fs/promises";
|
|
9565
|
-
import { join as
|
|
9982
|
+
import { join as join9 } from "path";
|
|
9566
9983
|
function stringifyAllocatedPorts(ports) {
|
|
9567
9984
|
const entries = Object.entries(ports).map(([key, value]) => [key, String(value)]);
|
|
9568
9985
|
return Object.fromEntries(entries);
|
|
@@ -9594,25 +10011,25 @@ function parseDotenv(content) {
|
|
|
9594
10011
|
}
|
|
9595
10012
|
async function loadDotenvLocal(worktreePath) {
|
|
9596
10013
|
try {
|
|
9597
|
-
const content = await Bun.file(
|
|
10014
|
+
const content = await Bun.file(join9(worktreePath, ".env.local")).text();
|
|
9598
10015
|
return parseDotenv(content);
|
|
9599
10016
|
} catch {
|
|
9600
10017
|
return {};
|
|
9601
10018
|
}
|
|
9602
10019
|
}
|
|
9603
10020
|
function getWorktreeStoragePaths(gitDir) {
|
|
9604
|
-
const webmuxDir =
|
|
10021
|
+
const webmuxDir = join9(gitDir, "webmux");
|
|
9605
10022
|
return {
|
|
9606
10023
|
gitDir,
|
|
9607
10024
|
webmuxDir,
|
|
9608
|
-
metaPath:
|
|
9609
|
-
runtimeEnvPath:
|
|
9610
|
-
controlEnvPath:
|
|
9611
|
-
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")
|
|
9612
10029
|
};
|
|
9613
10030
|
}
|
|
9614
10031
|
function getProjectArchiveStatePath(gitDir) {
|
|
9615
|
-
return
|
|
10032
|
+
return join9(gitDir, "webmux", "archive.json");
|
|
9616
10033
|
}
|
|
9617
10034
|
async function ensureWorktreeStorageDirs(gitDir) {
|
|
9618
10035
|
const paths = getWorktreeStoragePaths(gitDir);
|
|
@@ -9781,7 +10198,7 @@ var init_fs = __esm(() => {
|
|
|
9781
10198
|
|
|
9782
10199
|
// backend/src/adapters/tmux.ts
|
|
9783
10200
|
import { createHash } from "crypto";
|
|
9784
|
-
import { basename as
|
|
10201
|
+
import { basename as basename4, resolve as resolve3 } from "path";
|
|
9785
10202
|
function runTmux(args) {
|
|
9786
10203
|
const result = Bun.spawnSync(["tmux", ...args], {
|
|
9787
10204
|
stdout: "pipe",
|
|
@@ -9810,7 +10227,7 @@ function sanitizeTmuxNameSegment(value, maxLength = 24) {
|
|
|
9810
10227
|
}
|
|
9811
10228
|
function buildProjectSessionName(projectRoot) {
|
|
9812
10229
|
const resolved = resolve3(projectRoot);
|
|
9813
|
-
const base = sanitizeTmuxNameSegment(
|
|
10230
|
+
const base = sanitizeTmuxNameSegment(basename4(resolved), 18);
|
|
9814
10231
|
const hash = createHash("sha1").update(resolved).digest("hex").slice(0, 8);
|
|
9815
10232
|
return `wm-${base}-${hash}`;
|
|
9816
10233
|
}
|
|
@@ -9885,55 +10302,6 @@ class BunTmuxGateway {
|
|
|
9885
10302
|
}
|
|
9886
10303
|
var init_tmux = () => {};
|
|
9887
10304
|
|
|
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
10305
|
// backend/src/services/archive-service.ts
|
|
9938
10306
|
import { resolve as resolve4 } from "path";
|
|
9939
10307
|
function createArchiveState(entries) {
|
|
@@ -16957,8 +17325,8 @@ var init_dist5 = __esm(() => {
|
|
|
16957
17325
|
});
|
|
16958
17326
|
|
|
16959
17327
|
// backend/src/adapters/config.ts
|
|
16960
|
-
import { readFileSync as
|
|
16961
|
-
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";
|
|
16962
17330
|
function DEFAULT_ONESHOT_SYSTEM_PROMPT() {
|
|
16963
17331
|
return [
|
|
16964
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.",
|
|
@@ -17187,10 +17555,10 @@ function getDefaultProfileName(config) {
|
|
|
17187
17555
|
return Object.keys(config.profiles)[0] ?? "default";
|
|
17188
17556
|
}
|
|
17189
17557
|
function readConfigFile(root) {
|
|
17190
|
-
return
|
|
17558
|
+
return readFileSync7(join10(root, ".webmux.yaml"), "utf8");
|
|
17191
17559
|
}
|
|
17192
17560
|
function readLocalConfigFile(root) {
|
|
17193
|
-
return
|
|
17561
|
+
return readFileSync7(join10(root, ".webmux.local.yaml"), "utf8");
|
|
17194
17562
|
}
|
|
17195
17563
|
function parseConfigDocument(text) {
|
|
17196
17564
|
const parsed = $parse(text);
|
|
@@ -17695,7 +18063,7 @@ var init_docker = __esm(() => {
|
|
|
17695
18063
|
});
|
|
17696
18064
|
|
|
17697
18065
|
// backend/src/adapters/hooks.ts
|
|
17698
|
-
import { join as
|
|
18066
|
+
import { join as join11 } from "path";
|
|
17699
18067
|
function buildErrorMessage(name, exitCode, stdout, stderr) {
|
|
17700
18068
|
const output = stderr.trim() || stdout.trim();
|
|
17701
18069
|
if (output) {
|
|
@@ -17720,7 +18088,7 @@ class BunLifecycleHookRunner {
|
|
|
17720
18088
|
return this.direnvAvailable;
|
|
17721
18089
|
}
|
|
17722
18090
|
async buildCommand(cwd, command) {
|
|
17723
|
-
if (this.checkDirenv() && await Bun.file(
|
|
18091
|
+
if (this.checkDirenv() && await Bun.file(join11(cwd, ".envrc")).exists()) {
|
|
17724
18092
|
Bun.spawnSync(["direnv", "allow"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
17725
18093
|
return ["direnv", "exec", cwd, "bash", "-c", command];
|
|
17726
18094
|
}
|
|
@@ -18048,7 +18416,7 @@ var init_archive_state_service = __esm(() => {
|
|
|
18048
18416
|
|
|
18049
18417
|
// backend/src/adapters/agent-runtime.ts
|
|
18050
18418
|
import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
|
|
18051
|
-
import { dirname as dirname4, join as
|
|
18419
|
+
import { dirname as dirname4, join as join12, resolve as resolve6 } from "path";
|
|
18052
18420
|
function shellQuote(value) {
|
|
18053
18421
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
18054
18422
|
}
|
|
@@ -18467,7 +18835,7 @@ async function mergeCodexHooksFile(hooksPath, hookSettings, agentCtlPath) {
|
|
|
18467
18835
|
}
|
|
18468
18836
|
async function resolveGitCommonDir(gitDir) {
|
|
18469
18837
|
try {
|
|
18470
|
-
const commonDir = (await Bun.file(
|
|
18838
|
+
const commonDir = (await Bun.file(join12(gitDir, "commondir")).text()).trim();
|
|
18471
18839
|
if (!commonDir)
|
|
18472
18840
|
return gitDir;
|
|
18473
18841
|
return commonDir.startsWith("/") ? commonDir : resolve6(gitDir, commonDir);
|
|
@@ -18477,7 +18845,7 @@ async function resolveGitCommonDir(gitDir) {
|
|
|
18477
18845
|
}
|
|
18478
18846
|
async function ensureGeneratedCodexHooksIgnored(gitDir) {
|
|
18479
18847
|
const commonDir = await resolveGitCommonDir(gitDir);
|
|
18480
|
-
const excludePath =
|
|
18848
|
+
const excludePath = join12(commonDir, "info", "exclude");
|
|
18481
18849
|
let existing = "";
|
|
18482
18850
|
try {
|
|
18483
18851
|
existing = await Bun.file(excludePath).text();
|
|
@@ -18497,9 +18865,9 @@ async function ensureGeneratedCodexHooksIgnored(gitDir) {
|
|
|
18497
18865
|
async function ensureAgentRuntimeArtifacts(input) {
|
|
18498
18866
|
const storagePaths = getWorktreeStoragePaths(input.gitDir);
|
|
18499
18867
|
const artifacts = {
|
|
18500
|
-
agentCtlPath:
|
|
18501
|
-
claudeSettingsPath:
|
|
18502
|
-
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")
|
|
18503
18871
|
};
|
|
18504
18872
|
await mkdir3(dirname4(artifacts.claudeSettingsPath), { recursive: true });
|
|
18505
18873
|
await mkdir3(dirname4(artifacts.codexHooksPath), { recursive: true });
|
|
@@ -18533,7 +18901,7 @@ function buildDockerRuntimeBootstrap(runtimeEnvPath) {
|
|
|
18533
18901
|
function buildBuiltInAgentInvocation(input) {
|
|
18534
18902
|
const promptSuffix = input.prompt ? ` -- ${quoteShell(input.prompt)}` : "";
|
|
18535
18903
|
if (input.agent === "codex") {
|
|
18536
|
-
const hooksFlag = " --enable
|
|
18904
|
+
const hooksFlag = " --enable hooks";
|
|
18537
18905
|
const yoloFlag2 = input.yolo ? " --yolo" : "";
|
|
18538
18906
|
if (input.launchMode === "resume") {
|
|
18539
18907
|
return `codex${hooksFlag}${yoloFlag2} resume --last${promptSuffix}`;
|
|
@@ -19998,7 +20366,7 @@ async function mapWithConcurrency(items, limit, fn) {
|
|
|
19998
20366
|
}
|
|
19999
20367
|
|
|
20000
20368
|
// backend/src/services/reconciliation-service.ts
|
|
20001
|
-
import { basename as
|
|
20369
|
+
import { basename as basename5, resolve as resolve9 } from "path";
|
|
20002
20370
|
function makeUnmanagedWorktreeId(path) {
|
|
20003
20371
|
return `unmanaged:${resolve9(path)}`;
|
|
20004
20372
|
}
|
|
@@ -20033,7 +20401,7 @@ function findWindow(windows, sessionName, branch) {
|
|
|
20033
20401
|
return windows.find((window) => window.sessionName === sessionName && window.windowName === windowName) ?? null;
|
|
20034
20402
|
}
|
|
20035
20403
|
function resolveBranch(entry, metaBranch) {
|
|
20036
|
-
const fallback =
|
|
20404
|
+
const fallback = basename5(entry.path);
|
|
20037
20405
|
return entry.branch ?? metaBranch ?? (fallback.length > 0 ? fallback : "unknown");
|
|
20038
20406
|
}
|
|
20039
20407
|
|
|
@@ -20270,7 +20638,7 @@ __export(exports_worktree_commands, {
|
|
|
20270
20638
|
parseAddCommandArgs: () => parseAddCommandArgs,
|
|
20271
20639
|
getWorktreeCommandUsage: () => getWorktreeCommandUsage
|
|
20272
20640
|
});
|
|
20273
|
-
import { basename as
|
|
20641
|
+
import { basename as basename6, resolve as resolve10 } from "path";
|
|
20274
20642
|
function getWorktreeCommandUsage(command) {
|
|
20275
20643
|
switch (command) {
|
|
20276
20644
|
case "add":
|
|
@@ -20706,7 +21074,7 @@ async function listWorktrees(runtime, stdout, options) {
|
|
|
20706
21074
|
const projectGitDir = runtime.git.resolveWorktreeGitDir(projectDir);
|
|
20707
21075
|
const archivedPaths = buildArchivedWorktreePathSet(await readWorktreeArchiveState(projectGitDir));
|
|
20708
21076
|
const rows = await Promise.all(entries.map(async (entry) => {
|
|
20709
|
-
const branch = entry.branch ??
|
|
21077
|
+
const branch = entry.branch ?? basename6(entry.path);
|
|
20710
21078
|
const isOpen = openWindows.has(buildWorktreeWindowName(branch));
|
|
20711
21079
|
const gitDir = runtime.git.resolveWorktreeGitDir(entry.path);
|
|
20712
21080
|
const meta = await readWorktreeMeta(gitDir);
|
|
@@ -20950,13 +21318,13 @@ var init_worktree_commands = __esm(() => {
|
|
|
20950
21318
|
});
|
|
20951
21319
|
|
|
20952
21320
|
// bin/src/webmux.ts
|
|
20953
|
-
import { resolve as resolve11, dirname as dirname6, join as
|
|
20954
|
-
import { existsSync as
|
|
21321
|
+
import { resolve as resolve11, dirname as dirname6, join as join13 } from "path";
|
|
21322
|
+
import { existsSync as existsSync7 } from "fs";
|
|
20955
21323
|
import { fileURLToPath } from "url";
|
|
20956
21324
|
// package.json
|
|
20957
21325
|
var package_default = {
|
|
20958
21326
|
name: "webmux",
|
|
20959
|
-
version: "0.
|
|
21327
|
+
version: "0.34.0",
|
|
20960
21328
|
description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
|
|
20961
21329
|
type: "module",
|
|
20962
21330
|
repository: {
|
|
@@ -21060,6 +21428,7 @@ function isServeRootOption(value) {
|
|
|
21060
21428
|
}
|
|
21061
21429
|
function parseRootArgs(args) {
|
|
21062
21430
|
let port = parseInt(process.env.PORT || "5111", 10);
|
|
21431
|
+
let portExplicit = process.env.PORT !== undefined;
|
|
21063
21432
|
let debug = false;
|
|
21064
21433
|
let app = false;
|
|
21065
21434
|
let prefix = process.env.WEBMUX_PREFIX?.trim() || null;
|
|
@@ -21083,6 +21452,7 @@ function parseRootArgs(args) {
|
|
|
21083
21452
|
if (Number.isNaN(port)) {
|
|
21084
21453
|
throw new Error("Error: --port requires a numeric value");
|
|
21085
21454
|
}
|
|
21455
|
+
portExplicit = true;
|
|
21086
21456
|
index += 1;
|
|
21087
21457
|
break;
|
|
21088
21458
|
}
|
|
@@ -21120,6 +21490,7 @@ Run webmux --help for usage.`);
|
|
|
21120
21490
|
}
|
|
21121
21491
|
return {
|
|
21122
21492
|
port,
|
|
21493
|
+
portExplicit,
|
|
21123
21494
|
debug,
|
|
21124
21495
|
app,
|
|
21125
21496
|
prefix,
|
|
@@ -21131,7 +21502,7 @@ function isWorktreeCommand(command) {
|
|
|
21131
21502
|
return command === "add" || command === "list" || command === "open" || command === "close" || command === "archive" || command === "unarchive" || command === "label" || command === "remove" || command === "merge" || command === "send" || command === "prune";
|
|
21132
21503
|
}
|
|
21133
21504
|
async function loadEnvFile(path) {
|
|
21134
|
-
if (!
|
|
21505
|
+
if (!existsSync7(path))
|
|
21135
21506
|
return;
|
|
21136
21507
|
const lines = (await Bun.file(path).text()).split(`
|
|
21137
21508
|
`);
|
|
@@ -21164,7 +21535,7 @@ function findBrowserBinary() {
|
|
|
21164
21535
|
"brave-browser"
|
|
21165
21536
|
];
|
|
21166
21537
|
for (const candidate of candidates) {
|
|
21167
|
-
const found = candidate.startsWith("/") ?
|
|
21538
|
+
const found = candidate.startsWith("/") ? existsSync7(candidate) : Bun.spawnSync(["which", candidate], { stdout: "pipe", stderr: "pipe" }).success;
|
|
21168
21539
|
if (found)
|
|
21169
21540
|
return candidate;
|
|
21170
21541
|
}
|
|
@@ -21243,6 +21614,28 @@ async function main(args = process.argv.slice(2)) {
|
|
|
21243
21614
|
stderr: "inherit"
|
|
21244
21615
|
});
|
|
21245
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
|
+
}
|
|
21246
21639
|
process.exit(code);
|
|
21247
21640
|
}
|
|
21248
21641
|
await loadEnvFile(resolve11(process.cwd(), ".env.local"));
|
|
@@ -21271,7 +21664,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
21271
21664
|
usage2();
|
|
21272
21665
|
process.exit(0);
|
|
21273
21666
|
}
|
|
21274
|
-
if (!
|
|
21667
|
+
if (!existsSync7(resolve11(process.cwd(), ".webmux.yaml"))) {
|
|
21275
21668
|
console.error("No .webmux.yaml found in this directory.\nRun `webmux init` to set up your project.");
|
|
21276
21669
|
process.exit(1);
|
|
21277
21670
|
}
|
|
@@ -21279,6 +21672,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
21279
21672
|
...process.env,
|
|
21280
21673
|
PORT: String(parsed.port),
|
|
21281
21674
|
WEBMUX_PROJECT_DIR: process.cwd(),
|
|
21675
|
+
...parsed.portExplicit ? { WEBMUX_PORT_STRICT: "1" } : {},
|
|
21282
21676
|
...parsed.prefix ? { WEBMUX_PREFIX: parsed.prefix } : {},
|
|
21283
21677
|
...parsed.debug ? { WEBMUX_DEBUG: "1" } : {}
|
|
21284
21678
|
};
|
|
@@ -21306,13 +21700,13 @@ async function main(args = process.argv.slice(2)) {
|
|
|
21306
21700
|
}
|
|
21307
21701
|
process.on("SIGINT", cleanup);
|
|
21308
21702
|
process.on("SIGTERM", cleanup);
|
|
21309
|
-
const backendEntry =
|
|
21310
|
-
const staticDir =
|
|
21311
|
-
if (!
|
|
21703
|
+
const backendEntry = join13(PKG_ROOT, "backend", "dist", "server.js");
|
|
21704
|
+
const staticDir = join13(PKG_ROOT, "frontend", "dist");
|
|
21705
|
+
if (!existsSync7(staticDir)) {
|
|
21312
21706
|
console.error(`Error: frontend/dist/ not found. Run 'bun run build' first.`);
|
|
21313
21707
|
process.exit(1);
|
|
21314
21708
|
}
|
|
21315
|
-
console.log(`Starting webmux on port ${parsed.port} (falls back to a free port if taken)...`);
|
|
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)...`);
|
|
21316
21710
|
const be2 = Bun.spawn(["bun", backendEntry], {
|
|
21317
21711
|
env: { ...baseEnv, WEBMUX_STATIC_DIR: staticDir },
|
|
21318
21712
|
stdout: "pipe",
|