Skip to content

feat(cursor-cli): add Cursor CLI module #309

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 22 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a8d9b71
feat(cursor-cli): add Cursor Agent CLI module (interactive default, M…
matifali Aug 8, 2025
7483c77
feat(cursor-cli): add project MCP and rules support; non-interactive …
matifali Aug 8, 2025
a3aa1cf
chore(cursor-cli): remove Bun test; terraform tests only
matifali Aug 8, 2025
a075139
refactor(cursor-cli): drop extra_args, binary_name, base_command, add…
matifali Aug 8, 2025
2a65de4
chore: format cursor-cli module and tests
matifali Aug 8, 2025
5e3f555
feat(cursor-cli): configure Coder MCP status slug in module; docs cle…
matifali Aug 8, 2025
6f46bc9
chore(cursor-cli): move env to coder_env; update scripts
matifali Aug 8, 2025
ce902f6
wip
matifali Aug 8, 2025
df31818
docs(cursor-cli): enhance README with examples for MCP configuration …
matifali Aug 8, 2025
d8b9b2a
feat(cursor-cli): add pre-install and post-install script support
matifali Aug 8, 2025
079ff1f
feat(cursor-cli): add ai_prompt variable
matifali Aug 8, 2025
496efad
feat(cursor-cli): add output for app ID in main.tf
matifali Aug 8, 2025
538d08b
feat(cursor-cli): update script execution to include folder variable …
matifali Aug 8, 2025
0a87206
refactor(cursor-cli): comment out unused arguments in start.sh script
matifali Aug 8, 2025
8b6b659
refactor(cursor-cli): restore previously commented arguments in start…
matifali Aug 8, 2025
4edcb00
refactor(cursor-cli): remove output_format variable and related scrip…
matifali Aug 8, 2025
414b5f2
revert(claude-code): restore main.test.ts to origin/main state
matifali Aug 9, 2025
f525189
refactor(cursor-cli): remove non-interactive flags from main.test.ts
matifali Aug 9, 2025
fcd9f3a
refactor
matifali Aug 9, 2025
058cd8e
fixup!
matifali Aug 9, 2025
8d3e5d6
fixup!
matifali Aug 9, 2025
be5c394
wip
matifali Aug 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions registry/coder-labs/modules/cursor-cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
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.

A full example with MCP, rules, and pre/post install scripts:

```tf

data "coder_parameter" "ai_prompt" {
name = "ai_prompt"
type = "string"
default = "Write a simple hello world program in Python"
}

module "cursor_cli" {
source = "registry.coder.com/coder-labs/cursor-cli/coder"
version = "0.1.0"
agent_id = coder_agent.example.id
folder = "/home/coder/project"

# Optional
install_cursor_cli = true
cursor_cli_version = "latest"
force = true
model = "gpt-5"
ai_prompt = data.coder_parameter.ai_prompt.value

# Minimal MCP server (writes `~/.cursor/mcp.json`):
mcp_json = jsonencode({
mcpServers = {
playwright = {
command = "npx"
args = ["-y", "@playwright/mcp@latest", "--headless", "--isolated", "--no-sandbox"]
}
desktop-commander = {
command = "npx"
args = ["-y", "@wonderwhy-er/desktop-commander"]
}
}
})

# Use a pre_install_script to install the CLI
pre_install_script = <<-EOT
#!/usr/bin/env bash
set -euo pipefail
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
EOT

# Use post_install_script to wait for the repo to be ready
post_install_script = <<-EOT
#!/usr/bin/env bash
set -euo pipefail
TARGET="${FOLDER}/.git/config"
echo "[cursor-cli] waiting for ${TARGET}..."
for i in $(seq 1 600); do
[ -f "$TARGET" ] && { echo "ready"; exit 0; }
sleep 1
done
echo "timeout waiting for ${TARGET}" >&2
EOT

# Provide a map of file name to content; files are written to `~/.cursor/rules/<name>`.
rules_files = {
"python.yml" = <<-EOT
version: 1
rules:
- name: python
include: ['**/*.py']
description: Python-focused guidance
EOT

"frontend.yml" = <<-EOT
version: 1
rules:
- name: web
include: ['**/*.{ts,tsx,js,jsx,css}']
exclude: ['**/dist/**']
description: Frontend rules
EOT
}
}
```

## References

- See Cursor CLI docs: `https://docs.cursor.com/en/cli/overview`
- For MCP project config, see `https://docs.cursor.com/en/context/mcp#using-mcp-json`. This module writes your `mcp_json` into `~/.cursor/mcp.json`.
- For Rules, see `https://docs.cursor.com/en/context/rules#project-rules`. Provide `rules_files` (map of file name to content) to populate `~/.cursor/rules/`.

## Troubleshooting

- Ensure the CLI is installed (enable `install_cursor_cli = true` or preinstall it in your image)
- Logs are written to `~/.cursor-cli-module/`
116 changes: 116 additions & 0 deletions registry/coder-labs/modules/cursor-cli/cursor-cli.tftest.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Terraform tests for the cursor-cli module
// Validates that we render expected script content given inputs

run "defaults" {
command = plan

variables {
agent_id = "test-agent"
folder = "/home/coder"
}

assert {
condition = can(regex("Cursor CLI", resource.coder_script.cursor_cli.display_name))
error_message = "Expected coder_script to be created"
}
}

run "non_interactive_mode" {
command = plan

variables {
agent_id = "test-agent"
folder = "/home/coder"
output_format = "json"
ai_prompt = "refactor the auth module to use JWT tokens"
}

assert {
// non-interactive always prints; output format propagates
condition = can(regex("OUTPUT_FORMAT='json'", resource.coder_script.cursor_cli.script))
error_message = "Expected OUTPUT_FORMAT to be propagated"
}

assert {
condition = can(regex("AI_PROMPT='refactor the auth module to use JWT tokens'", resource.coder_script.cursor_cli.script))
error_message = "Expected ai_prompt to be propagated via AI_PROMPT"
}
}

run "model_and_force" {
command = plan

variables {
agent_id = "test-agent"
folder = "/home/coder"
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"
folder = "/home/coder"
mcp_json = jsonencode({ mcpServers = { foo = { command = "foo", type = "stdio" } } })
rules_files = {
"global.yml" = "version: 1\nrules:\n - name: global\n include: ['**/*']\n description: global rule"
}
pre_install_script = "#!/bin/bash\necho pre-install"
post_install_script = "#!/bin/bash\necho post-install"
}

// Ensure project mcp_json is passed
assert {
condition = can(regex(base64encode(jsonencode({ mcpServers = { foo = { command = "foo", type = "stdio" } } })), resource.coder_script.cursor_cli.script))
error_message = "Expected PROJECT_MCP_JSON (base64) to be in the install step"
}

// Ensure rules map is passed
assert {
condition = can(regex(base64encode(jsonencode({ "global.yml" : "version: 1\nrules:\n - name: global\n include: ['**/*']\n description: global rule" })), resource.coder_script.cursor_cli.script))
error_message = "Expected PROJECT_RULES_JSON (base64) to be in the install step"
}

// Ensure pre/post install scripts are embedded
assert {
condition = can(regex(base64encode("#!/bin/bash\necho pre-install"), resource.coder_script.cursor_cli.script))
error_message = "Expected pre-install script to be embedded"
}
assert {
condition = can(regex(base64encode("#!/bin/bash\necho post-install"), resource.coder_script.cursor_cli.script))
error_message = "Expected post-install script to be embedded"
}
}

run "api_key_env_var" {
command = plan

variables {
agent_id = "test-agent"
folder = "/home/coder"
api_key = "sk-test-123"
}

assert {
condition = resource.coder_env.cursor_api_key[0].name == "CURSOR_API_KEY"
error_message = "Expected CURSOR_API_KEY env to be created when api_key is set"
}

assert {
condition = resource.coder_env.cursor_api_key[0].value == "sk-test-123"
error_message = "Expected CURSOR_API_KEY env value to be set from api_key"
}
}
140 changes: 140 additions & 0 deletions registry/coder-labs/modules/cursor-cli/main.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { afterEach, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
import { execContainer, runTerraformInit, writeFileContainer } from "~test";
import { execModuleScript } from "../../../coder/modules/agentapi/test-util";
import { setupContainer, writeExecutable } from "../../../coder/modules/agentapi/test-util";

let cleanupFns: (() => Promise<void>)[] = [];
const registerCleanup = (fn: () => Promise<void>) => cleanupFns.push(fn);

afterEach(async () => {
const fns = cleanupFns.slice().reverse();
cleanupFns = [];
for (const fn of fns) {
try {
await fn();
} catch (err) {
console.error(err);
}
}
});

const setup = async (vars?: Record<string, string>) => {
const projectDir = "/home/coder/project";
const { id, coderScript, cleanup } = await setupContainer({
moduleDir: import.meta.dir,
image: "codercom/enterprise-minimal:latest",
vars: {
folder: projectDir,
install_cursor_cli: "false",
...vars,
},
});
registerCleanup(cleanup);
// Ensure project dir exists
await execContainer(id, ["bash", "-c", `mkdir -p '${projectDir}'`]);
// Write the module's script to the container
await writeExecutable({
containerId: id,
filePath: "/home/coder/script.sh",
content: coderScript.script,
});
return { id, projectDir };
};

setDefaultTimeout(180 * 1000);

describe("cursor-cli", async () => {
beforeAll(async () => {
await runTerraformInit(import.meta.dir);
});

test("installs Cursor via official installer and runs --help", async () => {
const { id } = await setup({ install_cursor_cli: "true", ai_prompt: "--help" });
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);

// Verify the start log captured the invocation
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/script.log",
]);
expect(startLog.exitCode).toBe(0);
expect(startLog.stdout).toContain("cursor-agent");
});

test("model and force flags propagate", async () => {
const { id } = await setup({ model: "sonnet-4", force: "true", ai_prompt: "status" });
await writeExecutable({
containerId: id,
filePath: "/usr/bin/cursor-agent",
content: `#!/bin/sh\necho cursor-agent invoked\nfor a in "$@"; do echo arg:$a; done\nexit 0\n`,
});

const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);

const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/script.log",
]);
expect(startLog.exitCode).toBe(0);
expect(startLog.stdout).toContain("-m sonnet-4");
expect(startLog.stdout).toContain("-f");
expect(startLog.stdout).toContain("status");
});

