Skip to content

Commit b794b1e

Browse files
authored
feat(mux): add package_manager and registry_url variables (#761)
## Summary Add two new customization variables to the Mux module so users can control how Mux is installed: ### `package_manager` (default: `"auto"`) Choose which Node package manager installs Mux: - **`auto`** (default) — auto-detects `npm` → `pnpm` → `bun` in order, falling back to a direct tarball download when none is available - **`npm`**, **`pnpm`**, **`bun`** — force a specific package manager (fails if not found on PATH) ### `registry_url` (default: `"https://registry.npmjs.org"`) Override the npm registry URL for private registries or mirrors. All previously hardcoded `registry.npmjs.org` references have been replaced with this variable. The `--registry` flag is passed to whichever package manager is used, and the tarball fallback path also uses it. ## Changes | File | What changed | |---|---| | `main.tf` | Added `package_manager` and `registry_url` variables with validation; pass both to template | | `run.sh` | Rewrote install logic: PM auto-detection loop, `case`/`esac` dispatch with PM-specific flags, replaced all hardcoded registry URLs with `${REGISTRY_URL}` | | `mux.tftest.hcl` | Added 6 new test cases: PM selection (npm/pnpm/bun), invalid PM validation, custom registry URL, trailing-slash stripping | | `main.test.ts` | Updated expected log messages to match new generic wording | | `README.md` | Updated description, added Custom Package Manager and Custom Registry examples, updated Notes section | ## Version Bumped **1.2.0 → 1.3.0** (minor: new backward-compatible features). ## Validation - ✅ `terraform validate` — clean - ✅ `terraform test` — **15 passed, 0 failed** - ✅ `terraform fmt` — clean --- Generated with [Mux](https://mux.coder.com) using Claude
1 parent 94e41d3 commit b794b1e

5 files changed

Lines changed: 200 additions & 22 deletions

File tree

registry/coder/modules/mux/README.md

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ tags: [ai, agents, development, multiplexer]
88

99
# Mux
1010

11-
Automatically install and run [Mux](https://github.com/coder/mux) in a Coder workspace. By default, the module installs `mux@next` from npm (with a fallback to downloading the npm tarball if npm is unavailable). Mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces.
11+
Automatically install and run [Mux](https://github.com/coder/mux) in a Coder workspace. By default, the module auto-detects an available package manager (`npm`, `pnpm`, or `bun`) to install `mux@next` (with a fallback to downloading the npm tarball if none is found). You can also force a specific package manager via `package_manager` and point to a custom registry with `registry_url`. Mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces.
1212

1313
```tf
1414
module "mux" {
1515
count = data.coder_workspace.me.start_count
1616
source = "registry.coder.com/coder/mux/coder"
17-
version = "1.2.0"
17+
version = "1.3.0"
1818
agent_id = coder_agent.main.id
1919
}
2020
```
@@ -37,7 +37,7 @@ module "mux" {
3737
module "mux" {
3838
count = data.coder_workspace.me.start_count
3939
source = "registry.coder.com/coder/mux/coder"
40-
version = "1.2.0"
40+
version = "1.3.0"
4141
agent_id = coder_agent.main.id
4242
}
4343
```
@@ -48,7 +48,7 @@ module "mux" {
4848
module "mux" {
4949
count = data.coder_workspace.me.start_count
5050
source = "registry.coder.com/coder/mux/coder"
51-
version = "1.2.0"
51+
version = "1.3.0"
5252
agent_id = coder_agent.main.id
5353
# Default is "latest"; set to a specific version to pin
5454
install_version = "0.4.0"
@@ -63,7 +63,7 @@ Start Mux with `mux server --add-project /path/to/project`:
6363
module "mux" {
6464
count = data.coder_workspace.me.start_count
6565
source = "registry.coder.com/coder/mux/coder"
66-
version = "1.2.0"
66+
version = "1.3.0"
6767
agent_id = coder_agent.main.id
6868
add-project = "/path/to/project"
6969
}
@@ -78,7 +78,7 @@ The module parses quoted values, so grouped arguments remain intact.
7878
module "mux" {
7979
count = data.coder_workspace.me.start_count
8080
source = "registry.coder.com/coder/mux/coder"
81-
version = "1.2.0"
81+
version = "1.3.0"
8282
agent_id = coder_agent.main.id
8383
additional_arguments = "--open-mode pinned --add-project '/workspaces/my repo'"
8484
}
@@ -90,12 +90,40 @@ module "mux" {
9090
module "mux" {
9191
count = data.coder_workspace.me.start_count
9292
source = "registry.coder.com/coder/mux/coder"
93-
version = "1.2.0"
93+
version = "1.3.0"
9494
agent_id = coder_agent.main.id
9595
port = 8080
9696
}
9797
```
9898

99+
### Custom Package Manager
100+
101+
Force a specific package manager instead of auto-detection:
102+
103+
```tf
104+
module "mux" {
105+
count = data.coder_workspace.me.start_count
106+
source = "registry.coder.com/coder/mux/coder"
107+
version = "1.3.0"
108+
agent_id = coder_agent.main.id
109+
package_manager = "pnpm" # or "npm", "bun"
110+
}
111+
```
112+
113+
### Custom Registry
114+
115+
Use a private or mirrored npm registry:
116+
117+
```tf
118+
module "mux" {
119+
count = data.coder_workspace.me.start_count
120+
source = "registry.coder.com/coder/mux/coder"
121+
version = "1.3.0"
122+
agent_id = coder_agent.main.id
123+
registry_url = "https://npm.pkg.github.com"
124+
}
125+
```
126+
99127
### Use Cached Installation
100128

101129
Run an existing copy of Mux if found, otherwise install from npm:
@@ -104,7 +132,7 @@ Run an existing copy of Mux if found, otherwise install from npm:
104132
module "mux" {
105133
count = data.coder_workspace.me.start_count
106134
source = "registry.coder.com/coder/mux/coder"
107-
version = "1.2.0"
135+
version = "1.3.0"
108136
agent_id = coder_agent.main.id
109137
use_cached = true
110138
}
@@ -118,7 +146,7 @@ Run without installing from the network (requires Mux to be pre-installed):
118146
module "mux" {
119147
count = data.coder_workspace.me.start_count
120148
source = "registry.coder.com/coder/mux/coder"
121-
version = "1.2.0"
149+
version = "1.3.0"
122150
agent_id = coder_agent.main.id
123151
install = false
124152
}
@@ -132,4 +160,6 @@ module "mux" {
132160

133161
- Mux is currently in preview and you may encounter bugs
134162
- Requires internet connectivity for agent operations (unless `install` is set to false)
135-
- Installs `mux@next` from npm by default (falls back to the npm tarball if npm is unavailable)
163+
- Auto-detects `npm`, `pnpm`, or `bun` by default; set `package_manager` to force a specific one
164+
- Installs `mux@next` from the npm registry by default; set `registry_url` to use a private or mirrored registry
165+
- Falls back to a direct tarball download when no package manager is found

registry/coder/modules/mux/main.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ describe("mux", async () => {
3535
}
3636
expect(output.exitCode).toBe(0);
3737
const expectedLines = [
38-
"📥 npm not found; downloading tarball from npm registry...",
38+
"📥 No package manager found; downloading tarball from registry...",
3939
"🥳 mux has been installed in /tmp/mux",
4040
"🚀 Starting mux server on port 4000...",
4141
"Check logs at /tmp/mux.log!",
@@ -111,7 +111,7 @@ chmod +x /tmp/mux/mux`,
111111
expect(output.exitCode).toBe(0);
112112
const expectedLines = [
113113
"📦 Installing mux via npm into /tmp/mux...",
114-
"⏭️ Skipping npm lifecycle scripts with --ignore-scripts",
114+
"⏭️ Skipping lifecycle scripts with --ignore-scripts",
115115
"🥳 mux has been installed in /tmp/mux",
116116
"🚀 Starting mux server on port 4000...",
117117
"Check logs at /tmp/mux.log!",

registry/coder/modules/mux/main.tf

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,23 @@ variable "install_version" {
6767
default = "next"
6868
}
6969

70+
variable "package_manager" {
71+
type = string
72+
description = "Package manager to install Mux. 'auto' detects npm, pnpm, or bun (falling back to tarball download). Set to 'npm', 'pnpm', or 'bun' to force a specific one."
73+
default = "auto"
74+
validation {
75+
condition = contains(["auto", "npm", "pnpm", "bun"], var.package_manager)
76+
error_message = "The 'package_manager' variable must be one of: 'auto', 'npm', 'pnpm', 'bun'."
77+
}
78+
}
79+
80+
variable "registry_url" {
81+
type = string
82+
description = "The npm-compatible registry URL to install Mux from. Override this for private registries or mirrors."
83+
default = "https://registry.npmjs.org"
84+
}
85+
86+
7087
variable "share" {
7188
type = string
7289
default = "owner"
@@ -137,6 +154,7 @@ resource "random_password" "mux_auth_token" {
137154

138155
locals {
139156
mux_auth_token = random_password.mux_auth_token.result
157+
registry_url = trimsuffix(var.registry_url, "/")
140158
}
141159

142160
resource "coder_script" "mux" {
@@ -153,6 +171,8 @@ resource "coder_script" "mux" {
153171
OFFLINE : !var.install,
154172
USE_CACHED : var.use_cached,
155173
AUTH_TOKEN : local.mux_auth_token,
174+
PACKAGE_MANAGER : var.package_manager,
175+
REGISTRY_URL : local.registry_url,
156176
})
157177
run_on_start = true
158178

registry/coder/modules/mux/mux.tftest.hcl

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,96 @@ run "use_cached_only_success" {
121121
use_cached = true
122122
}
123123
}
124+
125+
# Custom package_manager should appear in generated script
126+
run "custom_package_manager_npm" {
127+
command = plan
128+
129+
variables {
130+
agent_id = "foo"
131+
package_manager = "npm"
132+
}
133+
134+
assert {
135+
condition = strcontains(resource.coder_script.mux.script, "PM_CMD=\"npm\"")
136+
error_message = "mux script must set PM_CMD to the configured package manager"
137+
}
138+
}
139+
140+
run "custom_package_manager_pnpm" {
141+
command = plan
142+
143+
variables {
144+
agent_id = "foo"
145+
package_manager = "pnpm"
146+
}
147+
148+
assert {
149+
condition = strcontains(resource.coder_script.mux.script, "PM_CMD=\"pnpm\"")
150+
error_message = "mux script must set PM_CMD to the configured package manager"
151+
}
152+
}
153+
154+
run "custom_package_manager_bun" {
155+
command = plan
156+
157+
variables {
158+
agent_id = "foo"
159+
package_manager = "bun"
160+
}
161+
162+
assert {
163+
condition = strcontains(resource.coder_script.mux.script, "PM_CMD=\"bun\"")
164+
error_message = "mux script must set PM_CMD to the configured package manager"
165+
}
166+
}
167+
168+
# Invalid package_manager should fail validation
169+
run "invalid_package_manager" {
170+
command = plan
171+
172+
variables {
173+
agent_id = "foo"
174+
package_manager = "yarn"
175+
}
176+
177+
expect_failures = [
178+
var.package_manager
179+
]
180+
}
181+
182+
# Custom registry_url should appear in generated script
183+
run "custom_registry_url" {
184+
command = plan
185+
186+
variables {
187+
agent_id = "foo"
188+
registry_url = "https://npm.example.com"
189+
}
190+
191+
assert {
192+
condition = strcontains(resource.coder_script.mux.script, "https://npm.example.com")
193+
error_message = "mux script must use the configured registry URL"
194+
}
195+
196+
assert {
197+
condition = !strcontains(resource.coder_script.mux.script, "registry.npmjs.org")
198+
error_message = "mux script must not contain hardcoded registry.npmjs.org when custom registry is set"
199+
}
200+
}
201+
202+
# registry_url trailing slash should be stripped
203+
run "registry_url_trailing_slash" {
204+
command = plan
205+
206+
variables {
207+
agent_id = "foo"
208+
registry_url = "https://npm.example.com/"
209+
}
210+
211+
assert {
212+
condition = strcontains(resource.coder_script.mux.script, "https://npm.example.com/mux/")
213+
error_message = "registry URL trailing slash must be stripped to avoid double slashes"
214+
}
215+
}
216+

registry/coder/modules/mux/run.sh

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ fi
5454

5555
# If there is no cached install OR we don't want to use a cached install
5656
if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
57-
printf "$${BOLD}Installing mux from npm...\n"
57+
printf "$${BOLD}Installing mux...\n"
5858

5959
# Clean up from other install (in case install prefix changed).
6060
if [ -n "$CODER_SCRIPT_BIN_DIR" ] && [ -e "$CODER_SCRIPT_BIN_DIR/mux" ]; then
@@ -63,41 +63,76 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
6363

6464
mkdir -p "$(dirname "$MUX_BINARY")"
6565

66-
if command -v npm > /dev/null 2>&1; then
67-
echo "📦 Installing mux via npm into ${INSTALL_PREFIX}..."
66+
# Determine which package manager to use
67+
PM_CMD=""
68+
if [ "${PACKAGE_MANAGER}" = "auto" ]; then
69+
for pm in npm pnpm bun; do
70+
if command -v "$pm" > /dev/null 2>&1; then
71+
PM_CMD="$pm"
72+
break
73+
fi
74+
done
75+
else
76+
PM_CMD="${PACKAGE_MANAGER}"
77+
if ! command -v "$PM_CMD" > /dev/null 2>&1; then
78+
echo "❌ Configured package manager '${PACKAGE_MANAGER}' not found on PATH"
79+
exit 1
80+
fi
81+
fi
82+
83+
if [ -n "$PM_CMD" ]; then
84+
echo "📦 Installing mux via $PM_CMD into ${INSTALL_PREFIX}..."
6885
NPM_WORKDIR="${INSTALL_PREFIX}/npm"
6986
mkdir -p "$NPM_WORKDIR"
7087
cd "$NPM_WORKDIR" || exit 1
7188
if [ ! -f package.json ]; then
7289
echo '{}' > package.json
7390
fi
74-
echo "⏭️ Skipping npm lifecycle scripts with --ignore-scripts"
91+
echo "⏭️ Skipping lifecycle scripts with --ignore-scripts"
7592
PKG="mux"
7693
if [ -z "${VERSION}" ] || [ "${VERSION}" = "latest" ]; then
7794
PKG_SPEC="$PKG@latest"
7895
else
7996
PKG_SPEC="$PKG@${VERSION}"
8097
fi
81-
if ! npm install --no-audit --no-fund --omit=dev --ignore-scripts "$PKG_SPEC"; then
82-
echo "❌ Failed to install mux via npm"
98+
INSTALL_OK=true
99+
case "$PM_CMD" in
100+
npm)
101+
if ! npm install --no-audit --no-fund --omit=dev --ignore-scripts --registry "${REGISTRY_URL}" "$PKG_SPEC"; then
102+
INSTALL_OK=false
103+
fi
104+
;;
105+
pnpm)
106+
if ! pnpm add --ignore-scripts --registry "${REGISTRY_URL}" "$PKG_SPEC"; then
107+
INSTALL_OK=false
108+
fi
109+
;;
110+
bun)
111+
if ! bun add --ignore-scripts --registry "${REGISTRY_URL}" "$PKG_SPEC"; then
112+
INSTALL_OK=false
113+
fi
114+
;;
115+
esac
116+
if [ "$INSTALL_OK" != true ]; then
117+
echo "❌ Failed to install mux via $PM_CMD"
83118
exit 1
84119
fi
85120
# Determine the installed binary path
86121
BIN_DIR="$NPM_WORKDIR/node_modules/.bin"
87122
CANDIDATE="$BIN_DIR/mux"
88123
if [ ! -f "$CANDIDATE" ]; then
89-
echo "❌ Could not locate mux binary after npm install"
124+
echo "❌ Could not locate mux binary after $PM_CMD install"
90125
exit 1
91126
fi
92127
chmod +x "$CANDIDATE" || true
93128
ln -sf "$CANDIDATE" "$MUX_BINARY"
94129
else
95-
echo "📥 npm not found; downloading tarball from npm registry..."
130+
echo "📥 No package manager found; downloading tarball from registry..."
96131
VERSION_TO_USE="${VERSION}"
97132
if [ -z "$VERSION_TO_USE" ]; then
98133
VERSION_TO_USE="next"
99134
fi
100-
META_URL="https://registry.npmjs.org/mux/$VERSION_TO_USE"
135+
META_URL="${REGISTRY_URL}/mux/$VERSION_TO_USE"
101136
META_JSON="$(curl -fsSL "$META_URL" || true)"
102137
if [ -z "$META_JSON" ]; then
103138
echo "❌ Failed to fetch npm metadata: $META_URL"
@@ -136,7 +171,7 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then
136171
echo "❌ Could not determine version for mux"
137172
exit 1
138173
fi
139-
TARBALL_URL="https://registry.npmjs.org/mux/-/mux-$VERSION_TO_USE.tgz"
174+
TARBALL_URL="${REGISTRY_URL}/mux/-/mux-$VERSION_TO_USE.tgz"
140175
fi
141176
TMP_DIR="$(mktemp -d)"
142177
TAR_PATH="$TMP_DIR/mux.tgz"

0 commit comments

Comments
 (0)