-
Notifications
You must be signed in to change notification settings - Fork 24
Expand file tree
/
Copy pathcursor-cli.ts
More file actions
148 lines (131 loc) · 4.06 KB
/
cursor-cli.ts
File metadata and controls
148 lines (131 loc) · 4.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
import type { BuildExtension } from "@trigger.dev/build";
import { spawn } from "child_process";
import { existsSync, readdirSync } from "fs";
import type { CursorEvent } from "@/lib/cursor-events";
import { logger } from "@trigger.dev/sdk";
/** Where the build layer copies cursor-agent's resolved files */
export const CURSOR_AGENT_DIR = "/usr/local/lib/cursor-agent";
/** Install the Cursor CLI binary into the Trigger.dev container image */
export function cursorCli(): BuildExtension {
return {
name: "cursor-cli",
onBuildComplete(context) {
if (context.target === "dev") return;
context.addLayer({
id: "cursor-cli",
image: {
instructions: [
"RUN apt-get update && apt-get install -y curl ca-certificates && rm -rf /var/lib/apt/lists/*",
'ENV PATH="/root/.local/bin:$PATH"',
"RUN curl -fsSL https://cursor.com/install | bash",
// Copy the resolved index.js + deps to a fixed path so we can invoke with process.execPath at runtime
`RUN cp -r $(dirname $(readlink -f /root/.local/bin/cursor-agent)) ${CURSOR_AGENT_DIR}`,
],
},
});
},
};
}
export type ExitResult = {
exitCode: number;
stderr: string;
};
export type CursorAgent = {
stream: ReadableStream<CursorEvent>;
waitUntilExit: () => Promise<ExitResult>;
kill: () => void;
};
type SpawnCursorAgentOptions = {
cwd: string;
env?: Record<string, string | undefined>;
};
/**
* Spawn cursor-agent at runtime inside the Trigger.dev container.
*
* Uses cursor-agent's bundled node (not process.execPath) because its
* native .node modules require ABI compatibility.
*/
export function spawnCursorAgent(
args: string[],
options: SpawnCursorAgentOptions,
): CursorAgent {
const entryPoint = `${CURSOR_AGENT_DIR}/index.js`;
const bundledNode = `${CURSOR_AGENT_DIR}/node`;
if (!existsSync(entryPoint)) {
const dirExists = existsSync(CURSOR_AGENT_DIR);
throw new Error(
`cursor-agent not found at ${entryPoint}. Dir: ${dirExists}. Contents: ${dirExists ? readdirSync(CURSOR_AGENT_DIR).join(", ") : "N/A"}`,
);
}
const child = spawn(bundledNode, [entryPoint, ...args], {
stdio: ["ignore", "pipe", "pipe"],
env: {
...process.env,
...options.env,
CURSOR_INVOKED_AS: "cursor-agent",
},
cwd: options.cwd,
});
// Collect stderr
let stderr = "";
child.stderr!.on("data", (chunk: Buffer) => {
stderr += chunk.toString();
logger.warn("cursor-agent stderr", { text: chunk.toString() });
});
// Build NDJSON ReadableStream from stdout
let buffer = "";
let streamClosed = false;
const stream = new ReadableStream<CursorEvent>({
start(controller) {
const safeClose = () => {
if (!streamClosed) {
streamClosed = true;
controller.close();
}
};
child.stdout!.on("data", (chunk: Buffer) => {
buffer += chunk.toString();
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (line.trim()) {
try {
controller.enqueue(JSON.parse(line));
} catch {
logger.warn("Malformed NDJSON line", { line });
}
}
}
});
child.stdout!.on("end", () => {
if (buffer.trim()) {
try {
controller.enqueue(JSON.parse(buffer));
} catch {
// skip
}
}
safeClose();
});
child.stdout!.on("error", () => safeClose());
child.on("error", () => safeClose());
},
});
// waitUntilExit resolves when the child process exits
const waitUntilExit = (): Promise<ExitResult> =>
new Promise((resolve) => {
if (child.exitCode !== null) {
resolve({ exitCode: child.exitCode, stderr });
return;
}
child.on("close", (code) => {
resolve({ exitCode: code ?? 1, stderr });
});
});
const kill = () => {
if (child.exitCode === null) {
child.kill("SIGTERM");
}
};
return { stream, waitUntilExit, kill };
}