test("writes workspace mcp.json when provided", async () => {
const mcp = JSON.stringify({ mcpServers: { foo: { command: "foo", type: "stdio" } } });
const { id } = await setup({ mcp_json: mcp });
await writeExecutable({
containerId: id,
filePath: "/usr/bin/cursor-agent",
content: `#!/bin/sh\necho ok\n`,
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);

const mcpContent = await execContainer(id, [
"bash",
"-c",
`cat '/home/coder/.cursor/mcp.json'`,
]);
expect(mcpContent.exitCode).toBe(0);
expect(mcpContent.stdout).toContain("mcpServers");
expect(mcpContent.stdout).toContain("foo");
});

test("fails when cursor-agent missing", async () => {
const { id } = await setup();
const resp = await execModuleScript(id);
expect(resp.exitCode).not.toBe(0);
const startLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/script.log || true",
]);
expect(startLog.stdout).toContain("cursor-agent not found");
});

test("install step logs folder", async () => {
const { id } = await setup({ install_cursor_cli: "false" });
await writeExecutable({
containerId: id,
filePath: "/usr/bin/cursor-agent",
content: `#!/bin/sh\necho ok\n`,
});
const resp = await execModuleScript(id);
expect(resp.exitCode).toBe(0);
const installLog = await execContainer(id, [
"bash",
"-c",
"cat /home/coder/script.log",
]);
expect(installLog.exitCode).toBe(0);
expect(installLog.stdout).toContain("folder: /home/coder/project");
});
});


Loading