Skip to content

Commit 0ad83fd

Browse files
committed
Refactored into an extension
1 parent 150b057 commit 0ad83fd

3 files changed

Lines changed: 86 additions & 64 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import type { BuildExtension } from "@trigger.dev/build";
2+
import type { ChildProcess } from "child_process";
3+
import { spawn } from "child_process";
4+
import { chmodSync, copyFileSync, existsSync, readdirSync } from "fs";
5+
import type { Readable } from "stream";
6+
7+
export type CursorAgentProcess = ChildProcess & {
8+
stdout: Readable;
9+
stderr: Readable;
10+
};
11+
12+
/** Where the build layer copies cursor-agent's resolved files */
13+
export const CURSOR_AGENT_DIR = "/usr/local/lib/cursor-agent";
14+
15+
/** Install the Cursor CLI binary into the Trigger.dev container image */
16+
export function cursorCli(): BuildExtension {
17+
return {
18+
name: "cursor-cli",
19+
onBuildComplete(context) {
20+
if (context.target === "dev") return;
21+
22+
context.addLayer({
23+
id: "cursor-cli",
24+
image: {
25+
instructions: [
26+
"RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*",
27+
'ENV PATH="/root/.local/bin:$PATH"',
28+
"RUN curl -fsSL https://cursor.com/install | bash",
29+
// Copy the resolved index.js + deps to a fixed path so we can invoke with process.execPath at runtime
30+
`RUN cp -r $(dirname $(readlink -f /root/.local/bin/cursor-agent)) ${CURSOR_AGENT_DIR}`,
31+
],
32+
},
33+
});
34+
},
35+
};
36+
}
37+
38+
type SpawnCursorAgentOptions = {
39+
cwd: string;
40+
env?: Record<string, string | undefined>;
41+
};
42+
43+
/**
44+
* Spawn cursor-agent at runtime inside the Trigger.dev container.
45+
*
46+
* Handles the /tmp copy + chmod workaround needed because the runtime
47+
* strips execute permissions, and cursor-agent's native .node modules
48+
* require its bundled node (ABI mismatch with container node).
49+
*/
50+
export function spawnCursorAgent(
51+
args: string[],
52+
options: SpawnCursorAgentOptions,
53+
): CursorAgentProcess {
54+
const entryPoint = `${CURSOR_AGENT_DIR}/index.js`;
55+
const bundledNode = `${CURSOR_AGENT_DIR}/node`;
56+
const tmpNode = "/tmp/cursor-node";
57+
58+
if (!existsSync(entryPoint)) {
59+
const dirExists = existsSync(CURSOR_AGENT_DIR);
60+
throw new Error(
61+
`cursor-agent not found at ${entryPoint}. Dir: ${dirExists}. Contents: ${dirExists ? readdirSync(CURSOR_AGENT_DIR).join(", ") : "N/A"}`,
62+
);
63+
}
64+
65+
copyFileSync(bundledNode, tmpNode);
66+
chmodSync(tmpNode, 0o755);
67+
68+
// stdio: ["ignore", "pipe", "pipe"] guarantees stdout/stderr are non-null
69+
return spawn(tmpNode, [entryPoint, ...args], {
70+
stdio: ["ignore", "pipe", "pipe"],
71+
env: {
72+
...process.env,
73+
...options.env,
74+
CURSOR_INVOKED_AS: "cursor-agent",
75+
},
76+
cwd: options.cwd,
77+
}) as CursorAgentProcess;
78+
}

cursor-cli-demo/trigger.config.ts

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,5 @@
11
import { defineConfig } from "@trigger.dev/sdk";
2-
3-
import type { BuildExtension } from "@trigger.dev/build";
4-
5-
/** Install the Cursor CLI binary into the Trigger.dev container image */
6-
function cursorCli(): BuildExtension {
7-
return {
8-
name: "cursor-cli",
9-
onBuildComplete(context) {
10-
if (context.target === "dev") return;
11-
12-
context.addLayer({
13-
id: "cursor-cli",
14-
image: {
15-
instructions: [
16-
"RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*",
17-
'ENV PATH="/root/.local/bin:$PATH"',
18-
"RUN curl -fsSL https://cursor.com/install | bash",
19-
// Copy the resolved index.js + deps to a fixed path so we can invoke with process.execPath at runtime
20-
"RUN cp -r $(dirname $(readlink -f /root/.local/bin/cursor-agent)) /usr/local/lib/cursor-agent",
21-
],
22-
},
23-
});
24-
},
25-
};
26-
}
2+
import { cursorCli } from "./extensions/cursor-cli";
273

284
export default defineConfig({
295
project: process.env.TRIGGER_PROJECT_REF!,

cursor-cli-demo/trigger/cursor-agent.ts

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { logger, metadata, task } from "@trigger.dev/sdk";
2-
import { spawn } from "child_process";
3-
import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync } from "fs";
2+
import { mkdirSync } from "fs";
43
import type { CursorEvent } from "@/lib/cursor-events";
4+
import { spawnCursorAgent } from "../extensions/cursor-cli";
55

66
export type CursorAgentPayload = {
77
prompt: string;
@@ -22,44 +22,12 @@ export const cursorAgentTask = task({
2222

2323
const model = payload.model ?? "sonnet-4.5";
2424

25-
// The Trigger.dev runtime strips execute permissions from all binaries.
26-
// cursor-agent bundles native .node modules (pty, sqlite3, etc.) compiled for its own node,
27-
// so we must use cursor-agent's bundled node — not the container's node (ABI mismatch).
28-
// Workaround: copy the bundled node to /tmp and chmod +x it there.
29-
const cursorDir = "/usr/local/lib/cursor-agent";
30-
const entryPoint = `${cursorDir}/index.js`;
31-
const bundledNode = `${cursorDir}/node`;
32-
const tmpNode = "/tmp/cursor-node";
33-
34-
if (!existsSync(entryPoint)) {
35-
const dirExists = existsSync(cursorDir);
36-
throw new Error(`cursor-agent not found at ${entryPoint}. Dir: ${dirExists}. Contents: ${dirExists ? readdirSync(cursorDir).join(", ") : "N/A"}`);
37-
}
25+
logger.info("Spawning cursor-agent", { workspace, model });
3826

39-
// Copy bundled node to /tmp and make it executable
40-
copyFileSync(bundledNode, tmpNode);
41-
chmodSync(tmpNode, 0o755);
42-
43-
logger.info("Spawning cursor-agent", { node: tmpNode, entryPoint, workspace, model });
44-
45-
const child = spawn(tmpNode, [
46-
entryPoint,
47-
"-p",
48-
"--force",
49-
"--output-format",
50-
"stream-json",
51-
"--model",
52-
model,
53-
payload.prompt,
54-
], {
55-
stdio: ["ignore", "pipe", "pipe"],
56-
env: {
57-
...process.env,
58-
CURSOR_API_KEY: process.env.CURSOR_API_KEY,
59-
CURSOR_INVOKED_AS: "cursor-agent",
60-
},
61-
cwd: workspace,
62-
});
27+
const child = spawnCursorAgent(
28+
["-p", "--force", "--output-format", "stream-json", "--model", model, payload.prompt],
29+
{ cwd: workspace, env: { CURSOR_API_KEY: process.env.CURSOR_API_KEY } },
30+
);
6331

6432
let spawnError: Error | null = null;
6533
child.on("error", (err) => {

0 commit comments

Comments
 (0)