webmux 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -34,6 +34,12 @@ Track dev server ports across worktrees. webmux polls configured services and sh
|
|
|
34
34
|
|
|
35
35
|
Run agents in isolated Docker containers for untrusted or experimental work. webmux manages the container lifecycle, port forwarding, and volume mounts automatically.
|
|
36
36
|
|
|
37
|
+
### Linear Integration
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
See your assigned Linear issues alongside your worktrees. Webmux matches branches to issues automatically, so you can browse your backlog, pick an issue, and spin up a worktree for it in one click.
|
|
42
|
+
|
|
37
43
|
## Quick Start
|
|
38
44
|
|
|
39
45
|
```bash
|
package/backend/dist/server.js
CHANGED
|
@@ -6905,7 +6905,7 @@ var require_public_api = __commonJS((exports) => {
|
|
|
6905
6905
|
});
|
|
6906
6906
|
|
|
6907
6907
|
// backend/src/server.ts
|
|
6908
|
-
import { join as
|
|
6908
|
+
import { join as join6, resolve as resolve5 } from "path";
|
|
6909
6909
|
import { networkInterfaces } from "os";
|
|
6910
6910
|
|
|
6911
6911
|
// backend/src/lib/log.ts
|
|
@@ -7931,6 +7931,7 @@ async function removeContainer(branch) {
|
|
|
7931
7931
|
}
|
|
7932
7932
|
|
|
7933
7933
|
// backend/src/adapters/hooks.ts
|
|
7934
|
+
import { join as join2 } from "path";
|
|
7934
7935
|
function buildErrorMessage(name, exitCode, stdout, stderr) {
|
|
7935
7936
|
const output = stderr.trim() || stdout.trim();
|
|
7936
7937
|
if (output) {
|
|
@@ -7938,10 +7939,30 @@ function buildErrorMessage(name, exitCode, stdout, stderr) {
|
|
|
7938
7939
|
}
|
|
7939
7940
|
return `${name} hook failed (exit ${exitCode})`;
|
|
7940
7941
|
}
|
|
7942
|
+
function hasDirenv() {
|
|
7943
|
+
return Bun.spawnSync(["direnv", "version"], { stdout: "pipe", stderr: "pipe" }).exitCode === 0;
|
|
7944
|
+
}
|
|
7941
7945
|
|
|
7942
7946
|
class BunLifecycleHookRunner {
|
|
7947
|
+
direnvAvailable = null;
|
|
7948
|
+
checkDirenv() {
|
|
7949
|
+
if (this.direnvAvailable === null) {
|
|
7950
|
+
this.direnvAvailable = hasDirenv();
|
|
7951
|
+
}
|
|
7952
|
+
return this.direnvAvailable;
|
|
7953
|
+
}
|
|
7954
|
+
async buildCommand(cwd, command) {
|
|
7955
|
+
if (this.checkDirenv() && await Bun.file(join2(cwd, ".envrc")).exists()) {
|
|
7956
|
+
Bun.spawnSync(["direnv", "allow"], { cwd, stdout: "pipe", stderr: "pipe" });
|
|
7957
|
+
return ["direnv", "exec", cwd, "bash", "-c", command];
|
|
7958
|
+
}
|
|
7959
|
+
return ["bash", "-c", command];
|
|
7960
|
+
}
|
|
7943
7961
|
async run(input) {
|
|
7944
|
-
const
|
|
7962
|
+
const cmd = await this.buildCommand(input.cwd, input.command);
|
|
7963
|
+
console.debug(`[hook-runner] Spawning: ${cmd.join(" ")} cwd=${input.cwd}`);
|
|
7964
|
+
console.debug(`[hook-runner] Env keys: ${Object.keys(input.env).join(", ")}`);
|
|
7965
|
+
const proc = Bun.spawn(cmd, {
|
|
7945
7966
|
cwd: input.cwd,
|
|
7946
7967
|
env: {
|
|
7947
7968
|
...Bun.env,
|
|
@@ -7955,6 +7976,11 @@ class BunLifecycleHookRunner {
|
|
|
7955
7976
|
new Response(proc.stdout).text(),
|
|
7956
7977
|
new Response(proc.stderr).text()
|
|
7957
7978
|
]);
|
|
7979
|
+
console.debug(`[hook-runner] ${input.name} exitCode=${exitCode}`);
|
|
7980
|
+
if (stdout.trim())
|
|
7981
|
+
console.debug(`[hook-runner] stdout: ${stdout.trim()}`);
|
|
7982
|
+
if (stderr.trim())
|
|
7983
|
+
console.debug(`[hook-runner] stderr: ${stderr.trim()}`);
|
|
7958
7984
|
if (exitCode !== 0) {
|
|
7959
7985
|
throw new Error(buildErrorMessage(input.name, exitCode, stdout, stderr));
|
|
7960
7986
|
}
|
|
@@ -8678,11 +8704,11 @@ import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
|
8678
8704
|
|
|
8679
8705
|
// backend/src/adapters/agent-runtime.ts
|
|
8680
8706
|
import { chmod as chmod2, mkdir as mkdir3 } from "fs/promises";
|
|
8681
|
-
import { dirname as dirname2, join as
|
|
8707
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
8682
8708
|
|
|
8683
8709
|
// backend/src/adapters/fs.ts
|
|
8684
8710
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
8685
|
-
import { join as
|
|
8711
|
+
import { join as join3 } from "path";
|
|
8686
8712
|
var SAFE_ENV_VALUE_RE = /^[A-Za-z0-9_./:@%+=,-]+$/;
|
|
8687
8713
|
function stringifyAllocatedPorts(ports) {
|
|
8688
8714
|
const entries = Object.entries(ports).map(([key, value]) => [key, String(value)]);
|
|
@@ -8694,14 +8720,14 @@ function quoteEnvValue(value) {
|
|
|
8694
8720
|
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
8695
8721
|
}
|
|
8696
8722
|
function getWorktreeStoragePaths(gitDir) {
|
|
8697
|
-
const webmuxDir =
|
|
8723
|
+
const webmuxDir = join3(gitDir, "webmux");
|
|
8698
8724
|
return {
|
|
8699
8725
|
gitDir,
|
|
8700
8726
|
webmuxDir,
|
|
8701
|
-
metaPath:
|
|
8702
|
-
runtimeEnvPath:
|
|
8703
|
-
controlEnvPath:
|
|
8704
|
-
prsPath:
|
|
8727
|
+
metaPath: join3(webmuxDir, "meta.json"),
|
|
8728
|
+
runtimeEnvPath: join3(webmuxDir, "runtime.env"),
|
|
8729
|
+
controlEnvPath: join3(webmuxDir, "control.env"),
|
|
8730
|
+
prsPath: join3(webmuxDir, "prs.json")
|
|
8705
8731
|
};
|
|
8706
8732
|
}
|
|
8707
8733
|
async function ensureWorktreeStorageDirs(gitDir) {
|
|
@@ -9053,8 +9079,8 @@ async function mergeClaudeSettings(settingsPath, hookSettings) {
|
|
|
9053
9079
|
async function ensureAgentRuntimeArtifacts(input) {
|
|
9054
9080
|
const storagePaths = getWorktreeStoragePaths(input.gitDir);
|
|
9055
9081
|
const artifacts = {
|
|
9056
|
-
agentCtlPath:
|
|
9057
|
-
claudeSettingsPath:
|
|
9082
|
+
agentCtlPath: join4(storagePaths.webmuxDir, "webmux-agentctl"),
|
|
9083
|
+
claudeSettingsPath: join4(input.worktreePath, ".claude", "settings.local.json")
|
|
9058
9084
|
};
|
|
9059
9085
|
await mkdir3(dirname2(artifacts.claudeSettingsPath), { recursive: true });
|
|
9060
9086
|
await Bun.write(artifacts.agentCtlPath, buildAgentCtlScript());
|
|
@@ -9710,9 +9736,12 @@ class LifecycleService {
|
|
|
9710
9736
|
await this.deps.reconciliation.reconcile(this.deps.projectRoot);
|
|
9711
9737
|
}
|
|
9712
9738
|
async runLifecycleHook(input) {
|
|
9739
|
+
console.debug(`[lifecycle-hook] name=${input.name} command=${input.command ?? "UNDEFINED"} meta=${input.meta ? "present" : "NULL"} cwd=${input.worktreePath}`);
|
|
9713
9740
|
if (!input.command || !input.meta) {
|
|
9741
|
+
console.debug(`[lifecycle-hook] SKIPPING ${input.name}: command=${!!input.command} meta=${!!input.meta}`);
|
|
9714
9742
|
return;
|
|
9715
9743
|
}
|
|
9744
|
+
console.debug(`[lifecycle-hook] RUNNING ${input.name}: ${input.command} in ${input.worktreePath}`);
|
|
9716
9745
|
await this.deps.hooks.run({
|
|
9717
9746
|
name: input.name,
|
|
9718
9747
|
command: input.command,
|
|
@@ -9721,6 +9750,7 @@ class LifecycleService {
|
|
|
9721
9750
|
WEBMUX_WORKTREE_PATH: input.worktreePath
|
|
9722
9751
|
})
|
|
9723
9752
|
});
|
|
9753
|
+
console.debug(`[lifecycle-hook] COMPLETED ${input.name}`);
|
|
9724
9754
|
}
|
|
9725
9755
|
wrapOperationError(error) {
|
|
9726
9756
|
if (error instanceof LifecycleError) {
|
|
@@ -10839,7 +10869,7 @@ Bun.serve({
|
|
|
10839
10869
|
if (STATIC_DIR) {
|
|
10840
10870
|
const url = new URL(req.url);
|
|
10841
10871
|
const rawPath = url.pathname === "/" ? "index.html" : url.pathname;
|
|
10842
|
-
const filePath =
|
|
10872
|
+
const filePath = join6(STATIC_DIR, rawPath);
|
|
10843
10873
|
const staticRoot = resolve5(STATIC_DIR);
|
|
10844
10874
|
if (!resolve5(filePath).startsWith(staticRoot + "/")) {
|
|
10845
10875
|
return new Response("Forbidden", { status: 403 });
|
|
@@ -10849,7 +10879,7 @@ Bun.serve({
|
|
|
10849
10879
|
const headers = rawPath.startsWith("/assets/") ? { "Cache-Control": "public, max-age=31536000, immutable" } : {};
|
|
10850
10880
|
return new Response(file, { headers });
|
|
10851
10881
|
}
|
|
10852
|
-
return new Response(Bun.file(
|
|
10882
|
+
return new Response(Bun.file(join6(STATIC_DIR, "index.html")), {
|
|
10853
10883
|
headers: { "Cache-Control": "no-cache" }
|
|
10854
10884
|
});
|
|
10855
10885
|
}
|
package/bin/webmux.js
CHANGED
|
@@ -1557,7 +1557,7 @@ function createAgentStreamPrinter(label) {
|
|
|
1557
1557
|
}
|
|
1558
1558
|
};
|
|
1559
1559
|
}
|
|
1560
|
-
var deps, gitRoot, missing, webmuxYaml;
|
|
1560
|
+
var deps, gitRoot, missing, MIN_BUN_VERSION = "1.3.5", bunVersionResult, webmuxYaml;
|
|
1561
1561
|
var init_init = __esm(async () => {
|
|
1562
1562
|
init_dist2();
|
|
1563
1563
|
init_shared();
|
|
@@ -1589,6 +1589,19 @@ var init_init = __esm(async () => {
|
|
|
1589
1589
|
Gt("Setup incomplete.");
|
|
1590
1590
|
process.exit(1);
|
|
1591
1591
|
}
|
|
1592
|
+
bunVersionResult = run("bun", ["--version"]);
|
|
1593
|
+
if (bunVersionResult.success) {
|
|
1594
|
+
const bunVersion = bunVersionResult.stdout.toString().trim();
|
|
1595
|
+
const [major, minor, patch] = bunVersion.split(".").map(Number);
|
|
1596
|
+
const [reqMajor, reqMinor, reqPatch] = MIN_BUN_VERSION.split(".").map(Number);
|
|
1597
|
+
const tooOld = major < reqMajor || major === reqMajor && minor < reqMinor || major === reqMajor && minor === reqMinor && patch < reqPatch;
|
|
1598
|
+
if (tooOld) {
|
|
1599
|
+
R2.error(`Bun ${bunVersion} is too old. webmux requires Bun >= ${MIN_BUN_VERSION}.`);
|
|
1600
|
+
R2.info("Upgrade with: bun upgrade");
|
|
1601
|
+
Gt("Setup incomplete.");
|
|
1602
|
+
process.exit(1);
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1592
1605
|
if (which("gh")) {
|
|
1593
1606
|
const ghAuth = run("gh", ["auth", "status"]);
|
|
1594
1607
|
if (!ghAuth.success) {
|