mirror of
https://github.com/coder/registry.git
synced 2026-06-03 04:58:15 +00:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9525e2d4d1 | |||
| e07e289698 | |||
| 1be384252e | |||
| 8fa6be0edf | |||
| 82c86db196 | |||
| 019d735fdd | |||
| 520a2947e1 | |||
| abe133afe8 | |||
| 5335c85495 | |||
| d6a18d8419 | |||
| d03588356e | |||
| 6e94ecc165 | |||
| 25636524d3 | |||
| 504be92cf4 | |||
| 35f93c663f | |||
| 69e5dc5c80 | |||
| b143b7d9ba | |||
| 0a8930d60d | |||
| d21db0d220 | |||
| 392f6b120a | |||
| 7de72fc7cc | |||
| 3e1ddbf624 | |||
| 0021a9fe7d |
@@ -93,7 +93,7 @@ jobs:
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@v1.39.2
|
||||
uses: crate-ci/typos@v1.40.0
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 2.3 MiB |
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="250" cy="250" r="250" fill="#fff"/>
|
||||
<path d="m335 150h40v200h-40zm-130 0a100 100 0 1 0 0 200 100 100 0 1 0 0-200zm0 40a60 60 0 1 1 0 120 60 60 0 1 1 0-120z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 293 B |
@@ -0,0 +1,37 @@
|
||||
---
|
||||
display_name: Open WebUI
|
||||
description: A self-hosted AI chat interface supporting various LLM providers
|
||||
icon: ../../../../.icons/openwebui.svg
|
||||
verified: true
|
||||
tags: [ai, llm, chat, web-ui, python]
|
||||
---
|
||||
|
||||
# Open WebUI
|
||||
|
||||
Open WebUI is a user-friendly web interface for interacting with Large Language Models. It provides a ChatGPT-like interface that can connect to various LLM providers including OpenAI, Ollama, and more.
|
||||
|
||||
This module installs and runs Open WebUI using Python and pip within your Coder workspace.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Python 3.11 or higher** must be installed in your image (with `venv` module)
|
||||
- Port 7800 (default) or your custom port must be available
|
||||
|
||||
For Ubuntu/Debian, you can install Python 3.11 from [deadsnakes PPA](https://launchpad.net/~deadsnakes/+archive/ubuntu/ppa):
|
||||
|
||||
```shell
|
||||
sudo add-apt-repository -y ppa:deadsnakes/ppa
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3.11 python3.11-venv
|
||||
```
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```tf
|
||||
module "open-webui" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/open-webui/coder"
|
||||
version = "0.9.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,72 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Add required variables for your modules and remove any unneeded variables
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "http_server_log_path" {
|
||||
type = string
|
||||
description = "The path to log Open WebUI to."
|
||||
default = "/tmp/open-webui.log"
|
||||
}
|
||||
|
||||
variable "http_server_port" {
|
||||
type = number
|
||||
description = "The port to run Open WebUI on."
|
||||
default = 7800
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
resource "coder_script" "open-webui" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "open-webui"
|
||||
icon = "/icon/openwebui.svg"
|
||||
script = templatefile("${path.module}/run.sh", {
|
||||
HTTP_SERVER_LOG_PATH : var.http_server_log_path,
|
||||
HTTP_SERVER_PORT : var.http_server_port,
|
||||
})
|
||||
run_on_start = true
|
||||
}
|
||||
|
||||
resource "coder_app" "open-webui" {
|
||||
agent_id = var.agent_id
|
||||
slug = "open-webui"
|
||||
display_name = "Open WebUI"
|
||||
url = "http://localhost:${var.http_server_port}"
|
||||
icon = "/icon/openwebui.svg"
|
||||
subdomain = true
|
||||
share = var.share
|
||||
order = var.order
|
||||
group = var.group
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
set -eu
|
||||
|
||||
printf '\033[0;1mInstalling Open WebUI...\n\n'
|
||||
|
||||
check_python_version() {
|
||||
python_cmd="$1"
|
||||
if command -v "$python_cmd" > /dev/null 2>&1; then
|
||||
version=$("$python_cmd" --version 2>&1 | awk '{print $2}')
|
||||
major=$(echo "$version" | cut -d. -f1)
|
||||
minor=$(echo "$version" | cut -d. -f2)
|
||||
if [ "$major" -eq 3 ] && [ "$minor" -ge 11 ]; then
|
||||
echo "$python_cmd"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
PYTHON_CMD=""
|
||||
for cmd in python3.13 python3.12 python3.11 python3 python; do
|
||||
if result=$(check_python_version "$cmd"); then
|
||||
PYTHON_CMD="$result"
|
||||
echo "✅ Found suitable Python: $PYTHON_CMD ($($PYTHON_CMD --version 2>&1))"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PYTHON_CMD" ]; then
|
||||
echo "❌ Python 3.11 or higher is required but not found."
|
||||
echo ""
|
||||
echo "Please install Python 3.11+ in your image. For example on Ubuntu/Debian:"
|
||||
echo " sudo add-apt-repository -y ppa:deadsnakes/ppa"
|
||||
echo " sudo apt-get update"
|
||||
echo " sudo apt-get install -y python3.11 python3.11-venv"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
VENV_DIR="$HOME/.open-webui-venv"
|
||||
if [ ! -d "$VENV_DIR" ]; then
|
||||
echo "📦 Creating virtual environment..."
|
||||
"$PYTHON_CMD" -m venv "$VENV_DIR"
|
||||
fi
|
||||
. "$VENV_DIR/bin/activate"
|
||||
|
||||
if ! pip show open-webui > /dev/null 2>&1; then
|
||||
echo "📦 Installing Open WebUI..."
|
||||
pip install open-webui
|
||||
echo "🥳 Open WebUI has been installed"
|
||||
else
|
||||
echo "✅ Open WebUI is already installed"
|
||||
fi
|
||||
|
||||
echo "👷 Starting Open WebUI in background..."
|
||||
echo "Check logs at ${HTTP_SERVER_LOG_PATH}"
|
||||
|
||||
open-webui serve --host 0.0.0.0 --port "${HTTP_SERVER_PORT}" > "${HTTP_SERVER_LOG_PATH}" 2>&1 &
|
||||
|
||||
echo "🥳 Open WebUI is ready. HTTP server is listening on port ${HTTP_SERVER_PORT}"
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 249 KiB |
@@ -0,0 +1,67 @@
|
||||
---
|
||||
display_name: Antigravity
|
||||
description: Add a one-click button to launch Google Antigravity
|
||||
icon: ../../../../.icons/antigravity.svg
|
||||
verified: true
|
||||
tags: [ide, antigravity, ai, google]
|
||||
---
|
||||
|
||||
# Antigravity IDE
|
||||
|
||||
Add a button to open any workspace with a single click in [Antigravity IDE](https://antigravity.google).
|
||||
|
||||
Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder).
|
||||
|
||||
```tf
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Open in a specific directory
|
||||
|
||||
```tf
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
```
|
||||
|
||||
### Configure MCP servers for Antigravity
|
||||
|
||||
Provide a JSON-encoded string via the `mcp` input. When set, the module writes the value to `~/.gemini/antigravity/mcp_config.json` using a `coder_script` on workspace start.
|
||||
|
||||
The following example configures Antigravity to use the GitHub MCP server with authentication facilitated by the [`coder_external_auth`](https://coder.com/docs/admin/external-auth#configure-a-github-oauth-app) resource.
|
||||
|
||||
```tf
|
||||
module "antigravity" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/antigravity/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
mcpServers = {
|
||||
"github" : {
|
||||
"url" : "https://api.githubcopilot.com/mcp/",
|
||||
"headers" : {
|
||||
"Authorization" : "Bearer ${data.coder_external_auth.github.access_token}",
|
||||
},
|
||||
"type" : "http"
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
data "coder_external_auth" "github" {
|
||||
id = "github"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,130 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
runContainer,
|
||||
execContainer,
|
||||
removeContainer,
|
||||
findResourceInstance,
|
||||
readFileContainer,
|
||||
} from "~test";
|
||||
|
||||
describe("antigravity", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder and open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
open_recent: "true",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder but not open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
open_recent: "false",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("adds open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
open_recent: "true",
|
||||
});
|
||||
expect(state.outputs.antigravity_url.value).toBe(
|
||||
"antigravity://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
|
||||
it("writes ~/.gemini/antigravity/mcp_config.json when mcp provided", async () => {
|
||||
const id = await runContainer("alpine");
|
||||
try {
|
||||
const mcp = JSON.stringify({
|
||||
servers: { demo: { url: "http://localhost:1234" } },
|
||||
});
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
mcp,
|
||||
});
|
||||
const script = findResourceInstance(
|
||||
state,
|
||||
"coder_script",
|
||||
"antigravity_mcp",
|
||||
).script;
|
||||
const resp = await execContainer(id, ["sh", "-c", script]);
|
||||
if (resp.exitCode !== 0) {
|
||||
console.log(resp.stdout);
|
||||
console.log(resp.stderr);
|
||||
}
|
||||
expect(resp.exitCode).toBe(0);
|
||||
const content = await readFileContainer(
|
||||
id,
|
||||
"/root/.gemini/antigravity/mcp_config.json",
|
||||
);
|
||||
expect(content).toBe(mcp);
|
||||
} finally {
|
||||
await removeContainer(id);
|
||||
}
|
||||
}, 10000);
|
||||
});
|
||||
@@ -0,0 +1,104 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in Antigravity IDE."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "open_recent" {
|
||||
type = bool
|
||||
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
|
||||
default = false
|
||||
}
|
||||
|
||||
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 "slug" {
|
||||
type = string
|
||||
description = "The slug of the app."
|
||||
default = "antigravity"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app."
|
||||
default = "Antigravity IDE"
|
||||
}
|
||||
|
||||
variable "mcp" {
|
||||
type = string
|
||||
description = "JSON-encoded string to configure MCP servers for Antigravity. When set, writes ~/.gemini/antigravity/mcp_config.json."
|
||||
default = ""
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
locals {
|
||||
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
|
||||
}
|
||||
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
web_app_icon = "/icon/antigravity.svg"
|
||||
web_app_slug = var.slug
|
||||
web_app_display_name = var.display_name
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
protocol = "antigravity"
|
||||
}
|
||||
|
||||
resource "coder_script" "antigravity_mcp" {
|
||||
count = var.mcp != "" ? 1 : 0
|
||||
agent_id = var.agent_id
|
||||
display_name = "Antigravity MCP"
|
||||
icon = "/icon/antigravity.svg"
|
||||
run_on_start = true
|
||||
start_blocks_login = false
|
||||
script = <<-EOT
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
mkdir -p "$HOME/.gemini/antigravity"
|
||||
echo -n "${local.mcp_b64}" | base64 -d > "$HOME/.gemini/antigravity/mcp_config.json"
|
||||
chmod 600 "$HOME/.gemini/antigravity/mcp_config.json"
|
||||
EOT
|
||||
}
|
||||
|
||||
output "antigravity_url" {
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "Antigravity IDE URL."
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.3.3"
|
||||
version = "1.4.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "cursor" {
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.3.3"
|
||||
version = "1.4.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -45,7 +45,7 @@ The following example configures Cursor to use the GitHub MCP server with authen
|
||||
module "cursor" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor/coder"
|
||||
version = "1.3.3"
|
||||
version = "1.4.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
@@ -58,6 +58,7 @@ module "cursor" {
|
||||
"type" : "http"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,7 +26,10 @@ describe("cursor", async () => {
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "cursor",
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
@@ -76,21 +79,6 @@ describe("cursor", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "cursor",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
|
||||
it("writes ~/.cursor/mcp.json when mcp provided", async () => {
|
||||
const id = await runContainer("alpine");
|
||||
try {
|
||||
|
||||
@@ -64,26 +64,21 @@ locals {
|
||||
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
|
||||
}
|
||||
|
||||
resource "coder_app" "cursor" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/cursor.svg"
|
||||
slug = var.slug
|
||||
display_name = var.display_name
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = join("", [
|
||||
"cursor://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
coder_app_icon = "/icon/cursor.svg"
|
||||
coder_app_slug = var.slug
|
||||
coder_app_display_name = var.display_name
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
protocol = "cursor"
|
||||
}
|
||||
|
||||
resource "coder_script" "cursor_mcp" {
|
||||
@@ -103,6 +98,6 @@ resource "coder_script" "cursor_mcp" {
|
||||
}
|
||||
|
||||
output "cursor_url" {
|
||||
value = coder_app.cursor.url
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "Cursor IDE Desktop URL."
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ Install the JF CLI and authenticate package managers with Artifactory using OAut
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jfrog-oauth/coder"
|
||||
version = "1.2.3"
|
||||
version = "1.2.4"
|
||||
agent_id = coder_agent.main.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "username" # If you are using GitHub to login to both Coder and Artifactory, use username_field = "username"
|
||||
@@ -57,7 +57,7 @@ Configure the Python pip package manager to fetch packages from Artifactory whil
|
||||
module "jfrog" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/jfrog-oauth/coder"
|
||||
version = "1.2.3"
|
||||
version = "1.2.4"
|
||||
agent_id = coder_agent.main.id
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "email"
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
# Test for jfrog-oauth module
|
||||
|
||||
run "test_required_vars" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
# Mock external auth with valid access token for basic test
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
run "test_empty_access_token_fails" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
# Mock external auth with empty access token
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = ""
|
||||
}
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
resource.coder_script.jfrog
|
||||
]
|
||||
}
|
||||
|
||||
run "test_valid_access_token_succeeds" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
# Mock external auth with valid access token
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify the script resource is created
|
||||
assert {
|
||||
condition = resource.coder_script.jfrog.agent_id == "test-agent-id"
|
||||
error_message = "coder_script agent_id should match the input variable"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.jfrog.display_name == "jfrog"
|
||||
error_message = "coder_script display_name should be 'jfrog'"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_jfrog_url_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "invalid-url"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.jfrog_url
|
||||
]
|
||||
}
|
||||
|
||||
run "test_username_field_validation" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
username_field = "invalid"
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.username_field
|
||||
]
|
||||
}
|
||||
|
||||
run "test_with_npm_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
npm = ["global", "@foo:foo", "@bar:bar"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.jfrog.run_on_start == true
|
||||
error_message = "coder_script should run on start"
|
||||
}
|
||||
|
||||
# Verify npm configuration is in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf npmc --global --repo-resolve \"global\"")
|
||||
error_message = "script should contain jf npmc command for npm"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "@foo:registry=https://example.jfrog.io/artifactory/api/npm/foo")
|
||||
error_message = "script should contain scoped npm registry for @foo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "@bar:registry=https://example.jfrog.io/artifactory/api/npm/bar")
|
||||
error_message = "script should contain scoped npm registry for @bar"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_configure_code_server" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
configure_code_server = true
|
||||
package_managers = {}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# When configure_code_server is true, env vars should be created
|
||||
assert {
|
||||
condition = length(resource.coder_env.jfrog_ide_url) == 1
|
||||
error_message = "coder_env.jfrog_ide_url should be created when configure_code_server is true"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.jfrog_ide_access_token) == 1
|
||||
error_message = "coder_env.jfrog_ide_access_token should be created when configure_code_server is true"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_go_proxy_env" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
go = ["foo", "bar", "baz"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# When go package manager is configured, GOPROXY env should be set
|
||||
assert {
|
||||
condition = length(resource.coder_env.goproxy) == 1
|
||||
error_message = "coder_env.goproxy should be created when go package manager is configured"
|
||||
}
|
||||
|
||||
# Verify GOPROXY contains all repos
|
||||
assert {
|
||||
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/foo")
|
||||
error_message = "GOPROXY should contain foo repo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/bar")
|
||||
error_message = "GOPROXY should contain bar repo"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_env.goproxy[0].value, "example.jfrog.io/artifactory/api/go/baz")
|
||||
error_message = "GOPROXY should contain baz repo"
|
||||
}
|
||||
|
||||
# Verify script contains go configuration
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf goc --global --repo-resolve \"foo\"")
|
||||
error_message = "script should contain jf goc command"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_pypi_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
pypi = ["global", "foo", "bar"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify pip configuration in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf pipc --global --repo-resolve \"global\"")
|
||||
error_message = "script should contain jf pipc command"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "index-url = https://default:valid-token-value@example.jfrog.io/artifactory/api/pypi/global/simple")
|
||||
error_message = "script should contain pip index-url configuration"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "extra-index-url")
|
||||
error_message = "script should contain extra-index-url for additional repos"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_docker_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
docker = ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify docker registration commands in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"foo.jfrog.io\"")
|
||||
error_message = "script should contain register_docker for foo.jfrog.io"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"bar.jfrog.io\"")
|
||||
error_message = "script should contain register_docker for bar.jfrog.io"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "register_docker \"baz.jfrog.io\"")
|
||||
error_message = "script should contain register_docker for baz.jfrog.io"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_conda_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
conda = ["conda-main", "conda-secondary", "conda-local"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify conda configuration in script
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "channels:")
|
||||
error_message = "script should contain conda channels configuration"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-main")
|
||||
error_message = "script should contain conda-main channel"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-secondary")
|
||||
error_message = "script should contain conda-secondary channel"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "example.jfrog.io/artifactory/api/conda/conda-local")
|
||||
error_message = "script should contain conda-local channel"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_maven_package_manager" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-id"
|
||||
jfrog_url = "https://example.jfrog.io"
|
||||
package_managers = {
|
||||
maven = ["central", "snapshots", "local"]
|
||||
}
|
||||
}
|
||||
|
||||
override_data {
|
||||
target = data.coder_external_auth.jfrog
|
||||
values = {
|
||||
access_token = "valid-token-value"
|
||||
}
|
||||
}
|
||||
|
||||
# Verify maven jf mvnc command
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "jf mvnc --global")
|
||||
error_message = "script should contain jf mvnc command"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "--repo-resolve-releases \"central\"")
|
||||
error_message = "script should contain repo-resolve-releases for central"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "--repo-resolve-snapshots \"central\"")
|
||||
error_message = "script should contain repo-resolve-snapshots for central"
|
||||
}
|
||||
|
||||
# Verify settings.xml content
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<servers>")
|
||||
error_message = "script should contain maven servers configuration"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<id>central</id>")
|
||||
error_message = "script should contain central server id"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<id>snapshots</id>")
|
||||
error_message = "script should contain snapshots server id"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<id>local</id>")
|
||||
error_message = "script should contain local server id"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.jfrog.script, "<url>https://example.jfrog.io/artifactory/central</url>")
|
||||
error_message = "script should contain central repository URL"
|
||||
}
|
||||
}
|
||||
@@ -1,189 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
findResourceInstance,
|
||||
runTerraformInit,
|
||||
runTerraformApply,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("jfrog-oauth", async () => {
|
||||
type TestVariables = {
|
||||
agent_id: string;
|
||||
jfrog_url: string;
|
||||
package_managers: string;
|
||||
|
||||
username_field?: string;
|
||||
jfrog_server_id?: string;
|
||||
external_auth_id?: string;
|
||||
configure_code_server?: boolean;
|
||||
};
|
||||
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
const fakeFrogApi = "localhost:8081/artifactory/api";
|
||||
const fakeFrogUrl = "http://localhost:8081";
|
||||
const user = "default";
|
||||
|
||||
testRequiredVariables<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: "{}",
|
||||
});
|
||||
|
||||
it("generates an npmrc with scoped repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
npm: ["global", "@foo:foo", "@bar:bar"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const npmrcStanza = `cat << EOF > ~/.npmrc
|
||||
email=${user}@example.com
|
||||
registry=http://${fakeFrogApi}/npm/global
|
||||
//${fakeFrogApi}/npm/global/:_authToken=
|
||||
@foo:registry=http://${fakeFrogApi}/npm/foo
|
||||
//${fakeFrogApi}/npm/foo/:_authToken=
|
||||
@bar:registry=http://${fakeFrogApi}/npm/bar
|
||||
//${fakeFrogApi}/npm/bar/:_authToken=
|
||||
|
||||
EOF`;
|
||||
expect(coderScript.script).toContain(npmrcStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'jf npmc --global --repo-resolve "global"',
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured npm',
|
||||
);
|
||||
});
|
||||
|
||||
it("generates a pip config with extra-indexes", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
pypi: ["global", "foo", "bar"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const pipStanza = `cat << EOF > ~/.pip/pip.conf
|
||||
[global]
|
||||
index-url = https://${user}:@${fakeFrogApi}/pypi/global/simple
|
||||
extra-index-url =
|
||||
https://${user}:@${fakeFrogApi}/pypi/foo/simple
|
||||
https://${user}:@${fakeFrogApi}/pypi/bar/simple
|
||||
|
||||
EOF`;
|
||||
expect(coderScript.script).toContain(pipStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'jf pipc --global --repo-resolve "global"',
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured pypi',
|
||||
);
|
||||
});
|
||||
|
||||
it("registers multiple docker repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
docker: ["foo.jfrog.io", "bar.jfrog.io", "baz.jfrog.io"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const dockerStanza = ["foo", "bar", "baz"]
|
||||
.map((r) => `register_docker "${r}.jfrog.io"`)
|
||||
.join("\n");
|
||||
expect(coderScript.script).toContain(dockerStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured docker',
|
||||
);
|
||||
});
|
||||
|
||||
it("sets goproxy with multiple repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
go: ["foo", "bar", "baz"],
|
||||
}),
|
||||
});
|
||||
const proxyEnv = findResourceInstance(state, "coder_env", "goproxy");
|
||||
const proxies = ["foo", "bar", "baz"]
|
||||
.map((r) => `https://${user}:@${fakeFrogApi}/go/${r}`)
|
||||
.join(",");
|
||||
expect(proxyEnv.value).toEqual(proxies);
|
||||
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
expect(coderScript.script).toContain(
|
||||
'jf goc --global --repo-resolve "foo"',
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured go',
|
||||
);
|
||||
});
|
||||
|
||||
it("generates a conda config with multiple repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
conda: ["conda-main", "conda-secondary", "conda-local"],
|
||||
}),
|
||||
});
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
const condaStanza = `cat << EOF > ~/.condarc
|
||||
channels:
|
||||
- https://${user}:@${fakeFrogApi}/conda/conda-main
|
||||
- https://${user}:@${fakeFrogApi}/conda/conda-secondary
|
||||
- https://${user}:@${fakeFrogApi}/conda/conda-local
|
||||
- defaults
|
||||
ssl_verify: true
|
||||
|
||||
EOF`;
|
||||
expect(coderScript.script).toContain(condaStanza);
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured conda',
|
||||
);
|
||||
});
|
||||
it("generates a maven settings.xml with multiple repos", async () => {
|
||||
const state = await runTerraformApply<TestVariables>(import.meta.dir, {
|
||||
agent_id: "some-agent-id",
|
||||
jfrog_url: fakeFrogUrl,
|
||||
package_managers: JSON.stringify({
|
||||
maven: ["central", "snapshots", "local"],
|
||||
}),
|
||||
});
|
||||
|
||||
const coderScript = findResourceInstance(state, "coder_script");
|
||||
|
||||
expect(coderScript.script).toContain("jf mvnc --global");
|
||||
expect(coderScript.script).toContain('--server-id-resolve="0"');
|
||||
expect(coderScript.script).toContain('--repo-resolve-releases "central"');
|
||||
expect(coderScript.script).toContain('--repo-resolve-snapshots "central"');
|
||||
expect(coderScript.script).toContain('--server-id-deploy="0"');
|
||||
expect(coderScript.script).toContain('--repo-deploy-releases "central"');
|
||||
expect(coderScript.script).toContain('--repo-deploy-snapshots "central"');
|
||||
|
||||
expect(coderScript.script).toContain("<servers>");
|
||||
expect(coderScript.script).toContain("<id>central</id>");
|
||||
expect(coderScript.script).toContain("<id>snapshots</id>");
|
||||
expect(coderScript.script).toContain("<id>local</id>");
|
||||
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/central</url>",
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/snapshots</url>",
|
||||
);
|
||||
expect(coderScript.script).toContain(
|
||||
"<url>http://localhost:8081/artifactory/local</url>",
|
||||
);
|
||||
|
||||
expect(coderScript.script).toContain(
|
||||
'if [ -z "YES" ]; then\n not_configured maven',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -163,6 +163,13 @@ resource "coder_script" "jfrog" {
|
||||
}
|
||||
))
|
||||
run_on_start = true
|
||||
|
||||
lifecycle {
|
||||
precondition {
|
||||
condition = data.coder_external_auth.jfrog.access_token != ""
|
||||
error_message = "JFrog access token is empty. Please authenticate with JFrog using external auth."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "coder_env" "jfrog_ide_url" {
|
||||
|
||||
@@ -18,7 +18,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -31,7 +31,7 @@ module "kiro" {
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -47,7 +47,7 @@ The following example configures Kiro to use the GitHub MCP server with authenti
|
||||
module "kiro" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/kiro/coder"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
@@ -60,6 +60,7 @@ module "kiro" {
|
||||
"type" : "http"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -17,11 +17,6 @@ run "default_output" {
|
||||
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" {
|
||||
@@ -53,54 +48,6 @@ run "folder_and_open_recent" {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -26,7 +26,10 @@ describe("kiro", async () => {
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "kiro",
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
@@ -55,47 +58,6 @@ describe("kiro", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("custom slug and display_name", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
slug: "kiro-ai",
|
||||
display_name: "Kiro AI IDE",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "kiro",
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.slug).toBe("kiro-ai");
|
||||
expect(coder_app?.instances[0].attributes.display_name).toBe("Kiro AI IDE");
|
||||
});
|
||||
|
||||
it("sets order", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "5",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "kiro",
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(5);
|
||||
});
|
||||
|
||||
it("sets group", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
group: "AI IDEs",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "kiro",
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.group).toBe("AI IDEs");
|
||||
});
|
||||
|
||||
it("writes ~/.kiro/settings/mcp.json when mcp provided", async () => {
|
||||
const id = await runContainer("alpine");
|
||||
try {
|
||||
|
||||
@@ -38,18 +38,6 @@ variable "group" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the app."
|
||||
default = "kiro"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app."
|
||||
default = "Kiro IDE"
|
||||
}
|
||||
|
||||
variable "mcp" {
|
||||
type = string
|
||||
description = "JSON-encoded string to configure MCP servers for Kiro. When set, writes ~/.kiro/settings/mcp.json."
|
||||
@@ -63,26 +51,21 @@ locals {
|
||||
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
|
||||
}
|
||||
|
||||
resource "coder_app" "kiro" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/kiro.svg"
|
||||
slug = var.slug
|
||||
display_name = var.display_name
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = join("", [
|
||||
"kiro://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
coder_app_icon = "/icon/kiro.svg"
|
||||
coder_app_slug = "kiro-ai"
|
||||
coder_app_display_name = "Kiro AI IDE"
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
protocol = "kiro"
|
||||
}
|
||||
|
||||
resource "coder_script" "kiro_mcp" {
|
||||
@@ -102,6 +85,6 @@ resource "coder_script" "kiro_mcp" {
|
||||
}
|
||||
|
||||
output "kiro_url" {
|
||||
value = coder_app.kiro.url
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "Kiro IDE URL."
|
||||
}
|
||||
}
|
||||
@@ -2,23 +2,25 @@
|
||||
display_name: mux
|
||||
description: Coding Agent Multiplexer - Run multiple AI agents in parallel
|
||||
icon: ../../../../.icons/mux.svg
|
||||
verified: false
|
||||
verified: true
|
||||
tags: [ai, agents, development, multiplexer]
|
||||
---
|
||||
|
||||
# mux
|
||||
|
||||
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.
|
||||
Automatically install and run [mux](https://github.com/coder/mux) in a Coder workspace. By default, the module installs `mux@next` from npm (with a fallback to downloading the npm tarball if npm is unavailable). mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces.
|
||||
|
||||
```tf
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Parallel Agent Execution**: Run multiple AI agents simultaneously on different tasks
|
||||
@@ -35,7 +37,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -46,7 +48,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.main.id
|
||||
# Default is "latest"; set to a specific version to pin
|
||||
install_version = "0.4.0"
|
||||
@@ -59,7 +61,7 @@ module "mux" {
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.main.id
|
||||
port = 8080
|
||||
}
|
||||
@@ -73,7 +75,7 @@ Run an existing copy of mux if found, otherwise install from npm:
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
agent_id = coder_agent.main.id
|
||||
use_cached = true
|
||||
}
|
||||
@@ -87,7 +89,7 @@ Run without installing from the network (requires mux to be pre-installed):
|
||||
module "mux" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/mux/coder"
|
||||
version = "1.0.2"
|
||||
version = "1.0.4"
|
||||
agent_id = coder_agent.main.id
|
||||
install = false
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
display_name: VSCode Desktop Core
|
||||
display_name: Coder VSCode Desktop Core
|
||||
description: Building block for modules that need to link to an external VSCode-based IDE
|
||||
icon: ../../../../.icons/coder.svg
|
||||
verified: true
|
||||
@@ -11,20 +11,20 @@ tags: [internal, library]
|
||||
> [!CAUTION]
|
||||
> We do not recommend using this module directly. Instead, please consider using one of our [Desktop IDE modules](https://registry.coder.com/modules?search=tag%3Aide).
|
||||
|
||||
The VSCode Desktop Core module is a building block for modules that need to expose access to VSCode-based IDEs. It is intended primarily to be used as a library to create modules for VSCode-based IDEs.
|
||||
The VSCode Desktop Core module is a building block for modules that need to expose access to VSCode-based IDEs. It is intended primarily for internal use by Coder to create modules for VSCode-based IDEs.
|
||||
|
||||
```tf
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
coder_app_icon = "/icon/code.svg"
|
||||
coder_app_slug = "vscode"
|
||||
coder_app_display_name = "VS Code Desktop"
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
web_app_icon = "/icon/code.svg"
|
||||
web_app_slug = "vscode"
|
||||
web_app_display_name = "VS Code Desktop"
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
|
||||
@@ -10,9 +10,11 @@ const appName = "vscode-desktop";
|
||||
|
||||
const defaultVariables = {
|
||||
agent_id: "foo",
|
||||
coder_app_icon: "/icon/code.svg",
|
||||
coder_app_slug: "vscode",
|
||||
coder_app_display_name: "VS Code Desktop",
|
||||
|
||||
web_app_icon: "/icon/code.svg",
|
||||
web_app_slug: "vscode",
|
||||
web_app_display_name: "VS Code Desktop",
|
||||
|
||||
protocol: "vscode",
|
||||
};
|
||||
|
||||
@@ -21,80 +23,115 @@ describe("vscode-desktop-core", async () => {
|
||||
|
||||
testRequiredVariables(import.meta.dir, defaultVariables);
|
||||
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, defaultVariables);
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
describe("coder_app", () => {
|
||||
describe("IDE URI attributes", () => {
|
||||
it("default output", async () => {
|
||||
const state = await runTerraformApply(
|
||||
import.meta.dir,
|
||||
defaultVariables,
|
||||
);
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === appName,
|
||||
);
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === appName,
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBeNull();
|
||||
});
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
|
||||
...defaultVariables,
|
||||
...defaultVariables,
|
||||
});
|
||||
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder and open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
open_recent: "true",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder but not open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
openRecent: "false",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
open_recent: "true",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
it("sets custom slug and display_name", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, defaultVariables);
|
||||
|
||||
it("adds folder and open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
open_recent: "true",
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === appName,
|
||||
);
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds folder but not open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
openRecent: "false",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("adds open_recent", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
open_recent: "true",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
expect(state.outputs.ide_uri.value).toBe(
|
||||
`${defaultVariables.protocol}://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN`,
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
coder_app_order: "22",
|
||||
...defaultVariables,
|
||||
expect(coder_app?.instances[0].attributes.slug).toBe(
|
||||
defaultVariables.web_app_slug,
|
||||
);
|
||||
expect(coder_app?.instances[0].attributes.display_name).toBe(
|
||||
defaultVariables.web_app_display_name,
|
||||
);
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === appName,
|
||||
);
|
||||
it("sets order", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
web_app_order: "5",
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
...defaultVariables,
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === appName,
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(5);
|
||||
});
|
||||
|
||||
it("sets group", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
web_app_group: "web-app-group",
|
||||
|
||||
...defaultVariables,
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === appName,
|
||||
);
|
||||
|
||||
expect(coder_app?.instances[0].attributes.group).toBe("web-app-group");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,31 +28,31 @@ variable "open_recent" {
|
||||
|
||||
variable "protocol" {
|
||||
type = string
|
||||
description = "The URI protocol for the IDE."
|
||||
description = "The URI protocol the IDE."
|
||||
}
|
||||
|
||||
variable "coder_app_icon" {
|
||||
variable "web_app_icon" {
|
||||
type = string
|
||||
description = "The icon of the coder_app."
|
||||
}
|
||||
|
||||
variable "coder_app_slug" {
|
||||
variable "web_app_slug" {
|
||||
type = string
|
||||
description = "The slug of the coder_app."
|
||||
}
|
||||
|
||||
variable "coder_app_display_name" {
|
||||
variable "web_app_display_name" {
|
||||
type = string
|
||||
description = "The display name of the coder_app."
|
||||
}
|
||||
|
||||
variable "coder_app_order" {
|
||||
variable "web_app_order" {
|
||||
type = number
|
||||
description = "The order of the coder_app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "coder_app_group" {
|
||||
variable "web_app_group" {
|
||||
type = string
|
||||
description = "The group of the coder_app."
|
||||
default = null
|
||||
@@ -65,25 +65,38 @@ resource "coder_app" "vscode-desktop" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
|
||||
icon = var.coder_app_icon
|
||||
slug = var.coder_app_slug
|
||||
display_name = var.coder_app_display_name
|
||||
icon = var.web_app_icon
|
||||
slug = var.web_app_slug
|
||||
display_name = var.web_app_display_name
|
||||
|
||||
order = var.coder_app_order
|
||||
group = var.coder_app_group
|
||||
order = var.web_app_order
|
||||
group = var.web_app_group
|
||||
|
||||
# While the call to "join" is not strictly necessary, it makes the URL more readable.
|
||||
url = join("", [
|
||||
"${var.protocol}://coder.coder-remote/open",
|
||||
var.protocol,
|
||||
"://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
|
||||
/*
|
||||
url = join("", [
|
||||
"vscode://coder.coder-remote/open",
|
||||
"?owner=${data.coder_workspace_owner.me.name}",
|
||||
"&workspace=${data.coder_workspace.me.name}",
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=${data.coder_workspace.me.access_url}",
|
||||
# NOTE: There is a protocol whitelist for the token replacement, so this will only work with the protocols hardcoded in the front-end.
|
||||
# (https://github.com/coder/coder/blob/6ba4b5bbc95e2e528d7f5b1e31fffa200ae1a6db/site/src/modules/apps/apps.ts#L18)
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
*/
|
||||
}
|
||||
|
||||
output "ide_uri" {
|
||||
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "vscode" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-desktop/coder"
|
||||
version = "1.1.2"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "vscode" {
|
||||
module "vscode" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/vscode-desktop/coder"
|
||||
version = "1.1.2"
|
||||
version = "1.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ describe("vscode-desktop", async () => {
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "vscode",
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
@@ -71,19 +74,4 @@ describe("vscode-desktop", async () => {
|
||||
"vscode://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "vscode",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,33 +38,24 @@ variable "group" {
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
resource "coder_app" "vscode" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/code.svg"
|
||||
slug = "vscode"
|
||||
display_name = "VS Code Desktop"
|
||||
order = var.order
|
||||
group = var.group
|
||||
agent_id = var.agent_id
|
||||
|
||||
url = join("", [
|
||||
"vscode://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
coder_app_icon = "/icon/code.svg"
|
||||
coder_app_slug = "vscode"
|
||||
coder_app_display_name = "VS Code Desktop"
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
protocol = "vscode"
|
||||
}
|
||||
|
||||
output "vscode_url" {
|
||||
value = coder_app.vscode.url
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "VS Code Desktop URL."
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
}
|
||||
```
|
||||
@@ -29,7 +29,7 @@ module "windsurf" {
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
}
|
||||
@@ -45,7 +45,7 @@ The following example configures Windsurf to use the GitHub MCP server with auth
|
||||
module "windsurf" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/windsurf/coder"
|
||||
version = "1.2.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.main.id
|
||||
folder = "/home/coder/project"
|
||||
mcp = jsonencode({
|
||||
@@ -58,6 +58,7 @@ module "windsurf" {
|
||||
"type" : "http"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -26,7 +26,10 @@ describe("windsurf", async () => {
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "windsurf",
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
@@ -76,21 +79,6 @@ describe("windsurf", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: 22,
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "windsurf",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
|
||||
it("writes ~/.codeium/windsurf/mcp_config.json when mcp provided", async () => {
|
||||
const id = await runContainer("alpine");
|
||||
try {
|
||||
|
||||
@@ -16,7 +16,7 @@ variable "agent_id" {
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to open in Cursor IDE."
|
||||
description = "The folder to open in Windsurf Editor."
|
||||
default = ""
|
||||
}
|
||||
|
||||
@@ -63,26 +63,21 @@ locals {
|
||||
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
|
||||
}
|
||||
|
||||
resource "coder_app" "windsurf" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = "/icon/windsurf.svg"
|
||||
slug = var.slug
|
||||
display_name = var.display_name
|
||||
order = var.order
|
||||
group = var.group
|
||||
url = join("", [
|
||||
"windsurf://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
|
||||
coder_app_icon = "/icon/windsurf.svg"
|
||||
coder_app_slug = "windsurf"
|
||||
coder_app_display_name = "Windsurf Editor"
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
protocol = "windsurf"
|
||||
}
|
||||
|
||||
resource "coder_script" "windsurf_mcp" {
|
||||
@@ -102,6 +97,6 @@ resource "coder_script" "windsurf_mcp" {
|
||||
}
|
||||
|
||||
output "windsurf_url" {
|
||||
value = coder_app.windsurf.url
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "Windsurf Editor URL."
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,10 @@ describe("positron-desktop", async () => {
|
||||
);
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "positron",
|
||||
(res) =>
|
||||
res.type === "coder_app" &&
|
||||
res.module === "module.vscode-desktop-core" &&
|
||||
res.name === "vscode-desktop",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
@@ -70,19 +73,4 @@ describe("positron-desktop", async () => {
|
||||
"positron://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
});
|
||||
|
||||
const coder_app = state.resources.find(
|
||||
(res) => res.type === "coder_app" && res.name === "positron",
|
||||
);
|
||||
|
||||
expect(coder_app).not.toBeNull();
|
||||
expect(coder_app?.instances.length).toBe(1);
|
||||
expect(coder_app?.instances[0].attributes.order).toBe(22);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,10 +9,6 @@ terraform {
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
icon_url = "/icon/positron.svg"
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
@@ -42,33 +38,39 @@ variable "group" {
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "slug" {
|
||||
type = string
|
||||
description = "The slug of the app."
|
||||
default = "cursor"
|
||||
}
|
||||
|
||||
variable "display_name" {
|
||||
type = string
|
||||
description = "The display name of the app."
|
||||
default = "Cursor Desktop"
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
resource "coder_app" "positron" {
|
||||
agent_id = var.agent_id
|
||||
external = true
|
||||
icon = local.icon_url
|
||||
slug = "positron"
|
||||
display_name = "Positron Desktop"
|
||||
order = var.order
|
||||
group = var.group
|
||||
module "vscode-desktop-core" {
|
||||
source = "registry.coder.com/coder/vscode-desktop-core/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
url = join("", [
|
||||
"positron://coder.coder-remote/open",
|
||||
"?owner=",
|
||||
data.coder_workspace_owner.me.name,
|
||||
"&workspace=",
|
||||
data.coder_workspace.me.name,
|
||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
||||
var.open_recent ? "&openRecent" : "",
|
||||
"&url=",
|
||||
data.coder_workspace.me.access_url,
|
||||
"&token=$SESSION_TOKEN",
|
||||
])
|
||||
agent_id = var.agent_id
|
||||
|
||||
coder_app_icon = "/icon/positron.svg"
|
||||
coder_app_slug = var.slug
|
||||
coder_app_display_name = var.display_name
|
||||
coder_app_order = var.order
|
||||
coder_app_group = var.group
|
||||
|
||||
folder = var.folder
|
||||
open_recent = var.open_recent
|
||||
protocol = "positron"
|
||||
}
|
||||
|
||||
output "positron_url" {
|
||||
value = coder_app.positron.url
|
||||
value = module.vscode-desktop-core.ide_uri
|
||||
description = "Positron Desktop URL."
|
||||
}
|
||||
|
||||
@@ -112,6 +112,8 @@ type JsonValue =
|
||||
| { [key: string]: JsonValue };
|
||||
|
||||
type TerraformStateResource = {
|
||||
module: string;
|
||||
mode: string;
|
||||
type: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
|
||||
Reference in New Issue
Block a user