Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .icons/trae-cn.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 69 additions & 0 deletions registry/coder/modules/trae-cn/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
display_name: Trae CN
description: Add a one-click button to launch Trae CN
icon: ../../../../.icons/trae-cn.png
verified: false
tags: [ide, trae, ai]
---

# Trae CN

Add a button to open any workspace with a single click in Trae CN.

Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder).

```tf
module "trae_cn" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/trae-cn/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
}
```

## Examples

### Open in a specific directory

```tf
module "trae_cn" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/trae-cn/coder"
version = "1.0.0"
agent_id = coder_agent.main.id
folder = "/home/coder/project"
}
```

### Configure MCP servers for Trae CN

Provide a JSON-encoded string via the `mcp` input. When set, the module writes the value to `.trae/mcp.json` in `folder`, or `$HOME/.trae/mcp.json` when `folder` is not set.

If your MCP configuration includes credentials, either add `.trae/mcp.json` to the project's `.gitignore`, or set `mcp_config_path` to a path outside the repository.

The following example configures Trae CN 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 "trae_cn" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/trae-cn/coder"
version = "1.0.0"
agent_id = coder_agent.main.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"
}
```
137 changes: 137 additions & 0 deletions registry/coder/modules/trae-cn/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { describe, expect, it } from "bun:test";
import {
findResourceInstance,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
} from "~test";

const encodeBase64 = (value: string) => Buffer.from(value).toString("base64");

describe("trae-cn", 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.trae_cn_url.value).toBe(
"trae-cn://coder.coder-remote/open?owner=default&workspace=default&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);

const coderApp = state.resources.find(
(res) =>
res.type === "coder_app" &&
res.module === "module.vscode-desktop-core" &&
res.name === "vscode-desktop",
);

expect(coderApp).not.toBeNull();
expect(coderApp?.instances.length).toBe(1);
expect(coderApp?.instances[0].attributes.icon).toBe("/icon/trae-cn.png");
expect(coderApp?.instances[0].attributes.slug).toBe("trae-cn");
expect(coderApp?.instances[0].attributes.display_name).toBe("Trae CN");
expect(coderApp?.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.trae_cn_url.value).toBe(
"trae-cn://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.trae_cn_url.value).toBe(
"trae-cn://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.trae_cn_url.value).toBe(
"trae-cn://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.trae_cn_url.value).toBe(
"trae-cn://coder.coder-remote/open?owner=default&workspace=default&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN",
);
});

it("sets order", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
order: "22",
});

const coderApp = state.resources.find(
(res) =>
res.type === "coder_app" &&
res.module === "module.vscode-desktop-core" &&
res.name === "vscode-desktop",
);

expect(coderApp).not.toBeNull();
expect(coderApp?.instances.length).toBe(1);
expect(coderApp?.instances[0].attributes.order).toBe(22);
});

it("adds MCP script for folder/.trae/mcp.json when mcp and folder provided", async () => {
const mcp = JSON.stringify({
mcpServers: { demo: { url: "http://localhost:1234" } },
});
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/tmp/project",
mcp,
});
const script = findResourceInstance(state, "coder_script", "trae_cn_mcp");

expect(script.display_name).toBe("Trae CN MCP");
expect(script.icon).toBe("/icon/trae-cn.png");
expect(script.script).toContain(encodeBase64(mcp));
expect(script.script).toContain(
encodeBase64("/tmp/project/.trae/mcp.json"),
);
});

it("adds MCP script for custom mcp_config_path when provided", async () => {
const mcp = JSON.stringify({
mcpServers: { demo: { url: "http://localhost:1234" } },
});
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
folder: "/tmp/project",
mcp,
mcp_config_path: "$HOME/.config/trae/mcp.json",
});
const script = findResourceInstance(state, "coder_script", "trae_cn_mcp");

expect(script.script).toContain(encodeBase64(mcp));
expect(script.script).toContain(
encodeBase64("$HOME/.config/trae/mcp.json"),
);
});
});
124 changes: 124 additions & 0 deletions registry/coder/modules/trae-cn/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
terraform {
required_version = ">= 1.0"

required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}

variable "agent_id" {
description = "The ID of a Coder agent."
type = string
}

variable "folder" {
description = "The folder to open in Trae CN."
type = string
default = ""
}

variable "open_recent" {
description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open."
type = bool
default = false
}

variable "order" {
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."
type = number
default = null
}

variable "group" {
description = "The name of a group that this app belongs to."
type = string
default = null
}

variable "slug" {
description = "The slug of the app."
type = string
default = "trae-cn"
}

variable "display_name" {
description = "The display name of the app."
type = string
default = "Trae CN"
}

variable "mcp" {
description = "JSON-encoded string to configure MCP servers for Trae CN. When set, writes mcp_config_path."
type = string
default = ""
}

variable "mcp_config_path" {
description = "Path to write the Trae CN MCP configuration. Defaults to folder/.trae/mcp.json when folder is set, otherwise $HOME/.trae/mcp.json."
type = string
default = ""
}

data "coder_workspace" "me" {}

data "coder_workspace_owner" "me" {}

locals {
mcp_b64 = var.mcp != "" ? base64encode(var.mcp) : ""
mcp_config_path = (
var.mcp_config_path != ""
? var.mcp_config_path
: var.folder != ""
? "${var.folder}/.trae/mcp.json"
: "$HOME/.trae/mcp.json"
)
mcp_config_path_b64 = base64encode(local.mcp_config_path)
}

module "vscode-desktop-core" {
source = "registry.coder.com/coder/vscode-desktop-core/coder"
version = "1.0.2"

agent_id = var.agent_id

coder_app_icon = "/icon/trae-cn.png"
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 = "trae-cn"
}

resource "coder_script" "trae_cn_mcp" {
count = var.mcp != "" ? 1 : 0
agent_id = var.agent_id
display_name = "Trae CN MCP"
icon = "/icon/trae-cn.png"
run_on_start = true
start_blocks_login = false
script = <<-EOT
#!/bin/sh
set -eu

mcp_config_path="$(echo -n '${local.mcp_config_path_b64}' | base64 -d)"
case "$mcp_config_path" in
"\$HOME/"*) mcp_config_path="$HOME/$${mcp_config_path#\$HOME/}" ;;
"~/"*) mcp_config_path="$HOME/$${mcp_config_path#~/}" ;;
esac

mkdir -p "$(dirname "$mcp_config_path")"
echo -n '${local.mcp_b64}' | base64 -d > "$mcp_config_path"
chmod 600 "$mcp_config_path"
EOT
}

output "trae_cn_url" {
description = "Trae CN URL."
value = module.vscode-desktop-core.ide_uri
}
Loading
Loading