Compare commits

..

16 Commits

Author SHA1 Message Date
DevCats 8feb9a793e chore: update module tags
Co-authored-by: Atif Ali <atif@coder.com>
2025-12-02 13:48:50 -06:00
DevCats 36c06b5981 chore: update module description
Co-authored-by: Atif Ali <atif@coder.com>
2025-12-02 13:48:36 -06:00
DevCats 3bfb080a75 chore: shorten module display name
Co-authored-by: Atif Ali <atif@coder.com>
2025-12-02 13:48:12 -06:00
DevCats b1d0d70754 chore: set verified to true
Co-authored-by: Atif Ali <atif@coder.com>
2025-12-02 13:47:46 -06:00
DevCats 9afa0be06c Merge branch 'main' into cat/antigravity-module 2025-12-02 14:38:04 -05:00
DevelopmentCats f1d210964c refactor(antigravity): migrate coder_app to vscode-desktop-core module 2025-12-02 13:36:00 -06:00
Atif Ali d21db0d220 fix(jfrog-oauth): fail when access_token is empty (#574)
## Summary

Fixes #72 - The `jfrog-oauth` module now fails with a clear error
message when the JFrog access token is empty, instead of silently
creating configurations with empty tokens.

## Changes

### 1. Added Precondition Validation (`main.tf`)

```hcl
lifecycle {
  precondition {
    condition     = data.coder_external_auth.jfrog.access_token != ""
    error_message = "JFrog access token is empty. Please authenticate with JFrog using external auth."
  }
}
```

This ensures the module fails at **plan time** with a clear error when
users haven't authenticated via external auth.

### 2. Replaced `main.test.ts` with `jfrog-oauth.tftest.hcl`

**Why we removed the TypeScript tests:**

The TypeScript tests used `runTerraformApply()` which runs `terraform
apply` directly. This approach **cannot mock data sources** like
`coder_external_auth`. The Coder provider returns empty strings for
tokens by default when running outside a real Coder workspace.

With our new precondition, the TypeScript tests would always fail
because:
1. `terraform apply` runs → empty `access_token` from mock provider
2. Precondition check fails → "JFrog access token is empty"
3. Test fails before any assertions run

**The solution:** Terraform's native `.tftest.hcl` format supports
`override_data` blocks that can properly mock data sources:

```hcl
override_data {
  target = data.coder_external_auth.jfrog
  values = {
    access_token = "valid-token-value"  # or "" to test failure
  }
}
```

### 3. Comprehensive Test Coverage

The new `jfrog-oauth.tftest.hcl` includes **12 tests** (up from 7):

| Test | What it validates |
|------|------------------|
| `test_required_vars` | Basic module works with required variables |
| `test_empty_access_token_fails` | **NEW:** Precondition rejects empty
tokens |
| `test_valid_access_token_succeeds` | Module works with valid token |
| `test_jfrog_url_validation` | **NEW:** URL must start with http(s)://
|
| `test_username_field_validation` | **NEW:** Must be "email" or
"username" |
| `test_with_npm_package_manager` | NPM config with scoped repos (script
content) |
| `test_configure_code_server` | **NEW:** IDE env vars created when
enabled |
| `test_go_proxy_env` | GOPROXY env value with multiple repos |
| `test_pypi_package_manager` | pip.conf with extra-index-url |
| `test_docker_package_manager` | register_docker commands for all repos
|
| `test_conda_package_manager` | .condarc channels configuration |
| `test_maven_package_manager` | settings.xml with servers and repos |

All package manager tests use `strcontains()` to verify the actual
script content matches expected configuration formats.

## Test Limitations (Acknowledged)

The tests verify **template rendering** but not **runtime execution**:

|  What we test |  What we don't test |
|----------------|----------------------|
| Configuration file formats | Script syntax errors at runtime |
| Variable interpolation | JFrog CLI compatibility |
| Precondition validation | Actual JFrog authentication |
| Script contains expected content | Commands execute successfully |

**Rationale:** The original TypeScript tests also only checked script
content (`toContain()`), not execution. Full execution testing would
require a mock JFrog server, which adds significant complexity for
limited benefit. The script is straightforward bash that configures
files and runs CLI commands.

## Testing

```bash
cd registry/coder/modules/jfrog-oauth
terraform test
# Success! 12 passed, 0 failed.
```

_Generated with [mux](https://github.com/coder/mux)_
2025-12-02 13:17:39 -06:00
Atif Ali c5d31eb6b2 Merge branch 'main' into cat/antigravity-module 2025-12-02 23:44:23 +05:00
Atif Ali 392f6b120a fix(mux): move image to shared folder and fix path (#573)
## Summary
Fix the mux module image rendering by moving the image to the shared
images folder and updating the path.

## Changes
- Move `product-hero.webp` from `registry/coder/modules/mux/.images/` to
`registry/coder/.images/`
- Rename to `mux-product-hero.webp` for consistency with other module
images (e.g., `amazon-q.png`, `amazon-dcv-windows.png`)
- Update README path from `.images/product-hero.webp` to
`../../.images/mux-product-hero.webp`

This follows the same pattern as other modules like `amazon-q`,
`amazon-dcv-windows`, and `jetbrains-gateway` which all use
`../../.images/` paths.
2025-12-02 12:42:05 -06:00
Atif Ali 7de72fc7cc feat(mux): add GitHub link, set verified, bump to v1.0.3 (#572) 2025-12-02 23:03:49 +05:00
dependabot[bot] 3e1ddbf624 chore(deps): bump crate-ci/typos from 1.39.2 to 1.40.0 in the github-actions group (#570)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-01 14:15:39 +05:00
Phorcys 0021a9fe7d feat: update vscode-based desktop IDE modules to use vscode-desktop-core (#279) 2025-11-27 17:35:40 +05:00
DevCats b8442c27c5 Merge branch 'main' into cat/antigravity-module 2025-11-21 17:03:19 -05:00
DevelopmentCats cb4c267480 fix: increase timeout for antigravity tests to prevent timeouts 2025-11-21 15:59:12 -06:00
DevelopmentCats 7b8f8f7bf6 fix: update MCP configuration path in Antigravity module 2025-11-21 15:52:04 -06:00
DevelopmentCats 43d8483518 feat: add Antigravity IDE module with Terraform configuration and tests 2025-11-19 11:44:59 -06:00
29 changed files with 991 additions and 588 deletions
+1 -1
View File
@@ -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:
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",
openRecent: "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);
});
+104
View File
@@ -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."
}
+4 -3
View File
@@ -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"
}
}
})
}
+4 -16
View File
@@ -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 {
+17 -22
View File
@@ -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."
}
}
+2 -2
View File
@@ -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" {
+4 -3
View File
@@ -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
+4 -42
View File
@@ -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 {
+17 -34
View File
@@ -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."
}
}
+10 -8
View File
@@ -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
}
```
![mux](../../.images/mux-product-hero.webp)
## 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);
});
});
+15 -24
View File
@@ -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."
}
}
+4 -3
View File
@@ -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"
}
}
})
}
+4 -16
View File
@@ -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 {
+18 -23
View File
@@ -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);
});
});
+27 -25
View File
@@ -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."
}
+2
View File
@@ -112,6 +112,8 @@ type JsonValue =
| { [key: string]: JsonValue };
type TerraformStateResource = {
module: string;
mode: string;
type: string;
name: string;
provider: string;