From 843b1f1e5acb18bf70d493d4e6aa63ce5c161c52 Mon Sep 17 00:00:00 2001 From: DevCats Date: Wed, 22 Oct 2025 07:33:09 -0500 Subject: [PATCH 01/36] chore: change copilot default version to latest (#499) ## Description Changes `copilot_version` default to `latest` ## Type of Change - [ ] New module - [ ] New template - [X] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder-labs/modules/copilot` **New version:** `v0.2.2` **Breaking change:** [ ] Yes [X] No ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun fmt`) - [X] Changes tested locally ## Related Issues --- registry/coder-labs/modules/copilot/README.md | 14 +++++++------- registry/coder-labs/modules/copilot/main.tf | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/registry/coder-labs/modules/copilot/README.md b/registry/coder-labs/modules/copilot/README.md index 83f59c7c..e0b520e0 100644 --- a/registry/coder-labs/modules/copilot/README.md +++ b/registry/coder-labs/modules/copilot/README.md @@ -13,7 +13,7 @@ Run [GitHub Copilot CLI](https://docs.github.com/copilot/concepts/agents/about-c ```tf module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.1" + version = "0.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/projects" } @@ -51,7 +51,7 @@ data "coder_parameter" "ai_prompt" { module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.1" + version = "0.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/projects" @@ -71,12 +71,12 @@ Customize tool permissions, MCP servers, and Copilot settings: ```tf module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.1" + version = "0.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/projects" - # Version pinning (defaults to "0.0.334", use "latest" for newest version) - copilot_version = "latest" + # Version pinning (defaults to "latest", use specific version if desired) + copilot_version = "0.0.334" # Tool permissions allow_tools = ["shell(git)", "shell(npm)", "write"] @@ -142,7 +142,7 @@ variable "github_token" { module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.1" + version = "0.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/projects" github_token = var.github_token @@ -156,7 +156,7 @@ Run Copilot as a command-line tool without task reporting or web interface. This ```tf module "copilot" { source = "registry.coder.com/coder-labs/copilot/coder" - version = "0.2.1" + version = "0.2.2" agent_id = coder_agent.example.id workdir = "/home/coder" report_tasks = false diff --git a/registry/coder-labs/modules/copilot/main.tf b/registry/coder-labs/modules/copilot/main.tf index eb9f78d4..41a83d53 100644 --- a/registry/coder-labs/modules/copilot/main.tf +++ b/registry/coder-labs/modules/copilot/main.tf @@ -104,7 +104,7 @@ variable "agentapi_version" { variable "copilot_version" { type = string description = "The version of GitHub Copilot CLI to install. Use 'latest' for the latest version or specify a version like '0.0.334'." - default = "0.0.334" + default = "latest" } variable "report_tasks" { From 51ec6e3212229fb8b2108995f0112acf4efdb341 Mon Sep 17 00:00:00 2001 From: DevCats Date: Wed, 22 Oct 2025 10:58:01 -0500 Subject: [PATCH 02/36] fix: resolve issues with claude-code session resumption (#496) ## Description Fixes session resumption logic by having the continue flag decide whether to continue a workspace based on session history ## Type of Change - [ ] New module - [ ] New template - [X] Bug fix - [X] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/claude-code` **New version:** `v3.2.2` **Breaking change:** [ ] Yes [X] No ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun fmt`) - [X] Changes tested locally ## Related Issues --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- registry/coder/modules/claude-code/README.md | 16 +++-- .../coder/modules/claude-code/main.test.ts | 23 ++++-- registry/coder/modules/claude-code/main.tf | 4 +- .../modules/claude-code/scripts/start.sh | 70 ++++++++++++++----- 4 files changed, 81 insertions(+), 32 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 3e690ce2..d3cee145 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.1" + version = "3.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -32,6 +32,10 @@ module "claude-code" { - You can get the API key from the [Anthropic Console](https://console.anthropic.com/dashboard). - You can get the Session Token using the `claude setup-token` command. This is a long-lived authentication token (requires Claude subscription) +### Session Resumption Behavior + +By default, Claude Code automatically resumes existing conversations when your workspace restarts. Sessions are tracked per workspace directory, so conversations continue where you left off. If no session exists (first start), your `ai_prompt` will run normally. To disable this behavior and always start fresh, set `continue = false` + ## Examples ### Usage with Agent Boundaries @@ -66,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.1" + version = "3.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -102,7 +106,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.1" + version = "3.2.2" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -125,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.1" + version = "3.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -198,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.1" + version = "3.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -255,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.1" + version = "3.2.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index 9c132f1a..a7c2dd14 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -167,7 +167,7 @@ describe("claude-code", async () => { const { id } = await setup({ moduleVariables: { permission_mode: mode, - task_prompt: "test prompt", + ai_prompt: "test prompt", }, }); await execModuleScript(id); @@ -185,7 +185,7 @@ describe("claude-code", async () => { const { id } = await setup({ moduleVariables: { model: model, - task_prompt: "test prompt", + ai_prompt: "test prompt", }, }); await execModuleScript(id); @@ -198,13 +198,24 @@ describe("claude-code", async () => { expect(startLog.stdout).toContain(`--model ${model}`); }); - test("claude-continue-previous-conversation", async () => { + test("claude-continue-resume-existing-session", async () => { const { id } = await setup({ moduleVariables: { continue: "true", - task_prompt: "test prompt", + ai_prompt: "test prompt", }, }); + + // Create a mock session file with the predefined task session ID + const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; + const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; + await execContainer(id, ["mkdir", "-p", sessionDir]); + await execContainer(id, [ + "bash", + "-c", + `touch ${sessionDir}/session-${taskSessionId}.jsonl`, + ]); + await execModuleScript(id); const startLog = await execContainer(id, [ @@ -212,7 +223,9 @@ describe("claude-code", async () => { "-c", "cat /home/coder/.claude-module/agentapi-start.log", ]); - expect(startLog.stdout).toContain("--continue"); + expect(startLog.stdout).toContain("--resume"); + expect(startLog.stdout).toContain(taskSessionId); + expect(startLog.stdout).toContain("Resuming existing task session"); }); test("pre-post-install-scripts", async () => { diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index df3eaaa5..20a0cfee 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -134,8 +134,8 @@ variable "resume_session_id" { variable "continue" { type = bool - description = "Load the most recent conversation in the current directory. Task will fail in a new workspace with no conversation/session to continue" - default = false + description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)." + default = true } variable "dangerously_skip_permissions" { diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index daef71a3..fb3180af 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -64,37 +64,70 @@ function validate_claude_installation() { fi } +TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2" + +task_session_exists() { + if find "$HOME/.claude" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then + return 0 + else + return 1 + fi +} + ARGS=() -function build_claude_args() { +function start_agentapi() { + mkdir -p "$ARG_WORKDIR" + cd "$ARG_WORKDIR" + if [ -n "$ARG_MODEL" ]; then ARGS+=(--model "$ARG_MODEL") fi - if [ -n "$ARG_RESUME_SESSION_ID" ]; then - ARGS+=(--resume "$ARG_RESUME_SESSION_ID") - fi - - if [ "$ARG_CONTINUE" = "true" ]; then - ARGS+=(--continue) - fi - if [ -n "$ARG_PERMISSION_MODE" ]; then ARGS+=(--permission-mode "$ARG_PERMISSION_MODE") fi -} - -function start_agentapi() { - mkdir -p "$ARG_WORKDIR" - cd "$ARG_WORKDIR" - if [ -n "$ARG_AI_PROMPT" ]; then - ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT") - else - if [ -n "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" ]; then + if [ -n "$ARG_RESUME_SESSION_ID" ]; then + echo "Using explicit resume_session_id: $ARG_RESUME_SESSION_ID" + ARGS+=(--resume "$ARG_RESUME_SESSION_ID") + if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then ARGS+=(--dangerously-skip-permissions) fi + elif [ "$ARG_CONTINUE" = "true" ]; then + if task_session_exists; then + echo "Task session detected (ID: $TASK_SESSION_ID)" + ARGS+=(--resume "$TASK_SESSION_ID") + if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then + ARGS+=(--dangerously-skip-permissions) + fi + echo "Resuming existing task session" + else + echo "No existing task session found" + ARGS+=(--session-id "$TASK_SESSION_ID") + if [ -n "$ARG_AI_PROMPT" ]; then + ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT") + echo "Starting new task session with prompt" + else + if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then + ARGS+=(--dangerously-skip-permissions) + fi + echo "Starting new task session" + fi + fi + else + echo "Continue disabled, starting fresh session" + if [ -n "$ARG_AI_PROMPT" ]; then + ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT") + echo "Starting new session with prompt" + else + if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then + ARGS+=(--dangerously-skip-permissions) + fi + echo "Starting claude code session" + fi fi + printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")" if [ "${ARG_ENABLE_BOUNDARY:-false}" = "true" ]; then @@ -140,5 +173,4 @@ function start_agentapi() { } validate_claude_installation -build_claude_args start_agentapi From 0c5a8a2354f6af1938ccc901715899143370fafe Mon Sep 17 00:00:00 2001 From: Eric Paulsen Date: Wed, 22 Oct 2025 17:51:58 +0100 Subject: [PATCH 03/36] add nfs-deployment template (#502) ## Description this PR adds a new template to the registry, which shows how to mount an NFS share to a K8s deployment workspace. ## Type of Change - [ ] New module - [x] New template - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Template Information **Path:** `registry/ericpaulsen/templates/nfs-deployment` ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues None --------- Co-authored-by: DevCats --- .github/typos.toml | 1 + .../templates/nfs-deployment/README.md | 70 ++++ .../templates/nfs-deployment/main.tf | 348 ++++++++++++++++++ 3 files changed, 419 insertions(+) create mode 100644 registry/ericpaulsen/templates/nfs-deployment/README.md create mode 100644 registry/ericpaulsen/templates/nfs-deployment/main.tf diff --git a/.github/typos.toml b/.github/typos.toml index 600a39ba..7ebdacef 100644 --- a/.github/typos.toml +++ b/.github/typos.toml @@ -6,6 +6,7 @@ HashiCorp = "HashiCorp" mavrickrishi = "mavrickrishi" # Username mavrick = "mavrick" # Username inh = "inh" # Option in setpriv command +exportfs = "exportfs" # nfs related binary [files] extend-exclude = ["registry/coder/templates/aws-devcontainer/architecture.svg"] #False positive \ No newline at end of file diff --git a/registry/ericpaulsen/templates/nfs-deployment/README.md b/registry/ericpaulsen/templates/nfs-deployment/README.md new file mode 100644 index 00000000..c2bdbdc6 --- /dev/null +++ b/registry/ericpaulsen/templates/nfs-deployment/README.md @@ -0,0 +1,70 @@ +--- +display_name: "NFS K8s Deployment" +description: "Mount an NFS share to a Coder K8s workspace" +icon: "../../../../.icons/folder.svg" +verified: false +tags: ["kubernetes", "shared-dir", "nfs"] +--- + +# NFS K8s Deployment + +This template provisions a Coder workspace as a Kubernetes Deployment, with an NFS share mounted +as a volume. The NFS share will synchronize the server-side files onto the client (Coder workspace) +When you stop the Coder workspace and rebuild, the NFS share will be re-mounted, and the changes persisted. + +Note the `volume` and `volume_mount` blocks in the deployment and container spec, +respectively: + +```terraform +resource "kubernetes_deployment" "main" { + spec { + template { + spec { + container { + volume_mount { + mount_path = data.coder_parameter.nfs_mount_path.value # mount path in the container + name = "nfs-share" + } + } + volume { + name = "nfs-share" + nfs { + path = data.coder_parameter.nfs_mount_path.value # path to be exported from the server + server = data.coder_parameter.nfs_server.value # server IP address + } + } + } + } + } +} +``` + +## server-side configuration + +1. Create an NFS mount on the server for the clients to access: + + ```console + export NFS_MNT_PATH=/mnt/nfs_share + # Create directory to shaare + sudo mkdir -p $NFS_MNT_PATH + # Assign UID & GIDs access + sudo chown -R uid:gid $NFS_MNT_PATH + sudo chmod 777 $NFS_MNT_PATH + ``` + +1. Grant access to the client by updating the `/etc/exports` file, which + controls the directories shared with remote clients. See + [Red Hat's docs for more information about the configuration options](https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/5/html/deployment_guide/s1-nfs-server-config-exports). + + ```console + # Provides read/write access to clients accessing the NFS from any IP address. + /mnt/nfs_share *(rw,sync,no_subtree_check) + ``` + +1. Export the NFS file share directory. You must do this every time you change + `/etc/exports`. + + ```console + sudo exportfs -a + sudo systemctl restart + ``` diff --git a/registry/ericpaulsen/templates/nfs-deployment/main.tf b/registry/ericpaulsen/templates/nfs-deployment/main.tf new file mode 100644 index 00000000..e8c395e6 --- /dev/null +++ b/registry/ericpaulsen/templates/nfs-deployment/main.tf @@ -0,0 +1,348 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + } + kubernetes = { + source = "hashicorp/kubernetes" + } + } +} + +provider "coder" { +} + +provider "kubernetes" { + config_path = var.use_kubeconfig == true ? "~/.kube/config" : null +} + +variable "use_kubeconfig" { + type = bool + description = <<-EOF + Use host kubeconfig? (true/false) + + Set this to false if the Coder host is itself running as a Pod on the same + Kubernetes cluster as you are deploying workspaces to. + + Set this to true if the Coder host is running outside the Kubernetes cluster + for workspaces. A valid "~/.kube/config" must be present on the Coder host. + EOF + default = false +} + +variable "namespace" { + type = string + description = "The Kubernetes namespace to create workspaces in (must exist prior to creating workspaces). If the Coder host is itself running as a Pod on the same Kubernetes cluster as you are deploying workspaces to, set this to the same namespace." +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +data "coder_parameter" "cpu" { + name = "cpu" + display_name = "CPU" + description = "The number of CPU cores" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 Cores" + value = "2" + } + option { + name = "4 Cores" + value = "4" + } + option { + name = "6 Cores" + value = "6" + } + option { + name = "8 Cores" + value = "8" + } +} + +data "coder_parameter" "memory" { + name = "memory" + display_name = "Memory" + description = "The amount of memory in GB" + default = "2" + icon = "/icon/memory.svg" + mutable = true + option { + name = "2 GB" + value = "2" + } + option { + name = "4 GB" + value = "4" + } + option { + name = "6 GB" + value = "6" + } + option { + name = "8 GB" + value = "8" + } +} + +data "coder_parameter" "home_disk_size" { + name = "home_disk_size" + display_name = "Home disk size" + description = "The size of the home disk in GB" + default = "10" + type = "number" + icon = "/emojis/1f4be.png" + mutable = false + validation { + min = 1 + max = 99999 + } +} + +data "coder_parameter" "nfs_server" { + name = "nfs_server" + type = "string" + display_name = "NFS Server IP" + description = "The NFS server IP address to use for the workspace" +} + +data "coder_parameter" "nfs_mount_path" { + name = "nfs_mount_path" + type = "string" + display_name = "NFS Mount Path" + description = "The path in your workspace container to mount the NFS share to" + default = "/mnt/nfs-share" + validation { + regex = "^/[a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)*$" + error = "NFS mount path must be a valid path in your workspace container" + } +} + +resource "coder_agent" "coder" { + os = "linux" + arch = "amd64" + + # The following metadata blocks are optional. They are used to display + # information about your workspace in the dashboard. You can remove them + # if you don't want to display any information. + # For basic resources, you can use the `coder stat` command. + # If you need more control, you can write your own script. + metadata { + display_name = "CPU Usage" + key = "0_cpu_usage" + script = "coder stat cpu" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "RAM Usage" + key = "1_ram_usage" + script = "coder stat mem" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Home Disk" + key = "3_home_disk" + script = "coder stat disk --path $${HOME}" + interval = 60 + timeout = 1 + } + + metadata { + display_name = "CPU Usage (Host)" + key = "4_cpu_usage_host" + script = "coder stat cpu --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Memory Usage (Host)" + key = "5_mem_usage_host" + script = "coder stat mem --host" + interval = 10 + timeout = 1 + } + + metadata { + display_name = "Load Average (Host)" + key = "6_load_host" + # get load avg scaled by number of cores + script = < Date: Thu, 23 Oct 2025 15:28:58 +1100 Subject: [PATCH 04/36] chore: Update templates from Always to IfNotPresent for image_pull_policy (#501) ## Description Change `image_pull_policy` from `Always` to `IfNotPresent` on Coder owned templates. Given these are a reference point for users and customers and they copy them into their own templates I think it makes sense to encourage the use of caching of images. ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Template Information **Path:** https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes-devcontainer https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes-envbox https://github.com/coder/registry/tree/main/registry/coder/templates/kubernetes ## Testing & Validation - [ ] Tests pass (`bun test`) - [ ] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues None --- registry/coder/templates/kubernetes-devcontainer/main.tf | 4 ++-- registry/coder/templates/kubernetes-envbox/main.tf | 4 ++-- registry/coder/templates/kubernetes/main.tf | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/registry/coder/templates/kubernetes-devcontainer/main.tf b/registry/coder/templates/kubernetes-devcontainer/main.tf index 5e36226d..d391c75a 100644 --- a/registry/coder/templates/kubernetes-devcontainer/main.tf +++ b/registry/coder/templates/kubernetes-devcontainer/main.tf @@ -264,7 +264,7 @@ resource "kubernetes_deployment" "main" { container { name = "dev" image = var.cache_repo == "" ? local.devcontainer_builder_image : envbuilder_cached_image.cached.0.image - image_pull_policy = "Always" + image_pull_policy = "IfNotPresent" security_context { privileged = true } @@ -455,4 +455,4 @@ resource "coder_metadata" "container_info" { key = "cache repo" value = var.cache_repo == "" ? "not enabled" : var.cache_repo } -} \ No newline at end of file +} diff --git a/registry/coder/templates/kubernetes-envbox/main.tf b/registry/coder/templates/kubernetes-envbox/main.tf index e70ad2a3..98543d9c 100644 --- a/registry/coder/templates/kubernetes-envbox/main.tf +++ b/registry/coder/templates/kubernetes-envbox/main.tf @@ -152,7 +152,7 @@ resource "kubernetes_pod" "main" { name = "dev" # We highly recommend pinning this to a specific release of envbox, as the latest tag may change. image = "ghcr.io/coder/envbox:latest" - image_pull_policy = "Always" + image_pull_policy = "IfNotPresent" command = ["/envbox", "docker"] security_context { @@ -310,4 +310,4 @@ resource "kubernetes_pod" "main" { } } } -} \ No newline at end of file +} diff --git a/registry/coder/templates/kubernetes/main.tf b/registry/coder/templates/kubernetes/main.tf index c72316ff..7d7c0aa8 100644 --- a/registry/coder/templates/kubernetes/main.tf +++ b/registry/coder/templates/kubernetes/main.tf @@ -287,7 +287,7 @@ resource "kubernetes_deployment" "main" { container { name = "dev" image = "codercom/enterprise-base:ubuntu" - image_pull_policy = "Always" + image_pull_policy = "IfNotPresent" command = ["sh", "-c", coder_agent.main.init_script] security_context { run_as_user = "1000" From 19519a0a1302a599b32ae0b76de0462b0746afea Mon Sep 17 00:00:00 2001 From: DevCats Date: Thu, 23 Oct 2025 07:39:27 -0500 Subject: [PATCH 05/36] fix: add shebang to zed coder_script (#504) ## Description Add `#!/bin/sh` to zed_settings coder_script ## Type of Change - [ ] New module - [ ] New template - [X] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/zed` **New version:** `v1.1.1` **Breaking change:** [ ] Yes [X] No ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun fmt`) - [X] Changes tested locally ## Related Issues https://github.com/coder/registry/issues/482 --- registry/coder/modules/zed/README.md | 10 +++++----- registry/coder/modules/zed/main.tf | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/registry/coder/modules/zed/README.md b/registry/coder/modules/zed/README.md index 989df54d..132e489c 100644 --- a/registry/coder/modules/zed/README.md +++ b/registry/coder/modules/zed/README.md @@ -19,7 +19,7 @@ Zed is a high-performance, multiplayer code editor from the creators of Atom and module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id } ``` @@ -32,7 +32,7 @@ module "zed" { module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/home/coder/project" } @@ -44,7 +44,7 @@ module "zed" { module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id display_name = "Zed Editor" order = 1 @@ -57,7 +57,7 @@ module "zed" { module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id agent_name = coder_agent.example.name } @@ -73,7 +73,7 @@ You can declaratively set/merge settings with the `settings` input. Provide a JS module "zed" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/zed/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id settings = jsonencode({ diff --git a/registry/coder/modules/zed/main.tf b/registry/coder/modules/zed/main.tf index 94ec69a6..0029672a 100644 --- a/registry/coder/modules/zed/main.tf +++ b/registry/coder/modules/zed/main.tf @@ -73,6 +73,7 @@ resource "coder_script" "zed_settings" { icon = "/icon/zed.svg" run_on_start = true script = <<-EOT + #!/bin/sh set -eu SETTINGS_JSON='${replace(var.settings, "\"", "\\\"")}' if [ -z "$${SETTINGS_JSON}" ] || [ "$${SETTINGS_JSON}" = "{}" ]; then From f7c1be71f76b418c4940f31e70dd63a759aa3420 Mon Sep 17 00:00:00 2001 From: djarbz <30350993+djarbz@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:19:05 -0500 Subject: [PATCH 06/36] Add [copyparty] module (#486) ## Description This PR adds a module to install Copyparty as an alternative to Filebrowser. ## Type of Change - [x] New module - [ ] New template - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/djarbz/modules/copyparty` **New version:** `v0.1.0` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [N/A] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues None --------- Co-authored-by: DevCats --- .icons/copyparty.svg | 210 ++++++++++++++++++ registry/djarbz/.images/avatar.png | Bin 0 -> 1557 bytes .../djarbz/.images/copyparty_screenshot.png | Bin 0 -> 39447 bytes registry/djarbz/README.md | 11 + registry/djarbz/modules/copyparty/README.md | 68 ++++++ .../modules/copyparty/copyparty.tftest.hcl | 181 +++++++++++++++ registry/djarbz/modules/copyparty/main.tf | 174 +++++++++++++++ registry/djarbz/modules/copyparty/run.sh | 100 +++++++++ 8 files changed, 744 insertions(+) create mode 100644 .icons/copyparty.svg create mode 100644 registry/djarbz/.images/avatar.png create mode 100644 registry/djarbz/.images/copyparty_screenshot.png create mode 100644 registry/djarbz/README.md create mode 100644 registry/djarbz/modules/copyparty/README.md create mode 100644 registry/djarbz/modules/copyparty/copyparty.tftest.hcl create mode 100644 registry/djarbz/modules/copyparty/main.tf create mode 100755 registry/djarbz/modules/copyparty/run.sh diff --git a/.icons/copyparty.svg b/.icons/copyparty.svg new file mode 100644 index 00000000..2c4f0d04 --- /dev/null +++ b/.icons/copyparty.svg @@ -0,0 +1,210 @@ + + + copyparty_logo + + + + + + + + + + + + image/svg+xml + + copyparty_logo + github.com/9001/copyparty + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/registry/djarbz/.images/avatar.png b/registry/djarbz/.images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..f60192032e0581ddbb88633f9c8efaa893d41953 GIT binary patch literal 1557 zcmeAS@N?(olHy`uVBq!ia0y~yU|a&i985rwk9xzp%(1l;$4Bkqz=}n|~?D z=Uv~5XV3dEF=jZYYG{^)MzPhqmz?(bpe$|Ebeev}3B8If6niSxZe3>1@W*O8{mQKm e`V0*J|E~~M^7r`XTnjAj89ZJ6T-G@yGywqQMEvFe literal 0 HcmV?d00001 diff --git a/registry/djarbz/.images/copyparty_screenshot.png b/registry/djarbz/.images/copyparty_screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..690c716f9e2184197498a6d6a6a70a426f7c63ed GIT binary patch literal 39447 zcmd42bx@qm8ZU@zkl=2?-CY6!65M5QNMH!=8r(I(ZE$zO;7+h$GdK(cw*Z3^Bm`LU zopZMC-E+6<*4=-$Yl>-k`ti4a-90Zcn(A+Huqdz)5D;*b-pOktAfN&f5Rh;%ke_?3 zMWsLp2*?PUs=5kqT~wcY{(oC2X*g-bkEIaE2wBVo+b@_vfrI_B8a@DCm^Dpm^kq-P$k8B5So$g>3!ul&8wyh7|%Xh5#j@ z!Z9($MMb>~jJ$0nuPZX7LGe;vW-JmaOm>dppVADyzUT($8241kDJcV-qcv2_cuP~I zY=e|Od~lbRerM?|A6;qu^z>xpEG@0U>cpk@z3^>*e*5avL}c*?4J)C&HVt|Iz*PtW2706{*^tUc

WVLNJcUG;# z3h{h!6`5rXuwEqATV|96CwVH&Cn2)FemU_TIV>imwWq#YHl$t(<9N*G{_%8^ta8b}O7-f{zZ4{gWKSzM5iYrB!dCB!?#p#jQ0Rr5X zDW)FYahU~WV@=iR+MFgGKRr{^_CHke!4VMX5R~L)b$yoMph*qN1MSXoLM z9E3Z^A5lA%XT9Q8i$yX_UQHUlQjnf7k$g#7k|rvDLQhI7x}%+6K&neW^by(q3{gyr zfU`VIuZeQhwsT4^Iysp*3DFTdYO)Y|&$muX^QLR&+vTh6MS;^>-}SHO(9ii4;BOr}ZT_0aD#W^Y9?-)vmXg5dAI8=NH)45~% zJCA;$R*yqa;<^$G=ojk={TaoqV+d_ zHCr3RSS(-0pQX_S(~P6Nh~vQur5pLyL9^(mwo+>Qit{MlL9>D7C5mh!jqEC#9wqOw ztk6+o&B#}XhxT8s;)0z`t30z>?57ot7m{j>zuqd-JxdQia;q&~)|~Y!1(biV04xyH zcoPKtrBA|NcUxR8^CZP1h!Iu=r(0Kk&Jk1npsB!6^MbB{uHuYnmr3cZ3O=qBJ7{cJDk+Rx;p4N6xt7<`aLHelnk%(Q&qVtOSJ)(BSOQUVCo2elRV5%QLG%NxW{1 zv31UA+hX<1x>CB{R)V=HS7=25ddN7Ny^g#b?vF5j+TG6)Y#4*csjhOG~aCQ@k8}B#)-|@Y6M*(uzK$6 z#&XpzEV1ZV4ty1Khl>k3PO@Ry_db+(9ZeRbxW@2&>V~d<;S{FQP8%qQ1kFSd;VDTzY&68ANH}VlmFVA2eT{yh*C{_00 zq`eA$EAXjM5NDvoanwR2QmgSg^u7sO{HINMxrN~S;*|%^k{w)*mS`Wv1=?i+p=n}q zw4m38ybMYJ;UKR?^64^0B>BOwQGBLHr)ODa?`VHo9z?K0FScq}r}L=Eh=LI43vEsF z5@sBNoF#MUQ~ZDBo|66Q*6&&sCz!5%#^w5;Xg zX9It!5^3_c^FV8=Uc&w5Ku+T_Fa@ACh$T4w+6%PG|7DV- zawRWVu!T8pKK!o680^mXc^Z?0f_Ikt#7ouo*NKMp^e3;_R|LzIm3rDM>rdi-O)p^V zBysZ_jcFbq4q@R%lP*v!3UsmcXpavzxZ^HfYiM_W0{3W{j#jvv|GXLmrU+l29?t+m8jm|p-F#7Ue@ z-*Yz&e-A5W66h{WQ7PRl)Jb^^dzlAun|AV6wH3O^M*Flo$tR#BWzTosq+bc=!8c75 zJCupgtYul~E7i{q`qay=3%G$Hi@*O8x8|6ujV}m9Rt=ZFT z%*KRcG&7x8qU97&m*4c-D&HAgx+3>n?Gw*T5P;RvhA6aXzEkpFf!9qqN_N8aR05%X zZ&YT~fOb|L5lCN$EzX_K*imjA`gPTG_|#yWtW24@FpCHbSQb%@HV$dU=G~v{xye`{ z-XnmHMq%;T|3tIyd_2-lXyHesQV*B0S|9iT(3q6|@$%*>a%|C-mh`%03 z@J#iY_ZJn_x|C;d^nA0DAXYxzCRu3UPAw$1D#RRIzIGY`^z3x#dv;?^{MVdDyMoPc#lzTS0gB(bw=Ws#s#D@sl50PA*#V#EXZ%!OU`H~npODh5nRp$n5(eO!$Z0C+)4*t0_o0q>Unmc&m0K{gc*EnjheSg_keymm~{+Y>&$e z>h~sPFx|4gOjrj`RcQNNJ(4*J4`y!zMsf3La;@)!fg(gPJZ??Ebs1u2qjI;6_7)Bo zg7|W<^yKvt*aZA5*T^7ma!BD(q#V+dXQ01-+xm$~d_~ucr^6@rJNW71Y+u6r}dYnhehi-ktF;zwu4~E)5Mt ztY0Dh(~NBVQ!s72BSfL~7`cUPI$DcnAJ1le#(Eqq$c*X-cxBAyA|OMfA4Y8XM(Qa1 zS~a0G<%`&nT${D@2bjZ5kprw`eN#nkGPoND)kz9{>?YjwXL^g1?Eu=>y~TxWgL=Q~ z4bL#kqv+@W2QO9_i43iIvi$oS3B7QK1}B8jw7yJ%_gIcLciHRUrtB{?*7v;7s^sQD zp9LNx^If`Su)6rsJiKp;Me3?%igOb9%Lm%i=$rK&EYbCXo!{sS@JGypGu33^>;WU? zB|PYnz=GK(`gH(_;60rgIO>eE`-M~G#XPnO*!%SHjA3trL-ZoK#hJzGtkGUCvf`$~f41{<=m~O=H-18^&;i=!hqz-ggfZrc^A;xbK^5m&_UTE^yFpz^)yqn3@FZ3Tx7@>kfDOPr25- zA#Y7~;6K3{+S`ujG2Kt7&fmfDdr@@04_3_sPZUyGP42@Yu8`;#q(>$1N2mIN4VqqJ z*4B$3)l;+yNx1Zo=IJRFpUgM}NJE^^M}bEzGqwp{0%f?$*r_>*K#`cdKJYr4lmvwU zS1+*ACWuiPUfM7LhOYl87J~WTtabT5{=w;7oCXW#I3^iG^Ry-(^Si;Ajz?o#J~Qf+ z!^twsPP&V(4W^5Z8HwIwbOJv@;OJN1aw5>KEXdHEIsjAMU3C8aDwy14M-<7@vB-=1H%$rsPBJ^W9H6 zS!?Zz?_03o%;czCiLkN6uZw#Hj^_y8?l@@2FJ02L71{R#6U;Nrbr$zv?Kp{KZ(N1_ zfnhe<{gffsiNi1)i8svVDNwO(Rb74Mt+69Y(dy%2K(pOV9b3?~tC?e1y@UoPuPful zcyLfh|JSCkeYDgNvw82>wcH#S?;Kl!r^yD7l>3+F{yG0|`Fc>3>Y0`x>pf+CH8`Mt zYTu4v1G)x%+@>189Ob_QFLOX(06y&QH<-NVPIN%1zfktw63(&{L{?Mu$PGt4lX_H@ zTBTd|05*$jlfN*qWa)lAN|=NlHvlUq8g}sDMzz{? z%^XNzlGskOe6X%39cF{1eXRNB013?{w&?=m`ozR35%a}owr(fheR02G{zeMm0l}U9 zp6Z;?m2+lJ>8v~DvR&t1|4x-@Co_;u70S7^Qji`y62yGpw+DCT(Uc+$>aq|+_lx5G zk(*oO&HXOU=$Q8DOC6wDOnyB4GSAfctl3*{`T7oiEgujlTW72$2uV)2jzA>*g|=^_ z_RB6S=$qK5gC9akdF_MdW*zyx>Ziloa9x`Z^2oJ|VlJpnNp&IL;IN5X*Fmu4o*V1< z04&F{oofh2Dv4`i61xZI;m_u(KTRDYl5|REV0fz@@J4Uy(Lz1~912Ek>DONhJ&`0+ z0HX(eY;Z{w1MPn0yhB~D2EGLCW^&cG=82qM+P2(nY>oABM)1DTfL&`Ri2A2}nLu8? zzSbrmpz12SP<^~m`Jh4&T$7_-H>XLwB#4K@(SN0p$vRHx>@LPZbZIl+m8-Bb<6GwF z=htETvD!0W&%D`xagcL%wh7P8m*Qri08S`FAU4~iq!H*mL%)P>H-!x>|4FTLoYqbG zdkr{GlL_&+0$-#3()+lQv67-P3RPQ_m$+{%e(NZ%vG95=u=J5Ja_n^*-e+$9*uz$A z;D`W_dP-3YU3RnPgkmZ4!Q+-pbo?{irh6;+8fNsLAP*ZUEW@dNSLzS7cBw^}<4im#h(>82!ex5W5v|Et7&e*k}EMZRsg(Q8%k9hIu z>2ouj5Je?mGiM$qnlmNratCI}E|rdWzgaV?+`PQ-0KkPkz?k!ei>vyutW=TvVaHPX z4oNb`gaQ2|k5O{Y;pwH8E+0T@ILcFbS_SjSMtW`=do>~;&rY;(*jL^4LQ!Lk0l43h z-tIgtpwf)ZfI^RBPP)WL#qGL$K5gC>Vv~@3W=%StMiJEYKpp3f2zn3Cq201b7tNUy zo(e$zNZ~?_2Rz8O1(ftw$NsiUdfFI4Q_0FrPnS!@SURE-#dX97=5l6^3*ql^j=&Ae zdU;MBrwWjM3%&rmyJv`*#YI{Y)kf1LySEsv7D_G|tSjDwQNv7mBnr9%c({RF3(I98 zgl$E`?C%^XJ+=TW*$E>sznr+Gg#}7Yej=-W4ms%`UQKcMypTAbCj<&d4ZTsGeuIz zmB((B=q#ZQ$ljtVdvHS=Q7M-fnYk&M3C+=yFv9>W=FZLzw?}h=rf}F2NC>t(Q)Xdt zk7vnNz@Xwli4P>TH$g||!LyUJYo%rEt8~8RwXF(KWld#o)?LJCUkmG8i3cJ7IDPk@SWKBh5hk|>6%Gc6yz4o5|q2Q z2|sM*sIKmeCq8A`<`mL+$Hf_u*iG#78Cl>DzC2A$cZDa1ysNOwn@D=6@to5{*bJ1a zQB5e?a3aNol?Z;%=1`@LEHL*zs-QI+%tkzACxkA&T`&Mx6+Vjuj|sgi_4}L(HdtIp zRsav?G~YFM;{pe&kS^f;><8TQd_+O?L;DUb7TM_^0ch2_o#;&rzWaK7pC}Qx85G@x z?a4#14muhP-&bCHhmp5l-|!5_+sxD2^e_(#7Lfdy6j#QftB?Tt>r^JX<7Z-m6T%2X zTKY#*SaMy`O~N)eb%Rd#-?ojT<(TJaL;}bc$-Su|Cy{Q~B<1kWpQHI8(d$Z-Ic_!6 zSbVr{Ho--wTiUko$$>h&evwcv&uveA+goS_Y481xzO1Bi~p{rF! zNKzFNLO3-x!`8ROS|{Kv!6^o`9XGLq+}Nn8udQ=bjfU|gJ7X;6^Djw- zJkLEoBif`Ne&pD>7+v7LSD8Qxisx<)D`?FGo2y=XBN+kQ&SD6shPP_;9q3kJk5^Mk zZt2Uz69e)psM9vgMw4z@;BH|SH!a22TlJ*CQz2@VsZ0Spa~3*n^LN3|TNz-~jQVQ{ z{-QK5&s~Qvj#HRGF`LgI@E@6rO*XoqBAa{jopA8wZ#)#)r+iRafgYk`gKL54(ML3$ zd9)efpW)zxX*`!QdJH6Bnlzc(5u)Q~e%vN~)N-bYA%I46$XMv8OA9F|f(`>TFS=5V zWW+^SrbmV1F8quQ-(YKO&&wN~%fcpH*O{dNM-cZ9=+`p1#qsquCmwJ%g>ZutMRucR z02VVUh}x9xx*=lQ>C*BZinY=Q3H=oc#zzSOb3?Ahvv~P&Lthx>g7XA3XG<&$C`-hE zBQoEzBA=z|{kHuy!-tE8E>VlsBcGW8Hu_c52uyRFT9H73JYSPS>_F7Xh_(E-Z=Z2r zZz|GFUzhSloD0c6D*tj7=JNZNDoYs!HlmntgeE)k&Y}M^OB{4eITMeZA#1lONYANq7;GM#3@#2Z-%@?^ESfz4 zCUPijYZ}vV6DD>H9D`4oOI9Jt_?y!xl~BNzrhaxQDmP0XM4#`l95nV9gp9VI@l3)I%c%WdMpJ$V=q>F!=|+#ho&&2M{O<^> z-(uOm61~qGhRue8!~SuFDkK(ZdA3{#(nAV?3H;M#1<6wy1Q!_YEr7uabXr( z)q;&MCO0EWW7nm)!!Y-wn*f1V35wfU&~!!Dv&+{+54URur+=o3y`?9~phyTmT%c${ zifGTZyxePe{oXE2&|UFk`01qZ6F8qMhKFf7*M~+M0<}BVq)`m5QbQ!;FS|th@(MD)wv|j z;?G15_MR^w1tL_0))nw>tQ3`ctbVqQ!hlf6c5EyUBk-jHV&CyKJXT@}M!vjSVkc-) zEKyf+E9XX{MTGFf35pAUc-}M1nly#jrMsE_k7IVvV}(<2NYAmc(mZFa$w_wWFXUQR zapg|0Kh5qZSy9FJ_IucO@*XKkbTp_4KKirZ-O|@!x4i=K-KrOW2dA61Y611}m`kEs zeMtA}Kt(|7!bDh|rtRMeB!4F?g7c0}!Ut_|6|35H^-HfuE6e%RXJg#|PQZLWhN^K1 z_XrJ%10tTmiKRG}VGrNIU$er#+OeyAY;06Er#02R9669U_d8#Nh|On}`FRCC8_RV9 zhu($BwfjZi=H5H^)fZ#J= ztBuTS=HF6*9`5zAvXztt!)2 z!n#^r`7J9H1Zp_2)a%d_?p#~#4;hz^?2T4pBy}8P$enjXq@lXdKP_NwVKxKCjTu(Vd{jh>6`R{`V=7}eNU za7QTS@>Sq)RPq;;s|4_@~sI$mFa8$|cIp5v=^wp-TC|7=IOG=ai zwk16E4NlrHp?f#r?c13W;H$xuZ(zOmbI>ZS(rep3C86Rn52(JoB&B)96qtS7l-uZe z8M({DsY6{MjdvP|^pX?O`JFmj9`e3%#Pn-^!cc}B3wP8dTDey+rLXWBG<`5=^T_L& zOY_thgA4eeT*M{XsqT0{`11o*vj&GhE`dYOh?d*z%6^gO&ZkLmm*%HkR!0^FLK^wE zXtTy_fc>WyU(G?}NR`MzA^8yFOgu<8AL z$-yiJ^l*$iV+P6{R9q*vWUA)l#lOZbbyRtTad;7rWVxH?GbMociK8^XIb3`f72{$i z#7R+YZE@NYtEY4ywj;+>r&ExN+6VSMuGz;8H;u3_Jrue9&$|Fo`};gMH(_s6dYwxT z!CfTO{J_sDN5F(#^b5bmZU2-!wP9oLUW3KewGx(`wyXSO1*caYFS0ElB_4U#HQ`fRR^daDyaeWN2|sSBUdGxA%w2 zj&#h{czw;jS#PI@61ojs4uu~1t>f&R>aEe83^v^|Ew*$zT6>Hq^R;vt_G7yN(mA$^ zd-FXFa9dVm(*P;EzJZy{vAQr_)9sx2B3)fgoS=4W1nA)e6!)9?K`GEtr0sM>(mzXj zqzpoCK*PpeDLF76|D^E9hb+eAf3YmEr8G7kDP*jslQxlwfN#A)GMw|7+WT0Nwh}KH#ske`Ni$pZI?p_>bs+?;QTc_wS7V<-mW`hRIck#b!^t zy(rdF>00&>zvxYz?RYsPY+c!yS}7+(gi}slGaT7=kJz%7_6lSXhvt!;Qr$QU+K)^Y z{P25cW#Z!`G@JfMH6`Sivi;EFhp(MoDTWJ*3Y;l^1JQh8w z`yH(3Jki4-Z4X%(d%FWOavwjR6$u>M7CQ!9tf~ES82`BZ=-K)(_D%R)(GeBOJq(^a z%WS8EqFtgnok{0T>#u#$*P(17q-hed!x1amL>VzY-;7XEJ9f~6`A{Zo1XqE-W6g9V z*wj+^dU1&M=|p|MsAz=QN^mgH>bl=qYE|9&vR7c4uUV4vqyEW{Nn3XMAZ59_hR7vqdz@y%@4m0fGuxY| zz|_t2c2#5gn)U;(AdF^j^>07TUe{9uPsj%@jB)OuS}+Vb|=vE)u)`h6OQe9sUE`h zXWduGKeo^>1Yeb!h!w3#PPPmJwfwxQDJmNFYM%1D-S-`-l#tHwii0}~!UjA=@$X;v zxlEyC_gNq?F{dCBGHWyP;TLG4eTd)eCyxxle1VR>uCX+DF5d0()TgKCeOVY)I{k&> zb2*lwW1QV}an=1%6+<_N{2VRNvQz;-o;5@T6OCKYVr_RS6HM*4fHrBl-(s@Qxr|>+ z>B~{>lFR_qc{No|9c5vGV~n-{wDIWldn)jx$%vEd&y_sK5SE0Ce#tyzgcL3*$lh>b}*`ltcMeT3oWWKWW zkGqmc(flc5=b$7{vV0$rgoQx*7HQ6t$ZNh=>%sZyXi<0r7nV87+B?6DEM40di2c!? z0ElD6%CVgdE-BIW1l>Lx>EUlN44EMo81pc|dAN0hx*ICnjA-1B%7G#a~OVB;NA+hiE`z)1NtD zbQyA29?YzzH9e^UEYq@1XNcFw%9f7GDU(d}ta_s59?fkkw$`f6IQ1)4QZq(L*#`wt6&3M^_#g3}h9}!hf_rT()tHbo3 zJbZrG&n~HKtK%y-a9!xKhRD@m@<)bMO#(28!c#4u9v!06?W%LI)+YfjD(+*uMnf1c zXDJ!)XV60Y>EYoZ)QSe-m+g12my}5fOsT>5kmiOK5*=A|3;y;{gK=nR=A)KD+9ae_OYv+L;#zD~W zZ2boG|d{F#jpq$;{6ah<#5cHtsUYc7Gcdx;Y%I z!Pf5n39B9B_#RD^3^5R0TI^F8NNCtkCTorhGUKn>mhz7(RdcykKkyzN zS;=mUUrcw`d)TX&(e1^1c;jcAVZYqWEtA%Y{z-YXYx}*bs&80CxEdWiCt-+9G!Cx; zci+J>{~Di}6X^bFl?W}yxk$P8r#~L(*Q$qIrSj?(L+v!XXfpV!{ClR|i16X;rI6Uo zDO7vRW10ha%>Vg3e9$v3odAzh^gT)US+H1K(D+iAQJT44l1orn%)@PWLD~@oH|qqN zYz@seNwZMSyv);fsdqj>)azW?8*UhB^Rr^pYgwA?ntbtWB-a|E?`L#>UKQsUo!st( z#1yaM`fu`FjkCCU%gTn^jxLp|6?0z(ctMuBPRn#ud_9rPa@Q6~_7HzJyzf_v`UZ1) zT0v8H4P8R#(0-r6e&U!kFXkwy6JM>44Qw(e1^-z;5?6-|^vV-8G=L1nAN_$~{2HA- z_)1#ulf>T8z=gfLa7xye^#sV;(h{IT=W!5hR|qJc0FXSFJx=%Zi02|U{_VN^rLeSw zTo1#7zBW8_(_G4zQtf#>CQYmF)v7P2x?A)vt*@79&#FK5OUr%AdxZ13U00*w0sl1$ z>zrAHX*lh{P3s#k^Dp!NSV8!KS>OrSdL;O@(O*$w;~?vQ^#Pun*VMKA|K0olAo~B_ zZ0vG`NdLKUNeLu>fo5*vYGxWx#U0o>VgBTc_OSZU0isqv7Xb8+*kUv;d*677QaPHK za8YtO*}umkeLI>cvD~PNJ8$5GTOD<26zC0HF;*#^_&nEnV3VeL{Dm+xm1i!+(2vk* zxe=zytd#hZR>Jm*l=H0<=aIP6YD6w^2{KzZMdKWCY`S8u7A0}*!tsONo5uw^aRo5W zQ!V@JN~Z?V7tx^mjCBLR72 zxvJ$O8UR-G+^w%YaExBj3)dt`9#J5VdbUM?4J`4iYrP!`G^oq!@`ey$d*Q-|}(;d&oR64!5iAsoM55)crXs!FU ztF*+u!%zAxlk4wMV+;O?`J06>_GpBo+qL23$tHXQiBqZoV0DfgM6F7-GtkT zmb!0>A4G#kxJ#U#Z|=}>eZ_UL5~5<~c}J=b@#TpkXXzjDzOA6I!G;a7#Kg8toU&=D z0KHWDQ#XkxZZXAt)|ulmUfrR+hoabCHJ*p@%N6h1O!}H8ikvF0wGhzE)g%$c%)k+Mq*9`N%`0GUc-8-1E)mF++@I(ow!@);w@%0a*^+In5Fy-EL#7&{5e>k^ckrb`SJ^~0fPmpq+>nN(_k2yCo5!&gFk01)f zvvLR6)PFu!bc-NbHB7kCd|Qi~1^y1Zf6GNf!wICNNxMb)67UJ3R9adP3W-!YMWcPS zb2vg5Jyeq>q8WyWG7J{Ox8;2!qZdc+(}e)RzzV~cmg|G(Z*6a2-uYEa<8n9T74$6s zvI)x~xon40N8^f84iX0aj&%zIw}0)8^g^5wPm1{H-tt)0WSTi&^eGMehFWbEO-^H! zX{)aJ9l=qBCZd%*lVBjST`b20FJnyPHzG+E`mSE4q8*BWokF@c*>RnUHz2(Rf#x=O zzb$G^!BvC0X!=Y%vNBva-AIHBe3XqXm?_S!??D06n|59**xP2?#-Fv>T`X_<$ znLl*~$wC4Z>DdE{B{VBg%mQW7KLnSa%5Zz2h{8}7r~?u3ShvbnC^ zz0xpt-V<+1K(N%*>BQJ~aByAOwug)GUtS9iR%1=m&I5vX$X7b-TuAEVLw*5?1-Wa+ zFUL&D3v}7 z2hZz5w3E7h3ntu`%*cuBPcX5Z#o;8T;3rG|E){{O{cS5u6leD@eQ5N&-;}u-NX?m9 zfVjwOIq3XdeSQ2WZWv;Yk1 z?*w}eKSl6=3cizlzC?Q^ipxK`WY{RY9tIBDv$YG^rb`mHjb{t|_Fgy4n){AtM6Rgb zQxVk=B7U&7Vr;v-C_d!)>*)Bam_!$eyWxbosru`9l`x?5|Dx z^CF;+T6Mi8^5F)jI^0w=4mARlX0Mr_-QVM_#x3mmUWD|=%K@w`FceBI2Ap=Lqvy!m zNy#Yx;Z5lPg{5{iZfJf1B?tm_#v(zb)zQ77<3bxXYAj+o^g#FBr-%JE5Vd)%ZZE2U zGj3@2@sXw1LfN2h|48|hB`h_dOSNP z^V2J!j@$g&Cq;;2Hdw43Aqnikb*bW6=WYS1N?^DV?RpWDds zQGu{mlpz91drp3gc}jJq)_!y+VLng?m4+ID65_?VGnC^^`9w&-ZZ@qPA0Pxk+Am=3 z2x10gh_rNS&kCI=`Wr|r4a6$gpfM60QBYh`D=NVY;@W!ec5PH^L>+Oqt3p-z_Dy{_ z*=3oa`7|Kn^OVjC6sEQHla*sg-Kfuv+|n?$%LEcw{&rT2z$Kj^CdR&jZoQC^P0nMQ zilSMsOqdiw#E)+dB0#W|SPpvYaN_}_^e3w)vhD>J@Yi5oPTvxTIJyqz^y}h2WNWgv zOdL_lBOj`O#{Ffta2Tth(gffNs{B!<1_@~XIwAn2SN7@(-iZ_1#;s{IU3I!u!{}1= zqfw#6i7N0>H&egrkOG3subcPOzk%W=N2QgohH6_X!Z157zt+c2!=o@Rr0Q!;&{@MX zGD-pA*8et4o_QHhEDux?5-b-W`4~?D2^Aqm#%khObkVOCD!%uTRbO`YV7GYyW9>$Q zebj}9JlZ$Ezf)i|{S%Pin+*0t4rxOk0MuZA|F;XqINaahlRYP*0M$ohswl9|52+IU zID&2+s}b7Xkm;#m2NqyPr<+$}tb->mQ18zh4Q{o!b7K2ry~=^eprw2T5}ks0yKJ@S ze*OU_nnz}On;7sF4Drr+qY1&GgEA?Hk`&w`w^u{yjG#-*cL;t8o2p}B%g;bxcf|ix ze#Vk{4wx&4F8q{02(+3DhQe-nFqSF)FbTU~r(1bCqq9*bQ7_6=ubbhrnK5A^zp(u^ z#fLaZ#y){|AR*RiWMNGb{#33)yy;P#?`a>MHP>^Nt(T}Vm=Lc)~DkjR*sf(TAcDZ$jBZYt)xM9`2d z#e?Hw{uca0_OJbHN^!X#L$|SRIj2P8f6&b2K(kd}rESur{e@HyBzdMBb8}P@U|tau zFkof7v?L4puxGcVq-2+*3}eA&&sEeHlMU}y##!zs&aR$X*{rmt?zFvLD6}Gy4j`5p z36`;Tm0=1%h6F~y@ufWDesxRNsldeEyOVud9fI8fKb1ng0w9o#GlImNkq!)H8FNY* ziMvE+o}Zn@mRt0vZ;SPkD-Ctj*o+Ln5E)!Zt#W$kR$&zxS)g=(!ca%1l*!SIlnB8) zO+Al0IB~}+nM8#R6%z%N%|50(kHc7poaG1+v-7q%BgUDPKCsXJeVDE=f4XETnwOAc zh4|0_T_r8S0-)T}`ynS5;JLf;pGFjC{$Hp<(cbi}c)QG>7e**|7?zX3q`a9Nl{kl= zD~N$B=JkKd^;R;~?M9SH9CvDRG|hOwI)@v7*%!GDwU--#MuH-40nnH}5RO>R2PC0S z(eKClS5R4wWt(K}0kDQo_TnRF?@A6mBMW&N9irL$49L=BW#tVMstB;ZT))Mt@AgRU ztNN%pk!No88|U+AVTNW5b%fQb>tVODq|)G&56Cx$J9K8`VJrC~!7p*|l(F3!f3j*G zE;ihkrNyP;j+0tdd}R~A=t@{W5>fmJ9^?xuI}E0p`;9Vdt&C(hhd?hE{a8#D^l8zb zsE`hh*@6%yrvF*hx+BHr$GxJG1|ui21SkZVMJKrskc_6S;)--EqT%P4`x9#Z3n#tK z=DiKGj`>osrVdl)W6ojkyR;1t%_@q*=0oso_Mq_ChZw2$@KpgFfs>#5+zj40Hq4E} zUQPv-a_!?1>>nV~OsgaB`@@&^p(uYysj7*Fo1x8{_CKTyrdCW=<~t@yD|>Cm*RM}K zO~catjt4z@jO#z^y?E-bbKilF59p**TfEyuUg?KEKmgY?b4 z1b?oVC3o_h7Y7P!{1x8qjXL{qOFthpP=c9A(qHmNiS=GI5%t1_*v^w0u zOLm0(_pNN6IY~=&-sp|6q~RBlhlgq zdYdy|)zfF{ksVa441%#1sO7>{HAx`iY;jJI>)oZ#;N0z%?sMS|@2G8ueR)4Zh<5pJr1zVmE3D0`JeUM6b(t-xW$tw?Vx4-YVY~C9xz6Df7@sun7!D|&ZT>h>G(6v_ zcN2=_*%~Ta->kTP8_z0eHi>gAZ}*9!AM{Jqmwg`4Y*0y&oyLt|L zH79t2rEHWVb=>Ic&nt@cEPqJ1Q6~$*FD!p!Dyo(qX@E|YWH8xxvMr)XVL6!7`^@GW z!V%2yu@A6?$d2K$vXs4u?9E1;7UB;7zz`G@onjh~j}@ZE;zVzqKln0Id%nM+5}LHz ztAPSC2Mgfa--(a~SK}HZy0T4Hu%1Ldb85FSTp!R$Cih4*leR<|pq24v$++mK-?YK(| zbNb8-XGF_pfya!I9eaa*ZN?M3))JpFOFa$GNXOw@CU$C! zTxdU)zvgm1#saicZJwGyTu8mLD^e@%cMiK)IkiFvTTG@iS-Iyyxs8{bRLMZUKXwCcgq3sDV#VNIO`ujuY`VO35W< z3#HnH922FJ<;!a%da`h#6bu%1$)u6~-~qmPz(6P;8v1bm(2a)Zy185R@6rYLVQI6w z8q-kfW$kL(`FV2#y;l7tE>lSQM$Z>;OY|=(fA@uoETzohy#QiHD3UQ;kKgHMHr#r3skiZqXvds&4ZXSFTW0K zWB>ht?^KjKHHivUIj_fyqXU2R>;Gp8_9`g%Of@{L#; z{^2}&ni?xIH9{+$c|$s-c<4l4;4d05$0)`+C;zSG!hivj*$8x~z(`9I$kEthfq%11 z^sk{%fF}9KD?6!}sWu&;E?jVpG(VJnl=+y}PQL|tZv5%(g9DD=0*FfdWAs_F^OO4p z95yspk{uz<@3o@9J~Jc&T%a_)Fz{~{Wh$GP3Iu7NNvIUk;5*7@KuJ6A>ogyX-m>nzr2sP7}Iw*-g-u11_)oG?@GLNF$ zE$~a=04+X2LgVzkyoSa)8+l9Vektn)CwQ|OpBa$5Wz9^{r;({U=D!-~LtadASI z)}0d=8&*Uq_dTuOZzjlR zk7L#>AMjKFE}8IRL> zn@`q5?dP>=te1FdwcbLKmUm9*U{fc$>b*{@C3i5=ht!6`_4`PH*{d~nF|U4ondPRN zKTUgtt0;rax#(X?B7MFjkINS*C!E57%msicv+8FR$8u*A6lODiWyR}Ao9pgh%G6|?cvV)P>jgw~lT(h7qjUoX1>i?a-O?g2 z705BNe4>BR74k;`C-wB2t?7$44goNK*vazerq|i75k+n9S@l#imAwdBIFOrE*wD;Z ztcERMJKyHzr}eorS&h3Tt9>MwRN?KX_fW;7;X`2N@<6sa>{uOZNGlKnCP}Z22HZOI z&?|74uj>-HEH>)BV*zsk9|iT+A6OpPyE_&jUn=5#MBjYhSTnS=-lN!ac=2w@m8?BJ zcT!psGq870&t5^L=X$+XeM)ub#=Ci+zO)*Y<)S|z49Cp(Cf03NIAYd+L*2lR4+8Z* zc`_P1X?j@y-soMK|3Z^h7z+^I)(f(N{H3t_FTYQI26$oJGHv2NTIJqL(Cx+)RNTPy ze-`~W2hsmOs@gx4z3?G{z<;}=j%v>HlfY(YudbGTL5|5fUTLI$J zK}U?|^UISqr37XNcBtO-z=zJ-%#!z8Q=*PS0dbIT>?JlRQD zT(_x=3=#>-(vpAHYzQD7>8W_D!g#LD7(*)Bdu?8~-Pl*BSj@#hR|Lh~qU&M~++xj^ z1J?*N$EDczCzqAeLkPg{>)J+1NTL=4aw{yv7Q@gz7-CyIr4b);y>h+!Ucdt{jj42X&Gnt#2~k*Z@Uugf+0W+ zaUjb27b2iqr`E)NF0~A@-KFOvo}dzPr2|ii3rxKZ@Ah(xEtJXd=cY)6Xl&}y480ri z@Jz^!vla;FO1ly+yS^BEQr2T#=$4lL;eL3Xr9at7!B5APn6GJw`mdx4m!yY%yNOO@wWy%d*pC5OAO+8MI1k{R~#_wVfRn+656c69i8fP`b{{Tb7O4`+ zH*CTEAXMtG&Qf;+m>+-U=^GxcxJZUmB?w9duYyq@Sd@HKt-DrFbss{7_q?WBj$l2z z31hfnE{ZMmbj~bQCZ=fG`IC+Iti~$4RnZh9B)b$xpjqM5Elv8g!&MD>N5Ag)^e4CN zaFk?8cG|1?+e)qBAIjc5x!%T`*gz8B8Kvt_kze=TfpVufY8im_;=Ep{+WCg3<9>Se zZ@F4?QBd5xe8`1IS!6^DO>-Mb_L{q={b>uo_9+8HHXXj>i8@Ef4D9(d$}`d6abLa} zG@u;tB_vrtYD=mcWdc`yYA4AFO>O7oKm<}5%2%}RX|9W>ebGU~JNbG@rJ?a^J+Zmo zo^gjpp^QSKhT+`8VLJ%uH<$2#*6F%D!AXKS3ybB=KmjL20mPX}A>8_4qdT--K9Hw# zVXAmb^sXmjPCU0y*Y#GM)MT70aWiExxo??Rx9U|Nn$%dxGe(#O z1X6@cHK}fp$8+xGMJ36C8XhRXI31jgf;(d-uW}?>`Z@gThelQS#x5iaUn=w|onAO4 zMm;ST{@yIIK8Bf;b@_TyT0a7eUw~Ac>EoXUojIb?65WT^9~M{BN!ok-06BKs#aAw3 z9SQZU-Ej=X_{T*&YYJ{dVU;!)#govImQW&g-`!th;n5b#_yW;e25$TT-9`1QVkrH> z(R<5XgI7=l)kuJ@pUq-|(*@sX6P?t70;^y=L@6FT>F!C0sM=QYl4 z*t1Cj&#N0NXl>7G9y=HTaDoBZK2NcS`wANLli0=LR+6cUPwwZoSpb=Y1)Zc{yYJMR zR6Xd*Qcc%IxY$98h*s{km?B;L(qkn!4|z|7RsE%4k^-ak$|0hcP%5FUD>70XGgo4I z3&bKxQYCvA*&oUT7=^o9Y`CXo68h<$gtP&r4K-|#?cC|)TZiC?Fy5>qEURj@W_9_8 zKkR$berPjMyRmiOqDyBB7&!j)nAaMsfDpL3%EUBK>Q(BGK~)nT_^X7U5OypeMWW&H zhF>nx04<1NmRmZ?0|r#by?g3zy7H!@W+R}X&jKCDZF#5lz-OH|w11PY;*n{et8QT| zrHlSape73Lf>n%pnTpo2iwuzHZ7Aa{A+S2`r-6c|mbwYkA!>gFozolqT1E>MtR5pe z_uET6nQ7X0ev46l=#(-e`ZNX<=Lc*Y3CHDdP_P=u$a{T!B+SK`Qp~FRDkE7!&*;J2 zn{2!F)NMS&R;}|ecEiY(-!HE@%IqmRh~ghzw;pM^*h*+P=DD` zX=v!;vN{TuMhIE|)QYha`uWzt`}r*2RqxziJ{u=dT+`{->OE$$j?Fu&Bl*Y4nVBpr zG~XO^-)r#v7l@N9^7MyUK{aQmzhIJQ^_7GF=k-jy;-9>aSP#eW1|Or(`)6j zhJ>8?K=;sJQh(O>BhWABY!Pd67Y-0?_;6PYU|#{LXe?M;BL#dR=2d1hjiO?5@wH*@ zTF3#N5K^z@~ zkkX$99WrJto96F<;2dO$vgk<|3t40x8Vi-SQDn1*f&AZo3Ixt8yzWB;9YCAoo&)wW zWd=wm9o*=JPH?G{8A>APmR>6yw6dMC49edfa`)=P!%7Eh!zm1d_+X?TGdM1mms#9i zb#|jIeWS4Ar zU`SS__X?QO33rZr+1z${K~##3YL^WXa^U7gdTTLIH)$Y}?@J79D`JNruVc#Tk$d*N zSDcUvYC%GOjU-aqX3MNoEE$1{XY?$sA;-IOZCAO0bV@`kzk<=JP+4*dMeXfwVZ=aT z2H>OuV6|aY?PwY!!|M_&N&~H*25iL`mYy~VNSTAb7fw2)rf3Rd>H`OqUy@Zk@5YP+g}!9L!W z)%plEhdaA~0myo0AyDx^Ibl=WOMxvCY$8nn#)1r`QQrgQnP#7s_pze3iIV5xH$!(K9_2>Q_yL#6-5r&FoVAsI}mDR z=gRmq1esL1EG7>q;K)j$hVvQ@pHFdngf0DQU+lE8>|px;>ZAM)zJSX~6ex1t1jMdu zqcgd58&4VBGD(y^eVI^QF%lzUeFhWn<$*m^b=1Xn0QhaVzs@@#x{>@<+qA$n48$F5 z5ugI`wiRUhFQz8AV(9&kO!n7FQF)0huS1j~`a}}xRV^W&INrWmaGBc=yniqwpRnQnTf};a z?2q8y%L%^5Xh#qd78Kr6H-`MtM6q!8?f_D3Qfa2fm{2-J#c(vWlVl{KX@Y+88eqwW zZ{eMYK_J80R3CruNcwMYV;yRqzIaoZxEx|Cp!W*Rc zRpj0~NKtS=tZupj#-o-93Hka^V6%-{ssRfHbyTxYHMACe6ZNG^Q31(SymD-qlC2Q` z{e<^#E}+F|g1O^HIbHWa1FkZs!}w++$okioH#!%hpz>m9m$Y+8VF%K~*qysq>d}|f zicS3MQ@hdq%br8d^K^+@x;lpK|wn-OiQ)P2xk2x;1u;0kjj zMJ`*QR}T1klogxcBgtm(FXqaHj@&--Jn$FH#q*Qz0Hv_6G43=PMkJy3GWHdc4+>a; zs`gi>dk%b-z2rT*3o&+qVHH$KeFgDgQgI`9dQ@NzXrY20gJvLui({i0U0k!g11o!c zEWo+^R5jcX4vX7eS8#Cymp=(ph)UB2Sq^bXKQzrroLcMBwe@l9WUw0!!?bnl z1vGEjE&uG@Z2Z@BN!%#@Pgl){UD)7`TUsfnXmP*6R+WpcpdU4;+IeHFIyCTsPGu?& z{9ZHUwUiZ<)^iom4SJ5aVE_?9(18))f8(?MKYeoiL-PLyrRSKfgYbVP6$CZ#B8%PF z968w%?>W42k=0a#{*KZssg24vo*bymv(D)Aal73veEM~=mScA9>-`Uccdeq?d7vBL zx#-pvva@MmHg_^(4%Jp zNR-tD_5W~Cugv&A=fMAk40YdhxNFfe`46AdK>=4e|FHI7)Xb8=Wnh~ZDv*gNwRmy9 zP%h+sPM%7F-Ysl(eOUjrMQ;RZ8~3uWcACpMd7z0_Wsj6OU_E4yYE2C))bFdSDeevPg(tJ$~wf89a3 z6Z6)0t84%yAat?3%l$+5!oje#07K46@#Ab{MkkoIUm8U9Qk^U)2qbaHhwV7T;{W!m zJ@hw~Da{27EB;{u7QV>25My%qa{fA~4FTJ#X{KC-c64VA{Pd4;qDaJX)4dAn0**~# z=$mq7rtSA4hEgDtyDpicJ18KELfHpPqoqf9<9^rXFzQf+maGli@M=;U&*<{soZ7yN6|#_}#y&8t%1;kWHTht4li z{6vA&IwO6Ezn@%XAZ42g!T&E5iU(TT zt&+FW2*Dq%wy;j?H-3Lb@9Y#}ulXSm0UH_NN%4uCAD?p!N>9E)Apc#Mb&ymbW?^{P zbush+0M>qA&MKTK*qsg*ZSVL#&oJx4#rKTKq#`pRR7RNBBn6b>++;QjmEiJklpJ^oIuw>35= zJGlM{GdN?7`pssjG&848>+3pRPKOw-@XHj@xj{kq8woPb1U)hZ1?CKGYpglJ7Od-n z<(?-eCy~3-k6LE|r{X4NzG06Azk@B~cwwPO?7*ySSImDj^`N~+d%(buz9Bkkbto!n zvC4qqw^j%^k1((PO~MMzMT^{s4JROX0xI!wO~Sp4;(|7+FOA;pg8JEMB=~d|ZLsI^ z{WUeGx<#}lQWl4PnG`94oXn+8cuW;->&HW=DWZpcWUnd%Bq{Gcfi!p8-NV07>NdFV z81Z@yza0`R`6}}XfKLT9}6X1$6bKafmqu{lZu8jkWxh|p%XzN23 z*O@!n0&9`^-f(Ww7`Jz1H)f@RMYrzk&o9PFjPl_9>5F~{t_tFRe`58eTGeqg+GXpB z9(1CZc^{a*BU8xC6#~Aihf|VY_S1FOW#W0$N1N(^h1{B|A%HsV2SdbHCM{o=Yarho z6tAnnsFHK%mz_#Kh2pYA>4}Gp#+}10O7U?EcZ>P-ZVJFX2K%FvZ`td%x46Bt*{WIq zY_mf9xVPX6)iiMuD8OOiVjs=tGA2pZcBU&3MfG`6R?#}S^9`!S#x>>M3x-8~gLfo$ z9qY=`mE%?fjZ;7Pi)$-Q_BPkgvx}Wo*J0_L5V;>in|GE zaw7-&iQ*YRZkHeKHa~tt`CkKHo^ z!#+eTvb&~lNEAy$z6~)+d+Zm2G$?myWyzdKe-daUN874GSSaB=BUayZtn%8RC_wDthG&TCJ`F0cH4ZmgPg@Wz>{U6UNkT{xgpAH=W* zN6XsX&$`hw*6F~N4$k1apwkSvrTSXaYfu7+CBg-IGAaUClflXRRbx)c?82Lybt3#s zlv7g`AW^%5bHrxnpFPjPKelG9AVUY=>PLzRv%%p-37CSOR#6d>yKH3`jLzv`>Axy$ z?7%5Nk)(V2dT_BNM3GpwgXE`WMP+mrO>VMF)j+=>a1IL-DEwo9L%=0{B~&bP2u54+ z1pw^Q%AEQcx~a`CrpOpN`MO(J7Jab!&@$j6wq_m(8ImloeRN@>-)8ecFl}4u_efo5 z0$zUh5l$2xCn7pEAV#WMmHX0^MF{3n`Hlq{g9_(tOwqgJ1E0K|KZpcwgQ#$Uec?n> zbU-tl^1wkesXU(q@Xs8uFdIgy1M<))i`@A(ZxhNAz+`w zJ+0tbL7xPixJ2}v6+JL)TCJrjDm!<=fEFj>qvV*|ZK&BXqD2VU_#P8;F;0|%``1Vw z7IUUhuuilliQ}^bBg_E2fW%rB~9N;E6@}Oa(H0 zK`0UAwxS_R5l|VEnHvkngU3rqgCdN=V>;jH}ePz2Z$A6H|=seGYP zCV92d*WKV~YNMg^d4r6HHmwVM)XU1)VAFkwJ^<+F({YD(0_Iu`*4c%5^IDjFciIf- z8XZTDXswCfgGch7R8X_Bf|#n^a^b?4KayJOq4)iM z1o}AfxGRfxiAJ2xqgk5W3q4aWc17ktGmoAzFppI14hXFQ0%Gl0kO-IIXa=A)>&Q4Y ze>h?rfsL%`GeEvEcHrmyAJTipNFU9mT!K>9JH7<04!>)MaQr70KuS6BLLTY#;>ltW96w-H|Hf?Y#Q|Y-+?s+0B7+G@5%Qvg%hJZe zv*WwRr;{C25xkpAJ{JZOqEEsfD;Mpwdr0~N?94aeAHVoa*!iU=Qc)iIA+tWBDw(Y) z6N5j85xIDY4=hLBsbzr$X5VpuUjcW{gJb_=5N`;0pk`ZmWT)Wo z$WMyRz=n(SnbO8o`mVXmVCu8A?>)AH_WV|Gvv*9h$b`-=k=wyv1tj)})$>-s@J|1- z)AtH&-XQJQL;c<2Ot7$OAW8AGb=chYk#e*<`e2z ziVs=Of%ibq%#8x0ywis){N;)@cqWa#h9Oyuez;tmtzWzSyy%uaNV-O=m$b+iNinke z#->R$hnL>&tvh&m2TjP`oJ)9Ra}4m*8uV~?>-B=>PRzE{r&7*TbMj=)bszWvb*6uE z9N7}Gb_`ItF`zupjrM(SK7Lmqi|@dBS(Jt8Ml5Y}1!~%3{=r}M+NgjkVT*0!aL8ma zP(wHhTBY{$$^6jlJ%Lx~Oj0LTUq%vSQi^)0F{&w(EbL40qin-b+;SfT^7{TZf_R~~xI-9*Jl9@K6-LNKL z1^X}BmiR15RWNXrh)82rY{*B4EqD$wN13l3{)WWxLw^H6QuHm*r$nCn01)A~X1qjQ zRLTl_xG%y_;iE?97;;A)%Nc_@NOm=dM>~831l*D?AX#h!V?l%WP*Aiw1CYiqpn5^f z8WOolwQaSNqFLl0xzvE`$mM2KSXOv-ltzDvrAB^8UeV5!)5p&xUCDQD*it z_`#S5Uqc5=o22>oIyDq7Wx>c$=%{r+Q9k)DBU53>V;KFZ)!zvtpsuuz!E!gIPMxiKB^ z1#W~|3=L3M1AgW=5VS_us z2E{lOC}MWz-c1F}fIu6rb9So!4KPK3(Fp(|^lzxq6mYrypLKSClj~Ok{|i9T0jocE z1yB9~BktEop&Ldx)NX!egpI8eH^Gj(>D6=5svPj7zZ=N&AE?d*-}okjJ{{zhpjjI!`#7KyZ zHd`;RG>py<{7D|Nk5R9T8vQz^cu%_ubSq&G>#~ze>)Dmi)kB^Au9XH50~~u8_}R49 z`|dTlEYI^VeFhNwS>!^?zUtD8Z|Tt^Qj?+eRIPwrcg>RVOYXkaTolP273x@7|)mUcMcy7&k zZq3-BlHjn%Px;Q%wbp>Az}LHQRq0@|p>BN_!G8o(BkXwDf(bkk+ANR}7%9Q?yS?BT zhMp>+3x4rmUN_Yi)3EU|cmtSaEr9x{iun4Bp#0TtQ{ zQIB;^S6z`>$wvWKZabtmyw=}Vfq}WA9gyoW|A3}};z8%t*wQ;3oQ#cqzwt4_4$7hb z%tQ{9rfl#^jR^>hQLN*-XbbU5jV)W#dzEzeKaxIsmGs%Gq}TrFqz@<7I|+n<4aWXq z*x&qsD?djH==QyM`t4a)U1s=~6pG*ULPsV!9U*(^cu;7>-`)w#5h7c9f*ODBoJCSf z7qp0+D8y?NM2(Ty*^JJ8nh~1%1NjDa0WbW@=9I<*ig8U@F?R?Ea5Yo>IU4*EiZZA_ zkMq2CD$ozb8wo^mX`yBnXzqSn8yn|;k~6lnDC+&12407(j>+eFCue!;Z_;ZWR;k%{_D*eAJ!3Qe5!~goP*s{uUkpry+J|aUepk@#CNlDJ9RFq=gfR+;t3m*-Cypo z%{bR5UrwOXaO|%9b1@wn%5jtZ7x@5)>Rw$V;7&ev1)jhrcg{#wIHyhjewUNJ>@GF? zJ*%bOY7PnKIwR-&>H~Gpgw|9vSj(+hTRm>_C)Y6ah|*5%TplA4H#7Kr1}1WL_;F(J zbOmZ$H;@d9)YID%O*?AzA`pDqKHcWkgZz~O<$eR_AX%?|;GS^3eN7bz+WD%KYck2M zS+}}{1PeyH2=pYHQ&pkP^=H|x@UrN5fFX(cs=}aYoQ`x`xAu@Av5Ia5PPUPyP2+_qr_hQ`9X1ViS zGxhOYZa`;WZv498aIN093V^JS(m8x8sr!-s%j`C7UiVVIcKp-8S#Ol1k)a`yCPCzV zAkkj-#L{jf3n(B^i^RZgg15T{(8$)uW2t0s5r3q_WUZ1h>oN8PQfO{-9B!zaKF0+T zq@kw}2~PkXxnb0n#&|f`@qhb$>X@j@+n9Q_kt%!YvjbPi$K+(d--9?ICV4Z8Zx(P z|JLg#)Nc#+)Flx1A|+t!7vvs&es_DDfLxZtci*e=LZg~+D>jh^Ui<>NOFu9TF8oIOB{q06+X!l+w|bKj*rj1vx1AC61Ll1TZEfn$6b)yy)bI%4 z4#TXneW$F5TO8*_xPxzGuJ=xb5RK&C2AS3(U~ZQ%2NUD|lR?X9nHwlVeummtr3Ts? z;LAV6FI#7!ibBQEYWe0RE1Y}H*sB#4xx2?ohR_0hOUa8*E?)A^^7~E`ftwB0u!1;a z0%n{V>S*zf;8?PhD_pu8C3{jAJ=fdLE1`=KbEt8#35Eq@9m@^sr96!mdQE)vcQ{|w z5o|fy<-*U?w#m0*7DJ)$(0&{-qt2S#@`7s zaljm|n|R5%=}Jz6^_2}!XAZigP8DXW;jFxmUSEIcv;Da7dfZ1+o*$HZuoU)s5$AhJ zS5S#wevn`GU#i8YjYOwEthRLqhOduwDR{LTyqrkiTsf23igP~KyS;}r7+DWBui%ZN zrU;K6obt2Hs&x$Or%_2_KIlu$U7bs;>S z78C5LU7?+xk&$a5GQeeV^tS(-{j%5~9vs67zp9*7IF=P2a^|-z+v-%iW~<5)7w`9? zYHxsdtl0g%%~4;uQ02l?Wb@Dx5)i#UBOV-~T#)SCS6*Jv!BZwU?=&sCrqA*UQf=~U zzOSeI^nGNRD}mIQh0gIUEhv3BQ>*g+#4rqll1wc$;mIIWuoqj|Y7VH(Q__grxhL$F zG_-6!Z%ZsuwF%c;jjMp>>rvv7w&C_{(}mFeE4{4ocUP%fs>hNi@|}aBN7p|6djrH2 zi*NjIJ(6;YliO#|3=2Du8JWoJDG{C>mgl$Mp;d(WyKY62?YQ;>u^z`r@_V+)A0|L!XI^;EO+TJ}1+eY8` z)cYYbP&BUgZP~=>*rrW+i34gv?*@J|fxQPv@9H(7~sT zjKA(5MDglg%#QM%wqUbSVx}La-vD3uDInRsXW%}YfL+XE7j3u&wWzfkoI&r_uR*6> z&BUj$gv~_i7Ib!!GMkhn)B(0o`K(dgIaDfY!8pMrikeMWNpxwD(B3jEtDd*XcU&YFe|mu^B6-D< z-z>Au35U|(@DG~zZ(%DlVV2Zm=A6LwR;RsvhuNpg0dph5bk`~F|Zdw6bx(i^_ zq7+o6YlW8^dUd4|jgysO%ob?}^7xPBs;!)(}Hd1x29225U@$( zg)`+nh3&UDzNXa~(BS9VnrlSz`fod)(yM(O?lkLx=-_07ImJ0kNa}J-ve|(jrbELB z8?{^(RlAXEi>F~S_a^O8q0O|tqDF2QtKU&8B6mm0dt)SZe4~Qu-No3Qc5O*tJ^`=^ z5RbCgarJ#iEq++I<*;~lW`q#wVB^1Od-sMo8a1wfT;#sNPyQU@$Wi$Dq6BLwBx`p7 zcJ*WI?1fBicrAya(y3!ir(~9Op>pG?T=DjrB06{n>Vf_e7M|Og)#Nu-I=swVIb51c zTyWU|rQY)3Z7%?#NgO_ep2f0oDx0JOlf<5oa{sFF`69F{7ulCb74B3;JR- z$a*F6fE3etY&(lK1MlCklwYJInOCy#@=0Obzc2Lre&+`<8VLP}5SX>!kpTaSEkyrC z##fH6Ax%dVHWqw4!%QOaq0qjOHP1>pmp=M*d$!9w`Xul)u;M z=xqz(2ctzy-G zq&*~YXS7LdkO$1xll*Xd(=^5r8Y(j z5C%;vmuq9rGi{b1PUsT-NR+1yG11HIKnXMHC5P zXzW{I$cQa3X)fr4L78Ou^%Qd6@mLq0NkElm02O^Up(H}aG*W9iaTC|CfnoKkpP6Wv z1NXKmC}fUyh--#S;YGH*#?n2wmLt$E!+d^sn}Me}BIi{oX3p)~=G8$yarXMoOJfD=ITUscm@S1G&5X^HvHA=Ha|IwCCPW`EQr{oQluHb)x!farbg}^ssrXSmGhr19 zK5xgsGJf67A&L^_E)JE5Dc{q)EQjJ-VSPBo`Ie7Apl-2msHC)Glp%MglK`?YvTIj4 z`;rO`hYKE42u1w4Zs_@sb1R(RDm!q+goxWKiquREq;Vo3%r)Zm*QoUFUyGa8w0f?k z&W5Z{;K;gqQzt`Fs$l(bTsrCNq~2g&S^noec*p9cJbsPqqSCF!mJa}T)CST}_6&fC zFPUxLl0$o@rTQXc_gNalm&z4j&p0$AHSkyG#=*%R$vE_->HF%_Y>Na zzUQ$Xw_1hV8{XaH*fVLMFG8u(3PZ^<=i} z*A_c_ND`VT*fBw76bgo8*F>(*1bT4;@b7!|OZTrvt+o!!J+j>)>m;{l*&2c9z!w3w zfD7DR>UI@xi_dx0n++}-yEC}>bGKgtd3`?Qd4dAI33de6h#pG}OHfS{)9EI|+#LDB^VX$?xQ+fIwrc}?YuRhxBUSdPw=GRpXUu5S(_O}?oMdv9VJEm{ zT1sztay23sDTe`%)ub83%Sv4oXWV=MN?S2GGx69+OrPms6=ggS@WyV>9eS_ooQ~b#_qd{Z`m8;X3PRwqDBlVj{THLL|@K>DS@%+0i52 z#@^U&&YmP7prox!_r%D4!~#!g9Mrj|sLFAacNG!GFBpJ6(O~@pZAxLXlty4VCy)Q? z^=ZP)+rh1!&)K_afg78(_>|i$15{#pYvOmz)?{sVPW*o zV#AWjfsHZ?Qiq18bKYDu7<7b_BGt)1EOc}{b+GCwLq>Qp%*ehEN^+CxN!SIijy``n zH*t5!)hME-4Akm!+?N*I%0qW(LIt;bxy@=YJ5`y_T1yBt5dvqhn!1k|k@vpe6r?22g8YH%R zFh7S!@0V{t-klcjwTkkjpx+N%l^FRMDajQf^j{Jrfj+vbf(1q6M!i9uA)@@{J1W$1 zcO7)z-l(~a)<*k27>seyO9o?lIrM`8b^X8l?l>1XVuA`z6^MuGxo`cx4P9Om`=m!) zdVXDK-xF=($VMa85jI;#e|uz1@S#xF%Yh!`@x^X7()jZS-U3DJ)XD+R-S{bN#RK(h zkq5CY)bY;#m)NLA~n z%62uJA(%MPaj@<)1Bx%$Q$;*=cPGw_?+G(`dBEuj-~l?9rcxZt6KImYy=0dWQjyfO zo=yG!5~Am%iEeH%4X;#6sY_96nayE=P4O6ZGe8m0`C~DVDbnK02T&EH(i0m3F@~5{ z34CfbrdoKG;2)=e5Ywl%J01UNlGWfmY1qQ*_hk*AUq9jMX6s)#Qi;RanG?5DQnNI(Lp=; zZCFG(x-U?_Gr#m|k>%7#Q9s=QkIoYPyR_-PF5N0FyX}z0?ZH{eew)d}K!sn*V8>hqZ`BHGFAKcS3 z8Gu1@6KBr?pe}*5_p}%uF}$iD?RI%hTt2ChQIgW9&J7Gm>=6p)acBnC30F=JX%giU z&b0HSokiI-2^BIJZx!?y=31WrzCVmxF=Lyff^@5vNg=)^)9AXZEBM%sIWH&DsyXv{ zUX>nx!M4t$s9&M_sLonLcu>=2{aZGNUIhL;z8xCITFf+O5tVo7@SCdTPG)mf3P~mw zGn?}J0yPYGG0gC?9#;7Rc9#5i0vP@ny@(4ld*yl8U85FgTySv2MhY^L+NXy~;l3wF zh4Vgz)k|e%L>`g6w#2;Dw@ra-jH(nE!TK0{5x9IS=#i@SfM={P#e!hbu0vXEj$^=T46LFwZM_SRpPkt|=r z-XN(;8y1-Q+m9cgSmWV;4;KDZ{Kc?d{(S*pSiDtjDL&nB1KF^+Rlvcdji99+3eNm?^Yn4x#-X zuK1Hd@`+Acm0I88k1eEBvH^~oEW$z*jVPv)-KD!S76bkr`!WOlb~|FZF;cWFDJw@- zlH%jO@2U+K6UIDWZ@E)?)DMx1Ua~m9$O$3&ccdbgwvl*HBr*%WPr-$l4O$6vT9H|r zCZAD*kB=3NLoN{+<=-IM7J~ZxRO-c~BSP4tl#t<0k}Lq>N7Lz(O~I0@fWtRb;mjUm z;qbD!lx!@R=5MZB`Sr#+hUUa}mu)^0k!DyX#l^(qWIs*N$-b#nU83IigqX387f(Pr zaV|1!F?c;n5LC#Zp{ME0Zaq6Lc!(XHsBfbF=uBvhScJYf$mif*eO5P>k1dj{xcF-TSYs z5hV>}$d<6IKNDtbrSk3dwB>=` z7u>A>{GS{#>OA{b??Ul4Bji+6OZ{FW^d5PiXi25_FS`=2@9z3E^N(skwLE3rNxR2T zLssO8#}B8lJ(+JWNf`CCc({ugdKjiB;+Z;5oZhh;J$24L7gM=k56)E``LJgt{R*bA z!9l*+Jj}-N3g_i_pR8-b&Ej{5tzrzWF&sOrc)G%F-AQgwWe+K{+t8s`yJd;`DY;@} zoM@~4ez_$Qw9n8C1|%N)&r#NUaS>6tsVQnS$!7F|vILEsq9H4UN4Y?kYT;4g4~P}X zsz1V{vT%^qQAbk+8bd*bzJFT^P_3-&d)j|sO83|<%7oTET3kwa&`h*+R`WyYg_Fg( z{ZHE>t{>OzMdUfc%Bn6%={3x25tjdnaJ#dx*?n-TZF!9I*0m4Ai0(e)bzO3$6z;CZR}7qU1!kH_Igl)u^QjF`4J zEvu7}o3-01Z0|!ozrli+sAGTZ8f7wSo}@uwb;3*%8OHDYDc6xVHU&8d>L*j z%J)e1sgrve{!i*L(5J=u4C?#zkN-y@fE+HWPLnIALJJ&v$4 zTD;V}(!6A+_3DRtve|rGqsQG}rw1{c4yrTL!|w}frG4F>tkwKkUwq>^8rv{qeg<=I zMH5qV@9>Z2_4ROG?|!dY!RqSjb5wX@Z4k=vq&d06e z8#Xr$pSW1=o7wJ+?%r+999d7)-w&{AzJG6B`8BT5ZN8GfVMCdp==3h{B3|D>(JZ%P z53RId(aSSUqPf$u{N1YfV@k^>MAz0GoRZ`1@+=iE-(Ip5OufGfwUZP3?)UcCqbuiN zqI+bX?LDA!pQF0>$)U6)rj9LJP1R@MTGwC0#GcB&KCXS4V(L}0N2a@Lpmfu_2j+9N zk8}PSD2ObK+sa*h73;kEs+rr?E-f`7eoh@V3 z_v+q$m)@oCR?{38H82jr?34Z+-EFzP$WCMMD|v!5zUCYTPn-K+P!Aw9jmUZY9vqTu-2K+w&N0CLdMbQ2>e@iWRz zw(#z+0n5^D)j~geUpK_^%0salh80_(2kNq7uZP68(!6R;VW)6` zUolSeEuqd!!^0NWWNKnu%1O3M-<{1Tf|`zDye@`*W4G7(VopGlPm{^BueYDgWg^KuKe%qsMBRR{+ZO;YdY)SXN!y+?gl`( zpT6FL+g2OL3U^<=YI%pJD%bF9$t`*9>%qm@;78iD`GdrG^Y%Y~R>&4_q^3F01$_Oe zFaFl#Or_%QmKK%BdIp35b@)+5pV8c+#ssLECo9iYyz{3iXVmgVR{e?S z#O}*gZN9+sNmu^+p==NUE3C<2K_K>P?f00Dmd`G`Nc%rl8yiu}0x1)-q{@KK1P?Q) zv)l-$$9vLaYmT*+E#KQ)X;JZRM|oP zj&MpBr|B(`+L8b!qZz0k)m)FWkGuP*x7Pp>lUClZa#wEgy7q8H%cRzk4_o^j-nPUGD4&^8c8=>~A?hk$hsk~PVXXxwnj1{;Bb*jR09f=Yb!8rCF5MLeR1)GCCL5r%V3;&{TWe%a7+1|v)kexNH7k0*d&tsKtevwpCs z2-tM&tH2>*4PVLflrr5&i@+acsfi~TVt6@>nMcIdyg9*vTloHHyv$Cs2@3?SjKown zf_1`8%sNRq1}~o~b+@aDM`W(---lrSvM&}BjDtl%us_P|^Mui;_R#8ok|woC>;ondUOPBM$*nDE^% ziGo8%{3FSIxPA0x1i_SSU`Zyn62A&+!`G6d*lk1Wr@;QgPl`1B%NZXE7&CU5n!}jK zX19=}0|bwr!|Gt?Vh#T@1jd)BT7A-{^$=>fi1c39vSa5~$BO0eKYl2p^xF3?gD><{t1o3jZ416ep6(_lkW^_V`tq zF$n>+++h1xwbU*l(8fXoK@F-_D;`$k_tJFbP1(Wa(DegFVCY&9=B`h zCt*m|LwGSzuPJ4B?B9MPhQ1s$!GJZ6tR?x=I<5KTw2 zXX4|vvS64thO|Z@h8UI;Tdi20`_1^Im+i+DU)m2zU?9vGp+%v+dUj-R*#u*%OpDBQ zt)dJ$+@N;_d!B@v=4axBFwML3E%*g^EGZgsV@S4T@PXk}QM4o=rI*7zMlB&ED1(r# zfx|f`y|eW$-8{`enC8Fm^1}(mA5LuzN;7xph=$!!xzcKr`?~9aUcgFtfJPk`m~j(^ zKQ%sD;Zp2q*d3gTa%PQmOZ}TNbzD)D-;Vk*^4W9tK7^?N9AC`kHtz*#qbiDgbH)Z^ z=PBa_ixV7G-uYY$^+A>iS_zN`bg1=mpVh>(EIg!WWx+(02kR_OC>(a!av$ta*jgSx zm;&d^F^@7Y0RFf>caR&Szz9omVq@5PnAt_tvNLuMi)-BZ5>8eSy9?)^*C`ttr8na# zh9IzSRz1blFOg0}O`SHJ*R7NCi(?a|2w=qXC?P|6*CDSH*jCsw$mqfPj?Z9(%~4kR zGspUa{3A#IpMF$;nk>-3JQiZnb>6xkJ(Kp(G)QqG0SV7n4=866fKDVv1k8L}nUKtz z3aDaAwf{24*wd7Z5dE;o8CY`mvtq?c0b?d%5`ZX`65tW2S1+j=1pR4E8I{f z<8?SX{gEl@VYKPO`Sbiss2ZGPCa}G)qJX?NtAkhh}-0$2wVg%=sl2( zkP)^!Q7uisf_lygGsSX6zvzlYz?#BUB!J4u^FyzuA7rr|p+h^S)Aivlur7 z?0Y}Q2IDY`?h9w*rS~zY()q*jD#%xkyeU(%_9up@%ONE#{p6_cXtLNFfqqA(G`#zF%y zRpAHnB8mW}8~~~ls8Pokb-SOU(%1AUC&}Z4UmcyEJ3okUh?g%2Mo)@BYAlQrvWBNXevM4XapvBeQtCvZuoSPDgPGeNQ51^+<6oT_SR2O;dLMp6f1%b z)Q@mWDvKzEm%bBl(~cYjSW`P-yFh|VzdNs}&x?b}tLtz3z8wQ|XK7<%9ut%$R*J@2 zg^p8;kZ$Cr`L8?aWO(LqZNIA(Qj46zIlx#`nK*qBHN)A(^ofZ`B-~fFkjtjFV_glsf&E3dkaL49?7>m6WQ`itl?Du#M!wJe{JwvhzJn zFQaJruRf}-pAU8mXku=M_IgGrh$d(zmk0~LCGPHjv{`e|=hk@5z>gCz8snD_28G>- zRj=#jCgYCl%M4qzY`II;3ekK7mUJX1de9;P>BYUYIK_2xTh@~qUV;Wfvo}IAb1--j z@EiEqk%IYhy^-lEDq9XG;;vRQ<${vsg+NeFcz6HkJdbvY3BS0`0GCL3RkKpDZ_xhx2vz+o+Xj}m<(Ki&5l2>H+^t8 oJU(8F&KOlQkWsQ4OY7U(z1OMroIvqjicTcH#F2en8PDgXcg literal 0 HcmV?d00001 diff --git a/registry/djarbz/README.md b/registry/djarbz/README.md new file mode 100644 index 00000000..2319441a --- /dev/null +++ b/registry/djarbz/README.md @@ -0,0 +1,11 @@ +--- +display_name: "Austin" +bio: "IT Pro by day, script kiddie at night." +avatar: "./.images/avatar.png" +github: "djarbz" +status: "community" +--- + +# Austin + +I like to program as a hobby. diff --git a/registry/djarbz/modules/copyparty/README.md b/registry/djarbz/modules/copyparty/README.md new file mode 100644 index 00000000..fdd3ad56 --- /dev/null +++ b/registry/djarbz/modules/copyparty/README.md @@ -0,0 +1,68 @@ +--- +display_name: copyparty +description: A web based file explorer alternative to Filebrowser. +icon: ../../../../.icons/copyparty.svg +verified: false +tags: [files, filebrowser, web, copyparty] +--- + +# copyparty + + + +This module installs Copyparty, an alternative to Filebrowser. +[Copyparty](https://github.com/9001/copyparty) is a portable file server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps + +```tf +module "copyparty" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/djarbz/copyparty/coder" + version = "1.0.0" +} +``` + + + +![copyparty-browser-fs8](../../.images/copyparty_screenshot.png) + +## Examples + +### Example 1 + +Some basic command line options: + +```tf +module "copyparty" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/djarbz/copyparty/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + arguments = [ + "-v", "/home/coder/:/home:r", # Share home directory (read-only) + "-v", "${local.repo_dir}:/repo:rw", # Share project directory (read-write) + "-e2dsa", # Enables general file indexing" + ] +} +``` + +### Example 2 + +```tf +module "copyparty" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/djarbz/copyparty/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + subdomain = true + arguments = [ + "-v", "/tmp:/tmp:r", # Share tmp directory (read-only) + "-v", "/home/coder/:/home:rw", # Share home directory (read-write) + "-v", "${local.root_dir}:/work:A:c,dotsrch", # Share work directory (All Perms) + "-e2dsa", # Enables general file indexing" + "--re-maxage", "900", # Rescan filesystem for changes every SEC + "--see-dots", # Show dotfiles by default if user has correct permissions on volume + "--xff-src=lan", # List of trusted reverse-proxy CIDRs (comma-separated) or `lan` for private IPs. + "--rproxy", "1", # Which ip to associate clients with, index of X-FWD IP. + ] +} +``` diff --git a/registry/djarbz/modules/copyparty/copyparty.tftest.hcl b/registry/djarbz/modules/copyparty/copyparty.tftest.hcl new file mode 100644 index 00000000..e6fab843 --- /dev/null +++ b/registry/djarbz/modules/copyparty/copyparty.tftest.hcl @@ -0,0 +1,181 @@ +# --- Test Case 1: Required Variables --- +run "plan_with_required_vars" { + command = plan + + variables { + agent_id = "example-agent-id" + } +} + +# --- Test Case 2: Coder App URL uses custom port --- +run "app_url_uses_port" { + command = plan + + variables { + agent_id = "example-agent-id" + port = 19999 + } + + assert { + condition = resource.coder_app.copyparty.url == "http://localhost:19999" + error_message = "Expected copyparty app URL to include configured port" + } +} + +# --- Test Case 3: Default Values --- +run "test_defaults" { + # This run block applies the module with default values + # (except for the required 'agent_id' provided above). + + variables { + agent_id = "example-agent-id" + } + + # --- Asserts for coder_app "copyparty" --- + assert { + condition = resource.coder_app.copyparty.display_name == "copyparty" + error_message = "Default display_name is incorrect" + } + + assert { + condition = resource.coder_app.copyparty.slug == "copyparty" + error_message = "Default slug is incorrect" + } + + assert { + condition = resource.coder_app.copyparty.url == "http://localhost:3923" + error_message = "Default URL is incorrect, expected port 3923" + } + + assert { + condition = resource.coder_app.copyparty.subdomain == false + error_message = "Default subdomain should be false" + } + + assert { + condition = resource.coder_app.copyparty.share == "owner" + error_message = "Default share value should be 'owner'" + } + + assert { + condition = resource.coder_app.copyparty.open_in == "slim-window" + error_message = "Default open_in value should be 'slim-window'" + } + + # --- Asserts for coder_script "copyparty" --- + assert { + condition = coder_script.copyparty.display_name == "copyparty" + error_message = "Script display_name is incorrect" + } + + # Check rendered script content (this assumes your run.sh uses the variables) + assert { + condition = strcontains(coder_script.copyparty.script, "PORT=\"3923\"") + error_message = "Script content does not reflect default port" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "LOG_PATH=\"/tmp/copyparty.log\"") + error_message = "Script content does not reflect default log_path" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "IFS=',' read -r -a ARGUMENTS \u003c\u003c\u003c \"\"") + error_message = "Script content does not reflect default empty arguments" + } +} + +# --- Test Case 4: Custom Values --- +run "test_custom_values" { + # Override default variables for this specific run + variables { + agent_id = "example-agent-id" + port = 8080 + slug = "my-custom-app" + display_name = "My Custom App" + share = "authenticated" + open_in = "tab" + pinned_version = "v1.2.3" + arguments = ["--verbose", "-v"] + log_path = "/var/log/custom.log" + } + + # --- Asserts for coder_app "copyparty" --- + assert { + condition = resource.coder_app.copyparty.display_name == "My Custom App" + error_message = "Custom display_name was not applied" + } + + assert { + condition = resource.coder_app.copyparty.slug == "my-custom-app" + error_message = "Custom slug was not applied" + } + + assert { + condition = resource.coder_app.copyparty.url == "http://localhost:8080" + error_message = "Custom port was not applied to URL" + } + + assert { + condition = resource.coder_app.copyparty.share == "authenticated" + error_message = "Custom share value was not applied" + } + + assert { + condition = resource.coder_app.copyparty.open_in == "tab" + error_message = "Custom open_in value was not applied" + } + + # --- Asserts for coder_script "copyparty" --- + assert { + condition = strcontains(coder_script.copyparty.script, "PORT=\"8080\"") + error_message = "Script content does not reflect custom port" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "PINNED_VERSION=\"v1.2.3\"") + error_message = "Script content does not reflect custom pinned_version" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "IFS=',' read -r -a ARGUMENTS \u003c\u003c\u003c \"--verbose,-v\"") + error_message = "Script content does not reflect custom arguments" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "LOG_PATH=\"/var/log/custom.log\"") + error_message = "Script content does not reflect custom log_path" + } +} + +# --- Test Case 5: Validation Failure (open_in) --- +run "test_invalid_open_in" { + # This is a 'plan' test that expects a failure + command = plan + + variables { + agent_id = "example-agent-id" + open_in = "invalid-value" + } + + # Expect this plan to fail due to the validation rule in 'var.open_in' + expect_failures = [ + var.open_in, + ] +} + +# --- Test Case 6: Validation Failure (share) --- +run "test_invalid_share" { + # This is a 'plan' test that expects a failure + command = plan + + variables { + agent_id = "example-agent-id" + share = "everyone" # This is not 'owner', 'authenticated', or 'public' + } + + # Expect this plan to fail due to the validation rule in 'var.share' + expect_failures = [ + var.share, + ] +} diff --git a/registry/djarbz/modules/copyparty/main.tf b/registry/djarbz/modules/copyparty/main.tf new file mode 100644 index 00000000..822387c1 --- /dev/null +++ b/registry/djarbz/modules/copyparty/main.tf @@ -0,0 +1,174 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +locals { + # A built-in icon like "/icon/code.svg" or a full URL of icon + icon_url = "/icon/copyparty.svg" + # a map of all possible values + # options = { + # "Option 1" = { + # "name" = "Option 1", + # "value" = "1" + # "icon" = "/emojis/1.png" + # } + # "Option 2" = { + # "name" = "Option 2", + # "value" = "2" + # "icon" = "/emojis/2.png" + # } + # } +} + +# Add required variables for your modules and remove any unneeded variables +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "log_path" { + type = string + description = "The path to log copyparty to." + default = "/tmp/copyparty.log" +} + +variable "port" { + type = number + description = "ports to listen on (comma/range); ignored for unix-sockets (default: 3923)" + default = 3923 +} + +variable "slug" { + type = string + description = "The slug for the copyparty application." + default = "copyparty" +} + +variable "display_name" { + type = string + description = "The display name for the copyparty application." + default = "copyparty" +} + +variable "group" { + type = string + description = "The name of a group that this app belongs to." + default = null +} + +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'." + } +} + +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 "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 "mutable" { +# type = bool +# description = "Whether the parameter is mutable." +# default = true +# } + +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 +} +# Add other variables here + +variable "pinned_version" { + type = string + description = "Install a specific version in semver format (v1.19.16)." + default = "" +} + +variable "arguments" { + type = list(string) + description = "A list of arguments to pass to the application." + default = [] +} + + +resource "coder_script" "copyparty" { + agent_id = var.agent_id + display_name = "copyparty" + icon = local.icon_url + script = templatefile("${path.module}/run.sh", { + LOG_PATH : var.log_path, + PORT : var.port, + PINNED_VERSION : var.pinned_version, + ARGUMENTS : join(",", var.arguments), + }) + run_on_start = true + run_on_stop = false +} + +resource "coder_app" "copyparty" { + agent_id = var.agent_id + slug = var.slug + display_name = var.display_name + url = "http://localhost:${var.port}" + icon = local.icon_url + subdomain = var.subdomain + share = var.share + order = var.order + group = var.group + open_in = var.open_in + + # Remove if the app does not have a healthcheck endpoint + healthcheck { + url = "http://localhost:${var.port}" + interval = 5 + threshold = 6 + } +} + +# data "coder_parameter" "copyparty" { +# type = "list(string)" +# name = "copyparty" +# display_name = "copyparty" +# icon = local.icon_url +# mutable = var.mutable +# default = local.options["Option 1"]["value"] + +# dynamic "option" { +# for_each = local.options +# content { +# icon = option.value.icon +# name = option.value.name +# value = option.value.value +# } +# } +# } diff --git a/registry/djarbz/modules/copyparty/run.sh b/registry/djarbz/modules/copyparty/run.sh new file mode 100755 index 00000000..a138f540 --- /dev/null +++ b/registry/djarbz/modules/copyparty/run.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +# Convert templated variables to shell variables +# This variable is assigned to itself, so the assignment does nothing. +# shellcheck disable=SC2269 +LOG_PATH="${LOG_PATH}" + +# Ports to listen on (comma/range); ignored for unix-sockets (default: 3923) +PORT="${PORT}" +# Pinned version (e.g., v1.19.16); overrides latest release discovery if set +PINNED_VERSION="${PINNED_VERSION}" +# Custom CLI Arguments# The variable from Terraform is a single, comma-separated string. +# We need to split it into a proper bash array using the comma (,) as the delimiter. +IFS=',' read -r -a ARGUMENTS <<< "${ARGUMENTS}" + +# VARIABLE appears unused. Verify use (or export if used externally). +# shellcheck disable=SC2034 +MODULE_NAME="Copyparty" + +# VARIABLE appears unused. Verify use (or export if used externally). +# shellcheck disable=SC2034 +BOLD='\033[0;1m' + +printf '%sInstalling %s ...\n\n' "$${BOLD}" "$${MODULE_NAME}" + +# Add code here +# Use variables from the templatefile function in main.tf +# e.g. LOG_PATH, PORT, etc. + +printf "🐍 Verifying Python 3 installation...\n" +if ! command -v python3 &> /dev/null; then + printf "❌ Python3 could not be found. Please install it to continue.\n" + exit 1 +fi +printf "✅ Python3 is installed.\n\n" + +RELEASE_TO_INSTALL="" +# Install provided version to pin, otherwise discover latest github release from `https://github.com/9001/copyparty`. +if [[ -n "$${PINNED_VERSION}" ]]; then + printf "📌 Pinned version specified: %s\n" "$${PINNED_VERSION}" + # Verify that it is in v#.#.# format + if [[ ! "$${PINNED_VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + printf "❌ Invalid format for PINNED_VERSION. Expected 'v#.#.#' (e.g., v1.19.16).\n" + exit 1 + fi + RELEASE_TO_INSTALL="$${PINNED_VERSION}" + printf "✅ Using pinned version %s.\n\n" "$${RELEASE_TO_INSTALL}" +else + printf "🔎 Discovering latest release from GitHub...\n" + # Use curl to get the latest release tag from the GitHub API and sed to parse it + LATEST_RELEASE=$(curl -fsSL https://api.github.com/repos/9001/copyparty/releases/latest | grep '"tag_name":' | sed -E 's/.*"(v[^"]+)".*/\1/') + if [[ -z "$${LATEST_RELEASE}" ]]; then + printf "❌ Could not determine the latest release. Please check your internet connection.\n" + exit 1 + fi + RELEASE_TO_INSTALL="$${LATEST_RELEASE}" + printf "🏷️ Latest release is %s.\n\n" "$${RELEASE_TO_INSTALL}" +fi + +# Download appropriate release version assets: `copyparty-sfx.py` and `helptext.html`. +printf "🚀 Downloading copyparty v%s...\n" "$${RELEASE_TO_INSTALL}" +DOWNLOAD_URL="https://github.com/9001/copyparty/releases/download/$${RELEASE_TO_INSTALL}" + +printf "⏬ Downloading copyparty-sfx.py...\n" +if ! curl -fsSL -o /tmp/copyparty-sfx.py "$${DOWNLOAD_URL}/copyparty-sfx.py"; then + printf "❌ Failed to download copyparty-sfx.py.\n" + exit 1 +fi + +printf "⏬ Downloading helptext.html...\n" +if ! curl -fsSL -o /tmp/helptext.html "$${DOWNLOAD_URL}/helptext.html"; then + # This is not a fatal error, just a warning. + printf "⚠️ Could not download helptext.html. The application will still work.\n" +fi + +chmod +x /tmp/copyparty-sfx.py +printf "✅ Download complete.\n\n" + +printf "🥳 Installation complete!\n\n" + +# Build a clean, quoted string of the command for logging purposes only. +log_command="python3 /tmp/copyparty-sfx.py -p '$${PORT}'" +for arg in "$${ARGUMENTS[@]}"; do + # printf "DEBUG: ARG [$${arg}]\n" + log_command+=" '$${arg}'" +done + +# Clear the log file and write the header and command string using printf. +{ + printf "=== Starting copyparty at %s ===\n" "$(date)" + printf "EXECUTING: %s\n" "$${log_command}" +} > "$${LOG_PATH}" + +printf "👷 Starting %s in background...\n\n" "$${MODULE_NAME}" + +# Execute the actual command using the robust array expansion. +# Then, append its output (stdout and stderr) to the log file. +python3 /tmp/copyparty-sfx.py -p "$${PORT}" "$${ARGUMENTS[@]}" >> "$${LOG_PATH}" 2>&1 & + +printf "✅ Service started. Check logs at %s\n\n" "$${LOG_PATH}" From 30123e7ea3891c6b3a0a013f413719e1695ba6c8 Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Thu, 23 Oct 2025 14:18:30 -0400 Subject: [PATCH 07/36] feat: add boundary pprof server in claude-code module (#503) --- registry/coder/modules/claude-code/README.md | 14 +++++++------- registry/coder/modules/claude-code/main.tf | 14 ++++++++++++++ .../coder/modules/claude-code/scripts/start.sh | 8 ++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index d3cee145..1c17fab8 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.2" + version = "3.3.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -51,7 +51,7 @@ module "claude-code" { boundary_log_level = "WARN" boundary_additional_allowed_urls = ["GET *google.com"] boundary_proxy_port = "8087" - version = "3.2.1" + version = "3.3.0" } ``` @@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.2" + version = "3.3.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -106,7 +106,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.2" + version = "3.3.0" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -129,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.2" + version = "3.3.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.2" + version = "3.3.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.2.2" + version = "3.3.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 20a0cfee..926b2402 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -228,6 +228,18 @@ variable "boundary_proxy_port" { default = "8087" } +variable "enable_boundary_pprof" { + type = bool + description = "Whether to enable coder boundary pprof server" + default = false +} + +variable "boundary_pprof_port" { + type = string + description = "Port for pprof server used by Boundary" + default = "6067" +} + resource "coder_env" "claude_code_md_path" { count = var.claude_md_path == "" ? 0 : 1 @@ -343,6 +355,8 @@ module "agentapi" { ARG_BOUNDARY_LOG_LEVEL='${var.boundary_log_level}' \ ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join(" ", var.boundary_additional_allowed_urls)}' \ ARG_BOUNDARY_PROXY_PORT='${var.boundary_proxy_port}' \ + ARG_ENABLE_BOUNDARY_PPROF='${var.enable_boundary_pprof}' \ + ARG_BOUNDARY_PPROF_PORT='${var.boundary_pprof_port}' \ ARG_CODER_HOST='${local.coder_host}' \ /tmp/start.sh EOT diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index fb3180af..3ac840bd 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -22,6 +22,8 @@ ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"main"} ARG_BOUNDARY_LOG_DIR=${ARG_BOUNDARY_LOG_DIR:-"/tmp/boundary_logs"} ARG_BOUNDARY_LOG_LEVEL=${ARG_BOUNDARY_LOG_LEVEL:-"WARN"} ARG_BOUNDARY_PROXY_PORT=${ARG_BOUNDARY_PROXY_PORT:-"8087"} +ARG_ENABLE_BOUNDARY_PPROF=${ARG_ENABLE_BOUNDARY_PPROF:-false} +ARG_BOUNDARY_PPROF_PORT=${ARG_BOUNDARY_PPROF_PORT:-"6067"} ARG_CODER_HOST=${ARG_CODER_HOST:-} echo "--------------------------------" @@ -155,6 +157,12 @@ function start_agentapi() { # Set log level for boundary BOUNDARY_ARGS+=(--log-level $ARG_BOUNDARY_LOG_LEVEL) + if [ "${ARG_ENABLE_BOUNDARY_PPROF:-false}" = "true" ]; then + # Enable boundary pprof server on specified port + BOUNDARY_ARGS+=(--pprof) + BOUNDARY_ARGS+=(--pprof-port ${ARG_BOUNDARY_PPROF_PORT}) + fi + # Remove --dangerously-skip-permissions from ARGS when using boundary (it doesn't work with elevated permissions) # Create a new array without the dangerous permissions flag CLAUDE_ARGS=() From e3ff43c0a64482e909109bc1d598b6bf00a40ac1 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 24 Oct 2025 11:54:12 +0100 Subject: [PATCH 08/36] refactor(coder/agentapi): support terraform-provider-coder v2.12.0 (#485) In terraform-provider-coder v2.12.0 and the up-coming coder v2.28 release we have removed the requirement for the "AI Prompt" parameter, and are intending on slightly re-designing the API of the AI task modules. Instead of `agentapi` defining the `coder_ai_task` resource, it will output the `task_app_id`. Consumers of the module will then be expected to create the `coder_ai_task` resource themselves with this `task_app_id`. --- registry/coder/modules/agentapi/README.md | 2 +- registry/coder/modules/agentapi/main.tf | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/registry/coder/modules/agentapi/README.md b/registry/coder/modules/agentapi/README.md index d68af511..954db1ce 100644 --- a/registry/coder/modules/agentapi/README.md +++ b/registry/coder/modules/agentapi/README.md @@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI ```tf module "agentapi" { source = "registry.coder.com/coder/agentapi/coder" - version = "1.2.0" + version = "2.0.0" agent_id = var.agent_id web_app_slug = local.app_slug diff --git a/registry/coder/modules/agentapi/main.tf b/registry/coder/modules/agentapi/main.tf index e73f45f6..5c3ab9c4 100644 --- a/registry/coder/modules/agentapi/main.tf +++ b/registry/coder/modules/agentapi/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = ">= 2.7" + version = ">= 2.12" } } } @@ -239,8 +239,6 @@ resource "coder_app" "agentapi_cli" { group = var.cli_app_group } -resource "coder_ai_task" "agentapi" { - sidebar_app { - id = coder_app.agentapi_web.id - } +output "task_app_id" { + value = coder_app.agentapi_web.id } From bc39c2ee293bd2cb45dcba7ffbba0d50427a6242 Mon Sep 17 00:00:00 2001 From: Harsh Singh Panwar Date: Fri, 24 Oct 2025 20:55:40 +0530 Subject: [PATCH 09/36] Aider module support agentAPI (#356) Closes #239 /claim #239 ## Description video :- https://www.loom.com/share/d1d1d54d48bc45c4a48271ca9a387a88?sid=933e250d-78f8-4a7f-9745-0e908c0ee4d9 ## Type of Change - [x] New module - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/aider` **New version:** `v1.0.0` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [ ] Tests pass (`bun test`) - [ ] Code formatted (`bun run fmt`) - [ ] Changes tested locally ## Related Issues --------- Co-authored-by: DevCats --- registry/coder/modules/aider/README.md | 282 ++------- registry/coder/modules/aider/main.test.ts | 197 +++--- registry/coder/modules/aider/main.tf | 559 ++++++------------ registry/coder/modules/aider/main.tftest.hcl | 149 +++++ .../coder/modules/aider/scripts/install.sh | 49 ++ registry/coder/modules/aider/scripts/start.sh | 55 ++ .../modules/aider/testdata/aider-mock.sh | 14 + 7 files changed, 600 insertions(+), 705 deletions(-) create mode 100644 registry/coder/modules/aider/main.tftest.hcl create mode 100644 registry/coder/modules/aider/scripts/install.sh create mode 100644 registry/coder/modules/aider/scripts/start.sh create mode 100644 registry/coder/modules/aider/testdata/aider-mock.sh diff --git a/registry/coder/modules/aider/README.md b/registry/coder/modules/aider/README.md index c93fb89c..3ce9c88e 100644 --- a/registry/coder/modules/aider/README.md +++ b/registry/coder/modules/aider/README.md @@ -8,76 +8,58 @@ tags: [agent, ai, aider] # Aider -Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider and provides a persistent session using screen or tmux. +Run [Aider](https://aider.chat) AI pair programming in your workspace. This module installs Aider with AgentAPI for seamless Coder Tasks Support. ```tf -module "aider" { - source = "registry.coder.com/coder/aider/coder" - version = "1.1.2" - agent_id = coder_agent.example.id -} -``` - -## Features - -- **Interactive Parameter Selection**: Choose your AI provider, model, and configuration options when creating the workspace -- **Multiple AI Providers**: Supports Anthropic (Claude), OpenAI, DeepSeek, GROQ, and OpenRouter -- **Persistent Sessions**: Uses screen (default) or tmux to keep Aider running in the background -- **Optional Dependencies**: Install Playwright for web page scraping and PortAudio for voice coding -- **Project Integration**: Works with any project directory, including Git repositories -- **Browser UI**: Use Aider in your browser with a modern web interface instead of the terminal -- **Non-Interactive Mode**: Automatically processes tasks when provided via the `task_prompt` variable - -## Module Parameters - -> [!NOTE] -> The `use_screen` and `use_tmux` parameters cannot both be enabled at the same time. By default, `use_screen` is set to `true` and `use_tmux` is set to `false`. - -## Usage Examples - -### Basic setup with API key - -```tf -variable "anthropic_api_key" { +variable "api_key" { type = string - description = "Anthropic API key" + description = "API key" sensitive = true } module "aider" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/aider/coder" - version = "1.1.2" - agent_id = coder_agent.example.id - ai_api_key = var.anthropic_api_key -} -``` - -This basic setup will: - -- Install Aider in the workspace -- Create a persistent screen session named "aider" -- Configure Aider to use Anthropic Claude 3.7 Sonnet model -- Enable task reporting (configures Aider to report tasks to Coder MCP) - -### Using OpenAI with tmux - -```tf -variable "openai_api_key" { - type = string - description = "OpenAI API key" - sensitive = true -} - -module "aider" { - count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/aider/coder" - version = "1.1.2" + version = "2.0.0" agent_id = coder_agent.example.id - use_tmux = true - ai_provider = "openai" - ai_model = "4o" # Uses Aider's built-in alias for gpt-4o - ai_api_key = var.openai_api_key + api_key = var.api_key + ai_provider = "google" + model = "gemini" +} +``` + +## Prerequisites + +- pipx is automatically installed if not already available + +## Usage Example + +```tf +data "coder_parameter" "ai_prompt" { + name = "AI Prompt" + description = "Write an initial prompt for Aider to work on." + type = "string" + default = "" + mutable = true +} + +variable "gemini_api_key" { + type = string + description = "Gemini API key" + sensitive = true +} + +module "aider" { + source = "registry.coder.com/coder/aider/coder" + version = "2.0.0" + agent_id = coder_agent.example.id + api_key = var.gemini_api_key + install_aider = true + workdir = "/home/coder" + ai_provider = "google" + model = "gemini" + install_agentapi = true + ai_prompt = data.coder_parameter.ai_prompt.value + system_prompt = "..." } ``` @@ -93,174 +75,16 @@ variable "custom_api_key" { module "aider" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/aider/coder" - version = "1.1.2" + version = "2.0.0" agent_id = coder_agent.example.id + workdir = "/home/coder" ai_provider = "custom" custom_env_var_name = "MY_CUSTOM_API_KEY" - ai_model = "custom-model" - ai_api_key = var.custom_api_key + model = "custom-model" + api_key = var.custom_api_key } ``` -### Adding Custom Extensions (Experimental) - -You can extend Aider's capabilities by adding custom extensions: - -```tf -module "aider" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/aider/coder" - version = "1.1.2" - agent_id = coder_agent.example.id - ai_api_key = var.anthropic_api_key - - experiment_pre_install_script = <<-EOT - pip install some-custom-dependency - EOT - - experiment_additional_extensions = <<-EOT - custom-extension: - args: [] - cmd: custom-extension-command - description: A custom extension for Aider - enabled: true - envs: {} - name: custom-extension - timeout: 300 - type: stdio - EOT -} -``` - -Note: The indentation in the heredoc is preserved, so you can write the YAML naturally. - -## Task Reporting (Experimental) - -> This functionality is in early access as of Coder v2.21 and is still evolving. -> For now, we recommend testing it in a demo or staging environment, -> rather than deploying to production -> -> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents) -> -> Join our [Discord channel](https://discord.gg/coder) or -> [contact us](https://coder.com/contact) to get help or share feedback. - -Your workspace must have either `screen` or `tmux` installed to use this. - -Task reporting is **enabled by default** in this module, allowing you to: - -- Send an initial prompt to Aider during workspace creation -- Monitor task progress in the Coder UI -- Use the `coder_parameter` resource to collect prompts from users - -### Setting up Task Reporting - -To use task reporting effectively: - -1. Add the Coder Login module to your template -2. Configure the necessary variables to pass the task prompt -3. Optionally add a coder_parameter to collect prompts from users - -Here's a complete example: - -```tf -module "coder-login" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/modules/coder-login/coder" - version = "1.0.15" - agent_id = coder_agent.example.id -} - -variable "anthropic_api_key" { - type = string - description = "Anthropic API key" - sensitive = true -} - -data "coder_parameter" "ai_prompt" { - type = "string" - name = "AI Prompt" - default = "" - description = "Write a prompt for Aider" - mutable = true - ephemeral = true -} - -module "aider" { - count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/aider/coder" - version = "1.1.2" - agent_id = coder_agent.example.id - ai_api_key = var.anthropic_api_key - task_prompt = data.coder_parameter.ai_prompt.value - - # Optionally customize the system prompt - system_prompt = <<-EOT -You are a helpful Coding assistant. Aim to autonomously investigate -and solve issues the user gives you and test your work, whenever possible. -Avoid shortcuts like mocking tests. When you get stuck, you can ask the user -but opt for autonomy. -YOU MUST REPORT ALL TASKS TO CODER. -When reporting tasks, you MUST follow these EXACT instructions: -- IMMEDIATELY report status after receiving ANY user message. -- Be granular. If you are investigating with multiple steps, report each step to coder. -Task state MUST be one of the following: -- Use "state": "working" when actively processing WITHOUT needing additional user input. -- Use "state": "complete" only when finished with a task. -- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers. -Task summaries MUST: -- Include specifics about what you're doing. -- Include clear and actionable steps for the user. -- Be less than 160 characters in length. - EOT -} -``` - -When a task prompt is provided via the `task_prompt` variable, the module automatically: - -1. Combines the system prompt with the task prompt into a single message in the format: - -``` -SYSTEM PROMPT: -[system_prompt content] - -This is your current task: [task_prompt] -``` - -2. Executes the task during workspace creation using the `--message` and `--yes-always` flags -3. Logs task output to `$HOME/.aider.log` for reference - -If you want to disable task reporting, set `experiment_report_tasks = false` in your module configuration. - -## Using Aider in Your Workspace - -After the workspace starts, Aider will be installed and configured according to your parameters. A persistent session will automatically be started during workspace creation. - -### Session Options - -You can run Aider in three different ways: - -1. **Direct Mode**: Aider starts directly in the specified folder when you click the app button - -- Simple setup without persistent context -- Suitable for quick coding sessions - -2. **Screen Mode** (Default): Run Aider in a screen session that persists across connections - -- Session name: "aider" (or configured via `session_name`) - -3. **Tmux Mode**: Run Aider in a tmux session instead of screen - -- Set `use_tmux = true` to enable -- Session name: "aider" (or configured via `session_name`) -- Configures tmux with mouse support for shared sessions - -Persistent sessions (screen/tmux) allow you to: - -- Disconnect and reconnect without losing context -- Run Aider in the background while doing other work -- Switch between terminal and browser interfaces - ### Available AI Providers and Models Aider supports various providers and models, and this module integrates directly with Aider's built-in model aliases: @@ -280,10 +104,12 @@ For a complete and up-to-date list of supported aliases and models, please refer ## Troubleshooting -If you encounter issues: +- If `aider` is not found, ensure `install_aider = true` and your API key is valid +- Logs are written under `/home/coder/.aider-module/` (`install.log`, `agentapi-start.log`) for debugging +- If AgentAPI fails to start, verify that your container has network access and executable permissions for the scripts -1. **Screen/Tmux issues**: If you can't reconnect to your session, check if the session exists with `screen -list` or `tmux list-sessions` -2. **API key issues**: Ensure you've entered the correct API key for your selected provider -3. **Browser mode issues**: If the browser interface doesn't open, check that you're accessing it from a machine that can reach your Coder workspace +## References -For more information on using Aider, see the [Aider documentation](https://aider.chat/docs/). +- [Aider Documentation](https://aider.chat/docs) +- [AgentAPI Documentation](https://github.com/coder/agentapi) +- [Coder AI Agents Guide](https://coder.com/docs/tutorials/ai-agents) diff --git a/registry/coder/modules/aider/main.test.ts b/registry/coder/modules/aider/main.test.ts index c25513a5..c0aa51df 100644 --- a/registry/coder/modules/aider/main.test.ts +++ b/registry/coder/modules/aider/main.test.ts @@ -1,107 +1,138 @@ -import { describe, expect, it } from "bun:test"; import { - findResourceInstance, - runTerraformApply, - runTerraformInit, - testRequiredVariables, -} from "~test"; + test, + afterEach, + describe, + setDefaultTimeout, + beforeAll, + expect, +} from "bun:test"; +import { execContainer, readFileContainer, runTerraformInit } from "~test"; +import { + loadTestFile, + writeExecutable, + setup as setupUtil, + execModuleScript, + expectAgentAPIStarted, +} from "../../../coder/modules/agentapi/test-util"; -describe("aider", async () => { - await runTerraformInit(import.meta.dir); +let cleanupFunctions: (() => Promise)[] = []; +const registerCleanup = (cleanup: () => Promise) => { + cleanupFunctions.push(cleanup); +}; +afterEach(async () => { + const cleanupFnsCopy = cleanupFunctions.slice().reverse(); + cleanupFunctions = []; + for (const cleanup of cleanupFnsCopy) { + try { + await cleanup(); + } catch (error) { + console.error("Error during cleanup:", error); + } + } +}); - testRequiredVariables(import.meta.dir, { - agent_id: "foo", +interface SetupProps { + skipAgentAPIMock?: boolean; + skipAiderMock?: boolean; + moduleVariables?: Record; + agentapiMockScript?: string; +} + +const setup = async (props?: SetupProps): Promise<{ id: string }> => { + const projectDir = "/home/coder/project"; + const { id } = await setupUtil({ + moduleDir: import.meta.dir, + moduleVariables: { + install_aider: props?.skipAiderMock ? "true" : "false", + install_agentapi: props?.skipAgentAPIMock ? "true" : "false", + aider_model: "test-model", + ...props?.moduleVariables, + }, + registerCleanup, + projectDir, + skipAgentAPIMock: props?.skipAgentAPIMock, + agentapiMockScript: props?.agentapiMockScript, }); - it("configures task prompt correctly", async () => { - const testPrompt = "Add a hello world function"; - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - task_prompt: testPrompt, + // Place the Aider mock CLI binary inside the container + if (!props?.skipAiderMock) { + await writeExecutable({ + containerId: id, + filePath: "/usr/bin/aider", + content: await loadTestFile(`${import.meta.dir}`, "aider-mock.sh"), }); + } - const instance = findResourceInstance(state, "coder_script"); - expect(instance.script).toContain( - `This is your current task: ${testPrompt}`, - ); - expect(instance.script).toContain("aider --architect --yes-always"); + return { id }; +}; + +setDefaultTimeout(60 * 1000); + +describe("Aider", async () => { + beforeAll(async () => { + await runTerraformInit(import.meta.dir); }); - it("handles custom system prompt", async () => { - const customPrompt = "Report all tasks with state: working"; - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - system_prompt: customPrompt, + test("happy-path", async () => { + const { id } = await setup({ + moduleVariables: { + model: "gemini", + }, }); - - const instance = findResourceInstance(state, "coder_script"); - expect(instance.script).toContain(customPrompt); + await execModuleScript(id); + await expectAgentAPIStarted(id); }); - it("handles pre and post install scripts", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - experiment_pre_install_script: "echo 'Pre-install script executed'", - experiment_post_install_script: "echo 'Post-install script executed'", + test("api-key", async () => { + const apiKey = "test-api-key-123"; + const { id } = await setup({ + moduleVariables: { + api_key: apiKey, + model: "gemini", + }, }); - - const instance = findResourceInstance(state, "coder_script"); - - expect(instance.script).toContain("Running pre-install script"); - expect(instance.script).toContain("Running post-install script"); - expect(instance.script).toContain("base64 -d > /tmp/pre_install.sh"); - expect(instance.script).toContain("base64 -d > /tmp/post_install.sh"); + await execModuleScript(id); + const resp = await readFileContainer( + id, + "/home/coder/.aider-module/agentapi-start.log", + ); + expect(resp).toContain("API key provided!"); }); - it("validates that use_screen and use_tmux cannot both be true", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - use_screen: true, - use_tmux: true, + test("custom-folder", async () => { + const workdir = "/tmp/aider-test"; + const { id } = await setup({ + moduleVariables: { + workdir, + model: "gemini", + }, }); - - const instance = findResourceInstance(state, "coder_script"); - - expect(instance.script).toContain( - "Error: Both use_screen and use_tmux cannot be enabled at the same time", + await execModuleScript(id); + const resp = await readFileContainer( + id, + "/home/coder/.aider-module/install.log", ); - expect(instance.script).toContain("exit 1"); + expect(resp).toContain(workdir); }); - it("configures Aider with known provider and model", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - ai_provider: "anthropic", - ai_model: "sonnet", - ai_api_key: "test-anthropic-key", + test("pre-post-install-scripts", async () => { + const { id } = await setup({ + moduleVariables: { + pre_install_script: "#!/bin/bash\necho 'pre-install-script'", + post_install_script: "#!/bin/bash\necho 'post-install-script'", + model: "gemini", + }, }); - - const instance = findResourceInstance(state, "coder_script"); - expect(instance.script).toContain( - 'export ANTHROPIC_API_KEY=\\"test-anthropic-key\\"', + await execModuleScript(id); + const preLog = await readFileContainer( + id, + "/home/coder/.aider-module/pre_install.log", ); - expect(instance.script).toContain("--model sonnet"); - expect(instance.script).toContain( - "Starting Aider using anthropic provider and model: sonnet", - ); - }); - - it("handles custom provider with custom env var and API key", async () => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - ai_provider: "custom", - custom_env_var_name: "MY_CUSTOM_API_KEY", - ai_model: "custom-model", - ai_api_key: "test-custom-key", - }); - - const instance = findResourceInstance(state, "coder_script"); - expect(instance.script).toContain( - 'export MY_CUSTOM_API_KEY=\\"test-custom-key\\"', - ); - expect(instance.script).toContain("--model custom-model"); - expect(instance.script).toContain( - "Starting Aider using custom provider and model: custom-model", + expect(preLog).toContain("pre-install-script"); + const postLog = await readFileContainer( + id, + "/home/coder/.aider-module/post_install.log", ); + expect(postLog).toContain("post-install-script"); }); }); diff --git a/registry/coder/modules/aider/main.tf b/registry/coder/modules/aider/main.tf index e1f2eccd..70274cb8 100644 --- a/registry/coder/modules/aider/main.tf +++ b/registry/coder/modules/aider/main.tf @@ -36,87 +36,84 @@ variable "icon" { default = "/icon/aider.svg" } -variable "folder" { +variable "workdir" { type = string description = "The folder to run Aider in." default = "/home/coder" } +variable "report_tasks" { + type = bool + description = "Whether to enable task reporting to Coder UI via AgentAPI" + default = false +} + +variable "subdomain" { + type = bool + description = "Whether to use a subdomain for AgentAPI." + default = false +} + +variable "cli_app" { + type = bool + description = "Whether to create a CLI app for Aider" + default = false +} + +variable "web_app_display_name" { + type = string + description = "Display name for the web app" + default = "Aider" +} + +variable "cli_app_display_name" { + type = string + description = "Display name for the CLI app" + default = "Aider CLI" +} + +variable "pre_install_script" { + type = string + description = "Custom script to run before installing Aider." + default = null +} + +variable "post_install_script" { + type = string + description = "Custom script to run after installing Aider." + default = null +} + +variable "install_agentapi" { + type = bool + description = "Whether to install AgentAPI." + default = true +} + +variable "agentapi_version" { + type = string + description = "The version of AgentAPI to install." + default = "v0.10.0" +} + +variable "ai_prompt" { + type = string + description = "Initial task prompt for Aider." + default = "" +} + +# --------------------------------------------- + variable "install_aider" { type = bool description = "Whether to install Aider." default = true } -variable "aider_version" { - type = string - description = "The version of Aider to install." - default = "latest" -} - -variable "use_screen" { - type = bool - description = "Whether to use screen for running Aider in the background" - default = true -} - -variable "use_tmux" { - type = bool - description = "Whether to use tmux instead of screen for running Aider in the background" - default = false -} - -variable "session_name" { - type = string - description = "Name for the persistent session (screen or tmux)" - default = "aider" -} - -variable "experiment_report_tasks" { - type = bool - description = "Whether to enable task reporting." - default = true -} - variable "system_prompt" { type = string description = "System prompt for instructing Aider on task reporting and behavior" - default = <<-EOT -You are a helpful Coding assistant. Aim to autonomously investigate -and solve issues the user gives you and test your work, whenever possible. -Avoid shortcuts like mocking tests. When you get stuck, you can ask the user -but opt for autonomy. -YOU MUST REPORT ALL TASKS TO CODER. -When reporting tasks, you MUST follow these EXACT instructions: -- IMMEDIATELY report status after receiving ANY user message. -- Be granular. If you are investigating with multiple steps, report each step to coder. -Task state MUST be one of the following: -- Use "state": "working" when actively processing WITHOUT needing additional user input. -- Use "state": "complete" only when finished with a task. -- Use "state": "failure" when you need ANY user input, lack sufficient details, or encounter blockers. -Task summaries MUST: -- Include specifics about what you're doing. -- Include clear and actionable steps for the user. -- Be less than 160 characters in length. -EOT -} - -variable "task_prompt" { - type = string - description = "Task prompt to use with Aider" - default = "" -} - -variable "experiment_pre_install_script" { - type = string - description = "Custom script to run before installing Aider." - default = null -} - -variable "experiment_post_install_script" { - type = string - description = "Custom script to run after installing Aider." - default = null + default = "You are a helpful coding assistant that helps developers write, debug, and understand code. Provide clear explanations, follow best practices, and help solve coding problems efficiently." } variable "experiment_additional_extensions" { @@ -128,20 +125,19 @@ variable "experiment_additional_extensions" { variable "ai_provider" { type = string description = "AI provider to use with Aider (openai, anthropic, azure, google, etc.)" - default = "anthropic" + default = "google" validation { condition = contains(["openai", "anthropic", "azure", "google", "cohere", "mistral", "ollama", "custom"], var.ai_provider) - error_message = "ai_provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom" + error_message = "provider must be one of: openai, anthropic, azure, google, cohere, mistral, ollama, custom" } } -variable "ai_model" { +variable "model" { type = string description = "AI model to use with Aider. Can use Aider's built-in aliases like '4o' (gpt-4o), 'sonnet' (claude-3-7-sonnet), 'opus' (claude-3-opus), etc." - default = "sonnet" } -variable "ai_api_key" { +variable "api_key" { type = string description = "API key for the selected AI provider. This will be set as the appropriate environment variable based on the provider." default = "" @@ -154,55 +150,66 @@ variable "custom_env_var_name" { default = "" } +variable "base_aider_config" { + type = string + description = <<-EOT + Base Aider configuration in yaml format. Will be stored in .aider.conf.yml file. + + options include: + read: + - CONVENTIONS.md + - anotherfile.txt + - thirdfile.py + model: xxx + ##Specify the OpenAI API key + openai-api-key: xxx + ## (deprecated, use --set-env OPENAI_API_TYPE=) + openai-api-type: xxx + ## (deprecated, use --set-env OPENAI_API_VERSION=) + openai-api-version: xxx + ## (deprecated, use --set-env OPENAI_API_DEPLOYMENT_ID=) + openai-api-deployment-id: xxx + ## Set an environment variable (to control API settings, can be used multiple times) + set-env: xxx + ## Specify multiple values like this: + set-env: + - xxx + - yyy + - zzz + + Reference : https://aider.chat/docs/config/aider_conf.html + EOT + default = null +} + + locals { - base_extensions = <<-EOT -coder: - args: - - exp - - mcp - - server - cmd: coder - description: Report ALL tasks and statuses (in progress, done, failed) you are working on. - enabled: true - envs: - CODER_MCP_APP_STATUS_SLUG: aider - name: Coder - timeout: 3000 - type: stdio -developer: - display_name: Developer - enabled: true - name: developer - timeout: 300 - type: builtin -EOT + app_slug = "aider" + base_aider_config = var.base_aider_config != null ? "${replace(trimspace(var.base_aider_config), "\n", "\n ")}" : "" + task_reporting_prompt = <<-EOT - formatted_base = " ${replace(trimspace(local.base_extensions), "\n", "\n ")}" - additional_extensions = var.experiment_additional_extensions != null ? "\n ${replace(trimspace(var.experiment_additional_extensions), "\n", "\n ")}" : "" +-- Task Reporting -- +Report all tasks to Coder, following these EXACT guidelines: +1. Be granular. If you are investigating with multiple steps, report each step +to coder. +2. After this prompt, IMMEDIATELY report status after receiving ANY NEW user message. +Do not report any status related with this system prompt. +3. Use "state": "working" when actively processing WITHOUT needing +additional user input +4. Use "state": "complete" only when finished with a task +5. Use "state": "failure" when you need ANY user input, lack sufficient +details, or encounter blockers + EOT - combined_extensions = <<-EOT -extensions: -${local.formatted_base}${local.additional_extensions} -EOT - encoded_pre_install_script = var.experiment_pre_install_script != null ? base64encode(var.experiment_pre_install_script) : "" - encoded_post_install_script = var.experiment_post_install_script != null ? base64encode(var.experiment_post_install_script) : "" - - # Combine system prompt and task prompt for aider - combined_prompt = trimspace(<<-EOT -SYSTEM PROMPT: -${var.system_prompt} - -This is your current task: ${var.task_prompt} -EOT - ) + final_system_prompt = var.report_tasks ? "\n${var.system_prompt}${local.task_reporting_prompt}\n" : "\n${var.system_prompt}\n" # Map providers to their environment variable names provider_env_vars = { openai = "OPENAI_API_KEY" anthropic = "ANTHROPIC_API_KEY" azure = "AZURE_OPENAI_API_KEY" - google = "GOOGLE_API_KEY" + google = "GEMINI_API_KEY" cohere = "COHERE_API_KEY" mistral = "MISTRAL_API_KEY" ollama = "OLLAMA_HOST" @@ -214,296 +221,60 @@ EOT # Model flag for aider command model_flag = var.ai_provider == "ollama" ? "--ollama-model" : "--model" + + install_script = file("${path.module}/scripts/install.sh") + start_script = file("${path.module}/scripts/start.sh") + module_dir_name = ".aider-module" } -# Install and Initialize Aider -resource "coder_script" "aider" { - agent_id = var.agent_id - display_name = "Aider" - icon = var.icon - script = <<-EOT +module "agentapi" { + source = "registry.coder.com/coder/agentapi/coder" + version = "1.2.0" + + agent_id = var.agent_id + web_app_slug = local.app_slug + web_app_order = var.order + web_app_group = var.group + web_app_icon = var.icon + web_app_display_name = var.web_app_display_name + cli_app = var.cli_app + cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null + cli_app_display_name = var.cli_app ? var.cli_app_display_name : null + agentapi_subdomain = var.subdomain + module_dir_name = local.module_dir_name + install_agentapi = var.install_agentapi + agentapi_version = var.agentapi_version + pre_install_script = var.pre_install_script + post_install_script = var.post_install_script + start_script = <<-EOT #!/bin/bash - set -e + set -o errexit + set -o pipefail - command_exists() { - command -v "$1" >/dev/null 2>&1 - } - - echo "Setting up Aider AI pair programming..." - - if [ "${var.use_screen}" = "true" ] && [ "${var.use_tmux}" = "true" ]; then - echo "Error: Both use_screen and use_tmux cannot be enabled at the same time." - exit 1 - fi - - mkdir -p "${var.folder}" - - if [ "$(uname)" = "Linux" ]; then - echo "Checking dependencies for Linux..." - - if [ "${var.use_tmux}" = "true" ]; then - if ! command_exists tmux; then - echo "Installing tmux for persistent sessions..." - if command -v apt-get >/dev/null 2>&1; then - if command -v sudo >/dev/null 2>&1; then - sudo apt-get update -qq - sudo apt-get install -y -qq tmux - else - apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges" - apt-get install -y -qq tmux || echo "Warning: Cannot install tmux without sudo privileges" - fi - elif command -v dnf >/dev/null 2>&1; then - if command -v sudo >/dev/null 2>&1; then - sudo dnf install -y -q tmux - else - dnf install -y -q tmux || echo "Warning: Cannot install tmux without sudo privileges" - fi - else - echo "Warning: Unable to install tmux on this system. Neither apt-get nor dnf found." - fi - else - echo "tmux is already installed, skipping installation." - fi - elif [ "${var.use_screen}" = "true" ]; then - if ! command_exists screen; then - echo "Installing screen for persistent sessions..." - if command -v apt-get >/dev/null 2>&1; then - if command -v sudo >/dev/null 2>&1; then - sudo apt-get update -qq - sudo apt-get install -y -qq screen - else - apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges" - apt-get install -y -qq screen || echo "Warning: Cannot install screen without sudo privileges" - fi - elif command -v dnf >/dev/null 2>&1; then - if command -v sudo >/dev/null 2>&1; then - sudo dnf install -y -q screen - else - dnf install -y -q screen || echo "Warning: Cannot install screen without sudo privileges" - fi - else - echo "Warning: Unable to install screen on this system. Neither apt-get nor dnf found." - fi - else - echo "screen is already installed, skipping installation." - fi - fi - else - echo "This module currently only supports Linux workspaces." - exit 1 - fi - - if [ -n "${local.encoded_pre_install_script}" ]; then - echo "Running pre-install script..." - echo "${local.encoded_pre_install_script}" | base64 -d > /tmp/pre_install.sh - chmod +x /tmp/pre_install.sh - /tmp/pre_install.sh - fi - - if [ "${var.install_aider}" = "true" ]; then - echo "Installing Aider..." - - if ! command_exists python3 || ! command_exists pip3; then - echo "Installing Python dependencies required for Aider..." - if command -v apt-get >/dev/null 2>&1; then - if command -v sudo >/dev/null 2>&1; then - sudo apt-get update -qq - sudo apt-get install -y -qq python3-pip python3-venv - else - apt-get update -qq || echo "Warning: Cannot update package lists without sudo privileges" - apt-get install -y -qq python3-pip python3-venv || echo "Warning: Cannot install Python packages without sudo privileges" - fi - elif command -v dnf >/dev/null 2>&1; then - if command -v sudo >/dev/null 2>&1; then - sudo dnf install -y -q python3-pip python3-virtualenv - else - dnf install -y -q python3-pip python3-virtualenv || echo "Warning: Cannot install Python packages without sudo privileges" - fi - else - echo "Warning: Unable to install Python on this system. Neither apt-get nor dnf found." - fi - else - echo "Python is already installed, skipping installation." - fi - - if ! command_exists aider; then - curl -LsSf https://aider.chat/install.sh | sh - fi - - if [ -f "$HOME/.bashrc" ]; then - if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.bashrc"; then - echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.bashrc" - fi - fi - - if [ -f "$HOME/.zshrc" ]; then - if ! grep -q 'export PATH="$HOME/bin:$PATH"' "$HOME/.zshrc"; then - echo 'export PATH="$HOME/bin:$PATH"' >> "$HOME/.zshrc" - fi - fi - - fi - - if [ -n "${local.encoded_post_install_script}" ]; then - echo "Running post-install script..." - echo "${local.encoded_post_install_script}" | base64 -d > /tmp/post_install.sh - chmod +x /tmp/post_install.sh - /tmp/post_install.sh - fi - - if [ "${var.experiment_report_tasks}" = "true" ]; then - echo "Configuring Aider to report tasks via Coder MCP..." - - mkdir -p "$HOME/.config/aider" - - cat > "$HOME/.config/aider/config.yml" << EOL -${trimspace(local.combined_extensions)} -EOL - echo "Added Coder MCP extension to Aider config.yml" - fi - - echo "Starting persistent Aider session..." - - touch "$HOME/.aider.log" - - export LANG=en_US.UTF-8 - export LC_ALL=en_US.UTF-8 - - export PATH="$HOME/bin:$PATH" - - if [ "${var.use_tmux}" = "true" ]; then - if [ -n "${var.task_prompt}" ]; then - echo "Running Aider with message in tmux session..." - - # Configure tmux for shared sessions - if [ ! -f "$HOME/.tmux.conf" ]; then - echo "Creating ~/.tmux.conf with shared session settings..." - echo "set -g mouse on" > "$HOME/.tmux.conf" - fi - - if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then - echo "Adding 'set -g mouse on' to ~/.tmux.conf..." - echo "set -g mouse on" >> "$HOME/.tmux.conf" - fi - - echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}" - tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"" - echo "Aider task started in tmux session '${var.session_name}'. Check the UI for progress." - else - # Configure tmux for shared sessions - if [ ! -f "$HOME/.tmux.conf" ]; then - echo "Creating ~/.tmux.conf with shared session settings..." - echo "set -g mouse on" > "$HOME/.tmux.conf" - fi - - if ! grep -q "^set -g mouse on$" "$HOME/.tmux.conf"; then - echo "Adding 'set -g mouse on' to ~/.tmux.conf..." - echo "set -g mouse on" >> "$HOME/.tmux.conf" - fi - - echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}" - tmux new-session -d -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${var.system_prompt}\"" - echo "Tmux session '${var.session_name}' started. Access it by clicking the Aider button." - fi - else - if [ -n "${var.task_prompt}" ]; then - echo "Running Aider with message in screen session..." - - if [ ! -f "$HOME/.screenrc" ]; then - echo "Creating ~/.screenrc and adding multiuser settings..." - echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc" - fi - - if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then - echo "Adding 'multiuser on' to ~/.screenrc..." - echo "multiuser on" >> "$HOME/.screenrc" - fi - - if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then - echo "Adding 'acladd $(whoami)' to ~/.screenrc..." - echo "acladd $(whoami)" >> "$HOME/.screenrc" - fi - - echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}" - screen -U -dmS ${var.session_name} bash -c " - cd ${var.folder} - export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\" - export ${local.env_var_name}=\"${var.ai_api_key}\" - aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\" - /bin/bash - " - - echo "Aider task started in screen session '${var.session_name}'. Check the UI for progress." - else - - if [ ! -f "$HOME/.screenrc" ]; then - echo "Creating ~/.screenrc and adding multiuser settings..." - echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc" - fi - - if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then - echo "Adding 'multiuser on' to ~/.screenrc..." - echo "multiuser on" >> "$HOME/.screenrc" - fi - - if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then - echo "Adding 'acladd $(whoami)' to ~/.screenrc..." - echo "acladd $(whoami)" >> "$HOME/.screenrc" - fi - - echo "Starting Aider using ${var.ai_provider} provider and model: ${var.ai_model}" - screen -U -dmS ${var.session_name} bash -c " - cd ${var.folder} - export PATH=\"$HOME/bin:$HOME/.local/bin:$PATH\" - export ${local.env_var_name}=\"${var.ai_api_key}\" - aider --architect --yes-always ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\" - /bin/bash - " - echo "Screen session '${var.session_name}' started. Access it by clicking the Aider button." - fi - fi - - echo "Aider setup complete!" + echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh + chmod +x /tmp/start.sh + ARG_WORKDIR='${var.workdir}' \ + ARG_API_KEY='${base64encode(var.api_key)}' \ + ARG_MODEL='${var.model}' \ + ARG_PROVIDER='${var.ai_provider}' \ + ARG_ENV_API_NAME_HOLDER='${local.env_var_name}' \ + ARG_SYSTEM_PROMPT='${base64encode(local.final_system_prompt)}' \ + ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \ + /tmp/start.sh EOT - run_on_start = true -} -# Aider CLI app -resource "coder_app" "aider_cli" { - agent_id = var.agent_id - slug = "aider" - display_name = "Aider" - icon = var.icon - command = <<-EOT + install_script = <<-EOT #!/bin/bash - set -e + set -o errexit + set -o pipefail - export PATH="$HOME/bin:$HOME/.local/bin:$PATH" - - export LANG=en_US.UTF-8 - export LC_ALL=en_US.UTF-8 - - if [ "${var.use_tmux}" = "true" ]; then - if tmux has-session -t ${var.session_name} 2>/dev/null; then - echo "Attaching to existing Aider tmux session..." - tmux attach-session -t ${var.session_name} - else - echo "Starting new Aider tmux session..." - tmux new-session -s ${var.session_name} -c ${var.folder} "export ${local.env_var_name}=\"${var.ai_api_key}\"; aider ${local.model_flag} ${var.ai_model} --message \"${local.combined_prompt}\"; exec bash" - fi - elif [ "${var.use_screen}" = "true" ]; then - if ! screen -list | grep -q "${var.session_name}"; then - echo "Error: No existing Aider session found. Please wait for the script to start it." - exit 1 - fi - screen -xRR ${var.session_name} - else - cd "${var.folder}" - echo "Starting Aider directly..." - export ${local.env_var_name}="${var.ai_api_key}" - aider ${local.model_flag} ${var.ai_model} --message "${local.combined_prompt}" - fi + echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh + chmod +x /tmp/install.sh + ARG_WORKDIR='${var.workdir}' \ + ARG_INSTALL_AIDER='${var.install_aider}' \ + ARG_REPORT_TASKS='${var.report_tasks}' \ + ARG_AIDER_CONFIG="$(echo -n '${base64encode(local.base_aider_config)}' | base64 -d)" \ + /tmp/install.sh EOT - order = var.order - group = var.group } + diff --git a/registry/coder/modules/aider/main.tftest.hcl b/registry/coder/modules/aider/main.tftest.hcl new file mode 100644 index 00000000..281bde86 --- /dev/null +++ b/registry/coder/modules/aider/main.tftest.hcl @@ -0,0 +1,149 @@ +run "test_aider_basic" { + command = plan + + variables { + agent_id = "test-agent-123" + workdir = "/home/coder" + model = "gemini" + } + + assert { + condition = var.workdir == "/home/coder" + error_message = "Workdir variable should default to /home/coder" + } + + assert { + condition = var.agent_id == "test-agent-123" + error_message = "Agent ID variable should be set correctly" + } + + assert { + condition = var.install_aider == true + error_message = "install_aider should default to true" + } + + assert { + condition = var.install_agentapi == true + error_message = "install_agentapi should default to true" + } + + assert { + condition = var.report_tasks == false + error_message = "report_tasks should default to false" + } +} + +run "test_with_api_key" { + command = plan + + variables { + agent_id = "test-agent-456" + workdir = "/home/coder/workspace" + api_key = "test-api-key-123" + model = "gemini" + } + + assert { + condition = var.api_key == "test-api-key-123" + error_message = "API key value should match the input" + } +} + +run "test_custom_options" { + command = plan + + variables { + agent_id = "test-agent-789" + workdir = "/home/coder/custom" + order = 5 + group = "development" + icon = "/icon/custom.svg" + model = "4o" + ai_prompt = "Help me write better code" + install_aider = false + install_agentapi = false + agentapi_version = "v0.10.0" + api_key = "" + base_aider_config = "read:\n - CONVENTIONS.md" + } + + assert { + condition = var.order == 5 + error_message = "Order variable should be set to 5" + } + + assert { + condition = var.group == "development" + error_message = "Group variable should be set to 'development'" + } + + assert { + condition = var.icon == "/icon/custom.svg" + error_message = "Icon variable should be set to custom icon" + } + + assert { + condition = var.model == "4o" + error_message = "Model variable should be set to '4o'" + } + + assert { + condition = var.ai_prompt == "Help me write better code" + error_message = "AI prompt variable should be set correctly" + } + + assert { + condition = var.install_aider == false + error_message = "install_aider should be set to false" + } + + assert { + condition = var.install_agentapi == false + error_message = "install_agentapi should be set to false" + } + + assert { + condition = var.agentapi_version == "v0.10.0" + error_message = "AgentAPI version should be set to 'v0.10.0'" + } +} + +run "test_with_scripts" { + command = plan + + variables { + agent_id = "test-agent-scripts" + workdir = "/home/coder/scripts" + model = "gemini" + pre_install_script = "echo 'Pre-install script'" + post_install_script = "echo 'Post-install script'" + } + + assert { + condition = var.pre_install_script == "echo 'Pre-install script'" + error_message = "Pre-install script should be set correctly" + } + + assert { + condition = var.post_install_script == "echo 'Post-install script'" + error_message = "Post-install script should be set correctly" + } +} + +run "test_ai_provider_env_mapping" { + command = plan + + variables { + agent_id = "test-agent-provider" + workdir = "/home/coder/test" + ai_provider = "google" + model = "gemini" + custom_env_var_name = "" + } + + # Ensure provider -> env var mapping works as expected (based on locals.provider_env_vars) + assert { + condition = var.ai_provider == "google" + error_message = "AI provider should be set to 'google' for this test" + } +} diff --git a/registry/coder/modules/aider/scripts/install.sh b/registry/coder/modules/aider/scripts/install.sh new file mode 100644 index 00000000..b2244aa0 --- /dev/null +++ b/registry/coder/modules/aider/scripts/install.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -euo pipefail + +# Function to check if a command exists +command_exists() { + command -v "$1" > /dev/null 2>&1 +} + +# Inputs +ARG_WORKDIR=${ARG_WORKDIR:-/home/coder} +ARG_INSTALL_AIDER=${ARG_INSTALL_AIDER:-true} +ARG_AIDER_CONFIG=${ARG_AIDER_CONFIG:-} + +echo "--------------------------------" +echo "Install flag: $ARG_INSTALL_AIDER" +echo "Workspace: $ARG_WORKDIR" +echo "--------------------------------" + +function install_aider() { + echo "pipx installing..." + sudo apt-get install -y pipx + echo "pipx installed!" + pipx ensurepath + mkdir -p "$ARG_WORKDIR/.local/bin" + export PATH="$HOME/.local/bin:$ARG_WORKDIR/.local/bin:$PATH" + + if ! command_exists aider; then + echo "Installing Aider via pipx..." + pipx install --force aider-install + aider-install + fi + echo "Aider installed: $(aider --version || echo 'Aider installation check failed')" +} + +function configure_aider_settings() { + if [ -n "${ARG_AIDER_CONFIG}" ]; then + echo "Configuring Aider environment variables and model" + + mkdir -p "$HOME/.config/aider" + + echo "$ARG_AIDER_CONFIG" > "$HOME/.config/aider/.aider.conf.yml" + echo "Aider config created at $HOME/.config/aider/.aider.conf.yml" + else + printf "No Aider environment variables or model configured\n" + fi +} + +install_aider +configure_aider_settings diff --git a/registry/coder/modules/aider/scripts/start.sh b/registry/coder/modules/aider/scripts/start.sh new file mode 100644 index 00000000..1bd18ffa --- /dev/null +++ b/registry/coder/modules/aider/scripts/start.sh @@ -0,0 +1,55 @@ +#!/bin/bash +set -euo pipefail + +# Ensure pipx-installed apps are in PATH +export PATH="$HOME/.local/bin:$PATH" + +ARG_WORKDIR=${ARG_WORKDIR:-/home/coder} +ARG_API_KEY=$(echo -n "${ARG_API_KEY:-}" | base64 -d) +ARG_SYSTEM_PROMPT=$(echo -n "${ARG_SYSTEM_PROMPT:-}" | base64 -d 2> /dev/null || echo "") +ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d 2> /dev/null || echo "") +ARG_MODEL=${ARG_MODEL:-} +ARG_PROVIDER=${ARG_PROVIDER:-} +ARG_ENV_API_NAME_HOLDER=${ARG_ENV_API_NAME_HOLDER:-} + +echo "--------------------------------" +echo "Provider: $ARG_PROVIDER" +echo "Model: $ARG_MODEL" +echo "--------------------------------" + +if [ -n "$ARG_API_KEY" ]; then + printf "API key provided!\n" + export $ARG_ENV_API_NAME_HOLDER=$ARG_API_KEY +else + printf "API key not provided.\n" +fi + +build_initial_prompt() { + local initial_prompt="" + if [ -n "$ARG_AI_PROMPT" ]; then + if [ -n "$ARG_SYSTEM_PROMPT" ]; then + initial_prompt="$ARG_SYSTEM_PROMPT $ARG_AI_PROMPT" + else + initial_prompt="$ARG_AI_PROMPT" + fi + fi + echo "$initial_prompt" +} + +start_agentapi() { + echo "Starting in directory: $ARG_WORKDIR" + cd "$ARG_WORKDIR" + + local initial_prompt + initial_prompt=$(build_initial_prompt) + if [ -n "$initial_prompt" ]; then + echo "Starting agentapi with initial prompt" + agentapi server -I="$initial_prompt" --type aider --term-width=67 --term-height=1190 -- aider --model $ARG_MODEL --yes-always + else + agentapi server --term-width=67 --term-height=1190 -- aider --model $ARG_MODEL --yes-always + fi +} + +# TODO: Implement MCP server for coder when Aider support MCP servers. + +start_agentapi diff --git a/registry/coder/modules/aider/testdata/aider-mock.sh b/registry/coder/modules/aider/testdata/aider-mock.sh new file mode 100644 index 00000000..e021b2d2 --- /dev/null +++ b/registry/coder/modules/aider/testdata/aider-mock.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +if [[ "$1" == "--version" ]]; then + echo "HELLO: $(bash -c env)" + echo "aider version v0.86.0" + exit 0 +fi + +set -e + +while true; do + echo "$(date) - aider-agent-mock" + sleep 15 +done From a327e79bc4f2bb2c4b3483acea63618539b53c96 Mon Sep 17 00:00:00 2001 From: netsgnut <284779+netsgnut@users.noreply.github.com> Date: Fri, 24 Oct 2025 10:47:51 -0700 Subject: [PATCH 10/36] fix(kasmvnc): change installed check and bump default version (#505) ## Description This PR makes the following changes to the `coder/modules/kasmvnc`: - Change the installation check from checking `vncserver` to `kasmvncserver`. - Bump the default KasmVNC installation version to [1.4.0](https://docs.kasmvnc.com/docs/release_notes/1.4.0). In images where there is already TightVNC installed, the current installation check will erroneously report that KasmVNC is already installed. By checking `kasmvncserver` instead, it ensures KasmVNC is installed. Tested on Debian, Kali and Alpine-based images. ## Type of Change - [ ] New module - [ ] New template - [X] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/kasmvnc` **New version:** `v1.2.5` **Breaking change:** [ ] Yes [X] No ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun fmt`) - [X] Changes tested locally ## Related Issues None --- registry/coder/modules/kasmvnc/README.md | 2 +- registry/coder/modules/kasmvnc/main.tf | 2 +- registry/coder/modules/kasmvnc/run.sh | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/registry/coder/modules/kasmvnc/README.md b/registry/coder/modules/kasmvnc/README.md index 2bc862d4..7f01b45b 100644 --- a/registry/coder/modules/kasmvnc/README.md +++ b/registry/coder/modules/kasmvnc/README.md @@ -14,7 +14,7 @@ Automatically install [KasmVNC](https://kasmweb.com/kasmvnc) in a workspace, and module "kasmvnc" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/kasmvnc/coder" - version = "1.2.4" + version = "1.2.5" agent_id = coder_agent.example.id desktop_environment = "xfce" subdomain = true diff --git a/registry/coder/modules/kasmvnc/main.tf b/registry/coder/modules/kasmvnc/main.tf index ca7315ec..5a5b449b 100644 --- a/registry/coder/modules/kasmvnc/main.tf +++ b/registry/coder/modules/kasmvnc/main.tf @@ -23,7 +23,7 @@ variable "port" { variable "kasm_version" { type = string description = "Version of KasmVNC to install." - default = "1.3.2" + default = "1.4.0" } variable "desktop_environment" { diff --git a/registry/coder/modules/kasmvnc/run.sh b/registry/coder/modules/kasmvnc/run.sh index 04b8b9ee..089dce3e 100644 --- a/registry/coder/modules/kasmvnc/run.sh +++ b/registry/coder/modules/kasmvnc/run.sh @@ -8,10 +8,10 @@ error() { exit 1 } -# Function to check if vncserver is already installed +# Function to check if KasmVNC is already installed check_installed() { - if command -v vncserver &> /dev/null; then - echo "vncserver is already installed." + if command -v kasmvncserver &> /dev/null; then + echo "KasmVNC is already installed." return 0 # Don't exit, just indicate it's installed else return 1 # Indicates not installed @@ -158,7 +158,7 @@ case "$arch" in ;; esac -# Check if vncserver is installed, and install if not +# Check if KasmVNC is installed, and install if not if ! check_installed; then # Check for NOPASSWD sudo (required) if ! command -v sudo &> /dev/null || ! sudo -n true 2> /dev/null; then @@ -188,7 +188,7 @@ if ! check_installed; then ;; esac else - echo "vncserver already installed. Skipping installation." + echo "KasmVNC already installed. Skipping installation." fi if command -v sudo &> /dev/null && sudo -n true 2> /dev/null; then @@ -227,7 +227,7 @@ EOF # This password is not used since we start the server without auth. # The server is protected via the Coder session token / tunnel # and does not listen publicly -echo -e "password\npassword\n" | vncpasswd -wo -u "$USER" +echo -e "password\npassword\n" | kasmvncpasswd -wo -u "$USER" get_http_dir() { # determine the served file path @@ -290,7 +290,7 @@ VNC_LOG="/tmp/kasmvncserver.log" printf "🚀 Starting KasmVNC server...\n" set +e -vncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > "$VNC_LOG" 2>&1 +kasmvncserver -select-de "${DESKTOP_ENVIRONMENT}" -disableBasicAuth > "$VNC_LOG" 2>&1 RETVAL=$? set -e From 0ff3dbcc48411c243aefb89215395e05988d1d18 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Fri, 24 Oct 2025 23:14:34 +0500 Subject: [PATCH 11/36] chore(claude-code): limit MCP tools for task reporting (#507) --- registry/coder/modules/claude-code/README.md | 14 +++++++------- .../coder/modules/claude-code/scripts/install.sh | 5 ----- .../coder/modules/claude-code/scripts/start.sh | 3 +++ 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 1c17fab8..2058e917 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.0" + version = "3.3.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -51,7 +51,7 @@ module "claude-code" { boundary_log_level = "WARN" boundary_additional_allowed_urls = ["GET *google.com"] boundary_proxy_port = "8087" - version = "3.3.0" + version = "3.3.1" } ``` @@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.0" + version = "3.3.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -106,7 +106,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.0" + version = "3.3.1" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -129,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.0" + version = "3.3.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.0" + version = "3.3.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.0" + version = "3.3.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 1285df90..21133384 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -91,11 +91,6 @@ function report_tasks() { export CODER_MCP_APP_STATUS_SLUG="$ARG_MCP_APP_STATUS_SLUG" export CODER_MCP_AI_AGENTAPI_URL="http://localhost:3284" coder exp mcp configure claude-code "$ARG_WORKDIR" - else - export CODER_MCP_APP_STATUS_SLUG="" - export CODER_MCP_AI_AGENTAPI_URL="" - echo "Configuring Claude Code with Coder MCP..." - coder exp mcp configure claude-code "$ARG_WORKDIR" fi } diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 3ac840bd..525b8733 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -79,6 +79,9 @@ task_session_exists() { ARGS=() function start_agentapi() { + # For Task reporting + export CODER_MCP_ALLOWED_TOOLS="coder_report_task" + mkdir -p "$ARG_WORKDIR" cd "$ARG_WORKDIR" From 7e42a145faea94c10b890799dba2a2998fd30eaf Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Fri, 24 Oct 2025 16:35:20 -0400 Subject: [PATCH 12/36] feat: dropping perms before running claude (#509) Co-authored-by: DevCats Co-authored-by: Atif Ali --- registry/coder/modules/claude-code/README.md | 14 +++++++------- .../coder/modules/claude-code/scripts/start.sh | 11 +---------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 2058e917..af0e58e8 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.1" + version = "3.3.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -51,7 +51,7 @@ module "claude-code" { boundary_log_level = "WARN" boundary_additional_allowed_urls = ["GET *google.com"] boundary_proxy_port = "8087" - version = "3.3.1" + version = "3.3.2" } ``` @@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.1" + version = "3.3.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -106,7 +106,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.1" + version = "3.3.2" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -129,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.1" + version = "3.3.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.1" + version = "3.3.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.1" + version = "3.3.2" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 525b8733..1daae35f 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -166,18 +166,9 @@ function start_agentapi() { BOUNDARY_ARGS+=(--pprof-port ${ARG_BOUNDARY_PPROF_PORT}) fi - # Remove --dangerously-skip-permissions from ARGS when using boundary (it doesn't work with elevated permissions) - # Create a new array without the dangerous permissions flag - CLAUDE_ARGS=() - for arg in "${ARGS[@]}"; do - if [ "$arg" != "--dangerously-skip-permissions" ]; then - CLAUDE_ARGS+=("$arg") - fi - done - agentapi server --allowed-hosts="*" --type claude --term-width 67 --term-height 1190 -- \ sudo -E env PATH=$PATH setpriv --inh-caps=+net_admin --ambient-caps=+net_admin --bounding-set=+net_admin boundary "${BOUNDARY_ARGS[@]}" -- \ - claude "${CLAUDE_ARGS[@]}" + claude "${ARGS[@]}" else agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}" fi From 01f5100068148aaf302ed72e76aab0b77b26694a Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Fri, 24 Oct 2025 21:23:42 -0400 Subject: [PATCH 13/36] fix: drop perms for boundary process (#512) --- registry/coder/modules/claude-code/README.md | 14 +++++++------- .../coder/modules/claude-code/scripts/start.sh | 3 ++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index af0e58e8..c311eeb7 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.2" + version = "3.3.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -51,7 +51,7 @@ module "claude-code" { boundary_log_level = "WARN" boundary_additional_allowed_urls = ["GET *google.com"] boundary_proxy_port = "8087" - version = "3.3.2" + version = "3.3.3" } ``` @@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.2" + version = "3.3.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -106,7 +106,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.2" + version = "3.3.3" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -129,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.2" + version = "3.3.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.2" + version = "3.3.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.2" + version = "3.3.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 1daae35f..70452675 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -167,7 +167,8 @@ function start_agentapi() { fi agentapi server --allowed-hosts="*" --type claude --term-width 67 --term-height 1190 -- \ - sudo -E env PATH=$PATH setpriv --inh-caps=+net_admin --ambient-caps=+net_admin --bounding-set=+net_admin boundary "${BOUNDARY_ARGS[@]}" -- \ + sudo -E env PATH=$PATH setpriv --reuid=$(id -u) --regid=$(id -g) --clear-groups \ + --inh-caps=+net_admin --ambient-caps=+net_admin --bounding-set=+net_admin boundary "${BOUNDARY_ARGS[@]}" -- \ claude "${ARGS[@]}" else agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}" From d3b40c08f1b559f8f5fb349c9f3d1929cf6b58f7 Mon Sep 17 00:00:00 2001 From: DevCats Date: Mon, 27 Oct 2025 07:45:37 -0500 Subject: [PATCH 14/36] feat: add session resumption to codex (#506) ## Description Add continue variable, and logic for resuming task sessions ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [X] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder-labs/modules/codex` **New version:** `v3.1.0` **Breaking change:** [ ] Yes [X] No ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun fmt`) - [X] Changes tested locally ## Related Issues --- registry/coder-labs/modules/codex/README.md | 9 +- .../coder-labs/modules/codex/main.test.ts | 86 +++++++ registry/coder-labs/modules/codex/main.tf | 11 +- .../coder-labs/modules/codex/scripts/start.sh | 222 ++++++++++++++---- .../modules/codex/testdata/codex-mock.sh | 26 +- 5 files changed, 304 insertions(+), 50 deletions(-) diff --git a/registry/coder-labs/modules/codex/README.md b/registry/coder-labs/modules/codex/README.md index 9c749229..98062326 100644 --- a/registry/coder-labs/modules/codex/README.md +++ b/registry/coder-labs/modules/codex/README.md @@ -13,7 +13,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "3.0.0" + version = "3.1.0" agent_id = coder_agent.example.id openai_api_key = var.openai_api_key workdir = "/home/coder/project" @@ -33,7 +33,7 @@ module "codex" { module "codex" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder-labs/codex/coder" - version = "3.0.0" + version = "3.1.0" agent_id = coder_agent.example.id openai_api_key = "..." workdir = "/home/coder/project" @@ -61,7 +61,7 @@ module "coder-login" { module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "3.0.0" + version = "3.1.0" agent_id = coder_agent.example.id openai_api_key = "..." ai_prompt = data.coder_parameter.ai_prompt.value @@ -84,6 +84,7 @@ module "codex" { - **System Prompt**: If `codex_system_prompt` is set, writes the prompt to `AGENTS.md` in the `~/.codex/` directory - **Start**: Launches Codex CLI in the specified directory, wrapped by AgentAPI - **Configuration**: Sets `OPENAI_API_KEY` environment variable and passes `--model` flag to Codex CLI (if variables provided) +- **Session Continuity**: When `continue = true` (default), the module automatically tracks task sessions in `~/.codex-module/.codex-task-session`. On workspace restart, it resumes the existing session with full conversation history. Set `continue = false` to always start fresh sessions. ## Configuration @@ -107,7 +108,7 @@ For custom Codex configuration, use `base_config_toml` and/or `additional_mcp_se ```tf module "codex" { source = "registry.coder.com/coder-labs/codex/coder" - version = "3.0.0" + version = "3.1.0" # ... other variables ... # Override default configuration diff --git a/registry/coder-labs/modules/codex/main.test.ts b/registry/coder-labs/modules/codex/main.test.ts index 7d34a9c4..2041e36e 100644 --- a/registry/coder-labs/modules/codex/main.test.ts +++ b/registry/coder-labs/modules/codex/main.test.ts @@ -368,4 +368,90 @@ describe("codex", async () => { expect(prompt.exitCode).not.toBe(0); expect(prompt.stderr).toContain("No such file or directory"); }); + + test("codex-continue-capture-new-session", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + ai_prompt: "test task", + }, + }); + + const workdir = "/home/coder"; + const expectedSessionId = "019a1234-5678-9abc-def0-123456789012"; + const sessionsDir = "/home/coder/.codex/sessions"; + const sessionFile = `${sessionsDir}/${expectedSessionId}.jsonl`; + + await execContainer(id, ["mkdir", "-p", sessionsDir]); + await execContainer(id, [ + "bash", + "-c", + `echo '{"id":"${expectedSessionId}","cwd":"${workdir}","created":"2024-10-24T20:00:00Z","model":"gpt-4-turbo"}' > ${sessionFile}`, + ]); + + await execModuleScript(id); + + await expectAgentAPIStarted(id); + + const trackingFile = "/home/coder/.codex-module/.codex-task-session"; + const maxAttempts = 30; + let trackingFileContents = ""; + for (let attempt = 0; attempt < maxAttempts; attempt++) { + const result = await execContainer(id, [ + "bash", + "-c", + `cat ${trackingFile} 2>/dev/null || echo ""`, + ]); + if (result.stdout.trim().length > 0) { + trackingFileContents = result.stdout; + break; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + + expect(trackingFileContents).toContain(`${workdir}|${expectedSessionId}`); + + const startLog = await readFileContainer( + id, + "/home/coder/.codex-module/agentapi-start.log", + ); + expect(startLog).toContain("Capturing new session ID"); + expect(startLog).toContain("Session tracked"); + expect(startLog).toContain(expectedSessionId); + }); + + test("codex-continue-resume-existing-session", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + ai_prompt: "test prompt", + }, + }); + + const workdir = "/home/coder"; + const mockSessionId = "019a1234-5678-9abc-def0-123456789012"; + const trackingFile = "/home/coder/.codex-module/.codex-task-session"; + + await execContainer(id, ["mkdir", "-p", "/home/coder/.codex-module"]); + await execContainer(id, [ + "bash", + "-c", + `echo "${workdir}|${mockSessionId}" > ${trackingFile}`, + ]); + + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.codex-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain("Found existing task session"); + expect(startLog.stdout).toContain(mockSessionId); + expect(startLog.stdout).toContain("Resuming existing session"); + expect(startLog.stdout).toContain( + `Starting Codex with arguments: --model gpt-4-turbo resume ${mockSessionId}`, + ); + expect(startLog.stdout).not.toContain("test prompt"); + }); }); diff --git a/registry/coder-labs/modules/codex/main.tf b/registry/coder-labs/modules/codex/main.tf index d181c10f..a68cd79f 100644 --- a/registry/coder-labs/modules/codex/main.tf +++ b/registry/coder-labs/modules/codex/main.tf @@ -137,6 +137,12 @@ variable "ai_prompt" { default = "" } +variable "continue" { + type = bool + description = "Automatically continue existing sessions on workspace restart. When true, resumes existing conversation if found, otherwise runs prompt or starts new session. When false, always starts fresh (ignores existing sessions)." + default = true +} + variable "codex_system_prompt" { type = string description = "System instructions written to AGENTS.md in the ~/.codex directory" @@ -187,8 +193,9 @@ module "agentapi" { ARG_OPENAI_API_KEY='${var.openai_api_key}' \ ARG_REPORT_TASKS='${var.report_tasks}' \ ARG_CODEX_MODEL='${var.codex_model}' \ - ARG_CODEX_START_DIRECTORY='${var.workdir}' \ + ARG_CODEX_START_DIRECTORY='${local.workdir}' \ ARG_CODEX_TASK_PROMPT='${base64encode(var.ai_prompt)}' \ + ARG_CONTINUE='${var.continue}' \ /tmp/start.sh EOT @@ -206,7 +213,7 @@ module "agentapi" { ARG_BASE_CONFIG_TOML='${base64encode(var.base_config_toml)}' \ ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \ ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \ - ARG_CODEX_START_DIRECTORY='${var.workdir}' \ + ARG_CODEX_START_DIRECTORY='${local.workdir}' \ ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \ /tmp/install.sh EOT diff --git a/registry/coder-labs/modules/codex/scripts/start.sh b/registry/coder-labs/modules/codex/scripts/start.sh index be54d575..663e80e5 100644 --- a/registry/coder-labs/modules/codex/scripts/start.sh +++ b/registry/coder-labs/modules/codex/scripts/start.sh @@ -3,6 +3,7 @@ source "$HOME"/.bashrc set -o errexit set -o pipefail + command_exists() { command -v "$1" > /dev/null 2>&1 } @@ -16,6 +17,7 @@ fi printf "Version: %s\n" "$(codex --version)" set -o nounset ARG_CODEX_TASK_PROMPT=$(echo -n "$ARG_CODEX_TASK_PROMPT" | base64 -d) +ARG_CONTINUE=${ARG_CONTINUE:-true} echo "=== Codex Launch Configuration ===" printf "OpenAI API Key: %s\n" "$([ -n "$ARG_OPENAI_API_KEY" ] && echo "Provided" || echo "Not provided")" @@ -23,53 +25,187 @@ printf "Codex Model: %s\n" "${ARG_CODEX_MODEL:-"Default"}" printf "Start Directory: %s\n" "$ARG_CODEX_START_DIRECTORY" printf "Has Task Prompt: %s\n" "$([ -n "$ARG_CODEX_TASK_PROMPT" ] && echo "Yes" || echo "No")" printf "Report Tasks: %s\n" "$ARG_REPORT_TASKS" +printf "Continue Sessions: %s\n" "$ARG_CONTINUE" echo "======================================" set +o nounset -CODEX_ARGS=() -if command_exists codex; then - printf "Codex is installed\n" -else - printf "Error: Codex is not installed. Please enable install_codex or install it manually\n" - exit 1 -fi +SESSION_TRACKING_FILE="$HOME/.codex-module/.codex-task-session" -if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then - printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}" - cd "${ARG_CODEX_START_DIRECTORY}" || { - printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" - exit 1 - } -else - printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}" - mkdir -p "${ARG_CODEX_START_DIRECTORY}" || { - printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" - exit 1 - } - cd "${ARG_CODEX_START_DIRECTORY}" || { - printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" - exit 1 - } -fi +find_session_for_directory() { + local target_dir="$1" -if [ -n "$ARG_CODEX_MODEL" ]; then - CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL") -fi - -if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then - printf "Running the task prompt %s\n" "$ARG_CODEX_TASK_PROMPT" - if [ "${ARG_REPORT_TASKS}" == "true" ]; then - PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT" - else - PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT" + if [ ! -f "$SESSION_TRACKING_FILE" ]; then + return 1 fi - CODEX_ARGS+=("$PROMPT") -else - printf "No task prompt given.\n" -fi -# Terminal dimensions optimized for Coder Tasks UI sidebar: -# - Width 67: fits comfortably in sidebar -# - Height 1190: adjusted due to Codex terminal height bug -printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}" -agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}" + local session_id=$(grep "^$target_dir|" "$SESSION_TRACKING_FILE" | cut -d'|' -f2 | head -1) + + if [ -n "$session_id" ]; then + echo "$session_id" + return 0 + fi + + return 1 +} + +store_session_mapping() { + local dir="$1" + local session_id="$2" + + mkdir -p "$(dirname "$SESSION_TRACKING_FILE")" + + if [ -f "$SESSION_TRACKING_FILE" ]; then + grep -v "^$dir|" "$SESSION_TRACKING_FILE" > "$SESSION_TRACKING_FILE.tmp" 2> /dev/null || true + mv "$SESSION_TRACKING_FILE.tmp" "$SESSION_TRACKING_FILE" + fi + + echo "$dir|$session_id" >> "$SESSION_TRACKING_FILE" +} + +find_recent_session_file() { + local target_dir="$1" + local sessions_dir="$HOME/.codex/sessions" + + if [ ! -d "$sessions_dir" ]; then + return 1 + fi + + local latest_file="" + local latest_time=0 + + while IFS= read -r session_file; do + local file_time=$(stat -c %Y "$session_file" 2> /dev/null || stat -f %m "$session_file" 2> /dev/null || echo "0") + local first_line=$(head -n 1 "$session_file" 2> /dev/null) + local session_cwd=$(echo "$first_line" | grep -o '"cwd":"[^"]*"' | cut -d'"' -f4) + + if [ "$session_cwd" = "$target_dir" ] && [ "$file_time" -gt "$latest_time" ]; then + latest_file="$session_file" + latest_time="$file_time" + fi + done < <(find "$sessions_dir" -type f -name "*.jsonl" 2> /dev/null) + + if [ -n "$latest_file" ]; then + local first_line=$(head -n 1 "$latest_file") + local session_id=$(echo "$first_line" | grep -o '"id":"[^"]*"' | cut -d'"' -f4) + if [ -n "$session_id" ]; then + echo "$session_id" + return 0 + fi + fi + + return 1 +} + +wait_for_session_file() { + local target_dir="$1" + local max_attempts=20 + local attempt=0 + + while [ $attempt -lt $max_attempts ]; do + local session_id=$(find_recent_session_file "$target_dir" 2> /dev/null || echo "") + if [ -n "$session_id" ]; then + echo "$session_id" + return 0 + fi + sleep 0.5 + attempt=$((attempt + 1)) + done + + return 1 +} + +validate_codex_installation() { + if command_exists codex; then + printf "Codex is installed\n" + else + printf "Error: Codex is not installed. Please enable install_codex or install it manually\n" + exit 1 + fi +} + +setup_workdir() { + if [ -d "${ARG_CODEX_START_DIRECTORY}" ]; then + printf "Directory '%s' exists. Changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}" + cd "${ARG_CODEX_START_DIRECTORY}" || { + printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" + exit 1 + } + else + printf "Directory '%s' does not exist. Creating and changing to it.\\n" "${ARG_CODEX_START_DIRECTORY}" + mkdir -p "${ARG_CODEX_START_DIRECTORY}" || { + printf "Error: Could not create directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" + exit 1 + } + cd "${ARG_CODEX_START_DIRECTORY}" || { + printf "Error: Could not change to directory '%s'.\\n" "${ARG_CODEX_START_DIRECTORY}" + exit 1 + } + fi +} + +build_codex_args() { + CODEX_ARGS=() + + if [ -n "$ARG_CODEX_MODEL" ]; then + CODEX_ARGS+=("--model" "$ARG_CODEX_MODEL") + fi + + if [ "$ARG_CONTINUE" = "true" ]; then + existing_session=$(find_session_for_directory "$ARG_CODEX_START_DIRECTORY" 2> /dev/null || echo "") + + if [ -n "$existing_session" ]; then + printf "Found existing task session for this directory: %s\n" "$existing_session" + printf "Resuming existing session...\n" + CODEX_ARGS+=("resume" "$existing_session") + else + printf "No existing task session found for this directory\n" + printf "Starting new task session...\n" + + if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then + if [ "${ARG_REPORT_TASKS}" == "true" ]; then + PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT" + else + PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT" + fi + CODEX_ARGS+=("$PROMPT") + fi + fi + else + printf "Continue disabled, starting fresh session\n" + + if [ -n "$ARG_CODEX_TASK_PROMPT" ]; then + if [ "${ARG_REPORT_TASKS}" == "true" ]; then + PROMPT="Complete the task at hand in one go. Every step of the way, report your progress using coder_report_task tool with proper summary and statuses. Your task at hand: $ARG_CODEX_TASK_PROMPT" + else + PROMPT="Your task at hand: $ARG_CODEX_TASK_PROMPT" + fi + CODEX_ARGS+=("$PROMPT") + fi + fi +} + +capture_session_id() { + if [ "$ARG_CONTINUE" = "true" ] && [ -z "$existing_session" ]; then + printf "Capturing new session ID...\n" + new_session=$(wait_for_session_file "$ARG_CODEX_START_DIRECTORY" || echo "") + + if [ -n "$new_session" ]; then + store_session_mapping "$ARG_CODEX_START_DIRECTORY" "$new_session" + printf "✓ Session tracked: %s\n" "$new_session" + printf "This session will be automatically resumed on next restart\n" + else + printf "⚠ Could not capture session ID after 10s timeout\n" + fi + fi +} + +start_codex() { + printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}" + agentapi server --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}" & + capture_session_id +} + +validate_codex_installation +setup_workdir +build_codex_args +start_codex diff --git a/registry/coder-labs/modules/codex/testdata/codex-mock.sh b/registry/coder-labs/modules/codex/testdata/codex-mock.sh index 8c1c7366..fe8f3806 100644 --- a/registry/coder-labs/modules/codex/testdata/codex-mock.sh +++ b/registry/coder-labs/modules/codex/testdata/codex-mock.sh @@ -1,5 +1,6 @@ #!/bin/bash +# Handle --version flag if [[ "$1" == "--version" ]]; then echo "HELLO: $(bash -c env)" echo "codex version v1.0.0" @@ -8,7 +9,30 @@ fi set -e +SESSION_ID="" +IS_RESUME=false + +while [[ $# -gt 0 ]]; do + case $1 in + resume) + IS_RESUME=true + SESSION_ID="$2" + shift 2 + ;; + *) + shift + ;; + esac +done + +if [ "$IS_RESUME" = false ]; then + SESSION_ID="019a1234-5678-9abc-def0-123456789012" + echo "Created new session: $SESSION_ID" +else + echo "Resuming session: $SESSION_ID" +fi + while true; do - echo "$(date) - codex-mock" + echo "$(date) - codex-mock (session: $SESSION_ID)" sleep 15 done From d64851774bd902f78fadd6e7bcac38d675345b70 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Mon, 27 Oct 2025 18:36:19 +0500 Subject: [PATCH 15/36] fix(jetbrains): update Terraform version requirement to 1.9+ (#513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Updated `required_version` constraint from `>= 1.0` to `>= 1.9` in jetbrains module - Added inline comment explaining the cross-variable validation requirement - Bumped module version from `1.1.0` to `1.1.1` (patch version) ## Issue The jetbrains module uses cross-variable validation at line 169-171 where `var.options` is referenced within the `var.ide_config` validation block: ```tf validation { condition = alltrue([ for code in var.options : contains(keys(var.ide_config), code) ]) error_message = "The ide_config must be a superset of var.options." } ``` This pattern requires Terraform 1.9+ and fails on earlier versions with: ``` Error: Invalid reference in variable validation The condition for variable "ide_config" can only refer to the variable itself, using var.ide_config. ``` ## References - Terrafomr release blog that talks abut this feature: https://www.hashicorp.com/en/blog/terraform-1-9-enhances-input-variable-validations - Terraform PR that added this feature: https://github.com/hashicorp/terraform/pull/34955 - HashiCorp Support Article: https://support.hashicorp.com/hc/en-us/articles/43291233547027 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Co-authored-by: DevCats --- registry/coder/modules/jetbrains/README.md | 14 +++++++------- registry/coder/modules/jetbrains/main.tf | 5 +++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/registry/coder/modules/jetbrains/README.md b/registry/coder/modules/jetbrains/README.md index ef19ec20..9d08e645 100644 --- a/registry/coder/modules/jetbrains/README.md +++ b/registry/coder/modules/jetbrains/README.md @@ -14,7 +14,7 @@ This module adds JetBrains IDE buttons to launch IDEs directly from the dashboar module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/home/coder/project" # tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button." # Optional @@ -40,7 +40,7 @@ When `default` contains IDE codes, those IDEs are created directly without user module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/home/coder/project" default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA @@ -53,7 +53,7 @@ module "jetbrains" { module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/home/coder/project" # Show parameter with limited options @@ -67,7 +67,7 @@ module "jetbrains" { module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/home/coder/project" default = ["IU", "PY"] @@ -82,7 +82,7 @@ module "jetbrains" { module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/workspace/project" @@ -108,7 +108,7 @@ module "jetbrains" { module "jetbrains_pycharm" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/workspace/project" @@ -128,7 +128,7 @@ Add helpful tooltip text that appears when users hover over the IDE app buttons: module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.0" + version = "1.1.1" agent_id = coder_agent.example.id folder = "/home/coder/project" default = ["IU", "PY"] diff --git a/registry/coder/modules/jetbrains/main.tf b/registry/coder/modules/jetbrains/main.tf index d33fc6b2..8f0e0ac7 100644 --- a/registry/coder/modules/jetbrains/main.tf +++ b/registry/coder/modules/jetbrains/main.tf @@ -1,5 +1,5 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.9" required_providers { coder = { @@ -163,7 +163,8 @@ variable "ide_config" { condition = length(var.ide_config) > 0 error_message = "The ide_config must not be empty." } - # ide_config must be a superset of var.. options + # ide_config must be a superset of var.options + # Requires Terraform 1.9+ for cross-variable validation references validation { condition = alltrue([ for code in var.options : contains(keys(var.ide_config), code) From 1a15ad650a43a985d92f76da7f33264fdfd22095 Mon Sep 17 00:00:00 2001 From: Luis <25037200+angwdev@users.noreply.github.com> Date: Mon, 27 Oct 2025 17:12:24 -0500 Subject: [PATCH 16/36] Update Vault CLI download link to use architecture (#514) ## Description The download command was downloading only the amd64 version, ## Type of Change - [ ] New module - [ ] New template - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/[namespace]/modules/[module-name]` **New version:** `v1.0.0` **Breaking change:** [ ] Yes [ ] No ## Template Information **Path:** `registry/[namespace]/templates/[template-name]` ## Testing & Validation - [ ] Tests pass (`bun test`) - [ ] Code formatted (`bun fmt`) - [ ] Changes tested locally ## Related Issues --- registry/coder/modules/vault-token/README.md | 4 ++-- registry/coder/modules/vault-token/run.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/registry/coder/modules/vault-token/README.md b/registry/coder/modules/vault-token/README.md index e30abdc5..4561a170 100644 --- a/registry/coder/modules/vault-token/README.md +++ b/registry/coder/modules/vault-token/README.md @@ -19,7 +19,7 @@ variable "vault_token" { module "vault" { source = "registry.coder.com/coder/vault-token/coder" - version = "1.2.1" + version = "1.2.2" agent_id = coder_agent.example.id vault_token = var.token # optional vault_addr = "https://vault.example.com" @@ -73,7 +73,7 @@ variable "vault_token" { module "vault" { source = "registry.coder.com/coder/vault-token/coder" - version = "1.2.1" + version = "1.2.2" agent_id = coder_agent.example.id vault_addr = "https://vault.example.com" vault_token = var.token diff --git a/registry/coder/modules/vault-token/run.sh b/registry/coder/modules/vault-token/run.sh index e1da6ee8..9b83f32f 100644 --- a/registry/coder/modules/vault-token/run.sh +++ b/registry/coder/modules/vault-token/run.sh @@ -68,7 +68,7 @@ install() { else printf "Upgrading Vault CLI from version %s to %s ...\n\n" "$${CURRENT_VERSION}" "${INSTALL_VERSION}" fi - fetch vault.zip "https://releases.hashicorp.com/vault/$${INSTALL_VERSION}/vault_$${INSTALL_VERSION}_linux_amd64.zip" + fetch vault.zip "https://releases.hashicorp.com/vault/$${INSTALL_VERSION}/vault_$${INSTALL_VERSION}_linux_$${ARCH}.zip" if [ $? -ne 0 ]; then printf "Failed to download Vault.\n" return 1 From d6d0101f096259c93c982f02693fb1ded2e6fdc7 Mon Sep 17 00:00:00 2001 From: Rhys Williams <26030558+rhys96@users.noreply.github.com> Date: Tue, 28 Oct 2025 10:00:41 +0000 Subject: [PATCH 17/36] Fix Devolutions Auto-Complete (#508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description I’ve completed a set of modifications to improve the user experience and session behaviour within Devolutions Gateway: - Auto-Complete Fix: Resolved issues with auto-complete functionality. - Container Visibility: Implemented logic to hide the app-net-scan container, preventing it from displaying during the initial session load. - Default Settings: Enabled Unicode keyboard mode and dynamic window resizing by default to enhance usability. - Session Closure Behaviour: Modified the "Close Session" button to fully close the session window, avoiding returns to the session manager. - Dynamic Module Path Construction: Refactored the PowerShell module path setup to be dynamically constructed. - Input Variables: Added `slug` and `display_name` as input variables. ## Type of Change - [ ] New module - [ ] New template - [x] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/windows-rdp` **New version:** `v1.3.0` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues "None" --------- Co-authored-by: DevCats Co-authored-by: DevelopmentCats Co-authored-by: Eric Paulsen Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- registry/coder/modules/windows-rdp/README.md | 8 +- .../modules/windows-rdp/devolutions-patch.js | 743 +++++++++--------- .../coder/modules/windows-rdp/main.test.ts | 10 +- registry/coder/modules/windows-rdp/main.tf | 28 +- .../powershell-installation-script.tftpl | 49 +- 5 files changed, 462 insertions(+), 376 deletions(-) diff --git a/registry/coder/modules/windows-rdp/README.md b/registry/coder/modules/windows-rdp/README.md index f19afc47..92c5ac17 100644 --- a/registry/coder/modules/windows-rdp/README.md +++ b/registry/coder/modules/windows-rdp/README.md @@ -15,7 +15,7 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de module "windows_rdp" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windows-rdp/coder" - version = "1.2.3" + version = "1.3.0" agent_id = resource.coder_agent.main.id } ``` @@ -32,7 +32,7 @@ module "windows_rdp" { module "windows_rdp" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windows-rdp/coder" - version = "1.2.3" + version = "1.3.0" agent_id = resource.coder_agent.main.id } ``` @@ -43,7 +43,7 @@ module "windows_rdp" { module "windows_rdp" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windows-rdp/coder" - version = "1.2.3" + version = "1.3.0" agent_id = resource.coder_agent.main.id } ``` @@ -54,7 +54,7 @@ module "windows_rdp" { module "windows_rdp" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/windows-rdp/coder" - version = "1.2.3" + version = "1.3.0" agent_id = resource.coder_agent.main.id devolutions_gateway_version = "2025.2.2" # Specify a specific version } diff --git a/registry/coder/modules/windows-rdp/devolutions-patch.js b/registry/coder/modules/windows-rdp/devolutions-patch.js index ef736452..1f231ca3 100644 --- a/registry/coder/modules/windows-rdp/devolutions-patch.js +++ b/registry/coder/modules/windows-rdp/devolutions-patch.js @@ -25,401 +25,426 @@ * @typedef {Readonly<{ querySelector: string; value: string; }>} FormFieldEntry * @typedef {Readonly>} FormFieldEntries */ +(function () { + /** + * The communication protocol to set Devolutions to. + */ + const PROTOCOL = "RDP"; -/** - * The communication protocol to set Devolutions to. - */ -const PROTOCOL = "RDP"; + /** + * The hostname to use with Devolutions. + */ + const HOSTNAME = "localhost"; -/** - * The hostname to use with Devolutions. - */ -const HOSTNAME = "localhost"; + /** + * How often to poll the screen for the main Devolutions form. + */ + const POLL_INTERVAL_MS = 500; -/** - * How often to poll the screen for the main Devolutions form. - */ -const SCREEN_POLL_INTERVAL_MS = 500; - -/** - * The fields in the Devolutions sign-in form that should be populated with - * values from the Coder workspace. - * - * All properties should be defined as placeholder templates in the form - * VALUE_NAME. The Coder module, when spun up, should then run some logic to - * replace the template slots with actual values. These values should never - * change from within JavaScript itself. - * - * @satisfies {FormFieldEntries} - */ -const formFieldEntries = { - /** @readonly */ - username: { + /** + * The fields in the Devolutions sign-in form that should be populated with + * values from the Coder workspace. + * + * All properties should be defined as placeholder templates in the form + * VALUE_NAME. The Coder module, when spun up, should then run some logic to + * replace the template slots with actual values. These values should never + * change from within JavaScript itself. + * + * @satisfies {FormFieldEntries} + */ + const formFieldEntries = { /** @readonly */ - querySelector: "web-client-username-control input", + username: { + /** @readonly */ + querySelector: "web-client-username-control input", + /** @readonly */ + value: "${CODER_USERNAME}", + }, /** @readonly */ - value: "${CODER_USERNAME}", - }, + password: { + /** @readonly */ + querySelector: "web-client-password-control input", - /** @readonly */ - password: { - /** @readonly */ - querySelector: "web-client-password-control input", - - /** @readonly */ - value: "${CODER_PASSWORD}", - }, -}; - -/** - * Handles typing in the values for the input form. All values are written - * immediately, even though that would be physically impossible with a real - * keyboard. - * - * Note: this code will never break, but you might get warnings in the console - * from Angular about unexpected value changes. Angular patches over a lot of - * the built-in browser APIs to support its component change detection system. - * As part of that, it has validations for checking whether an input it - * previously had control over changed without it doing anything. - * - * But the only way to simulate a keyboard input is by setting the input's - * .value property, and then firing an input event. So basically, the inner - * value will change, which Angular won't be happy about, but then the input - * event will fire and sync everything back together. - * - * @param {HTMLInputElement} inputField - * @param {string} inputText - * @returns {Promise} - */ -function setInputValue(inputField, inputText) { - return new Promise((resolve, reject) => { - // Adding timeout for input event, even though we'll be dispatching it - // immediately, just in the off chance that something in the Angular app - // intercepts it or stops it from propagating properly - const timeoutId = window.setTimeout(() => { - reject(new Error("Input event did not get processed correctly in time.")); - }, 3_000); - - const handleSuccessfulDispatch = () => { - window.clearTimeout(timeoutId); - inputField.removeEventListener("input", handleSuccessfulDispatch); - resolve(); - }; - - inputField.addEventListener("input", handleSuccessfulDispatch); - - // Code assumes that Angular will have an event handler in place to handle - // the new event - const inputEvent = new Event("input", { - bubbles: true, - cancelable: true, - }); - - inputField.value = inputText; - inputField.dispatchEvent(inputEvent); - }); -} - -/** - * Takes a Devolutions remote session form, auto-fills it with data, and then - * submits it. - * - * The logic here is more convoluted than it should be for two main reasons: - * 1. Devolutions' HTML markup has errors. There are labels, but they aren't - * bound to the inputs they're supposed to describe. This means no easy hooks - * for selecting the elements, unfortunately. - * 2. Trying to modify the .value properties on some of the inputs doesn't - * work. Probably some combo of Angular data-binding and some inputs having - * the readonly attribute. Have to simulate user input to get around this. - * - * @param {HTMLFormElement} myForm - * @returns {Promise} - */ -async function autoSubmitForm(myForm) { - const setProtocolValue = () => { - /** @type {HTMLDivElement | null} */ - const protocolDropdownTrigger = myForm.querySelector('div[role="button"]'); - if (protocolDropdownTrigger === null) { - throw new Error("No clickable trigger for setting protocol value"); - } - - protocolDropdownTrigger.click(); - - // Can't use form as container for querying the list of dropdown options, - // because the elements don't actually exist inside the form. They're placed - // in the top level of the HTML doc, and repositioned to make it look like - // they're part of the form. Avoids CSS stacking context issues, maybe? - /** @type {HTMLLIElement | null} */ - const protocolOption = document.querySelector( - // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation - 'p-dropdownitem[ng-reflect-label="' + PROTOCOL + '"] li', - ); - - if (protocolOption === null) { - throw new Error( - "Unable to find protocol option on screen that matches desired protocol", - ); - } - - protocolOption.click(); + /** @readonly */ + value: "${CODER_PASSWORD}", + }, }; - const setHostname = () => { - /** @type {HTMLInputElement | null} */ - const hostnameInput = myForm.querySelector("p-autocomplete#hostname input"); + /** + * This ensures that the Devolutions login form (which by default, always shows + * up on screen when the app first launches) stays visually hidden from the user + * when they open Devolutions via the Coder module. + * + * The form will still be filled out automatically and submitted in the + * background via the rest of the logic in this file, so this function is mainly + * to help avoid screen flickering and make the overall experience feel a little + * more polished (even though it's just one giant hack). + * + * @returns {void} + */ + function hideFormForInitialSubmission() { + const styleId = "coder-patch--styles-initial-submission"; + const cssOpacityVariableName = "--coder-opacity-multiplier"; - if (hostnameInput === null) { - throw new Error("Unable to find field for adding hostname"); + /** @type {HTMLStyleElement | null} */ + // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation + let styleContainer = document.querySelector("#" + styleId); + if (!styleContainer) { + styleContainer = document.createElement("style"); + styleContainer.id = styleId; + styleContainer.innerHTML = ` + /* + Have to use opacity instead of visibility, because the element still + needs to be interactive via the script so that it can be auto-filled. + */ + :root { + /* + Can be 0 or 1. Start off invisible to avoid risks of UI flickering, + but the rest of the function should be in charge of making the form + container visible again if something goes wrong during setup. + + Double dollar sign needed to avoid Terraform script false positives + */ + $${cssOpacityVariableName}: 0; + } + + /* + web-client-form is the container for the main session form, while + the div is for the dropdown that is used for selecting the protocol. + The dropdown is not inside of the form for CSS styling reasons, so we + need to select both. + */ + web-client-form, + body > div.p-overlay { + /* + Double dollar sign needed to avoid Terraform script false positives + */ + opacity: calc(100% * var($${cssOpacityVariableName})) !important; + } + `; + + document.head.appendChild(styleContainer); } - return setInputValue(hostnameInput, HOSTNAME); - }; - - const setCoderFormFieldValues = async () => { - // The RDP form will not appear on screen unless the dropdown is set to use - // the RDP protocol - const rdpSubsection = myForm.querySelector("rdp-form"); - if (rdpSubsection === null) { - throw new Error( - "Unable to find RDP subsection. Is the value of the protocol set to RDP?", - ); - } - - for (const { value, querySelector } of Object.values(formFieldEntries)) { - /** @type {HTMLInputElement | null} */ - const input = document.querySelector(querySelector); - - if (input === null) { - throw new Error( - // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation - 'Unable to element that matches query "' + querySelector + '"', - ); - } - - await setInputValue(input, value); - } - }; - - const triggerSubmission = () => { - /** @type {HTMLButtonElement | null} */ - const submitButton = myForm.querySelector( - 'p-button[ng-reflect-type="submit"] button', - ); - - if (submitButton === null) { - throw new Error("Unable to find submission button"); - } - - if (submitButton.disabled) { - throw new Error( - "Unable to submit form because submit button is disabled. Are all fields filled out correctly?", - ); - } - - submitButton.click(); - }; - - setProtocolValue(); - await setHostname(); - await setCoderFormFieldValues(); - triggerSubmission(); -} - -/** - * Sets up logic for auto-populating the form data when the form appears on - * screen. - * - * @returns {void} - */ -function setupFormDetection() { - /** @type {HTMLFormElement | null} */ - let formValueFromLastMutation = null; - - /** @returns {void} */ - const onDynamicTabMutation = () => { - /** @type {HTMLFormElement | null} */ - const latestForm = document.querySelector("web-client-form > form"); - - // Only try to auto-fill if we went from having no form on screen to - // having a form on screen. That way, we don't accidentally override the - // form if the user is trying to customize values, and this essentially - // makes the script values function as default values - const mounted = formValueFromLastMutation === null && latestForm !== null; - if (mounted) { - autoSubmitForm(latestForm); - } - - formValueFromLastMutation = latestForm; - }; - - /** @type {number | undefined} */ - let pollingId = undefined; - - /** @returns {void} */ - const checkScreenForDynamicTab = () => { - const dynamicTab = document.querySelector("web-client-dynamic-tab"); - - // Keep polling until the main content container is on screen - if (dynamicTab === null) { + // The root node being undefined should be physically impossible (if it's + // undefined, the browser itself is busted), but we need to do a type check + // here so that the rest of the function doesn't need to do type checks over + // and over. + const rootNode = document.querySelector(":root"); + if (!(rootNode instanceof HTMLHtmlElement)) { + // Remove the container entirely because if the browser is busted, who knows + // if the CSS variables can be applied correctly. Better to have something + // be a bit more ugly/painful to use, than have it be impossible to use + styleContainer.remove(); return; } - window.clearInterval(pollingId); + // It's safe to make the form visible preemptively because Devolutions + // outputs the Windows view through an HTML canvas that it overlays on top + // of the rest of the app. Even if the form isn't hidden at the style level, + // it will still be covered up. + const restoreOpacity = () => { + rootNode.style.setProperty(cssOpacityVariableName, "1"); + }; - // Call the mutation callback manually, to ensure it runs at least once - onDynamicTabMutation(); + // If this file gets more complicated, it might make sense to set up the + // timeout and event listener so that if one triggers, it cancels the other, + // but having restoreOpacity run more than once is a no-op for right now. + // Not a big deal if these don't get cleaned up. - // Having the mutation observer is kind of an extra safety net that isn't - // really expected to run that often. Most of the content in the dynamic - // tab is being rendered through Canvas, which won't trigger any mutations - // that the observer can detect - const dynamicTabObserver = new MutationObserver(onDynamicTabMutation); - dynamicTabObserver.observe(dynamicTab, { - subtree: true, - childList: true, - }); - }; + // Have the form automatically reappear no matter what, so that if something + // does break, the user isn't left out to dry + window.setTimeout(restoreOpacity, 5_000); - pollingId = window.setInterval( - checkScreenForDynamicTab, - SCREEN_POLL_INTERVAL_MS, - ); -} - -/** - * Sets up custom styles for hiding default Devolutions elements that Coder - * users shouldn't need to care about. - * - * @returns {void} - */ -function setupAlwaysOnStyles() { - const styleId = "coder-patch--styles-always-on"; - // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation - const existingContainer = document.querySelector("#" + styleId); - if (existingContainer) { - return; + /** @type {HTMLFormElement | null} */ + const form = document.querySelector("web-client-form > form"); + form?.addEventListener( + "submit", + () => { + // Not restoring opacity right away just to give the HTML canvas a little + // bit of time to get spun up and cover up the main form + window.setTimeout(restoreOpacity, 1_000); + }, + { once: true }, + ); } - const styleContainer = document.createElement("style"); - styleContainer.id = styleId; - styleContainer.innerHTML = ` - /* app-menu corresponds to the sidebar of the default view. */ - app-menu { - display: none !important; + /** + * Sets up custom styles for hiding default Devolutions elements that Coder + * users shouldn't need to care about. + * + * @returns {void} + */ + function setupAlwaysOnStyles() { + const styleId = "coder-patch--styles-always-on"; + // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation + const existingContainer = document.querySelector("#" + styleId); + if (existingContainer) { + return; } - `; - document.head.appendChild(styleContainer); -} - -/** - * This ensures that the Devolutions login form (which by default, always shows - * up on screen when the app first launches) stays visually hidden from the user - * when they open Devolutions via the Coder module. - * - * The form will still be filled out automatically and submitted in the - * background via the rest of the logic in this file, so this function is mainly - * to help avoid screen flickering and make the overall experience feel a little - * more polished (even though it's just one giant hack). - * - * @returns {void} - */ -function hideFormForInitialSubmission() { - const styleId = "coder-patch--styles-initial-submission"; - const cssOpacityVariableName = "--coder-opacity-multiplier"; - - /** @type {HTMLStyleElement | null} */ - // biome-ignore lint/style/useTemplate: Have to skip interpolation for the main.tf interpolation - let styleContainer = document.querySelector("#" + styleId); - if (!styleContainer) { - styleContainer = document.createElement("style"); + const styleContainer = document.createElement("style"); styleContainer.id = styleId; styleContainer.innerHTML = ` - /* - Have to use opacity instead of visibility, because the element still - needs to be interactive via the script so that it can be auto-filled. - */ - :root { - /* - Can be 0 or 1. Start off invisible to avoid risks of UI flickering, - but the rest of the function should be in charge of making the form - container visible again if something goes wrong during setup. - - Double dollar sign needed to avoid Terraform script false positives - */ - $${cssOpacityVariableName}: 0; + /* app-menu corresponds to the sidebar of the default view. */ + app-menu { + display: none !important; } - /* - web-client-form is the container for the main session form, while - the div is for the dropdown that is used for selecting the protocol. - The dropdown is not inside of the form for CSS styling reasons, so we - need to select both. - */ - web-client-form, - body > div.p-overlay { - /* - Double dollar sign needed to avoid Terraform script false positives - */ - opacity: calc(100% * var($${cssOpacityVariableName})) !important; + /* app-net-scan corresponds to the auto-discovery feature. */ + app-net-scan { + display: none !important; } `; document.head.appendChild(styleContainer); } - // The root node being undefined should be physically impossible (if it's - // undefined, the browser itself is busted), but we need to do a type check - // here so that the rest of the function doesn't need to do type checks over - // and over. - const rootNode = document.querySelector(":root"); - if (!(rootNode instanceof HTMLHtmlElement)) { - // Remove the container entirely because if the browser is busted, who knows - // if the CSS variables can be applied correctly. Better to have something - // be a bit more ugly/painful to use, than have it be impossible to use - styleContainer.remove(); - return; + /** + * Handles typing in the values for the input form. All values are written + * immediately, even though that would be physically impossible with a real + * keyboard. + * + * Note: this code will never break, but you might get warnings in the console + * from Angular about unexpected value changes. Angular patches over a lot of + * the built-in browser APIs to support its component change detection system. + * As part of that, it has validations for checking whether an input it + * previously had control over changed without it doing anything. + * + * But the only way to simulate a keyboard input is by setting the input's + * .value property, and then firing an input event. So basically, the inner + * value will change, which Angular won't be happy about, but then the input + * event will fire and sync everything back together. + * + * @param {HTMLInputElement} inputField + * @param {string} inputText + * @returns {Promise} + */ + function setInputValue(inputField, inputText) { + return new Promise((resolve, reject) => { + // Adding timeout for input event, even though we'll be dispatching it + // immediately, just in the off chance that something in the Angular app + // intercepts it or stops it from propagating properly + const timeoutId = window.setTimeout(() => { + reject( + new Error("Input event did not get processed correctly in time."), + ); + }, 3_000); + + const handleSuccessfulDispatch = () => { + window.clearTimeout(timeoutId); + inputField.removeEventListener("input", handleSuccessfulDispatch); + resolve(); + }; + + inputField.addEventListener("input", handleSuccessfulDispatch); + + // Code assumes that Angular will have an event handler in place to handle + // the new event + const inputEvent = new Event("input", { + bubbles: true, + cancelable: true, + }); + + inputField.value = inputText; + inputField.dispatchEvent(inputEvent); + }); } - // It's safe to make the form visible preemptively because Devolutions - // outputs the Windows view through an HTML canvas that it overlays on top - // of the rest of the app. Even if the form isn't hidden at the style level, - // it will still be covered up. - const restoreOpacity = () => { - rootNode.style.setProperty(cssOpacityVariableName, "1"); - }; + /** + * Takes a Devolutions remote session form, auto-fills it with data, and then + * submits it. + * + * The logic here is more convoluted than it should be for two main reasons: + * 1. Devolutions' HTML markup has errors. There are labels, but they aren't + * bound to the inputs they're supposed to describe. This means no easy hooks + * for selecting the elements, unfortunately. + * 2. Trying to modify the .value properties on some of the inputs doesn't + * work. Probably some combo of Angular data-binding and some inputs having + * the readonly attribute. Have to simulate user input to get around this. + * + * @param {HTMLFormElement} form + */ + async function fillForm(form) { + try { + log("Form detected. Starting auto-fill..."); - // If this file gets more complicated, it might make sense to set up the - // timeout and event listener so that if one triggers, it cancels the other, - // but having restoreOpacity run more than once is a no-op for right now. - // Not a big deal if these don't get cleaned up. + // By default, RDP is selected. Leaving this here if needed + // in the future. + const protocolTrigger = form.querySelector('p-dropdown[id="protocol"]'); + if (protocolTrigger) { + protocolTrigger.click(); + const protocolOption = document.querySelector( + `li[aria-label="$${PROTOCOL}"]`, + ); + if (protocolOption) { + protocolOption.click(); + log(`Protocol set to $${PROTOCOL}`); + } else { + log("Protocol option not found."); + } + } else { + log("Protocol dropdown trigger not found."); + } - // Have the form automatically reappear no matter what, so that if something - // does break, the user isn't left out to dry - window.setTimeout(restoreOpacity, 5_000); + const hostnameInput = form.querySelector("p-autocomplete#hostname input"); + if (hostnameInput) { + await setInputValue(hostnameInput, HOSTNAME); + log(`Hostname set to $${HOSTNAME}`); + } else { + log("Hostname input not found."); + } - /** @type {HTMLFormElement | null} */ - const form = document.querySelector("web-client-form > form"); - form?.addEventListener( - "submit", - () => { - // Not restoring opacity right away just to give the HTML canvas a little - // bit of time to get spun up and cover up the main form - window.setTimeout(restoreOpacity, 1_000); - }, - { once: true }, - ); -} + for (const [key, { querySelector, value }] of Object.entries( + formFieldEntries, + )) { + const input = document.querySelector(querySelector); + if (input) { + await setInputValue(input, value); + log(`Set $${key} to $${value}`); + } else { + log(`Input for $${key} not found with selector: $${querySelector}`); + } + } -// Always safe to call these immediately because even if the Angular app isn't -// loaded by the time the function gets called, the CSS will always be globally -// available for when Angular is finally ready -setupAlwaysOnStyles(); -hideFormForInitialSubmission(); + const submitButton = form.querySelector( + 'p-button[class="p-element"] button', + ); + if (submitButton && !submitButton.disabled) { + submitButton.click(); + log("Form submitted."); + } else { + log("Submit button not found or disabled."); + } + } catch (err) { + console.error("[Devolutions Patch] Error during form fill:", err); + } + } -if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", setupFormDetection); -} else { - setupFormDetection(); -} + /** + * Attaches a click event listener to the "Close Session" button within the provided top bar element. + * When clicked, the listener triggers the window to close. + * Logs a message indicating whether the listener was successfully attached or if the button was not found. + * + * @param {HTMLElement} topBar - The container element that includes the "Close Session" button. + * @returns {void} + */ + function attachCloseListener(topBar) { + const buttons = topBar.querySelectorAll("button"); + + const closeButton = Array.from(buttons).find((button) => { + const labelSpan = button.querySelector(".p-button-label"); + return labelSpan && labelSpan.textContent.trim() === "Close Session"; + }); + + if (closeButton) { + closeButton.parentElement.addEventListener("click", () => { + window.close(); + }); + log("Close listener attached."); + } else { + log("Close button not found in top bar."); + } + } + + /** + * Sets the checked state of a checkbox based on its label text. + * Searches all components in the document and identifies the one + * whose label matches the provided `filterText`. Once found, it sets the checkbox + * to the specified `checked` state (true or false) and dispatches a change event + * to ensure any bound listeners (e.g., Angular change detection) are triggered. + * Logs the outcome of the operation for debugging or audit purposes. + * + * @param {string} filterText - The exact label text of the checkbox to target. + * @param {boolean} checked - The desired checked state (true to check, false to uncheck). + * @returns {void} + */ + function setCheckbox(filterText, checked) { + const checkboxes = document.querySelectorAll("p-checkbox"); + + const targetCheckbox = Array.from(checkboxes).find((checkbox) => { + const label = checkbox.querySelector(".p-checkbox-label"); + return label && label.textContent.trim() === filterText; + }); + + if (targetCheckbox) { + const input = targetCheckbox.querySelector('input[type="checkbox"]'); + if (input) { + input.checked = checked; + input.dispatchEvent(new Event("change", { bubbles: true })); + } + log(`$${filterText} set to $${checked}.`); + } else { + log(`$${filterText} checkbox not found in top bar.`); + } + } + + /** + * Continuously polls the DOM for a specific form element. + * - Searches for a

inside a element. + * - If found, calls `fillForm(form)` to process it. + * - If not found, logs a retry message and schedules another check after a delay. + * + * @returns {void} + */ + function pollForForm() { + const form = document.querySelector("web-client-form form"); + if (form) { + fillForm(form); + + // Start polling for top bar after form is filled + pollForSessionToolBar(); + } else { + log("Form not yet available. Retrying..."); + setTimeout(pollForForm, POLL_INTERVAL_MS); + } + } + + /** + * Continuously polls the DOM for a specific form element. + * - Searches for a element. + * - If found, adds another listener to session toolbar + * - If not found, logs a retry message and schedules another check after a delay. + * + * @returns {void} + */ + function pollForSessionToolBar() { + const sessionToolBar = document.querySelector("session-toolbar"); + if (sessionToolBar) { + log("Top bar detected. Proceeding with next steps..."); + attachCloseListener(sessionToolBar); + + // Automatically set checkboxes to improve user experience + setCheckbox("Unicode Keyboard Mode", true); + setCheckbox("Dynamic Resize", true); + } else { + log("Top bar not yet available. Retrying..."); + setTimeout(pollForSessionToolBar, POLL_INTERVAL_MS); + } + } + + /** + * Logs a message to the console with a standardized prefix. + * Format: [Devolutions Patch] $ + * + * @param {string} msg - The message to log. + * @returns {void} + */ + function log(msg) { + console.log(`[Devolutions Patch] $${msg}`); + } + + // Always safe to call these immediately because even if the Angular app isn't + // loaded by the time the function gets called, the CSS will always be globally + // available for when Angular is finally ready + setupAlwaysOnStyles(); + hideFormForInitialSubmission(); + + log("Script loaded. Starting form detection..."); + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", pollForForm); + } else { + pollForForm(); + } +})(); diff --git a/registry/coder/modules/windows-rdp/main.test.ts b/registry/coder/modules/windows-rdp/main.test.ts index 125b3b3b..80c09fd0 100644 --- a/registry/coder/modules/windows-rdp/main.test.ts +++ b/registry/coder/modules/windows-rdp/main.test.ts @@ -59,9 +59,11 @@ describe("Web RDP", async () => { expect(lines).toEqual( expect.arrayContaining([ '$moduleName = "DevolutionsGateway"', - // Devolutions does versioning in the format year.minor.patch - expect.stringMatching(/^\$moduleVersion = "\d{4}\.\d+\.\d+"$/), - "Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force", + // Default is "latest" to automatically get the newest version + '$moduleVersion = "latest"', + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12", + "Set-PSRepository -Name PSGallery -InstallationPolicy Trusted", + "Install-Module -Name $moduleName -Force", ]), ); }); @@ -86,7 +88,7 @@ describe("Web RDP", async () => { * @see {@link https://regex101.com/r/UMgQpv/2} */ const formEntryValuesRe = - /^const formFieldEntries = \{$.*?^\s+username: \{$.*?^\s*?querySelector.*?,$.*?^\s*value: "(?.+?)",$.*?password: \{$.*?^\s+querySelector: .*?,$.*?^\s*value: "(?.+?)",$.*?^};$/ms; + /username:\s*\{[\s\S]*?value:\s*"(?[^"]+)"[\s\S]*?password:\s*\{[\s\S]*?value:\s*"(?[^"]+)"/; // Test that things work with the default username/password const defaultState = await runTerraformApply( diff --git a/registry/coder/modules/windows-rdp/main.tf b/registry/coder/modules/windows-rdp/main.tf index c1b996dd..3c83d195 100644 --- a/registry/coder/modules/windows-rdp/main.tf +++ b/registry/coder/modules/windows-rdp/main.tf @@ -9,6 +9,24 @@ terraform { } } +variable "display_name" { + type = string + description = "The display name for the Web RDP application." + default = "Web RDP" +} + +variable "slug" { + type = string + description = "The slug for the Web RDP application." + default = "web-rdp" +} + +variable "icon" { + type = string + description = "The icon for the Web RDP application." + default = "/icon/desktop.svg" +} + 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)." @@ -48,8 +66,8 @@ variable "admin_password" { variable "devolutions_gateway_version" { type = string - default = "2025.2.2" - description = "Version of Devolutions Gateway to install. Defaults to the latest available version." + default = "latest" + description = "Version of Devolutions Gateway to install. Use 'latest' for the most recent version, or specify a version like '2025.3.2'." } resource "coder_script" "windows-rdp" { @@ -77,10 +95,10 @@ resource "coder_script" "windows-rdp" { resource "coder_app" "windows-rdp" { agent_id = var.agent_id share = var.share - slug = "web-rdp" - display_name = "Web RDP" + slug = var.slug + display_name = var.display_name url = "http://localhost:7171" - icon = "/icon/desktop.svg" + icon = var.icon subdomain = true order = var.order group = var.group diff --git a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl index 27c45b45..1657b878 100644 --- a/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl +++ b/registry/coder/modules/windows-rdp/powershell-installation-script.tftpl @@ -2,6 +2,9 @@ function Set-AdminPassword { param ( [string]$adminPassword ) + # Explicitly import LocalAccounts module + Import-Module Microsoft.PowerShell.LocalAccounts -ErrorAction SilentlyContinue + # Set admin password Get-LocalUser -Name "${admin_username}" | Set-LocalUser -Password (ConvertTo-SecureString -AsPlainText $adminPassword -Force) # Enable admin user @@ -28,23 +31,61 @@ function Install-DevolutionsGateway { $moduleName = "DevolutionsGateway" $moduleVersion = "${devolutions_gateway_version}" +# Ensure TLS 1.2 is enabled for PSGallery +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + # Install the module with the specified version for all users # This requires administrator privileges try { # Install-PackageProvider is required for AWS. Need to set command to # terminate on failure so that try/catch actually triggers Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -ErrorAction Stop - Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + + # Set PSGallery as trusted after NuGet is installed + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + + if ($moduleVersion -eq "latest" -or [string]::IsNullOrWhiteSpace($moduleVersion)) { + Install-Module -Name $moduleName -Force + } else { + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + } } catch { # If the first command failed, assume that we're on GCP and run # Install-Module only - Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + if ($moduleVersion -eq "latest" -or [string]::IsNullOrWhiteSpace($moduleVersion)) { + Install-Module -Name $moduleName -Force + } else { + Install-Module -Name $moduleName -RequiredVersion $moduleVersion -Force + } } # Construct the module path for system-wide installation -$moduleBasePath = "C:\Windows\system32\config\systemprofile\Documents\PowerShell\Modules\$moduleName\$moduleVersion" -$modulePath = Join-Path -Path $moduleBasePath -ChildPath "$moduleName.psd1" +$modulePath = $null # Declare outside the loop + +if ($moduleVersion -eq "latest" -or [string]::IsNullOrWhiteSpace($moduleVersion)) { + $installedModule = Get-InstalledModule -Name $moduleName -ErrorAction SilentlyContinue + if ($installedModule) { + $installedVersion = $installedModule.Version.ToString() + } +} else { + $installedVersion = $moduleVersion +} + +$paths = $env:PSModulePath -split ';' + +foreach ($path in $paths) { + $candidatePath = Join-Path -Path $path -ChildPath $moduleName + if ($installedVersion) { + $candidatePath = Join-Path -Path $candidatePath -ChildPath $installedVersion + } + + $psd1Path = Join-Path -Path $candidatePath -ChildPath "$moduleName.psd1" + if (Test-Path $psd1Path) { + $modulePath = $psd1Path + break + } +} # Import the module using the full path Import-Module $modulePath From 92ab526733542e77e56cf0fc1c629b70ec22cadb Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Wed, 29 Oct 2025 19:57:15 -0400 Subject: [PATCH 18/36] feat: change boundary rules according to new spec (#517) --- registry/coder/modules/claude-code/README.md | 14 +++++++------- registry/coder/modules/claude-code/main.tf | 2 +- .../coder/modules/claude-code/scripts/start.sh | 5 +++-- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index c311eeb7..3a0ec420 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.3" + version = "3.4.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -51,7 +51,7 @@ module "claude-code" { boundary_log_level = "WARN" boundary_additional_allowed_urls = ["GET *google.com"] boundary_proxy_port = "8087" - version = "3.3.3" + version = "3.4.3" } ``` @@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.3" + version = "3.4.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -106,7 +106,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.3" + version = "3.4.3" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -129,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.3" + version = "3.4.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.3" + version = "3.4.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.3.3" + version = "3.4.3" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 926b2402..93b3761b 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -353,7 +353,7 @@ module "agentapi" { ARG_BOUNDARY_VERSION='${var.boundary_version}' \ ARG_BOUNDARY_LOG_DIR='${var.boundary_log_dir}' \ ARG_BOUNDARY_LOG_LEVEL='${var.boundary_log_level}' \ - ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join(" ", var.boundary_additional_allowed_urls)}' \ + ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS='${join("|", var.boundary_additional_allowed_urls)}' \ ARG_BOUNDARY_PROXY_PORT='${var.boundary_proxy_port}' \ ARG_ENABLE_BOUNDARY_PPROF='${var.enable_boundary_pprof}' \ ARG_BOUNDARY_PPROF_PORT='${var.boundary_pprof_port}' \ diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 70452675..783e908d 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -144,12 +144,13 @@ function start_agentapi() { # Build boundary args with conditional --unprivileged flag BOUNDARY_ARGS=(--log-dir "$ARG_BOUNDARY_LOG_DIR") # Add default allowed URLs - BOUNDARY_ARGS+=(--allow "*anthropic.com" --allow "registry.npmjs.org" --allow "*sentry.io" --allow "claude.ai" --allow "$ARG_CODER_HOST") + BOUNDARY_ARGS+=(--allow "domain=anthropic.com" --allow "domain=registry.npmjs.org" --allow "domain=sentry.io" --allow "domain=claude.ai" --allow "domain=$ARG_CODER_HOST") # Add any additional allowed URLs from the variable if [ -n "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" ]; then - IFS=' ' read -ra ADDITIONAL_URLS <<< "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" + IFS='|' read -ra ADDITIONAL_URLS <<< "$ARG_BOUNDARY_ADDITIONAL_ALLOWED_URLS" for url in "${ADDITIONAL_URLS[@]}"; do + # Quote the URL to preserve spaces within the allow rule BOUNDARY_ARGS+=(--allow "$url") done fi From 0ce65b2b586ffb4abedbee282b28b4a277c98979 Mon Sep 17 00:00:00 2001 From: uzair-coder07 Date: Thu, 30 Oct 2025 00:28:52 -0500 Subject: [PATCH 19/36] fix(coder-labs/modules/sourcegraph-amp): explicitly require external provider (#519) Co-authored-by: Atif Ali --- registry/coder-labs/modules/sourcegraph-amp/README.md | 4 ++-- registry/coder-labs/modules/sourcegraph-amp/main.tf | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/registry/coder-labs/modules/sourcegraph-amp/README.md b/registry/coder-labs/modules/sourcegraph-amp/README.md index 5a5039f0..608defd6 100644 --- a/registry/coder-labs/modules/sourcegraph-amp/README.md +++ b/registry/coder-labs/modules/sourcegraph-amp/README.md @@ -13,7 +13,7 @@ Run [Amp CLI](https://ampcode.com/) in your workspace to access Sourcegraph's AI ```tf module "amp-cli" { source = "registry.coder.com/coder-labs/sourcegraph-amp/coder" - version = "2.0.0" + version = "2.0.1" agent_id = coder_agent.example.id sourcegraph_amp_api_key = var.sourcegraph_amp_api_key install_sourcegraph_amp = true @@ -48,7 +48,7 @@ variable "amp_api_key" { module "amp-cli" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder-labs/sourcegraph-amp/coder" - amp_version = "2.0.0" + amp_version = "2.0.1" agent_id = coder_agent.example.id amp_api_key = var.amp_api_key # recommended for tasks usage workdir = "/home/coder/project" diff --git a/registry/coder-labs/modules/sourcegraph-amp/main.tf b/registry/coder-labs/modules/sourcegraph-amp/main.tf index ddaa475d..fc36ea8d 100644 --- a/registry/coder-labs/modules/sourcegraph-amp/main.tf +++ b/registry/coder-labs/modules/sourcegraph-amp/main.tf @@ -6,7 +6,12 @@ terraform { source = "coder/coder" version = ">= 2.7" } + external = { + source = "hashicorp/external" + version = "2.3.5" + } } + } variable "agent_id" { From 5450113939db17321d70a0dda4c74e9897edb281 Mon Sep 17 00:00:00 2001 From: Atif Ali Date: Thu, 30 Oct 2025 20:17:33 +0500 Subject: [PATCH 20/36] fix(coder/modules/claude-code): move `set -euo pipefail` after sourcing `.bashrc` (#520) Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> --- registry/coder/modules/claude-code/README.md | 12 ++++++------ .../coder/modules/claude-code/scripts/install.sh | 4 +++- registry/coder/modules/claude-code/scripts/start.sh | 5 ++++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 3a0ec420..8e26416e 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.4.3" + version = "3.4.4" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.4.3" + version = "3.4.4" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -106,7 +106,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.4.3" + version = "3.4.4" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -129,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.4.3" + version = "3.4.4" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.4.3" + version = "3.4.4" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.4.3" + version = "3.4.4" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/scripts/install.sh b/registry/coder/modules/claude-code/scripts/install.sh index 21133384..80f84e6d 100644 --- a/registry/coder/modules/claude-code/scripts/install.sh +++ b/registry/coder/modules/claude-code/scripts/install.sh @@ -1,10 +1,12 @@ #!/bin/bash -set -euo pipefail if [ -f "$HOME/.bashrc" ]; then source "$HOME"/.bashrc fi +# Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles +set -euo pipefail + BOLD='\033[0;1m' command_exists() { diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index 783e908d..f5527b4f 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -1,9 +1,12 @@ #!/bin/bash -set -euo pipefail if [ -f "$HOME/.bashrc" ]; then source "$HOME"/.bashrc fi + +# Set strict error handling AFTER sourcing bashrc to avoid unbound variable errors from user dotfiles +set -euo pipefail + export PATH="$HOME/.local/bin:$PATH" command_exists() { From 925c71e6410d98a88e66e2924e2989257c90f732 Mon Sep 17 00:00:00 2001 From: DevCats Date: Fri, 31 Oct 2025 07:45:52 -0500 Subject: [PATCH 21/36] fix: improve version extraction logic to prevent false positives (#511) ## Description Makes the version extract and replace logic more specific so it wont replace any field that does is not specifically `field` under the detected modules. ## Type of Change - [ ] New module - [ ] New template - [X] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun fmt`) - [X] Changes tested locally ## Related Issues #510 --------- Co-authored-by: Atif Ali --- .github/scripts/version-bump.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/scripts/version-bump.sh b/.github/scripts/version-bump.sh index fc316619..6af3eca2 100755 --- a/.github/scripts/version-bump.sh +++ b/.github/scripts/version-bump.sh @@ -77,16 +77,19 @@ update_readme_version() { in_target_module = 0 } } - /version.*=.*"/ { + /^[[:space:]]*version[[:space:]]*=/ { if (in_target_module) { - gsub(/version[[:space:]]*=[[:space:]]*"[^"]*"/, "version = \"" new_version "\"") + match($0, /^[[:space]]*/ + indent = substr($0, 1, RLENGTH) + print indent "version = \"" new_version "\"" in_target_module = 0 + next } } { print } ' "$readme_path" > "${readme_path}.tmp" && mv "${readme_path}.tmp" "$readme_path" return 0 - elif grep -q 'version\s*=\s*"' "$readme_path"; then + elif grep -q '^[[:space:]]*version[[:space:]]*=' "$readme_path"; then echo "⚠️ Found version references but no module source match for $namespace/$module_name" return 1 fi @@ -148,9 +151,9 @@ main() { local current_version if [ -z "$latest_tag" ]; then - if [ -f "$readme_path" ] && grep -q 'version\s*=\s*"' "$readme_path"; then + if [ -f "$readme_path" ] && grep -q '^[[:space:]]*version[[:space:]]*=' "$readme_path"; then local readme_version - readme_version=$(grep 'version\s*=\s*"' "$readme_path" | head -1 | sed 's/.*version\s*=\s*"\([^"]*\)".*/\1/') + readme_version=$(awk '/^[[:space:]]*version[[:space:]]*=/ { match($0, /"[^"]*"/); print substr($0, RSTART+1, RLENGTH-2); exit }' "$readme_path") echo "No git tag found, but README shows version: $readme_version" if ! validate_version "$readme_version"; then From 4a11b06cba125a44341c0c9d484b5ae8a48fbff6 Mon Sep 17 00:00:00 2001 From: djarbz <30350993+djarbz@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:04:07 -0500 Subject: [PATCH 22/36] Fix/djarbz copyparty argcommas (#516) ## Description I discovered that if we included a comma inside an argument that bash would split it out as a separate argument. I added a test to verify. I also cleaned up some log formatting. ## Type of Change - [ ] New module - [ ] New template - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/djarbz/modules/copyparty` **New version:** `v1.0.1` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [N/A] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues None --------- Co-authored-by: DevCats --- registry/djarbz/modules/copyparty/README.md | 8 ++-- .../modules/copyparty/copyparty.tftest.hcl | 27 +++++++++++++- registry/djarbz/modules/copyparty/main.tf | 2 +- registry/djarbz/modules/copyparty/run.sh | 37 +++++++++---------- 4 files changed, 47 insertions(+), 27 deletions(-) diff --git a/registry/djarbz/modules/copyparty/README.md b/registry/djarbz/modules/copyparty/README.md index fdd3ad56..1209f852 100644 --- a/registry/djarbz/modules/copyparty/README.md +++ b/registry/djarbz/modules/copyparty/README.md @@ -17,7 +17,7 @@ This module installs Copyparty, an alternative to Filebrowser. module "copyparty" { count = data.coder_workspace.me.start_count source = "registry.coder.com/djarbz/copyparty/coder" - version = "1.0.0" + version = "1.0.1" } ``` @@ -35,7 +35,7 @@ Some basic command line options: module "copyparty" { count = data.coder_workspace.me.start_count source = "registry.coder.com/djarbz/copyparty/coder" - version = "1.0.0" + version = "1.0.1" agent_id = coder_agent.example.id arguments = [ "-v", "/home/coder/:/home:r", # Share home directory (read-only) @@ -51,14 +51,14 @@ module "copyparty" { module "copyparty" { count = data.coder_workspace.me.start_count source = "registry.coder.com/djarbz/copyparty/coder" - version = "1.0.0" + version = "1.0.1" agent_id = coder_agent.example.id subdomain = true arguments = [ "-v", "/tmp:/tmp:r", # Share tmp directory (read-only) "-v", "/home/coder/:/home:rw", # Share home directory (read-write) "-v", "${local.root_dir}:/work:A:c,dotsrch", # Share work directory (All Perms) - "-e2dsa", # Enables general file indexing" + "-e2dsa", # Enables general file indexing "--re-maxage", "900", # Rescan filesystem for changes every SEC "--see-dots", # Show dotfiles by default if user has correct permissions on volume "--xff-src=lan", # List of trusted reverse-proxy CIDRs (comma-separated) or `lan` for private IPs. diff --git a/registry/djarbz/modules/copyparty/copyparty.tftest.hcl b/registry/djarbz/modules/copyparty/copyparty.tftest.hcl index e6fab843..a6cb66af 100644 --- a/registry/djarbz/modules/copyparty/copyparty.tftest.hcl +++ b/registry/djarbz/modules/copyparty/copyparty.tftest.hcl @@ -80,7 +80,7 @@ run "test_defaults" { } assert { - condition = strcontains(coder_script.copyparty.script, "IFS=',' read -r -a ARGUMENTS \u003c\u003c\u003c \"\"") + condition = strcontains(coder_script.copyparty.script, "ARGUMENTS=()") error_message = "Script content does not reflect default empty arguments" } } @@ -138,7 +138,7 @@ run "test_custom_values" { } assert { - condition = strcontains(coder_script.copyparty.script, "IFS=',' read -r -a ARGUMENTS \u003c\u003c\u003c \"--verbose,-v\"") + condition = strcontains(coder_script.copyparty.script, "ARGUMENTS=(\"--verbose\" \"-v\")") error_message = "Script content does not reflect custom arguments" } @@ -179,3 +179,26 @@ run "test_invalid_share" { var.share, ] } + +# --- Test Case 7: Comma in Arguments [Readme Example 2] --- +run "test_comma_args" { + # Arguments containing commas + variables { + agent_id = "example-agent-id" + arguments = [ + "-v", "/tmp:/tmp:r", # Share tmp directory (read-only) + "-v", "/home/coder/:/home:rw", # Share home directory (read-write) + "-v", "/work:/work:A:c,dotsrch", # Share work directory (All Perms) + "-e2dsa", # Enables general file indexing + "--re-maxage", "900", # Rescan filesystem for changes every SEC + "--see-dots", # Show dotfiles by default if user has correct permissions on volume + "--xff-src=lan", # List of trusted reverse-proxy CIDRs (comma-separated) or `lan` for private IPs. + "--rproxy", "1", # Which ip to associate clients with, index of X-FWD IP. + ] + } + + assert { + condition = strcontains(coder_script.copyparty.script, "ARGUMENTS=(\"-v\" \"/tmp:/tmp:r\" \"-v\" \"/home/coder/:/home:rw\" \"-v\" \"/work:/work:A:c,dotsrch\" \"-e2dsa\" \"--re-maxage\" \"900\" \"--see-dots\" \"--xff-src=lan\" \"--rproxy\" \"1\")") + error_message = "Script content does not reflect Readme Example #2 arguments with commas" + } +} diff --git a/registry/djarbz/modules/copyparty/main.tf b/registry/djarbz/modules/copyparty/main.tf index 822387c1..e9976da5 100644 --- a/registry/djarbz/modules/copyparty/main.tf +++ b/registry/djarbz/modules/copyparty/main.tf @@ -129,7 +129,7 @@ resource "coder_script" "copyparty" { LOG_PATH : var.log_path, PORT : var.port, PINNED_VERSION : var.pinned_version, - ARGUMENTS : join(",", var.arguments), + ARGUMENTS : join(" ", formatlist("\"%s\"", var.arguments)), }) run_on_start = true run_on_stop = false diff --git a/registry/djarbz/modules/copyparty/run.sh b/registry/djarbz/modules/copyparty/run.sh index a138f540..99388759 100755 --- a/registry/djarbz/modules/copyparty/run.sh +++ b/registry/djarbz/modules/copyparty/run.sh @@ -9,19 +9,16 @@ LOG_PATH="${LOG_PATH}" PORT="${PORT}" # Pinned version (e.g., v1.19.16); overrides latest release discovery if set PINNED_VERSION="${PINNED_VERSION}" -# Custom CLI Arguments# The variable from Terraform is a single, comma-separated string. -# We need to split it into a proper bash array using the comma (,) as the delimiter. -IFS=',' read -r -a ARGUMENTS <<< "${ARGUMENTS}" +# Custom CLI Arguments +# The variable from Terraform is a series of quoted and space separated strings. +# We need to parse it into a proper bash array. +ARGUMENTS=(${ARGUMENTS}) # VARIABLE appears unused. Verify use (or export if used externally). # shellcheck disable=SC2034 MODULE_NAME="Copyparty" -# VARIABLE appears unused. Verify use (or export if used externally). -# shellcheck disable=SC2034 -BOLD='\033[0;1m' - -printf '%sInstalling %s ...\n\n' "$${BOLD}" "$${MODULE_NAME}" +printf '\e[1mInstalling %s ...\e[0m\n' "$${MODULE_NAME}" # Add code here # Use variables from the templatefile function in main.tf @@ -32,7 +29,7 @@ if ! command -v python3 &> /dev/null; then printf "❌ Python3 could not be found. Please install it to continue.\n" exit 1 fi -printf "✅ Python3 is installed.\n\n" +printf "✅ Python3 is installed.\n" RELEASE_TO_INSTALL="" # Install provided version to pin, otherwise discover latest github release from `https://github.com/9001/copyparty`. @@ -44,7 +41,7 @@ if [[ -n "$${PINNED_VERSION}" ]]; then exit 1 fi RELEASE_TO_INSTALL="$${PINNED_VERSION}" - printf "✅ Using pinned version %s.\n\n" "$${RELEASE_TO_INSTALL}" + printf "✅ Using pinned version %s.\n" "$${RELEASE_TO_INSTALL}" else printf "🔎 Discovering latest release from GitHub...\n" # Use curl to get the latest release tag from the GitHub API and sed to parse it @@ -54,11 +51,11 @@ else exit 1 fi RELEASE_TO_INSTALL="$${LATEST_RELEASE}" - printf "🏷️ Latest release is %s.\n\n" "$${RELEASE_TO_INSTALL}" + printf "🏷️ Latest release is %s.\n" "$${RELEASE_TO_INSTALL}" fi # Download appropriate release version assets: `copyparty-sfx.py` and `helptext.html`. -printf "🚀 Downloading copyparty v%s...\n" "$${RELEASE_TO_INSTALL}" +printf "🚀 Downloading copyparty %s...\n" "$${RELEASE_TO_INSTALL}" DOWNLOAD_URL="https://github.com/9001/copyparty/releases/download/$${RELEASE_TO_INSTALL}" printf "⏬ Downloading copyparty-sfx.py...\n" @@ -74,9 +71,9 @@ if ! curl -fsSL -o /tmp/helptext.html "$${DOWNLOAD_URL}/helptext.html"; then fi chmod +x /tmp/copyparty-sfx.py -printf "✅ Download complete.\n\n" +printf "✅ Download complete.\n" -printf "🥳 Installation complete!\n\n" +printf "🥳 Installation complete!\n" # Build a clean, quoted string of the command for logging purposes only. log_command="python3 /tmp/copyparty-sfx.py -p '$${PORT}'" @@ -85,16 +82,16 @@ for arg in "$${ARGUMENTS[@]}"; do log_command+=" '$${arg}'" done -# Clear the log file and write the header and command string using printf. +# Dump the executing command to a tmp file for diagnostic review. { printf "=== Starting copyparty at %s ===\n" "$(date)" printf "EXECUTING: %s\n" "$${log_command}" -} > "$${LOG_PATH}" +} > "/tmp/copyparty.cmd" -printf "👷 Starting %s in background...\n\n" "$${MODULE_NAME}" +printf "👷 Starting %s in background...\n" "$${MODULE_NAME}" # Execute the actual command using the robust array expansion. -# Then, append its output (stdout and stderr) to the log file. -python3 /tmp/copyparty-sfx.py -p "$${PORT}" "$${ARGUMENTS[@]}" >> "$${LOG_PATH}" 2>&1 & +# Then, capture its output (stdout and stderr) to the log file. +python3 /tmp/copyparty-sfx.py -p "$${PORT}" "$${ARGUMENTS[@]}" > "$${LOG_PATH}" 2>&1 & -printf "✅ Service started. Check logs at %s\n\n" "$${LOG_PATH}" +printf "✅ Service started. Check logs at %s\n" "$${LOG_PATH}" From ff02249128ab25ddf7727bf48e7ecde570761134 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 3 Nov 2025 11:26:08 -0500 Subject: [PATCH 23/36] refactor(coder/claude-code): support terraform provider coder 2.12.0 (#488) ## Description Updates the module to use the new version of the agentapi module for the upcoming Coder 2.28 release ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/claude-code` **New version:** `v4.0.0` **Breaking change:** [x] Yes [ ] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [ ] Changes tested locally ## Related Issues - https://github.com/coder/internal/issues/1065 ## Related PRs - https://github.com/coder/registry/pull/485 - https://github.com/coder/registry/pull/497 Co-authored-by: Cian Johnston --- registry/coder/modules/claude-code/README.md | 12 ++++++------ registry/coder/modules/claude-code/main.tf | 11 +++++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 8e26416e..4d186149 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.4.4" + version = "4.0.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.4.4" + version = "4.0.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -106,7 +106,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.4.4" + version = "4.0.0" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -129,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.4.4" + version = "4.0.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.4.4" + version = "4.0.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "3.4.4" + version = "4.0.0" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 93b3761b..4f95dcb3 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = ">= 2.7" + version = ">= 2.12" } } } @@ -270,7 +270,7 @@ resource "coder_env" "claude_api_key" { locals { # we have to trim the slash because otherwise coder exp mcp will - # set up an invalid claude config + # set up an invalid claude config workdir = trimsuffix(var.workdir, "/") app_slug = "ccw" install_script = file("${path.module}/scripts/install.sh") @@ -313,9 +313,8 @@ locals { } module "agentapi" { - source = "registry.coder.com/coder/agentapi/coder" - version = "1.2.0" + version = "2.0.0" agent_id = var.agent_id web_app_slug = local.app_slug @@ -379,3 +378,7 @@ module "agentapi" { /tmp/install.sh EOT } + +output "task_app_id" { + value = module.agentapi.task_app_id +} From 99e51bd365cda3329fde9aa719d8f10fe8dfd1ce Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 3 Nov 2025 11:32:06 -0500 Subject: [PATCH 24/36] refactor(coder/goose): support terraform provider coder v2.12.0 (#490) ## Description Updates the module to use the new version of the agentapi module for the upcoming Coder 2.28 release ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/goose` **New version:** `v1.0.0` **Breaking change:** [x] Yes [ ] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [ ] Changes tested locally ## Related Issues - https://github.com/coder/internal/issues/1065 ## Related PRs - https://github.com/coder/registry/pull/485 Co-authored-by: Cian Johnston --- registry/coder/modules/goose/README.md | 4 ++-- registry/coder/modules/goose/main.tf | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/registry/coder/modules/goose/README.md b/registry/coder/modules/goose/README.md index f4f91ab5..89fd7280 100644 --- a/registry/coder/modules/goose/README.md +++ b/registry/coder/modules/goose/README.md @@ -13,7 +13,7 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener ```tf module "goose" { source = "registry.coder.com/coder/goose/coder" - version = "2.2.1" + version = "3.0.0" agent_id = coder_agent.example.id folder = "/home/coder" install_goose = true @@ -79,7 +79,7 @@ resource "coder_agent" "main" { module "goose" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/goose/coder" - version = "2.2.1" + version = "3.0.0" agent_id = coder_agent.example.id folder = "/home/coder" install_goose = true diff --git a/registry/coder/modules/goose/main.tf b/registry/coder/modules/goose/main.tf index 51f8b6d6..b7db4f99 100644 --- a/registry/coder/modules/goose/main.tf +++ b/registry/coder/modules/goose/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = ">= 2.7" + version = ">= 2.12" } } } @@ -140,7 +140,7 @@ EOT module "agentapi" { source = "registry.coder.com/coder/agentapi/coder" - version = "1.2.0" + version = "2.0.0" agent_id = var.agent_id web_app_slug = local.app_slug @@ -174,3 +174,7 @@ module "agentapi" { /tmp/install.sh EOT } + +output "task_app_id" { + value = module.agentapi.task_app_id +} From 7249e902ea755b512890412344f5577912f7aa5d Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Mon, 3 Nov 2025 11:37:54 -0500 Subject: [PATCH 25/36] refactor(coder/amazon-q): support terraform provider coder 2.12.0 (#489) ## Description Updates the module to use the new version of the agentapi module for the upcoming Coder 2.28 release ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/amazon-q` **New version:** `v3.0.0` **Breaking change:** [x] Yes [ ] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [ ] Changes tested locally ## Related Issues - https://github.com/coder/internal/issues/1065 ## Related PRs - https://github.com/coder/registry/pull/485 Co-authored-by: Cian Johnston --- registry/coder/modules/amazon-q/README.md | 18 +++++++++--------- registry/coder/modules/amazon-q/main.tf | 8 ++++++-- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/registry/coder/modules/amazon-q/README.md b/registry/coder/modules/amazon-q/README.md index 3146f01e..71444a65 100644 --- a/registry/coder/modules/amazon-q/README.md +++ b/registry/coder/modules/amazon-q/README.md @@ -13,7 +13,7 @@ Run [Amazon Q](https://aws.amazon.com/q/) in your workspace to access Amazon's A ```tf module "amazon-q" { source = "registry.coder.com/coder/amazon-q/coder" - version = "2.1.1" + version = "3.0.0" agent_id = coder_agent.example.id workdir = "/home/coder" @@ -102,7 +102,7 @@ data "coder_parameter" "ai_prompt" { module "amazon-q" { source = "registry.coder.com/coder/amazon-q/coder" - version = "2.1.1" + version = "3.0.0" agent_id = coder_agent.example.id workdir = "/home/coder" auth_tarball = var.amazon_q_auth_tarball @@ -228,7 +228,7 @@ If no custom `agent_config` is provided, the default agent name "agent" is used. ```tf module "amazon-q" { source = "registry.coder.com/coder/amazon-q/coder" - version = "2.1.1" + version = "3.0.0" agent_id = coder_agent.example.id workdir = "/home/coder" auth_tarball = var.amazon_q_auth_tarball @@ -258,7 +258,7 @@ This example will: ```tf module "amazon-q" { source = "registry.coder.com/coder/amazon-q/coder" - version = "2.1.1" + version = "3.0.0" agent_id = coder_agent.example.id workdir = "/home/coder" auth_tarball = var.amazon_q_auth_tarball @@ -279,7 +279,7 @@ module "amazon-q" { ```tf module "amazon-q" { source = "registry.coder.com/coder/amazon-q/coder" - version = "2.1.1" + version = "3.0.0" agent_id = coder_agent.example.id workdir = "/home/coder" auth_tarball = var.amazon_q_auth_tarball @@ -305,7 +305,7 @@ module "amazon-q" { ```tf module "amazon-q" { source = "registry.coder.com/coder/amazon-q/coder" - version = "2.1.1" + version = "3.0.0" agent_id = coder_agent.example.id workdir = "/home/coder" auth_tarball = var.amazon_q_auth_tarball @@ -319,7 +319,7 @@ module "amazon-q" { ```tf module "amazon-q" { source = "registry.coder.com/coder/amazon-q/coder" - version = "2.1.1" + version = "3.0.0" agent_id = coder_agent.example.id workdir = "/home/coder" auth_tarball = var.amazon_q_auth_tarball @@ -340,7 +340,7 @@ module "amazon-q" { ```tf module "amazon-q" { source = "registry.coder.com/coder/amazon-q/coder" - version = "2.1.1" + version = "3.0.0" agent_id = coder_agent.example.id workdir = "/home/coder" auth_tarball = var.amazon_q_auth_tarball @@ -358,7 +358,7 @@ For environments without direct internet access, you can host Amazon Q installat ```tf module "amazon-q" { source = "registry.coder.com/coder/amazon-q/coder" - version = "2.1.1" + version = "3.0.0" agent_id = coder_agent.example.id workdir = "/home/coder" auth_tarball = var.amazon_q_auth_tarball diff --git a/registry/coder/modules/amazon-q/main.tf b/registry/coder/modules/amazon-q/main.tf index 84ac3c03..1fec87da 100644 --- a/registry/coder/modules/amazon-q/main.tf +++ b/registry/coder/modules/amazon-q/main.tf @@ -6,7 +6,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = ">= 2.7" + version = ">= 2.12" } } } @@ -214,7 +214,7 @@ locals { module "agentapi" { source = "registry.coder.com/coder/agentapi/coder" - version = "1.2.0" + version = "2.0.0" agent_id = var.agent_id folder = local.workdir @@ -268,3 +268,7 @@ module "agentapi" { /tmp/install.sh EOT } + +output "task_app_id" { + value = module.agentapi.task_app_id +} From b8bde9bf1213795b578369264c896f80d8d703d6 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:58:10 -0500 Subject: [PATCH 26/36] feat: add cmux module (#523) ## Description ## Type of Change - [x] New module - [ ] New template - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/[namespace]/modules/[module-name]` **New version:** `v1.0.0` **Breaking change:** [ ] Yes [ ] No ## Template Information **Path:** `registry/[namespace]/templates/[template-name]` ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues --- .icons/cmux.svg | 47 ++++++ registry/coder/modules/cmux/README.md | 104 ++++++++++++++ registry/coder/modules/cmux/cmux.tftest.hcl | 64 +++++++++ registry/coder/modules/cmux/main.test.ts | 66 +++++++++ registry/coder/modules/cmux/main.tf | 149 ++++++++++++++++++++ registry/coder/modules/cmux/run.sh | 135 ++++++++++++++++++ 6 files changed, 565 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.test.ts 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 00000000..95b56bb0 --- /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 00000000..503bbea2 --- /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 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" { + 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 +- **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 + +## 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 +} +``` + +### Skip Install + +Run without installing 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 + install = false +} +``` + +## Supported Platforms + +- Linux (x86_64, aarch64) + +## Notes + +- cmux is currently in preview and you may encounter bugs +- 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 new file mode 100644 index 00000000..3b831b37 --- /dev/null +++ b/registry/coder/modules/cmux/cmux.tftest.hcl @@ -0,0 +1,64 @@ +run "required_vars" { + command = plan + + variables { + agent_id = "foo" + } +} + +run "install_false_and_use_cached_conflict" { + command = plan + + variables { + agent_id = "foo" + use_cached = true + install = false + } + + 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" + } +} + +# install=false should succeed +run "install_false_only_success" { + command = plan + + variables { + agent_id = "foo" + install = false + } +} + +# 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 00000000..5ff42c3f --- /dev/null +++ b/registry/coder/modules/cmux/main.test.ts @@ -0,0 +1,66 @@ +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 --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...", + "🥳 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 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 new file mode 100644 index 00000000..b1c318d8 --- /dev/null +++ b/registry/coder/modules/cmux/main.tf @@ -0,0 +1,149 @@ +terraform { + # Requires Terraform 1.9+ for cross-variable validation references + required_version = ">= 1.9" + + 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 for cmux logs." + 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 "install" { + type = bool + description = "Install cmux from the network (npm or tarball). If false, run without installing (requires a pre-installed cmux)." + default = true +} + +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.install, + USE_CACHED : var.use_cached, + }) + run_on_start = true + + lifecycle { + precondition { + condition = var.install || !var.use_cached + error_message = "Cannot use 'use_cached' when 'install' is false" + } + } +} + +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 00000000..23656f8f --- /dev/null +++ b/registry/coder/modules/cmux/run.sh @@ -0,0 +1,135 @@ +#!/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="" + # 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 + # 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" + 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 e357fcf1f376a2a53d82d99db01c4db1da99d292 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:22:55 +0100 Subject: [PATCH 27/36] fix: updated icon for cmux module (#531) ## Description ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/cmux` **New version:** `v1.0.1` **Breaking change:** [ ] Yes [ ] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally --- registry/coder/modules/cmux/main.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/cmux/main.tf b/registry/coder/modules/cmux/main.tf index b1c318d8..37ec5202 100644 --- a/registry/coder/modules/cmux/main.tf +++ b/registry/coder/modules/cmux/main.tf @@ -110,7 +110,7 @@ variable "open_in" { resource "coder_script" "cmux" { agent_id = var.agent_id display_name = "cmux" - icon = "/icon/terminal.svg" + icon = "/icon/cmux.svg" script = templatefile("${path.module}/run.sh", { VERSION : var.install_version, PORT : var.port, @@ -134,7 +134,7 @@ resource "coder_app" "cmux" { slug = var.slug display_name = var.display_name url = "http://localhost:${var.port}" - icon = "/icon/terminal.svg" + icon = "/icon/cmux.svg" subdomain = var.subdomain share = var.share order = var.order From f0ccb20846e83918b05f56c5493fbddd9e1e3596 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:41:36 +0100 Subject: [PATCH 28/36] fix: bump version in README.md (#532) ## Description ## Type of Change - [ ] New module - [ ] New template - [x] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/[namespace]/modules/[module-name]` **New version:** `v1.0.0` **Breaking change:** [ ] Yes [ ] No ## Template Information **Path:** `registry/[namespace]/templates/[template-name]` ## Testing & Validation - [ ] Tests pass (`bun test`) - [ ] Code formatted (`bun fmt`) - [ ] Changes tested locally ## Related Issues --- registry/coder/modules/cmux/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/registry/coder/modules/cmux/README.md b/registry/coder/modules/cmux/README.md index 503bbea2..c1ced283 100644 --- a/registry/coder/modules/cmux/README.md +++ b/registry/coder/modules/cmux/README.md @@ -14,7 +14,7 @@ Automatically install and run [cmux](https://github.com/coder/cmux) in a Coder w module "cmux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/cmux/coder" - version = "1.0.0" + version = "1.0.2" agent_id = coder_agent.example.id } ``` @@ -35,7 +35,7 @@ module "cmux" { module "cmux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/cmux/coder" - version = "1.0.0" + version = "1.0.2" agent_id = coder_agent.example.id } ``` @@ -46,7 +46,7 @@ module "cmux" { module "cmux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/cmux/coder" - version = "1.0.0" + version = "1.0.2" agent_id = coder_agent.example.id # Default is "latest"; set to a specific version to pin install_version = "0.4.0" @@ -59,7 +59,7 @@ module "cmux" { module "cmux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/cmux/coder" - version = "1.0.0" + version = "1.0.2" agent_id = coder_agent.example.id port = 8080 } @@ -73,7 +73,7 @@ Run an existing copy of cmux if found, otherwise install from npm: module "cmux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/cmux/coder" - version = "1.0.0" + version = "1.0.2" agent_id = coder_agent.example.id use_cached = true } @@ -87,7 +87,7 @@ Run without installing from the network (requires cmux to be pre-installed): module "cmux" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/cmux/coder" - version = "1.0.0" + version = "1.0.2" agent_id = coder_agent.example.id install = false } From 578ed89697c2b72c1e84851e7d3a82bdd8e2d20c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 07:56:03 -0600 Subject: [PATCH 29/36] chore(deps): bump the github-actions group across 1 directory with 2 updates (#530) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the github-actions group with 2 updates in the / directory: [crate-ci/typos](https://github.com/crate-ci/typos) and [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action). Updates `crate-ci/typos` from 1.38.1 to 1.39.0
Release notes

Sourced from crate-ci/typos's releases.

v1.39.0

[1.39.0] - 2025-10-31

Features

Fixes

  • When a typo is pluralized, prefer pluralized corrections
Changelog

Sourced from crate-ci/typos's changelog.

[1.39.0] - 2025-10-31

Features

Fixes

  • When a typo is pluralized, prefer pluralized corrections
Commits

Updates `golangci/golangci-lint-action` from 8 to 9
Release notes

Sourced from golangci/golangci-lint-action's releases.

v9.0.0

In the scope of this release, we change Nodejs runtime from node20 to node24 (https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/).

What's Changed

Changes

Full Changelog: https://github.com/golangci/golangci-lint-action/compare/v8.0.0...v9.0.0

Commits
  • 0a35821 docs: update readme
  • 043b1b8 feat: support Module Plugin System (#1306)
  • a66d26a feat: add install-only option (#1305)
  • 7fe1b22 build(deps): bump the dependencies group with 2 updates (#1303)
  • 14973f1 build(deps-dev): bump the dev-dependencies group with 2 updates (#1299)
  • 8c2d575 build(deps): bump @​types/node from 24.8.1 to 24.9.1 in the dependencies group...
  • b002b6e build(deps): bump actions/setup-node from 5 to 6 (#1296)
  • c13f4ed build(deps): bump @​types/node from 24.7.2 to 24.8.1 in the dependencies group...
  • b68d21b docs: improve readme
  • 06188a2 build(deps): bump github/codeql-action from 3 to 4 (#1293)
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore ` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore ` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore ` will remove the ignore condition of the specified dependency and ignore conditions
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: DevCats --- .github/workflows/ci.yaml | 2 +- .github/workflows/golangci-lint.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 502511c9..75e859ab 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -82,7 +82,7 @@ jobs: - name: Validate formatting run: bun fmt:ci - name: Check for typos - uses: crate-ci/typos@v1.38.1 + uses: crate-ci/typos@v1.39.0 with: config: .github/typos.toml validate-readme-files: diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 29e7ef69..c275a7af 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -19,6 +19,6 @@ jobs: with: go-version: stable - name: golangci-lint - uses: golangci/golangci-lint-action@v8 + uses: golangci/golangci-lint-action@v9 with: version: v2.1 From 69abf48390aefae725f97f2cc53bbdc5c6ef3c42 Mon Sep 17 00:00:00 2001 From: Anis Khalfallah Date: Mon, 10 Nov 2025 22:34:44 +0100 Subject: [PATCH 30/36] test: add terraform tests to kiro module (#529) ## Description Add terraform tests to kiro module ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/kiro` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [x] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues #308 Signed-off-by: Anis KHALFALLAH Co-authored-by: DevCats --- registry/coder/modules/kiro/kiro.tftest.hcl | 124 ++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 registry/coder/modules/kiro/kiro.tftest.hcl diff --git a/registry/coder/modules/kiro/kiro.tftest.hcl b/registry/coder/modules/kiro/kiro.tftest.hcl new file mode 100644 index 00000000..b132551a --- /dev/null +++ b/registry/coder/modules/kiro/kiro.tftest.hcl @@ -0,0 +1,124 @@ +run "required_vars" { + command = plan + + variables { + agent_id = "foo" + } +} + +run "default_output" { + command = plan + + variables { + agent_id = "foo" + } + + assert { + condition = output.kiro_url == "kiro://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN" + error_message = "Default kiro_url must match expected value" + } + + assert { + condition = coder_app.kiro.order == null + error_message = "coder_app order must be null by default" + } +} + +run "adds_folder" { + command = plan + + variables { + agent_id = "foo" + folder = "/foo/bar" + } + + assert { + condition = output.kiro_url == "kiro://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN" + error_message = "URL must include folder parameter" + } +} + +run "folder_and_open_recent" { + command = plan + + variables { + agent_id = "foo" + folder = "/foo/bar" + open_recent = true + } + + assert { + condition = output.kiro_url == "kiro://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN" + error_message = "URL must include folder and openRecent parameters" + } +} + +run "custom_slug_display_name" { + command = plan + + variables { + agent_id = "foo" + slug = "kiro-ai" + display_name = "Kiro AI IDE" + } + + assert { + condition = coder_app.kiro.slug == "kiro-ai" + error_message = "coder_app slug must be set to kiro-ai" + } + + assert { + condition = coder_app.kiro.display_name == "Kiro AI IDE" + error_message = "coder_app display_name must be set to Kiro AI IDE" + } +} + +run "sets_order" { + command = plan + + variables { + agent_id = "foo" + order = 5 + } + + assert { + condition = coder_app.kiro.order == 5 + error_message = "coder_app order must be set to 5" + } +} + +run "sets_group" { + command = plan + + variables { + agent_id = "foo" + group = "AI IDEs" + } + + assert { + condition = coder_app.kiro.group == "AI IDEs" + error_message = "coder_app group must be set to AI IDEs" + } +} + +run "writes_mcp_json" { + command = plan + + variables { + agent_id = "foo" + mcp = jsonencode({ + servers = { + demo = { url = "http://localhost:1234" } + } + }) + } + + assert { + condition = strcontains(coder_script.kiro_mcp[0].script, base64encode(jsonencode({ + servers = { + demo = { url = "http://localhost:1234" } + } + }))) + error_message = "coder_script must contain base64-encoded MCP JSON" + } +} \ No newline at end of file From 4edfdae572572832f8e6659e56ed5f2f5871e1dc Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 12 Nov 2025 10:31:13 +0000 Subject: [PATCH 31/36] refactor(coder-labs/tasks-docker): support coder/claude-code 4.0.0 (#497) Updates the template to use the new version of the claude-code module for the Coder 2.28 release. This closely matches the [built-in template](https://github.com/coder/coder/blob/main/examples/templates/tasks-docker/main.tf) defined in [coder/coder](https://github.com/coder/coder). --- .../coder-labs/templates/tasks-docker/main.tf | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/registry/coder-labs/templates/tasks-docker/main.tf b/registry/coder-labs/templates/tasks-docker/main.tf index c0a165fc..5fbea1af 100644 --- a/registry/coder-labs/templates/tasks-docker/main.tf +++ b/registry/coder-labs/templates/tasks-docker/main.tf @@ -1,7 +1,8 @@ terraform { required_providers { coder = { - source = "coder/coder" + source = "coder/coder" + version = ">= 2.13" } docker = { source = "kreuzwerker/docker" @@ -12,22 +13,32 @@ terraform { # This template requires a valid Docker socket # However, you can reference our Kubernetes/VM # example templates and adapt the Claude Code module -# -# see: https://registry.coder.com/templates +# +# see: https://registry.coder.com/templates provider "docker" {} +# A `coder_ai_task` resource enables Tasks and associates +# the task with the coder_app that will act as an AI agent. +resource "coder_ai_task" "task" { + count = data.coder_workspace.me.start_count + app_id = module.claude-code[count.index].task_app_id +} + +# You can read the task prompt from the `coder_task` data source. +data "coder_task" "me" {} + # The Claude Code module does the automatic task reporting # Other agent modules: https://registry.coder.com/modules?search=agent -# Or use a custom agent: +# Or use a custom agent: module "claude-code" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/claude-code/coder" - version = "3.0.0" + version = "4.0.0" agent_id = coder_agent.main.id workdir = "/home/coder/projects" order = 999 claude_api_key = "" - ai_prompt = data.coder_parameter.ai_prompt.value + ai_prompt = data.coder_task.me.prompt system_prompt = data.coder_parameter.system_prompt.value model = "sonnet" permission_mode = "plan" @@ -51,13 +62,13 @@ data "coder_workspace_preset" "default" { (servers, dev watchers, GUI apps). - Built-in tools - use for everything else: (file operations, git commands, builds & installs, one-off shell commands) - + Remember this decision rule: - Stays running? → desktop-commander - Finishes immediately? → built-in tools - + -- Context -- - There is an existing app and tmux dev server running on port 8000. Be sure to read it's CLAUDE.md (./realworld-django-rest-framework-angular/CLAUDE.md) to learn more about it. + There is an existing app and tmux dev server running on port 8000. Be sure to read it's CLAUDE.md (./realworld-django-rest-framework-angular/CLAUDE.md) to learn more about it. Since this app is for demo purposes and the user is previewing the homepage and subsequent pages, aim to make the first visual change/prototype very quickly so the user can preview it, then focus on backend or logic which can be a more involved, long-running architecture plan. @@ -107,7 +118,7 @@ data "coder_workspace_preset" "default" { # Pre-builds is a Coder Premium # feature to speed up workspace creation - # + # # see https://coder.com/docs/admin/templates/extending-templates/prebuilt-workspaces # prebuilds { # instances = 1 @@ -126,13 +137,6 @@ data "coder_parameter" "system_prompt" { description = "System prompt for the agent with generalized instructions" mutable = false } -data "coder_parameter" "ai_prompt" { - type = "string" - name = "AI Prompt" - default = "" - description = "Write a prompt for Claude Code" - mutable = true -} data "coder_parameter" "setup_script" { name = "setup_script" display_name = "Setup Script" @@ -373,4 +377,4 @@ resource "docker_container" "workspace" { label = "coder.workspace_name" value = data.coder_workspace.me.name } -} \ No newline at end of file +} From 9e89f046916db1a1285443393b0b7cb4fa042039 Mon Sep 17 00:00:00 2001 From: 35C4n0r <70096901+35C4n0r@users.noreply.github.com> Date: Wed, 12 Nov 2025 21:03:37 +0530 Subject: [PATCH 32/36] fix: session resumption fix, and bug fixes for arg path logic (#522) ## Description Fix issue with commands being injected through prompt. Bug fix for logic in arg paths. ## Type of Change - [ ] New module - [ ] New template - [X] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/claude-code` **New version:** `v4.0.1` **Breaking change:** [ ] Yes [X] No ## Testing & Validation - [X] Tests pass (`bun test`) - [X] Code formatted (`bun fmt`) - [ ] Changes tested locally ## Related Issues --------- Co-authored-by: DevelopmentCats Co-authored-by: DevelopmentCats --- registry/coder/modules/claude-code/README.md | 12 ++-- .../coder/modules/claude-code/main.test.ts | 42 ++++++++++- registry/coder/modules/claude-code/main.tf | 1 + .../coder/modules/claude-code/main.tftest.hcl | 6 +- .../scripts/remove-last-session-id.sh | 10 ++- .../modules/claude-code/scripts/start.sh | 71 ++++++++++++++----- 6 files changed, 112 insertions(+), 30 deletions(-) diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md index 4d186149..d2a92aff 100644 --- a/registry/coder/modules/claude-code/README.md +++ b/registry/coder/modules/claude-code/README.md @@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.0.0" + version = "4.0.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_api_key = "xxxx-xxxxx-xxxx" @@ -70,7 +70,7 @@ data "coder_parameter" "ai_prompt" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.0.0" + version = "4.0.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" @@ -106,7 +106,7 @@ Run and configure Claude Code as a standalone CLI in your workspace. ```tf module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.0.0" + version = "4.0.1" agent_id = coder_agent.example.id workdir = "/home/coder" install_claude_code = true @@ -129,7 +129,7 @@ variable "claude_code_oauth_token" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.0.0" + version = "4.0.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" claude_code_oauth_token = var.claude_code_oauth_token @@ -202,7 +202,7 @@ resource "coder_env" "bedrock_api_key" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.0.0" + version = "4.0.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0" @@ -259,7 +259,7 @@ resource "coder_env" "google_application_credentials" { module "claude-code" { source = "registry.coder.com/coder/claude-code/coder" - version = "4.0.0" + version = "4.0.1" agent_id = coder_agent.example.id workdir = "/home/coder/project" model = "claude-sonnet-4@20250514" diff --git a/registry/coder/modules/claude-code/main.test.ts b/registry/coder/modules/claude-code/main.test.ts index a7c2dd14..94fcb391 100644 --- a/registry/coder/modules/claude-code/main.test.ts +++ b/registry/coder/modules/claude-code/main.test.ts @@ -198,15 +198,16 @@ describe("claude-code", async () => { expect(startLog.stdout).toContain(`--model ${model}`); }); - test("claude-continue-resume-existing-session", async () => { + test("claude-continue-resume-task-session", async () => { const { id } = await setup({ moduleVariables: { continue: "true", + report_tasks: "true", ai_prompt: "test prompt", }, }); - // Create a mock session file with the predefined task session ID + // Create a mock task session file with the hardcoded task session ID const taskSessionId = "cd32e253-ca16-4fd3-9825-d837e74ae3c2"; const sessionDir = `/home/coder/.claude/projects/-home-coder-project`; await execContainer(id, ["mkdir", "-p", sessionDir]); @@ -226,6 +227,43 @@ describe("claude-code", async () => { expect(startLog.stdout).toContain("--resume"); expect(startLog.stdout).toContain(taskSessionId); expect(startLog.stdout).toContain("Resuming existing task session"); + expect(startLog.stdout).toContain("--dangerously-skip-permissions"); + }); + + test("claude-continue-resume-standalone-session", async () => { + const { id } = await setup({ + moduleVariables: { + continue: "true", + report_tasks: "false", + ai_prompt: "test prompt", + }, + }); + + const sessionId = "some-random-session-id"; + const workdir = "/home/coder/project"; + const claudeJson = { + projects: { + [workdir]: { + lastSessionId: sessionId, + }, + }, + }; + + await execContainer(id, [ + "bash", + "-c", + `echo '${JSON.stringify(claudeJson)}' > /home/coder/.claude.json`, + ]); + + await execModuleScript(id); + + const startLog = await execContainer(id, [ + "bash", + "-c", + "cat /home/coder/.claude-module/agentapi-start.log", + ]); + expect(startLog.stdout).toContain("--continue"); + expect(startLog.stdout).toContain("Resuming existing session"); }); test("pre-post-install-scripts", async () => { diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf index 4f95dcb3..9c1816ad 100644 --- a/registry/coder/modules/claude-code/main.tf +++ b/registry/coder/modules/claude-code/main.tf @@ -348,6 +348,7 @@ module "agentapi" { ARG_PERMISSION_MODE='${var.permission_mode}' \ ARG_WORKDIR='${local.workdir}' \ ARG_AI_PROMPT='${base64encode(var.ai_prompt)}' \ + ARG_REPORT_TASKS='${var.report_tasks}' \ ARG_ENABLE_BOUNDARY='${var.enable_boundary}' \ ARG_BOUNDARY_VERSION='${var.boundary_version}' \ ARG_BOUNDARY_LOG_DIR='${var.boundary_log_dir}' \ diff --git a/registry/coder/modules/claude-code/main.tftest.hcl b/registry/coder/modules/claude-code/main.tftest.hcl index 6994caf2..adfca6d2 100644 --- a/registry/coder/modules/claude-code/main.tftest.hcl +++ b/registry/coder/modules/claude-code/main.tftest.hcl @@ -57,7 +57,7 @@ run "test_claude_code_with_custom_options" { group = "development" icon = "/icon/custom.svg" model = "opus" - task_prompt = "Help me write better code" + ai_prompt = "Help me write better code" permission_mode = "plan" continue = true install_claude_code = false @@ -88,8 +88,8 @@ run "test_claude_code_with_custom_options" { } assert { - condition = var.task_prompt == "Help me write better code" - error_message = "Task prompt variable should be set correctly" + condition = var.ai_prompt == "Help me write better code" + error_message = "AI prompt variable should be set correctly" } assert { diff --git a/registry/coder/modules/claude-code/scripts/remove-last-session-id.sh b/registry/coder/modules/claude-code/scripts/remove-last-session-id.sh index dac86a03..d72369fa 100755 --- a/registry/coder/modules/claude-code/scripts/remove-last-session-id.sh +++ b/registry/coder/modules/claude-code/scripts/remove-last-session-id.sh @@ -26,15 +26,19 @@ echo ".claude.json path $claude_json_path" # Check if .claude.json exists if [ ! -f "$claude_json_path" ]; then echo "No .claude.json file found" - exit 0 + exit 1 fi # Use jq to check if lastSessionId exists for the working directory and remove it if jq -e ".projects[\"$working_dir\"].lastSessionId" "$claude_json_path" > /dev/null 2>&1; then # Remove lastSessionId and update the file - jq "del(.projects[\"$working_dir\"].lastSessionId)" "$claude_json_path" > "${claude_json_path}.tmp" && mv "${claude_json_path}.tmp" "$claude_json_path" - echo "Removed lastSessionId from .claude.json" + if jq "del(.projects[\"$working_dir\"].lastSessionId)" "$claude_json_path" > "${claude_json_path}.tmp" && mv "${claude_json_path}.tmp" "$claude_json_path"; then + echo "Removed lastSessionId from .claude.json" + exit 0 + else + echo "Failed to remove lastSessionId from .claude.json" + fi else echo "No lastSessionId found in .claude.json - nothing to do" fi diff --git a/registry/coder/modules/claude-code/scripts/start.sh b/registry/coder/modules/claude-code/scripts/start.sh index f5527b4f..fb5bafcc 100644 --- a/registry/coder/modules/claude-code/scripts/start.sh +++ b/registry/coder/modules/claude-code/scripts/start.sh @@ -20,6 +20,7 @@ ARG_DANGEROUSLY_SKIP_PERMISSIONS=${ARG_DANGEROUSLY_SKIP_PERMISSIONS:-} ARG_PERMISSION_MODE=${ARG_PERMISSION_MODE:-} ARG_WORKDIR=${ARG_WORKDIR:-"$HOME"} ARG_AI_PROMPT=$(echo -n "${ARG_AI_PROMPT:-}" | base64 -d) +ARG_REPORT_TASKS=${ARG_REPORT_TASKS:-true} ARG_ENABLE_BOUNDARY=${ARG_ENABLE_BOUNDARY:-false} ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"main"} ARG_BOUNDARY_LOG_DIR=${ARG_BOUNDARY_LOG_DIR:-"/tmp/boundary_logs"} @@ -38,6 +39,7 @@ printf "ARG_DANGEROUSLY_SKIP_PERMISSIONS: %s\n" "$ARG_DANGEROUSLY_SKIP_PERMISSIO printf "ARG_PERMISSION_MODE: %s\n" "$ARG_PERMISSION_MODE" printf "ARG_AI_PROMPT: %s\n" "$ARG_AI_PROMPT" printf "ARG_WORKDIR: %s\n" "$ARG_WORKDIR" +printf "ARG_REPORT_TASKS: %s\n" "$ARG_REPORT_TASKS" printf "ARG_ENABLE_BOUNDARY: %s\n" "$ARG_ENABLE_BOUNDARY" printf "ARG_BOUNDARY_VERSION: %s\n" "$ARG_BOUNDARY_VERSION" printf "ARG_BOUNDARY_LOG_DIR: %s\n" "$ARG_BOUNDARY_LOG_DIR" @@ -47,10 +49,18 @@ printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST" echo "--------------------------------" -# see the remove-last-session-id.sh script for details -# about why we need it -# avoid exiting if the script fails -bash "/tmp/remove-last-session-id.sh" "$(pwd)" 2> /dev/null || true +# Clean up stale session data (see remove-last-session-id.sh for details) +CAN_CONTINUE_CONVERSATION=false +set +e +bash "/tmp/remove-last-session-id.sh" "$(pwd)" 2> /dev/null +session_cleanup_exit_code=$? +set -e + +case $session_cleanup_exit_code in + 0) + CAN_CONTINUE_CONVERSATION=true + ;; +esac function install_boundary() { # Install boundary from public github repo @@ -69,10 +79,15 @@ function validate_claude_installation() { fi } +# Hardcoded task session ID for Coder task reporting +# This ensures all task sessions use a consistent, predictable ID TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2" task_session_exists() { - if find "$HOME/.claude" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then + local workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-') + local project_dir="$HOME/.claude/projects/${workdir_normalized}" + + if [ -d "$project_dir" ] && find "$project_dir" -type f -name "*${TASK_SESSION_ID}*" 2> /dev/null | grep -q .; then return 0 else return 1 @@ -97,39 +112,63 @@ function start_agentapi() { fi if [ -n "$ARG_RESUME_SESSION_ID" ]; then - echo "Using explicit resume_session_id: $ARG_RESUME_SESSION_ID" + echo "Resuming task session by ID: $ARG_RESUME_SESSION_ID" ARGS+=(--resume "$ARG_RESUME_SESSION_ID") if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then ARGS+=(--dangerously-skip-permissions) fi elif [ "$ARG_CONTINUE" = "true" ]; then - if task_session_exists; then + if [ "$ARG_REPORT_TASKS" = "true" ] && task_session_exists; then echo "Task session detected (ID: $TASK_SESSION_ID)" ARGS+=(--resume "$TASK_SESSION_ID") + ARGS+=(--dangerously-skip-permissions) + echo "Resuming existing task session" + elif [ "$ARG_REPORT_TASKS" = "false" ] && [ "$CAN_CONTINUE_CONVERSATION" = true ]; then + echo "Previous session exists" + ARGS+=(--continue) if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then ARGS+=(--dangerously-skip-permissions) fi - echo "Resuming existing task session" + echo "Resuming existing session" else - echo "No existing task session found" - ARGS+=(--session-id "$TASK_SESSION_ID") + echo "No existing session found" + if [ "$ARG_REPORT_TASKS" = "true" ]; then + ARGS+=(--session-id "$TASK_SESSION_ID") + fi if [ -n "$ARG_AI_PROMPT" ]; then - ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT") - echo "Starting new task session with prompt" + if [ "$ARG_REPORT_TASKS" = "true" ]; then + ARGS+=(--dangerously-skip-permissions -- "$ARG_AI_PROMPT") + else + if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then + ARGS+=(--dangerously-skip-permissions) + fi + ARGS+=(-- "$ARG_AI_PROMPT") + fi + echo "Starting new session with prompt" else - if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then + if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then ARGS+=(--dangerously-skip-permissions) fi - echo "Starting new task session" + echo "Starting new session" fi fi else echo "Continue disabled, starting fresh session" + if [ "$ARG_REPORT_TASKS" = "true" ]; then + ARGS+=(--session-id "$TASK_SESSION_ID") + fi if [ -n "$ARG_AI_PROMPT" ]; then - ARGS+=(--dangerously-skip-permissions "$ARG_AI_PROMPT") + if [ "$ARG_REPORT_TASKS" = "true" ]; then + ARGS+=(--dangerously-skip-permissions -- "$ARG_AI_PROMPT") + else + if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then + ARGS+=(--dangerously-skip-permissions) + fi + ARGS+=(-- "$ARG_AI_PROMPT") + fi echo "Starting new session with prompt" else - if [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then + if [ "$ARG_REPORT_TASKS" = "true" ] || [ "$ARG_DANGEROUSLY_SKIP_PERMISSIONS" = "true" ]; then ARGS+=(--dangerously-skip-permissions) fi echo "Starting claude code session" From 8bf17899965ffadacfb70231f2d7d67301d050cd Mon Sep 17 00:00:00 2001 From: "blinkagent[bot]" <237617714+blinkagent[bot]@users.noreply.github.com> Date: Thu, 13 Nov 2025 07:43:16 -0600 Subject: [PATCH 33/36] feat: add additional_args variable to code-server module (#536) This PR adds an `extra_args` variable to the code-server module, allowing users to pass additional command-line arguments to code-server. ## Changes - Added `additional_args` variable to `main.tf` with a default empty string - Updated `run.sh` to include `${ADDITIONAL_ARGS}` in the code-server command - Added documentation and example usage in `README.md` ## Use Case This solves the issue where users want to disable the workspace trust prompt by passing `--disable-workspace-trust` to code-server. See the discussion in https://codercom.slack.com/archives/C09H8LRLG8K/p1762983278455979 ## Example Usage ```tf module "code-server" { source = "registry.coder.com/coder/code-server/coder" version = "1.3.1" agent_id = coder_agent.example.id additional_args = "--disable-workspace-trust" } ``` The `additional_args` variable can accept any additional command-line arguments that code-server supports. --------- Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com> Co-authored-by: DevelopmentCats --- registry/coder/modules/code-server/README.md | 28 +++++++++++++++----- registry/coder/modules/code-server/main.tf | 7 +++++ registry/coder/modules/code-server/run.sh | 2 +- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/registry/coder/modules/code-server/README.md b/registry/coder/modules/code-server/README.md index 13fb7b72..b9ed6b72 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.3.1" + version = "1.4.0" 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.3.1" + version = "1.4.0" agent_id = coder_agent.example.id install_version = "4.8.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.3.1" + version = "1.4.0" 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.3.1" + version = "1.4.0" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { @@ -78,12 +78,26 @@ 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.3.1" + version = "1.4.0" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] } ``` +### Pass Additional Arguments + +You can pass additional command-line arguments to code-server using the `additional_args` variable. For example, to disable workspace trust: + +```tf +module "code-server" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/code-server/coder" + version = "1.4.0" + agent_id = coder_agent.example.id + additional_args = "--disable-workspace-trust" +} +``` + ### Offline and Use Cached Modes By default the module looks for code-server at `/tmp/code-server` but this can be changed with `install_prefix`. @@ -94,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.3.1" + version = "1.4.0" agent_id = coder_agent.example.id use_cached = true extensions = ["dracula-theme.theme-dracula", "ms-azuretools.vscode-docker"] @@ -107,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.3.1" + version = "1.4.0" 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 650829f6..f5651353 100644 --- a/registry/coder/modules/code-server/main.tf +++ b/registry/coder/modules/code-server/main.tf @@ -148,6 +148,12 @@ variable "open_in" { } } +variable "additional_args" { + type = string + description = "Additional command-line arguments to pass to code-server (e.g., '--disable-workspace-trust')." + default = "" +} + resource "coder_script" "code-server" { agent_id = var.agent_id display_name = "code-server" @@ -168,6 +174,7 @@ resource "coder_script" "code-server" { EXTENSIONS_DIR : var.extensions_dir, FOLDER : var.folder, AUTO_INSTALL_EXTENSIONS : var.auto_install_extensions, + ADDITIONAL_ARGS : var.additional_args, }) run_on_start = true diff --git a/registry/coder/modules/code-server/run.sh b/registry/coder/modules/code-server/run.sh index 73bcd689..55918fa4 100644 --- a/registry/coder/modules/code-server/run.sh +++ b/registry/coder/modules/code-server/run.sh @@ -16,7 +16,7 @@ fi function run_code_server() { echo "👷 Running code-server in the background..." echo "Check logs at ${LOG_PATH}!" - $CODE_SERVER "$EXTENSION_ARG" --auth none --port "${PORT}" --app-name "${APP_NAME}" > "${LOG_PATH}" 2>&1 & + $CODE_SERVER "$EXTENSION_ARG" --auth none --port "${PORT}" --app-name "${APP_NAME}" ${ADDITIONAL_ARGS} > "${LOG_PATH}" 2>&1 & } # Check if the settings file exists... From f304201b6f2546cf37849f4ea2479d961b2cad6f Mon Sep 17 00:00:00 2001 From: djarbz <30350993+djarbz@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:47:01 -0600 Subject: [PATCH 34/36] Add IDE metadata output with tests and examples (#526) ## Description Exposes the metadata of the selected IDEs for use in the main template. Also adds tests to verify that the output metadata matches the "default" mappings. ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [x] Feature/enhancement - [ ] Documentation - [ ] Other ## Module Information **Path:** `registry/coder/modules/jetbrains` **New version:** `v1.2.0` **Breaking change:** [ ] Yes [x] No ## Testing & Validation - [N/A] Tests pass (`bun test`) - [x] Code formatted (`bun fmt`) - [x] Changes tested locally ## Related Issues None --------- Co-authored-by: DevCats --- registry/coder/modules/jetbrains/README.md | 34 ++++- .../modules/jetbrains/jetbrains.tftest.hcl | 134 ++++++++++++++++++ registry/coder/modules/jetbrains/main.tf | 11 +- 3 files changed, 171 insertions(+), 8 deletions(-) diff --git a/registry/coder/modules/jetbrains/README.md b/registry/coder/modules/jetbrains/README.md index 9d08e645..7b55232c 100644 --- a/registry/coder/modules/jetbrains/README.md +++ b/registry/coder/modules/jetbrains/README.md @@ -14,7 +14,7 @@ This module adds JetBrains IDE buttons to launch IDEs directly from the dashboar module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.1" + version = "1.2.0" agent_id = coder_agent.example.id folder = "/home/coder/project" # tooltip = "You need to [Install Coder Desktop](https://coder.com/docs/user-guides/desktop#install-coder-desktop) to use this button." # Optional @@ -40,7 +40,7 @@ When `default` contains IDE codes, those IDEs are created directly without user module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.1" + version = "1.2.0" agent_id = coder_agent.example.id folder = "/home/coder/project" default = ["PY", "IU"] # Pre-configure GoLand and IntelliJ IDEA @@ -53,7 +53,7 @@ module "jetbrains" { module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.1" + version = "1.2.0" agent_id = coder_agent.example.id folder = "/home/coder/project" # Show parameter with limited options @@ -67,7 +67,7 @@ module "jetbrains" { module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.1" + version = "1.2.0" agent_id = coder_agent.example.id folder = "/home/coder/project" default = ["IU", "PY"] @@ -82,7 +82,7 @@ module "jetbrains" { module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.1" + version = "1.2.0" agent_id = coder_agent.example.id folder = "/workspace/project" @@ -108,7 +108,7 @@ module "jetbrains" { module "jetbrains_pycharm" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.1" + version = "1.2.0" agent_id = coder_agent.example.id folder = "/workspace/project" @@ -128,7 +128,7 @@ Add helpful tooltip text that appears when users hover over the IDE app buttons: module "jetbrains" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/jetbrains/coder" - version = "1.1.1" + version = "1.2.0" agent_id = coder_agent.example.id folder = "/home/coder/project" default = ["IU", "PY"] @@ -136,6 +136,26 @@ module "jetbrains" { } ``` +### Accessing the IDE Metadata + +You can now reference the output `ide_metadata` as a map. + +```tf +# Add metadata to the container showing the installed IDEs and their build versions. +resource "coder_metadata" "container_info" { + count = data.coder_workspace.me.start_count + resource_id = one(docker_container.workspace).id + + dynamic "item" { + for_each = length(module.jetbrains) > 0 ? one(module.jetbrains).ide_metadata : {} + content { + key = item.value.build + value = "${item.value.name} [${item.key}]" + } + } +} +``` + ## Behavior ### Parameter vs Direct Apps diff --git a/registry/coder/modules/jetbrains/jetbrains.tftest.hcl b/registry/coder/modules/jetbrains/jetbrains.tftest.hcl index 7676c34f..21726c25 100644 --- a/registry/coder/modules/jetbrains/jetbrains.tftest.hcl +++ b/registry/coder/modules/jetbrains/jetbrains.tftest.hcl @@ -1,3 +1,53 @@ +variables { + # Default IDE config, mirrored from main.tf for test assertions. + # If main.tf defaults change, update this map to match. + expected_ide_config = { + "CL" = { name = "CLion", icon = "/icon/clion.svg", build = "251.26927.39" }, + "GO" = { name = "GoLand", icon = "/icon/goland.svg", build = "251.26927.50" }, + "IU" = { name = "IntelliJ IDEA", icon = "/icon/intellij.svg", build = "251.26927.53" }, + "PS" = { name = "PhpStorm", icon = "/icon/phpstorm.svg", build = "251.26927.60" }, + "PY" = { name = "PyCharm", icon = "/icon/pycharm.svg", build = "251.26927.74" }, + "RD" = { name = "Rider", icon = "/icon/rider.svg", build = "251.26927.67" }, + "RM" = { name = "RubyMine", icon = "/icon/rubymine.svg", build = "251.26927.47" }, + "RR" = { name = "RustRover", icon = "/icon/rustrover.svg", build = "251.26927.79" }, + "WS" = { name = "WebStorm", icon = "/icon/webstorm.svg", build = "251.26927.40" } + } +} + +run "validate_test_config_matches_defaults" { + command = plan + + variables { + # Provide minimal vars to allow plan to read module variables + agent_id = "foo" + folder = "/home/coder" + } + + assert { + condition = length(var.ide_config) == length(var.expected_ide_config) + error_message = "Test configuration mismatch: 'var.ide_config' in main.tf has ${length(var.ide_config)} items, but 'var.expected_ide_config' in the test file has ${length(var.expected_ide_config)} items. Please update the test file's global variables block." + } + + assert { + # Check that all keys in the test local are present in the module's default + condition = alltrue([ + for key in keys(var.expected_ide_config) : + can(var.ide_config[key]) + ]) + error_message = "Test configuration mismatch: Keys in 'var.expected_ide_config' are out of sync with 'var.ide_config' defaults. Please update the test file's global variables block." + } + + assert { + # Check if all build numbers in the test local match the module's defaults + # This relies on the previous two assertions passing (same length, same keys) + condition = alltrue([ + for key, config in var.expected_ide_config : + var.ide_config[key].build == config.build + ]) + error_message = "Test configuration mismatch: One or more build numbers in 'var.expected_ide_config' do not match the defaults in 'var.ide_config'. Please update the test file's global variables block." + } +} + run "requires_agent_and_folder" { command = plan @@ -160,3 +210,87 @@ run "tooltip_null_when_not_provided" { error_message = "Expected coder_app tooltip to be null when not provided" } } + +run "output_empty_when_default_empty" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + # var.default is empty + } + + assert { + condition = length(output.ide_metadata) == 0 + error_message = "Expected ide_metadata output to be empty when var.default is not set" + } +} + +run "output_single_ide_uses_fallback_build" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["GO"] + # Force HTTP data source to fail to test fallback logic + releases_base_link = "https://coder.com" + } + + assert { + condition = length(output.ide_metadata) == 1 + error_message = "Expected ide_metadata output to have 1 item" + } + + assert { + condition = can(output.ide_metadata["GO"]) + error_message = "Expected ide_metadata output to have key 'GO'" + } + + assert { + condition = output.ide_metadata["GO"].name == var.expected_ide_config["GO"].name + error_message = "Expected ide_metadata['GO'].name to be '${var.expected_ide_config["GO"].name}'" + } + + assert { + condition = output.ide_metadata["GO"].build == var.expected_ide_config["GO"].build + error_message = "Expected ide_metadata['GO'].build to use the fallback '${var.expected_ide_config["GO"].build}'" + } + + assert { + condition = output.ide_metadata["GO"].icon == var.expected_ide_config["GO"].icon + error_message = "Expected ide_metadata['GO'].icon to be '${var.expected_ide_config["GO"].icon}'" + } +} + +run "output_multiple_ides" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder" + default = ["IU", "PY"] + # Force HTTP data source to fail to test fallback logic + releases_base_link = "https://coder.com" + } + + assert { + condition = length(output.ide_metadata) == 2 + error_message = "Expected ide_metadata output to have 2 items" + } + + assert { + condition = can(output.ide_metadata["IU"]) && can(output.ide_metadata["PY"]) + error_message = "Expected ide_metadata output to have keys 'IU' and 'PY'" + } + + assert { + condition = output.ide_metadata["PY"].name == var.expected_ide_config["PY"].name + error_message = "Expected ide_metadata['PY'].name to be '${var.expected_ide_config["PY"].name}'" + } + + assert { + condition = output.ide_metadata["PY"].build == var.expected_ide_config["PY"].build + error_message = "Expected ide_metadata['PY'].build to be the fallback '${var.expected_ide_config["PY"].build}'" + } +} diff --git a/registry/coder/modules/jetbrains/main.tf b/registry/coder/modules/jetbrains/main.tf index 8f0e0ac7..51f7c816 100644 --- a/registry/coder/modules/jetbrains/main.tf +++ b/registry/coder/modules/jetbrains/main.tf @@ -257,4 +257,13 @@ resource "coder_app" "jetbrains" { local.options_metadata[each.key].build, var.agent_name != null ? "&agent_name=${var.agent_name}" : "", ]) -} \ No newline at end of file +} + +output "ide_metadata" { + description = "A map of the metadata for each selected JetBrains IDE." + value = { + # We iterate directly over the selected_ides map. + # 'key' will be the IDE key (e.g., "IC", "PY") + for key, val in local.selected_ides : key => local.options_metadata[key] + } +} From 0e3263fd6f5e3a8ea3fef0be1aa4b074ba1c8213 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Sat, 15 Nov 2025 18:57:42 +0100 Subject: [PATCH 35/36] fix: change cmux npm package to mux (#533) --- .icons/{cmux.svg => mux.svg} | 0 registry/coder/modules/cmux/run.sh | 135 ------------ .../coder/modules/{cmux => mux}/README.md | 56 ++--- .../coder/modules/{cmux => mux}/main.test.ts | 16 +- registry/coder/modules/{cmux => mux}/main.tf | 45 ++-- .../cmux.tftest.hcl => mux/mux.tftest.hcl} | 6 +- registry/coder/modules/mux/run.sh | 195 ++++++++++++++++++ 7 files changed, 262 insertions(+), 191 deletions(-) rename .icons/{cmux.svg => mux.svg} (100%) delete mode 100644 registry/coder/modules/cmux/run.sh rename registry/coder/modules/{cmux => mux}/README.md (50%) rename registry/coder/modules/{cmux => mux}/main.test.ts (79%) rename registry/coder/modules/{cmux => mux}/main.tf (76%) rename registry/coder/modules/{cmux/cmux.tftest.hcl => mux/mux.tftest.hcl} (89%) create mode 100644 registry/coder/modules/mux/run.sh diff --git a/.icons/cmux.svg b/.icons/mux.svg similarity index 100% rename from .icons/cmux.svg rename to .icons/mux.svg diff --git a/registry/coder/modules/cmux/run.sh b/registry/coder/modules/cmux/run.sh deleted file mode 100644 index 23656f8f..00000000 --- a/registry/coder/modules/cmux/run.sh +++ /dev/null @@ -1,135 +0,0 @@ -#!/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="" - # 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 - # 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" - 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 diff --git a/registry/coder/modules/cmux/README.md b/registry/coder/modules/mux/README.md similarity index 50% rename from registry/coder/modules/cmux/README.md rename to registry/coder/modules/mux/README.md index c1ced283..9bd85e20 100644 --- a/registry/coder/modules/cmux/README.md +++ b/registry/coder/modules/mux/README.md @@ -1,20 +1,20 @@ --- -display_name: cmux +display_name: mux description: Coding Agent Multiplexer - Run multiple AI agents in parallel -icon: ../../../../.icons/cmux.svg +icon: ../../../../.icons/mux.svg verified: false tags: [ai, agents, development, multiplexer] --- -# cmux +# mux -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. +Automatically install and run 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. ```tf -module "cmux" { +module "mux" { count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/cmux/coder" - version = "1.0.2" + source = "registry.coder.com/coder/mux/coder" + version = "1.0.0" agent_id = coder_agent.example.id } ``` @@ -22,8 +22,8 @@ module "cmux" { ## Features - **Parallel Agent Execution**: Run multiple AI agents simultaneously on different tasks -- **Cmux Workspace Isolation**: Each agent works in its own isolated environment -- **Git Divergence Visualization**: Track changes across different cmux agent workspaces +- **Mux Workspace Isolation**: Each agent works in its own isolated environment +- **Git Divergence Visualization**: Track changes across different mux agent workspaces - **Long-Running Processes**: Resume AI work after interruptions - **Cost Tracking**: Monitor API usage across agents @@ -32,10 +32,10 @@ module "cmux" { ### Basic Usage ```tf -module "cmux" { +module "mux" { count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/cmux/coder" - version = "1.0.2" + source = "registry.coder.com/coder/mux/coder" + version = "1.0.0" agent_id = coder_agent.example.id } ``` @@ -43,10 +43,10 @@ module "cmux" { ### Pin Version ```tf -module "cmux" { +module "mux" { count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/cmux/coder" - version = "1.0.2" + source = "registry.coder.com/coder/mux/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" @@ -56,10 +56,10 @@ module "cmux" { ### Custom Port ```tf -module "cmux" { +module "mux" { count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/cmux/coder" - version = "1.0.2" + source = "registry.coder.com/coder/mux/coder" + version = "1.0.0" agent_id = coder_agent.example.id port = 8080 } @@ -67,13 +67,13 @@ module "cmux" { ### Use Cached Installation -Run an existing copy of cmux if found, otherwise install from npm: +Run an existing copy of mux if found, otherwise install from npm: ```tf -module "cmux" { +module "mux" { count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/cmux/coder" - version = "1.0.2" + source = "registry.coder.com/coder/mux/coder" + version = "1.0.0" agent_id = coder_agent.example.id use_cached = true } @@ -81,13 +81,13 @@ module "cmux" { ### Skip Install -Run without installing from the network (requires cmux to be pre-installed): +Run without installing from the network (requires mux to be pre-installed): ```tf -module "cmux" { +module "mux" { count = data.coder_workspace.me.start_count - source = "registry.coder.com/coder/cmux/coder" - version = "1.0.2" + source = "registry.coder.com/coder/mux/coder" + version = "1.0.0" agent_id = coder_agent.example.id install = false } @@ -99,6 +99,6 @@ module "cmux" { ## Notes -- cmux 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) -- Installs `@coder/cmux` from npm by default (falls back to the npm tarball if npm is unavailable) +- Installs `mux@next` from npm by default (falls back to the npm tarball if npm is unavailable) diff --git a/registry/coder/modules/cmux/main.test.ts b/registry/coder/modules/mux/main.test.ts similarity index 79% rename from registry/coder/modules/cmux/main.test.ts rename to registry/coder/modules/mux/main.test.ts index 5ff42c3f..efc00460 100644 --- a/registry/coder/modules/cmux/main.test.ts +++ b/registry/coder/modules/mux/main.test.ts @@ -6,7 +6,7 @@ import { testRequiredVariables, } from "~test"; -describe("cmux", async () => { +describe("mux", async () => { await runTerraformInit(import.meta.dir); testRequiredVariables(import.meta.dir, { @@ -31,9 +31,9 @@ describe("cmux", async () => { 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!", + "🥳 mux has been installed in /tmp/mux", + "🚀 Starting mux server on port 4000...", + "Check logs at /tmp/mux.log!", ]; for (const line of expectedLines) { expect(output.stdout).toContain(line); @@ -54,10 +54,10 @@ describe("cmux", async () => { 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!", + "📦 Installing mux via npm into /tmp/mux...", + "🥳 mux has been installed in /tmp/mux", + "🚀 Starting mux server on port 4000...", + "Check logs at /tmp/mux.log!", ]; for (const line of expectedLines) { expect(output.stdout).toContain(line); diff --git a/registry/coder/modules/cmux/main.tf b/registry/coder/modules/mux/main.tf similarity index 76% rename from registry/coder/modules/cmux/main.tf rename to registry/coder/modules/mux/main.tf index 37ec5202..08c70aab 100644 --- a/registry/coder/modules/cmux/main.tf +++ b/registry/coder/modules/mux/main.tf @@ -17,38 +17,44 @@ variable "agent_id" { variable "port" { type = number - description = "The port to run cmux on." + description = "The port to run mux on." default = 4000 } variable "display_name" { type = string - description = "The display name for the cmux application." - default = "cmux" + description = "The display name for the mux application." + default = "mux" } variable "slug" { type = string - description = "The slug for the cmux application." - default = "cmux" + description = "The slug for the mux application." + default = "mux" } variable "install_prefix" { type = string - description = "The prefix to install cmux to." - default = "/tmp/cmux" + description = "The prefix to install mux to." + default = "/tmp/mux" } variable "log_path" { type = string - description = "The path for cmux logs." - default = "/tmp/cmux.log" + description = "The path for mux logs." + default = "/tmp/mux.log" +} + +variable "add-project" { + type = string + description = "Path to add/open as a project in mux (idempotent)." + default = "" } variable "install_version" { type = string - description = "The version of cmux to install." - default = "latest" + description = "The version or dist-tag of mux to install." + default = "next" } variable "share" { @@ -74,13 +80,13 @@ variable "group" { variable "install" { type = bool - description = "Install cmux from the network (npm or tarball). If false, run without installing (requires a pre-installed cmux)." + description = "Install mux from the network (npm or tarball). If false, run without installing (requires a pre-installed mux)." default = true } variable "use_cached" { type = bool - description = "Use cached copy of cmux if present; otherwise install from npm" + description = "Use cached copy of mux if present; otherwise install from npm" default = false } @@ -107,14 +113,15 @@ variable "open_in" { } } -resource "coder_script" "cmux" { +resource "coder_script" "mux" { agent_id = var.agent_id - display_name = "cmux" - icon = "/icon/cmux.svg" + display_name = "mux" + icon = "/icon/mux.svg" script = templatefile("${path.module}/run.sh", { VERSION : var.install_version, PORT : var.port, LOG_PATH : var.log_path, + ADD_PROJECT : var.add-project, INSTALL_PREFIX : var.install_prefix, OFFLINE : !var.install, USE_CACHED : var.use_cached, @@ -129,12 +136,12 @@ resource "coder_script" "cmux" { } } -resource "coder_app" "cmux" { +resource "coder_app" "mux" { agent_id = var.agent_id slug = var.slug display_name = var.display_name url = "http://localhost:${var.port}" - icon = "/icon/cmux.svg" + icon = "/icon/mux.svg" subdomain = var.subdomain share = var.share order = var.order @@ -147,3 +154,5 @@ resource "coder_app" "cmux" { threshold = 6 } } + + diff --git a/registry/coder/modules/cmux/cmux.tftest.hcl b/registry/coder/modules/mux/mux.tftest.hcl similarity index 89% rename from registry/coder/modules/cmux/cmux.tftest.hcl rename to registry/coder/modules/mux/mux.tftest.hcl index 3b831b37..c403d377 100644 --- a/registry/coder/modules/cmux/cmux.tftest.hcl +++ b/registry/coder/modules/mux/mux.tftest.hcl @@ -16,7 +16,7 @@ run "install_false_and_use_cached_conflict" { } expect_failures = [ - resource.coder_script.cmux + resource.coder_script.mux ] } @@ -29,7 +29,7 @@ run "custom_port" { } assert { - condition = resource.coder_app.cmux.url == "http://localhost:8080" + condition = resource.coder_app.mux.url == "http://localhost:8080" error_message = "coder_app URL must use the configured port" } } @@ -62,3 +62,5 @@ run "use_cached_only_success" { use_cached = true } } + + diff --git a/registry/coder/modules/mux/run.sh b/registry/coder/modules/mux/run.sh new file mode 100644 index 00000000..c202a9ee --- /dev/null +++ b/registry/coder/modules/mux/run.sh @@ -0,0 +1,195 @@ +#!/usr/bin/env bash + +BOLD='\033[0;1m' +RESET='\033[0m' +MUX_BINARY="${INSTALL_PREFIX}/mux" + +function run_mux() { + local port_value + port_value="${PORT}" + if [ -z "$port_value" ]; then + port_value="4000" + fi + # Build args for mux (POSIX-compatible, avoid bash arrays) + set -- server --port "$port_value" + if [ -n "${ADD_PROJECT}" ]; then + set -- "$@" --add-project "${ADD_PROJECT}" + fi + echo "🚀 Starting mux server on port $port_value..." + echo "Check logs at ${LOG_PATH}!" + PORT="$port_value" "$MUX_BINARY" "$@" > "${LOG_PATH}" 2>&1 & +} + +# Check if mux is already installed for offline mode +if [ "${OFFLINE}" = true ]; then + if [ -f "$MUX_BINARY" ]; then + echo "🥳 Found a copy of mux" + run_mux + exit 0 + fi + echo "❌ Failed to find a copy of mux" + exit 1 +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" + + # Clean up from other install (in case install prefix changed). + if [ -n "$CODER_SCRIPT_BIN_DIR" ] && [ -e "$CODER_SCRIPT_BIN_DIR/mux" ]; then + rm "$CODER_SCRIPT_BIN_DIR/mux" + fi + + mkdir -p "$(dirname "$MUX_BINARY")" + + if command -v npm > /dev/null 2>&1; then + echo "📦 Installing mux 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="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 "$PKG_SPEC"; then + echo "❌ Failed to install mux via npm" + 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" + exit 1 + fi + chmod +x "$CANDIDATE" || true + ln -sf "$CANDIDATE" "$MUX_BINARY" + else + echo "📥 npm not found; downloading tarball from npm 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_JSON="$(curl -fsSL "$META_URL" || true)" + if [ -z "$META_JSON" ]; then + echo "❌ Failed to fetch npm metadata: $META_URL" + exit 1 + fi + # Normalize JSON to a single line for robust pattern matching across environments + META_ONE_LINE="$(printf "%s" "$META_JSON" | tr -d '\n' || true)" + if [ -z "$META_ONE_LINE" ]; then + META_ONE_LINE="$META_JSON" + fi + # Try to extract tarball URL directly from metadata (prefer Node if available for robust JSON parsing) + TARBALL_URL="" + if command -v node > /dev/null 2>&1; then + TARBALL_URL="$(printf "%s" "$META_JSON" | node -e 'try{const fs=require("fs");const data=JSON.parse(fs.readFileSync(0,"utf8"));if(data&&data.dist&&data.dist.tarball){console.log(data.dist.tarball);}}catch(e){}')" + fi + # sed-based fallback + if [ -z "$TARBALL_URL" ]; then + TARBALL_URL="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*\"tarball\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)" + fi + # Fallback: resolve version then construct tarball URL + if [ -z "$TARBALL_URL" ]; then + RESOLVED_VERSION="" + if command -v node > /dev/null 2>&1; then + RESOLVED_VERSION="$(printf "%s" "$META_JSON" | node -e 'try{const fs=require("fs");const data=JSON.parse(fs.readFileSync(0,"utf8"));if(data&&data.version){console.log(data.version);}}catch(e){}')" + fi + if [ -z "$RESOLVED_VERSION" ]; then + RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | sed -n 's/.*\"version\":\"\\([^\"]*\\)\".*/\\1/p' | head -n1)" + fi + if [ -z "$RESOLVED_VERSION" ]; then + RESOLVED_VERSION="$(printf "%s" "$META_ONE_LINE" | grep -o '\"version\":\"[^\"]*\"' | head -n1 | cut -d '\"' -f4)" + fi + if [ -n "$RESOLVED_VERSION" ]; then + VERSION_TO_USE="$RESOLVED_VERSION" + fi + if [ -z "$VERSION_TO_USE" ]; then + echo "❌ Could not determine version for mux" + exit 1 + fi + TARBALL_URL="https://registry.npmjs.org/mux/-/mux-$VERSION_TO_USE.tgz" + fi + TMP_DIR="$(mktemp -d)" + TAR_PATH="$TMP_DIR/mux.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="" + BIN_PATH="" + # Prefer reading bin path from package.json + if [ -f "$TMP_DIR/package/package.json" ]; then + if command -v node > /dev/null 2>&1; then + BIN_PATH="$(node -e 'try{const fs=require("fs");const p=JSON.parse(fs.readFileSync(process.argv[1],"utf8"));let bp=typeof p.bin==="string"?p.bin:(p.bin&&p.bin.mux);if(bp){console.log(bp)}}catch(e){}' "$TMP_DIR/package/package.json")" + fi + if [ -z "$BIN_PATH" ]; then + # sed fallbacks (handle both string and object forms) + 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/.*\"mux\"[[:space:]]*:[[:space:]]*\"\\([^\"]*\\)\".*/\\1/p' | head -n1) + fi + fi + if [ -n "$BIN_PATH" ] && [ -f "$TMP_DIR/package/$BIN_PATH" ]; then + CANDIDATE="$TMP_DIR/package/$BIN_PATH" + fi + fi + # Fallback: check common locations + if [ -z "$CANDIDATE" ]; then + if [ -f "$TMP_DIR/package/bin/mux" ]; then + CANDIDATE="$TMP_DIR/package/bin/mux" + elif [ -f "$TMP_DIR/package/bin/mux.js" ]; then + CANDIDATE="$TMP_DIR/package/bin/mux.js" + elif [ -f "$TMP_DIR/package/bin/mux.mjs" ]; then + CANDIDATE="$TMP_DIR/package/bin/mux.mjs" + fi + fi + # Fallback: search for plausible filenames + if [ -z "$CANDIDATE" ] || [ ! -f "$CANDIDATE" ]; then + CANDIDATE=$(find "$TMP_DIR/package" -maxdepth 4 -type f \( -name "mux" -o -name "mux.js" -o -name "mux.mjs" -o -name "mux.cjs" -o -name "main.js" \) | head -n1) + fi + if [ -z "$CANDIDATE" ] || [ ! -f "$CANDIDATE" ]; then + echo "❌ Could not locate mux binary in tarball" + rm -rf "$TMP_DIR" + exit 1 + fi + # Copy entire package to installation directory to preserve relative imports + DEST_DIR="${INSTALL_PREFIX}/.mux-package" + rm -rf "$DEST_DIR" + mkdir -p "$DEST_DIR" + cp -R "$TMP_DIR/package/." "$DEST_DIR/" + # Create/refresh launcher symlink + if [ -n "$BIN_PATH" ] && [ -f "$DEST_DIR/$BIN_PATH" ]; then + ln -sf "$DEST_DIR/$BIN_PATH" "$MUX_BINARY" + chmod +x "$DEST_DIR/$BIN_PATH" || true + else + ln -sf "$DEST_DIR/$(basename "$CANDIDATE")" "$MUX_BINARY" + chmod +x "$DEST_DIR/$(basename "$CANDIDATE")" || true + fi + rm -rf "$TMP_DIR" + fi + + printf "🥳 mux has been installed in ${INSTALL_PREFIX}\n\n" +fi + +# Make mux available in PATH if CODER_SCRIPT_BIN_DIR is set +if [ -n "$CODER_SCRIPT_BIN_DIR" ]; then + if [ ! -e "$CODER_SCRIPT_BIN_DIR/mux" ]; then + ln -s "$MUX_BINARY" "$CODER_SCRIPT_BIN_DIR/mux" + fi +fi + +# Start mux +run_mux From c12fca57ad097e569e21502c49def25c92115737 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Sun, 16 Nov 2025 00:39:11 +0100 Subject: [PATCH 36/36] fix: add back cmux icon (#542) ## Description ## Type of Change - [ ] New module - [ ] New template - [ ] Bug fix - [ ] Feature/enhancement - [ ] Documentation - [ ] Other --- .icons/cmux.svg | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .icons/cmux.svg diff --git a/.icons/cmux.svg b/.icons/cmux.svg new file mode 100644 index 00000000..95b56bb0 --- /dev/null +++ b/.icons/cmux.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +