From e81bc25acdc8a20c06c0aa313aa5aa5913e9be8e Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:48:39 +0000 Subject: [PATCH 1/6] feat: add cmux module --- .icons/cmux.svg | 47 +++++++ registry/coder/modules/cmux/README.md | 104 ++++++++++++++ registry/coder/modules/cmux/cmux.tftest.hcl | 44 ++++++ registry/coder/modules/cmux/main.tf | 148 ++++++++++++++++++++ registry/coder/modules/cmux/run.sh | 117 ++++++++++++++++ 5 files changed, 460 insertions(+) create mode 100644 .icons/cmux.svg create mode 100644 registry/coder/modules/cmux/README.md create mode 100644 registry/coder/modules/cmux/cmux.tftest.hcl create mode 100644 registry/coder/modules/cmux/main.tf create mode 100644 registry/coder/modules/cmux/run.sh diff --git a/.icons/cmux.svg b/.icons/cmux.svg new file mode 100644 index 000000000..95b56bb00 --- /dev/null +++ b/.icons/cmux.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/registry/coder/modules/cmux/README.md b/registry/coder/modules/cmux/README.md new file mode 100644 index 000000000..f868c260d --- /dev/null +++ b/registry/coder/modules/cmux/README.md @@ -0,0 +1,104 @@ +--- +display_name: cmux +description: Coding Agent Multiplexer - Run multiple AI agents in parallel +icon: ../../../../.icons/cmux.svg +verified: false +tags: [ai, agents, development, multiplexer] +--- + +# cmux + +Automatically install and run [cmux](https://github.com/coder/cmux) in a workspace. By default, the module installs `@coder/cmux@latest` from npm (with a fallback to downloading the npm tarball if npm is unavailable). cmux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces. + +```tf +module "cmux" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/cmux/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +## Features + +- **Parallel Agent Execution**: Run multiple AI agents simultaneously on different tasks +- **Workspace Isolation**: Each agent works in its own isolated environment +- **Git Divergence Visualization**: Track changes across different agent workspaces +- **Long-Running Processes**: Resume AI work after interruptions +- **Cost Tracking**: Monitor API usage across agents + +## Examples + +### Basic Usage + +```tf +module "cmux" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/cmux/coder" + version = "1.0.0" + agent_id = coder_agent.example.id +} +``` + +### Pin Version + +```tf +module "cmux" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/cmux/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + # Default is "latest"; set to a specific version to pin + install_version = "0.4.0" +} +``` + +### Custom Port + +```tf +module "cmux" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/cmux/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + port = 8080 +} +``` + +### Use Cached Installation + +Run an existing copy of cmux if found, otherwise install from npm: + +```tf +module "cmux" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/cmux/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + use_cached = true +} +``` + +### Offline Mode + +Just run cmux in the background; do not install from the network (requires cmux to be pre-installed): + +```tf +module "cmux" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/cmux/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + offline = true +} +``` + +## Supported Platforms + +- Linux (x86_64, aarch64) + +## Notes + +- cmux is currently in preview and you may encounter bugs +- Requires internet connectivity for agent operations (unless running in offline mode) +- Installs `@coder/cmux` from npm by default (falls back to the npm tarball if npm is unavailable) diff --git a/registry/coder/modules/cmux/cmux.tftest.hcl b/registry/coder/modules/cmux/cmux.tftest.hcl new file mode 100644 index 000000000..70f84c4e2 --- /dev/null +++ b/registry/coder/modules/cmux/cmux.tftest.hcl @@ -0,0 +1,44 @@ +run "required_vars" { + command = plan + + variables { + agent_id = "foo" + } +} + +run "offline_and_use_cached_conflict" { + command = plan + + variables { + agent_id = "foo" + use_cached = true + offline = true + } + + expect_failures = [ + resource.coder_script.cmux + ] +} + +run "custom_port" { + command = plan + + variables { + agent_id = "foo" + port = 8080 + } + + assert { + condition = resource.coder_app.cmux.url == "http://localhost:8080" + error_message = "coder_app URL must use the configured port" + } +} + +run "custom_version" { + command = plan + + variables { + agent_id = "foo" + install_version = "0.3.0" + } +} diff --git a/registry/coder/modules/cmux/main.tf b/registry/coder/modules/cmux/main.tf new file mode 100644 index 000000000..7cdd8314e --- /dev/null +++ b/registry/coder/modules/cmux/main.tf @@ -0,0 +1,148 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "port" { + type = number + description = "The port to run cmux on." + default = 4000 +} + +variable "display_name" { + type = string + description = "The display name for the cmux application." + default = "cmux" +} + +variable "slug" { + type = string + description = "The slug for the cmux application." + default = "cmux" +} + +variable "install_prefix" { + type = string + description = "The prefix to install cmux to." + default = "/tmp/cmux" +} + +variable "log_path" { + type = string + description = "The path to log cmux to." + default = "/tmp/cmux.log" +} + +variable "install_version" { + type = string + description = "The version of cmux to install." + default = "latest" +} + +variable "share" { + type = string + default = "owner" + validation { + condition = var.share == "owner" || var.share == "authenticated" || var.share == "public" + error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'." + } +} + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} + +variable "group" { + type = string + description = "The name of a group that this app belongs to." + default = null +} + +variable "offline" { + type = bool + description = "Just run cmux in the background; do not install from the network" + default = false +} + +variable "use_cached" { + type = bool + description = "Use cached copy of cmux if present; otherwise install from npm" + default = false +} + +variable "subdomain" { + type = bool + description = <<-EOT + Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder. + If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible. + EOT + default = false +} + +variable "open_in" { + type = string + description = <<-EOT + Determines where the app will be opened. Valid values are `"tab"` and `"slim-window" (default)`. + `"tab"` opens in a new tab in the same browser window. + `"slim-window"` opens a new browser window without navigation controls. + EOT + default = "slim-window" + validation { + condition = contains(["tab", "slim-window"], var.open_in) + error_message = "The 'open_in' variable must be one of: 'tab', 'slim-window'." + } +} + +resource "coder_script" "cmux" { + agent_id = var.agent_id + display_name = "cmux" + icon = "/icon/terminal.svg" + script = templatefile("${path.module}/run.sh", { + VERSION : var.install_version, + PORT : var.port, + LOG_PATH : var.log_path, + INSTALL_PREFIX : var.install_prefix, + OFFLINE : var.offline, + USE_CACHED : var.use_cached, + }) + run_on_start = true + + lifecycle { + precondition { + condition = !var.offline || !var.use_cached + error_message = "Offline and Use Cached can not be used together" + } + } +} + +resource "coder_app" "cmux" { + agent_id = var.agent_id + slug = var.slug + display_name = var.display_name + url = "http://localhost:${var.port}" + icon = "/icon/terminal.svg" + subdomain = var.subdomain + share = var.share + order = var.order + group = var.group + open_in = var.open_in + + healthcheck { + url = "http://localhost:${var.port}/health" + interval = 5 + threshold = 6 + } +} diff --git a/registry/coder/modules/cmux/run.sh b/registry/coder/modules/cmux/run.sh new file mode 100644 index 000000000..d722de9e0 --- /dev/null +++ b/registry/coder/modules/cmux/run.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash + +BOLD='\033[0;1m' +RESET='\033[0m' +CMUX_BINARY="${INSTALL_PREFIX}/cmux" + +function run_cmux() { + local port_value + port_value="${PORT}" + if [ -z "$port_value" ]; then + port_value="4000" + fi + echo "🚀 Starting cmux server on port $port_value..." + echo "Check logs at ${LOG_PATH}!" + PORT="$port_value" "$CMUX_BINARY" server --port "$port_value" > "${LOG_PATH}" 2>&1 & +} + +# Check if cmux is already installed for offline mode +if [ "${OFFLINE}" = true ]; then + if [ -f "$CMUX_BINARY" ]; then + echo "🥳 Found a copy of cmux" + run_cmux + exit 0 + fi + echo "❌ Failed to find a copy of cmux" + exit 1 +fi + +# If there is no cached install OR we don't want to use a cached install +if [ ! -f "$CMUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then + printf "$${BOLD}Installing cmux from npm...\n" + + # Clean up from other install (in case install prefix changed). + if [ -n "$CODER_SCRIPT_BIN_DIR" ] && [ -e "$CODER_SCRIPT_BIN_DIR/cmux" ]; then + rm "$CODER_SCRIPT_BIN_DIR/cmux" + fi + + mkdir -p "$(dirname "$CMUX_BINARY")" + + if command -v npm > /dev/null 2>&1; then + echo "📦 Installing @coder/cmux via npm 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 + PKG="@coder/cmux" + 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 "$PKG_SPEC"; then + echo "❌ Failed to install @coder/cmux via npm" + exit 1 + fi + # Determine the installed binary path + BIN_DIR="$NPM_WORKDIR/node_modules/.bin" + CANDIDATE="$BIN_DIR/cmux" + if [ ! -f "$CANDIDATE" ]; then + echo "❌ Could not locate cmux binary after npm install" + exit 1 + fi + chmod +x "$CANDIDATE" || true + ln -sf "$CANDIDATE" "$CMUX_BINARY" + else + echo "📥 npm not found; downloading tarball from npm registry..." + VERSION_TO_USE="${VERSION}" + if [ -z "$VERSION_TO_USE" ] || [ "$VERSION_TO_USE" = "latest" ]; then + # Try to determine the latest version + META_URL="https://registry.npmjs.org/@coder/cmux/latest" + VERSION_TO_USE="$(curl -fsSL "$META_URL" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p' | head -n1)" + if [ -z "$VERSION_TO_USE" ]; then + echo "❌ Could not determine latest version for @coder/cmux" + exit 1 + fi + fi + TARBALL_URL="https://registry.npmjs.org/@coder/cmux/-/cmux-$${VERSION_TO_USE}.tgz" + TMP_DIR="$(mktemp -d)" + TAR_PATH="$TMP_DIR/cmux.tgz" + if ! curl -fsSL "$TARBALL_URL" -o "$TAR_PATH"; then + echo "❌ Failed to download tarball: $TARBALL_URL" + rm -rf "$TMP_DIR" + exit 1 + fi + if ! tar -xzf "$TAR_PATH" -C "$TMP_DIR"; then + echo "❌ Failed to extract tarball" + rm -rf "$TMP_DIR" + exit 1 + fi + CANDIDATE="" + if [ -f "$TMP_DIR/package/bin/cmux" ]; then + CANDIDATE="$TMP_DIR/package/bin/cmux" + else + CANDIDATE="$(find "$TMP_DIR/package" -maxdepth 3 -type f -name "cmux" | head -n1)" + fi + if [ -z "$CANDIDATE" ] || [ ! -f "$CANDIDATE" ]; then + echo "❌ Could not locate cmux binary in tarball" + rm -rf "$TMP_DIR" + exit 1 + fi + cp "$CANDIDATE" "$CMUX_BINARY" + chmod +x "$CMUX_BINARY" || true + rm -rf "$TMP_DIR" + fi + + printf "🥳 cmux has been installed in ${INSTALL_PREFIX}\n\n" +fi + +# Make cmux available in PATH if CODER_SCRIPT_BIN_DIR is set +if [ -n "$CODER_SCRIPT_BIN_DIR" ] && [ ! -e "$CODER_SCRIPT_BIN_DIR/cmux" ]; then + ln -s "$CMUX_BINARY" "$CODER_SCRIPT_BIN_DIR/cmux" +fi + +# Start cmux +run_cmux From 7a87f1274fd2f91069732b7ef28035b176046d2a Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 6 Nov 2025 12:33:47 -0500 Subject: [PATCH 2/6] test: extended tests to verify the setup process --- registry/coder/modules/cmux/README.md | 6 +- registry/coder/modules/cmux/cmux.tftest.hcl | 20 ++++++ registry/coder/modules/cmux/main.test.ts | 73 +++++++++++++++++++++ registry/coder/modules/cmux/main.tf | 5 +- registry/coder/modules/cmux/run.sh | 2 +- 5 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 registry/coder/modules/cmux/main.test.ts diff --git a/registry/coder/modules/cmux/README.md b/registry/coder/modules/cmux/README.md index f868c260d..13b6d86ad 100644 --- a/registry/coder/modules/cmux/README.md +++ b/registry/coder/modules/cmux/README.md @@ -8,7 +8,7 @@ tags: [ai, agents, development, multiplexer] # cmux -Automatically install and run [cmux](https://github.com/coder/cmux) in a workspace. By default, the module installs `@coder/cmux@latest` from npm (with a fallback to downloading the npm tarball if npm is unavailable). cmux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces. +Automatically install and run [cmux](https://github.com/coder/cmux) in a Coder workspace. By default, the module installs `@coder/cmux@latest` from npm (with a fallback to downloading the npm tarball if npm is unavailable). cmux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated cmux workspaces. ```tf module "cmux" { @@ -22,8 +22,8 @@ module "cmux" { ## Features - **Parallel Agent Execution**: Run multiple AI agents simultaneously on different tasks -- **Workspace Isolation**: Each agent works in its own isolated environment -- **Git Divergence Visualization**: Track changes across different agent workspaces +- **Cmux Workspace Isolation**: Each agent works in its own isolated environment +- **Git Divergence Visualization**: Track changes across different cmux agent workspaces - **Long-Running Processes**: Resume AI work after interruptions - **Cost Tracking**: Monitor API usage across agents diff --git a/registry/coder/modules/cmux/cmux.tftest.hcl b/registry/coder/modules/cmux/cmux.tftest.hcl index 70f84c4e2..b29094c59 100644 --- a/registry/coder/modules/cmux/cmux.tftest.hcl +++ b/registry/coder/modules/cmux/cmux.tftest.hcl @@ -42,3 +42,23 @@ run "custom_version" { install_version = "0.3.0" } } + +# offline-only should succeed +run "offline_only_success" { + command = plan + + variables { + agent_id = "foo" + offline = true + } +} + +# use_cached-only should succeed +run "use_cached_only_success" { + command = plan + + variables { + agent_id = "foo" + use_cached = true + } +} diff --git a/registry/coder/modules/cmux/main.test.ts b/registry/coder/modules/cmux/main.test.ts new file mode 100644 index 000000000..dafdc68e3 --- /dev/null +++ b/registry/coder/modules/cmux/main.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "bun:test"; +import { + executeScriptInContainer, + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "~test"; + +describe("cmux", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + agent_id: "foo", + }); + + it( + "runs with default", + async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + + const output = await executeScriptInContainer( + state, + "alpine/curl", + "sh", + "apk add bash", + ); + + expect(output.exitCode).toBe(0); + const expectedLines = [ + "📥 npm not found; downloading tarball from npm registry...", + "🥳 cmux has been installed in /tmp/cmux", + "🚀 Starting cmux server on port 4000...", + "Check logs at /tmp/cmux.log!", + ]; + for (const line of expectedLines) { + expect(output.stdout).toContain(line); + } + }, + 15000, + ); + + it( + "runs with npm present", + async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + + const output = await executeScriptInContainer( + state, + "node:20-alpine", + "sh", + "apk add bash", + ); + + expect(output.exitCode).toBe(0); + const expectedLines = [ + "📦 Installing @coder/cmux via npm into /tmp/cmux...", + "🥳 cmux has been installed in /tmp/cmux", + "🚀 Starting cmux server on port 4000...", + "Check logs at /tmp/cmux.log!", + ]; + for (const line of expectedLines) { + expect(output.stdout).toContain(line); + } + }, + 60000, + ); +}); + + diff --git a/registry/coder/modules/cmux/main.tf b/registry/coder/modules/cmux/main.tf index 7cdd8314e..bb649fa93 100644 --- a/registry/coder/modules/cmux/main.tf +++ b/registry/coder/modules/cmux/main.tf @@ -1,5 +1,6 @@ terraform { - required_version = ">= 1.0" + # Requires Terraform 1.9+ for cross-variable validation references + required_version = ">= 1.9" required_providers { coder = { @@ -40,7 +41,7 @@ variable "install_prefix" { variable "log_path" { type = string - description = "The path to log cmux to." + description = "The path for cmux logs." default = "/tmp/cmux.log" } diff --git a/registry/coder/modules/cmux/run.sh b/registry/coder/modules/cmux/run.sh index d722de9e0..6df24b3de 100644 --- a/registry/coder/modules/cmux/run.sh +++ b/registry/coder/modules/cmux/run.sh @@ -76,7 +76,7 @@ if [ ! -f "$CMUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then exit 1 fi fi - TARBALL_URL="https://registry.npmjs.org/@coder/cmux/-/cmux-$${VERSION_TO_USE}.tgz" + TARBALL_URL="https://registry.npmjs.org/@coder/cmux/-/cmux-$VERSION_TO_USE.tgz" TMP_DIR="$(mktemp -d)" TAR_PATH="$TMP_DIR/cmux.tgz" if ! curl -fsSL "$TARBALL_URL" -o "$TAR_PATH"; then From 2efe4285fdf502684981bbb6227984f154cb9d90 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 6 Nov 2025 12:37:24 -0500 Subject: [PATCH 3/6] fix: fmt --- registry/coder/modules/cmux/main.test.ts | 104 ++++++++++------------- 1 file changed, 47 insertions(+), 57 deletions(-) diff --git a/registry/coder/modules/cmux/main.test.ts b/registry/coder/modules/cmux/main.test.ts index dafdc68e3..4f185dc06 100644 --- a/registry/coder/modules/cmux/main.test.ts +++ b/registry/coder/modules/cmux/main.test.ts @@ -13,61 +13,51 @@ describe("cmux", async () => { agent_id: "foo", }); - it( - "runs with default", - async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - }); - - const output = await executeScriptInContainer( - state, - "alpine/curl", - "sh", - "apk add bash", - ); - - expect(output.exitCode).toBe(0); - const expectedLines = [ - "📥 npm not found; downloading tarball from npm registry...", - "🥳 cmux has been installed in /tmp/cmux", - "🚀 Starting cmux server on port 4000...", - "Check logs at /tmp/cmux.log!", - ]; - for (const line of expectedLines) { - expect(output.stdout).toContain(line); - } - }, - 15000, - ); - - it( - "runs with npm present", - async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - }); - - const output = await executeScriptInContainer( - state, - "node:20-alpine", - "sh", - "apk add bash", - ); - - expect(output.exitCode).toBe(0); - const expectedLines = [ - "📦 Installing @coder/cmux via npm into /tmp/cmux...", - "🥳 cmux has been installed in /tmp/cmux", - "🚀 Starting cmux server on port 4000...", - "Check logs at /tmp/cmux.log!", - ]; - for (const line of expectedLines) { - expect(output.stdout).toContain(line); - } - }, - 60000, - ); + it("runs with default", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + + const output = await executeScriptInContainer( + state, + "alpine/curl", + "sh", + "apk add bash", + ); + + expect(output.exitCode).toBe(0); + const expectedLines = [ + "📥 npm not found; downloading tarball from npm registry...", + "🥳 cmux has been installed in /tmp/cmux", + "🚀 Starting cmux server on port 4000...", + "Check logs at /tmp/cmux.log!", + ]; + for (const line of expectedLines) { + expect(output.stdout).toContain(line); + } + }, 15000); + + it("runs with npm present", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + }); + + const output = await executeScriptInContainer( + state, + "node:20-alpine", + "sh", + "apk add bash", + ); + + expect(output.exitCode).toBe(0); + const expectedLines = [ + "📦 Installing @coder/cmux via npm into /tmp/cmux...", + "🥳 cmux has been installed in /tmp/cmux", + "🚀 Starting cmux server on port 4000...", + "Check logs at /tmp/cmux.log!", + ]; + for (const line of expectedLines) { + expect(output.stdout).toContain(line); + } + }, 60000); }); - - From c936e925486fbca010c5cba54cb099e313b383ae Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 6 Nov 2025 12:42:44 -0500 Subject: [PATCH 4/6] fix: tests not working --- registry/coder/modules/cmux/main.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/coder/modules/cmux/main.test.ts b/registry/coder/modules/cmux/main.test.ts index 4f185dc06..27fa18cd4 100644 --- a/registry/coder/modules/cmux/main.test.ts +++ b/registry/coder/modules/cmux/main.test.ts @@ -22,7 +22,7 @@ describe("cmux", async () => { state, "alpine/curl", "sh", - "apk add bash", + "apk add bash tar gzip", ); expect(output.exitCode).toBe(0); From 9599fc7cf34e8e11d8a6bdd46c91c0d9dfdc37b2 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 6 Nov 2025 13:07:24 -0500 Subject: [PATCH 5/6] fix: correct tarball handling --- registry/coder/modules/cmux/main.test.ts | 9 ++++++--- registry/coder/modules/cmux/run.sh | 20 +++++++++++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/registry/coder/modules/cmux/main.test.ts b/registry/coder/modules/cmux/main.test.ts index 27fa18cd4..5ff42c3f0 100644 --- a/registry/coder/modules/cmux/main.test.ts +++ b/registry/coder/modules/cmux/main.test.ts @@ -22,9 +22,12 @@ describe("cmux", async () => { state, "alpine/curl", "sh", - "apk add bash tar gzip", + "apk add --no-cache bash tar gzip ca-certificates findutils nodejs && update-ca-certificates", ); - + if (output.exitCode !== 0) { + console.log("STDOUT:\n" + output.stdout.join("\n")); + console.log("STDERR:\n" + output.stderr.join("\n")); + } expect(output.exitCode).toBe(0); const expectedLines = [ "📥 npm not found; downloading tarball from npm registry...", @@ -35,7 +38,7 @@ describe("cmux", async () => { for (const line of expectedLines) { expect(output.stdout).toContain(line); } - }, 15000); + }, 60000); it("runs with npm present", async () => { const state = await runTerraformApply(import.meta.dir, { diff --git a/registry/coder/modules/cmux/run.sh b/registry/coder/modules/cmux/run.sh index 6df24b3de..23656f8f7 100644 --- a/registry/coder/modules/cmux/run.sh +++ b/registry/coder/modules/cmux/run.sh @@ -90,10 +90,28 @@ if [ ! -f "$CMUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then exit 1 fi CANDIDATE="" + # Common locations if [ -f "$TMP_DIR/package/bin/cmux" ]; then CANDIDATE="$TMP_DIR/package/bin/cmux" + elif [ -f "$TMP_DIR/package/bin/cmux.js" ]; then + CANDIDATE="$TMP_DIR/package/bin/cmux.js" + elif [ -f "$TMP_DIR/package/bin/cmux.mjs" ]; then + CANDIDATE="$TMP_DIR/package/bin/cmux.mjs" else - CANDIDATE="$(find "$TMP_DIR/package" -maxdepth 3 -type f -name "cmux" | head -n1)" + # Try to read package.json bin field + if [ -f "$TMP_DIR/package/package.json" ]; then + BIN_PATH=$(sed -n 's/.*"bin"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$TMP_DIR/package/package.json" | head -n1) + if [ -z "$BIN_PATH" ]; then + BIN_PATH=$(sed -n '/"bin"[[:space:]]*:[[:space:]]*{/,/}/p' "$TMP_DIR/package/package.json" | sed -n 's/.*"cmux"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1) + fi + if [ -n "$BIN_PATH" ] && [ -f "$TMP_DIR/package/$BIN_PATH" ]; then + CANDIDATE="$TMP_DIR/package/$BIN_PATH" + fi + fi + # Fallback: search for plausible filenames + if [ -z "$CANDIDATE" ] || [ ! -f "$CANDIDATE" ]; then + CANDIDATE=$(find "$TMP_DIR/package" -maxdepth 4 -type f \( -name "cmux" -o -name "cmux.js" -o -name "cmux.mjs" -o -name "cmux.cjs" \) | head -n1) + fi fi if [ -z "$CANDIDATE" ] || [ ! -f "$CANDIDATE" ]; then echo "❌ Could not locate cmux binary in tarball" From 79f1b129fdc421b5904e70dcb96650657ad4a088 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 6 Nov 2025 13:16:18 -0500 Subject: [PATCH 6/6] fix: replaced offline with install config --- registry/coder/modules/cmux/README.md | 8 ++++---- registry/coder/modules/cmux/cmux.tftest.hcl | 10 +++++----- registry/coder/modules/cmux/main.tf | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/registry/coder/modules/cmux/README.md b/registry/coder/modules/cmux/README.md index 13b6d86ad..503bbea27 100644 --- a/registry/coder/modules/cmux/README.md +++ b/registry/coder/modules/cmux/README.md @@ -79,9 +79,9 @@ module "cmux" { } ``` -### Offline Mode +### Skip Install -Just run cmux in the background; do not install from the network (requires cmux to be pre-installed): +Run without installing from the network (requires cmux to be pre-installed): ```tf module "cmux" { @@ -89,7 +89,7 @@ module "cmux" { source = "registry.coder.com/coder/cmux/coder" version = "1.0.0" agent_id = coder_agent.example.id - offline = true + install = false } ``` @@ -100,5 +100,5 @@ module "cmux" { ## Notes - cmux is currently in preview and you may encounter bugs -- Requires internet connectivity for agent operations (unless running in offline mode) +- Requires internet connectivity for agent operations (unless `install` is set to false) - Installs `@coder/cmux` from npm by default (falls back to the npm tarball if npm is unavailable) diff --git a/registry/coder/modules/cmux/cmux.tftest.hcl b/registry/coder/modules/cmux/cmux.tftest.hcl index b29094c59..3b831b373 100644 --- a/registry/coder/modules/cmux/cmux.tftest.hcl +++ b/registry/coder/modules/cmux/cmux.tftest.hcl @@ -6,13 +6,13 @@ run "required_vars" { } } -run "offline_and_use_cached_conflict" { +run "install_false_and_use_cached_conflict" { command = plan variables { agent_id = "foo" use_cached = true - offline = true + install = false } expect_failures = [ @@ -43,13 +43,13 @@ run "custom_version" { } } -# offline-only should succeed -run "offline_only_success" { +# install=false should succeed +run "install_false_only_success" { command = plan variables { agent_id = "foo" - offline = true + install = false } } diff --git a/registry/coder/modules/cmux/main.tf b/registry/coder/modules/cmux/main.tf index bb649fa93..b1c318d80 100644 --- a/registry/coder/modules/cmux/main.tf +++ b/registry/coder/modules/cmux/main.tf @@ -72,10 +72,10 @@ variable "group" { default = null } -variable "offline" { +variable "install" { type = bool - description = "Just run cmux in the background; do not install from the network" - default = false + description = "Install cmux from the network (npm or tarball). If false, run without installing (requires a pre-installed cmux)." + default = true } variable "use_cached" { @@ -116,15 +116,15 @@ resource "coder_script" "cmux" { PORT : var.port, LOG_PATH : var.log_path, INSTALL_PREFIX : var.install_prefix, - OFFLINE : var.offline, + OFFLINE : !var.install, USE_CACHED : var.use_cached, }) run_on_start = true lifecycle { precondition { - condition = !var.offline || !var.use_cached - error_message = "Offline and Use Cached can not be used together" + condition = var.install || !var.use_cached + error_message = "Cannot use 'use_cached' when 'install' is false" } } }