-
Notifications
You must be signed in to change notification settings - Fork 44
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
matifali
wants to merge
24
commits into
main
Choose a base branch
from
feat/cursor-cli-from-main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+703
−0
Draft
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
a8d9b71
feat(cursor-cli): add Cursor Agent CLI module (interactive default, M…
matifali 7483c77
feat(cursor-cli): add project MCP and rules support; non-interactive …
matifali a3aa1cf
chore(cursor-cli): remove Bun test; terraform tests only
matifali a075139
refactor(cursor-cli): drop extra_args, binary_name, base_command, add…
matifali 2a65de4
chore: format cursor-cli module and tests
matifali 5e3f555
feat(cursor-cli): configure Coder MCP status slug in module; docs cle…
matifali 6f46bc9
chore(cursor-cli): move env to coder_env; update scripts
matifali ce902f6
wip
matifali df31818
docs(cursor-cli): enhance README with examples for MCP configuration …
matifali d8b9b2a
feat(cursor-cli): add pre-install and post-install script support
matifali 079ff1f
feat(cursor-cli): add ai_prompt variable
matifali 496efad
feat(cursor-cli): add output for app ID in main.tf
matifali 538d08b
feat(cursor-cli): update script execution to include folder variable …
matifali 0a87206
refactor(cursor-cli): comment out unused arguments in start.sh script
matifali 8b6b659
refactor(cursor-cli): restore previously commented arguments in start…
matifali 4edcb00
refactor(cursor-cli): remove output_format variable and related scrip…
matifali 414b5f2
revert(claude-code): restore main.test.ts to origin/main state
matifali f525189
refactor(cursor-cli): remove non-interactive flags from main.test.ts
matifali fcd9f3a
refactor
matifali 058cd8e
fixup!
matifali 8d3e5d6
fixup!
matifali be5c394
wip
matifali 7391082
Update registry/coder-labs/modules/cursor-cli/README.md
matifali ee1bd53
Merge branch 'main' into feat/cursor-cli-from-main
DevelopmentCats File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
116
registry/coder-labs/modules/cursor-cli/cursor-cli.tftest.hcl
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
}); | ||
}); | ||
|
||
|
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.