diff --git a/.icons/mux.svg b/.icons/mux.svg
index 3ee3276d..70dff277 100644
--- a/.icons/mux.svg
+++ b/.icons/mux.svg
@@ -1 +1,11 @@
-
\ No newline at end of file
+
diff --git a/registry/coder/modules/code-server/README.md b/registry/coder/modules/code-server/README.md
index 3312f979..fdb3f1a7 100644
--- a/registry/coder/modules/code-server/README.md
+++ b/registry/coder/modules/code-server/README.md
@@ -14,7 +14,7 @@ Automatically install [code-server](https://github.com/coder/code-server) in a w
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
- version = "1.4.2"
+ version = "1.4.3"
agent_id = coder_agent.example.id
}
```
@@ -29,7 +29,7 @@ module "code-server" {
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
- version = "1.4.2"
+ version = "1.4.3"
agent_id = coder_agent.example.id
install_version = "4.106.3"
}
@@ -43,7 +43,7 @@ Install the Dracula theme from [OpenVSX](https://open-vsx.org/):
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
- version = "1.4.2"
+ version = "1.4.3"
agent_id = coder_agent.example.id
extensions = [
"dracula-theme.theme-dracula"
@@ -61,7 +61,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
- version = "1.4.2"
+ version = "1.4.3"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula"]
settings = {
@@ -78,7 +78,7 @@ Just run code-server in the background, don't fetch it from GitHub:
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
- version = "1.4.2"
+ version = "1.4.3"
agent_id = coder_agent.example.id
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
}
@@ -92,7 +92,7 @@ You can pass additional command-line arguments to code-server using the `additio
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
- version = "1.4.2"
+ version = "1.4.3"
agent_id = coder_agent.example.id
additional_args = "--disable-workspace-trust"
}
@@ -108,7 +108,7 @@ Run an existing copy of code-server if found, otherwise download from GitHub:
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
- version = "1.4.2"
+ version = "1.4.3"
agent_id = coder_agent.example.id
use_cached = true
extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"]
@@ -121,7 +121,7 @@ Just run code-server in the background, don't fetch it from GitHub:
module "code-server" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/code-server/coder"
- version = "1.4.2"
+ version = "1.4.3"
agent_id = coder_agent.example.id
offline = true
}
diff --git a/registry/coder/modules/code-server/main.tf b/registry/coder/modules/code-server/main.tf
index f5651353..090b6a53 100644
--- a/registry/coder/modules/code-server/main.tf
+++ b/registry/coder/modules/code-server/main.tf
@@ -44,7 +44,7 @@ variable "settings" {
default = {}
}
-variable "machine-settings" {
+variable "machine_settings" {
type = any
description = "A map of template level machine settings to apply to code-server. This will be overwritten at each container start."
default = {}
@@ -167,7 +167,7 @@ resource "coder_script" "code-server" {
INSTALL_PREFIX : var.install_prefix,
// This is necessary otherwise the quotes are stripped!
SETTINGS : replace(jsonencode(var.settings), "\"", "\\\""),
- MACHINE_SETTINGS : replace(jsonencode(var.machine-settings), "\"", "\\\""),
+ MACHINE_SETTINGS : replace(jsonencode(var.machine_settings), "\"", "\\\""),
OFFLINE : var.offline,
USE_CACHED : var.use_cached,
USE_CACHED_EXTENSIONS : var.use_cached_extensions,
diff --git a/registry/coder/modules/dotfiles/README.md b/registry/coder/modules/dotfiles/README.md
index 9cb6a45d..c78b80c3 100644
--- a/registry/coder/modules/dotfiles/README.md
+++ b/registry/coder/modules/dotfiles/README.md
@@ -18,7 +18,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
- version = "1.3.0"
+ version = "1.3.2"
agent_id = coder_agent.example.id
}
```
@@ -31,7 +31,7 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
- version = "1.3.0"
+ version = "1.3.2"
agent_id = coder_agent.example.id
}
```
@@ -42,7 +42,7 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
- version = "1.3.0"
+ version = "1.3.2"
agent_id = coder_agent.example.id
user = "root"
}
@@ -54,20 +54,34 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
- version = "1.3.0"
+ version = "1.3.2"
agent_id = coder_agent.example.id
}
module "dotfiles-root" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
- version = "1.3.0"
+ version = "1.3.2"
agent_id = coder_agent.example.id
user = "root"
dotfiles_uri = module.dotfiles.dotfiles_uri
}
```
+## SSH vs HTTPS URLs
+
+If your Git provider (e.g. GitLab, GitHub Enterprise) restricts HTTPS cloning, use an SSH URL instead:
+
+```text
+# HTTPS (may fail if HTTP cloning is disabled)
+https://gitlab.example.com/user/dotfiles.git
+
+# SSH (uses the workspace's SSH key)
+git@gitlab.example.com:user/dotfiles.git
+```
+
+When a Git provider has HTTPS cloning disabled server-side, the clone will silently fail (the `.git` folder may exist but the working tree will be empty). SSH URLs avoid this because they authenticate with the workspace's SSH key instead of a token-based HTTPS flow.
+
## Setting a default dotfiles repository
You can set a default dotfiles repository for all users by setting the `default_dotfiles_uri` variable:
@@ -76,7 +90,7 @@ You can set a default dotfiles repository for all users by setting the `default_
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
- version = "1.3.0"
+ version = "1.3.2"
agent_id = coder_agent.example.id
default_dotfiles_uri = "https://github.com/coder/dotfiles"
}
diff --git a/registry/coder/modules/dotfiles/main.test.ts b/registry/coder/modules/dotfiles/main.test.ts
index 90fe91c8..8cde2510 100644
--- a/registry/coder/modules/dotfiles/main.test.ts
+++ b/registry/coder/modules/dotfiles/main.test.ts
@@ -26,6 +26,7 @@ describe("dotfiles", async () => {
"git@github.com:coder/dotfiles.git",
"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) {
const state = await runTerraformApply(import.meta.dir, {
diff --git a/registry/coder/modules/dotfiles/main.tf b/registry/coder/modules/dotfiles/main.tf
index 40b1a4e0..7b15a391 100644
--- a/registry/coder/modules/dotfiles/main.tf
+++ b/registry/coder/modules/dotfiles/main.tf
@@ -29,7 +29,7 @@ variable "agent_id" {
variable "description" {
type = string
description = "A custom description for the dotfiles parameter. This is shown in the UI - and allows you to customize the instructions you give to your users."
- default = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace"
+ default = "Enter a URL for a [dotfiles repository](https://dotfiles.github.io) to personalize your workspace. Use an SSH URL (e.g. `git@host:user/repo`) if your Git provider restricts HTTPS cloning."
}
variable "default_dotfiles_uri" {
@@ -40,7 +40,7 @@ variable "default_dotfiles_uri" {
validation {
condition = (
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."
}
@@ -55,7 +55,7 @@ variable "dotfiles_uri" {
condition = (
var.dotfiles_uri == null ||
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."
}
@@ -102,7 +102,7 @@ data "coder_parameter" "dotfiles_uri" {
icon = "/icon/dotfiles.svg"
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."
}
}
diff --git a/registry/coder/modules/mux/README.md b/registry/coder/modules/mux/README.md
index b9cfafc0..6a5c3b0f 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.1.0"
+ version = "1.3.1"
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.1.0"
+ version = "1.3.1"
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.1.0"
+ version = "1.3.1"
agent_id = coder_agent.main.id
# Default is "latest"; set to a specific version to pin
install_version = "0.4.0"
@@ -63,9 +63,24 @@ 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.1.0"
+ version = "1.3.1"
agent_id = coder_agent.main.id
- add-project = "/path/to/project"
+ add_project = "/path/to/project"
+}
+```
+
+### Pass Arbitrary `mux server` Arguments
+
+Use `additional_arguments` to append additional arguments to `mux server`.
+The module parses quoted values, so grouped arguments remain intact.
+
+```tf
+module "mux" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/mux/coder"
+ version = "1.3.1"
+ agent_id = coder_agent.main.id
+ additional_arguments = "--open-mode pinned --add-project '/workspaces/my repo'"
}
```
@@ -75,12 +90,40 @@ module "mux" {
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
- version = "1.1.0"
+ version = "1.3.1"
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.1"
+ 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.1"
+ 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:
@@ -89,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.1.0"
+ version = "1.3.1"
agent_id = coder_agent.main.id
use_cached = true
}
@@ -103,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.1.0"
+ version = "1.3.1"
agent_id = coder_agent.main.id
install = false
}
@@ -117,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 96fae5e4..cc2e70db 100644
--- a/registry/coder/modules/mux/main.test.ts
+++ b/registry/coder/modules/mux/main.test.ts
@@ -1,6 +1,11 @@
import { describe, expect, it } from "bun:test";
import {
executeScriptInContainer,
+ execContainer,
+ findResourceInstance,
+ readFileContainer,
+ removeContainer,
+ runContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
@@ -30,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!",
@@ -40,6 +45,57 @@ describe("mux", async () => {
}
}, 60000);
+ it("parses custom additional_arguments", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ install: false,
+ log_path: "/tmp/mux.log",
+ additional_arguments:
+ "--open-mode pinned --add-project '/workspaces/my repo'",
+ });
+
+ const instance = findResourceInstance(state, "coder_script");
+ const id = await runContainer("alpine/curl");
+
+ try {
+ const setup = await execContainer(id, [
+ "sh",
+ "-c",
+ `apk add --no-cache bash >/dev/null
+mkdir -p /tmp/mux
+cat <<'EOF' > /tmp/mux/mux
+#!/usr/bin/env sh
+i=1
+for arg in "$@"; do
+ echo "arg$i=$arg"
+ i=$((i + 1))
+done
+EOF
+chmod +x /tmp/mux/mux`,
+ ]);
+ expect(setup.exitCode).toBe(0);
+
+ const output = await execContainer(id, ["sh", "-c", instance.script]);
+ if (output.exitCode !== 0) {
+ console.log("STDOUT:\n" + output.stdout);
+ console.log("STDERR:\n" + output.stderr);
+ }
+ expect(output.exitCode).toBe(0);
+
+ await execContainer(id, ["sh", "-c", "sleep 1"]);
+ const log = await readFileContainer(id, "/tmp/mux.log");
+ expect(log).toContain("arg1=server");
+ expect(log).toContain("arg2=--port");
+ expect(log).toContain("arg3=4000");
+ expect(log).toContain("arg4=--open-mode");
+ expect(log).toContain("arg5=pinned");
+ expect(log).toContain("arg6=--add-project");
+ expect(log).toContain("arg7=/workspaces/my repo");
+ } finally {
+ await removeContainer(id);
+ }
+ }, 60000);
+
it("runs with npm present", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
@@ -55,7 +111,7 @@ describe("mux", async () => {
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 1eeddecf..ba475b0c 100644
--- a/registry/coder/modules/mux/main.tf
+++ b/registry/coder/modules/mux/main.tf
@@ -49,18 +49,41 @@ variable "log_path" {
default = "/tmp/mux.log"
}
-variable "add-project" {
+variable "add_project" {
type = string
description = "Optional path to add/open as a project in Mux on startup."
default = null
}
+variable "additional_arguments" {
+ type = string
+ description = "Additional command-line arguments to pass to `mux server` (for example: `--add-project /path --open-mode pinned`)."
+ default = ""
+}
+
variable "install_version" {
type = string
description = "The version or dist-tag of Mux to install."
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"
@@ -131,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" {
@@ -141,11 +165,14 @@ resource "coder_script" "mux" {
VERSION : var.install_version,
PORT : var.port,
LOG_PATH : var.log_path,
- ADD_PROJECT : var.add-project == null ? "" : var.add-project,
+ ADD_PROJECT : var.add_project == null ? "" : var.add_project,
+ ADDITIONAL_ARGUMENTS : var.additional_arguments,
INSTALL_PREFIX : var.install_prefix,
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 af103ae2..42569997 100644
--- a/registry/coder/modules/mux/mux.tftest.hcl
+++ b/registry/coder/modules/mux/mux.tftest.hcl
@@ -79,6 +79,20 @@ run "auth_token_in_url" {
}
}
+run "custom_additional_arguments" {
+ command = plan
+
+ variables {
+ agent_id = "foo"
+ additional_arguments = "--open-mode pinned --add-project '/workspaces/my repo'"
+ }
+
+ assert {
+ condition = strcontains(resource.coder_script.mux.script, "--open-mode pinned --add-project '/workspaces/my repo'")
+ error_message = "mux launch script must include the configured additional arguments"
+ }
+}
+
run "custom_version" {
command = plan
@@ -107,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 0d0c6520..2dbd5ea9 100644
--- a/registry/coder/modules/mux/run.sh
+++ b/registry/coder/modules/mux/run.sh
@@ -20,6 +20,22 @@ function run_mux() {
if [ -n "${ADD_PROJECT}" ]; then
set -- "$@" --add-project "${ADD_PROJECT}"
fi
+
+ # Parse additional user-supplied server arguments while preserving quoted groups.
+ if [ -n "${ADDITIONAL_ARGUMENTS}" ]; then
+ local parsed_additional_arguments
+ if ! parsed_additional_arguments="$(printf "%s\n" "${ADDITIONAL_ARGUMENTS}" | xargs -n1 printf "%s\n" 2> /dev/null)"; then
+ echo "❌ Failed to parse additional_arguments. Ensure quotes are balanced."
+ exit 1
+ fi
+ while IFS= read -r parsed_arg; do
+ [ -n "$parsed_arg" ] || continue
+ set -- "$@" "$parsed_arg"
+ done << EOF
+$${parsed_additional_arguments}
+EOF
+ fi
+
echo "🚀 Starting mux server on port $port_value..."
echo "Check logs at ${LOG_PATH}!"
MUX_SERVER_AUTH_TOKEN="$auth_token_value" PORT="$port_value" "$MUX_BINARY" "$@" > "${LOG_PATH}" 2>&1 &
@@ -38,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
@@ -47,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"
@@ -120,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"
diff --git a/scripts/terraform_validate.sh b/scripts/terraform_validate.sh
index 9126e28e..44f95b61 100755
--- a/scripts/terraform_validate.sh
+++ b/scripts/terraform_validate.sh
@@ -11,6 +11,34 @@ set -euo pipefail
#
# This script only validates changed modules. Documentation and template changes are ignored.
+# Validates that Terraform variable names use underscores (snake_case) instead
+# of hyphens. Hyphens are technically valid but deprecated and non-idiomatic.
+# See: https://developer.hashicorp.com/terraform/language/values/variables
+validate_variable_names() {
+ local dir="$1"
+ local found_issues=0
+
+ while IFS= read -r tf_file; do
+ while IFS= read -r match; do
+ local line_num
+ line_num=$(echo "$match" | cut -d: -f1)
+ local line_content
+ line_content=$(echo "$match" | cut -d: -f2-)
+ local var_name
+ var_name=$(echo "$line_content" | sed -n 's/.*variable "\([^"]*\)".*/\1/p')
+
+ if [[ -n "$var_name" ]]; then
+ echo " ERROR: $tf_file:$line_num"
+ echo " Variable \"$var_name\" contains a hyphen."
+ echo " Rename to \"${var_name//-/_}\" (use underscores instead of hyphens)."
+ found_issues=$((found_issues + 1))
+ fi
+ done < <(grep -n 'variable "[^"]*-[^"]*"' "$tf_file" 2> /dev/null || true)
+ done < <(find "$dir" -name '*.tf' -type f | sort)
+
+ return "$found_issues"
+}
+
validate_terraform_directory() {
local dir="$1"
echo "Running \`terraform validate\` in $dir"
@@ -91,6 +119,16 @@ main() {
fi
done
+ echo ""
+ echo "==> Validating Terraform variable names use snake_case..."
+ for dir in $subdirs; do
+ if test -f "$dir/main.tf"; then
+ if ! validate_variable_names "$dir"; then
+ status=1
+ fi
+ fi
+ done
+
exit $status
}