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 }