@velum-labs/cursorkit 0.1.0 → 0.1.2
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 +18 -17
- package/dist/src/ckLauncher.d.ts +16 -6
- package/dist/src/ckLauncher.js +334 -478
- package/dist/src/cli.d.ts +2 -1
- package/dist/src/cli.js +25 -253
- package/dist/src/commands/doctor.d.ts +2 -0
- package/dist/src/commands/doctor.js +129 -0
- package/dist/src/commands/maintenance.d.ts +2 -0
- package/dist/src/commands/maintenance.js +94 -0
- package/dist/src/commands/render.d.ts +14 -0
- package/dist/src/commands/render.js +17 -0
- package/dist/src/commands/serve.d.ts +2 -0
- package/dist/src/commands/serve.js +52 -0
- package/dist/src/cursorDesktopState.d.ts +10 -0
- package/dist/src/cursorDesktopState.js +204 -0
- package/dist/src/desktopConnectProxy.d.ts +10 -0
- package/dist/src/desktopConnectProxy.js +7 -1
- package/dist/src/server.js +35 -2
- package/dist/src/tools/releaseCheck.d.ts +1 -1
- package/dist/src/tools/releaseCheck.js +1 -6
- package/dist/src/ui/index.d.ts +8 -0
- package/dist/src/ui/index.js +6 -0
- package/dist/src/ui/prompt.d.ts +30 -0
- package/dist/src/ui/prompt.js +182 -0
- package/dist/src/ui/runtime.d.ts +14 -0
- package/dist/src/ui/runtime.js +35 -0
- package/dist/src/ui/spinner.d.ts +31 -0
- package/dist/src/ui/spinner.js +102 -0
- package/dist/src/ui/steps.d.ts +38 -0
- package/dist/src/ui/steps.js +154 -0
- package/dist/src/ui/theme.d.ts +35 -0
- package/dist/src/ui/theme.js +63 -0
- package/package.json +5 -3
- package/dist/src/ck.d.ts +0 -2
- package/dist/src/ck.js +0 -6
package/dist/src/ckLauncher.js
CHANGED
|
@@ -4,10 +4,16 @@ import fs from "node:fs";
|
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { Command, InvalidArgumentError } from "commander";
|
|
7
8
|
import { loadConfig } from "./config.js";
|
|
9
|
+
import { buildLocalDesktopModelEntry, mergeLocalAgentBackendUrlsIntoApplicationUser, mergeLocalDesktopModelsIntoApplicationUser, } from "./cursorDesktopState.js";
|
|
10
|
+
// Re-exported for backwards compatibility (callers/tests import these from
|
|
11
|
+
// ckLauncher); the implementations now live in cursorDesktopState.
|
|
12
|
+
export { buildLocalDesktopModelEntry, mergeLocalAgentBackendUrlsIntoApplicationUser, mergeLocalDesktopModelsIntoApplicationUser, };
|
|
8
13
|
import { startDesktopConnectProxy, } from "./desktopConnectProxy.js";
|
|
9
14
|
import { DESKTOP_CERT_PATH, DESKTOP_HOSTNAME, DESKTOP_HOSTNAMES, DESKTOP_KEY_PATH, desktopCertificateStatus, desktopDnsStatus, desktopEnv, desktopTrustCommand, localModelBackendStatus, upstreamReachabilityStatus, writeDesktopCertificate, } from "./desktop.js";
|
|
10
15
|
import { AGENT_RUN_PATH, AGENT_RUN_SSE_PATH, AVAILABLE_MODELS_PATH, BIDI_APPEND_PATH, GET_DEFAULT_MODEL_FOR_CLI_PATH, GET_USABLE_MODELS_PATH, STREAM_CHAT_WITH_TOOLS_PATH, } from "./routes.js";
|
|
16
|
+
import { StepList, bold, brandHeader, cyan, dim, glyph, gray, green, note, red, uiStream, withSpinner, yellow, } from "./ui/index.js";
|
|
11
17
|
export const CK_STATE_DIR = path.join(".cursor-rpc", "ck");
|
|
12
18
|
export const CK_STATE_PATH = path.join(CK_STATE_DIR, "state.json");
|
|
13
19
|
const DEFAULT_CURSOR_USER_DATA_DIR = path.join(process.env.HOME ?? "", "Library", "Application Support", "Cursor");
|
|
@@ -24,158 +30,143 @@ const LOCAL_AGENT_CANDIDATE_PATHS = [
|
|
|
24
30
|
BIDI_APPEND_PATH,
|
|
25
31
|
STREAM_CHAT_WITH_TOOLS_PATH,
|
|
26
32
|
];
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
ck --instance-id name Use a fresh isolated ck state/profile subdirectory
|
|
36
|
-
ck --seed-auth-from-default Copy Cursor auth rows from your default profile
|
|
37
|
-
ck --no-seed-auth-from-default Start isolated Cursor without copying auth rows
|
|
38
|
-
ck --print Print commands without launching
|
|
39
|
-
ck doctor Check desktop launch readiness
|
|
40
|
-
ck cert Generate desktop proxy certificate
|
|
41
|
-
ck route Print manual desktop routing setup and rollback commands
|
|
42
|
-
ck route status Check desktop routing prerequisites and current DNS state
|
|
43
|
-
ck route rollback Print rollback commands only
|
|
44
|
-
ck route --method direct Print direct :443 routing commands instead of pf redirect
|
|
45
|
-
ck stop Stop ck-owned bridge process
|
|
46
|
-
ck --help Show this help
|
|
47
|
-
`;
|
|
48
|
-
export function parseCkArgs(argv) {
|
|
49
|
-
const args = argv.slice(2);
|
|
50
|
-
let dryRun = false;
|
|
51
|
-
let profileMode = "isolated";
|
|
52
|
-
let routeMethod = "pf";
|
|
53
|
-
let routeAction = "plan";
|
|
54
|
-
let debugPort;
|
|
55
|
-
let instanceId;
|
|
56
|
-
let seedAuthFromDefault;
|
|
57
|
-
let timeoutMs = DEFAULT_ROUTE_INVENTORY_TIMEOUT_MS;
|
|
58
|
-
const commandArgs = [];
|
|
59
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
60
|
-
const arg = args[index];
|
|
61
|
-
if (arg === "--print") {
|
|
62
|
-
dryRun = true;
|
|
63
|
-
}
|
|
64
|
-
else if (arg === "--use-default-profile") {
|
|
65
|
-
profileMode = "default";
|
|
66
|
-
}
|
|
67
|
-
else if (arg === "--profile") {
|
|
68
|
-
const next = args[index + 1];
|
|
69
|
-
if (next !== "isolated" && next !== "default") {
|
|
70
|
-
throw new Error("--profile must be isolated or default");
|
|
71
|
-
}
|
|
72
|
-
profileMode = next;
|
|
73
|
-
index += 1;
|
|
74
|
-
}
|
|
75
|
-
else if (arg === "--timeout-ms") {
|
|
76
|
-
const next = args[index + 1];
|
|
77
|
-
const parsed = Number(next);
|
|
78
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
79
|
-
throw new Error("--timeout-ms must be a positive integer");
|
|
80
|
-
}
|
|
81
|
-
timeoutMs = parsed;
|
|
82
|
-
index += 1;
|
|
83
|
-
}
|
|
84
|
-
else if (arg === "--method") {
|
|
85
|
-
const next = args[index + 1];
|
|
86
|
-
if (next !== "pf" && next !== "direct") {
|
|
87
|
-
throw new Error("--method must be pf or direct");
|
|
88
|
-
}
|
|
89
|
-
routeMethod = next;
|
|
90
|
-
index += 1;
|
|
91
|
-
}
|
|
92
|
-
else if (arg === "--debug-port") {
|
|
93
|
-
const next = args[index + 1];
|
|
94
|
-
const parsed = Number(next);
|
|
95
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
96
|
-
throw new Error("--debug-port must be a positive integer");
|
|
97
|
-
}
|
|
98
|
-
debugPort = parsed;
|
|
99
|
-
index += 1;
|
|
100
|
-
}
|
|
101
|
-
else if (arg === "--instance-id") {
|
|
102
|
-
const next = args[index + 1];
|
|
103
|
-
if (next === undefined || !/^[A-Za-z0-9._-]+$/.test(next)) {
|
|
104
|
-
throw new Error("--instance-id must use only letters, numbers, dot, underscore, or dash");
|
|
105
|
-
}
|
|
106
|
-
instanceId = next;
|
|
107
|
-
index += 1;
|
|
108
|
-
}
|
|
109
|
-
else if (arg === "--seed-auth-from-default") {
|
|
110
|
-
seedAuthFromDefault = true;
|
|
111
|
-
}
|
|
112
|
-
else if (arg === "--no-seed-auth-from-default") {
|
|
113
|
-
seedAuthFromDefault = false;
|
|
114
|
-
}
|
|
115
|
-
else if (arg !== undefined) {
|
|
116
|
-
commandArgs.push(arg);
|
|
33
|
+
const CK_ENV_HELP = `
|
|
34
|
+
cursorkit ck launches an isolated Cursor against a local desktop-proxy bridge.
|
|
35
|
+
Bridge logs stream live during \`ck\` and are written to .cursor-rpc/ck/bridge.log.`;
|
|
36
|
+
function parsePositiveInt(flag) {
|
|
37
|
+
return (value) => {
|
|
38
|
+
const parsed = Number(value);
|
|
39
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
40
|
+
throw new InvalidArgumentError(`${flag} must be a positive integer`);
|
|
117
41
|
}
|
|
42
|
+
return parsed;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function parseProfile(value) {
|
|
46
|
+
if (value !== "isolated" && value !== "default") {
|
|
47
|
+
throw new InvalidArgumentError("--profile must be isolated or default");
|
|
118
48
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
else if (second === "status" || second === "rollback") {
|
|
126
|
-
routeAction = second;
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
129
|
-
throw new Error("ck route subcommand must be status or rollback");
|
|
130
|
-
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
function parseMethod(value) {
|
|
52
|
+
if (value !== "pf" && value !== "direct") {
|
|
53
|
+
throw new InvalidArgumentError("--method must be pf or direct");
|
|
131
54
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
timeoutMs,
|
|
138
|
-
routeMethod,
|
|
139
|
-
routeAction,
|
|
140
|
-
debugPort,
|
|
141
|
-
instanceId,
|
|
142
|
-
seedAuthFromDefault,
|
|
143
|
-
});
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
function parseInstanceId(value) {
|
|
58
|
+
if (!/^[A-Za-z0-9._-]+$/.test(value)) {
|
|
59
|
+
throw new InvalidArgumentError("--instance-id must use only letters, numbers, dot, underscore, or dash");
|
|
144
60
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
function normalizeRouteAction(action) {
|
|
64
|
+
if (action === undefined)
|
|
65
|
+
return "plan";
|
|
66
|
+
if (action === "status" || action === "rollback")
|
|
67
|
+
return action;
|
|
68
|
+
throw new Error("ck route subcommand must be status or rollback");
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Shared launch/test options. Defaults are applied later in {@link ckArgsFromOpts}
|
|
72
|
+
* (not via commander) so the same flag works before or after a subcommand name:
|
|
73
|
+
* an unset option stays absent and the parent value wins via `optsWithGlobals`.
|
|
74
|
+
*/
|
|
75
|
+
function applyLaunchOptions(cmd) {
|
|
76
|
+
return cmd
|
|
77
|
+
.option("--print", "print commands without launching")
|
|
78
|
+
.option("--use-default-profile", "reuse your logged-in Cursor profile for auth-sensitive testing")
|
|
79
|
+
.option("--profile <mode>", "isolated | default", parseProfile)
|
|
80
|
+
.option("--timeout-ms <ms>", "launch/test route-inventory wait time", parsePositiveInt("--timeout-ms"))
|
|
81
|
+
.option("--debug-port <port>", "launch Cursor with a Chromium remote debugging port", parsePositiveInt("--debug-port"))
|
|
82
|
+
.option("--instance-id <name>", "use a fresh isolated ck state/profile subdirectory", parseInstanceId)
|
|
83
|
+
.option("--seed-auth-from-default", "copy Cursor auth rows from your default profile")
|
|
84
|
+
.option("--no-seed-auth-from-default", "start isolated Cursor without copying auth rows");
|
|
85
|
+
}
|
|
86
|
+
function ckArgsFromOpts(command, opts, routeAction) {
|
|
87
|
+
const profileMode = opts.profile ??
|
|
88
|
+
(opts.useDefaultProfile === true ? "default" : "isolated");
|
|
89
|
+
return compactCkArgs({
|
|
90
|
+
command,
|
|
91
|
+
dryRun: opts.print === true,
|
|
92
|
+
profileMode,
|
|
93
|
+
timeoutMs: opts.timeoutMs ??
|
|
94
|
+
DEFAULT_ROUTE_INVENTORY_TIMEOUT_MS,
|
|
95
|
+
routeMethod: opts.method ?? "pf",
|
|
96
|
+
routeAction,
|
|
97
|
+
debugPort: opts.debugPort,
|
|
98
|
+
instanceId: opts.instanceId,
|
|
99
|
+
seedAuthFromDefault: opts.seedAuthFromDefault,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Build the commander program for `ck`. `dispatch` receives the resolved
|
|
104
|
+
* {@link CkArgs}; the real binary runs the command while {@link parseCkArgs}
|
|
105
|
+
* captures the args for tests.
|
|
106
|
+
*/
|
|
107
|
+
export function buildCkProgram(dispatch) {
|
|
108
|
+
const program = new Command();
|
|
109
|
+
program
|
|
110
|
+
.name("ck")
|
|
111
|
+
.description("desktop proxy launcher for Cursor")
|
|
112
|
+
.addHelpText("after", CK_ENV_HELP);
|
|
113
|
+
applyLaunchOptions(program).action(function () {
|
|
114
|
+
return dispatch(ckArgsFromOpts("launch", this.optsWithGlobals(), "plan"));
|
|
115
|
+
});
|
|
116
|
+
applyLaunchOptions(program
|
|
117
|
+
.command("test")
|
|
118
|
+
.description("launch, monitor route inventory, print diagnosis, then stop bridge")).action(function () {
|
|
119
|
+
return dispatch(ckArgsFromOpts("test", this.optsWithGlobals(), "plan"));
|
|
120
|
+
});
|
|
121
|
+
program
|
|
122
|
+
.command("doctor")
|
|
123
|
+
.description("check desktop launch readiness")
|
|
124
|
+
.action(function () {
|
|
125
|
+
return dispatch(ckArgsFromOpts("doctor", this.optsWithGlobals(), "plan"));
|
|
126
|
+
});
|
|
127
|
+
program
|
|
128
|
+
.command("cert")
|
|
129
|
+
.description("generate desktop proxy certificate")
|
|
130
|
+
.action(function () {
|
|
131
|
+
return dispatch(ckArgsFromOpts("cert", this.optsWithGlobals(), "plan"));
|
|
132
|
+
});
|
|
133
|
+
program
|
|
134
|
+
.command("route [action]")
|
|
135
|
+
.description("print manual desktop routing setup and rollback commands (action: status | rollback)")
|
|
136
|
+
.option("--method <method>", "pf | direct", parseMethod)
|
|
137
|
+
.action(function (action) {
|
|
138
|
+
return dispatch(ckArgsFromOpts("route", this.optsWithGlobals(), normalizeRouteAction(action)));
|
|
139
|
+
});
|
|
140
|
+
program
|
|
141
|
+
.command("stop")
|
|
142
|
+
.description("stop ck-owned bridge process")
|
|
143
|
+
.action(function () {
|
|
144
|
+
return dispatch(ckArgsFromOpts("stop", this.optsWithGlobals(), "plan"));
|
|
145
|
+
});
|
|
146
|
+
return program;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Pure argument parser built on the commander program: returns the resolved
|
|
150
|
+
* {@link CkArgs} (or throws on invalid input) without running the command.
|
|
151
|
+
*/
|
|
152
|
+
export function parseCkArgs(argv) {
|
|
153
|
+
let captured;
|
|
154
|
+
const program = buildCkProgram((args) => {
|
|
155
|
+
captured = args;
|
|
156
|
+
});
|
|
157
|
+
// Throw (rather than exit) on invalid input, and stay silent; applied to
|
|
158
|
+
// every command since subcommands like `route` validate their own options.
|
|
159
|
+
const makeParseOnly = (cmd) => {
|
|
160
|
+
cmd.exitOverride();
|
|
161
|
+
cmd.configureOutput({ writeOut: () => { }, writeErr: () => { } });
|
|
162
|
+
cmd.commands.forEach(makeParseOnly);
|
|
163
|
+
};
|
|
164
|
+
makeParseOnly(program);
|
|
165
|
+
program.parse(argv);
|
|
166
|
+
if (captured === undefined) {
|
|
167
|
+
throw new Error("ck: no command parsed");
|
|
178
168
|
}
|
|
169
|
+
return captured;
|
|
179
170
|
}
|
|
180
171
|
function compactCkArgs(args) {
|
|
181
172
|
const { debugPort, instanceId, seedAuthFromDefault, ...rest } = args;
|
|
@@ -498,11 +489,14 @@ function desktopTestDiagnosis(report) {
|
|
|
498
489
|
return diagnosis;
|
|
499
490
|
}
|
|
500
491
|
export async function runCk(argv = process.argv) {
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
492
|
+
const program = buildCkProgram(runCkCommand);
|
|
493
|
+
await program.parseAsync(argv);
|
|
494
|
+
}
|
|
495
|
+
/** Attach the `ck` desktop launcher as a subcommand group of another program. */
|
|
496
|
+
export function registerCk(program) {
|
|
497
|
+
program.addCommand(buildCkProgram(runCkCommand));
|
|
498
|
+
}
|
|
499
|
+
async function runCkCommand(parsed) {
|
|
506
500
|
if (parsed.command === "cert") {
|
|
507
501
|
await printCertInstructions();
|
|
508
502
|
return;
|
|
@@ -536,9 +530,12 @@ export async function runCk(argv = process.argv) {
|
|
|
536
530
|
printPlan(plan);
|
|
537
531
|
return;
|
|
538
532
|
}
|
|
539
|
-
const cert = await writeDesktopCertificate()
|
|
533
|
+
const cert = await withSpinner("preparing desktop certificate", () => writeDesktopCertificate(), {
|
|
534
|
+
success: (result) => result.created
|
|
535
|
+
? "generated desktop proxy certificate"
|
|
536
|
+
: "desktop certificate ready",
|
|
537
|
+
});
|
|
540
538
|
if (cert.created) {
|
|
541
|
-
console.warn("Generated desktop proxy certificate.");
|
|
542
539
|
printTrustInstructions(cert.certPath);
|
|
543
540
|
}
|
|
544
541
|
if (parsed.command === "test") {
|
|
@@ -570,17 +567,25 @@ async function chooseFreePort() {
|
|
|
570
567
|
server.on("error", reject);
|
|
571
568
|
});
|
|
572
569
|
}
|
|
570
|
+
/** Write a single human-facing line to the UI stream (stderr). */
|
|
571
|
+
function out(line = "") {
|
|
572
|
+
uiStream().write(`${line}\n`);
|
|
573
|
+
}
|
|
573
574
|
async function launch(plan, timeoutMs) {
|
|
574
|
-
|
|
575
|
+
out(`\n${brandHeader("desktop launch")}\n`);
|
|
576
|
+
const { bridge, log, connectProxy } = await startBridge(plan, {
|
|
577
|
+
mirror: true,
|
|
578
|
+
});
|
|
575
579
|
const routeSeen = waitForRouteInventory(bridge, timeoutMs);
|
|
576
|
-
launchCursor(plan);
|
|
580
|
+
launchCursor(plan, {});
|
|
581
|
+
out(`${green(glyph.tick())} ${bold("ck ready")} ${dim(`https://127.0.0.1:${plan.bridgePort}`)} ${dim(`(log: ${plan.logPath})`)}`);
|
|
577
582
|
const observed = await routeSeen;
|
|
578
583
|
if (observed) {
|
|
579
|
-
|
|
584
|
+
out(`${green(glyph.tick())} desktop route inventory observed`);
|
|
580
585
|
}
|
|
581
586
|
else {
|
|
582
587
|
for (const line of routeInventoryTimeoutDiagnosis()) {
|
|
583
|
-
|
|
588
|
+
out(`${yellow(glyph.warn())} ${line}`);
|
|
584
589
|
}
|
|
585
590
|
}
|
|
586
591
|
await new Promise((resolve) => {
|
|
@@ -592,18 +597,38 @@ async function launch(plan, timeoutMs) {
|
|
|
592
597
|
});
|
|
593
598
|
}
|
|
594
599
|
async function testDesktopLaunch(plan, timeoutMs) {
|
|
595
|
-
|
|
600
|
+
out(`\n${brandHeader("desktop test")}\n`);
|
|
601
|
+
const steps = new StepList([
|
|
602
|
+
{ id: "bridge", label: "start bridge" },
|
|
603
|
+
{ id: "cursor", label: "launch Cursor" },
|
|
604
|
+
{ id: "inventory", label: "monitor route inventory" },
|
|
605
|
+
], { title: dim(`bridge log: ${plan.logPath}`) }).start();
|
|
606
|
+
steps.setActive("bridge");
|
|
607
|
+
const { bridge, log, connectProxy } = await startBridge(plan, {
|
|
608
|
+
mirror: false,
|
|
609
|
+
quiet: true,
|
|
610
|
+
});
|
|
611
|
+
steps.setDone("bridge", `127.0.0.1:${plan.bridgePort}`);
|
|
596
612
|
try {
|
|
597
|
-
|
|
598
|
-
|
|
613
|
+
steps.setActive("cursor");
|
|
614
|
+
launchCursor(plan, { quiet: true });
|
|
615
|
+
steps.setDone("cursor", plan.profileMode === "isolated" ? "isolated profile" : "default profile");
|
|
616
|
+
steps.setActive("inventory", `up to ${timeoutMs}ms`);
|
|
599
617
|
await delay(timeoutMs);
|
|
600
618
|
const logText = fs.existsSync(plan.logPath)
|
|
601
619
|
? fs.readFileSync(plan.logPath, "utf8")
|
|
602
620
|
: "";
|
|
603
621
|
const report = analyzeRouteInventoryLog(logText);
|
|
622
|
+
steps.setDone("inventory", report.routeInventorySeen ? "observed" : "none seen");
|
|
623
|
+
steps.stop();
|
|
604
624
|
writeLatestStatus(plan, report);
|
|
605
625
|
printDesktopTestReport(plan, report);
|
|
606
626
|
}
|
|
627
|
+
catch (error) {
|
|
628
|
+
steps.setFailed("inventory");
|
|
629
|
+
steps.stop();
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
607
632
|
finally {
|
|
608
633
|
bridge.kill("SIGTERM");
|
|
609
634
|
await connectProxy?.close();
|
|
@@ -613,55 +638,62 @@ async function testDesktopLaunch(plan, timeoutMs) {
|
|
|
613
638
|
}
|
|
614
639
|
}
|
|
615
640
|
}
|
|
616
|
-
async function startBridge(plan) {
|
|
641
|
+
async function startBridge(plan, options = {}) {
|
|
642
|
+
const { mirror = true, quiet = false } = options;
|
|
617
643
|
fs.mkdirSync(plan.stateDir, { recursive: true });
|
|
618
644
|
if (plan.userDataDir !== undefined) {
|
|
619
645
|
fs.mkdirSync(plan.userDataDir, { recursive: true });
|
|
620
646
|
}
|
|
621
647
|
fs.mkdirSync(plan.extensionsDir, { recursive: true });
|
|
622
648
|
plan.authSeedStatus = seedCursorAuthFromDefault(plan);
|
|
623
|
-
if (
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
plan.
|
|
628
|
-
|
|
649
|
+
if (!quiet) {
|
|
650
|
+
if (plan.authSeedStatus === "seeded") {
|
|
651
|
+
out(`${green(glyph.tick())} seeded isolated Cursor profile with default auth rows`);
|
|
652
|
+
}
|
|
653
|
+
else if (plan.seedAuthFromDefault &&
|
|
654
|
+
plan.authSeedStatus !== "not-isolated") {
|
|
655
|
+
out(`${yellow(glyph.warn())} Cursor auth seeding status: ${plan.authSeedStatus}`);
|
|
656
|
+
}
|
|
629
657
|
}
|
|
630
658
|
plan.localModelSeedStatus = seedLocalModelsIntoCursorState(plan);
|
|
631
|
-
if (
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
659
|
+
if (!quiet) {
|
|
660
|
+
if (plan.localModelSeedStatus === "seeded") {
|
|
661
|
+
out(`${green(glyph.tick())} seeded isolated Cursor profile with local model entries`);
|
|
662
|
+
}
|
|
663
|
+
else if (plan.localModelSeedStatus !== "not-isolated") {
|
|
664
|
+
out(`${yellow(glyph.warn())} Cursor local model seeding status: ${plan.localModelSeedStatus}`);
|
|
665
|
+
}
|
|
636
666
|
}
|
|
637
667
|
configureCursorNodeTlsEnv(plan);
|
|
638
668
|
assertSafe(plan.bridge);
|
|
639
669
|
assertSafe(plan.cursor);
|
|
640
|
-
|
|
641
|
-
|
|
670
|
+
if (!quiet) {
|
|
671
|
+
out(`${cyan(glyph.arrow())} starting bridge on 127.0.0.1:${plan.bridgePort} ${dim(`(log: ${plan.logPath})`)}`);
|
|
672
|
+
}
|
|
642
673
|
const log = fs.createWriteStream(plan.logPath, { flags: "w" });
|
|
643
674
|
const bridge = spawn(plan.bridge.executable, plan.bridge.args, {
|
|
644
675
|
env: { ...process.env, ...plan.bridge.env },
|
|
645
676
|
stdio: ["ignore", "pipe", "pipe"],
|
|
646
677
|
});
|
|
647
|
-
attachBridgeOutput(bridge, log);
|
|
678
|
+
attachBridgeOutput(bridge, log, mirror);
|
|
648
679
|
writeState(plan, bridge);
|
|
649
680
|
await waitForBridgeListening(bridge);
|
|
650
|
-
const connectProxy = await startConnectProxy(plan);
|
|
681
|
+
const connectProxy = await startConnectProxy(plan, quiet);
|
|
651
682
|
return {
|
|
652
683
|
bridge,
|
|
653
684
|
log,
|
|
654
685
|
...(connectProxy === undefined ? {} : { connectProxy }),
|
|
655
686
|
};
|
|
656
687
|
}
|
|
657
|
-
async function startConnectProxy(plan) {
|
|
688
|
+
async function startConnectProxy(plan, quiet = false) {
|
|
658
689
|
if (plan.connectProxyPort === undefined ||
|
|
659
690
|
plan.connectProxyLogPath === undefined) {
|
|
660
691
|
return undefined;
|
|
661
692
|
}
|
|
662
693
|
fs.writeFileSync(plan.connectProxyLogPath, "");
|
|
663
|
-
|
|
664
|
-
|
|
694
|
+
if (!quiet) {
|
|
695
|
+
out(`${cyan(glyph.arrow())} starting CONNECT proxy on 127.0.0.1:${plan.connectProxyPort} ${dim(`(log: ${plan.connectProxyLogPath})`)}`);
|
|
696
|
+
}
|
|
665
697
|
return startDesktopConnectProxy({
|
|
666
698
|
host: "127.0.0.1",
|
|
667
699
|
port: plan.connectProxyPort,
|
|
@@ -739,101 +771,24 @@ export function seedLocalModelsIntoCursorState(plan) {
|
|
|
739
771
|
if (typeof agentPublicOrigin === "string") {
|
|
740
772
|
mergeLocalAgentBackendUrlsIntoApplicationUser(applicationUser, agentPublicOrigin);
|
|
741
773
|
}
|
|
774
|
+
// The seed file mirrors the user's Cursor credentials (cursorCreds), so it is
|
|
775
|
+
// written with owner-only permissions and removed once sqlite3 has imported
|
|
776
|
+
// it rather than being left on disk.
|
|
742
777
|
const seedPath = path.join(globalStorageDir, "applicationUser.seed.json");
|
|
743
|
-
fs.writeFileSync(seedPath, JSON.stringify(applicationUser));
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
return "seeded";
|
|
753
|
-
}
|
|
754
|
-
export function mergeLocalAgentBackendUrlsIntoApplicationUser(applicationUser, agentOrigin) {
|
|
755
|
-
const cursorCreds = ensureRecord(applicationUser, "cursorCreds");
|
|
756
|
-
const urls = { default: agentOrigin };
|
|
757
|
-
cursorCreds.agentBackendUrlPrivacy = urls;
|
|
758
|
-
cursorCreds.agentBackendUrlNonPrivacy = urls;
|
|
759
|
-
}
|
|
760
|
-
export function mergeLocalDesktopModelsIntoApplicationUser(applicationUser, models) {
|
|
761
|
-
const localModelIds = new Set(models.map((model) => model.id));
|
|
762
|
-
const current = Array.isArray(applicationUser.availableDefaultModels2)
|
|
763
|
-
? applicationUser.availableDefaultModels2.filter((item) => {
|
|
764
|
-
if (!isPlainRecord(item) || typeof item.name !== "string") {
|
|
765
|
-
return true;
|
|
766
|
-
}
|
|
767
|
-
return !localModelIds.has(item.name);
|
|
768
|
-
})
|
|
769
|
-
: [];
|
|
770
|
-
applicationUser.availableDefaultModels2 = current;
|
|
771
|
-
const aiSettings = ensureRecord(applicationUser, "aiSettings");
|
|
772
|
-
for (const model of models) {
|
|
773
|
-
appendUnique(ensureStringArray(aiSettings, "userAddedModels"), model.id);
|
|
774
|
-
appendUnique(ensureStringArray(aiSettings, "modelOverrideEnabled"), model.id);
|
|
775
|
-
removeValue(ensureStringArray(aiSettings, "modelOverrideDisabled"), model.id);
|
|
776
|
-
}
|
|
777
|
-
const preferences = ensureRecord(aiSettings, "modelParameterPreferences");
|
|
778
|
-
const updatedAt = new Date().toISOString();
|
|
779
|
-
for (const model of models) {
|
|
780
|
-
preferences[model.id] = {
|
|
781
|
-
modelId: model.id,
|
|
782
|
-
parameters: localDesktopParameterValues(true),
|
|
783
|
-
updatedAt,
|
|
784
|
-
};
|
|
785
|
-
}
|
|
786
|
-
const firstModel = models[0];
|
|
787
|
-
if (firstModel !== undefined) {
|
|
788
|
-
applicationUser.useOpenAIKey = true;
|
|
789
|
-
applicationUser.openAIBaseUrl = firstModel.baseUrl;
|
|
790
|
-
applicationUser.openAIKey = firstModel.apiKey;
|
|
791
|
-
const modelConfig = ensureRecord(aiSettings, "modelConfig");
|
|
792
|
-
const selectedModel = {
|
|
793
|
-
modelId: firstModel.id,
|
|
794
|
-
parameters: localDesktopParameterValues(true),
|
|
795
|
-
};
|
|
796
|
-
for (const key of ["composer", "background-composer"]) {
|
|
797
|
-
modelConfig[key] = {
|
|
798
|
-
modelName: firstModel.id,
|
|
799
|
-
maxMode: true,
|
|
800
|
-
selectedModels: [selectedModel],
|
|
801
|
-
};
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
const featureModelConfigs = ensureRecord(applicationUser, "featureModelConfigs");
|
|
805
|
-
for (const value of Object.values(featureModelConfigs)) {
|
|
806
|
-
if (!isPlainRecord(value)) {
|
|
807
|
-
continue;
|
|
808
|
-
}
|
|
809
|
-
const fallbackModels = ensureStringArray(value, "fallbackModels");
|
|
810
|
-
for (const model of models) {
|
|
811
|
-
appendUnique(fallbackModels, model.id);
|
|
778
|
+
fs.writeFileSync(seedPath, JSON.stringify(applicationUser), { mode: 0o600 });
|
|
779
|
+
try {
|
|
780
|
+
const escapedSeedPath = seedPath.replaceAll("'", "''");
|
|
781
|
+
const seed = spawnSync("sqlite3", [
|
|
782
|
+
targetDb,
|
|
783
|
+
`insert or replace into ItemTable(key, value) values('${key}', cast(readfile('${escapedSeedPath}') as text));`,
|
|
784
|
+
], { encoding: "utf8" });
|
|
785
|
+
if (seed.status !== 0) {
|
|
786
|
+
return "sqlite-unavailable";
|
|
812
787
|
}
|
|
788
|
+
return "seeded";
|
|
813
789
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
if (!isPlainRecord(target[key])) {
|
|
817
|
-
target[key] = {};
|
|
818
|
-
}
|
|
819
|
-
return target[key];
|
|
820
|
-
}
|
|
821
|
-
function ensureStringArray(target, key) {
|
|
822
|
-
const values = Array.isArray(target[key])
|
|
823
|
-
? target[key].filter((value) => typeof value === "string")
|
|
824
|
-
: [];
|
|
825
|
-
target[key] = values;
|
|
826
|
-
return values;
|
|
827
|
-
}
|
|
828
|
-
function appendUnique(values, value) {
|
|
829
|
-
if (!values.includes(value)) {
|
|
830
|
-
values.push(value);
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
function removeValue(values, value) {
|
|
834
|
-
const index = values.indexOf(value);
|
|
835
|
-
if (index !== -1) {
|
|
836
|
-
values.splice(index, 1);
|
|
790
|
+
finally {
|
|
791
|
+
fs.rmSync(seedPath, { force: true });
|
|
837
792
|
}
|
|
838
793
|
}
|
|
839
794
|
function defaultCursorStateDbPath() {
|
|
@@ -853,119 +808,6 @@ function readCursorStateValue(dbPath, key) {
|
|
|
853
808
|
}
|
|
854
809
|
return result.stdout;
|
|
855
810
|
}
|
|
856
|
-
export function buildLocalDesktopModelEntry(model) {
|
|
857
|
-
const tooltipData = {
|
|
858
|
-
primaryText: "",
|
|
859
|
-
secondaryText: "",
|
|
860
|
-
secondaryWarningText: false,
|
|
861
|
-
icon: "",
|
|
862
|
-
tertiaryText: "",
|
|
863
|
-
tertiaryTextUrl: "",
|
|
864
|
-
markdownContent: `**${model.displayName}**<br />Local OpenAI-compatible model served by cursorkit.<br /><br />${model.contextTokenLimit.toLocaleString()} token context window`,
|
|
865
|
-
};
|
|
866
|
-
return {
|
|
867
|
-
name: model.id,
|
|
868
|
-
serverModelName: model.id,
|
|
869
|
-
clientDisplayName: model.displayName,
|
|
870
|
-
inputboxShortModelName: model.displayName,
|
|
871
|
-
vendorName: "local",
|
|
872
|
-
vendor: { displayName: "Local" },
|
|
873
|
-
supportsAgent: true,
|
|
874
|
-
supportsCmdK: false,
|
|
875
|
-
supportsImages: false,
|
|
876
|
-
supportsMaxMode: true,
|
|
877
|
-
supportsNonMaxMode: true,
|
|
878
|
-
supportsPlanMode: true,
|
|
879
|
-
supportsSandboxing: false,
|
|
880
|
-
supportsThinking: false,
|
|
881
|
-
cloudAgentEffortModes: [],
|
|
882
|
-
defaultOn: true,
|
|
883
|
-
degradationStatus: 0,
|
|
884
|
-
isRecommendedForBackgroundComposer: false,
|
|
885
|
-
legacySlugs: [model.id],
|
|
886
|
-
idAliases: [model.id, model.displayName],
|
|
887
|
-
parameterDefinitions: localDesktopParameterDefinitions(),
|
|
888
|
-
namedModelSectionIndex: 10_000,
|
|
889
|
-
visibleInRoutedModelView: true,
|
|
890
|
-
tooltipData,
|
|
891
|
-
tooltipDataForMaxMode: tooltipData,
|
|
892
|
-
variants: [
|
|
893
|
-
localDesktopVariantConfig(model, tooltipData, false),
|
|
894
|
-
localDesktopVariantConfig(model, tooltipData, true),
|
|
895
|
-
],
|
|
896
|
-
};
|
|
897
|
-
}
|
|
898
|
-
function localDesktopVariantConfig(model, tooltipData, isMaxMode) {
|
|
899
|
-
return {
|
|
900
|
-
parameterValues: localDesktopParameterValues(isMaxMode),
|
|
901
|
-
displayName: model.displayName,
|
|
902
|
-
isMaxMode,
|
|
903
|
-
isDefaultMaxConfig: isMaxMode,
|
|
904
|
-
isDefaultNonMaxConfig: !isMaxMode,
|
|
905
|
-
tooltipData,
|
|
906
|
-
displayNameOutsidePicker: model.displayName,
|
|
907
|
-
variantStringRepresentation: localDesktopVariantString(model.id, isMaxMode),
|
|
908
|
-
legacySlug: model.id,
|
|
909
|
-
};
|
|
910
|
-
}
|
|
911
|
-
function localDesktopVariantString(modelId, isMaxMode) {
|
|
912
|
-
const context = isMaxMode ? "1m" : "272k";
|
|
913
|
-
return `${modelId}[context=${context},reasoning=medium,fast=false]`;
|
|
914
|
-
}
|
|
915
|
-
function localDesktopParameterValues(isMaxMode) {
|
|
916
|
-
return [
|
|
917
|
-
{ id: "context", value: isMaxMode ? "1m" : "272k" },
|
|
918
|
-
{ id: "reasoning", value: "medium" },
|
|
919
|
-
{ id: "fast", value: "false" },
|
|
920
|
-
];
|
|
921
|
-
}
|
|
922
|
-
function localDesktopParameterDefinitions() {
|
|
923
|
-
return [
|
|
924
|
-
{
|
|
925
|
-
id: "context",
|
|
926
|
-
name: "Context",
|
|
927
|
-
markdownTooltip: "Context size the model has available.",
|
|
928
|
-
parameterType: {
|
|
929
|
-
enumParameter: {
|
|
930
|
-
values: [
|
|
931
|
-
{ value: "272k", displayName: "272K" },
|
|
932
|
-
{ value: "1m", displayName: "1M" },
|
|
933
|
-
],
|
|
934
|
-
},
|
|
935
|
-
},
|
|
936
|
-
},
|
|
937
|
-
{
|
|
938
|
-
id: "reasoning",
|
|
939
|
-
name: "Reasoning",
|
|
940
|
-
markdownTooltip: "Reasoning effort the model uses to generate its response.",
|
|
941
|
-
parameterType: {
|
|
942
|
-
enumParameter: {
|
|
943
|
-
values: [
|
|
944
|
-
{ value: "none", displayName: "None" },
|
|
945
|
-
{ value: "low", displayName: "Low" },
|
|
946
|
-
{ value: "medium", displayName: "Medium" },
|
|
947
|
-
{ value: "high", displayName: "High" },
|
|
948
|
-
{ value: "extra-high", displayName: "Extra High" },
|
|
949
|
-
],
|
|
950
|
-
},
|
|
951
|
-
},
|
|
952
|
-
isCycleableByHotkey: true,
|
|
953
|
-
},
|
|
954
|
-
{
|
|
955
|
-
id: "fast",
|
|
956
|
-
name: "Fast",
|
|
957
|
-
markdownTooltip: "Use the provider's fast lane when supported.",
|
|
958
|
-
parameterType: {
|
|
959
|
-
booleanParameter: {
|
|
960
|
-
values: [{ value: "false" }, { value: "true", displayName: "Fast" }],
|
|
961
|
-
},
|
|
962
|
-
},
|
|
963
|
-
},
|
|
964
|
-
];
|
|
965
|
-
}
|
|
966
|
-
function isPlainRecord(value) {
|
|
967
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
968
|
-
}
|
|
969
811
|
export function cleanupIsolatedCursorProcesses(userDataDir) {
|
|
970
812
|
const killed = terminateIsolatedCursorProcesses(userDataDir, "SIGTERM");
|
|
971
813
|
spawnSync("sleep", ["1"]);
|
|
@@ -1014,30 +856,35 @@ function cursorCommandUsesUserDataDir(command, userDataDir) {
|
|
|
1014
856
|
command.includes(`--user-data-dir "${userDataDir}"`) ||
|
|
1015
857
|
command.includes(`--user-data-dir '${userDataDir}'`));
|
|
1016
858
|
}
|
|
1017
|
-
function launchCursor(plan) {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
859
|
+
function launchCursor(plan, options = {}) {
|
|
860
|
+
const quiet = options.quiet ?? false;
|
|
861
|
+
if (!quiet) {
|
|
862
|
+
out(plan.profileMode === "isolated"
|
|
863
|
+
? `${cyan(glyph.arrow())} launching isolated Cursor instance`
|
|
864
|
+
: `${cyan(glyph.arrow())} launching Cursor with the default signed-in profile`);
|
|
865
|
+
if (plan.profileMode === "default") {
|
|
866
|
+
out(`${yellow(glyph.warn())} default profile mode reuses your existing Cursor auth state; it is less isolated but avoids browser login callback loss.`);
|
|
867
|
+
}
|
|
1023
868
|
}
|
|
1024
869
|
const cursor = spawn(plan.cursor.executable, plan.cursor.args, {
|
|
1025
870
|
env: { ...process.env, ...plan.cursor.env },
|
|
1026
871
|
stdio: "ignore",
|
|
1027
872
|
});
|
|
1028
873
|
cursor.on("error", (error) => {
|
|
1029
|
-
|
|
874
|
+
out(`${red(glyph.cross())} Cursor launch failed: ${error.message}`);
|
|
1030
875
|
});
|
|
1031
876
|
return cursor;
|
|
1032
877
|
}
|
|
1033
|
-
function attachBridgeOutput(bridge, log) {
|
|
878
|
+
function attachBridgeOutput(bridge, log, mirror) {
|
|
1034
879
|
bridge.stdout?.on("data", (chunk) => {
|
|
1035
880
|
log.write(chunk);
|
|
1036
|
-
|
|
881
|
+
if (mirror)
|
|
882
|
+
process.stdout.write(chunk);
|
|
1037
883
|
});
|
|
1038
884
|
bridge.stderr?.on("data", (chunk) => {
|
|
1039
885
|
log.write(chunk);
|
|
1040
|
-
|
|
886
|
+
if (mirror)
|
|
887
|
+
process.stderr.write(chunk);
|
|
1041
888
|
});
|
|
1042
889
|
}
|
|
1043
890
|
function writeLatestStatus(plan, report) {
|
|
@@ -1061,29 +908,32 @@ function writeLatestStatus(plan, report) {
|
|
|
1061
908
|
}, null, 2));
|
|
1062
909
|
}
|
|
1063
910
|
function printDesktopTestReport(plan, report) {
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
911
|
+
const field = (label, value) => out(`${dim(`${label}:`)} ${value}`);
|
|
912
|
+
out("");
|
|
913
|
+
out(bold("Desktop Test Report"));
|
|
914
|
+
field("route inventory", report.routeInventorySeen ? green("yes") : yellow("no"));
|
|
915
|
+
field("model routes seen", report.modelRoutesSeen.length > 0
|
|
1068
916
|
? report.modelRoutesSeen.join(", ")
|
|
1069
|
-
: "none"
|
|
1070
|
-
|
|
917
|
+
: "none");
|
|
918
|
+
field("model routes missing", report.missingModelRoutes.length > 0
|
|
1071
919
|
? report.missingModelRoutes.join(", ")
|
|
1072
|
-
: "none"
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
920
|
+
: "none");
|
|
921
|
+
field("observed paths", report.observedPaths.length > 0 ? report.observedPaths.join(", ") : "none");
|
|
922
|
+
field("failed routes", report.failedRoutes.length > 0
|
|
923
|
+
? red(String(report.failedRoutes.length))
|
|
924
|
+
: String(report.failedRoutes.length));
|
|
925
|
+
field("pass-through routes", String(report.passThroughRoutes.length));
|
|
926
|
+
field("auth seed", `${plan.authSeedStatus ?? "not-run"}; local model seed: ${plan.localModelSeedStatus ?? "not-run"}`);
|
|
927
|
+
field("route categories", report.routeCategories.length > 0
|
|
1078
928
|
? report.routeCategories
|
|
1079
929
|
.map((entry) => `${entry.category}:${entry.path}`)
|
|
1080
930
|
.join(", ")
|
|
1081
|
-
: "none"
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
931
|
+
: "none");
|
|
932
|
+
field("log", plan.logPath);
|
|
933
|
+
field("state", plan.statePath);
|
|
934
|
+
out(bold("diagnosis:"));
|
|
1085
935
|
for (const line of report.diagnosis) {
|
|
1086
|
-
|
|
936
|
+
out(` ${cyan(glyph.bullet())} ${line}`);
|
|
1087
937
|
}
|
|
1088
938
|
}
|
|
1089
939
|
function bridgeCommandSpec(cwd) {
|
|
@@ -1160,52 +1010,58 @@ function configureCursorNodeTlsEnv(plan) {
|
|
|
1160
1010
|
if (plan.profileMode !== "isolated") {
|
|
1161
1011
|
return;
|
|
1162
1012
|
}
|
|
1013
|
+
// Trust only the bridge's self-signed certificate via NODE_EXTRA_CA_CERTS
|
|
1014
|
+
// instead of globally disabling TLS verification. This keeps certificate
|
|
1015
|
+
// validation intact for real upstream traffic (e.g. api2.cursor.sh) while
|
|
1016
|
+
// allowing the spawned Cursor process to connect to the local bridge.
|
|
1017
|
+
const bridgeCertPath = plan.bridge.env?.BRIDGE_CERT_PATH ?? DESKTOP_CERT_PATH;
|
|
1163
1018
|
plan.cursor.env = {
|
|
1164
1019
|
...plan.cursor.env,
|
|
1165
|
-
|
|
1020
|
+
NODE_EXTRA_CA_CERTS: bridgeCertPath,
|
|
1166
1021
|
};
|
|
1167
1022
|
}
|
|
1168
1023
|
function printPlan(plan) {
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1024
|
+
out(bold("Bridge:"));
|
|
1025
|
+
out(commandForDisplay(plan.bridge));
|
|
1026
|
+
out("");
|
|
1027
|
+
out(bold("Cursor:"));
|
|
1028
|
+
out(commandForDisplay(plan.cursor));
|
|
1029
|
+
out("");
|
|
1030
|
+
out(`${dim("State:")} ${plan.statePath}`);
|
|
1031
|
+
out(`${dim("Log:")} ${plan.logPath}`);
|
|
1177
1032
|
if (plan.connectProxyPort !== undefined) {
|
|
1178
|
-
|
|
1033
|
+
out(`${dim("CONNECT proxy:")} 127.0.0.1:${plan.connectProxyPort}`);
|
|
1179
1034
|
}
|
|
1180
1035
|
if (plan.agentHttpPort !== undefined) {
|
|
1181
|
-
|
|
1036
|
+
out(`${dim("Agent HTTP bridge:")} 127.0.0.1:${plan.agentHttpPort}`);
|
|
1182
1037
|
}
|
|
1183
1038
|
if (plan.connectProxyLogPath !== undefined) {
|
|
1184
|
-
|
|
1039
|
+
out(`${dim("CONNECT proxy log:")} ${plan.connectProxyLogPath}`);
|
|
1185
1040
|
}
|
|
1186
1041
|
}
|
|
1187
1042
|
async function printCertInstructions() {
|
|
1188
|
-
const cert = await writeDesktopCertificate();
|
|
1189
|
-
|
|
1190
|
-
|
|
1043
|
+
const cert = await withSpinner("generating desktop proxy certificate", () => writeDesktopCertificate());
|
|
1044
|
+
out(`${dim("cert:")} ${cert.certPath}`);
|
|
1045
|
+
out(`${dim("key:")} ${cert.keyPath}`);
|
|
1191
1046
|
printTrustInstructions(cert.certPath);
|
|
1192
1047
|
}
|
|
1193
1048
|
function printTrustInstructions(certPath) {
|
|
1194
|
-
|
|
1195
|
-
|
|
1049
|
+
out(bold("Manual macOS trust command:"));
|
|
1050
|
+
out(desktopTrustCommand(certPath).map(shellQuote).join(" "));
|
|
1196
1051
|
}
|
|
1197
1052
|
async function printDoctor() {
|
|
1198
1053
|
const env = desktopEnv(process.env);
|
|
1199
1054
|
const config = loadConfig(env);
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1055
|
+
out(`\n${brandHeader("desktop launch readiness")}\n`);
|
|
1056
|
+
out(`${dim("desktop cert:")} ${desktopCertificateStatus(config)}`);
|
|
1057
|
+
out(`${dim("desktop dns:")} ${await desktopDnsStatus(config)}`);
|
|
1058
|
+
out(`${dim("upstream reachability:")} ${await upstreamReachabilityStatus(config)}`);
|
|
1059
|
+
out(`${dim("local model backend:")} ${await localModelBackendStatus(config)}`);
|
|
1204
1060
|
if (!fs.existsSync(DESKTOP_CERT_PATH) || !fs.existsSync(DESKTOP_KEY_PATH)) {
|
|
1205
|
-
|
|
1061
|
+
out(`${yellow(glyph.warn())} run ${bold("ck cert")} before launching.`);
|
|
1206
1062
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1063
|
+
out("");
|
|
1064
|
+
note("manual route plan: pnpm ck route");
|
|
1209
1065
|
}
|
|
1210
1066
|
async function printRoute(action, method) {
|
|
1211
1067
|
if (action === "status") {
|
|
@@ -1220,90 +1076,90 @@ async function printRoute(action, method) {
|
|
|
1220
1076
|
printRoutePlan(plan);
|
|
1221
1077
|
}
|
|
1222
1078
|
function printRoutePlan(plan) {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1079
|
+
out(bold("Desktop Manual Routing Plan"));
|
|
1080
|
+
out(`${dim("method:")} ${plan.method}`);
|
|
1081
|
+
out(`${dim("primary hostname:")} ${plan.hostname}`);
|
|
1082
|
+
out(`${dim("hostnames:")} ${plan.hostnames.join(", ")}`);
|
|
1083
|
+
out(`${dim("bridge port:")} ${String(plan.bridgePort)}`);
|
|
1084
|
+
out(`${dim("upstream connect host:")} ${plan.upstreamConnectHost ?? "<set manually>"}`);
|
|
1085
|
+
out("");
|
|
1230
1086
|
if (plan.warnings.length > 0) {
|
|
1231
|
-
|
|
1087
|
+
out(bold("Warnings:"));
|
|
1232
1088
|
for (const warning of plan.warnings) {
|
|
1233
|
-
|
|
1089
|
+
out(`${yellow(glyph.warn())} ${warning}`);
|
|
1234
1090
|
}
|
|
1235
|
-
|
|
1091
|
+
out("");
|
|
1236
1092
|
}
|
|
1237
|
-
|
|
1093
|
+
out(bold("Setup commands to run manually:"));
|
|
1238
1094
|
for (const command of plan.setupCommands) {
|
|
1239
|
-
|
|
1095
|
+
out(commandForDisplay(command));
|
|
1240
1096
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1097
|
+
out("");
|
|
1098
|
+
out(bold("Verification:"));
|
|
1243
1099
|
for (const command of plan.verificationCommands) {
|
|
1244
|
-
|
|
1100
|
+
out(commandForDisplay(command));
|
|
1245
1101
|
}
|
|
1246
|
-
|
|
1247
|
-
|
|
1102
|
+
out("");
|
|
1103
|
+
out(bold("Rollback:"));
|
|
1248
1104
|
for (const command of plan.rollbackCommands) {
|
|
1249
|
-
|
|
1105
|
+
out(commandForDisplay(command));
|
|
1250
1106
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1107
|
+
out("");
|
|
1108
|
+
note("ck prints these commands only. It does not install trust, edit hosts, configure pf, or kill Cursor for you.");
|
|
1253
1109
|
}
|
|
1254
1110
|
async function printRouteStatus() {
|
|
1255
1111
|
const env = desktopEnv(process.env);
|
|
1256
1112
|
const config = loadConfig(env);
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1113
|
+
out(bold("Desktop Routing Status"));
|
|
1114
|
+
out(`${dim("desktop cert:")} ${desktopCertificateStatus(config)}`);
|
|
1115
|
+
out(`${dim("desktop dns:")} ${await desktopDnsStatus(config)}`);
|
|
1260
1116
|
for (const hostname of DESKTOP_HOSTNAMES.filter((hostname) => hostname !== DESKTOP_HOSTNAME)) {
|
|
1261
|
-
|
|
1117
|
+
out(`${dim("desktop dns:")} ${await desktopDnsStatusForHostname(hostname)}`);
|
|
1262
1118
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1119
|
+
out(`${dim("detected upstream connect host:")} ${(await detectUpstreamConnectHost()) ?? "<none; set CURSOR_UPSTREAM_CONNECT_HOST manually>"}`);
|
|
1120
|
+
out(`${dim("configured upstream connect:")} ${config.upstreamConnectHost === undefined
|
|
1265
1121
|
? "system DNS"
|
|
1266
1122
|
: `${config.upstreamConnectHost}${config.upstreamConnectPort === undefined ? "" : `:${config.upstreamConnectPort}`}`}`);
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1123
|
+
out(`${dim("upstream reachability:")} ${await upstreamReachabilityStatus(config)}`);
|
|
1124
|
+
out(`${dim("local model backend:")} ${await localModelBackendStatus(config)}`);
|
|
1125
|
+
out("");
|
|
1126
|
+
out(bold("Next steps:"));
|
|
1127
|
+
note("Run `pnpm ck route` before system cutover to capture a real upstream IP.");
|
|
1128
|
+
note("Run `pnpm ck route rollback` to print the rollback commands.");
|
|
1273
1129
|
}
|
|
1274
1130
|
function printRouteRollback(plan) {
|
|
1275
|
-
|
|
1131
|
+
out(bold("Desktop Routing Rollback"));
|
|
1276
1132
|
for (const command of plan.rollbackCommands) {
|
|
1277
|
-
|
|
1133
|
+
out(commandForDisplay(command));
|
|
1278
1134
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1135
|
+
out("");
|
|
1136
|
+
note("ck prints rollback commands only. Review them before running; especially `pkill -x Cursor`.");
|
|
1281
1137
|
}
|
|
1282
1138
|
async function stopBridgeFromState() {
|
|
1283
1139
|
if (!fs.existsSync(CK_STATE_PATH)) {
|
|
1284
|
-
|
|
1140
|
+
out(`${gray(glyph.bullet())} No ck state file found.`);
|
|
1285
1141
|
return;
|
|
1286
1142
|
}
|
|
1287
1143
|
const state = JSON.parse(fs.readFileSync(CK_STATE_PATH, "utf8"));
|
|
1288
1144
|
if (state.bridgePid === undefined) {
|
|
1289
|
-
|
|
1145
|
+
out(`${gray(glyph.bullet())} No bridge PID recorded.`);
|
|
1290
1146
|
return;
|
|
1291
1147
|
}
|
|
1292
1148
|
const command = processCommandForPid(state.bridgePid);
|
|
1293
1149
|
if (command === undefined) {
|
|
1294
|
-
|
|
1150
|
+
out(`${yellow(glyph.warn())} No running process found for ck bridge PID ${state.bridgePid}.`);
|
|
1295
1151
|
return;
|
|
1296
1152
|
}
|
|
1297
1153
|
if (!bridgeProcessMatchesState(command, state)) {
|
|
1298
|
-
|
|
1154
|
+
out(`${yellow(glyph.warn())} Refusing to stop PID ${state.bridgePid}; it does not look like the ck-owned desktop bridge recorded in state.`);
|
|
1299
1155
|
return;
|
|
1300
1156
|
}
|
|
1301
1157
|
try {
|
|
1302
1158
|
process.kill(state.bridgePid, "SIGTERM");
|
|
1303
|
-
|
|
1159
|
+
out(`${green(glyph.tick())} Stopped ck bridge process ${state.bridgePid}.`);
|
|
1304
1160
|
}
|
|
1305
1161
|
catch (error) {
|
|
1306
|
-
|
|
1162
|
+
out(`${yellow(glyph.warn())} Could not stop bridge process ${state.bridgePid}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1307
1163
|
}
|
|
1308
1164
|
}
|
|
1309
1165
|
export function bridgeProcessMatchesState(command, state = {}) {
|