From 1946b7f56159d63ad69dfaeb60f9368c938bf7f8 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 8 Aug 2025 16:45:16 +0500 Subject: [PATCH 1/6] chore(ci): prettier-format terraform_test_all.sh to satisfy fmt:ci --- scripts/terraform_test_all.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/terraform_test_all.sh b/scripts/terraform_test_all.sh index 3ba9b5b9..01258904 100755 --- a/scripts/terraform_test_all.sh +++ b/scripts/terraform_test_all.sh @@ -6,7 +6,7 @@ set -euo pipefail 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) + (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) From 50144625d5d6c5d087ad67da6ccdebd55e393f9a Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 8 Aug 2025 16:50:21 +0500 Subject: [PATCH 2/6] chore(ci): terraform fmt jetbrains.tftest.hcl to satisfy formatting --- registry/coder/modules/jetbrains/jetbrains.tftest.hcl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/registry/coder/modules/jetbrains/jetbrains.tftest.hcl b/registry/coder/modules/jetbrains/jetbrains.tftest.hcl index 8fe152b5..e5c00a78 100644 --- a/registry/coder/modules/jetbrains/jetbrains.tftest.hcl +++ b/registry/coder/modules/jetbrains/jetbrains.tftest.hcl @@ -103,8 +103,8 @@ run "parameter_order_when_default_empty" { command = plan variables { - agent_id = "foo" - folder = "/home/coder" + agent_id = "foo" + folder = "/home/coder" coder_parameter_order = 5 } From e4bfd018a5333f96ab4373fc4141eeeddd77e976 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 8 Aug 2025 17:42:06 +0500 Subject: [PATCH 3/6] test(terraform): add zed and code-server .tftest.hcl to PR --- .../code-server/code-server.tftest.hcl | 50 +++++++++++++++++++ registry/coder/modules/zed/zed.tftest.hcl | 40 +++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 registry/coder/modules/code-server/code-server.tftest.hcl create mode 100644 registry/coder/modules/zed/zed.tftest.hcl diff --git a/registry/coder/modules/code-server/code-server.tftest.hcl b/registry/coder/modules/code-server/code-server.tftest.hcl new file mode 100644 index 00000000..ebbb7175 --- /dev/null +++ b/registry/coder/modules/code-server/code-server.tftest.hcl @@ -0,0 +1,50 @@ +run "required_vars" { + command = plan + + variables { + agent_id = "foo" + } +} + +run "offline_and_use_cached_conflict" { + command = plan + + variables { + agent_id = "foo" + use_cached = true + offline = true + } + + expect_failures = [ + resource.coder_script.code-server + ] +} + +run "offline_disallows_extensions" { + command = plan + + variables { + agent_id = "foo" + offline = true + extensions = ["ms-python.python", "golang.go"] + } + + expect_failures = [ + resource.coder_script.code-server + ] +} + +run "url_with_folder_query" { + command = plan + + variables { + agent_id = "foo" + folder = "/home/coder/project" + port = 13337 + } + + assert { + condition = resource.coder_app.code-server.url == "http://localhost:13337/?folder=%2Fhome%2Fcoder%2Fproject" + error_message = "coder_app URL must include encoded folder query param" + } +} diff --git a/registry/coder/modules/zed/zed.tftest.hcl b/registry/coder/modules/zed/zed.tftest.hcl new file mode 100644 index 00000000..508b6550 --- /dev/null +++ b/registry/coder/modules/zed/zed.tftest.hcl @@ -0,0 +1,40 @@ +run "default_output" { + command = apply + + variables { + agent_id = "foo" + } + + assert { + condition = output.zed_url == "zed://ssh/default.coder" + error_message = "zed_url did not match expected default URL" + } +} + +run "adds_folder" { + command = apply + + variables { + agent_id = "foo" + folder = "/foo/bar" + } + + assert { + condition = output.zed_url == "zed://ssh/default.coder/foo/bar" + error_message = "zed_url did not include provided folder path" + } +} + +run "adds_agent_name" { + command = apply + + variables { + agent_id = "foo" + agent_name = "myagent" + } + + assert { + condition = output.zed_url == "zed://ssh/myagent.default.default.coder" + error_message = "zed_url did not include agent_name in hostname" + } +} From 31a59ef74387dc413d8e6f23dfd9aded6c134062 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 8 Aug 2025 17:44:59 +0500 Subject: [PATCH 4/6] docs: update docs and samples to use terraform test; route npm test to terraform runner; no direct pushes to main --- CONTRIBUTING.md | 27 +++++++++++++++---------- MAINTAINER.md | 4 ++-- examples/modules/MODULE_NAME.tftest.hcl | 21 +++++++++++++++++++ package.json | 2 +- 4 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 examples/modules/MODULE_NAME.tftest.hcl diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3b2b4d1e..44ac9ab0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ The Coder Registry is a collection of Terraform modules and templates for Coder ### Install Dependencies -Install Bun: +Install Bun (for formatting and scripts): ```bash curl -fsSL https://bun.sh/install | bash @@ -124,19 +124,23 @@ This script generates: - Accurate description and usage examples - Correct icon path (usually `../../../../.icons/your-icon.svg`) - Proper tags that describe your module -3. **Create `main.test.ts`** to test your module +3. **Create at least one `.tftest.hcl`** to test your module with `terraform test` 4. **Add any scripts** or additional files your module needs ### 4. Test and Submit ```bash -# Test your module -bun test -t 'module-name' +# Test your module (from the module directory) +terraform init -upgrade +terraform test -verbose + +# Or run all tests in the repo +./scripts/terraform_test_all.sh # Format code -bun fmt +bun run fmt -# Commit and create PR +# Commit and create PR (do not push to main directly) git add . git commit -m "Add [module-name] module" git push origin your-branch @@ -335,11 +339,12 @@ coder templates push test-[template-name] -d . ### 2. Test Your Changes ```bash -# Test a specific module -bun test -t 'module-name' +# Test a specific module (from the module directory) +terraform init -upgrade +terraform test -verbose # Test all modules -bun test +./scripts/terraform_test_all.sh ``` ### 3. Maintain Backward Compatibility @@ -388,7 +393,7 @@ Example: `https://github.com/coder/registry/compare/main...your-branch?template= ### Every Module Must Have - `main.tf` - Terraform code -- `main.test.ts` - Working tests +- One or more `.tftest.hcl` files - Working tests with `terraform test` - `README.md` - Documentation with frontmatter ### Every Template Must Have @@ -488,6 +493,6 @@ When reporting bugs, include: 2. **No tests** or broken tests 3. **Hardcoded values** instead of variables 4. **Breaking changes** without defaults -5. **Not running** `bun fmt` before submitting +5. **Not running** formatting (`bun run fmt`) and tests (`terraform test`) before submitting Happy contributing! 🚀 diff --git a/MAINTAINER.md b/MAINTAINER.md index 22326574..9959e59c 100644 --- a/MAINTAINER.md +++ b/MAINTAINER.md @@ -18,9 +18,9 @@ sudo apt install golang-go Check that PRs have: -- [ ] All required files (`main.tf`, `main.test.ts`, `README.md`) +- [ ] All required files (`main.tf`, `README.md`, at least one `.tftest.hcl`) - [ ] Proper frontmatter in README -- [ ] Working tests (`bun test`) +- [ ] Working tests (`terraform test`) - [ ] Formatted code (`bun run fmt`) - [ ] Avatar image for new namespaces (`avatar.png` or `avatar.svg` in `.images/`) diff --git a/examples/modules/MODULE_NAME.tftest.hcl b/examples/modules/MODULE_NAME.tftest.hcl new file mode 100644 index 00000000..6f11666b --- /dev/null +++ b/examples/modules/MODULE_NAME.tftest.hcl @@ -0,0 +1,21 @@ +run "plan_with_required_vars" { + command = plan + + variables { + agent_id = "example-agent-id" + } +} + +run "app_url_uses_port" { + command = plan + + variables { + agent_id = "example-agent-id" + port = 19999 + } + + assert { + condition = resource.coder_app.MODULE_NAME.url == "http://localhost:19999" + error_message = "Expected MODULE_NAME app URL to include configured port" + } +} diff --git a/package.json b/package.json index 7ca9f2ec..c2f9ff69 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "fmt": "bun x prettier --write **/*.sh **/*.ts **/*.md *.md && terraform fmt -recursive -diff", "fmt:ci": "bun x prettier --check **/*.sh **/*.ts **/*.md *.md && terraform fmt -check -recursive -diff", "terraform-validate": "./scripts/terraform_validate.sh", - "test": "bun test", + "test": "./scripts/terraform_test_all.sh", "update-version": "./update-version.sh" }, "devDependencies": { From 8ea853ca3066ba69488101d8a23e056486771a74 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Fri, 8 Aug 2025 19:41:26 +0500 Subject: [PATCH 5/6] feat(cursor-cli): add Cursor Agent CLI module (interactive default, MCP settings, model/force) - Runs `cursor-agent` directly (no AgentAPI); interactive chat by default - Supports non-interactive prints (-p) with output-format, model (-m), force (-f) - Merges MCP settings into ~/.cursor/settings.json - Installs via npm (uses nvm if needed); terraform tests added --- .../coder-labs/modules/cursor-cli/README.md | 57 ++++++ .../modules/cursor-cli/cursor-cli.tftest.hcl | 91 +++++++++ .../modules/cursor-cli/main.test.ts | 96 +++++++++ .../coder-labs/modules/cursor-cli/main.tf | 186 ++++++++++++++++++ .../modules/cursor-cli/scripts/install.sh | 91 +++++++++ .../modules/cursor-cli/scripts/start.sh | 92 +++++++++ .../cursor-cli/testdata/cursor-mock.sh | 5 + 7 files changed, 618 insertions(+) create mode 100644 registry/coder-labs/modules/cursor-cli/README.md create mode 100644 registry/coder-labs/modules/cursor-cli/cursor-cli.tftest.hcl create mode 100644 registry/coder-labs/modules/cursor-cli/main.test.ts create mode 100644 registry/coder-labs/modules/cursor-cli/main.tf create mode 100644 registry/coder-labs/modules/cursor-cli/scripts/install.sh create mode 100644 registry/coder-labs/modules/cursor-cli/scripts/start.sh create mode 100644 registry/coder-labs/modules/cursor-cli/testdata/cursor-mock.sh diff --git a/registry/coder-labs/modules/cursor-cli/README.md b/registry/coder-labs/modules/cursor-cli/README.md new file mode 100644 index 00000000..15210c26 --- /dev/null +++ b/registry/coder-labs/modules/cursor-cli/README.md @@ -0,0 +1,57 @@ +--- +display_name: Cursor CLI +icon: ../../../../.icons/cursor.svg +description: Run Cursor CLI agent in your workspace (no AgentAPI) +verified: true +tags: [agent, cursor, ai, cli] +--- + +# Cursor CLI + +Run the Cursor Coding Agent in your workspace using the Cursor CLI directly. This module does not use AgentAPI and executes the Cursor agent process itself. + +- Defaults to interactive mode, with an option for non-interactive mode +- Supports `--force` runs +- Allows configuring MCP servers (settings merge) +- Lets you choose a model and pass extra CLI arguments + +```tf +module "cursor_cli" { + source = "registry.coder.com/coder-labs/cursor-cli/coder" + version = "0.1.0" + agent_id = coder_agent.example.id + + # Optional + folder = "/home/coder/project" + install_cursor_cli = true + cursor_cli_version = "latest" + interactive = true + non_interactive_cmd = "run --once" + force = false + model = "gpt-4o" + additional_settings = jsonencode({ + mcpServers = { + coder = { + command = "coder" + args = ["exp", "mcp", "server"] + type = "stdio" + name = "Coder" + env = {} + enabled = true + } + } + }) + extra_args = ["--verbose"] +} +``` + +## Notes + +- See Cursor CLI docs: `https://docs.cursor.com/en/cli/overview` +- The module writes merged settings to `~/.cursor/settings.json` +- Interactive by default; set `interactive = false` to run non-interactively via `non_interactive_cmd` + +## Troubleshooting + +- Ensure the CLI is installed (enable `install_cursor_cli = true` or preinstall it in your image) +- Logs are written to `~/.cursor-cli-module/` diff --git a/registry/coder-labs/modules/cursor-cli/cursor-cli.tftest.hcl b/registry/coder-labs/modules/cursor-cli/cursor-cli.tftest.hcl new file mode 100644 index 00000000..f05a6dc9 --- /dev/null +++ b/registry/coder-labs/modules/cursor-cli/cursor-cli.tftest.hcl @@ -0,0 +1,91 @@ +// Terraform tests for the cursor-cli module +// Validates that we render expected script content given inputs + +run "defaults_interactive" { + command = plan + + variables { + agent_id = "test-agent" + } + + assert { + condition = can(regex("INTERACTIVE='true'", resource.coder_script.cursor_cli.script)) + error_message = "Expected INTERACTIVE default to be true" + } + + assert { + condition = can(regex("BINARY_NAME='cursor-agent'", resource.coder_script.cursor_cli.script)) + error_message = "Expected default binary_name to be cursor-agent" + } +} + +run "non_interactive_mode" { + command = plan + + variables { + agent_id = "test-agent" + interactive = false + non_interactive_cmd = "run --once" + } + + assert { + condition = can(regex("INTERACTIVE='false'", resource.coder_script.cursor_cli.script)) + error_message = "Expected INTERACTIVE to be false when interactive=false" + } + + assert { + condition = can(regex("NON_INTERACTIVE_CMD='run --once'", resource.coder_script.cursor_cli.script)) + error_message = "Expected NON_INTERACTIVE_CMD to be propagated" + } +} + +run "model_and_force" { + command = plan + + variables { + agent_id = "test-agent" + model = "test-model" + force = true + } + + assert { + condition = can(regex("MODEL='test-model'", resource.coder_script.cursor_cli.script)) + error_message = "Expected MODEL to be propagated" + } + + assert { + condition = can(regex("FORCE='true'", resource.coder_script.cursor_cli.script)) + error_message = "Expected FORCE true to be propagated" + } +} + +run "additional_settings_propagated" { + command = plan + + variables { + agent_id = "test-agent" + additional_settings = jsonencode({ + mcpServers = { + coder = { + command = "coder" + args = ["exp", "mcp", "server"] + type = "stdio" + } + } + }) + } + + // Ensure the encoded settings are passed into the install invocation + assert { + condition = can(regex(base64encode(jsonencode({ + mcpServers = { + coder = { + command = "coder" + args = ["exp", "mcp", "server"] + type = "stdio" + } + } + })), resource.coder_script.cursor_cli.script)) + error_message = "Expected ADDITIONAL_SETTINGS (base64) to be in the install step" + } +} diff --git a/registry/coder-labs/modules/cursor-cli/main.test.ts b/registry/coder-labs/modules/cursor-cli/main.test.ts new file mode 100644 index 00000000..421f90b4 --- /dev/null +++ b/registry/coder-labs/modules/cursor-cli/main.test.ts @@ -0,0 +1,96 @@ +import { test, afterEach, describe, setDefaultTimeout, beforeAll, expect } from "bun:test"; +import { execContainer, readFileContainer, runTerraformInit, runTerraformApply, writeFileContainer, runContainer, removeContainer, findResourceInstance } from "~test"; +import dedent from "dedent"; + +let cleanupFunctions: (() => Promise)[] = []; +const registerCleanup = (cleanup: () => Promise) => { + cleanupFunctions.push(cleanup); +}; + +afterEach(async () => { + const cleanupFnsCopy = cleanupFunctions.slice().reverse(); + cleanupFunctions = []; + for (const cleanup of cleanupFnsCopy) { + try { + await cleanup(); + } catch (error) { + console.error("Error during cleanup:", error); + } + } +}); + +const writeExecutable = async (containerId: string, filePath: string, content: string) => { + await writeFileContainer(containerId, filePath, content, { user: "root" }); + await execContainer(containerId, ["bash", "-c", `chmod 755 ${filePath}`], ["--user", "root"]); +}; + +const loadTestFile = async (...relativePath: string[]) => { + return await Bun.file(new URL(`./testdata/${relativePath.join("/")}`, import.meta.url)).text(); +}; + +const setup = async (vars?: Record): Promise<{ id: string }> => { + const state = await runTerraformApply(import.meta.dir, { + agent_id: "foo", + install_cursor_cli: "false", + ...vars, + }); + const coderScript = findResourceInstance(state, "coder_script"); + const id = await runContainer("codercom/enterprise-node:latest"); + registerCleanup(async () => removeContainer(id)); + await writeExecutable(id, "/home/coder/script.sh", coderScript.script); + await writeExecutable(id, "/usr/bin/cursor", await loadTestFile("cursor-mock.sh")); + return { id }; +}; + +setDefaultTimeout(60 * 1000); + +describe("cursor-cli", async () => { + beforeAll(async () => { + await runTerraformInit(import.meta.dir); + }); + + test("happy-path-interactive", async () => { + const { id } = await setup(); + const resp = await execContainer(id, ["bash", "-c", "cd /home/coder && ./script.sh"]); + if (resp.exitCode !== 0) { + console.log(resp.stdout); + console.log(resp.stderr); + } + expect(resp.exitCode).toBe(0); + const startLog = await readFileContainer(id, "/home/coder/.cursor-cli-module/start.log"); + expect(startLog).toContain("agent"); + expect(startLog).toContain("--interactive"); + }); + + test("non-interactive-with-cmd", async () => { + const { id } = await setup({ interactive: "false", non_interactive_cmd: "run --once" }); + const resp = await execContainer(id, ["bash", "-c", "cd /home/coder && ./script.sh"]); + expect(resp.exitCode).toBe(0); + const startLog = await readFileContainer(id, "/home/coder/.cursor-cli-module/start.log"); + expect(startLog).toContain("run"); + expect(startLog).toContain("--once"); + expect(startLog).not.toContain("--interactive"); + }); + + test("model-and-force-and-extra-args", async () => { + const { id } = await setup({ model: "test-model", force: "true" }); + const resp = await execContainer(id, ["bash", "-c", "cd /home/coder && ./script.sh"], ["--env", "TF_VAR_extra_args=--foo\nbar"]); + expect(resp.exitCode).toBe(0); + const startLog = await readFileContainer(id, "/home/coder/.cursor-cli-module/start.log"); + expect(startLog).toContain("--model"); + expect(startLog).toContain("test-model"); + expect(startLog).toContain("--force"); + }); + + test("additional-settings-merge", async () => { + const settings = dedent` + {"mcpServers": {"coder": {"command": "coder", "args": ["exp","mcp","server"], "type": "stdio"}}} + `; + const { id } = await setup({ additional_settings: settings }); + const resp = await execContainer(id, ["bash", "-c", "cd /home/coder && ./script.sh"]); + expect(resp.exitCode).toBe(0); + const cfg = await readFileContainer(id, "/home/coder/.cursor/settings.json"); + expect(cfg).toContain("mcpServers"); + expect(cfg).toContain("coder"); + }); +}); diff --git a/registry/coder-labs/modules/cursor-cli/main.tf b/registry/coder-labs/modules/cursor-cli/main.tf new file mode 100644 index 00000000..07c4a2a6 --- /dev/null +++ b/registry/coder-labs/modules/cursor-cli/main.tf @@ -0,0 +1,186 @@ +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 "cursor_cli_version" { + type = string + description = "The version of Cursor CLI to install (latest for latest)." + default = "latest" +} + +variable "interactive" { + type = bool + description = "Run in interactive chat mode (default)." + default = true +} + +variable "initial_prompt" { + type = string + description = "Initial prompt to start the chat with (passed as trailing arg)." + default = "" +} + +variable "non_interactive_cmd" { + type = string + description = "Additional arguments appended when interactive=false (advanced usage)." + default = "" +} + +variable "force" { + type = bool + description = "Pass -f/--force to allow commands unless explicitly denied." + default = false +} + +variable "model" { + type = string + description = "Pass -m/--model to select model (e.g., sonnet-4, gpt-5)." + default = "" +} + +variable "output_format" { + type = string + description = "Output format with -p: text, json, or stream-json." + default = "" +} + +variable "api_key" { + type = string + description = "API key (sets CURSOR_API_KEY env or pass via -a)." + default = "" + sensitive = true +} + +variable "extra_args" { + type = list(string) + description = "Additional args to pass to the Cursor CLI." + default = [] +} + +variable "binary_name" { + type = string + description = "Cursor Agent binary name (default: cursor-agent)." + default = "cursor-agent" +} + +variable "base_command" { + type = string + description = "Base Cursor CLI command to run (default: none for chat)." + default = "" +} + +variable "additional_settings" { + type = string + description = "JSON to merge into ~/.cursor/settings.json (e.g., mcpServers)." + default = "" +} + +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" +} + +resource "coder_script" "cursor_cli" { + agent_id = var.agent_id + display_name = "Cursor CLI" + icon = var.icon + 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_INSTALL='${var.install_cursor_cli}' \ + ARG_VERSION='${var.cursor_cli_version}' \ + ADDITIONAL_SETTINGS='${base64encode(replace(var.additional_settings, "'", "'\\''"))}' \ + MODULE_DIR_NAME='${local.module_dir_name}' \ + FOLDER='${var.folder}' \ + /tmp/install.sh | tee "$HOME/${local.module_dir_name}/install.log" + + echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh + chmod +x /tmp/start.sh + INTERACTIVE='${var.interactive}' \ + INITIAL_PROMPT='${replace(var.initial_prompt, "'", "'\\''")}' \ + NON_INTERACTIVE_CMD='${replace(var.non_interactive_cmd, "'", "'\\''")}' \ + BASE_COMMAND='${var.base_command}' \ + FORCE='${var.force}' \ + MODEL='${var.model}' \ + OUTPUT_FORMAT='${var.output_format}' \ + API_KEY_SECRET='${var.api_key}' \ + EXTRA_ARGS='${base64encode(join("\n", var.extra_args))}' \ + MODULE_DIR_NAME='${local.module_dir_name}' \ + FOLDER='${var.folder}' \ + BINARY_NAME='${var.binary_name}' \ + /tmp/start.sh | tee "$HOME/${local.module_dir_name}/start.log" + EOT + run_on_start = true +} + +resource "coder_app" "cursor_cli" { + agent_id = var.agent_id + slug = local.app_slug + display_name = "Cursor CLI" + icon = var.icon + order = var.order + group = var.group + command = <<-EOT + #!/bin/bash + set -o errexit + set -o pipefail + if [ -f "$HOME/${local.module_dir_name}/start.log" ]; then + tail -n +1 -f "$HOME/${local.module_dir_name}/start.log" + else + echo "Cursor CLI not started yet. Check install/start logs in $HOME/${local.module_dir_name}/" + /bin/bash + fi + EOT +} diff --git a/registry/coder-labs/modules/cursor-cli/scripts/install.sh b/registry/coder-labs/modules/cursor-cli/scripts/install.sh new file mode 100644 index 00000000..efdbdc97 --- /dev/null +++ b/registry/coder-labs/modules/cursor-cli/scripts/install.sh @@ -0,0 +1,91 @@ +#!/bin/bash + +set -o errexit +set -o pipefail + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +# Inputs +ARG_INSTALL=${ARG_INSTALL:-true} +ARG_VERSION=${ARG_VERSION:-latest} +MODULE_DIR_NAME=${MODULE_DIR_NAME:-.cursor-cli-module} +FOLDER=${FOLDER:-$HOME} + +mkdir -p "$HOME/$MODULE_DIR_NAME" + +ADDITIONAL_SETTINGS=$(echo -n "$ADDITIONAL_SETTINGS" | base64 -d) + +{ + echo "--------------------------------" + echo "install: $ARG_INSTALL" + echo "version: $ARG_VERSION" + echo "folder: $FOLDER" + echo "--------------------------------" +} | tee -a "$HOME/$MODULE_DIR_NAME/install.log" + +# Install Cursor Agent CLI if requested. +# The docs show Cursor Agent CLI usage; we will install via npm globally. +# This requires Node/npm; install Node via NVM if not present (similar to gemini module approach). +if [ "$ARG_INSTALL" = "true" ]; then + echo "Installing Cursor Agent CLI..." | tee -a "$HOME/$MODULE_DIR_NAME/install.log" + + install_node() { + if ! command_exists npm; then + if ! command_exists node; then + export NVM_DIR="$HOME/.nvm" + if [ ! -d "$NVM_DIR" ]; then + mkdir -p "$NVM_DIR" + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + else + [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" + fi + nvm install --lts + nvm use --lts + nvm alias default node + else + echo "Node is installed but npm missing; please install npm manually." | tee -a "$HOME/$MODULE_DIR_NAME/install.log" + fi + fi + } + + install_node + + # If nvm not present, create local npm global dir to avoid permissions issues + if ! command_exists nvm; then + mkdir -p "$HOME/.npm-global" + npm config set prefix "$HOME/.npm-global" + export PATH="$HOME/.npm-global/bin:$PATH" + if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" "$HOME/.bashrc" 2>/dev/null; then + echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> "$HOME/.bashrc" + fi + fi + + if [ -n "$ARG_VERSION" ] && [ "$ARG_VERSION" != "latest" ]; then + npm install -g "cursor-agent@$ARG_VERSION" 2>&1 | tee -a "$HOME/$MODULE_DIR_NAME/install.log" + else + npm install -g cursor-agent 2>&1 | tee -a "$HOME/$MODULE_DIR_NAME/install.log" + fi + + echo "Installed cursor-agent: $(command -v cursor-agent || true)" | tee -a "$HOME/$MODULE_DIR_NAME/install.log" +fi + +# Ensure settings path exists and merge additional_settings JSON +SETTINGS_PATH="$HOME/.cursor/settings.json" +mkdir -p "$(dirname "$SETTINGS_PATH")" + +# If settings file doesn't exist, initialize basic structure +if [ ! -f "$SETTINGS_PATH" ]; then + echo '{}' > "$SETTINGS_PATH" +fi + +if [ -n "$ADDITIONAL_SETTINGS" ]; then + echo "Merging additional settings into $SETTINGS_PATH" | tee -a "$HOME/$MODULE_DIR_NAME/install.log" + TMP_SETTINGS=$(mktemp) + # Merge JSON: deep merge mcpServers and top-level keys + jq --argjson add "$ADDITIONAL_SETTINGS" 'def deepmerge(a;b): reduce (b|keys[]) as $key (a; .[$key] = if ( (.[ $key ]|type?) == "object" and (b[$key]|type?) == "object" ) then deepmerge(.[ $key ]; b[$key]) else b[$key] end); deepmerge(.;$add)' "$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH" +fi + +exit 0 diff --git a/registry/coder-labs/modules/cursor-cli/scripts/start.sh b/registry/coder-labs/modules/cursor-cli/scripts/start.sh new file mode 100644 index 00000000..27f2e394 --- /dev/null +++ b/registry/coder-labs/modules/cursor-cli/scripts/start.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +set -o errexit +set -o pipefail + +command_exists() { + command -v "$1" >/dev/null 2>&1 +} + +INTERACTIVE=${INTERACTIVE:-true} +INITIAL_PROMPT=${INITIAL_PROMPT:-} +NON_INTERACTIVE_CMD=${NON_INTERACTIVE_CMD:-} +FORCE=${FORCE:-false} +MODEL=${MODEL:-} +OUTPUT_FORMAT=${OUTPUT_FORMAT:-} +API_KEY_SECRET=${API_KEY_SECRET:-} +EXTRA_ARGS_BASE64=${EXTRA_ARGS:-} +MODULE_DIR_NAME=${MODULE_DIR_NAME:-.cursor-cli-module} +FOLDER=${FOLDER:-$HOME} +BINARY_NAME=${BINARY_NAME:-cursor-agent} + +mkdir -p "$HOME/$MODULE_DIR_NAME" + +# Decode EXTRA_ARGS lines into an array +IFS=$'\n' read -r -d '' -a EXTRA_ARR < <(echo -n "$EXTRA_ARGS_BASE64" | base64 -d; printf '\0') || true + +# Find cursor agent cli +if command_exists "$BINARY_NAME"; then + CURSOR_CMD="$BINARY_NAME" +elif [ -x "$HOME/.local/bin/$BINARY_NAME" ]; then + CURSOR_CMD="$HOME/.local/bin/$BINARY_NAME" +else + echo "Error: $BINARY_NAME not found. Install it or set install_cursor_cli=true." | tee -a "$HOME/$MODULE_DIR_NAME/start.log" + exit 1 +fi + +# Ensure working directory exists +if [ -d "$FOLDER" ]; then + cd "$FOLDER" +else + mkdir -p "$FOLDER" + cd "$FOLDER" +fi + +ARGS=() + +# base command: if provided, append; otherwise chat mode (no command) +if [ -n "${BASE_COMMAND:-}" ]; then + ARGS+=("${BASE_COMMAND}") +fi + +# global flags +if [ -n "$MODEL" ]; then + ARGS+=("-m" "$MODEL") +fi +if [ "$FORCE" = "true" ]; then + ARGS+=("-f") +fi + +# Non-interactive printing flags +PRINT_TO_CONSOLE=false +if [ "$INTERACTIVE" != "true" ]; then + PRINT_TO_CONSOLE=true + ARGS+=("-p") + if [ -n "$OUTPUT_FORMAT" ]; then + ARGS+=("--output-format" "$OUTPUT_FORMAT") + fi + if [ -n "$NON_INTERACTIVE_CMD" ]; then + # shellcheck disable=SC2206 + CMD_PARTS=($NON_INTERACTIVE_CMD) + ARGS+=("${CMD_PARTS[@]}") + fi +fi + +# Extra args, if any +if [ ${#EXTRA_ARR[@]} -gt 0 ]; then + ARGS+=("${EXTRA_ARR[@]}") +fi + +# If initial prompt specified (chat mode), pass as trailing arg +if [ -n "$INITIAL_PROMPT" ]; then + ARGS+=("$INITIAL_PROMPT") +fi + +# Set API key env if provided +if [ -n "$API_KEY_SECRET" ]; then + export CURSOR_API_KEY="$API_KEY_SECRET" +fi + +# Log and exec +printf "Running: %q %s\n" "$CURSOR_CMD" "$(printf '%q ' "${ARGS[@]}")" | tee -a "$HOME/$MODULE_DIR_NAME/start.log" +exec "$CURSOR_CMD" "${ARGS[@]}" diff --git a/registry/coder-labs/modules/cursor-cli/testdata/cursor-mock.sh b/registry/coder-labs/modules/cursor-cli/testdata/cursor-mock.sh new file mode 100644 index 00000000..fd160696 --- /dev/null +++ b/registry/coder-labs/modules/cursor-cli/testdata/cursor-mock.sh @@ -0,0 +1,5 @@ +#!/bin/sh +# minimal mock that prints args and exits +printf "cursor mock invoked with: %s\n" "$*" +# Exit successfully regardless +exit 0 From 03198c886014e3f021f4599e0491c89dad9a7ea3 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Sat, 9 Aug 2025 00:11:38 +0500 Subject: [PATCH 6/6] Revert "feat(cursor-cli): add Cursor Agent CLI module (interactive default, MCP settings, model/force)" This reverts commit 8ea853ca3066ba69488101d8a23e056486771a74. --- .../coder-labs/modules/cursor-cli/README.md | 57 ------ .../modules/cursor-cli/cursor-cli.tftest.hcl | 91 --------- .../modules/cursor-cli/main.test.ts | 96 --------- .../coder-labs/modules/cursor-cli/main.tf | 186 ------------------ .../modules/cursor-cli/scripts/install.sh | 91 --------- .../modules/cursor-cli/scripts/start.sh | 92 --------- .../cursor-cli/testdata/cursor-mock.sh | 5 - 7 files changed, 618 deletions(-) delete mode 100644 registry/coder-labs/modules/cursor-cli/README.md delete mode 100644 registry/coder-labs/modules/cursor-cli/cursor-cli.tftest.hcl delete mode 100644 registry/coder-labs/modules/cursor-cli/main.test.ts delete mode 100644 registry/coder-labs/modules/cursor-cli/main.tf delete mode 100644 registry/coder-labs/modules/cursor-cli/scripts/install.sh delete mode 100644 registry/coder-labs/modules/cursor-cli/scripts/start.sh delete mode 100644 registry/coder-labs/modules/cursor-cli/testdata/cursor-mock.sh diff --git a/registry/coder-labs/modules/cursor-cli/README.md b/registry/coder-labs/modules/cursor-cli/README.md deleted file mode 100644 index 15210c26..00000000 --- a/registry/coder-labs/modules/cursor-cli/README.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -display_name: Cursor CLI -icon: ../../../../.icons/cursor.svg -description: Run Cursor CLI agent in your workspace (no AgentAPI) -verified: true -tags: [agent, cursor, ai, cli] ---- - -# Cursor CLI - -Run the Cursor Coding Agent in your workspace using the Cursor CLI directly. This module does not use AgentAPI and executes the Cursor agent process itself. - -- Defaults to interactive mode, with an option for non-interactive mode -- Supports `--force` runs -- Allows configuring MCP servers (settings merge) -- Lets you choose a model and pass extra CLI arguments - -```tf -module "cursor_cli" { - source = "registry.coder.com/coder-labs/cursor-cli/coder" - version = "0.1.0" - agent_id = coder_agent.example.id - - # Optional - folder = "/home/coder/project" - install_cursor_cli = true - cursor_cli_version = "latest" - interactive = true - non_interactive_cmd = "run --once" - force = false - model = "gpt-4o" - additional_settings = jsonencode({ - mcpServers = { - coder = { - command = "coder" - args = ["exp", "mcp", "server"] - type = "stdio" - name = "Coder" - env = {} - enabled = true - } - } - }) - extra_args = ["--verbose"] -} -``` - -## Notes - -- See Cursor CLI docs: `https://docs.cursor.com/en/cli/overview` -- The module writes merged settings to `~/.cursor/settings.json` -- Interactive by default; set `interactive = false` to run non-interactively via `non_interactive_cmd` - -## Troubleshooting - -- Ensure the CLI is installed (enable `install_cursor_cli = true` or preinstall it in your image) -- Logs are written to `~/.cursor-cli-module/` diff --git a/registry/coder-labs/modules/cursor-cli/cursor-cli.tftest.hcl b/registry/coder-labs/modules/cursor-cli/cursor-cli.tftest.hcl deleted file mode 100644 index f05a6dc9..00000000 --- a/registry/coder-labs/modules/cursor-cli/cursor-cli.tftest.hcl +++ /dev/null @@ -1,91 +0,0 @@ -// Terraform tests for the cursor-cli module -// Validates that we render expected script content given inputs - -run "defaults_interactive" { - command = plan - - variables { - agent_id = "test-agent" - } - - assert { - condition = can(regex("INTERACTIVE='true'", resource.coder_script.cursor_cli.script)) - error_message = "Expected INTERACTIVE default to be true" - } - - assert { - condition = can(regex("BINARY_NAME='cursor-agent'", resource.coder_script.cursor_cli.script)) - error_message = "Expected default binary_name to be cursor-agent" - } -} - -run "non_interactive_mode" { - command = plan - - variables { - agent_id = "test-agent" - interactive = false - non_interactive_cmd = "run --once" - } - - assert { - condition = can(regex("INTERACTIVE='false'", resource.coder_script.cursor_cli.script)) - error_message = "Expected INTERACTIVE to be false when interactive=false" - } - - assert { - condition = can(regex("NON_INTERACTIVE_CMD='run --once'", resource.coder_script.cursor_cli.script)) - error_message = "Expected NON_INTERACTIVE_CMD to be propagated" - } -} - -run "model_and_force" { - command = plan - - variables { - agent_id = "test-agent" - model = "test-model" - force = true - } - - assert { - condition = can(regex("MODEL='test-model'", resource.coder_script.cursor_cli.script)) - error_message = "Expected MODEL to be propagated" - } - - assert { - condition = can(regex("FORCE='true'", resource.coder_script.cursor_cli.script)) - error_message = "Expected FORCE true to be propagated" - } -} - -run "additional_settings_propagated" { - command = plan - - variables { - agent_id = "test-agent" - additional_settings = jsonencode({ - mcpServers = { - coder = { - command = "coder" - args = ["exp", "mcp", "server"] - type = "stdio" - } - } - }) - } - - // Ensure the encoded settings are passed into the install invocation - assert { - condition = can(regex(base64encode(jsonencode({ - mcpServers = { - coder = { - command = "coder" - args = ["exp", "mcp", "server"] - type = "stdio" - } - } - })), resource.coder_script.cursor_cli.script)) - error_message = "Expected ADDITIONAL_SETTINGS (base64) to be in the install step" - } -} diff --git a/registry/coder-labs/modules/cursor-cli/main.test.ts b/registry/coder-labs/modules/cursor-cli/main.test.ts deleted file mode 100644 index 421f90b4..00000000 --- a/registry/coder-labs/modules/cursor-cli/main.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { test, afterEach, describe, setDefaultTimeout, beforeAll, expect } from "bun:test"; -import { execContainer, readFileContainer, runTerraformInit, runTerraformApply, writeFileContainer, runContainer, removeContainer, findResourceInstance } from "~test"; -import dedent from "dedent"; - -let cleanupFunctions: (() => Promise)[] = []; -const registerCleanup = (cleanup: () => Promise) => { - cleanupFunctions.push(cleanup); -}; - -afterEach(async () => { - const cleanupFnsCopy = cleanupFunctions.slice().reverse(); - cleanupFunctions = []; - for (const cleanup of cleanupFnsCopy) { - try { - await cleanup(); - } catch (error) { - console.error("Error during cleanup:", error); - } - } -}); - -const writeExecutable = async (containerId: string, filePath: string, content: string) => { - await writeFileContainer(containerId, filePath, content, { user: "root" }); - await execContainer(containerId, ["bash", "-c", `chmod 755 ${filePath}`], ["--user", "root"]); -}; - -const loadTestFile = async (...relativePath: string[]) => { - return await Bun.file(new URL(`./testdata/${relativePath.join("/")}`, import.meta.url)).text(); -}; - -const setup = async (vars?: Record): Promise<{ id: string }> => { - const state = await runTerraformApply(import.meta.dir, { - agent_id: "foo", - install_cursor_cli: "false", - ...vars, - }); - const coderScript = findResourceInstance(state, "coder_script"); - const id = await runContainer("codercom/enterprise-node:latest"); - registerCleanup(async () => removeContainer(id)); - await writeExecutable(id, "/home/coder/script.sh", coderScript.script); - await writeExecutable(id, "/usr/bin/cursor", await loadTestFile("cursor-mock.sh")); - return { id }; -}; - -setDefaultTimeout(60 * 1000); - -describe("cursor-cli", async () => { - beforeAll(async () => { - await runTerraformInit(import.meta.dir); - }); - - test("happy-path-interactive", async () => { - const { id } = await setup(); - const resp = await execContainer(id, ["bash", "-c", "cd /home/coder && ./script.sh"]); - if (resp.exitCode !== 0) { - console.log(resp.stdout); - console.log(resp.stderr); - } - expect(resp.exitCode).toBe(0); - const startLog = await readFileContainer(id, "/home/coder/.cursor-cli-module/start.log"); - expect(startLog).toContain("agent"); - expect(startLog).toContain("--interactive"); - }); - - test("non-interactive-with-cmd", async () => { - const { id } = await setup({ interactive: "false", non_interactive_cmd: "run --once" }); - const resp = await execContainer(id, ["bash", "-c", "cd /home/coder && ./script.sh"]); - expect(resp.exitCode).toBe(0); - const startLog = await readFileContainer(id, "/home/coder/.cursor-cli-module/start.log"); - expect(startLog).toContain("run"); - expect(startLog).toContain("--once"); - expect(startLog).not.toContain("--interactive"); - }); - - test("model-and-force-and-extra-args", async () => { - const { id } = await setup({ model: "test-model", force: "true" }); - const resp = await execContainer(id, ["bash", "-c", "cd /home/coder && ./script.sh"], ["--env", "TF_VAR_extra_args=--foo\nbar"]); - expect(resp.exitCode).toBe(0); - const startLog = await readFileContainer(id, "/home/coder/.cursor-cli-module/start.log"); - expect(startLog).toContain("--model"); - expect(startLog).toContain("test-model"); - expect(startLog).toContain("--force"); - }); - - test("additional-settings-merge", async () => { - const settings = dedent` - {"mcpServers": {"coder": {"command": "coder", "args": ["exp","mcp","server"], "type": "stdio"}}} - `; - const { id } = await setup({ additional_settings: settings }); - const resp = await execContainer(id, ["bash", "-c", "cd /home/coder && ./script.sh"]); - expect(resp.exitCode).toBe(0); - const cfg = await readFileContainer(id, "/home/coder/.cursor/settings.json"); - expect(cfg).toContain("mcpServers"); - expect(cfg).toContain("coder"); - }); -}); diff --git a/registry/coder-labs/modules/cursor-cli/main.tf b/registry/coder-labs/modules/cursor-cli/main.tf deleted file mode 100644 index 07c4a2a6..00000000 --- a/registry/coder-labs/modules/cursor-cli/main.tf +++ /dev/null @@ -1,186 +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 "cursor_cli_version" { - type = string - description = "The version of Cursor CLI to install (latest for latest)." - default = "latest" -} - -variable "interactive" { - type = bool - description = "Run in interactive chat mode (default)." - default = true -} - -variable "initial_prompt" { - type = string - description = "Initial prompt to start the chat with (passed as trailing arg)." - default = "" -} - -variable "non_interactive_cmd" { - type = string - description = "Additional arguments appended when interactive=false (advanced usage)." - default = "" -} - -variable "force" { - type = bool - description = "Pass -f/--force to allow commands unless explicitly denied." - default = false -} - -variable "model" { - type = string - description = "Pass -m/--model to select model (e.g., sonnet-4, gpt-5)." - default = "" -} - -variable "output_format" { - type = string - description = "Output format with -p: text, json, or stream-json." - default = "" -} - -variable "api_key" { - type = string - description = "API key (sets CURSOR_API_KEY env or pass via -a)." - default = "" - sensitive = true -} - -variable "extra_args" { - type = list(string) - description = "Additional args to pass to the Cursor CLI." - default = [] -} - -variable "binary_name" { - type = string - description = "Cursor Agent binary name (default: cursor-agent)." - default = "cursor-agent" -} - -variable "base_command" { - type = string - description = "Base Cursor CLI command to run (default: none for chat)." - default = "" -} - -variable "additional_settings" { - type = string - description = "JSON to merge into ~/.cursor/settings.json (e.g., mcpServers)." - default = "" -} - -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" -} - -resource "coder_script" "cursor_cli" { - agent_id = var.agent_id - display_name = "Cursor CLI" - icon = var.icon - 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_INSTALL='${var.install_cursor_cli}' \ - ARG_VERSION='${var.cursor_cli_version}' \ - ADDITIONAL_SETTINGS='${base64encode(replace(var.additional_settings, "'", "'\\''"))}' \ - MODULE_DIR_NAME='${local.module_dir_name}' \ - FOLDER='${var.folder}' \ - /tmp/install.sh | tee "$HOME/${local.module_dir_name}/install.log" - - echo -n '${base64encode(local.start_script)}' | base64 -d > /tmp/start.sh - chmod +x /tmp/start.sh - INTERACTIVE='${var.interactive}' \ - INITIAL_PROMPT='${replace(var.initial_prompt, "'", "'\\''")}' \ - NON_INTERACTIVE_CMD='${replace(var.non_interactive_cmd, "'", "'\\''")}' \ - BASE_COMMAND='${var.base_command}' \ - FORCE='${var.force}' \ - MODEL='${var.model}' \ - OUTPUT_FORMAT='${var.output_format}' \ - API_KEY_SECRET='${var.api_key}' \ - EXTRA_ARGS='${base64encode(join("\n", var.extra_args))}' \ - MODULE_DIR_NAME='${local.module_dir_name}' \ - FOLDER='${var.folder}' \ - BINARY_NAME='${var.binary_name}' \ - /tmp/start.sh | tee "$HOME/${local.module_dir_name}/start.log" - EOT - run_on_start = true -} - -resource "coder_app" "cursor_cli" { - agent_id = var.agent_id - slug = local.app_slug - display_name = "Cursor CLI" - icon = var.icon - order = var.order - group = var.group - command = <<-EOT - #!/bin/bash - set -o errexit - set -o pipefail - if [ -f "$HOME/${local.module_dir_name}/start.log" ]; then - tail -n +1 -f "$HOME/${local.module_dir_name}/start.log" - else - echo "Cursor CLI not started yet. Check install/start logs in $HOME/${local.module_dir_name}/" - /bin/bash - fi - EOT -} diff --git a/registry/coder-labs/modules/cursor-cli/scripts/install.sh b/registry/coder-labs/modules/cursor-cli/scripts/install.sh deleted file mode 100644 index efdbdc97..00000000 --- a/registry/coder-labs/modules/cursor-cli/scripts/install.sh +++ /dev/null @@ -1,91 +0,0 @@ -#!/bin/bash - -set -o errexit -set -o pipefail - -command_exists() { - command -v "$1" >/dev/null 2>&1 -} - -# Inputs -ARG_INSTALL=${ARG_INSTALL:-true} -ARG_VERSION=${ARG_VERSION:-latest} -MODULE_DIR_NAME=${MODULE_DIR_NAME:-.cursor-cli-module} -FOLDER=${FOLDER:-$HOME} - -mkdir -p "$HOME/$MODULE_DIR_NAME" - -ADDITIONAL_SETTINGS=$(echo -n "$ADDITIONAL_SETTINGS" | base64 -d) - -{ - echo "--------------------------------" - echo "install: $ARG_INSTALL" - echo "version: $ARG_VERSION" - echo "folder: $FOLDER" - echo "--------------------------------" -} | tee -a "$HOME/$MODULE_DIR_NAME/install.log" - -# Install Cursor Agent CLI if requested. -# The docs show Cursor Agent CLI usage; we will install via npm globally. -# This requires Node/npm; install Node via NVM if not present (similar to gemini module approach). -if [ "$ARG_INSTALL" = "true" ]; then - echo "Installing Cursor Agent CLI..." | tee -a "$HOME/$MODULE_DIR_NAME/install.log" - - install_node() { - if ! command_exists npm; then - if ! command_exists node; then - export NVM_DIR="$HOME/.nvm" - if [ ! -d "$NVM_DIR" ]; then - mkdir -p "$NVM_DIR" - curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - else - [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" - fi - nvm install --lts - nvm use --lts - nvm alias default node - else - echo "Node is installed but npm missing; please install npm manually." | tee -a "$HOME/$MODULE_DIR_NAME/install.log" - fi - fi - } - - install_node - - # If nvm not present, create local npm global dir to avoid permissions issues - if ! command_exists nvm; then - mkdir -p "$HOME/.npm-global" - npm config set prefix "$HOME/.npm-global" - export PATH="$HOME/.npm-global/bin:$PATH" - if ! grep -q "export PATH=$HOME/.npm-global/bin:\$PATH" "$HOME/.bashrc" 2>/dev/null; then - echo "export PATH=$HOME/.npm-global/bin:\$PATH" >> "$HOME/.bashrc" - fi - fi - - if [ -n "$ARG_VERSION" ] && [ "$ARG_VERSION" != "latest" ]; then - npm install -g "cursor-agent@$ARG_VERSION" 2>&1 | tee -a "$HOME/$MODULE_DIR_NAME/install.log" - else - npm install -g cursor-agent 2>&1 | tee -a "$HOME/$MODULE_DIR_NAME/install.log" - fi - - echo "Installed cursor-agent: $(command -v cursor-agent || true)" | tee -a "$HOME/$MODULE_DIR_NAME/install.log" -fi - -# Ensure settings path exists and merge additional_settings JSON -SETTINGS_PATH="$HOME/.cursor/settings.json" -mkdir -p "$(dirname "$SETTINGS_PATH")" - -# If settings file doesn't exist, initialize basic structure -if [ ! -f "$SETTINGS_PATH" ]; then - echo '{}' > "$SETTINGS_PATH" -fi - -if [ -n "$ADDITIONAL_SETTINGS" ]; then - echo "Merging additional settings into $SETTINGS_PATH" | tee -a "$HOME/$MODULE_DIR_NAME/install.log" - TMP_SETTINGS=$(mktemp) - # Merge JSON: deep merge mcpServers and top-level keys - jq --argjson add "$ADDITIONAL_SETTINGS" 'def deepmerge(a;b): reduce (b|keys[]) as $key (a; .[$key] = if ( (.[ $key ]|type?) == "object" and (b[$key]|type?) == "object" ) then deepmerge(.[ $key ]; b[$key]) else b[$key] end); deepmerge(.;$add)' "$SETTINGS_PATH" > "$TMP_SETTINGS" && mv "$TMP_SETTINGS" "$SETTINGS_PATH" -fi - -exit 0 diff --git a/registry/coder-labs/modules/cursor-cli/scripts/start.sh b/registry/coder-labs/modules/cursor-cli/scripts/start.sh deleted file mode 100644 index 27f2e394..00000000 --- a/registry/coder-labs/modules/cursor-cli/scripts/start.sh +++ /dev/null @@ -1,92 +0,0 @@ -#!/bin/bash - -set -o errexit -set -o pipefail - -command_exists() { - command -v "$1" >/dev/null 2>&1 -} - -INTERACTIVE=${INTERACTIVE:-true} -INITIAL_PROMPT=${INITIAL_PROMPT:-} -NON_INTERACTIVE_CMD=${NON_INTERACTIVE_CMD:-} -FORCE=${FORCE:-false} -MODEL=${MODEL:-} -OUTPUT_FORMAT=${OUTPUT_FORMAT:-} -API_KEY_SECRET=${API_KEY_SECRET:-} -EXTRA_ARGS_BASE64=${EXTRA_ARGS:-} -MODULE_DIR_NAME=${MODULE_DIR_NAME:-.cursor-cli-module} -FOLDER=${FOLDER:-$HOME} -BINARY_NAME=${BINARY_NAME:-cursor-agent} - -mkdir -p "$HOME/$MODULE_DIR_NAME" - -# Decode EXTRA_ARGS lines into an array -IFS=$'\n' read -r -d '' -a EXTRA_ARR < <(echo -n "$EXTRA_ARGS_BASE64" | base64 -d; printf '\0') || true - -# Find cursor agent cli -if command_exists "$BINARY_NAME"; then - CURSOR_CMD="$BINARY_NAME" -elif [ -x "$HOME/.local/bin/$BINARY_NAME" ]; then - CURSOR_CMD="$HOME/.local/bin/$BINARY_NAME" -else - echo "Error: $BINARY_NAME not found. Install it or set install_cursor_cli=true." | tee -a "$HOME/$MODULE_DIR_NAME/start.log" - exit 1 -fi - -# Ensure working directory exists -if [ -d "$FOLDER" ]; then - cd "$FOLDER" -else - mkdir -p "$FOLDER" - cd "$FOLDER" -fi - -ARGS=() - -# base command: if provided, append; otherwise chat mode (no command) -if [ -n "${BASE_COMMAND:-}" ]; then - ARGS+=("${BASE_COMMAND}") -fi - -# global flags -if [ -n "$MODEL" ]; then - ARGS+=("-m" "$MODEL") -fi -if [ "$FORCE" = "true" ]; then - ARGS+=("-f") -fi - -# Non-interactive printing flags -PRINT_TO_CONSOLE=false -if [ "$INTERACTIVE" != "true" ]; then - PRINT_TO_CONSOLE=true - ARGS+=("-p") - if [ -n "$OUTPUT_FORMAT" ]; then - ARGS+=("--output-format" "$OUTPUT_FORMAT") - fi - if [ -n "$NON_INTERACTIVE_CMD" ]; then - # shellcheck disable=SC2206 - CMD_PARTS=($NON_INTERACTIVE_CMD) - ARGS+=("${CMD_PARTS[@]}") - fi -fi - -# Extra args, if any -if [ ${#EXTRA_ARR[@]} -gt 0 ]; then - ARGS+=("${EXTRA_ARR[@]}") -fi - -# If initial prompt specified (chat mode), pass as trailing arg -if [ -n "$INITIAL_PROMPT" ]; then - ARGS+=("$INITIAL_PROMPT") -fi - -# Set API key env if provided -if [ -n "$API_KEY_SECRET" ]; then - export CURSOR_API_KEY="$API_KEY_SECRET" -fi - -# Log and exec -printf "Running: %q %s\n" "$CURSOR_CMD" "$(printf '%q ' "${ARGS[@]}")" | tee -a "$HOME/$MODULE_DIR_NAME/start.log" -exec "$CURSOR_CMD" "${ARGS[@]}" diff --git a/registry/coder-labs/modules/cursor-cli/testdata/cursor-mock.sh b/registry/coder-labs/modules/cursor-cli/testdata/cursor-mock.sh deleted file mode 100644 index fd160696..00000000 --- a/registry/coder-labs/modules/cursor-cli/testdata/cursor-mock.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh -# minimal mock that prints args and exits -printf "cursor mock invoked with: %s\n" "$*" -# Exit successfully regardless -exit 0