Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
51 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
7391082
Update registry/coder-labs/modules/cursor-cli/README.md
matifali Aug 12, 2025
ee1bd53
Merge branch 'main' into feat/cursor-cli-from-main
DevelopmentCats Aug 12, 2025
62a0a9c
feat: add support for agentapi
35C4n0r Aug 13, 2025
2107f93
feat: add tests
35C4n0r Aug 13, 2025
1a38b7e
feat: update readme
35C4n0r Aug 13, 2025
d9e49f5
feat: update readme
35C4n0r Aug 13, 2025
a8328f4
feat: update app slug
35C4n0r Aug 13, 2025
3fdae36
feat: remove app and cli
35C4n0r Aug 14, 2025
5da3721
feat: update readme
35C4n0r Aug 14, 2025
3a4fc36
feat: fix typo
35C4n0r Aug 14, 2025
d1f518a
feat: update mcp_json to mcp
35C4n0r Aug 14, 2025
2322298
feat: update tests
35C4n0r Aug 14, 2025
805b385
Merge branch 'main' into feat/cursor-cli-from-main
DevelopmentCats Aug 14, 2025
10a96aa
Update registry/coder-labs/modules/cursor-cli/scripts/start.sh
35C4n0r Aug 14, 2025
f480d74
Update registry/coder-labs/modules/cursor-cli/scripts/start.sh
35C4n0r Aug 14, 2025
528b823
Update registry/coder-labs/modules/cursor-cli/testdata/cursor-cli-moc…
35C4n0r Aug 14, 2025
c336e44
chore: update mock
35C4n0r Aug 14, 2025
b99023b
chore: remove comments
35C4n0r Aug 14, 2025
a844fbb
feat: update prompt and README.md
35C4n0r Aug 15, 2025
f1bca89
feat: update test
35C4n0r Aug 16, 2025
d673f3a
feat: bun fmt
35C4n0r Aug 16, 2025
dfc007d
feat: bump agentapi version
35C4n0r Aug 18, 2025
037c223
feat: bump agentapi version
35C4n0r Aug 18, 2025
6748088
Update main.tf
35C4n0r Aug 18, 2025
7db6161
feat: add agent type
35C4n0r Aug 18, 2025
bfe56f3
Merge branch 'main' into feat/cursor-cli-from-main
DevelopmentCats Aug 18, 2025
77709d1
fix: documentation
35C4n0r Aug 18, 2025
71a1f3d
fix: update documentation
35C4n0r Aug 18, 2025
522f095
bun fmt
35C4n0r Aug 18, 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