cursor-agent-bridge 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -16
- package/dist/cli.mjs +828 -5
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +18 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{server-CuHDT_fJ.mjs → server-Bk7ol2lA.mjs} +275 -95
- package/dist/server-Bk7ol2lA.mjs.map +1 -0
- package/package.json +2 -2
- package/dist/server-CuHDT_fJ.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,16 +1,700 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as startServer } from "./server-
|
|
3
|
-
|
|
2
|
+
import { i as CursorRunner, n as engines, r as version, s as toOpenAIModelList, t as startServer } from "./server-Bk7ol2lA.mjs";
|
|
3
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
4
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join, resolve } from "node:path";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { chmodSync, existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
//#region src/cli-args.ts
|
|
4
10
|
function readArg(name, fallback) {
|
|
5
11
|
const index = process.argv.indexOf(name);
|
|
6
|
-
|
|
12
|
+
if (index < 0) return fallback;
|
|
13
|
+
const value = process.argv[index + 1];
|
|
14
|
+
if (!value || value.startsWith("-")) throw new Error(`Missing value for ${name}`);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function parsePort(value, fallback) {
|
|
18
|
+
if (value === void 0) return fallback;
|
|
19
|
+
const port = Number(value);
|
|
20
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid port: ${value}`);
|
|
21
|
+
return port;
|
|
22
|
+
}
|
|
23
|
+
function readHostAndPort(defaultHost = "127.0.0.1", defaultPort = 4646) {
|
|
24
|
+
return {
|
|
25
|
+
host: readArg("--host", process.env.HOST) ?? defaultHost,
|
|
26
|
+
port: parsePort(readArg("--port", process.env.PORT), defaultPort)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/codex-config.ts
|
|
31
|
+
const DEFAULT_CODEX_PROFILE = "cursor";
|
|
32
|
+
function resolveCodexConfigPath(profile = DEFAULT_CODEX_PROFILE, homeDir = homedir()) {
|
|
33
|
+
assertValidProfile(profile);
|
|
34
|
+
return join(homeDir, ".codex", `${profile}.config.toml`);
|
|
35
|
+
}
|
|
36
|
+
function buildBaseUrl(host, port) {
|
|
37
|
+
return `http://${host}:${port}/v1`;
|
|
38
|
+
}
|
|
39
|
+
function buildCodexConfigToml(options) {
|
|
40
|
+
const profile = options.profile ?? "cursor";
|
|
41
|
+
assertValidProfile(profile);
|
|
42
|
+
const baseUrl = buildBaseUrl(options.host, options.port);
|
|
43
|
+
return `model_provider = ${formatTomlString(profile)}
|
|
44
|
+
model = "auto"
|
|
45
|
+
|
|
46
|
+
[model_providers.${profile}]
|
|
47
|
+
name = "Cursor Agent Bridge"
|
|
48
|
+
base_url = ${formatTomlString(baseUrl)}
|
|
49
|
+
wire_api = "responses"
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
function parseCodexConfig(content, profile = DEFAULT_CODEX_PROFILE) {
|
|
53
|
+
assertValidProfile(profile);
|
|
54
|
+
const fields = {};
|
|
55
|
+
const providerSection = `model_providers.${profile}`;
|
|
56
|
+
let section = "";
|
|
57
|
+
for (const rawLine of content.split("\n")) {
|
|
58
|
+
const line = rawLine.trim();
|
|
59
|
+
if (!line || line.startsWith("#")) continue;
|
|
60
|
+
const sectionMatch = line.match(/^\[(.+)\]$/);
|
|
61
|
+
if (sectionMatch) {
|
|
62
|
+
section = sectionMatch[1] ?? "";
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const assignment = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
|
|
66
|
+
if (!assignment) continue;
|
|
67
|
+
const key = assignment[1] ?? "";
|
|
68
|
+
const value = parseTomlValue(assignment[2] ?? "");
|
|
69
|
+
if (section === providerSection) {
|
|
70
|
+
if (key === "name") fields.providerName = value;
|
|
71
|
+
if (key === "base_url") fields.baseUrl = value;
|
|
72
|
+
if (key === "wire_api") fields.wireApi = value;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (section) continue;
|
|
76
|
+
if (key === "model_provider") fields.modelProvider = value;
|
|
77
|
+
if (key === "model") fields.model = value;
|
|
78
|
+
}
|
|
79
|
+
return fields;
|
|
80
|
+
}
|
|
81
|
+
function checkCodexConfig(content, options) {
|
|
82
|
+
const profile = options.profile ?? "cursor";
|
|
83
|
+
const expectedBaseUrl = buildBaseUrl(options.host, options.port);
|
|
84
|
+
const fields = parseCodexConfig(content, profile);
|
|
85
|
+
const issues = [];
|
|
86
|
+
if (fields.modelProvider !== profile) issues.push(`model_provider should be "${profile}"${fields.modelProvider ? `, found "${fields.modelProvider}"` : ", but it is missing"}`);
|
|
87
|
+
if (fields.baseUrl !== expectedBaseUrl) issues.push(`base_url should be "${expectedBaseUrl}"${fields.baseUrl ? `, found "${fields.baseUrl}"` : ", but it is missing"}`);
|
|
88
|
+
if (fields.wireApi !== "responses") issues.push(`wire_api should be "responses"${fields.wireApi ? `, found "${fields.wireApi}"` : ", but it is missing"}`);
|
|
89
|
+
return {
|
|
90
|
+
ok: issues.length === 0,
|
|
91
|
+
issues
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function writeCodexConfig(options) {
|
|
95
|
+
let existing;
|
|
96
|
+
try {
|
|
97
|
+
existing = await readFile(options.filePath, "utf8");
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error.code !== "ENOENT") throw error;
|
|
100
|
+
}
|
|
101
|
+
if (existing === void 0) {
|
|
102
|
+
await mkdir(dirname(options.filePath), { recursive: true });
|
|
103
|
+
await writeFile(options.filePath, buildCodexConfigToml(options), "utf8");
|
|
104
|
+
return {
|
|
105
|
+
path: options.filePath,
|
|
106
|
+
created: true,
|
|
107
|
+
updated: false
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const merged = mergeCodexConfig(existing, options);
|
|
111
|
+
if ("error" in merged) throw new Error(merged.error);
|
|
112
|
+
if (!merged.changed) return {
|
|
113
|
+
path: options.filePath,
|
|
114
|
+
created: false,
|
|
115
|
+
updated: false
|
|
116
|
+
};
|
|
117
|
+
await writeFile(options.filePath, merged.content, "utf8");
|
|
118
|
+
return {
|
|
119
|
+
path: options.filePath,
|
|
120
|
+
created: false,
|
|
121
|
+
updated: true
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function mergeCodexConfig(existing, options) {
|
|
125
|
+
const profile = options.profile ?? "cursor";
|
|
126
|
+
assertValidProfile(profile);
|
|
127
|
+
const providerSection = `model_providers.${profile}`;
|
|
128
|
+
const expected = buildCodexConfigToml(options);
|
|
129
|
+
const parsed = parseCodexConfig(existing, profile);
|
|
130
|
+
if (parsed.modelProvider && parsed.modelProvider !== profile && !options.force) return { error: `model_provider is "${parsed.modelProvider}". Re-run with --force to switch it to "${profile}".` };
|
|
131
|
+
const lines = existing.split("\n");
|
|
132
|
+
const output = [];
|
|
133
|
+
let section = "";
|
|
134
|
+
let inProviderSection = false;
|
|
135
|
+
let sawModelProvider = false;
|
|
136
|
+
let sawModel = false;
|
|
137
|
+
let sawProviderSection = false;
|
|
138
|
+
const handledProviderKeys = /* @__PURE__ */ new Set();
|
|
139
|
+
for (const rawLine of lines) {
|
|
140
|
+
const trimmed = rawLine.trim();
|
|
141
|
+
const sectionMatch = trimmed.match(/^\[(.+)\]$/);
|
|
142
|
+
if (sectionMatch) {
|
|
143
|
+
if (inProviderSection) appendMissingProviderKeys();
|
|
144
|
+
section = sectionMatch[1] ?? "";
|
|
145
|
+
inProviderSection = section === providerSection;
|
|
146
|
+
if (inProviderSection) sawProviderSection = true;
|
|
147
|
+
output.push(rawLine);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const assignment = trimmed.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
|
|
151
|
+
if (!assignment) {
|
|
152
|
+
output.push(rawLine);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const key = assignment[1] ?? "";
|
|
156
|
+
if (!section) {
|
|
157
|
+
if (key === "model_provider") {
|
|
158
|
+
sawModelProvider = true;
|
|
159
|
+
output.push(`model_provider = ${formatTomlString(profile)}`);
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (key === "model") {
|
|
163
|
+
sawModel = true;
|
|
164
|
+
if (parsed.model === void 0 || options.force) output.push("model = \"auto\"");
|
|
165
|
+
else output.push(rawLine);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (inProviderSection) {
|
|
170
|
+
if (key === "name") {
|
|
171
|
+
handledProviderKeys.add("name");
|
|
172
|
+
output.push("name = \"Cursor Agent Bridge\"");
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (key === "base_url") {
|
|
176
|
+
handledProviderKeys.add("base_url");
|
|
177
|
+
output.push(`base_url = ${formatTomlString(buildBaseUrl(options.host, options.port))}`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (key === "wire_api") {
|
|
181
|
+
handledProviderKeys.add("wire_api");
|
|
182
|
+
output.push("wire_api = \"responses\"");
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
output.push(rawLine);
|
|
187
|
+
}
|
|
188
|
+
if (inProviderSection) appendMissingProviderKeys();
|
|
189
|
+
const missingTopLevel = [];
|
|
190
|
+
if (!sawModelProvider) missingTopLevel.push(`model_provider = ${formatTomlString(profile)}`);
|
|
191
|
+
if (!sawModel) missingTopLevel.push("model = \"auto\"");
|
|
192
|
+
let content = output.join("\n").trimEnd();
|
|
193
|
+
if (missingTopLevel.length > 0) content = `${missingTopLevel.join("\n")}\n${content}`;
|
|
194
|
+
if (!sawProviderSection) {
|
|
195
|
+
const providerBlock = expected.split("\n").slice(3).join("\n");
|
|
196
|
+
content = `${content}\n\n${providerBlock}\n`;
|
|
197
|
+
}
|
|
198
|
+
const changed = normalizeToml(content) !== normalizeToml(existing);
|
|
199
|
+
return {
|
|
200
|
+
content: `${content.trimEnd()}\n`,
|
|
201
|
+
changed
|
|
202
|
+
};
|
|
203
|
+
function appendMissingProviderKeys() {
|
|
204
|
+
if (!handledProviderKeys.has("name")) output.push("name = \"Cursor Agent Bridge\"");
|
|
205
|
+
if (!handledProviderKeys.has("base_url")) output.push(`base_url = ${formatTomlString(buildBaseUrl(options.host, options.port))}`);
|
|
206
|
+
if (!handledProviderKeys.has("wire_api")) output.push("wire_api = \"responses\"");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function assertValidProfile(profile) {
|
|
210
|
+
if (/^[A-Za-z0-9_-]+$/.test(profile)) return;
|
|
211
|
+
throw new Error("Invalid Codex profile. Use only letters, numbers, underscores, or hyphens.");
|
|
212
|
+
}
|
|
213
|
+
function formatTomlString(value) {
|
|
214
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"").replaceAll("\b", "\\b").replaceAll(" ", "\\t").replaceAll("\n", "\\n").replaceAll("\f", "\\f").replaceAll("\r", "\\r")}"`;
|
|
215
|
+
}
|
|
216
|
+
function normalizeToml(content) {
|
|
217
|
+
return content.replace(/\r\n/g, "\n").trimEnd();
|
|
218
|
+
}
|
|
219
|
+
function parseTomlValue(raw) {
|
|
220
|
+
const trimmed = stripInlineTomlComment(raw).trim();
|
|
221
|
+
if (trimmed.startsWith("\"") && trimmed.endsWith("\"")) return unescapeTomlString(trimmed.slice(1, -1));
|
|
222
|
+
if (trimmed.startsWith("'") && trimmed.endsWith("'")) return trimmed.slice(1, -1);
|
|
223
|
+
return trimmed;
|
|
224
|
+
}
|
|
225
|
+
function unescapeTomlString(value) {
|
|
226
|
+
return value.replace(/\\(["\\btnfr])/g, (_match, escaped) => ({
|
|
227
|
+
"\"": "\"",
|
|
228
|
+
"\\": "\\",
|
|
229
|
+
b: "\b",
|
|
230
|
+
t: " ",
|
|
231
|
+
n: "\n",
|
|
232
|
+
f: "\f",
|
|
233
|
+
r: "\r"
|
|
234
|
+
})[escaped] ?? escaped);
|
|
235
|
+
}
|
|
236
|
+
function stripInlineTomlComment(raw) {
|
|
237
|
+
let quote;
|
|
238
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
239
|
+
const char = raw[index];
|
|
240
|
+
if ((char === "\"" || char === "'") && raw[index - 1] !== "\\") {
|
|
241
|
+
quote = quote === char ? void 0 : quote ?? char;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (!quote && char === "#") return raw.slice(0, index);
|
|
245
|
+
}
|
|
246
|
+
return raw;
|
|
247
|
+
}
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/doctor.ts
|
|
250
|
+
const execFileAsync$1 = promisify(execFile);
|
|
251
|
+
async function runDoctor(options) {
|
|
252
|
+
const checks = [];
|
|
253
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
254
|
+
const execFileFn = options.execFileFn ?? execFileAsync$1;
|
|
255
|
+
const readFileFn = options.readFileFn ?? readFile;
|
|
256
|
+
const agentPath = options.agentPath ?? process.env.CURSOR_AGENT_PATH ?? "agent";
|
|
257
|
+
const profile = options.profile ?? "cursor";
|
|
258
|
+
const codexConfigPath = options.codexConfigPath ?? resolveCodexConfigPath(profile);
|
|
259
|
+
checks.push(checkNodeVersion(options.nodeVersionRange ?? engines.node));
|
|
260
|
+
checks.push({
|
|
261
|
+
name: "bridge-version",
|
|
262
|
+
ok: true,
|
|
263
|
+
message: `cursor-agent-bridge ${version}`
|
|
264
|
+
});
|
|
265
|
+
const agentCheck = await checkAgentExecutable(agentPath, execFileFn);
|
|
266
|
+
checks.push(agentCheck);
|
|
267
|
+
if (agentCheck.ok) checks.push(await checkAgentLogin(agentPath, options.runner));
|
|
268
|
+
checks.push(await checkBridgeHealth(options.host, options.port, fetchFn));
|
|
269
|
+
if (!options.skipCodexConfig) checks.push(await checkCodexConfigFile({
|
|
270
|
+
codexConfigPath,
|
|
271
|
+
host: options.host,
|
|
272
|
+
port: options.port,
|
|
273
|
+
profile,
|
|
274
|
+
readFileFn
|
|
275
|
+
}));
|
|
276
|
+
return {
|
|
277
|
+
ok: checks.every((check) => check.ok),
|
|
278
|
+
checks
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function formatDoctorReport(result) {
|
|
282
|
+
const lines = result.checks.map((check) => {
|
|
283
|
+
const prefix = check.ok ? "✓" : "✗";
|
|
284
|
+
const hint = check.hint ? `\n → ${check.hint}` : "";
|
|
285
|
+
return `${prefix} ${check.name}: ${check.message}${hint}`;
|
|
286
|
+
});
|
|
287
|
+
lines.push("");
|
|
288
|
+
lines.push(result.ok ? "All checks passed. Codex can use Cursor Agent through the bridge." : "Some checks failed. Fix the items above before starting Codex.");
|
|
289
|
+
return `${lines.join("\n")}\n`;
|
|
290
|
+
}
|
|
291
|
+
function checkNodeVersion(requiredRange) {
|
|
292
|
+
const current = process.version.slice(1);
|
|
293
|
+
const minimum = parseMinimumNodeVersion(requiredRange);
|
|
294
|
+
const ok = compareNodeVersion(current, minimum) >= 0;
|
|
295
|
+
return ok ? {
|
|
296
|
+
name: "node-version",
|
|
297
|
+
ok,
|
|
298
|
+
message: `Node ${current} satisfies ${requiredRange}`
|
|
299
|
+
} : {
|
|
300
|
+
name: "node-version",
|
|
301
|
+
ok,
|
|
302
|
+
message: `Node ${current} does not satisfy ${requiredRange}`,
|
|
303
|
+
hint: `Install Node ${minimum} or newer.`
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
async function checkAgentExecutable(agentPath, execFileFn) {
|
|
307
|
+
try {
|
|
308
|
+
await execFileFn(agentPath, ["--help"], { timeout: 5e3 });
|
|
309
|
+
return {
|
|
310
|
+
name: "agent-cli",
|
|
311
|
+
ok: true,
|
|
312
|
+
message: `Cursor Agent CLI found at ${agentPath}`
|
|
313
|
+
};
|
|
314
|
+
} catch (error) {
|
|
315
|
+
return {
|
|
316
|
+
name: "agent-cli",
|
|
317
|
+
ok: false,
|
|
318
|
+
message: error instanceof Error ? error.message : "Cursor Agent CLI not found",
|
|
319
|
+
hint: "Install the Cursor Agent CLI and ensure `agent` is on PATH, or set CURSOR_AGENT_PATH."
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
async function checkAgentLogin(agentPath, runner = new CursorRunner({ agentPath })) {
|
|
324
|
+
try {
|
|
325
|
+
const models = await runner.listModels({ refresh: true });
|
|
326
|
+
if (models.length === 0) return {
|
|
327
|
+
name: "agent-login",
|
|
328
|
+
ok: false,
|
|
329
|
+
message: "Cursor Agent responded, but returned no models",
|
|
330
|
+
hint: "Run `agent login` and confirm `agent --list-models` returns models."
|
|
331
|
+
};
|
|
332
|
+
return {
|
|
333
|
+
name: "agent-login",
|
|
334
|
+
ok: true,
|
|
335
|
+
message: `Cursor Agent is logged in (${models.length} models available)`
|
|
336
|
+
};
|
|
337
|
+
} catch (error) {
|
|
338
|
+
return {
|
|
339
|
+
name: "agent-login",
|
|
340
|
+
ok: false,
|
|
341
|
+
message: error instanceof Error ? error.message : "Cursor Agent login check failed",
|
|
342
|
+
hint: "Run `agent login` and retry `cursor-agent-bridge doctor`."
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function checkBridgeHealth(host, port, fetchFn) {
|
|
347
|
+
const url = `http://${host}:${port}/health`;
|
|
348
|
+
try {
|
|
349
|
+
const response = await fetchFn(url, { signal: AbortSignal.timeout(3e3) });
|
|
350
|
+
if (!response.ok) return {
|
|
351
|
+
name: "bridge-health",
|
|
352
|
+
ok: false,
|
|
353
|
+
message: `${url} returned HTTP ${response.status}`,
|
|
354
|
+
hint: "Start the bridge with `cursor-agent-bridge serve` or `cursor-agent-bridge launch-agent install`."
|
|
355
|
+
};
|
|
356
|
+
const payload = await response.json();
|
|
357
|
+
return {
|
|
358
|
+
name: "bridge-health",
|
|
359
|
+
ok: true,
|
|
360
|
+
message: payload.version ? `Bridge is listening on ${url} (version ${payload.version})` : `Bridge is listening on ${url}`
|
|
361
|
+
};
|
|
362
|
+
} catch (error) {
|
|
363
|
+
return {
|
|
364
|
+
name: "bridge-health",
|
|
365
|
+
ok: false,
|
|
366
|
+
message: error instanceof Error ? error.message : "Bridge health check failed",
|
|
367
|
+
hint: "Start the bridge with `cursor-agent-bridge serve` or `cursor-agent-bridge launch-agent install`."
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
async function checkCodexConfigFile(options) {
|
|
372
|
+
try {
|
|
373
|
+
const result = checkCodexConfig(await options.readFileFn(options.codexConfigPath, "utf8"), {
|
|
374
|
+
host: options.host,
|
|
375
|
+
port: options.port,
|
|
376
|
+
profile: options.profile
|
|
377
|
+
});
|
|
378
|
+
if (result.ok) return {
|
|
379
|
+
name: "codex-config",
|
|
380
|
+
ok: true,
|
|
381
|
+
message: `Codex config looks correct at ${options.codexConfigPath}`
|
|
382
|
+
};
|
|
383
|
+
return {
|
|
384
|
+
name: "codex-config",
|
|
385
|
+
ok: false,
|
|
386
|
+
message: result.issues.join("; "),
|
|
387
|
+
hint: "Run `cursor-agent-bridge config write` or `cursor-agent-bridge config print`."
|
|
388
|
+
};
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (error.code === "ENOENT") return {
|
|
391
|
+
name: "codex-config",
|
|
392
|
+
ok: false,
|
|
393
|
+
message: `Codex config not found at ${options.codexConfigPath}`,
|
|
394
|
+
hint: "Run `cursor-agent-bridge config write` to create it."
|
|
395
|
+
};
|
|
396
|
+
return {
|
|
397
|
+
name: "codex-config",
|
|
398
|
+
ok: false,
|
|
399
|
+
message: error instanceof Error ? error.message : "Codex config check failed",
|
|
400
|
+
hint: "Verify the Codex config path and file permissions."
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function parseMinimumNodeVersion(range) {
|
|
405
|
+
const match = range.match(/(\d+)\.(\d+)(?:\.(\d+))?/);
|
|
406
|
+
if (!match) return "0.0.0";
|
|
407
|
+
return `${match[1]}.${match[2]}.${match[3] ?? 0}`;
|
|
408
|
+
}
|
|
409
|
+
function compareNodeVersion(left, right) {
|
|
410
|
+
const toParts = (value) => value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
411
|
+
const [leftMajor = 0, leftMinor = 0, leftPatch = 0] = toParts(left);
|
|
412
|
+
const [rightMajor = 0, rightMinor = 0, rightPatch = 0] = toParts(right);
|
|
413
|
+
if (leftMajor !== rightMajor) return leftMajor - rightMajor;
|
|
414
|
+
if (leftMinor !== rightMinor) return leftMinor - rightMinor;
|
|
415
|
+
return leftPatch - rightPatch;
|
|
416
|
+
}
|
|
417
|
+
//#endregion
|
|
418
|
+
//#region src/launch-agent.ts
|
|
419
|
+
const defaultLabel = "com.xwartz.cursor-agent-bridge";
|
|
420
|
+
const defaultLogDir = join(homedir(), ".codex", "logs");
|
|
421
|
+
function getLaunchAgentPaths(label = defaultLabel) {
|
|
422
|
+
return {
|
|
423
|
+
label,
|
|
424
|
+
plistPath: join(homedir(), "Library", "LaunchAgents", `${label}.plist`),
|
|
425
|
+
stdoutPath: join(defaultLogDir, "cursor-agent-bridge.log"),
|
|
426
|
+
stderrPath: join(defaultLogDir, "cursor-agent-bridge.err.log")
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function createLaunchAgentPlist(options) {
|
|
430
|
+
const label = options.label ?? defaultLabel;
|
|
431
|
+
const host = options.host ?? "127.0.0.1";
|
|
432
|
+
const port = options.port ?? 4646;
|
|
433
|
+
const paths = getLaunchAgentPaths(label);
|
|
434
|
+
const args = [
|
|
435
|
+
resolve(options.cliPath),
|
|
436
|
+
"serve",
|
|
437
|
+
"--host",
|
|
438
|
+
host,
|
|
439
|
+
"--port",
|
|
440
|
+
String(port)
|
|
441
|
+
];
|
|
442
|
+
const env = { PATH: [
|
|
443
|
+
dirname(resolve(options.cliPath)),
|
|
444
|
+
join(homedir(), ".local", "bin"),
|
|
445
|
+
"/usr/local/bin",
|
|
446
|
+
"/opt/homebrew/bin",
|
|
447
|
+
"/usr/bin",
|
|
448
|
+
"/bin",
|
|
449
|
+
"/usr/sbin",
|
|
450
|
+
"/sbin"
|
|
451
|
+
].join(":") };
|
|
452
|
+
if (options.agentPath) env.CURSOR_AGENT_PATH = options.agentPath;
|
|
453
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
454
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
455
|
+
<plist version="1.0">
|
|
456
|
+
<dict>
|
|
457
|
+
<key>Label</key>
|
|
458
|
+
<string>${escapePlist(label)}</string>
|
|
459
|
+
|
|
460
|
+
<key>ProgramArguments</key>
|
|
461
|
+
<array>
|
|
462
|
+
${args.map((arg) => ` <string>${escapePlist(arg)}</string>`).join("\n")}
|
|
463
|
+
</array>
|
|
464
|
+
|
|
465
|
+
<key>EnvironmentVariables</key>
|
|
466
|
+
<dict>
|
|
467
|
+
${Object.entries(env).map(([key, value]) => ` <key>${escapePlist(key)}</key>\n <string>${escapePlist(value)}</string>`).join("\n")}
|
|
468
|
+
</dict>
|
|
469
|
+
|
|
470
|
+
<key>RunAtLoad</key>
|
|
471
|
+
<true/>
|
|
472
|
+
|
|
473
|
+
<key>KeepAlive</key>
|
|
474
|
+
<true/>
|
|
475
|
+
|
|
476
|
+
<key>StandardOutPath</key>
|
|
477
|
+
<string>${escapePlist(paths.stdoutPath)}</string>
|
|
478
|
+
|
|
479
|
+
<key>StandardErrorPath</key>
|
|
480
|
+
<string>${escapePlist(paths.stderrPath)}</string>
|
|
481
|
+
</dict>
|
|
482
|
+
</plist>
|
|
483
|
+
`;
|
|
484
|
+
}
|
|
485
|
+
function installLaunchAgent(options) {
|
|
486
|
+
ensureMacOS();
|
|
487
|
+
const paths = getLaunchAgentPaths(options.label ?? defaultLabel);
|
|
488
|
+
mkdirSync(dirname(paths.plistPath), { recursive: true });
|
|
489
|
+
mkdirSync(defaultLogDir, { recursive: true });
|
|
490
|
+
if (existsSync(paths.plistPath)) bootout(paths.plistPath);
|
|
491
|
+
writeFileSync(paths.plistPath, createLaunchAgentPlist(options));
|
|
492
|
+
chmodSync(paths.plistPath, 420);
|
|
493
|
+
execFileSync("launchctl", [
|
|
494
|
+
"bootstrap",
|
|
495
|
+
launchctlDomain(),
|
|
496
|
+
paths.plistPath
|
|
497
|
+
], { stdio: "pipe" });
|
|
498
|
+
return paths;
|
|
499
|
+
}
|
|
500
|
+
function uninstallLaunchAgent(label = defaultLabel) {
|
|
501
|
+
ensureMacOS();
|
|
502
|
+
const paths = getLaunchAgentPaths(label);
|
|
503
|
+
bootout(paths.plistPath);
|
|
504
|
+
rmSync(paths.plistPath, { force: true });
|
|
505
|
+
return paths;
|
|
506
|
+
}
|
|
507
|
+
function printLaunchAgentStatus(label = defaultLabel) {
|
|
508
|
+
ensureMacOS();
|
|
509
|
+
return execFileSync("launchctl", ["print", `${launchctlDomain()}/${label}`], { encoding: "utf8" });
|
|
510
|
+
}
|
|
511
|
+
function bootout(plistPath) {
|
|
512
|
+
try {
|
|
513
|
+
execFileSync("launchctl", [
|
|
514
|
+
"bootout",
|
|
515
|
+
launchctlDomain(),
|
|
516
|
+
plistPath
|
|
517
|
+
], { stdio: "pipe" });
|
|
518
|
+
} catch {}
|
|
519
|
+
}
|
|
520
|
+
function launchctlDomain() {
|
|
521
|
+
return `gui/${process.getuid?.() ?? execFileSync("id", ["-u"], { encoding: "utf8" }).trim()}`;
|
|
522
|
+
}
|
|
523
|
+
function ensureMacOS() {
|
|
524
|
+
if (process.platform !== "darwin") throw new Error("LaunchAgent management is only available on macOS.");
|
|
525
|
+
}
|
|
526
|
+
function escapePlist(value) {
|
|
527
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/upgrade.ts
|
|
531
|
+
const execFileAsync = promisify(execFile);
|
|
532
|
+
const PACKAGE_NAME = "cursor-agent-bridge";
|
|
533
|
+
const DEFAULT_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
|
|
534
|
+
const REGISTRY_TIMEOUT_MS = 1e4;
|
|
535
|
+
function compareSemver(left, right) {
|
|
536
|
+
const toParts = (value) => {
|
|
537
|
+
const [major = 0, minor = 0, patch = 0] = value.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
538
|
+
return [
|
|
539
|
+
major,
|
|
540
|
+
minor,
|
|
541
|
+
patch
|
|
542
|
+
];
|
|
543
|
+
};
|
|
544
|
+
const [leftMajor, leftMinor, leftPatch] = toParts(left);
|
|
545
|
+
const [rightMajor, rightMinor, rightPatch] = toParts(right);
|
|
546
|
+
if (leftMajor !== rightMajor) return leftMajor < rightMajor ? -1 : 1;
|
|
547
|
+
if (leftMinor !== rightMinor) return leftMinor < rightMinor ? -1 : 1;
|
|
548
|
+
if (leftPatch !== rightPatch) return leftPatch < rightPatch ? -1 : 1;
|
|
549
|
+
return 0;
|
|
550
|
+
}
|
|
551
|
+
async function fetchLatestVersion(options) {
|
|
552
|
+
const registryUrl = options?.registryUrl ?? DEFAULT_REGISTRY_URL;
|
|
553
|
+
const fetchFn = options?.fetchFn ?? fetch;
|
|
554
|
+
const timeoutMs = options?.timeoutMs ?? REGISTRY_TIMEOUT_MS;
|
|
555
|
+
const controller = new AbortController();
|
|
556
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
557
|
+
try {
|
|
558
|
+
const response = await fetchFn(registryUrl, { signal: controller.signal });
|
|
559
|
+
if (!response.ok) throw new Error(`Registry returned ${response.status}`);
|
|
560
|
+
const payload = await response.json();
|
|
561
|
+
if (!payload.version) throw new Error("Registry response missing version");
|
|
562
|
+
return payload.version;
|
|
563
|
+
} finally {
|
|
564
|
+
clearTimeout(timeout);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
async function commandExists(command, execFileFn) {
|
|
568
|
+
try {
|
|
569
|
+
await execFileFn("which", [command]);
|
|
570
|
+
return true;
|
|
571
|
+
} catch {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async function detectPackageManager(preference, execFileFn = execFileAsync) {
|
|
576
|
+
if (preference === "npm") return "npm";
|
|
577
|
+
if (preference === "pnpm") return "pnpm";
|
|
578
|
+
if (!await commandExists("pnpm", execFileFn)) return "npm";
|
|
579
|
+
try {
|
|
580
|
+
await execFileFn("pnpm", [
|
|
581
|
+
"list",
|
|
582
|
+
"-g",
|
|
583
|
+
PACKAGE_NAME,
|
|
584
|
+
"--json"
|
|
585
|
+
], { env: process.env });
|
|
586
|
+
return "pnpm";
|
|
587
|
+
} catch {
|
|
588
|
+
return "npm";
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function buildInstallCommand(manager, target) {
|
|
592
|
+
const packageSpec = `${PACKAGE_NAME}${target === "latest" ? "@latest" : `@${target.replace(/^@/, "")}`}`;
|
|
593
|
+
if (manager === "pnpm") return {
|
|
594
|
+
command: "pnpm",
|
|
595
|
+
args: [
|
|
596
|
+
"add",
|
|
597
|
+
"-g",
|
|
598
|
+
packageSpec
|
|
599
|
+
]
|
|
600
|
+
};
|
|
601
|
+
return {
|
|
602
|
+
command: "npm",
|
|
603
|
+
args: [
|
|
604
|
+
"install",
|
|
605
|
+
"-g",
|
|
606
|
+
packageSpec
|
|
607
|
+
]
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function printManualUpgradeHint(errorLog) {
|
|
611
|
+
errorLog("Upgrade manually:");
|
|
612
|
+
errorLog(` pnpm add -g ${PACKAGE_NAME}@latest`);
|
|
613
|
+
errorLog(` npm install -g ${PACKAGE_NAME}@latest`);
|
|
614
|
+
}
|
|
615
|
+
async function resolveTargetVersion(target, registryUrl, fetchFn) {
|
|
616
|
+
if (target === "latest") return fetchLatestVersion({
|
|
617
|
+
registryUrl,
|
|
618
|
+
fetchFn
|
|
619
|
+
});
|
|
620
|
+
return target.replace(/^@/, "");
|
|
621
|
+
}
|
|
622
|
+
async function runInstall(manager, target, spawnFn) {
|
|
623
|
+
const { command, args } = buildInstallCommand(manager, target);
|
|
624
|
+
return new Promise((resolve, reject) => {
|
|
625
|
+
const child = spawnFn(command, args, {
|
|
626
|
+
stdio: "inherit",
|
|
627
|
+
env: process.env
|
|
628
|
+
});
|
|
629
|
+
child.on("error", reject);
|
|
630
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
631
|
+
});
|
|
7
632
|
}
|
|
633
|
+
async function runUpgrade(options) {
|
|
634
|
+
const log = options.log ?? console.log;
|
|
635
|
+
const errorLog = options.errorLog ?? console.error;
|
|
636
|
+
const checkOnly = options.checkOnly ?? false;
|
|
637
|
+
const target = options.target ?? "latest";
|
|
638
|
+
const managerPreference = options.manager ?? "auto";
|
|
639
|
+
const registryUrl = options.registryUrl ?? DEFAULT_REGISTRY_URL;
|
|
640
|
+
const fetchFn = options.fetchFn ?? fetch;
|
|
641
|
+
const spawnFn = options.spawnFn ?? ((command, args, spawnOptions) => spawn(command, args, spawnOptions));
|
|
642
|
+
const execFileFn = options.execFileFn ?? execFileAsync;
|
|
643
|
+
const existsFn = options.existsFn ?? existsSync;
|
|
644
|
+
let targetVersion;
|
|
645
|
+
try {
|
|
646
|
+
targetVersion = await resolveTargetVersion(target, registryUrl, fetchFn);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
errorLog(`Failed to check for updates: ${error instanceof Error ? error.message : String(error)}`);
|
|
649
|
+
printManualUpgradeHint(errorLog);
|
|
650
|
+
return 1;
|
|
651
|
+
}
|
|
652
|
+
if (compareSemver(options.currentVersion, targetVersion) >= 0) {
|
|
653
|
+
log(`${PACKAGE_NAME} is up to date (${options.currentVersion})`);
|
|
654
|
+
return 0;
|
|
655
|
+
}
|
|
656
|
+
if (checkOnly) {
|
|
657
|
+
log(`Update available: ${options.currentVersion} -> ${targetVersion}`);
|
|
658
|
+
return 1;
|
|
659
|
+
}
|
|
660
|
+
let manager;
|
|
661
|
+
manager = await detectPackageManager(managerPreference, execFileFn);
|
|
662
|
+
log(`Installing ${PACKAGE_NAME}@${targetVersion} via ${manager}...`);
|
|
663
|
+
try {
|
|
664
|
+
const exitCode = await runInstall(manager, target, spawnFn);
|
|
665
|
+
if (exitCode !== 0) {
|
|
666
|
+
errorLog(`${manager} install failed with exit code ${exitCode}`);
|
|
667
|
+
printManualUpgradeHint(errorLog);
|
|
668
|
+
return exitCode;
|
|
669
|
+
}
|
|
670
|
+
} catch (error) {
|
|
671
|
+
errorLog(`Install failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
672
|
+
printManualUpgradeHint(errorLog);
|
|
673
|
+
return 1;
|
|
674
|
+
}
|
|
675
|
+
log(`Installed ${PACKAGE_NAME}@${targetVersion}`);
|
|
676
|
+
log("Run `cursor-agent-bridge --version` to verify the upgrade.");
|
|
677
|
+
const launchAgentPlist = getLaunchAgentPaths().plistPath;
|
|
678
|
+
if (existsFn(launchAgentPlist)) log("LaunchAgent detected. Run `cursor-agent-bridge launch-agent install` to refresh the service.");
|
|
679
|
+
return 0;
|
|
680
|
+
}
|
|
681
|
+
//#endregion
|
|
682
|
+
//#region src/cli.ts
|
|
8
683
|
const command = process.argv[2] && !process.argv[2]?.startsWith("-") ? process.argv[2] : "serve";
|
|
9
684
|
if (command === "help" || process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
10
685
|
console.log(`cursor-agent-bridge
|
|
11
686
|
|
|
12
687
|
Usage:
|
|
13
688
|
cursor-agent-bridge serve [--host 127.0.0.1] [--port 4646]
|
|
689
|
+
cursor-agent-bridge doctor [--host 127.0.0.1] [--port 4646] [--profile cursor] [--file ~/.codex/cursor.config.toml] [--skip-codex-config]
|
|
690
|
+
cursor-agent-bridge config print [--host 127.0.0.1] [--port 4646] [--profile cursor]
|
|
691
|
+
cursor-agent-bridge config check [--file ~/.codex/cursor.config.toml] [--host 127.0.0.1] [--port 4646] [--profile cursor]
|
|
692
|
+
cursor-agent-bridge config write [--file ~/.codex/cursor.config.toml] [--host 127.0.0.1] [--port 4646] [--profile cursor] [--force]
|
|
693
|
+
cursor-agent-bridge models [--json] [--refresh]
|
|
694
|
+
cursor-agent-bridge launch-agent install [--host 127.0.0.1] [--port 4646] [--agent-path agent]
|
|
695
|
+
cursor-agent-bridge launch-agent uninstall
|
|
696
|
+
cursor-agent-bridge launch-agent status
|
|
697
|
+
cursor-agent-bridge upgrade [--check] [--target latest] [--manager auto|npm|pnpm]
|
|
14
698
|
|
|
15
699
|
Environment:
|
|
16
700
|
HOST Listen host, default 127.0.0.1
|
|
@@ -19,12 +703,151 @@ Environment:
|
|
|
19
703
|
`);
|
|
20
704
|
process.exit(0);
|
|
21
705
|
}
|
|
706
|
+
if (command === "version" || process.argv.includes("--version")) {
|
|
707
|
+
console.log(version);
|
|
708
|
+
process.exit(0);
|
|
709
|
+
}
|
|
710
|
+
if (command === "upgrade") {
|
|
711
|
+
const checkOnly = process.argv.includes("--check");
|
|
712
|
+
const target = readArg("--target", "latest") ?? "latest";
|
|
713
|
+
const manager = readArg("--manager", "auto") ?? "auto";
|
|
714
|
+
if (manager !== "auto" && manager !== "npm" && manager !== "pnpm") {
|
|
715
|
+
console.error("Invalid --manager value. Use auto, npm, or pnpm.");
|
|
716
|
+
process.exit(1);
|
|
717
|
+
}
|
|
718
|
+
const exitCode = await runUpgrade({
|
|
719
|
+
currentVersion: version,
|
|
720
|
+
checkOnly,
|
|
721
|
+
target,
|
|
722
|
+
manager
|
|
723
|
+
});
|
|
724
|
+
process.exit(exitCode);
|
|
725
|
+
}
|
|
726
|
+
if (command === "doctor") try {
|
|
727
|
+
const { host, port } = readHostAndPort();
|
|
728
|
+
const profile = readArg("--profile", "cursor") ?? "cursor";
|
|
729
|
+
const codexConfigPath = readArg("--file", void 0);
|
|
730
|
+
const result = await runDoctor({
|
|
731
|
+
host,
|
|
732
|
+
port,
|
|
733
|
+
profile,
|
|
734
|
+
skipCodexConfig: process.argv.includes("--skip-codex-config"),
|
|
735
|
+
...process.env.CURSOR_AGENT_PATH ? { agentPath: process.env.CURSOR_AGENT_PATH } : {},
|
|
736
|
+
...codexConfigPath ? { codexConfigPath } : {}
|
|
737
|
+
});
|
|
738
|
+
process.stdout.write(formatDoctorReport(result));
|
|
739
|
+
process.exit(result.ok ? 0 : 1);
|
|
740
|
+
} catch (error) {
|
|
741
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
742
|
+
process.exit(1);
|
|
743
|
+
}
|
|
744
|
+
if (command === "config") {
|
|
745
|
+
const action = process.argv[3] ?? "print";
|
|
746
|
+
try {
|
|
747
|
+
const { host, port } = readHostAndPort();
|
|
748
|
+
const profile = readArg("--profile", "cursor") ?? "cursor";
|
|
749
|
+
const filePath = readArg("--file", void 0) ?? resolveCodexConfigPath(profile);
|
|
750
|
+
if (action === "print") {
|
|
751
|
+
process.stdout.write(buildCodexConfigToml({
|
|
752
|
+
host,
|
|
753
|
+
port,
|
|
754
|
+
profile
|
|
755
|
+
}));
|
|
756
|
+
console.error(`Start Codex with: codex --profile ${profile}`);
|
|
757
|
+
process.exit(0);
|
|
758
|
+
}
|
|
759
|
+
if (action === "check") {
|
|
760
|
+
const result = checkCodexConfig(await readFile(filePath, "utf8"), {
|
|
761
|
+
host,
|
|
762
|
+
port,
|
|
763
|
+
profile
|
|
764
|
+
});
|
|
765
|
+
if (result.ok) {
|
|
766
|
+
console.log(`Codex config looks correct: ${filePath}`);
|
|
767
|
+
process.exit(0);
|
|
768
|
+
}
|
|
769
|
+
for (const issue of result.issues) console.error(issue);
|
|
770
|
+
process.exit(1);
|
|
771
|
+
}
|
|
772
|
+
if (action === "write") {
|
|
773
|
+
const result = await writeCodexConfig({
|
|
774
|
+
filePath,
|
|
775
|
+
host,
|
|
776
|
+
port,
|
|
777
|
+
profile,
|
|
778
|
+
force: process.argv.includes("--force")
|
|
779
|
+
});
|
|
780
|
+
if (result.created) console.log(`Created Codex config at ${result.path}`);
|
|
781
|
+
else if (result.updated) console.log(`Updated Codex config at ${result.path}`);
|
|
782
|
+
else console.log(`Codex config already up to date at ${result.path}`);
|
|
783
|
+
console.log(`Start Codex with: codex --profile ${profile}`);
|
|
784
|
+
process.exit(0);
|
|
785
|
+
}
|
|
786
|
+
} catch (error) {
|
|
787
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}
|
|
790
|
+
console.error(`Unknown config action: ${action}`);
|
|
791
|
+
process.exit(1);
|
|
792
|
+
}
|
|
793
|
+
if (command === "models") try {
|
|
794
|
+
const models = await new CursorRunner({ ...process.env.CURSOR_AGENT_PATH ? { agentPath: process.env.CURSOR_AGENT_PATH } : {} }).listModels({ refresh: process.argv.includes("--refresh") });
|
|
795
|
+
if (process.argv.includes("--json")) {
|
|
796
|
+
console.log(JSON.stringify(toOpenAIModelList(models), null, 2));
|
|
797
|
+
process.exit(0);
|
|
798
|
+
}
|
|
799
|
+
for (const model of models) console.log(model.id);
|
|
800
|
+
process.exit(0);
|
|
801
|
+
} catch (error) {
|
|
802
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
803
|
+
process.exit(1);
|
|
804
|
+
}
|
|
805
|
+
if (command === "launch-agent") {
|
|
806
|
+
const action = process.argv[3] ?? "status";
|
|
807
|
+
try {
|
|
808
|
+
if (action === "install") {
|
|
809
|
+
const { host, port } = readHostAndPort();
|
|
810
|
+
const agentPath = readArg("--agent-path", process.env.CURSOR_AGENT_PATH);
|
|
811
|
+
const paths = installLaunchAgent({
|
|
812
|
+
cliPath: process.argv[1] ?? "cursor-agent-bridge",
|
|
813
|
+
host,
|
|
814
|
+
port,
|
|
815
|
+
...agentPath ? { agentPath } : {}
|
|
816
|
+
});
|
|
817
|
+
console.log(`Installed ${paths.label}`);
|
|
818
|
+
console.log(paths.plistPath);
|
|
819
|
+
process.exit(0);
|
|
820
|
+
}
|
|
821
|
+
if (action === "uninstall") {
|
|
822
|
+
const paths = uninstallLaunchAgent();
|
|
823
|
+
console.log(`Uninstalled ${paths.label}`);
|
|
824
|
+
process.exit(0);
|
|
825
|
+
}
|
|
826
|
+
if (action === "status") {
|
|
827
|
+
process.stdout.write(printLaunchAgentStatus());
|
|
828
|
+
process.exit(0);
|
|
829
|
+
}
|
|
830
|
+
} catch (error) {
|
|
831
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
832
|
+
process.exit(1);
|
|
833
|
+
}
|
|
834
|
+
console.error(`Unknown launch-agent action: ${action}`);
|
|
835
|
+
process.exit(1);
|
|
836
|
+
}
|
|
22
837
|
if (command !== "serve") {
|
|
23
838
|
console.error(`Unknown command: ${command}`);
|
|
24
839
|
process.exit(1);
|
|
25
840
|
}
|
|
26
|
-
|
|
27
|
-
|
|
841
|
+
let host;
|
|
842
|
+
let port;
|
|
843
|
+
try {
|
|
844
|
+
const options = readHostAndPort();
|
|
845
|
+
host = options.host;
|
|
846
|
+
port = options.port;
|
|
847
|
+
} catch (error) {
|
|
848
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
849
|
+
process.exit(1);
|
|
850
|
+
}
|
|
28
851
|
const server = await startServer({
|
|
29
852
|
host,
|
|
30
853
|
port,
|