From 798cb1d79c9355018ebadb42c93b89ba84aff792 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 11:37:08 +0000 Subject: [PATCH] Add Cursor CLI support to cursor module - Add AgentAPI integration similar to goose module - Support both interactive and non-interactive modes - Include installation and start scripts for cursor-agent - Update README with comprehensive CLI usage examples - Add tests for new CLI functionality - Maintain backward compatibility with desktop app - Configure interactive mode with text output Co-authored-by: matifali <10648092+matifali@users.noreply.github.com> --- registry/coder/modules/cursor/README.md | 149 ++++++++++++++++-- registry/coder/modules/cursor/main.test.ts | 65 +++++--- registry/coder/modules/cursor/main.tf | 126 +++++++++++---- .../coder/modules/cursor/scripts/install.sh | 45 ++++++ .../coder/modules/cursor/scripts/start.sh | 45 ++++++ 5 files changed, 368 insertions(+), 62 deletions(-) create mode 100644 registry/coder/modules/cursor/scripts/install.sh create mode 100644 registry/coder/modules/cursor/scripts/start.sh diff --git a/registry/coder/modules/cursor/README.md b/registry/coder/modules/cursor/README.md index 9dba52b5..509a4d1b 100644 --- a/registry/coder/modules/cursor/README.md +++ b/registry/coder/modules/cursor/README.md @@ -1,36 +1,163 @@ --- -display_name: Cursor IDE -description: Add a one-click button to launch Cursor IDE +display_name: Cursor +description: Run Cursor IDE and CLI in your workspace icon: ../../../../.icons/cursor.svg verified: true -tags: [ide, cursor, ai] +tags: [ide, cursor, ai, cli, agent] --- -# Cursor IDE +# Cursor -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). +Run [Cursor IDE](https://cursor.com) and [Cursor CLI](https://docs.cursor.com/en/cli/overview) in your workspace. Provides both desktop IDE integration and terminal-based AI coding assistance. ```tf module "cursor" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/cursor/coder" - version = "1.2.1" + version = "2.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 + +- **Desktop IDE**: One-click button to launch Cursor IDE (uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)) +- **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 + ## Examples -### Open in a specific directory +### Basic setup with CLI enabled + +```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" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/cursor/coder" + version = "2.0.0" + agent_id = coder_agent.example.id + folder = "/home/coder/project" + install_cursor_cli = true + install_agentapi = true +} +``` + +### Desktop IDE only (legacy mode) + +```tf +module "cursor" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/coder/cursor/coder" + version = "2.0.0" + agent_id = coder_agent.example.id + folder = "/home/coder/project" + install_cursor_cli = false + install_agentapi = false +} +``` + +### With custom pre-install script ```tf module "cursor" { count = data.coder_workspace.me.start_count source = "registry.coder.com/coder/cursor/coder" - version = "1.2.1" + version = "2.0.0" agent_id = coder_agent.example.id - folder = "/home/coder/project" + + pre_install_script = <<-EOT + # Install additional dependencies + npm install -g typescript + EOT } ``` + +## Usage + +### Desktop IDE +Click the "Cursor Desktop" button in your workspace to launch Cursor IDE. + +### CLI Agent + +#### Web Interface +1. Click the "Cursor" 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 + +# Use specific model +cursor-agent -p "add error handling" --model "gpt-5" + +# 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 + +## Configuration + +The module supports the same configuration options as the Cursor CLI: +- **MCP (Model Context Protocol)**: Automatically detects `mcp.json` configuration +- **Rules System**: Supports `.cursor/rules` directory for custom agent behavior +- **Environment Variables**: Respects Cursor CLI environment settings + +## Troubleshooting + +The module creates log files in the workspace's `~/.cursor-module` directory. Check these files if you encounter issues: + +```bash +# Check installation logs +cat ~/.cursor-module/install.log + +# Check runtime logs +cat ~/.cursor-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 diff --git a/registry/coder/modules/cursor/main.test.ts b/registry/coder/modules/cursor/main.test.ts index ed92b9c9..268a8e63 100644 --- a/registry/coder/modules/cursor/main.test.ts +++ b/registry/coder/modules/cursor/main.test.ts @@ -12,29 +12,37 @@ describe("cursor", async () => { agent_id: "foo", }); - it("default output", async () => { + it("default output with CLI enabled", 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", + + // Check desktop app output + expect(state.outputs.cursor_desktop_url.value).toBe( + "cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/home/coder&url=https://mydeployment.coder.com&token=$SESSION_TOKEN", ); + // Check that AgentAPI module is created + const agentapi_module = state.resources.find( + (res) => res.type === "module" && res.name === "agentapi", + ); + expect(agentapi_module).not.toBeNull(); + + // Check desktop app resource const coder_app = state.resources.find( - (res) => res.type === "coder_app" && res.name === "cursor", + (res) => res.type === "coder_app" && res.name === "cursor_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 () => { + it("adds custom folder", async () => { const state = await runTerraformApply(import.meta.dir, { agent_id: "foo", folder: "/foo/bar", }); - expect(state.outputs.cursor_url.value).toBe( + expect(state.outputs.cursor_desktop_url.value).toBe( "cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/foo/bar&url=https://mydeployment.coder.com&token=$SESSION_TOKEN", ); }); @@ -45,29 +53,18 @@ describe("cursor", async () => { folder: "/foo/bar", open_recent: "true", }); - expect(state.outputs.cursor_url.value).toBe( + expect(state.outputs.cursor_desktop_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 () => { + it("adds open_recent with default folder", 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", + expect(state.outputs.cursor_desktop_url.value).toBe( + "cursor://coder.coder-remote/open?owner=default&workspace=default&folder=/home/coder&openRecent&url=https://mydeployment.coder.com&token=$SESSION_TOKEN", ); }); @@ -78,11 +75,31 @@ describe("cursor", async () => { }); const coder_app = state.resources.find( - (res) => res.type === "coder_app" && res.name === "cursor", + (res) => res.type === "coder_app" && res.name === "cursor_desktop", ); expect(coder_app).not.toBeNull(); expect(coder_app?.instances.length).toBe(1); - expect(coder_app?.instances[0].attributes.order).toBe(22); + expect(coder_app?.instances[0].attributes.order).toBe(23); // order + 1 for desktop app + }); + + it("disables CLI installation", async () => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + install_cursor_cli: "false", + install_agentapi: "false", + }); + + // Should still have desktop app + const coder_app = state.resources.find( + (res) => res.type === "coder_app" && res.name === "cursor_desktop", + ); + expect(coder_app).not.toBeNull(); + + // 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(); }); }); diff --git a/registry/coder/modules/cursor/main.tf b/registry/coder/modules/cursor/main.tf index 47d35b62..8292ab6f 100644 --- a/registry/coder/modules/cursor/main.tf +++ b/registry/coder/modules/cursor/main.tf @@ -4,7 +4,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = ">= 2.5" + version = ">= 2.7" } } } @@ -14,17 +14,9 @@ variable "agent_id" { description = "The ID of a Coder agent." } -variable "folder" { - type = string - description = "The folder to open in Cursor IDE." - default = "" -} +data "coder_workspace" "me" {} -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 -} +data "coder_workspace_owner" "me" {} variable "order" { type = number @@ -38,28 +30,108 @@ variable "group" { default = null } -variable "slug" { +variable "icon" { type = string - description = "The slug of the app." - default = "cursor" + description = "The icon to use for the app." + default = "/icon/cursor.svg" } -variable "display_name" { +variable "folder" { type = string - description = "The display name of the app." - default = "Cursor Desktop" + description = "The folder to run Cursor in." + default = "/home/coder" } -data "coder_workspace" "me" {} -data "coder_workspace_owner" "me" {} +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 +} -resource "coder_app" "cursor" { +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 +} + +locals { + app_slug = "cursor" + install_script = file("${path.module}/scripts/install.sh") + start_script = file("${path.module}/scripts/start.sh") + module_dir_name = ".cursor-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_app_slug = "${local.app_slug}-cli" + cli_app_display_name = "Cursor CLI" + 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}' \ + /tmp/install.sh + EOT +} + +# Legacy desktop app for backward compatibility +resource "coder_app" "cursor_desktop" { agent_id = var.agent_id external = true - icon = "/icon/cursor.svg" - slug = var.slug - display_name = var.display_name - order = var.order + icon = var.icon + slug = "cursor-desktop" + display_name = "Cursor Desktop" + order = var.order != null ? var.order + 1 : null group = var.group url = join("", [ "cursor://coder.coder-remote/open", @@ -67,7 +139,7 @@ resource "coder_app" "cursor" { data.coder_workspace_owner.me.name, "&workspace=", data.coder_workspace.me.name, - var.folder != "" ? join("", ["&folder=", var.folder]) : "", + var.folder != "/home/coder" ? join("", ["&folder=", var.folder]) : "", var.open_recent ? "&openRecent" : "", "&url=", data.coder_workspace.me.access_url, @@ -75,7 +147,7 @@ resource "coder_app" "cursor" { ]) } -output "cursor_url" { - value = coder_app.cursor.url +output "cursor_desktop_url" { + value = coder_app.cursor_desktop.url description = "Cursor IDE Desktop URL." } diff --git a/registry/coder/modules/cursor/scripts/install.sh b/registry/coder/modules/cursor/scripts/install.sh new file mode 100644 index 00000000..7d401a84 --- /dev/null +++ b/registry/coder/modules/cursor/scripts/install.sh @@ -0,0 +1,45 @@ +#!/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 "--------------------------------" + +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" +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" diff --git a/registry/coder/modules/cursor/scripts/start.sh b/registry/coder/modules/cursor/scripts/start.sh new file mode 100644 index 00000000..e3bdc09e --- /dev/null +++ b/registry/coder/modules/cursor/scripts/start.sh @@ -0,0 +1,45 @@ +#!/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 " - List sessions: cursor-agent ls" +echo " - Resume session: cursor-agent resume" +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