diff --git a/registry/coder/modules/mux/README.md b/registry/coder/modules/mux/README.md index 69073926..eacf005a 100644 --- a/registry/coder/modules/mux/README.md +++ b/registry/coder/modules/mux/README.md @@ -8,13 +8,13 @@ tags: [ai, agents, development, multiplexer] # Mux -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. +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. ```tf module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.2.0" + version = "1.3.0" agent_id = coder_agent.main.id } ``` @@ -37,7 +37,7 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.2.0" + version = "1.3.0" agent_id = coder_agent.main.id } ``` @@ -48,7 +48,7 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.2.0" + version = "1.3.0" agent_id = coder_agent.main.id # Default is "latest"; set to a specific version to pin install_version = "0.4.0" @@ -63,7 +63,7 @@ Start Mux with `mux server --add-project /path/to/project`: module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.2.0" + version = "1.3.0" agent_id = coder_agent.main.id add-project = "/path/to/project" } @@ -78,7 +78,7 @@ The module parses quoted values, so grouped arguments remain intact. module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.2.0" + version = "1.3.0" agent_id = coder_agent.main.id additional_arguments = "--open-mode pinned --add-project '/workspaces/my repo'" } @@ -90,12 +90,40 @@ module "mux" { module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.2.0" + version = "1.3.0" agent_id = coder_agent.main.id port = 8080 } ``` +### Custom Package Manager + +Force a specific package manager instead of auto-detection: + +```tf +module "mux" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/mux/coder" + version = "1.3.0" + agent_id = coder_agent.main.id + package_manager = "pnpm" # or "npm", "bun" +} +``` + +### Custom Registry + +Use a private or mirrored npm registry: + +```tf +module "mux" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/mux/coder" + version = "1.3.0" + agent_id = coder_agent.main.id + registry_url = "https://npm.pkg.github.com" +} +``` + ### Use Cached Installation 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: module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.2.0" + version = "1.3.0" agent_id = coder_agent.main.id use_cached = true } @@ -118,7 +146,7 @@ Run without installing from the network (requires Mux to be pre-installed): module "mux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/mux/coder" - version = "1.2.0" + version = "1.3.0" agent_id = coder_agent.main.id install = false } @@ -132,4 +160,6 @@ module "mux" { - Mux is currently in preview and you may encounter bugs - Requires internet connectivity for agent operations (unless `install` is set to false) -- Installs `mux@next` from npm by default (falls back to the npm tarball if npm is unavailable) +- Auto-detects `npm`, `pnpm`, or `bun` by default; set `package_manager` to force a specific one +- Installs `mux@next` from the npm registry by default; set `registry_url` to use a private or mirrored registry +- Falls back to a direct tarball download when no package manager is found diff --git a/registry/coder/modules/mux/main.test.ts b/registry/coder/modules/mux/main.test.ts index c1b6e238..cc2e70db 100644 --- a/registry/coder/modules/mux/main.test.ts +++ b/registry/coder/modules/mux/main.test.ts @@ -35,7 +35,7 @@ describe("mux", async () => { } expect(output.exitCode).toBe(0); const expectedLines = [ - "📥 npm not found; downloading tarball from npm registry...", + "📥 No package manager found; downloading tarball from registry...", "🥳 mux has been installed in /tmp/mux", "🚀 Starting mux server on port 4000...", "Check logs at /tmp/mux.log!", @@ -111,7 +111,7 @@ chmod +x /tmp/mux/mux`, expect(output.exitCode).toBe(0); const expectedLines = [ "📦 Installing mux via npm into /tmp/mux...", - "⏭️ Skipping npm lifecycle scripts with --ignore-scripts", + "⏭️ Skipping lifecycle scripts with --ignore-scripts", "🥳 mux has been installed in /tmp/mux", "🚀 Starting mux server on port 4000...", "Check logs at /tmp/mux.log!", diff --git a/registry/coder/modules/mux/main.tf b/registry/coder/modules/mux/main.tf index dbc585d8..7ca97ccc 100644 --- a/registry/coder/modules/mux/main.tf +++ b/registry/coder/modules/mux/main.tf @@ -67,6 +67,23 @@ variable "install_version" { default = "next" } +variable "package_manager" { + type = string + 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." + default = "auto" + validation { + condition = contains(["auto", "npm", "pnpm", "bun"], var.package_manager) + error_message = "The 'package_manager' variable must be one of: 'auto', 'npm', 'pnpm', 'bun'." + } +} + +variable "registry_url" { + type = string + description = "The npm-compatible registry URL to install Mux from. Override this for private registries or mirrors." + default = "https://registry.npmjs.org" +} + + variable "share" { type = string default = "owner" @@ -137,6 +154,7 @@ resource "random_password" "mux_auth_token" { locals { mux_auth_token = random_password.mux_auth_token.result + registry_url = trimsuffix(var.registry_url, "/") } resource "coder_script" "mux" { @@ -153,6 +171,8 @@ resource "coder_script" "mux" { OFFLINE : !var.install, USE_CACHED : var.use_cached, AUTH_TOKEN : local.mux_auth_token, + PACKAGE_MANAGER : var.package_manager, + REGISTRY_URL : local.registry_url, }) run_on_start = true diff --git a/registry/coder/modules/mux/mux.tftest.hcl b/registry/coder/modules/mux/mux.tftest.hcl index 688ddedb..42569997 100644 --- a/registry/coder/modules/mux/mux.tftest.hcl +++ b/registry/coder/modules/mux/mux.tftest.hcl @@ -121,3 +121,96 @@ run "use_cached_only_success" { use_cached = true } } + +# Custom package_manager should appear in generated script +run "custom_package_manager_npm" { + command = plan + + variables { + agent_id = "foo" + package_manager = "npm" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "PM_CMD=\"npm\"") + error_message = "mux script must set PM_CMD to the configured package manager" + } +} + +run "custom_package_manager_pnpm" { + command = plan + + variables { + agent_id = "foo" + package_manager = "pnpm" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "PM_CMD=\"pnpm\"") + error_message = "mux script must set PM_CMD to the configured package manager" + } +} + +run "custom_package_manager_bun" { + command = plan + + variables { + agent_id = "foo" + package_manager = "bun" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "PM_CMD=\"bun\"") + error_message = "mux script must set PM_CMD to the configured package manager" + } +} + +# Invalid package_manager should fail validation +run "invalid_package_manager" { + command = plan + + variables { + agent_id = "foo" + package_manager = "yarn" + } + + expect_failures = [ + var.package_manager + ] +} + +# Custom registry_url should appear in generated script +run "custom_registry_url" { + command = plan + + variables { + agent_id = "foo" + registry_url = "https://npm.example.com" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "https://npm.example.com") + error_message = "mux script must use the configured registry URL" + } + + assert { + condition = !strcontains(resource.coder_script.mux.script, "registry.npmjs.org") + error_message = "mux script must not contain hardcoded registry.npmjs.org when custom registry is set" + } +} + +# registry_url trailing slash should be stripped +run "registry_url_trailing_slash" { + command = plan + + variables { + agent_id = "foo" + registry_url = "https://npm.example.com/" + } + + assert { + condition = strcontains(resource.coder_script.mux.script, "https://npm.example.com/mux/") + error_message = "registry URL trailing slash must be stripped to avoid double slashes" + } +} + diff --git a/registry/coder/modules/mux/run.sh b/registry/coder/modules/mux/run.sh index 099448fb..2dbd5ea9 100644 --- a/registry/coder/modules/mux/run.sh +++ b/registry/coder/modules/mux/run.sh @@ -54,7 +54,7 @@ fi # If there is no cached install OR we don't want to use a cached install if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then - printf "$${BOLD}Installing mux from npm...\n" + printf "$${BOLD}Installing mux...\n" # Clean up from other install (in case install prefix changed). 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 mkdir -p "$(dirname "$MUX_BINARY")" - if command -v npm > /dev/null 2>&1; then - echo "📦 Installing mux via npm into ${INSTALL_PREFIX}..." + # Determine which package manager to use + PM_CMD="" + if [ "${PACKAGE_MANAGER}" = "auto" ]; then + for pm in npm pnpm bun; do + if command -v "$pm" > /dev/null 2>&1; then + PM_CMD="$pm" + break + fi + done + else + PM_CMD="${PACKAGE_MANAGER}" + if ! command -v "$PM_CMD" > /dev/null 2>&1; then + echo "❌ Configured package manager '${PACKAGE_MANAGER}' not found on PATH" + exit 1 + fi + fi + + if [ -n "$PM_CMD" ]; then + echo "📦 Installing mux via $PM_CMD into ${INSTALL_PREFIX}..." NPM_WORKDIR="${INSTALL_PREFIX}/npm" mkdir -p "$NPM_WORKDIR" cd "$NPM_WORKDIR" || exit 1 if [ ! -f package.json ]; then echo '{}' > package.json fi - echo "⏭️ Skipping npm lifecycle scripts with --ignore-scripts" + echo "⏭️ Skipping lifecycle scripts with --ignore-scripts" PKG="mux" if [ -z "${VERSION}" ] || [ "${VERSION}" = "latest" ]; then PKG_SPEC="$PKG@latest" else PKG_SPEC="$PKG@${VERSION}" fi - if ! npm install --no-audit --no-fund --omit=dev --ignore-scripts "$PKG_SPEC"; then - echo "❌ Failed to install mux via npm" + INSTALL_OK=true + case "$PM_CMD" in + npm) + if ! npm install --no-audit --no-fund --omit=dev --ignore-scripts --registry "${REGISTRY_URL}" "$PKG_SPEC"; then + INSTALL_OK=false + fi + ;; + pnpm) + if ! pnpm add --ignore-scripts --registry "${REGISTRY_URL}" "$PKG_SPEC"; then + INSTALL_OK=false + fi + ;; + bun) + if ! bun add --ignore-scripts --registry "${REGISTRY_URL}" "$PKG_SPEC"; then + INSTALL_OK=false + fi + ;; + esac + if [ "$INSTALL_OK" != true ]; then + echo "❌ Failed to install mux via $PM_CMD" exit 1 fi # Determine the installed binary path BIN_DIR="$NPM_WORKDIR/node_modules/.bin" CANDIDATE="$BIN_DIR/mux" if [ ! -f "$CANDIDATE" ]; then - echo "❌ Could not locate mux binary after npm install" + echo "❌ Could not locate mux binary after $PM_CMD install" exit 1 fi chmod +x "$CANDIDATE" || true ln -sf "$CANDIDATE" "$MUX_BINARY" else - echo "📥 npm not found; downloading tarball from npm registry..." + echo "📥 No package manager found; downloading tarball from registry..." VERSION_TO_USE="${VERSION}" if [ -z "$VERSION_TO_USE" ]; then VERSION_TO_USE="next" fi - META_URL="https://registry.npmjs.org/mux/$VERSION_TO_USE" + META_URL="${REGISTRY_URL}/mux/$VERSION_TO_USE" META_JSON="$(curl -fsSL "$META_URL" || true)" if [ -z "$META_JSON" ]; then echo "❌ Failed to fetch npm metadata: $META_URL" @@ -136,7 +171,7 @@ if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then echo "❌ Could not determine version for mux" exit 1 fi - TARBALL_URL="https://registry.npmjs.org/mux/-/mux-$VERSION_TO_USE.tgz" + TARBALL_URL="${REGISTRY_URL}/mux/-/mux-$VERSION_TO_USE.tgz" fi TMP_DIR="$(mktemp -d)" TAR_PATH="$TMP_DIR/mux.tgz"