diff --git a/.icons/cloudcli.svg b/.icons/cloudcli.svg
new file mode 100644
index 000000000..35b24715a
--- /dev/null
+++ b/.icons/cloudcli.svg
@@ -0,0 +1,18 @@
+
+
diff --git a/registry/edd88-pixel/.images/avatar.png b/registry/edd88-pixel/.images/avatar.png
new file mode 100644
index 000000000..402805c1d
Binary files /dev/null and b/registry/edd88-pixel/.images/avatar.png differ
diff --git a/registry/edd88-pixel/README.md b/registry/edd88-pixel/README.md
new file mode 100644
index 000000000..6327c06f0
--- /dev/null
+++ b/registry/edd88-pixel/README.md
@@ -0,0 +1,11 @@
+---
+display_name: "Eddy Marc"
+bio: "Open source contributor focused on Terraform, DevOps, and cloud platform engineering"
+avatar: "./.images/avatar.png"
+github: "Edd88-pixel"
+status: "community"
+---
+
+# Eddy Marc
+
+Open source contributor focused on Terraform, DevOps, and cloud platform engineering.
diff --git a/registry/edd88-pixel/modules/cloudcli/README.md b/registry/edd88-pixel/modules/cloudcli/README.md
new file mode 100644
index 000000000..2d7643aad
--- /dev/null
+++ b/registry/edd88-pixel/modules/cloudcli/README.md
@@ -0,0 +1,95 @@
+---
+display_name: CloudCLI
+description: Run the CloudCLI web interface securely inside a Coder workspace
+icon: ../../../../.icons/cloudcli.svg
+verified: false
+tags: [agent, ai, cloudcli, web]
+---
+
+# CloudCLI
+
+Install and run the open source [CloudCLI](https://cloudcli.ai/) web interface for AI coding agents already available in a Coder workspace.
+
+```tf
+module "cloudcli" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/edd88-pixel/cloudcli/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+}
+```
+
+## Prerequisites
+
+The workspace image must provide Node.js 22 or newer and npm. Install and authenticate at least one CloudCLI-supported coding agent, such as Claude Code, Cursor CLI, Codex, Gemini CLI, or OpenCode, before using this module. The module reuses those existing agent installations and credentials; it does not install agents or modify their authentication, permissions, settings, or MCP configuration.
+
+For example, install and authenticate Claude Code alongside CloudCLI:
+
+```tf
+module "claude-code" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/coder/claude-code/coder"
+ version = "5.2.0"
+ agent_id = coder_agent.main.id
+
+ anthropic_api_key = var.anthropic_api_key
+}
+
+module "cloudcli" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/edd88-pixel/cloudcli/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+}
+```
+
+See the [Claude Code module](https://registry.coder.com/modules/coder/claude-code) for OAuth and Coder AI Gateway authentication alternatives.
+
+## Security
+
+CloudCLI has access to files and authenticated coding agents inside the workspace. This module therefore:
+
+- binds CloudCLI explicitly to `127.0.0.1`;
+- exposes it only through an owner-only Coder app;
+- installs the pinned npm package under `$HOME/.coder-modules/edd88-pixel/cloudcli/runtime`;
+- stores module-managed logs, runtime data, and process state under `$HOME/.coder-modules/edd88-pixel/cloudcli`.
+
+No public listener, TLS proxy, tunnel, process manager, or nested container runtime is created.
+
+> [!IMPORTANT]
+> CloudCLI currently requires Coder's [wildcard access URL](https://coder.com/docs/admin/networking/wildcard-access-url). Its frontend uses root-relative API and WebSocket routes that are not compatible with Coder's path-based app proxy.
+
+> [!NOTE]
+> CloudCLI's open source single-user mode currently requires a one-time account setup. The Coder app remains restricted to the workspace owner, but this module does not bypass CloudCLI's authentication.
+
+## Limit project discovery
+
+CloudCLI discovers projects under the workspace user's home directory by default. For a narrower and safer scope, set `workspaces_root` to an absolute path:
+
+```tf
+module "cloudcli" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/edd88-pixel/cloudcli/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ workspaces_root = "/home/coder/project"
+}
+```
+
+The path is validated before it is rendered into the startup script. Relative paths, whitespace, shell metacharacters, and parent-directory components are rejected.
+
+## Troubleshooting
+
+```bash
+cat ~/.coder-modules/edd88-pixel/cloudcli/logs/install.log
+cat ~/.coder-modules/edd88-pixel/cloudcli/logs/start.log
+cat ~/.coder-modules/edd88-pixel/cloudcli/logs/cloudcli.log
+```
+
+If startup reports that the configured port is already in use, choose a different `port` or stop the conflicting process. The module never terminates an unrelated listener.
+
+## References
+
+- [CloudCLI prerequisites](https://cloudcli.ai/docs/open-source-self-hosting/prerequisites)
+- [CloudCLI environment variables](https://cloudcli.ai/docs/configuration/environment-variables)
+- [CloudCLI source](https://github.com/siteboon/claudecodeui)
diff --git a/registry/edd88-pixel/modules/cloudcli/main.test.ts b/registry/edd88-pixel/modules/cloudcli/main.test.ts
new file mode 100644
index 000000000..f85a6febe
--- /dev/null
+++ b/registry/edd88-pixel/modules/cloudcli/main.test.ts
@@ -0,0 +1,490 @@
+import {
+ afterEach,
+ beforeAll,
+ describe,
+ expect,
+ setDefaultTimeout,
+ test,
+} from "bun:test";
+import {
+ execContainer,
+ readFileContainer,
+ removeContainer,
+ runContainer,
+ runTerraformApply,
+ runTerraformInit,
+ TerraformState,
+ testRequiredVariables,
+ writeCoder,
+ writeFileContainer,
+} from "~test";
+import path from "node:path";
+
+const MODULE_ROOT = "/home/coder/.coder-modules/edd88-pixel/cloudcli";
+const DEFAULT_PATH =
+ "/usr/local/test-bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
+
+interface ModuleScripts {
+ install: string;
+ start: string;
+}
+
+interface SetupResult {
+ id: string;
+ scripts: ModuleScripts;
+}
+
+let defaultScripts: ModuleScripts;
+let customScripts: ModuleScripts;
+let cleanupFunctions: Array<() => Promise> = [];
+
+const collectScripts = (state: TerraformState): ModuleScripts => {
+ const scripts: Partial = {};
+
+ for (const resource of state.resources) {
+ if (resource.type !== "coder_script") {
+ continue;
+ }
+
+ for (const instance of resource.instances) {
+ const attributes = instance.attributes as Record;
+ const displayName = attributes.display_name;
+ const script = attributes.script;
+ if (typeof displayName !== "string" || typeof script !== "string") {
+ continue;
+ }
+
+ if (displayName === "CloudCLI: Install Script") {
+ scripts.install = script;
+ } else if (displayName === "CloudCLI: Start Script") {
+ scripts.start = script;
+ }
+ }
+ }
+
+ if (!scripts.install || !scripts.start) {
+ throw new Error("CloudCLI install and start scripts were not found");
+ }
+
+ return scripts as ModuleScripts;
+};
+
+const npmMock = `#!/usr/bin/env bash
+set -euo pipefail
+
+if [ "\${1:-}" = "--version" ]; then
+ printf '10.0.0\\n'
+ exit 0
+fi
+
+if [ "\${1:-}" != "install" ]; then
+ printf 'unexpected npm command: %s\\n' "$*" >&2
+ exit 2
+fi
+
+printf '%s\\n' "$*" > /tmp/npm-invocation
+prefix=""
+package_spec=""
+shift
+
+while [ "$#" -gt 0 ]; do
+ case "$1" in
+ --prefix)
+ prefix="$2"
+ shift 2
+ ;;
+ @cloudcli-ai/cloudcli@*)
+ package_spec="$1"
+ shift
+ ;;
+ *)
+ shift
+ ;;
+ esac
+done
+
+if [ -z "$prefix" ] || [ -z "$package_spec" ]; then
+ printf 'missing npm prefix or CloudCLI package spec\\n' >&2
+ exit 2
+fi
+
+version="\${package_spec##*@}"
+package_dir="$prefix/node_modules/@cloudcli-ai/cloudcli"
+mkdir -p "$package_dir" "$prefix/node_modules/.bin"
+printf '{"name":"@cloudcli-ai/cloudcli","version":"%s"}\\n' "$version" > "$package_dir/package.json"
+cp /tmp/cloudcli-mock.sh "$prefix/node_modules/.bin/cloudcli"
+chmod 755 "$prefix/node_modules/.bin/cloudcli"
+`;
+
+const runScript = async (
+ id: string,
+ name: string,
+ script: string,
+ pathValue = DEFAULT_PATH,
+) => {
+ const scriptPath = `/tmp/${name}.sh`;
+ await writeFileContainer(id, scriptPath, script, { user: "root" });
+ const chmod = await execContainer(
+ id,
+ ["chmod", "755", scriptPath],
+ ["--user", "root"],
+ );
+ expect(chmod.exitCode).toBe(0);
+
+ return execContainer(
+ id,
+ ["bash", scriptPath],
+ [
+ "--user",
+ "coder",
+ "--env",
+ "HOME=/home/coder",
+ "--env",
+ `PATH=${pathValue}`,
+ ],
+ );
+};
+
+const setup = async (scripts = defaultScripts): Promise => {
+ const id = await runContainer("node:22-bookworm-slim");
+ cleanupFunctions.push(async () => {
+ await removeContainer(id);
+ });
+
+ const createUser = await execContainer(
+ id,
+ [
+ "bash",
+ "-c",
+ "id coder >/dev/null 2>&1 || useradd --create-home --shell /bin/bash coder",
+ ],
+ ["--user", "root"],
+ );
+ expect(createUser.exitCode).toBe(0);
+
+ await writeCoder(id, "#!/usr/bin/env bash\nexit 0\n");
+ await writeFileContainer(
+ id,
+ "/tmp/cloudcli-mock.sh",
+ await Bun.file(
+ path.join(import.meta.dir, "testdata", "cloudcli-mock.sh"),
+ ).text(),
+ { user: "root" },
+ );
+ await writeFileContainer(id, "/tmp/npm-mock.sh", npmMock, {
+ user: "root",
+ });
+
+ const installMocks = await execContainer(
+ id,
+ [
+ "bash",
+ "-c",
+ "mkdir -p /usr/local/test-bin && cp /tmp/npm-mock.sh /usr/local/test-bin/npm && chmod 755 /usr/local/test-bin/npm /tmp/cloudcli-mock.sh",
+ ],
+ ["--user", "root"],
+ );
+ expect(installMocks.exitCode).toBe(0);
+
+ return { id, scripts };
+};
+
+const installCloudCLI = async (result: SetupResult) => {
+ const response = await runScript(
+ result.id,
+ "cloudcli-install",
+ result.scripts.install,
+ );
+ if (response.exitCode !== 0) {
+ console.error(response.stdout);
+ console.error(response.stderr);
+ }
+ expect(response.exitCode).toBe(0);
+};
+
+const startCloudCLI = async (result: SetupResult) => {
+ const response = await runScript(
+ result.id,
+ "cloudcli-start",
+ result.scripts.start,
+ );
+ if (response.exitCode !== 0) {
+ console.error(response.stdout);
+ console.error(response.stderr);
+ }
+ expect(response.exitCode).toBe(0);
+ return response;
+};
+
+afterEach(async () => {
+ const cleanups = cleanupFunctions.reverse();
+ cleanupFunctions = [];
+
+ for (const cleanup of cleanups) {
+ try {
+ await cleanup();
+ } catch (error) {
+ console.error("Container cleanup failed:", error);
+ }
+ }
+});
+
+setDefaultTimeout(120_000);
+
+describe("cloudcli", async () => {
+ beforeAll(async () => {
+ await runTerraformInit(import.meta.dir);
+ defaultScripts = collectScripts(
+ await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ }),
+ );
+ customScripts = collectScripts(
+ await runTerraformApply(import.meta.dir, {
+ agent_id: "test-agent",
+ port: 43123,
+ workspaces_root: "/home/coder/project",
+ }),
+ );
+ });
+
+ testRequiredVariables(import.meta.dir, {
+ agent_id: "test-agent",
+ });
+
+ test("installs the exact package version in the isolated runtime", async () => {
+ const result = await setup();
+ await installCloudCLI(result);
+
+ const packageJSON = await readFileContainer(
+ result.id,
+ `${MODULE_ROOT}/runtime/node_modules/@cloudcli-ai/cloudcli/package.json`,
+ );
+ expect(JSON.parse(packageJSON).version).toBe("1.35.0");
+
+ const invocation = await readFileContainer(
+ result.id,
+ "/tmp/npm-invocation",
+ );
+ expect(invocation).toContain(`--prefix ${MODULE_ROOT}/runtime`);
+ expect(invocation).toContain("@cloudcli-ai/cloudcli@1.35.0");
+ expect(invocation).not.toMatch(/(^|\s)(-g|--global)(\s|$)/);
+
+ const globalBinary = await execContainer(
+ result.id,
+ ["bash", "-c", "command -v cloudcli"],
+ [
+ "--user",
+ "coder",
+ "--env",
+ "HOME=/home/coder",
+ "--env",
+ `PATH=${DEFAULT_PATH}`,
+ ],
+ );
+ expect(globalBinary.exitCode).not.toBe(0);
+ });
+
+ test("starts on IPv4 loopback with the secure defaults", async () => {
+ const result = await setup();
+ await installCloudCLI(result);
+
+ await execContainer(
+ result.id,
+ [
+ "bash",
+ "-c",
+ `mkdir -p '${MODULE_ROOT}/run' && echo 999999 > '${MODULE_ROOT}/run/cloudcli.pid' && chown -R coder:coder '/home/coder/.coder-modules'`,
+ ],
+ ["--user", "root"],
+ );
+ const response = await startCloudCLI(result);
+ expect(response.stdout).toContain("Waiting for CloudCLI to come online...");
+ expect(response.stderr).not.toContain("curl:");
+
+ const health = await execContainer(
+ result.id,
+ [
+ "node",
+ "-e",
+ "require('node:http').get('http://127.0.0.1:3001/health',r=>{r.resume();process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))",
+ ],
+ ["--user", "coder"],
+ );
+ expect(health.exitCode).toBe(0);
+
+ const listener = await execContainer(result.id, [
+ "bash",
+ "-c",
+ "port_hex=$(printf '%04X' 3001); awk -v expected=\"0100007F:$port_hex\" '$2 == expected && $4 == \"0A\" { print $2 }' /proc/net/tcp",
+ ]);
+ expect(listener.stdout.trim()).toBe("0100007F:0BB9");
+
+ const environment = await readFileContainer(
+ result.id,
+ `${MODULE_ROOT}/run/mock-environment`,
+ );
+ expect(environment).toContain("HOST=127.0.0.1");
+ expect(environment).toContain("SERVER_PORT=3001");
+ expect(environment).toContain(`DATABASE_PATH=${MODULE_ROOT}/data/auth.db`);
+ expect(environment).toContain("WORKSPACES_ROOT=\n");
+
+ const pid = await readFileContainer(
+ result.id,
+ `${MODULE_ROOT}/run/cloudcli.pid`,
+ );
+ expect(pid.trim()).not.toBe("999999");
+ });
+
+ test("renders a custom port and workspace root safely", async () => {
+ const result = await setup(customScripts);
+ await installCloudCLI(result);
+ await startCloudCLI(result);
+
+ const environment = await readFileContainer(
+ result.id,
+ `${MODULE_ROOT}/run/mock-environment`,
+ );
+ expect(environment).toContain("HOST=127.0.0.1");
+ expect(environment).toContain("SERVER_PORT=43123");
+ expect(environment).toContain("WORKSPACES_ROOT=/home/coder/project");
+
+ const argumentsFile = await readFileContainer(
+ result.id,
+ `${MODULE_ROOT}/run/mock-arguments`,
+ );
+ expect(argumentsFile).toContain("--port 43123");
+ expect(argumentsFile).toContain(
+ `--database-path ${MODULE_ROOT}/data/auth.db`,
+ );
+ });
+
+ test("does not start a second process when CloudCLI is healthy", async () => {
+ const result = await setup();
+ await installCloudCLI(result);
+ await startCloudCLI(result);
+
+ const firstPID = (
+ await readFileContainer(result.id, `${MODULE_ROOT}/run/cloudcli.pid`)
+ ).trim();
+ await startCloudCLI(result);
+ const secondPID = (
+ await readFileContainer(result.id, `${MODULE_ROOT}/run/cloudcli.pid`)
+ ).trim();
+
+ expect(secondPID).toBe(firstPID);
+ });
+
+ test("fails without terminating an unrelated listener", async () => {
+ const result = await setup();
+ await installCloudCLI(result);
+
+ const occupied = await execContainer(
+ result.id,
+ [
+ "bash",
+ "-c",
+ "nohup node -e \"require('node:http').createServer((q,s)=>{s.statusCode=404;s.end()}).listen(3001,'127.0.0.1')\" >/tmp/occupied.log 2>&1 & echo $! >/tmp/occupied.pid; sleep 1",
+ ],
+ ["--user", "coder", "--env", "HOME=/home/coder"],
+ );
+ expect(occupied.exitCode).toBe(0);
+
+ const response = await runScript(
+ result.id,
+ "cloudcli-start-occupied",
+ result.scripts.start,
+ );
+ expect(response.exitCode).not.toBe(0);
+ expect(`${response.stdout}\n${response.stderr}`).toContain(
+ "already used by another process",
+ );
+
+ const stillRunning = await execContainer(
+ result.id,
+ ["bash", "-c", 'kill -0 "$(cat /tmp/occupied.pid)"'],
+ ["--user", "coder"],
+ );
+ expect(stillRunning.exitCode).toBe(0);
+ });
+
+ test("rejects a healthy listener that is not restricted to loopback", async () => {
+ const result = await setup();
+ await installCloudCLI(result);
+
+ const occupied = await execContainer(
+ result.id,
+ [
+ "bash",
+ "-c",
+ "nohup node -e \"require('node:http').createServer((q,s)=>{s.setHeader('content-type','application/json');s.end(JSON.stringify({status:'ok'}))}).listen(3001,'0.0.0.0')\" >/tmp/wildcard.log 2>&1 & echo $! >/tmp/wildcard.pid; sleep 1",
+ ],
+ ["--user", "coder", "--env", "HOME=/home/coder"],
+ );
+ expect(occupied.exitCode).toBe(0);
+
+ const response = await runScript(
+ result.id,
+ "cloudcli-start-wildcard",
+ result.scripts.start,
+ );
+ expect(response.exitCode).not.toBe(0);
+ expect(`${response.stdout}\n${response.stderr}`).toContain(
+ "not restricted to IPv4 loopback",
+ );
+
+ const stillRunning = await execContainer(
+ result.id,
+ ["bash", "-c", 'kill -0 "$(cat /tmp/wildcard.pid)"'],
+ ["--user", "coder"],
+ );
+ expect(stillRunning.exitCode).toBe(0);
+ });
+
+ test("rejects a workspace without Node.js", async () => {
+ const result = await setup();
+ const response = await runScript(
+ result.id,
+ "cloudcli-install-no-node",
+ result.scripts.install,
+ "/usr/local/test-bin:/usr/bin:/bin",
+ );
+
+ expect(response.exitCode).not.toBe(0);
+ expect(`${response.stdout}\n${response.stderr}`).toContain(
+ "node is required",
+ );
+ });
+
+ test("rejects Node.js versions older than 22", async () => {
+ const result = await setup();
+ await writeFileContainer(
+ result.id,
+ "/tmp/node",
+ "#!/usr/bin/env bash\nprintf 'v20.19.0\\n'\n",
+ { user: "root" },
+ );
+ const prepareOldNode = await execContainer(
+ result.id,
+ [
+ "bash",
+ "-c",
+ "mkdir -p /tmp/old-node-bin && cp /tmp/node /tmp/old-node-bin/node && chmod 755 /tmp/old-node-bin/node",
+ ],
+ ["--user", "root"],
+ );
+ expect(prepareOldNode.exitCode).toBe(0);
+
+ const response = await runScript(
+ result.id,
+ "cloudcli-install-old-node",
+ result.scripts.install,
+ "/tmp/old-node-bin:/usr/local/test-bin:/usr/bin:/bin",
+ );
+
+ expect(response.exitCode).not.toBe(0);
+ expect(`${response.stdout}\n${response.stderr}`).toContain(
+ "requires Node.js 22 or newer; found v20.19.0",
+ );
+ });
+});
diff --git a/registry/edd88-pixel/modules/cloudcli/main.tf b/registry/edd88-pixel/modules/cloudcli/main.tf
new file mode 100644
index 000000000..5b7214bf5
--- /dev/null
+++ b/registry/edd88-pixel/modules/cloudcli/main.tf
@@ -0,0 +1,139 @@
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = ">= 2.13"
+ }
+ }
+}
+
+variable "agent_id" {
+ description = "The ID of a Coder agent."
+ type = string
+}
+
+variable "port" {
+ description = "The loopback port used by the CloudCLI server and Coder app."
+ type = number
+ default = 3001
+
+ validation {
+ condition = var.port >= 1024 && var.port <= 65535 && floor(var.port) == var.port
+ error_message = "port must be an integer between 1024 and 65535."
+ }
+}
+
+variable "cloudcli_version" {
+ description = "Exact CloudCLI package version to install."
+ type = string
+ default = "1.35.0"
+
+ validation {
+ condition = can(regex("^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$", var.cloudcli_version))
+ error_message = "cloudcli_version must be an exact stable semantic version such as 1.35.0."
+ }
+}
+
+variable "workspaces_root" {
+ description = "Optional absolute directory that limits CloudCLI project discovery. When unset, CloudCLI uses the workspace user's home directory."
+ type = string
+ default = null
+
+ validation {
+ condition = var.workspaces_root == null ? true : (
+ can(regex("^/[A-Za-z0-9._/-]+$", var.workspaces_root)) &&
+ !can(regex("(^|/)\\.\\.(/|$)", var.workspaces_root))
+ )
+ error_message = "workspaces_root must be an absolute path containing only letters, numbers, dots, underscores, hyphens, and slashes, without parent-directory components."
+ }
+}
+
+variable "order" {
+ description = "The order determines the position of the app in the Coder UI. The lowest order is shown first."
+ type = number
+ default = null
+}
+
+variable "group" {
+ description = "The name of a group that this app belongs to."
+ type = string
+ default = null
+}
+
+locals {
+ icon_url = "https://avatars.githubusercontent.com/u/252026187?s=200&v=4"
+ module_directory = "$HOME/.coder-modules/edd88-pixel/cloudcli"
+ start_script_name = "edd88-pixel-cloudcli-start_script"
+ start_script_path = "${local.module_directory}/scripts/start.sh"
+ start_log_path = "${local.module_directory}/logs/start.log"
+
+ install_script = templatefile("${path.module}/scripts/install.sh.tftpl", {
+ ARG_CLOUDCLI_VERSION = var.cloudcli_version
+ })
+
+ start_script = templatefile("${path.module}/scripts/start.sh.tftpl", {
+ ARG_PORT = tostring(var.port)
+ ARG_WORKSPACES_ROOT_B64 = var.workspaces_root == null ? "" : base64encode(var.workspaces_root)
+ })
+}
+
+module "coder_utils" {
+ source = "registry.coder.com/coder/coder-utils/coder"
+ version = "0.0.1"
+
+ agent_id = var.agent_id
+ module_directory = local.module_directory
+ display_name_prefix = "CloudCLI"
+ icon = local.icon_url
+ install_script = local.install_script
+}
+
+resource "coder_script" "start_script" {
+ agent_id = var.agent_id
+ display_name = "CloudCLI: Start Script"
+ icon = local.icon_url
+ run_on_start = true
+ start_blocks_login = false
+
+ script = <<-EOT
+ #!/bin/bash
+ set -o errexit
+ set -o pipefail
+
+ trap 'coder exp sync complete ${local.start_script_name}' EXIT
+
+ coder exp sync want ${local.start_script_name} ${module.coder_utils.scripts[0]}
+ coder exp sync start --timeout 30m ${local.start_script_name}
+
+ echo -n '${base64encode(local.start_script)}' | base64 -d > ${local.start_script_path}
+ chmod +x ${local.start_script_path}
+
+ ${local.start_script_path} 2>&1 | tee ${local.start_log_path}
+ EOT
+}
+
+# CloudCLI 1.35.0 uses root-relative API and WebSocket routes, so Coder's path proxy cannot serve it correctly.
+resource "coder_app" "cloudcli" {
+ agent_id = var.agent_id
+ slug = "cloudcli"
+ display_name = "CloudCLI"
+ url = "http://localhost:${var.port}"
+ icon = local.icon_url
+ subdomain = true
+ share = "owner"
+ order = var.order
+ group = var.group
+
+ healthcheck {
+ url = "http://localhost:${var.port}/health"
+ interval = 5
+ threshold = 6
+ }
+}
+
+output "scripts" {
+ description = "Ordered list of coder exp sync names produced by the CloudCLI install and start pipeline."
+ value = concat(module.coder_utils.scripts, [local.start_script_name])
+}
diff --git a/registry/edd88-pixel/modules/cloudcli/main.tftest.hcl b/registry/edd88-pixel/modules/cloudcli/main.tftest.hcl
new file mode 100644
index 000000000..9a6d3277a
--- /dev/null
+++ b/registry/edd88-pixel/modules/cloudcli/main.tftest.hcl
@@ -0,0 +1,184 @@
+mock_provider "coder" {}
+
+run "defaults_are_secure" {
+ command = apply
+
+ variables {
+ agent_id = "test-agent"
+ }
+
+ assert {
+ condition = coder_app.cloudcli.url == "http://localhost:3001"
+ error_message = "The default app URL must use port 3001."
+ }
+
+ assert {
+ condition = coder_app.cloudcli.subdomain
+ error_message = "CloudCLI must use a Coder subdomain."
+ }
+
+ assert {
+ condition = coder_app.cloudcli.share == "owner"
+ error_message = "CloudCLI must be restricted to the workspace owner."
+ }
+
+ assert {
+ condition = coder_app.cloudcli.icon == "https://avatars.githubusercontent.com/u/252026187?s=200&v=4"
+ error_message = "The app must use the CloudCLI project icon."
+ }
+
+ assert {
+ condition = one(coder_app.cloudcli.healthcheck).url == "http://localhost:3001/health"
+ error_message = "The health check must use CloudCLI's /health endpoint."
+ }
+
+ assert {
+ condition = strcontains(local.start_script, "export HOST=\"127.0.0.1\"")
+ error_message = "The startup script must bind CloudCLI to IPv4 loopback."
+ }
+
+ assert {
+ condition = !strcontains(local.start_script, "0.0.0.0")
+ error_message = "The default startup script must not contain an all-interface listener."
+ }
+
+ assert {
+ condition = strcontains(local.start_script, "DATABASE_PATH=\"$DATA_DIR/auth.db\"")
+ error_message = "CloudCLI's database must be stored in the module data directory."
+ }
+
+ assert {
+ condition = local.module_directory == "$HOME/.coder-modules/edd88-pixel/cloudcli"
+ error_message = "The module must use the standard per-module data root."
+ }
+
+ assert {
+ condition = length(output.scripts) == 2
+ error_message = "The coder-utils pipeline must expose install and start scripts."
+ }
+
+ assert {
+ condition = strcontains(coder_script.start_script.script, "coder exp sync start --timeout 30m edd88-pixel-cloudcli-start_script")
+ error_message = "The start script must allow slow first-time CloudCLI installations to finish."
+ }
+
+ assert {
+ condition = (
+ strcontains(local.start_script, "Waiting for CloudCLI to come online...") &&
+ !strcontains(local.start_script, "--show-error")
+ )
+ error_message = "Readiness polling must log a calm status message without transient curl warnings."
+ }
+}
+
+run "custom_configuration" {
+ command = apply
+
+ variables {
+ agent_id = "test-agent"
+ port = 43123
+ workspaces_root = "/home/coder/project"
+ order = 7
+ group = "AI Tools"
+ }
+
+ assert {
+ condition = coder_app.cloudcli.url == "http://localhost:43123"
+ error_message = "The app URL must use the configured port."
+ }
+
+ assert {
+ condition = one(coder_app.cloudcli.healthcheck).url == "http://localhost:43123/health"
+ error_message = "The health check must use the configured port."
+ }
+
+ assert {
+ condition = coder_app.cloudcli.order == 7 && coder_app.cloudcli.group == "AI Tools"
+ error_message = "The app must preserve its configured order and group."
+ }
+
+ assert {
+ condition = strcontains(local.start_script, base64encode("/home/coder/project"))
+ error_message = "The configured workspace root must be encoded into the startup script."
+ }
+}
+
+run "rejects_low_port" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ port = 1023
+ }
+
+ expect_failures = [var.port]
+}
+
+run "rejects_high_port" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ port = 65536
+ }
+
+ expect_failures = [var.port]
+}
+
+run "rejects_relative_workspace_root" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workspaces_root = "project"
+ }
+
+ expect_failures = [var.workspaces_root]
+}
+
+run "rejects_dangerous_workspace_root" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workspaces_root = "/home/coder/project;touch-pwned"
+ }
+
+ expect_failures = [var.workspaces_root]
+}
+
+run "rejects_parent_workspace_root" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workspaces_root = "/home/coder/../root"
+ }
+
+ expect_failures = [var.workspaces_root]
+}
+
+run "rejects_unpinned_version" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ cloudcli_version = "latest"
+ }
+
+ expect_failures = [var.cloudcli_version]
+}
+
+run "accepts_exact_version" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ cloudcli_version = "2.4.6"
+ }
+
+ assert {
+ condition = strcontains(local.install_script, "ARG_CLOUDCLI_VERSION='2.4.6'")
+ error_message = "The exact CloudCLI version must be rendered into the install script."
+ }
+}
diff --git a/registry/edd88-pixel/modules/cloudcli/scripts/install.sh.tftpl b/registry/edd88-pixel/modules/cloudcli/scripts/install.sh.tftpl
new file mode 100644
index 000000000..c3400e0fe
--- /dev/null
+++ b/registry/edd88-pixel/modules/cloudcli/scripts/install.sh.tftpl
@@ -0,0 +1,86 @@
+#!/usr/bin/env bash
+
+if [ -f "$HOME/.bashrc" ]; then
+ # shellcheck disable=SC1090
+ source "$HOME/.bashrc"
+fi
+
+if [ -s "$HOME/.nvm/nvm.sh" ]; then
+ # shellcheck disable=SC1091
+ source "$HOME/.nvm/nvm.sh"
+fi
+
+set -euo pipefail
+
+ARG_CLOUDCLI_VERSION='${ARG_CLOUDCLI_VERSION}'
+MODULE_ROOT="$HOME/.coder-modules/edd88-pixel/cloudcli"
+RUNTIME_DIR="$MODULE_ROOT/runtime"
+PACKAGE_DIR="$RUNTIME_DIR/node_modules/@cloudcli-ai/cloudcli"
+CLOUDCLI_BIN="$RUNTIME_DIR/node_modules/.bin/cloudcli"
+PACKAGE_SPEC="@cloudcli-ai/cloudcli@$ARG_CLOUDCLI_VERSION"
+
+require_command() {
+ if ! command -v "$1" > /dev/null 2>&1; then
+ printf 'Error: %s is required. Install Node.js 22 or newer with npm in the workspace image.\n' "$1" >&2
+ exit 1
+ fi
+}
+
+require_supported_node() {
+ local node_version
+ local node_major
+
+ node_version="$(node --version)"
+ node_major="$${node_version#v}"
+ node_major="$${node_major%%.*}"
+
+ if ! [[ "$node_major" =~ ^[0-9]+$ ]] || ((node_major < 22)); then
+ printf 'Error: CloudCLI requires Node.js 22 or newer; found %s.\n' "$node_version" >&2
+ exit 1
+ fi
+
+ printf 'Using Node.js %s and npm %s.\n' "$node_version" "$(npm --version)"
+}
+
+installed_version() {
+ if [ ! -f "$PACKAGE_DIR/package.json" ]; then
+ return 1
+ fi
+
+ PACKAGE_JSON="$PACKAGE_DIR/package.json" node -p "require(process.env.PACKAGE_JSON).version"
+}
+
+require_command node
+require_command npm
+require_supported_node
+
+mkdir -p "$RUNTIME_DIR" "$MODULE_ROOT/data" "$MODULE_ROOT/run"
+
+current_version="$(installed_version || true)"
+if [ "$current_version" = "$ARG_CLOUDCLI_VERSION" ] && [ -x "$CLOUDCLI_BIN" ]; then
+ printf 'CloudCLI %s is already installed in %s.\n' "$current_version" "$RUNTIME_DIR"
+ exit 0
+fi
+
+printf 'Installing %s in %s.\n' "$PACKAGE_SPEC" "$RUNTIME_DIR"
+npm install \
+ --prefix "$RUNTIME_DIR" \
+ --no-save \
+ --package-lock=false \
+ --omit=dev \
+ --no-audit \
+ --no-fund \
+ "$PACKAGE_SPEC"
+
+if [ ! -x "$CLOUDCLI_BIN" ]; then
+ printf 'Error: CloudCLI installation completed without creating %s.\n' "$CLOUDCLI_BIN" >&2
+ exit 1
+fi
+
+current_version="$(installed_version)"
+if [ "$current_version" != "$ARG_CLOUDCLI_VERSION" ]; then
+ printf 'Error: expected CloudCLI %s but npm installed %s.\n' "$ARG_CLOUDCLI_VERSION" "$current_version" >&2
+ exit 1
+fi
+
+printf 'Installed CloudCLI %s successfully.\n' "$current_version"
diff --git a/registry/edd88-pixel/modules/cloudcli/scripts/start.sh.tftpl b/registry/edd88-pixel/modules/cloudcli/scripts/start.sh.tftpl
new file mode 100644
index 000000000..b9255b7ba
--- /dev/null
+++ b/registry/edd88-pixel/modules/cloudcli/scripts/start.sh.tftpl
@@ -0,0 +1,185 @@
+#!/usr/bin/env bash
+
+if [ -f "$HOME/.bashrc" ]; then
+ # shellcheck disable=SC1090
+ source "$HOME/.bashrc"
+fi
+
+if [ -s "$HOME/.nvm/nvm.sh" ]; then
+ # shellcheck disable=SC1091
+ source "$HOME/.nvm/nvm.sh"
+fi
+
+set -euo pipefail
+
+ARG_PORT='${ARG_PORT}'
+ARG_WORKSPACES_ROOT_B64='${ARG_WORKSPACES_ROOT_B64}'
+MODULE_ROOT="$HOME/.coder-modules/edd88-pixel/cloudcli"
+CLOUDCLI_BIN="$MODULE_ROOT/runtime/node_modules/.bin/cloudcli"
+DATA_DIR="$MODULE_ROOT/data"
+RUN_DIR="$MODULE_ROOT/run"
+PID_FILE="$RUN_DIR/cloudcli.pid"
+SERVICE_LOG="$MODULE_ROOT/logs/cloudcli.log"
+HEALTH_URL="http://127.0.0.1:$ARG_PORT/health"
+
+health_ready() {
+ if command -v curl > /dev/null 2>&1; then
+ curl --fail --silent --max-time 2 "$HEALTH_URL" > /dev/null
+ return
+ fi
+
+ if command -v wget > /dev/null 2>&1; then
+ wget --quiet --timeout=2 --output-document=/dev/null "$HEALTH_URL"
+ return
+ fi
+
+ node -e '
+ const http = require("node:http");
+ const request = http.get(process.argv[1], { timeout: 2000 }, (response) => {
+ response.resume();
+ process.exit(response.statusCode >= 200 && response.statusCode < 400 ? 0 : 1);
+ });
+ request.on("timeout", () => request.destroy());
+ request.on("error", () => process.exit(1));
+ ' "$HEALTH_URL"
+}
+
+port_in_use() {
+ node -e '
+ const net = require("node:net");
+ const socket = net.createConnection({
+ host: process.argv[1],
+ port: Number(process.argv[2]),
+ timeout: 500,
+ });
+ socket.on("connect", () => {
+ socket.destroy();
+ process.exit(0);
+ });
+ socket.on("timeout", () => {
+ socket.destroy();
+ process.exit(1);
+ });
+ socket.on("error", () => process.exit(1));
+ ' "127.0.0.1" "$ARG_PORT"
+}
+
+stop_started_process() {
+ local pid="$1"
+
+ if kill -0 "$pid" 2> /dev/null; then
+ kill "$pid" 2> /dev/null || true
+ wait "$pid" 2> /dev/null || true
+ fi
+ rm -f "$PID_FILE"
+}
+
+verify_loopback_listener() {
+ local listeners
+ local address
+ local port_hex=""
+
+ if command -v ss > /dev/null 2>&1; then
+ listeners="$(ss -H -ltn "sport = :$ARG_PORT" | awk '{print $4}')"
+ elif [ -r /proc/net/tcp ]; then
+ printf -v port_hex '%04X' "$ARG_PORT"
+ listeners="$(
+ awk -v port=":$port_hex" \
+ '$4 == "0A" && substr($2, length($2) - length(port) + 1) == port { print $2 }' \
+ /proc/net/tcp
+ )"
+ else
+ return 0
+ fi
+
+ if [ -z "$listeners" ]; then
+ printf 'Error: CloudCLI health check succeeded but no listener was found on port %s.\n' "$ARG_PORT" >&2
+ return 1
+ fi
+
+ while IFS= read -r address; do
+ if [ "$address" != "127.0.0.1:$ARG_PORT" ] &&
+ { [ -z "$port_hex" ] || [ "$address" != "0100007F:$port_hex" ]; }; then
+ printf 'Error: CloudCLI listener %s is not restricted to IPv4 loopback.\n' "$address" >&2
+ return 1
+ fi
+ done <<< "$listeners"
+}
+
+if [ ! -x "$CLOUDCLI_BIN" ]; then
+ printf 'Error: CloudCLI is not installed at %s.\n' "$CLOUDCLI_BIN" >&2
+ exit 1
+fi
+
+mkdir -p "$DATA_DIR" "$RUN_DIR" "$(dirname "$SERVICE_LOG")"
+
+if health_ready; then
+ if ! verify_loopback_listener; then
+ printf 'Error: the existing healthy process was left running.\n' >&2
+ exit 1
+ fi
+ printf 'CloudCLI is already healthy at %s; leaving the existing process running.\n' "$HEALTH_URL"
+ exit 0
+fi
+
+if [ -f "$PID_FILE" ]; then
+ existing_pid="$(cat "$PID_FILE")"
+ if [[ "$existing_pid" =~ ^[0-9]+$ ]] && kill -0 "$existing_pid" 2> /dev/null; then
+ printf 'Error: CloudCLI process %s is running but its health check is failing.\n' "$existing_pid" >&2
+ exit 1
+ fi
+
+ printf 'Removing stale CloudCLI PID file.\n'
+ rm -f "$PID_FILE"
+fi
+
+if port_in_use; then
+ printf 'Error: port %s is already used by another process; no process was terminated.\n' "$ARG_PORT" >&2
+ exit 1
+fi
+
+export HOST="127.0.0.1"
+export SERVER_PORT="$ARG_PORT"
+export PORT="$ARG_PORT"
+export DATABASE_PATH="$DATA_DIR/auth.db"
+export ENABLE_HTTPS="false"
+
+if [ -n "$ARG_WORKSPACES_ROOT_B64" ]; then
+ WORKSPACES_ROOT="$(printf '%s' "$ARG_WORKSPACES_ROOT_B64" | base64 --decode)"
+ export WORKSPACES_ROOT
+else
+ unset WORKSPACES_ROOT
+fi
+
+printf 'Starting CloudCLI on %s with data stored under %s.\n' "$HEALTH_URL" "$DATA_DIR"
+nohup "$CLOUDCLI_BIN" start \
+ --port "$ARG_PORT" \
+ --database-path "$DATABASE_PATH" \
+ >> "$SERVICE_LOG" 2>&1 &
+cloudcli_pid=$!
+printf '%s\n' "$cloudcli_pid" > "$PID_FILE"
+printf 'Waiting for CloudCLI to come online...\n'
+
+for _ in $(seq 1 60); do
+ if health_ready; then
+ if ! verify_loopback_listener; then
+ stop_started_process "$cloudcli_pid"
+ exit 1
+ fi
+ printf 'CloudCLI is ready at %s.\n' "$HEALTH_URL"
+ exit 0
+ fi
+
+ if ! kill -0 "$cloudcli_pid" 2> /dev/null; then
+ wait "$cloudcli_pid" || exit_code=$?
+ rm -f "$PID_FILE"
+ printf 'Error: CloudCLI exited before becoming healthy (exit code %s). See %s.\n' "$${exit_code:-unknown}" "$SERVICE_LOG" >&2
+ exit 1
+ fi
+
+ sleep 1
+done
+
+printf 'Error: CloudCLI did not become healthy within 60 seconds. See %s.\n' "$SERVICE_LOG" >&2
+stop_started_process "$cloudcli_pid"
+exit 1
diff --git a/registry/edd88-pixel/modules/cloudcli/testdata/cloudcli-mock.sh b/registry/edd88-pixel/modules/cloudcli/testdata/cloudcli-mock.sh
new file mode 100644
index 000000000..85423cd43
--- /dev/null
+++ b/registry/edd88-pixel/modules/cloudcli/testdata/cloudcli-mock.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+if [ "${1:-}" = "version" ] || [ "${1:-}" = "--version" ]; then
+ printf '1.35.0\n'
+ exit 0
+fi
+
+if [ "${1:-}" != "start" ]; then
+ printf 'unexpected CloudCLI command: %s\n' "$*" >&2
+ exit 2
+fi
+
+module_root="$(dirname "$(dirname "$DATABASE_PATH")")"
+printf '%s\n' "$*" > "$module_root/run/mock-arguments"
+printf 'HOST=%s\nSERVER_PORT=%s\nDATABASE_PATH=%s\nWORKSPACES_ROOT=%s\n' \
+ "$HOST" \
+ "$SERVER_PORT" \
+ "$DATABASE_PATH" \
+ "${WORKSPACES_ROOT:-}" \
+ > "$module_root/run/mock-environment"
+
+exec node << 'NODE'
+const http = require("node:http");
+
+const server = http.createServer((request, response) => {
+ if (request.url === "/health") {
+ response.writeHead(200, { "content-type": "application/json" });
+ response.end(JSON.stringify({ status: "ok" }));
+ return;
+ }
+
+ response.writeHead(404);
+ response.end();
+});
+
+server.listen(Number(process.env.SERVER_PORT), process.env.HOST);
+NODE