Merge branch 'main' into 35C4n0r/feat-agentapi-architecture-improv

This commit is contained in:
35C4n0r 2026-02-27 15:11:37 +05:30
commit 71c5a4fe11
No known key found for this signature in database
GPG Key ID: 5B71E5C9D18D5675
8 changed files with 210 additions and 31 deletions

View File

@ -18,7 +18,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@ -31,7 +31,7 @@ module "dotfiles" {
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@ -42,7 +42,7 @@ module "dotfiles" {
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
user = "root" user = "root"
} }
@ -54,14 +54,14 @@ module "dotfiles" {
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
module "dotfiles-root" { module "dotfiles-root" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
user = "root" user = "root"
dotfiles_uri = module.dotfiles.dotfiles_uri dotfiles_uri = module.dotfiles.dotfiles_uri
@ -76,7 +76,7 @@ You can set a default dotfiles repository for all users by setting the `default_
module "dotfiles" { module "dotfiles" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder" source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.0" version = "1.3.1"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
default_dotfiles_uri = "https://github.com/coder/dotfiles" default_dotfiles_uri = "https://github.com/coder/dotfiles"
} }

View File

@ -26,6 +26,7 @@ describe("dotfiles", async () => {
"git@github.com:coder/dotfiles.git", "git@github.com:coder/dotfiles.git",
"git://github.com/coder/dotfiles.git", "git://github.com/coder/dotfiles.git",
"ssh://git@github.com/coder/dotfiles.git", "ssh://git@github.com/coder/dotfiles.git",
"ssh://git@bitbucket.example.org:7999/~myusername/dotfiles.git",
]; ];
for (const url of validUrls) { for (const url of validUrls) {
const state = await runTerraformApply(import.meta.dir, { const state = await runTerraformApply(import.meta.dir, {

View File

@ -40,7 +40,7 @@ variable "default_dotfiles_uri" {
validation { validation {
condition = ( condition = (
var.default_dotfiles_uri == "" || var.default_dotfiles_uri == "" ||
can(regex("^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$", var.default_dotfiles_uri)) can(regex("^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@~-]+$", var.default_dotfiles_uri))
) )
error_message = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters." error_message = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters."
} }
@ -55,7 +55,7 @@ variable "dotfiles_uri" {
condition = ( condition = (
var.dotfiles_uri == null || var.dotfiles_uri == null ||
var.dotfiles_uri == "" || var.dotfiles_uri == "" ||
can(regex("^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$", var.dotfiles_uri)) can(regex("^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@~-]+$", var.dotfiles_uri))
) )
error_message = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters." error_message = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters."
} }
@ -102,7 +102,7 @@ data "coder_parameter" "dotfiles_uri" {
icon = "/icon/dotfiles.svg" icon = "/icon/dotfiles.svg"
validation { validation {
regex = "^$|^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@-]+$" regex = "^$|^(https?://|ssh://|git@|git://)[a-zA-Z0-9._/:@~-]+$"
error = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters." error = "Must be a valid dotfiles repository URL (https, git@, or git://) without special characters."
} }
} }

View File

@ -8,13 +8,13 @@ tags: [ai, agents, development, multiplexer]
# Mux # 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 ```tf
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.2.0" version = "1.3.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@ -37,7 +37,7 @@ module "mux" {
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.2.0" version = "1.3.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
} }
``` ```
@ -48,7 +48,7 @@ module "mux" {
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.2.0" version = "1.3.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
# Default is "latest"; set to a specific version to pin # Default is "latest"; set to a specific version to pin
install_version = "0.4.0" install_version = "0.4.0"
@ -63,7 +63,7 @@ Start Mux with `mux server --add-project /path/to/project`:
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.2.0" version = "1.3.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
add-project = "/path/to/project" add-project = "/path/to/project"
} }
@ -78,7 +78,7 @@ The module parses quoted values, so grouped arguments remain intact.
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.2.0" version = "1.3.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
additional_arguments = "--open-mode pinned --add-project '/workspaces/my repo'" additional_arguments = "--open-mode pinned --add-project '/workspaces/my repo'"
} }
@ -90,12 +90,40 @@ module "mux" {
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.2.0" version = "1.3.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
port = 8080 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 ### Use Cached Installation
Run an existing copy of Mux if found, otherwise install from npm: 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" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.2.0" version = "1.3.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
use_cached = true use_cached = true
} }
@ -118,7 +146,7 @@ Run without installing from the network (requires Mux to be pre-installed):
module "mux" { module "mux" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder" source = "registry.coder.com/coder/mux/coder"
version = "1.2.0" version = "1.3.0"
agent_id = coder_agent.main.id agent_id = coder_agent.main.id
install = false install = false
} }
@ -132,4 +160,6 @@ module "mux" {
- Mux is currently in preview and you may encounter bugs - Mux is currently in preview and you may encounter bugs
- Requires internet connectivity for agent operations (unless `install` is set to false) - 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

View File

@ -35,7 +35,7 @@ describe("mux", async () => {
} }
expect(output.exitCode).toBe(0); expect(output.exitCode).toBe(0);
const expectedLines = [ 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", "🥳 mux has been installed in /tmp/mux",
"🚀 Starting mux server on port 4000...", "🚀 Starting mux server on port 4000...",
"Check logs at /tmp/mux.log!", "Check logs at /tmp/mux.log!",
@ -111,7 +111,7 @@ chmod +x /tmp/mux/mux`,
expect(output.exitCode).toBe(0); expect(output.exitCode).toBe(0);
const expectedLines = [ const expectedLines = [
"📦 Installing mux via npm into /tmp/mux...", "📦 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", "🥳 mux has been installed in /tmp/mux",
"🚀 Starting mux server on port 4000...", "🚀 Starting mux server on port 4000...",
"Check logs at /tmp/mux.log!", "Check logs at /tmp/mux.log!",

View File

@ -67,6 +67,23 @@ variable "install_version" {
default = "next" 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" { variable "share" {
type = string type = string
default = "owner" default = "owner"
@ -137,6 +154,7 @@ resource "random_password" "mux_auth_token" {
locals { locals {
mux_auth_token = random_password.mux_auth_token.result mux_auth_token = random_password.mux_auth_token.result
registry_url = trimsuffix(var.registry_url, "/")
} }
resource "coder_script" "mux" { resource "coder_script" "mux" {
@ -153,6 +171,8 @@ resource "coder_script" "mux" {
OFFLINE : !var.install, OFFLINE : !var.install,
USE_CACHED : var.use_cached, USE_CACHED : var.use_cached,
AUTH_TOKEN : local.mux_auth_token, AUTH_TOKEN : local.mux_auth_token,
PACKAGE_MANAGER : var.package_manager,
REGISTRY_URL : local.registry_url,
}) })
run_on_start = true run_on_start = true

View File

@ -121,3 +121,96 @@ run "use_cached_only_success" {
use_cached = true 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"
}
}

View File

@ -54,7 +54,7 @@ fi
# If there is no cached install OR we don't want to use a cached install # If there is no cached install OR we don't want to use a cached install
if [ ! -f "$MUX_BINARY" ] || [ "${USE_CACHED}" != true ]; then 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). # Clean up from other install (in case install prefix changed).
if [ -n "$CODER_SCRIPT_BIN_DIR" ] && [ -e "$CODER_SCRIPT_BIN_DIR/mux" ]; then 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")" mkdir -p "$(dirname "$MUX_BINARY")"
if command -v npm > /dev/null 2>&1; then # Determine which package manager to use
echo "📦 Installing mux via npm into ${INSTALL_PREFIX}..." 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" NPM_WORKDIR="${INSTALL_PREFIX}/npm"
mkdir -p "$NPM_WORKDIR" mkdir -p "$NPM_WORKDIR"
cd "$NPM_WORKDIR" || exit 1 cd "$NPM_WORKDIR" || exit 1
if [ ! -f package.json ]; then if [ ! -f package.json ]; then
echo '{}' > package.json echo '{}' > package.json
fi fi
echo "⏭️ Skipping npm lifecycle scripts with --ignore-scripts" echo "⏭️ Skipping lifecycle scripts with --ignore-scripts"
PKG="mux" PKG="mux"
if [ -z "${VERSION}" ] || [ "${VERSION}" = "latest" ]; then if [ -z "${VERSION}" ] || [ "${VERSION}" = "latest" ]; then
PKG_SPEC="$PKG@latest" PKG_SPEC="$PKG@latest"
else else
PKG_SPEC="$PKG@${VERSION}" PKG_SPEC="$PKG@${VERSION}"
fi fi
if ! npm install --no-audit --no-fund --omit=dev --ignore-scripts "$PKG_SPEC"; then INSTALL_OK=true
echo "❌ Failed to install mux via npm" 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 exit 1
fi fi
# Determine the installed binary path # Determine the installed binary path
BIN_DIR="$NPM_WORKDIR/node_modules/.bin" BIN_DIR="$NPM_WORKDIR/node_modules/.bin"
CANDIDATE="$BIN_DIR/mux" CANDIDATE="$BIN_DIR/mux"
if [ ! -f "$CANDIDATE" ]; then 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 exit 1
fi fi
chmod +x "$CANDIDATE" || true chmod +x "$CANDIDATE" || true
ln -sf "$CANDIDATE" "$MUX_BINARY" ln -sf "$CANDIDATE" "$MUX_BINARY"
else else
echo "📥 npm not found; downloading tarball from npm registry..." echo "📥 No package manager found; downloading tarball from registry..."
VERSION_TO_USE="${VERSION}" VERSION_TO_USE="${VERSION}"
if [ -z "$VERSION_TO_USE" ]; then if [ -z "$VERSION_TO_USE" ]; then
VERSION_TO_USE="next" VERSION_TO_USE="next"
fi 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)" META_JSON="$(curl -fsSL "$META_URL" || true)"
if [ -z "$META_JSON" ]; then if [ -z "$META_JSON" ]; then
echo "❌ Failed to fetch npm metadata: $META_URL" 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" echo "❌ Could not determine version for mux"
exit 1 exit 1
fi fi
TARBALL_URL="https://registry.npmjs.org/mux/-/mux-$VERSION_TO_USE.tgz" TARBALL_URL="${REGISTRY_URL}/mux/-/mux-$VERSION_TO_USE.tgz"
fi fi
TMP_DIR="$(mktemp -d)" TMP_DIR="$(mktemp -d)"
TAR_PATH="$TMP_DIR/mux.tgz" TAR_PATH="$TMP_DIR/mux.tgz"