From f10f5a44032e9cc37ec2e30b31727fbdb2bc578e Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Wed, 16 Apr 2025 02:44:34 +0000 Subject: [PATCH] chore: add sample data --- .gitignore | 3 +- cmd/readmevalidation/coderResources.go | 7 + cmd/readmevalidation/main.go | 8 +- cmd/readmevalidation/repoStructure.go | 6 + registry/coder/modules/claude-code/README.md | 113 ++++++++++++ registry/coder/modules/claude-code/main.tf | 170 +++++++++++++++++++ registry/coder/modules/cursor/README.md | 36 ++++ registry/coder/modules/cursor/main.test.ts | 88 ++++++++++ registry/coder/modules/cursor/main.tf | 62 +++++++ 9 files changed, 488 insertions(+), 5 deletions(-) create mode 100644 registry/coder/modules/claude-code/README.md create mode 100644 registry/coder/modules/claude-code/main.tf create mode 100644 registry/coder/modules/cursor/README.md create mode 100644 registry/coder/modules/cursor/main.test.ts create mode 100644 registry/coder/modules/cursor/main.tf diff --git a/.gitignore b/.gitignore index 0f945ce6..2922fed7 100644 --- a/.gitignore +++ b/.gitignore @@ -135,5 +135,6 @@ dist .yarn/install-state.gz .pnp.* -# Script output +# Things needed for CI /readmevalidation +/readmevalidation-git diff --git a/cmd/readmevalidation/coderResources.go b/cmd/readmevalidation/coderResources.go index d4a6c39e..eda5fd6f 100644 --- a/cmd/readmevalidation/coderResources.go +++ b/cmd/readmevalidation/coderResources.go @@ -12,6 +12,13 @@ import ( "coder.com/coder-registry/cmd/github" ) +// dummyGitDirectory is the directory that a full version of the Registry will +// be cloned into during CI. The CI needs to use Git history to validate +// certain README files, and using the root branch itself (even though it's +// fully equivalent) has a risk of breaking other CI steps when switching +// branches. Better to make a full isolated copy and manipulate that. +const dummyGitDirectory = "./readmevalidation-git" + var supportedResourceTypes = []string{"modules", "templates"} type coderResourceFrontmatter struct { diff --git a/cmd/readmevalidation/main.go b/cmd/readmevalidation/main.go index d1c38e38..f58fc47d 100644 --- a/cmd/readmevalidation/main.go +++ b/cmd/readmevalidation/main.go @@ -15,6 +15,7 @@ import ( "coder.com/coder-registry/cmd/github" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/transport/http" "github.com/joho/godotenv" ) @@ -110,9 +111,9 @@ func main() { } fmt.Printf("------ got %d back\n", len(baseRefReadmeFiles)) - repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{ - DetectDotGit: false, - EnableDotGitCommonDir: false, + repo, err := git.PlainClone(dummyGitDirectory, true, &git.CloneOptions{ + URL: "https://github.com/coder/registry", + Auth: &http.BasicAuth{}, }) if err != nil { return err @@ -123,7 +124,6 @@ func main() { return err } activeBranchName := head.Name().Short() - fmt.Println("yeah...") tree, err := repo.Worktree() if err != nil { diff --git a/cmd/readmevalidation/repoStructure.go b/cmd/readmevalidation/repoStructure.go index 732a2a40..ebc88097 100644 --- a/cmd/readmevalidation/repoStructure.go +++ b/cmd/readmevalidation/repoStructure.go @@ -71,6 +71,12 @@ func validateRegistryDirectory() []error { problems = append(problems, err) } + mainTerraformPath := path.Join(dirPath, "main.tf") + _, err = os.Stat(mainTerraformPath) + if err != nil { + problems = append(problems, err) + } + for _, rType := range supportedResourceTypes { resourcePath := path.Join(dirPath, rType) if errs := validateCoderResourceSubdirectory(resourcePath); len(errs) != 0 { diff --git a/registry/coder/modules/claude-code/README.md b/registry/coder/modules/claude-code/README.md new file mode 100644 index 00000000..2cb506bf --- /dev/null +++ b/registry/coder/modules/claude-code/README.md @@ -0,0 +1,113 @@ +--- +display_name: Claude Code +description: Run Claude Code in your workspace +icon: ../.icons/claude.svg +verified: true +tags: [agent, claude-code] +--- + +# Claude Code + +Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) agent in your workspace to generate code and perform tasks. + +```tf +module "claude-code" { + source = "registry.coder.com/modules/claude-code/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_claude_code = true + claude_code_version = "latest" +} +``` + +## Prerequisites + +- Node.js and npm must be installed in your workspace to install Claude Code +- `screen` must be installed in your workspace to run Claude Code in the background +- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template + +The `codercom/oss-dogfood:latest` container image can be used for testing on container-based workspaces. + +## Examples + +### Run in the background and report tasks (Experimental) + +> This functionality is in early access as of Coder v2.21 and is still evolving. +> For now, we recommend testing it in a demo or staging environment, +> rather than deploying to production +> +> Learn more in [the Coder documentation](https://coder.com/docs/tutorials/ai-agents) +> +> Join our [Discord channel](https://discord.gg/coder) or +> [contact us](https://coder.com/contact) to get help or share feedback. + +Your workspace must have `screen` installed to use this. + +```tf +variable "anthropic_api_key" { + type = string + description = "The Anthropic API key" + sensitive = true +} + +module "coder-login" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/coder-login/coder" + version = "1.0.15" + agent_id = coder_agent.example.id +} + +data "coder_parameter" "ai_prompt" { + type = "string" + name = "AI Prompt" + default = "" + description = "Write a prompt for Claude Code" + mutable = true +} + +# Set the prompt and system prompt for Claude Code via environment variables +resource "coder_agent" "main" { + # ... + env = { + CODER_MCP_CLAUDE_API_KEY = var.anthropic_api_key # or use a coder_parameter + CODER_MCP_CLAUDE_TASK_PROMPT = data.coder_parameter.ai_prompt.value + CODER_MCP_APP_STATUS_SLUG = "claude-code" + CODER_MCP_CLAUDE_SYSTEM_PROMPT = <<-EOT + You are a helpful assistant that can help with code. + EOT + } +} + +module "claude-code" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/claude-code/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_claude_code = true + claude_code_version = "0.2.57" + + # Enable experimental features + experiment_use_screen = true + experiment_report_tasks = true +} +``` + +## Run standalone + +Run Claude Code as a standalone app in your workspace. This will install Claude Code and run it directly without using screen or any task reporting to the Coder UI. + +```tf +module "claude-code" { + source = "registry.coder.com/modules/claude-code/coder" + version = "1.0.31" + agent_id = coder_agent.example.id + folder = "/home/coder" + install_claude_code = true + claude_code_version = "latest" + + # Icon is not available in Coder v2.20 and below, so we'll use a custom icon URL + icon = "https://registry.npmmirror.com/@lobehub/icons-static-png/1.24.0/files/dark/claude-color.png" +} +``` diff --git a/registry/coder/modules/claude-code/main.tf b/registry/coder/modules/claude-code/main.tf new file mode 100644 index 00000000..349af17f --- /dev/null +++ b/registry/coder/modules/claude-code/main.tf @@ -0,0 +1,170 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.17" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +data "coder_workspace" "me" {} + +data "coder_workspace_owner" "me" {} + +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 "icon" { + type = string + description = "The icon to use for the app." + default = "/icon/claude.svg" +} + +variable "folder" { + type = string + description = "The folder to run Claude Code in." + default = "/home/coder" +} + +variable "install_claude_code" { + type = bool + description = "Whether to install Claude Code." + default = true +} + +variable "claude_code_version" { + type = string + description = "The version of Claude Code to install." + default = "latest" +} + +variable "experiment_use_screen" { + type = bool + description = "Whether to use screen for running Claude Code in the background." + default = false +} + +variable "experiment_report_tasks" { + type = bool + description = "Whether to enable task reporting." + default = false +} + +# Install and Initialize Claude Code +resource "coder_script" "claude_code" { + agent_id = var.agent_id + display_name = "Claude Code" + icon = var.icon + script = <<-EOT + #!/bin/bash + set -e + + # Function to check if a command exists + command_exists() { + command -v "$1" >/dev/null 2>&1 + } + + # Install Claude Code if enabled + if [ "${var.install_claude_code}" = "true" ]; then + if ! command_exists npm; then + echo "Error: npm is not installed. Please install Node.js and npm first." + exit 1 + fi + echo "Installing Claude Code..." + npm install -g @anthropic-ai/claude-code@${var.claude_code_version} + fi + + if [ "${var.experiment_report_tasks}" = "true" ]; then + echo "Configuring Claude Code to report tasks via Coder MCP..." + coder exp mcp configure claude-code ${var.folder} + fi + + # Run with screen if enabled + if [ "${var.experiment_use_screen}" = "true" ]; then + echo "Running Claude Code in the background..." + + # Check if screen is installed + if ! command_exists screen; then + echo "Error: screen is not installed. Please install screen manually." + exit 1 + fi + + touch "$HOME/.claude-code.log" + + # Ensure the screenrc exists + if [ ! -f "$HOME/.screenrc" ]; then + echo "Creating ~/.screenrc and adding multiuser settings..." | tee -a "$HOME/.claude-code.log" + echo -e "multiuser on\nacladd $(whoami)" > "$HOME/.screenrc" + fi + + if ! grep -q "^multiuser on$" "$HOME/.screenrc"; then + echo "Adding 'multiuser on' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log" + echo "multiuser on" >> "$HOME/.screenrc" + fi + + if ! grep -q "^acladd $(whoami)$" "$HOME/.screenrc"; then + echo "Adding 'acladd $(whoami)' to ~/.screenrc..." | tee -a "$HOME/.claude-code.log" + echo "acladd $(whoami)" >> "$HOME/.screenrc" + fi + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + + screen -U -dmS claude-code bash -c ' + cd ${var.folder} + claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log" + exec bash + ' + # Extremely hacky way to send the prompt to the screen session + # This will be fixed in the future, but `claude` was not sending MCP + # tasks when an initial prompt is provided. + screen -S claude-code -X stuff "$CODER_MCP_CLAUDE_TASK_PROMPT" + sleep 5 + screen -S claude-code -X stuff "^M" + else + # Check if claude is installed before running + if ! command_exists claude; then + echo "Error: Claude Code is not installed. Please enable install_claude_code or install it manually." + exit 1 + fi + fi + EOT + run_on_start = true +} + +resource "coder_app" "claude_code" { + slug = "claude-code" + display_name = "Claude Code" + agent_id = var.agent_id + command = <<-EOT + #!/bin/bash + set -e + + if [ "${var.experiment_use_screen}" = "true" ]; then + if screen -list | grep -q "claude-code"; then + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + echo "Attaching to existing Claude Code session." | tee -a "$HOME/.claude-code.log" + screen -xRR claude-code + else + echo "Starting a new Claude Code session." | tee -a "$HOME/.claude-code.log" + screen -S claude-code bash -c 'export LANG=en_US.UTF-8; export LC_ALL=en_US.UTF-8; claude --dangerously-skip-permissions | tee -a "$HOME/.claude-code.log"; exec bash' + fi + else + cd ${var.folder} + export LANG=en_US.UTF-8 + export LC_ALL=en_US.UTF-8 + claude + fi + EOT + icon = var.icon +} diff --git a/registry/coder/modules/cursor/README.md b/registry/coder/modules/cursor/README.md new file mode 100644 index 00000000..691526cb --- /dev/null +++ b/registry/coder/modules/cursor/README.md @@ -0,0 +1,36 @@ +--- +display_name: Cursor IDE +description: Add a one-click button to launch Cursor IDE +icon: ../.icons/cursor.svg +verified: true +tags: [ide, cursor, helper] +--- + +# Cursor IDE + +Add a button to open any workspace with a single click in Cursor IDE. + +Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder). + +```tf +module "cursor" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/cursor/coder" + version = "1.0.19" + agent_id = coder_agent.example.id +} +``` + +## Examples + +### Open in a specific directory + +```tf +module "cursor" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/modules/cursor/coder" + version = "1.0.19" + agent_id = coder_agent.example.id + folder = "/home/coder/project" +} +``` diff --git a/registry/coder/modules/cursor/main.test.ts b/registry/coder/modules/cursor/main.test.ts new file mode 100644 index 00000000..3c164698 --- /dev/null +++ b/registry/coder/modules/cursor/main.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "bun:test"; +import { + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "../test"; + +describe("cursor", 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.cursor_url.value).toBe( + "cursor://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 === "cursor", + ); + + 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.cursor_url.value).toBe( + "cursor://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.cursor_url.value).toBe( + "cursor://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.cursor_url.value).toBe( + "cursor://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.cursor_url.value).toBe( + "cursor://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 === "cursor", + ); + + expect(coder_app).not.toBeNull(); + expect(coder_app?.instances.length).toBe(1); + expect(coder_app?.instances[0].attributes.order).toBe(22); + }); +}); diff --git a/registry/coder/modules/cursor/main.tf b/registry/coder/modules/cursor/main.tf new file mode 100644 index 00000000..f350f942 --- /dev/null +++ b/registry/coder/modules/cursor/main.tf @@ -0,0 +1,62 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 0.23" + } + } +} + +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "folder" { + type = string + description = "The folder to open in Cursor 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 +} + +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +resource "coder_app" "cursor" { + agent_id = var.agent_id + external = true + icon = "/icon/cursor.svg" + slug = "cursor" + display_name = "Cursor Desktop" + order = var.order + 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", + ]) +} + +output "cursor_url" { + value = coder_app.cursor.url + description = "Cursor IDE Desktop URL." +}