mirror of
https://github.com/coder/registry.git
synced 2026-06-03 04:58:15 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c4a5184725 | |||
| 63d56eadc9 | |||
| 507b73a07e | |||
| 814f765313 | |||
| 92a154f54a | |||
| 7aa7dea5ad | |||
| 59b0472125 | |||
| 673caf2e95 | |||
| ab5ff4b4be | |||
| f5a68b500b | |||
| a5edad7f17 | |||
| fb657b875d | |||
| 016d4dc523 | |||
| c8d99cfba3 |
@@ -14,6 +14,7 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/deploy-registry.yaml"
|
||||
- "registry/**/templates/**"
|
||||
- "registry/**/README.md"
|
||||
- ".icons/**"
|
||||
|
||||
jobs:
|
||||
|
||||
+2
-2
@@ -89,7 +89,7 @@ Create `registry/[your-username]/README.md`:
|
||||
---
|
||||
display_name: "Your Name"
|
||||
bio: "Brief description of who you are and what you do"
|
||||
avatar_url: "./.images/avatar.png"
|
||||
avatar: "./.images/avatar.png"
|
||||
github: "your-username"
|
||||
linkedin: "https://www.linkedin.com/in/your-username" # Optional
|
||||
website: "https://yourwebsite.com" # Optional
|
||||
@@ -102,7 +102,7 @@ status: "community"
|
||||
Brief description of who you are and what you do.
|
||||
```
|
||||
|
||||
> **Note**: The `avatar_url` must point to `./.images/avatar.png` or `./.images/avatar.svg`.
|
||||
> **Note**: The `avatar` must point to `./.images/avatar.png` or `./.images/avatar.svg`.
|
||||
|
||||
### 2. Generate Module Files
|
||||
|
||||
|
||||
+1
-1
@@ -127,7 +127,7 @@ tags: ["tag1", "tag2"]
|
||||
```yaml
|
||||
display_name: "Your Name"
|
||||
bio: "Brief description of who you are and what you do"
|
||||
avatar_url: "./.images/avatar.png"
|
||||
avatar: "./.images/avatar.png"
|
||||
github: "username"
|
||||
linkedin: "https://www.linkedin.com/in/username" # Optional
|
||||
website: "https://yourwebsite.com" # Optional
|
||||
|
||||
@@ -336,12 +336,12 @@ func validateAllCoderResourceFilesOfType(resourceType string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info(context.Background(), "rocessing README files", "num_files", len(allReadmeFiles))
|
||||
logger.Info(context.Background(), "processing README files", "num_files", len(allReadmeFiles))
|
||||
resources, err := parseCoderResourceReadmeFiles(resourceType, allReadmeFiles)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info(context.Background(), "rocessed README files as valid Coder resources", "num_files", len(resources), "type", resourceType)
|
||||
logger.Info(context.Background(), "processed README files as valid Coder resources", "num_files", len(resources), "type", resourceType)
|
||||
|
||||
if err := validateCoderResourceRelativeURLs(resources); err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
display_name: "Jay Kumar"
|
||||
bio: "I'm a Software Engineer :)"
|
||||
avatar_url: "./.images/avatar.png"
|
||||
avatar: "./.images/avatar.jpeg"
|
||||
github: "35C4n0r"
|
||||
linkedin: "https://www.linkedin.com/in/jaykum4r"
|
||||
support_email: "work.jaykumar@gmail.com"
|
||||
|
||||
@@ -13,7 +13,7 @@ Run [Gemini CLI](https://ai.google.dev/gemini-api/docs/cli) in your workspace to
|
||||
```tf
|
||||
module "gemini" {
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key
|
||||
gemini_model = "gemini-2.5-pro"
|
||||
@@ -42,7 +42,7 @@ variable "gemini_api_key" {
|
||||
module "gemini" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder-labs/gemini/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
agent_id = coder_agent.example.id
|
||||
gemini_api_key = var.gemini_api_key # we recommend providing this parameter inorder to have a smoother experience (i.e. no google sign-in)
|
||||
gemini_model = "gemini-2.5-flash"
|
||||
|
||||
@@ -166,7 +166,7 @@ EOT
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.0.0"
|
||||
version = "1.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -16,7 +16,7 @@ The AgentAPI module is a building block for modules that need to run an AgentAPI
|
||||
```tf
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -236,4 +236,17 @@ describe("agentapi", async () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("agentapi-allowed-hosts", async () => {
|
||||
// verify that the agentapi binary has access to the AGENTAPI_ALLOWED_HOSTS environment variable
|
||||
// set in main.sh
|
||||
const { id } = await setup();
|
||||
await execModuleScript(id);
|
||||
await expectAgentAPIStarted(id);
|
||||
const agentApiStartLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/agentapi-mock.log",
|
||||
);
|
||||
expect(agentApiStartLog).toContain("AGENTAPI_ALLOWED_HOSTS: *");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -95,5 +95,7 @@ export LC_ALL=en_US.UTF-8
|
||||
cd "${WORKDIR}"
|
||||
|
||||
export AGENTAPI_CHAT_BASE_PATH="${AGENTAPI_CHAT_BASE_PATH:-}"
|
||||
# Disable host header check since AgentAPI is proxied by Coder (which does its own validation)
|
||||
export AGENTAPI_ALLOWED_HOSTS="*"
|
||||
nohup "$module_path/scripts/agentapi-start.sh" true "${AGENTAPI_PORT}" &>"$module_path/agentapi-start.log" &
|
||||
"$module_path/scripts/agentapi-wait-for-start.sh" "${AGENTAPI_PORT}"
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const http = require("http");
|
||||
const fs = require("fs");
|
||||
const args = process.argv.slice(2);
|
||||
const portIdx = args.findIndex((arg) => arg === "--port") + 1;
|
||||
const port = portIdx ? args[portIdx] : 3284;
|
||||
|
||||
console.log(`starting server on port ${port}`);
|
||||
fs.writeFileSync("/home/coder/agentapi-mock.log", `AGENTAPI_ALLOWED_HOSTS: ${process.env.AGENTAPI_ALLOWED_HOSTS}`);
|
||||
|
||||
http
|
||||
.createServer(function (_request, response) {
|
||||
|
||||
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.0.6"
|
||||
version = "2.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -84,7 +84,7 @@ resource "coder_agent" "main" {
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.0.6"
|
||||
version = "2.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
@@ -102,7 +102,7 @@ Run Claude Code as a standalone app in your workspace. This will install Claude
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.0.6"
|
||||
version = "2.0.7"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_claude_code = true
|
||||
|
||||
@@ -10,6 +10,7 @@ import path from "path";
|
||||
import {
|
||||
execContainer,
|
||||
findResourceInstance,
|
||||
readFileContainer,
|
||||
removeContainer,
|
||||
runContainer,
|
||||
runTerraformApply,
|
||||
@@ -319,4 +320,21 @@ describe("claude-code", async () => {
|
||||
agentApiUrl: "http://localhost:3284",
|
||||
});
|
||||
});
|
||||
|
||||
// verify that the agentapi binary has access to the AGENTAPI_ALLOWED_HOSTS environment variable
|
||||
// set in main.tf
|
||||
test("agentapi-allowed-hosts", async () => {
|
||||
const { id } = await setup();
|
||||
|
||||
const respModuleScript = await execModuleScript(id);
|
||||
expect(respModuleScript.exitCode).toBe(0);
|
||||
|
||||
await expectAgentAPIStarted(id);
|
||||
|
||||
const agentApiStartLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/agentapi-mock.log",
|
||||
);
|
||||
expect(agentApiStartLog).toContain("AGENTAPI_ALLOWED_HOSTS: *");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -241,6 +241,10 @@ resource "coder_script" "claude_code" {
|
||||
export LC_ALL=en_US.UTF-8
|
||||
|
||||
cd "${local.workdir}"
|
||||
|
||||
# Disable host header check since AgentAPI is proxied by Coder (which does its own validation)
|
||||
export AGENTAPI_ALLOWED_HOSTS="*"
|
||||
|
||||
nohup "$module_path/scripts/agentapi-start.sh" use_prompt &> "$module_path/agentapi-start.log" &
|
||||
"$module_path/scripts/agentapi-wait-for-start.sh"
|
||||
EOT
|
||||
|
||||
@@ -20,6 +20,8 @@ if (
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.writeFileSync("/home/coder/agentapi-mock.log", `AGENTAPI_ALLOWED_HOSTS: ${process.env.AGENTAPI_ALLOWED_HOSTS}`);
|
||||
|
||||
console.log(`starting server on port ${port}`);
|
||||
|
||||
http
|
||||
|
||||
@@ -1,261 +0,0 @@
|
||||
---
|
||||
display_name: Cursor CLI
|
||||
description: Run Cursor CLI agent in your workspace with MCP and force mode support
|
||||
icon: ../../../../.icons/cursor.svg
|
||||
verified: true
|
||||
tags: [cli, cursor, ai, agent, mcp, automation]
|
||||
---
|
||||
|
||||
# Cursor CLI
|
||||
|
||||
Run the [Cursor CLI](https://docs.cursor.com/en/cli/overview) agent in your workspace for terminal-based AI coding assistance. Supports both interactive and non-interactive modes, MCP (Model Context Protocol), and automation features.
|
||||
|
||||
```tf
|
||||
module "cursor-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor-cli/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
}
|
||||
```
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You must add the [Coder Login](https://registry.coder.com/modules/coder-login) module to your template
|
||||
|
||||
## Features
|
||||
|
||||
- **CLI Agent**: Terminal-based AI coding assistant with interactive and non-interactive modes
|
||||
- **AgentAPI Integration**: Web interface for CLI interactions
|
||||
- **Interactive Mode**: Conversational sessions with text output
|
||||
- **Non-Interactive Mode**: Automation-friendly for scripts and CI pipelines
|
||||
- **Session Management**: List, resume, and manage coding sessions
|
||||
- **Model Selection**: Support for multiple AI models (GPT-5, Claude, etc.)
|
||||
- **MCP Support**: Model Context Protocol for extended functionality
|
||||
- **Rules System**: Custom agent behavior configuration
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic setup
|
||||
|
||||
```tf
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/coder-login/coder"
|
||||
version = "1.0.15"
|
||||
agent_id = coder_agent.example.id
|
||||
}
|
||||
|
||||
module "cursor-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor-cli/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
install_cursor_cli = true
|
||||
install_agentapi = true
|
||||
}
|
||||
```
|
||||
|
||||
### CLI only (no web interface)
|
||||
|
||||
```tf
|
||||
module "cursor-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor-cli/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
install_cursor_cli = true
|
||||
install_agentapi = false
|
||||
}
|
||||
```
|
||||
|
||||
### With MCP and force mode for automation
|
||||
|
||||
```tf
|
||||
module "cursor-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor-cli/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder/project"
|
||||
|
||||
# MCP Configuration
|
||||
enable_mcp = true
|
||||
mcp_config_path = "/home/coder/.cursor/custom-mcp.json"
|
||||
|
||||
# Automation Features
|
||||
enable_force_mode = true
|
||||
default_model = "gpt-5"
|
||||
|
||||
# Rules System
|
||||
enable_rules = true
|
||||
}
|
||||
```
|
||||
|
||||
### Integration with Coder Tasks
|
||||
|
||||
```tf
|
||||
# Cursor CLI module with automation features
|
||||
module "cursor-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor-cli/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
enable_force_mode = true
|
||||
default_model = "claude-4-sonnet"
|
||||
}
|
||||
|
||||
# Automated code review task
|
||||
resource "coder_task" "ai_code_review" {
|
||||
agent_id = coder_agent.example.id
|
||||
name = "AI Code Review"
|
||||
command = "cursor-agent -p 'review the latest git changes for security issues and best practices' --force --output-format text"
|
||||
cron = "0 9 * * 1-5" # Weekdays at 9 AM
|
||||
}
|
||||
|
||||
# Automated test generation
|
||||
resource "coder_task" "generate_tests" {
|
||||
agent_id = coder_agent.example.id
|
||||
name = "Generate Missing Tests"
|
||||
command = "cursor-agent -p 'analyze the src/ directory and generate unit tests for functions missing test coverage' --force"
|
||||
cron = "0 18 * * *" # Daily at 6 PM
|
||||
}
|
||||
|
||||
# Documentation updates
|
||||
resource "coder_task" "update_docs" {
|
||||
agent_id = coder_agent.example.id
|
||||
name = "Update Documentation"
|
||||
command = "cursor-agent -p 'review and update README.md to reflect any new features or API changes' --force --model gpt-5"
|
||||
cron = "0 12 * * 0" # Sundays at noon
|
||||
}
|
||||
```
|
||||
|
||||
### With custom pre-install script
|
||||
|
||||
```tf
|
||||
module "cursor-cli" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/cursor-cli/coder"
|
||||
version = "1.0.0"
|
||||
agent_id = coder_agent.example.id
|
||||
|
||||
pre_install_script = <<-EOT
|
||||
# Install additional dependencies
|
||||
npm install -g typescript
|
||||
EOT
|
||||
}
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Web Interface
|
||||
|
||||
1. Click the "Cursor CLI" button to access the web interface
|
||||
2. Start interactive sessions with text output
|
||||
|
||||
### Terminal Usage
|
||||
|
||||
```bash
|
||||
# Interactive mode (default)
|
||||
cursor-agent
|
||||
|
||||
# Interactive mode with initial prompt
|
||||
cursor-agent "refactor the auth module to use JWT tokens"
|
||||
|
||||
# Non-interactive mode with text output
|
||||
cursor-agent -p "find and fix performance issues" --output-format text
|
||||
|
||||
# Force mode for automation (non-interactive)
|
||||
cursor-agent -p "review code for security issues" --force
|
||||
|
||||
# Use specific model
|
||||
cursor-agent -p "add error handling" --model "gpt-5"
|
||||
|
||||
# Combine force mode with model selection
|
||||
cursor-agent -p "generate comprehensive tests" --force --model "claude-4-sonnet"
|
||||
|
||||
# Session management
|
||||
cursor-agent ls # List all previous chats
|
||||
cursor-agent resume # Resume latest conversation
|
||||
cursor-agent --resume="chat-id" # Resume specific conversation
|
||||
```
|
||||
|
||||
### Interactive Mode Features
|
||||
|
||||
- Conversational sessions with the agent
|
||||
- Review proposed changes before applying
|
||||
- Real-time guidance and steering
|
||||
- Text-based output optimized for terminal use
|
||||
- Session persistence and resumption
|
||||
|
||||
### Non-Interactive Mode Features
|
||||
|
||||
- Automation-friendly for scripts and CI pipelines
|
||||
- Direct prompt execution with text output
|
||||
- Model selection support
|
||||
- Git integration for change reviews
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Cursor CLI with Coder Tasks Integration
|
||||
|
||||
*Screenshot showing the cursor-cli module working with automated Coder Tasks will be added here*
|
||||
|
||||
- Interactive web interface for cursor-agent
|
||||
- Automated code review tasks running in background
|
||||
- Terminal output showing force mode execution
|
||||
- MCP integration with custom tools
|
||||
|
||||
## Configuration
|
||||
|
||||
The module supports comprehensive configuration options:
|
||||
|
||||
### Core Features
|
||||
- **MCP (Model Context Protocol)**: Automatically detects `mcp.json` configuration or uses custom path
|
||||
- **Rules System**: Supports `.cursor/rules` directory for custom agent behavior
|
||||
- **Force Mode**: Enable non-interactive automation for CI/CD pipelines
|
||||
- **Model Selection**: Set default AI model (gpt-5, claude-4-sonnet, etc.)
|
||||
- **Environment Variables**: Respects Cursor CLI environment settings
|
||||
|
||||
### Available Variables
|
||||
|
||||
| Variable | Type | Default | Description |
|
||||
|----------|------|---------|-------------|
|
||||
| `enable_mcp` | bool | `true` | Enable MCP (Model Context Protocol) support |
|
||||
| `mcp_config_path` | string | `""` | Path to custom MCP configuration file |
|
||||
| `enable_force_mode` | bool | `false` | Enable force mode for non-interactive automation |
|
||||
| `default_model` | string | `""` | Default AI model (e.g., gpt-5, claude-4-sonnet) |
|
||||
| `enable_rules` | bool | `true` | Enable the rules system (.cursor/rules directory) |
|
||||
| `install_cursor_cli` | bool | `true` | Whether to install Cursor CLI |
|
||||
| `install_agentapi` | bool | `true` | Whether to install AgentAPI web interface |
|
||||
| `folder` | string | `"/home/coder"` | Working directory for cursor-agent |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
The module creates log files in the workspace's `~/.cursor-cli-module` directory. Check these files if you encounter issues:
|
||||
|
||||
```bash
|
||||
# Check installation logs
|
||||
cat ~/.cursor-cli-module/install.log
|
||||
|
||||
# Check runtime logs
|
||||
cat ~/.cursor-cli-module/runtime.log
|
||||
|
||||
# Verify Cursor CLI installation
|
||||
cursor-agent --help
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Cursor CLI not found**: Ensure `install_cursor_cli = true` or install manually:
|
||||
|
||||
```bash
|
||||
curl https://cursor.com/install -fsS | bash
|
||||
```
|
||||
|
||||
2. **Permission issues**: Check that the installation script has proper permissions
|
||||
|
||||
3. **Path issues**: The module automatically adds Cursor CLI to PATH, but you may need to restart your shell
|
||||
@@ -1,80 +0,0 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
describe("cursor-cli", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
testRequiredVariables(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
it("default output with CLI enabled", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
});
|
||||
|
||||
// Check that AgentAPI module is created
|
||||
const agentapi_module = state.resources.find(
|
||||
(res) => res.type === "module" && res.name === "agentapi",
|
||||
);
|
||||
expect(agentapi_module).not.toBeNull();
|
||||
});
|
||||
|
||||
it("adds custom folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
folder: "/foo/bar",
|
||||
});
|
||||
|
||||
// Check that AgentAPI module is created with custom folder
|
||||
const agentapi_module = state.resources.find(
|
||||
(res) => res.type === "module" && res.name === "agentapi",
|
||||
);
|
||||
expect(agentapi_module).not.toBeNull();
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
order: "22",
|
||||
});
|
||||
|
||||
// Check that AgentAPI module is created
|
||||
const agentapi_module = state.resources.find(
|
||||
(res) => res.type === "module" && res.name === "agentapi",
|
||||
);
|
||||
expect(agentapi_module).not.toBeNull();
|
||||
});
|
||||
|
||||
it("disables CLI installation", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
install_cursor_cli: "false",
|
||||
install_agentapi: "false",
|
||||
});
|
||||
|
||||
// AgentAPI module should still exist but with install_agentapi = false
|
||||
const agentapi_module = state.resources.find(
|
||||
(res) => res.type === "module" && res.name === "agentapi",
|
||||
);
|
||||
expect(agentapi_module).not.toBeNull();
|
||||
});
|
||||
|
||||
it("enables only CLI without web interface", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
install_cursor_cli: "true",
|
||||
install_agentapi: "false",
|
||||
});
|
||||
|
||||
// AgentAPI module should exist but with install_agentapi = false
|
||||
const agentapi_module = state.resources.find(
|
||||
(res) => res.type === "module" && res.name === "agentapi",
|
||||
);
|
||||
expect(agentapi_module).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,154 +0,0 @@
|
||||
terraform {
|
||||
required_version = ">= 1.0"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 "group" {
|
||||
type = string
|
||||
description = "The name of a group that this app belongs to."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "icon" {
|
||||
type = string
|
||||
description = "The icon to use for the app."
|
||||
default = "/icon/cursor.svg"
|
||||
}
|
||||
|
||||
variable "folder" {
|
||||
type = string
|
||||
description = "The folder to run Cursor CLI in."
|
||||
default = "/home/coder"
|
||||
}
|
||||
|
||||
variable "install_cursor_cli" {
|
||||
type = bool
|
||||
description = "Whether to install Cursor CLI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "install_agentapi" {
|
||||
type = bool
|
||||
description = "Whether to install AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "agentapi_version" {
|
||||
type = string
|
||||
description = "The version of AgentAPI to install."
|
||||
default = "v0.3.3"
|
||||
}
|
||||
|
||||
variable "subdomain" {
|
||||
type = bool
|
||||
description = "Whether to use a subdomain for AgentAPI."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "pre_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run before installing Cursor CLI."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "post_install_script" {
|
||||
type = string
|
||||
description = "Custom script to run after installing Cursor CLI."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "enable_mcp" {
|
||||
type = bool
|
||||
description = "Whether to enable MCP (Model Context Protocol) support."
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "mcp_config_path" {
|
||||
type = string
|
||||
description = "Path to the MCP configuration file (mcp.json)."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "enable_force_mode" {
|
||||
type = bool
|
||||
description = "Whether to enable force mode for non-interactive automation."
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "default_model" {
|
||||
type = string
|
||||
description = "Default AI model to use (e.g., gpt-5, claude-4-sonnet)."
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "enable_rules" {
|
||||
type = bool
|
||||
description = "Whether to enable the rules system (.cursor/rules directory)."
|
||||
default = true
|
||||
}
|
||||
|
||||
locals {
|
||||
app_slug = "cursor-cli"
|
||||
install_script = file("${path.module}/scripts/install.sh")
|
||||
start_script = file("${path.module}/scripts/start.sh")
|
||||
module_dir_name = ".cursor-cli-module"
|
||||
}
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.0"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
web_app_order = var.order
|
||||
web_app_group = var.group
|
||||
web_app_icon = var.icon
|
||||
web_app_display_name = "Cursor CLI"
|
||||
cli_app_slug = "${local.app_slug}-terminal"
|
||||
cli_app_display_name = "Cursor CLI Terminal"
|
||||
module_dir_name = local.module_dir_name
|
||||
install_agentapi = var.install_agentapi
|
||||
agentapi_version = var.agentapi_version
|
||||
agentapi_subdomain = var.subdomain
|
||||
pre_install_script = var.pre_install_script
|
||||
post_install_script = var.post_install_script
|
||||
start_script = local.start_script
|
||||
install_script = <<-EOT
|
||||
#!/bin/bash
|
||||
set -o errexit
|
||||
set -o pipefail
|
||||
|
||||
echo -n '${base64encode(local.install_script)}' | base64 -d > /tmp/install.sh
|
||||
chmod +x /tmp/install.sh
|
||||
|
||||
ARG_FOLDER='${var.folder}' \
|
||||
ARG_INSTALL='${var.install_cursor_cli}' \
|
||||
ARG_ENABLE_MCP='${var.enable_mcp}' \
|
||||
ARG_MCP_CONFIG_PATH='${var.mcp_config_path}' \
|
||||
ARG_ENABLE_FORCE_MODE='${var.enable_force_mode}' \
|
||||
ARG_DEFAULT_MODEL='${var.default_model}' \
|
||||
ARG_ENABLE_RULES='${var.enable_rules}' \
|
||||
/tmp/install.sh
|
||||
EOT
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
set -o nounset
|
||||
|
||||
echo "--------------------------------"
|
||||
echo "folder: $ARG_FOLDER"
|
||||
echo "install: $ARG_INSTALL"
|
||||
echo "enable_mcp: $ARG_ENABLE_MCP"
|
||||
echo "mcp_config_path: $ARG_MCP_CONFIG_PATH"
|
||||
echo "enable_force_mode: $ARG_ENABLE_FORCE_MODE"
|
||||
echo "default_model: $ARG_DEFAULT_MODEL"
|
||||
echo "enable_rules: $ARG_ENABLE_RULES"
|
||||
echo "--------------------------------"
|
||||
|
||||
set +o nounset
|
||||
|
||||
if [ "${ARG_INSTALL}" = "true" ]; then
|
||||
echo "Installing Cursor CLI..."
|
||||
|
||||
# Install Cursor CLI using the official installer
|
||||
curl https://cursor.com/install -fsS | bash
|
||||
|
||||
# Add cursor-agent to PATH if not already there
|
||||
if ! command_exists cursor-agent; then
|
||||
echo 'export PATH="$HOME/.cursor/bin:$PATH"' >> "$HOME/.bashrc"
|
||||
echo 'export PATH="$HOME/.cursor/bin:$PATH"' >> "$HOME/.zshrc" 2> /dev/null || true
|
||||
export PATH="$HOME/.cursor/bin:$PATH"
|
||||
fi
|
||||
|
||||
echo "Cursor CLI installed"
|
||||
|
||||
# Configure MCP if enabled
|
||||
if [ "${ARG_ENABLE_MCP}" = "true" ]; then
|
||||
echo "Configuring MCP (Model Context Protocol)..."
|
||||
|
||||
# Create MCP config directory if it doesn't exist
|
||||
mkdir -p "$HOME/.cursor"
|
||||
|
||||
# If custom MCP config path is provided, copy it
|
||||
if [ -n "${ARG_MCP_CONFIG_PATH}" ] && [ -f "${ARG_MCP_CONFIG_PATH}" ]; then
|
||||
cp "${ARG_MCP_CONFIG_PATH}" "$HOME/.cursor/mcp.json"
|
||||
echo "MCP configuration copied from ${ARG_MCP_CONFIG_PATH}"
|
||||
else
|
||||
# Create a basic MCP config if none exists
|
||||
if [ ! -f "$HOME/.cursor/mcp.json" ]; then
|
||||
cat > "$HOME/.cursor/mcp.json" << 'EOF'
|
||||
{
|
||||
"mcpServers": {
|
||||
"filesystem": {
|
||||
"command": "npx",
|
||||
"args": ["@modelcontextprotocol/server-filesystem", "/tmp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
echo "Basic MCP configuration created"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Configure rules system if enabled
|
||||
if [ "${ARG_ENABLE_RULES}" = "true" ]; then
|
||||
echo "Setting up Cursor rules system..."
|
||||
mkdir -p "$HOME/.cursor/rules"
|
||||
|
||||
# Create a basic rules file if none exists
|
||||
if [ ! -f "$HOME/.cursor/rules/general.md" ]; then
|
||||
cat > "$HOME/.cursor/rules/general.md" << 'EOF'
|
||||
# General Coding Rules
|
||||
|
||||
## Code Style
|
||||
- Use consistent indentation (2 spaces for JS/TS, 4 for Python)
|
||||
- Add meaningful comments for complex logic
|
||||
- Follow language-specific naming conventions
|
||||
|
||||
## Best Practices
|
||||
- Write tests for new functionality
|
||||
- Handle errors gracefully
|
||||
- Use descriptive variable and function names
|
||||
EOF
|
||||
echo "Basic rules configuration created"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Skipping Cursor CLI installation"
|
||||
fi
|
||||
|
||||
# Verify installation
|
||||
if command_exists cursor-agent; then
|
||||
CURSOR_CMD=cursor-agent
|
||||
elif [ -f "$HOME/.cursor/bin/cursor-agent" ]; then
|
||||
CURSOR_CMD="$HOME/.cursor/bin/cursor-agent"
|
||||
else
|
||||
echo "Warning: Cursor CLI is not installed or not found in PATH. Please enable install_cursor_cli or install it manually."
|
||||
echo "You can install it manually with: curl https://cursor.com/install -fsS | bash"
|
||||
fi
|
||||
|
||||
echo "Cursor CLI setup complete"
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Function to check if a command exists
|
||||
command_exists() {
|
||||
command -v "$1" > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Set working directory
|
||||
if [ -n "${ARG_FOLDER:-}" ] && [ -d "${ARG_FOLDER}" ]; then
|
||||
cd "${ARG_FOLDER}" || {
|
||||
echo "Warning: Could not change to directory ${ARG_FOLDER}, using current directory"
|
||||
}
|
||||
fi
|
||||
|
||||
# Find cursor-agent command
|
||||
if command_exists cursor-agent; then
|
||||
CURSOR_CMD=cursor-agent
|
||||
elif [ -f "$HOME/.cursor/bin/cursor-agent" ]; then
|
||||
CURSOR_CMD="$HOME/.cursor/bin/cursor-agent"
|
||||
else
|
||||
echo "Error: Cursor CLI is not installed. Please enable install_cursor_cli or install it manually."
|
||||
echo "You can install it manually with: curl https://cursor.com/install -fsS | bash"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Starting Cursor CLI in $(pwd)"
|
||||
echo "Interactive mode with text output enabled"
|
||||
echo "Available commands:"
|
||||
echo " - Start interactive session: cursor-agent"
|
||||
echo " - Non-interactive mode: cursor-agent -p 'your prompt here'"
|
||||
echo " - With specific model: cursor-agent -p 'prompt' --model 'gpt-5'"
|
||||
echo " - Text output format: cursor-agent -p 'prompt' --output-format text"
|
||||
echo " - Force mode (non-interactive): cursor-agent -p 'prompt' --force"
|
||||
echo " - List sessions: cursor-agent ls"
|
||||
echo " - Resume session: cursor-agent resume"
|
||||
echo ""
|
||||
|
||||
# Set up environment variables for configuration
|
||||
if [ -n "${ARG_DEFAULT_MODEL:-}" ]; then
|
||||
export CURSOR_DEFAULT_MODEL="${ARG_DEFAULT_MODEL}"
|
||||
echo "Default model set to: ${ARG_DEFAULT_MODEL}"
|
||||
fi
|
||||
|
||||
if [ "${ARG_ENABLE_FORCE_MODE:-false}" = "true" ]; then
|
||||
export CURSOR_FORCE_MODE="true"
|
||||
echo "Force mode enabled for non-interactive automation"
|
||||
fi
|
||||
|
||||
if [ "${ARG_ENABLE_MCP:-true}" = "true" ]; then
|
||||
echo "MCP (Model Context Protocol) support enabled"
|
||||
fi
|
||||
|
||||
if [ "${ARG_ENABLE_RULES:-true}" = "true" ]; then
|
||||
echo "Rules system enabled (.cursor/rules directory)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Configure for interactive mode with text output
|
||||
# If no arguments provided, start in interactive mode
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Starting interactive session..."
|
||||
exec "$CURSOR_CMD"
|
||||
else
|
||||
# Pass through all arguments for custom usage
|
||||
exec "$CURSOR_CMD" "$@"
|
||||
fi
|
||||
@@ -13,7 +13,7 @@ Run the [Goose](https://block.github.io/goose/) agent in your workspace to gener
|
||||
```tf
|
||||
module "goose" {
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
@@ -79,7 +79,7 @@ resource "coder_agent" "main" {
|
||||
module "goose" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/goose/coder"
|
||||
version = "2.1.0"
|
||||
version = "2.1.1"
|
||||
agent_id = coder_agent.example.id
|
||||
folder = "/home/coder"
|
||||
install_goose = true
|
||||
|
||||
@@ -139,7 +139,7 @@ EOT
|
||||
|
||||
module "agentapi" {
|
||||
source = "registry.coder.com/coder/agentapi/coder"
|
||||
version = "1.1.0"
|
||||
version = "1.1.1"
|
||||
|
||||
agent_id = var.agent_id
|
||||
web_app_slug = local.app_slug
|
||||
|
||||
@@ -231,6 +231,7 @@ resource "coder_app" "jetbrains" {
|
||||
icon = local.options_metadata[each.key].icon
|
||||
external = true
|
||||
order = var.coder_app_order
|
||||
group = var.group
|
||||
url = join("", [
|
||||
"jetbrains://gateway/coder?&workspace=", # requires 2.6.3+ version of Toolbox
|
||||
data.coder_workspace.me.name,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
display_name: VSCode Desktop Core
|
||||
description: Building block for modules that need to link to an external VSCode-based IDE
|
||||
icon: ../../../../.icons/coder.svg
|
||||
verified: true
|
||||
tags: [internal, library]
|
||||
---
|
||||
|
||||
# VS Code Desktop Core
|
||||
|
||||
> [!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.
|
||||
|
||||
```tf
|
||||
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/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"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import {
|
||||
runTerraformApply,
|
||||
runTerraformInit,
|
||||
testRequiredVariables,
|
||||
} from "~test";
|
||||
|
||||
// hardcoded coder_app name in main.tf
|
||||
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",
|
||||
protocol: "vscode",
|
||||
}
|
||||
|
||||
describe("vscode-desktop-core", async () => {
|
||||
await runTerraformInit(import.meta.dir);
|
||||
|
||||
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`,
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
it("adds folder", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
folder: "/foo/bar",
|
||||
|
||||
...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`,
|
||||
);
|
||||
});
|
||||
|
||||
it("expect order to be set", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
coder_app_order: "22",
|
||||
...defaultVariables
|
||||
});
|
||||
|
||||
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).toBe(22);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,92 @@
|
||||
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 the 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 "protocol" {
|
||||
type = string
|
||||
description = "The URI protocol for the IDE."
|
||||
}
|
||||
|
||||
variable "coder_app_icon" {
|
||||
type = string
|
||||
description = "The icon of the coder_app."
|
||||
}
|
||||
|
||||
variable "coder_app_slug" {
|
||||
type = string
|
||||
description = "The slug of the coder_app."
|
||||
}
|
||||
|
||||
variable "coder_app_display_name" {
|
||||
type = string
|
||||
description = "The display name of the coder_app."
|
||||
}
|
||||
|
||||
variable "coder_app_order" {
|
||||
type = number
|
||||
description = "The order of the coder_app."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "coder_app_group" {
|
||||
type = string
|
||||
description = "The group of the coder_app."
|
||||
default = null
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {}
|
||||
data "coder_workspace_owner" "me" {}
|
||||
|
||||
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
|
||||
|
||||
order = var.coder_app_order
|
||||
group = var.coder_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",
|
||||
"?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" {
|
||||
value = coder_app.vscode-desktop.url
|
||||
description = "IDE URI."
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
display_name: "Eric Paulsen"
|
||||
bio: "Field CTO, EMEA @ Coder"
|
||||
avatar_url: "./.images/avatar.png"
|
||||
avatar: "./.images/avatar.png"
|
||||
github: "ericpaulsen"
|
||||
linkedin: "https://www.linkedin.com/in/ericpaulsen17" # Optional
|
||||
website: "https://ericpaulsen.io" # Optional
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -0,0 +1,32 @@
|
||||
---
|
||||
display_name: "Mark Milligan"
|
||||
bio: "VP of Revenue at https://nuon.co. Former VP of Sales at Coder. Love building startup revenue teams and tinkering with technology."
|
||||
avatar: "./.images/avatar.png"
|
||||
github: "sharkymark"
|
||||
linkedin: "https://www.linkedin.com/in/marktmilligan" # Optional
|
||||
website: "https://markmilligan.io" # Optional
|
||||
support_email: "mtm20176@gmail.com" # Optional
|
||||
status: "community"
|
||||
---
|
||||
|
||||
# Mark Milligan
|
||||
|
||||
Former VP of Sales at Coder for 4 years, and now VP of Revenue at Nuon. I love building startup revenue teams and tinkering with technology.
|
||||
|
||||
## About Me
|
||||
|
||||
Visit my [website](https://markmilligan.io) to learn more about my work and interests.
|
||||
|
||||
## Links
|
||||
|
||||
[My presentation about Great White Sharks](https://docs.google.com/presentation/d/13I3Af7l-ZSVCh-ovEvOKIM30ABIvNKhkRC3CnYZN450/edit?slide=id.p#slide=id.p) - given twice in 2020 and 2021 to the Coder team.
|
||||
|
||||
[NOAA Radar](https://radar.weather.gov/)
|
||||
|
||||
[Flight Radar](https://www.flightradar24.com/airport/aus)
|
||||
|
||||
### Webcams
|
||||
|
||||
[Austin - facing south](https://cctv.austinmobility.io/image/51.jpg)
|
||||
|
||||
[Austin - facing north](https://cctv.austinmobility.io/image/52.jpg)
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
display_name: "Claude Code AI Agent Template"
|
||||
description: The goal is to try the experimental ai agent integration with Claude CodeAI agent
|
||||
icon: "../../../../.icons/claude.svg"
|
||||
verified: false
|
||||
tags: ["ai", "docker", "container", "claude", "agent", "tasks"]
|
||||
---
|
||||
|
||||
# ai agent template for a workspace in a container on a Docker host
|
||||
|
||||
### Docker image
|
||||
|
||||
1. Based on Coder-managed image `codercom/example-universal:ubuntu`
|
||||
|
||||
[Image on DockerHub](https://hub.docker.com/r/codercom/example-universal)
|
||||
|
||||
### Apps included
|
||||
|
||||
1. A web-based terminal
|
||||
1. code-server Web IDE
|
||||
1. A [sample app](https://github.com/gothinkster/realworld) to test the environment
|
||||
1. [Claude Code AI agent](https://www.anthropic.com/claude-code) to assist with development tasks
|
||||
|
||||
### Resources
|
||||
|
||||
[Coder docs on AI agents and tasks](https://coder.com/docs/ai-coder/tasks)
|
||||
|
||||
[main.tf for Coder example](https://github.com/coder/registry/blob/main/registry/coder-labs/templates/tasks-docker/main.tf)
|
||||
|
||||
[Claude Code Coder Terraform module](https://registry.coder.com/modules/coder/claude-code)
|
||||
|
||||
[Docker Terraform provider](https://registry.terraform.io/providers/kreuzwerker/docker/latest/docs)
|
||||
|
||||
[Coder Terraform provider](https://registry.terraform.io/providers/coder/coder/latest/docs)
|
||||
@@ -0,0 +1,363 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
}
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "docker" {
|
||||
host = var.socket
|
||||
}
|
||||
|
||||
provider "coder" {
|
||||
}
|
||||
|
||||
data "coder_workspace" "me" {
|
||||
}
|
||||
|
||||
data "coder_workspace_owner" "me" {
|
||||
}
|
||||
|
||||
data "coder_provisioner" "me" {
|
||||
}
|
||||
|
||||
variable "socket" {
|
||||
type = string
|
||||
description = <<-EOF
|
||||
The Unix socket that the Docker daemon listens on and how containers
|
||||
communicate with the Docker daemon.
|
||||
|
||||
Either Unix or TCP
|
||||
e.g., unix:///var/run/docker.sock
|
||||
|
||||
EOF
|
||||
default = "unix:///var/run/docker.sock"
|
||||
}
|
||||
|
||||
variable "anthropic_api_key" {
|
||||
type = string
|
||||
description = "Generate one at: https://console.anthropic.com/settings/keys"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
resource "coder_env" "anthropic_api_key" {
|
||||
agent_id = coder_agent.dev.id
|
||||
name = "CODER_MCP_CLAUDE_API_KEY"
|
||||
value = var.anthropic_api_key
|
||||
}
|
||||
|
||||
# The Claude Code module does the automatic task reporting
|
||||
# Other agent modules: https://registry.coder.com/modules?search=agent
|
||||
# Or use a custom agent:
|
||||
module "claude-code" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "2.0.0"
|
||||
agent_id = coder_agent.dev.id
|
||||
folder = "/home/coder/projects"
|
||||
install_claude_code = true
|
||||
claude_code_version = "latest"
|
||||
order = 999
|
||||
|
||||
experiment_post_install_script = data.coder_parameter.setup_script.value
|
||||
|
||||
# This enables Coder Tasks
|
||||
experiment_report_tasks = true
|
||||
}
|
||||
|
||||
# We are using presets to set the prompts, image, and set up instructions
|
||||
# See https://coder.com/docs/admin/templates/extending-templates/parameters#workspace-presets
|
||||
data "coder_workspace_preset" "default" {
|
||||
name = "Real World App: Angular + Django"
|
||||
default = true
|
||||
parameters = {
|
||||
"system_prompt" = <<-EOT
|
||||
-- Framing --
|
||||
You are a helpful assistant that can help with code. You are running inside a Coder Workspace and provide status updates to the user via Coder MCP. Stay on track, feel free to debug, but when the original plan fails, do not choose a different route/architecture without checking the user first.
|
||||
|
||||
-- Tool Selection --
|
||||
- playwright: previewing your changes after you made them
|
||||
to confirm it worked as expected
|
||||
- desktop-commander - use only for commands that keep running
|
||||
(servers, dev watchers, GUI apps).
|
||||
- Built-in tools - use for everything else:
|
||||
(file operations, git commands, builds & installs, one-off shell commands)
|
||||
|
||||
Remember this decision rule:
|
||||
- Stays running? → desktop-commander
|
||||
- Finishes immediately? → built-in tools
|
||||
|
||||
-- Context --
|
||||
There is an existing app and tmux dev server running on port 8000. Be sure to read it's CLAUDE.md (./realworld-django-rest-framework-angular/CLAUDE.md) to learn more about it.
|
||||
|
||||
Since this app is for demo purposes and the user is previewing the homepage and subsequent pages, aim to make the first visual change/prototype very quickly so the user can preview it, then focus on backend or logic which can be a more involved, long-running architecture plan.
|
||||
|
||||
EOT
|
||||
|
||||
"setup_script" = <<-EOT
|
||||
# Set up projects dir
|
||||
mkdir -p /home/coder/projects
|
||||
cd $HOME/projects
|
||||
|
||||
# Packages: Install additional packages
|
||||
sudo apt-get update && sudo apt-get install -y tmux
|
||||
if ! command -v google-chrome >/dev/null 2>&1; then
|
||||
yes | npx playwright install chrome
|
||||
fi
|
||||
|
||||
# MCP: Install and configure MCP Servers
|
||||
npm install -g @wonderwhy-er/desktop-commander
|
||||
claude mcp add playwright npx -- @playwright/mcp@latest --headless --isolated --no-sandbox
|
||||
claude mcp add desktop-commander desktop-commander
|
||||
|
||||
# Repo: Clone and pull changes from the git repository
|
||||
if [ ! -d "realworld-django-rest-framework-angular" ]; then
|
||||
git clone https://github.com/coder-contrib/realworld-django-rest-framework-angular.git
|
||||
else
|
||||
cd realworld-django-rest-framework-angular
|
||||
git fetch
|
||||
# Check for uncommitted changes
|
||||
if git diff-index --quiet HEAD -- && \
|
||||
[ -z "$(git status --porcelain --untracked-files=no)" ] && \
|
||||
[ -z "$(git log --branches --not --remotes)" ]; then
|
||||
echo "Repo is clean. Pulling latest changes..."
|
||||
git pull
|
||||
else
|
||||
echo "Repo has uncommitted or unpushed changes. Skipping pull."
|
||||
fi
|
||||
|
||||
cd ..
|
||||
fi
|
||||
|
||||
# Initialize: Start the development server
|
||||
cd realworld-django-rest-framework-angular && ./start-dev.sh
|
||||
EOT
|
||||
"preview_port" = "4200"
|
||||
"container_image" = "codercom/example-universal:ubuntu"
|
||||
}
|
||||
}
|
||||
|
||||
# Advanced parameters (these are all set via preset)
|
||||
data "coder_parameter" "system_prompt" {
|
||||
name = "system_prompt"
|
||||
display_name = "System Prompt"
|
||||
type = "string"
|
||||
form_type = "textarea"
|
||||
description = "System prompt for the agent with generalized instructions"
|
||||
mutable = false
|
||||
}
|
||||
data "coder_parameter" "ai_prompt" {
|
||||
type = "string"
|
||||
name = "AI Prompt"
|
||||
default = ""
|
||||
description = "Write a prompt for Claude Code"
|
||||
mutable = true
|
||||
}
|
||||
data "coder_parameter" "setup_script" {
|
||||
name = "setup_script"
|
||||
display_name = "Setup Script"
|
||||
type = "string"
|
||||
form_type = "textarea"
|
||||
description = "Script to run before running the agent"
|
||||
mutable = false
|
||||
}
|
||||
data "coder_parameter" "container_image" {
|
||||
name = "container_image"
|
||||
display_name = "Container Image"
|
||||
type = "string"
|
||||
default = "codercom/example-universal:ubuntu"
|
||||
mutable = false
|
||||
}
|
||||
data "coder_parameter" "preview_port" {
|
||||
name = "preview_port"
|
||||
display_name = "Preview Port"
|
||||
description = "The port the web app is running to preview in Tasks"
|
||||
type = "number"
|
||||
default = "3000"
|
||||
mutable = false
|
||||
}
|
||||
|
||||
# Other variables for Claude Code
|
||||
resource "coder_env" "claude_task_prompt" {
|
||||
agent_id = coder_agent.dev.id
|
||||
name = "CODER_MCP_CLAUDE_TASK_PROMPT"
|
||||
value = data.coder_parameter.ai_prompt.value
|
||||
}
|
||||
resource "coder_env" "app_status_slug" {
|
||||
agent_id = coder_agent.dev.id
|
||||
name = "CODER_MCP_APP_STATUS_SLUG"
|
||||
value = "claude-code"
|
||||
}
|
||||
resource "coder_env" "claude_system_prompt" {
|
||||
agent_id = coder_agent.dev.id
|
||||
name = "CODER_MCP_CLAUDE_SYSTEM_PROMPT"
|
||||
value = data.coder_parameter.system_prompt.value
|
||||
}
|
||||
|
||||
module "coder-login" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/coder-login/coder"
|
||||
agent_id = coder_agent.dev.id
|
||||
}
|
||||
|
||||
module "dotfiles" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/dotfiles/coder"
|
||||
agent_id = coder_agent.dev.id
|
||||
}
|
||||
|
||||
module "code-server" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/code-server/coder"
|
||||
agent_id = coder_agent.dev.id
|
||||
folder = "/home/coder/projects"
|
||||
}
|
||||
|
||||
module "git-config" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/modules/git-config/coder"
|
||||
agent_id = coder_agent.dev.id
|
||||
}
|
||||
|
||||
resource "coder_agent" "dev" {
|
||||
arch = data.coder_provisioner.me.arch
|
||||
os = "linux"
|
||||
|
||||
# The following metadata blocks are optional. They are used to display
|
||||
# information about your workspace in the dashboard. You can remove them
|
||||
# if you don't want to display any information.
|
||||
# For basic resources, you can use the `coder stat` command.
|
||||
# If you need more control, you can write your own script.
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage"
|
||||
key = "0_cpu_usage"
|
||||
script = "coder stat cpu"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "RAM Usage"
|
||||
key = "1_ram_usage"
|
||||
script = "coder stat mem"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Home Disk"
|
||||
key = "3_home_disk"
|
||||
script = "coder stat disk --path $${HOME}"
|
||||
interval = 60
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "CPU Usage (Host)"
|
||||
key = "4_cpu_usage_host"
|
||||
script = "coder stat cpu --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
metadata {
|
||||
display_name = "Memory Usage (Host)"
|
||||
key = "5_mem_usage_host"
|
||||
script = "coder stat mem --host"
|
||||
interval = 10
|
||||
timeout = 1
|
||||
}
|
||||
|
||||
display_apps {
|
||||
vscode = true
|
||||
vscode_insiders = false
|
||||
ssh_helper = false
|
||||
port_forwarding_helper = true
|
||||
web_terminal = true
|
||||
}
|
||||
|
||||
startup_script_behavior = "non-blocking"
|
||||
connection_timeout = 300
|
||||
|
||||
env = {
|
||||
|
||||
GIT_AUTHOR_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_AUTHOR_EMAIL = "${data.coder_workspace_owner.me.email}"
|
||||
GIT_COMMITTER_NAME = coalesce(data.coder_workspace_owner.me.full_name, data.coder_workspace_owner.me.name)
|
||||
GIT_COMMITTER_EMAIL = "${data.coder_workspace_owner.me.email}"
|
||||
}
|
||||
|
||||
startup_script = <<EOT
|
||||
#!/bin/sh
|
||||
|
||||
EOT
|
||||
|
||||
}
|
||||
|
||||
resource "coder_app" "preview" {
|
||||
agent_id = coder_agent.dev.id
|
||||
slug = "preview"
|
||||
display_name = "Preview your app"
|
||||
icon = "${data.coder_workspace.me.access_url}/emojis/1f50e.png"
|
||||
url = "http://localhost:${data.coder_parameter.preview_port.value}"
|
||||
share = "authenticated"
|
||||
subdomain = true
|
||||
open_in = "tab"
|
||||
order = 0
|
||||
healthcheck {
|
||||
url = "http://localhost:${data.coder_parameter.preview_port.value}/"
|
||||
interval = 5
|
||||
threshold = 15
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_container" "workspace" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
image = data.coder_parameter.container_image.value
|
||||
# Uses lower() to avoid Docker restriction on container names.
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${lower(data.coder_workspace.me.name)}"
|
||||
hostname = lower(data.coder_workspace.me.name)
|
||||
dns = ["1.1.1.1"]
|
||||
|
||||
# Use the docker gateway if the access URL is 127.0.0.1
|
||||
#entrypoint = ["sh", "-c", replace(coder_agent.dev.init_script, "127.0.0.1", "host.docker.internal")]
|
||||
|
||||
# Use the docker gateway if the access URL is 127.0.0.1
|
||||
command = [
|
||||
"sh", "-c",
|
||||
<<EOT
|
||||
trap '[ $? -ne 0 ] && echo === Agent script exited with non-zero code. Sleeping infinitely to preserve logs... && sleep infinity' EXIT
|
||||
${replace(coder_agent.dev.init_script, "/localhost|127\\.0\\.0\\.1/", "host.docker.internal")}
|
||||
EOT
|
||||
]
|
||||
|
||||
|
||||
env = ["CODER_AGENT_TOKEN=${coder_agent.dev.token}"]
|
||||
volumes {
|
||||
container_path = "/home/coder/"
|
||||
volume_name = docker_volume.coder_volume.name
|
||||
read_only = false
|
||||
}
|
||||
host {
|
||||
host = "host.docker.internal"
|
||||
ip = "host-gateway"
|
||||
}
|
||||
}
|
||||
|
||||
resource "docker_volume" "coder_volume" {
|
||||
name = "coder-${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}"
|
||||
}
|
||||
|
||||
resource "coder_metadata" "workspace_info" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
resource_id = docker_container.workspace[0].id
|
||||
item {
|
||||
key = "image"
|
||||
value = data.coder_parameter.container_image.value
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Find all directories that contain any .tftest.hcl files and run terraform test in each
|
||||
|
||||
run_dir() {
|
||||
local dir="$1"
|
||||
echo "==> Running terraform test in $dir"
|
||||
(cd "$dir" && terraform init -upgrade -input=false -no-color > /dev/null && terraform test -no-color -verbose)
|
||||
}
|
||||
|
||||
mapfile -t test_dirs < <(find . -type f -name "*.tftest.hcl" -print0 | xargs -0 -I{} dirname {} | sort -u)
|
||||
|
||||
if [[ ${#test_dirs[@]} -eq 0 ]]; then
|
||||
echo "No .tftest.hcl tests found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
status=0
|
||||
for d in "${test_dirs[@]}"; do
|
||||
if ! run_dir "$d"; then
|
||||
status=1
|
||||
fi
|
||||
done
|
||||
|
||||
exit $status
|
||||
+1
-1
@@ -247,8 +247,8 @@ export const runTerraformApply = async <TVars extends TerraformVariables>(
|
||||
"-compact-warnings",
|
||||
"-input=false",
|
||||
"-auto-approve",
|
||||
"-state",
|
||||
"-no-color",
|
||||
"-state",
|
||||
stateFile,
|
||||
],
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user