ocx-cursor 1.0.1 → 1.0.3
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 +7 -3
- package/bin/ocx-cursor.mjs +29 -1
- package/package.json +1 -1
- package/src/cursor-patch.mjs +83 -13
- package/src/cursor-state.mjs +11 -0
- package/src/install.mjs +57 -18
- package/src/paths.mjs +2 -1
- package/src/service.mjs +23 -4
- package/src/sync.mjs +8 -4
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
Use your active [OpenCodex](https://github.com/lidge-jun/opencodex) models in Cursor through an authenticated HTTPS endpoint on your Mac.
|
|
7
7
|
|
|
8
8
|
> [!WARNING]
|
|
9
|
-
> The installer edits two private workbench files inside `Cursor.app`. This invalidates Cursor's vendor signature
|
|
9
|
+
> The installer edits two private workbench files inside `Cursor.app`. This invalidates Cursor's vendor signature, so the bridge applies a local ad-hoc seal and removes the app's quarantine attribute after each patch to prevent macOS from reporting a corrupt installation. The bridge saves the original files under `~/.opencodex/cursor-bridge/cursor-app-backups`. Reinstall Cursor to restore a vendor-signed app.
|
|
10
10
|
|
|
11
11
|
## Requirements
|
|
12
12
|
|
|
@@ -125,14 +125,18 @@ The patch keeps the model metadata that Cursor would discard. It leaves Cursor's
|
|
|
125
125
|
|
|
126
126
|
The login service refreshes the catalog every 15 seconds. It waits for Cursor to quit before writing the state database or reapplying a patch after a Cursor update.
|
|
127
127
|
|
|
128
|
+
On macOS, the patch monitor pauses while Cursor's `ShipIt` updater is running. After Cursor and the updater stop, it waits until all patch targets remain unchanged for five seconds before writing them. This prevents the bridge from modifying `Cursor.app` while an in-app update is replacing it.
|
|
129
|
+
|
|
128
130
|
## Commands
|
|
129
131
|
|
|
130
132
|
| Command | Action |
|
|
131
133
|
| --- | --- |
|
|
132
134
|
| `ocx-cursor init --base-url URL` | Install the service, test the HTTPS endpoint, configure Cursor, and sync models. |
|
|
133
135
|
| `ocx-cursor install` | Reinstall the service and check the two metadata patches. |
|
|
136
|
+
| `ocx-cursor stop` | Stop the service without removing its configuration or state. |
|
|
137
|
+
| `ocx-cursor start` | Start the installed service. |
|
|
134
138
|
| `ocx-cursor update` | Install the current npm release and restart the service. |
|
|
135
|
-
| `ocx-cursor sync` |
|
|
139
|
+
| `ocx-cursor sync` | With Cursor closed, pause the service, verify the app patch, sync the active catalog, and restart the service. |
|
|
136
140
|
| `ocx-cursor status` | Print service, gateway, catalog, and pending-sync status. |
|
|
137
141
|
| `ocx-cursor uninstall` | Remove the service, command link, and bridge state. |
|
|
138
142
|
|
|
@@ -159,7 +163,7 @@ The bridge reads `~/.opencodex/service-api-token` when OpenCodex uses service au
|
|
|
159
163
|
| Cursor returns 401 | Quit Cursor and rerun `npx ocx-cursor init --base-url https://YOUR_HOST/v1`. |
|
|
160
164
|
| The hostname returns 502 | Run `ocx-cursor status`, then check that the tunnel points to port `10101`. |
|
|
161
165
|
| Cloudflare returns 1016 | Run `cloudflared tunnel info ocx-cursor` and check the service. |
|
|
162
|
-
| Models or controls are missing | Quit Cursor and run `ocx-cursor sync`. |
|
|
166
|
+
| Models or controls are missing | Quit Cursor and run `ocx-cursor sync`. It repairs the foreground app patch before writing model metadata. |
|
|
163
167
|
| A Cursor update removed the patch | Quit Cursor and run `ocx-cursor install`. |
|
|
164
168
|
|
|
165
169
|
Read the service logs:
|
package/bin/ocx-cursor.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import process from "node:process";
|
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { configureCursorOpenAI, storedCursorOpenAIBaseUrl } from "../src/cursor-config.mjs";
|
|
7
7
|
import { cursorIsRunning } from "../src/cursor-state.mjs";
|
|
8
|
-
import { installService, prepareInstallSecret, serviceStatus, uninstallService, updateService } from "../src/install.mjs";
|
|
8
|
+
import { installService, prepareInstallSecret, repairCursorMetadataPatch, serviceStatus, startService, stopService, uninstallService, updateService } from "../src/install.mjs";
|
|
9
9
|
import { cursorOpenAIBaseUrl, pendingFile } from "../src/paths.mjs";
|
|
10
10
|
import { runService } from "../src/service.mjs";
|
|
11
11
|
import { normalizeBaseUrl, testEndpoint } from "../src/setup.mjs";
|
|
@@ -19,6 +19,8 @@ Usage:
|
|
|
19
19
|
Install the service, test the endpoint, configure Cursor,
|
|
20
20
|
and sync models
|
|
21
21
|
ocx-cursor install Install and start the macOS companion service
|
|
22
|
+
ocx-cursor stop Stop the companion service without removing its state
|
|
23
|
+
ocx-cursor start Start the installed companion service
|
|
22
24
|
ocx-cursor update Install the latest companion release and restart it
|
|
23
25
|
ocx-cursor sync Sync active OpenCodex models into Cursor
|
|
24
26
|
ocx-cursor launch [--port <port>]
|
|
@@ -71,6 +73,10 @@ function printSync(result) {
|
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
function printCursorPatches(result) {
|
|
76
|
+
if (!result.cursorInstalled) {
|
|
77
|
+
process.stdout.write("Cursor is not installed; app patches will be applied after it is installed.\n");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
74
80
|
if (result.cursorPatches === null) {
|
|
75
81
|
process.stdout.write("Cursor app patches will be applied after Cursor quits.\n");
|
|
76
82
|
return;
|
|
@@ -131,7 +137,29 @@ async function main() {
|
|
|
131
137
|
await updateService();
|
|
132
138
|
return;
|
|
133
139
|
}
|
|
140
|
+
if (command === "stop") {
|
|
141
|
+
stopService();
|
|
142
|
+
process.stdout.write("Stopped OpenCodex Cursor Bridge.\n");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (command === "start") {
|
|
146
|
+
await startService();
|
|
147
|
+
process.stdout.write("Started OpenCodex Cursor Bridge.\n");
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
134
150
|
if (command === "sync") {
|
|
151
|
+
if (!cursorIsRunning()) {
|
|
152
|
+
stopService();
|
|
153
|
+
try {
|
|
154
|
+
const cursorPatches = await repairCursorMetadataPatch();
|
|
155
|
+
const changed = cursorPatches.filter(({ status }) => status === "patched").length;
|
|
156
|
+
process.stdout.write(`Verified ${cursorPatches.length} Cursor metadata patches (${changed} changed).\n`);
|
|
157
|
+
printSync(await syncNow());
|
|
158
|
+
} finally {
|
|
159
|
+
await startService();
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
135
163
|
printSync(await syncNow());
|
|
136
164
|
return;
|
|
137
165
|
}
|
package/package.json
CHANGED
package/src/cursor-patch.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { chmod, copyFile, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
3
4
|
import { basename, dirname, join } from "node:path";
|
|
4
5
|
import {
|
|
6
|
+
cursorAppPath,
|
|
5
7
|
cursorBundleFiles,
|
|
6
8
|
cursorGlassWorkbenchFile,
|
|
7
9
|
cursorLocalRuntimeFiles,
|
|
@@ -36,6 +38,21 @@ const localRuntimeBuilderExport = /buildBottlerocketPickerModels:\(\)=>(?<builde
|
|
|
36
38
|
const transientPatchErrorCodes = new Set(["EACCES", "EBUSY", "EPERM"]);
|
|
37
39
|
const directoryPatchQueues = new Map();
|
|
38
40
|
|
|
41
|
+
export function clearCursorAppQuarantine(options = {}) {
|
|
42
|
+
const execute = options.execFileSync || execFileSync;
|
|
43
|
+
execute("/usr/bin/xattr", ["-dr", "com.apple.quarantine", options.appPath || cursorAppPath], {
|
|
44
|
+
stdio: "ignore",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function finalizeCursorAppPatch(options = {}) {
|
|
49
|
+
const execute = options.execFileSync || execFileSync;
|
|
50
|
+
const appPath = options.appPath || cursorAppPath;
|
|
51
|
+
execute("/usr/bin/codesign", ["--force", "--sign", "-", appPath], { stdio: "ignore" });
|
|
52
|
+
execute("/usr/bin/codesign", ["--verify", "--deep", "--strict", appPath], { stdio: "ignore" });
|
|
53
|
+
clearCursorAppQuarantine({ ...options, appPath, execFileSync: execute });
|
|
54
|
+
}
|
|
55
|
+
|
|
39
56
|
function replaceSingleMatch(source, pattern, replacement, label) {
|
|
40
57
|
const matches = [...source.matchAll(pattern)];
|
|
41
58
|
if (matches.length !== 1) throw new Error(`Expected one Cursor ${label}, found ${matches.length}`);
|
|
@@ -354,33 +371,86 @@ export function startCursorModelMetadataPatchMonitor(options = {}) {
|
|
|
354
371
|
|
|
355
372
|
export function startCursorPatchMonitor(options = {}) {
|
|
356
373
|
const intervalMs = options.intervalMs || 250;
|
|
374
|
+
const stableMs = options.stableMs || 0;
|
|
357
375
|
const bundleFiles = options.file ? [options.file] : (options.bundleFiles || options.files || cursorBundleFiles);
|
|
358
376
|
const runtimeFiles = options.file ? [] : (options.localRuntimeFiles || cursorLocalRuntimeFiles);
|
|
359
377
|
const files = [...bundleFiles, ...runtimeFiles];
|
|
360
378
|
const localRuntimeFiles = new Set(runtimeFiles);
|
|
361
379
|
const lastSignatures = new Map();
|
|
380
|
+
let stableFingerprint = null;
|
|
381
|
+
let stableSince = 0;
|
|
362
382
|
let patchPromise = null;
|
|
363
383
|
let stopped = false;
|
|
364
384
|
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
385
|
+
const resetStability = () => {
|
|
386
|
+
stableFingerprint = null;
|
|
387
|
+
stableSince = 0;
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
const filesAreStable = async () => {
|
|
391
|
+
if (stableMs === 0) return true;
|
|
392
|
+
const signatures = await Promise.all(files.map(async (file) => {
|
|
369
393
|
try {
|
|
370
|
-
|
|
394
|
+
return await cursorWorkbenchSignature(file);
|
|
371
395
|
} catch {
|
|
372
|
-
return;
|
|
396
|
+
return null;
|
|
373
397
|
}
|
|
374
|
-
|
|
398
|
+
}));
|
|
399
|
+
if (signatures.some((signature) => signature === null)) {
|
|
400
|
+
resetStability();
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
const fingerprint = signatures.join("|");
|
|
404
|
+
const now = (options.now || Date.now)();
|
|
405
|
+
if (fingerprint !== stableFingerprint) {
|
|
406
|
+
stableFingerprint = fingerprint;
|
|
407
|
+
stableSince = now;
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
return now - stableSince >= stableMs;
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const check = async () => {
|
|
414
|
+
if (stopped || patchPromise) return patchPromise;
|
|
415
|
+
if (options.shouldPatch?.() === false) {
|
|
416
|
+
resetStability();
|
|
417
|
+
lastSignatures.clear();
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
patchPromise = (async () => {
|
|
421
|
+
if (!await filesAreStable()) return [];
|
|
422
|
+
let retryAfterStabilityDelay = false;
|
|
423
|
+
const results = await Promise.all(files.map(async (file) => {
|
|
424
|
+
let signature;
|
|
425
|
+
try {
|
|
426
|
+
signature = await cursorWorkbenchSignature(file);
|
|
427
|
+
} catch {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (signature === lastSignatures.get(file)) return;
|
|
431
|
+
try {
|
|
432
|
+
const result = await ensureCursorPatchFile(file, localRuntimeFiles, options);
|
|
433
|
+
lastSignatures.set(file, result.signature);
|
|
434
|
+
options.onResult?.(result);
|
|
435
|
+
return result;
|
|
436
|
+
} catch (error) {
|
|
437
|
+
if (transientPatchErrorCodes.has(error.code)) retryAfterStabilityDelay = true;
|
|
438
|
+
else lastSignatures.set(file, signature);
|
|
439
|
+
options.onError?.(error, file);
|
|
440
|
+
}
|
|
441
|
+
}));
|
|
442
|
+
if (retryAfterStabilityDelay) resetStability();
|
|
443
|
+
const patched = results.filter((result) => result?.status === "patched");
|
|
375
444
|
try {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
options.onResult?.(result);
|
|
445
|
+
if (patched.length > 0) await options.onPatched?.(patched);
|
|
446
|
+
if (results.length === files.length && results.every(Boolean)) await options.onReady?.(results);
|
|
379
447
|
} catch (error) {
|
|
380
|
-
|
|
381
|
-
|
|
448
|
+
resetStability();
|
|
449
|
+
lastSignatures.clear();
|
|
450
|
+
options.onError?.(error, options.appPath || cursorAppPath);
|
|
382
451
|
}
|
|
383
|
-
|
|
452
|
+
return results;
|
|
453
|
+
})().finally(() => {
|
|
384
454
|
patchPromise = null;
|
|
385
455
|
});
|
|
386
456
|
return patchPromise;
|
package/src/cursor-state.mjs
CHANGED
|
@@ -64,6 +64,17 @@ export function cursorIsRunning() {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
export function cursorUpdateIsRunning(execute = execFileSync) {
|
|
68
|
+
try {
|
|
69
|
+
execute("pgrep", ["-f", "(com\\.todesktop\\.230313mzl4w4u92|co\\.anysphere\\.cursor[^ ]*)\\.ShipIt"], {
|
|
70
|
+
stdio: "ignore",
|
|
71
|
+
});
|
|
72
|
+
return true;
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
67
78
|
export function defaultEffort(model) {
|
|
68
79
|
const modelId = model.sourceId.split("/").at(-1);
|
|
69
80
|
if (/^(?:gpt|gemini)-/.test(modelId) && model.reasoningEfforts.includes("medium")) return "medium";
|
package/src/install.mjs
CHANGED
|
@@ -4,10 +4,11 @@ import { access, chmod, copyFile, cp, lstat, mkdir, mkdtemp, readFile, readlink,
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { ensureCursorModelMetadataPatched } from "./cursor-patch.mjs";
|
|
7
|
+
import { ensureCursorModelMetadataPatched, finalizeCursorAppPatch } from "./cursor-patch.mjs";
|
|
8
8
|
import { cursorIsRunning } from "./cursor-state.mjs";
|
|
9
9
|
import {
|
|
10
10
|
cliLinkFile,
|
|
11
|
+
cursorAppPath,
|
|
11
12
|
installRoot,
|
|
12
13
|
installedPackageRoot,
|
|
13
14
|
launchAgentFile,
|
|
@@ -39,16 +40,16 @@ function commandOutput(command, args) {
|
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
function bootout(label) {
|
|
43
|
+
function bootout(label, execute = execFileSync) {
|
|
43
44
|
try {
|
|
44
|
-
|
|
45
|
+
execute("launchctl", ["bootout", `${launchDomain}/${label}`], { stdio: "ignore" });
|
|
45
46
|
} catch {}
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
async function bootstrap(plist) {
|
|
49
|
+
async function bootstrap(plist, execute = execFileSync) {
|
|
49
50
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
50
51
|
try {
|
|
51
|
-
|
|
52
|
+
execute("launchctl", ["bootstrap", launchDomain, plist], { stdio: "ignore" });
|
|
52
53
|
return;
|
|
53
54
|
} catch (error) {
|
|
54
55
|
if (attempt === 4) throw error;
|
|
@@ -168,27 +169,50 @@ function launchAgent(nodePath) {
|
|
|
168
169
|
export async function installService() {
|
|
169
170
|
await mkdir(dirname(launchAgentFile), { recursive: true });
|
|
170
171
|
const { legacyPlist, secretStatus } = await prepareInstallSecret();
|
|
171
|
-
const cursorPatches = cursorIsRunning()
|
|
172
|
-
? null
|
|
173
|
-
: await Promise.all(cursorModelMetadataFiles.map(async (file) => ({
|
|
174
|
-
file,
|
|
175
|
-
...await ensureCursorModelMetadataPatched({ file }),
|
|
176
|
-
})));
|
|
177
|
-
await copyPackage();
|
|
178
|
-
await installCliLink();
|
|
179
|
-
|
|
180
|
-
const nodePath = (await exists("/opt/homebrew/bin/node")) ? "/opt/homebrew/bin/node" : process.execPath;
|
|
181
|
-
await writeFile(launchAgentFile, launchAgent(nodePath), { mode: 0o644 });
|
|
182
172
|
bootout(serviceLabel);
|
|
183
173
|
bootout(legacyServiceLabel);
|
|
184
|
-
|
|
174
|
+
let cursorInstalled;
|
|
175
|
+
let cursorPatches;
|
|
176
|
+
try {
|
|
177
|
+
cursorInstalled = await exists(cursorAppPath);
|
|
178
|
+
cursorPatches = cursorIsRunning() || !cursorInstalled
|
|
179
|
+
? null
|
|
180
|
+
: await repairCursorMetadataPatch();
|
|
181
|
+
await copyPackage();
|
|
182
|
+
await installCliLink();
|
|
183
|
+
|
|
184
|
+
const nodePath = (await exists("/opt/homebrew/bin/node")) ? "/opt/homebrew/bin/node" : process.execPath;
|
|
185
|
+
await writeFile(launchAgentFile, launchAgent(nodePath), { mode: 0o644 });
|
|
186
|
+
await bootstrap(launchAgentFile);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (await exists(launchAgentFile)) await bootstrap(launchAgentFile).catch(() => {});
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
185
191
|
|
|
186
192
|
if (legacyPlist.startsWith(dirname(legacyLaunchAgentFile)) && await exists(legacyPlist)) {
|
|
187
193
|
const disabled = `${legacyPlist}.disabled-by-${serviceLabel}`;
|
|
188
194
|
await rm(disabled, { force: true });
|
|
189
195
|
await rename(legacyPlist, disabled);
|
|
190
196
|
}
|
|
191
|
-
return { installRoot, launchAgentFile, cliLinkFile, secretStatus, cursorPatches };
|
|
197
|
+
return { installRoot, launchAgentFile, cliLinkFile, secretStatus, cursorInstalled, cursorPatches };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function repairCursorMetadataPatch(options = {}) {
|
|
201
|
+
const running = options.cursorRunning ?? cursorIsRunning();
|
|
202
|
+
if (running) throw new Error("Quit Cursor before repairing its metadata patch");
|
|
203
|
+
const appPath = options.cursorAppPath || cursorAppPath;
|
|
204
|
+
const pathExists = options.exists || exists;
|
|
205
|
+
if (!await pathExists(appPath)) throw new Error("Cursor is not installed in /Applications");
|
|
206
|
+
|
|
207
|
+
const patchFile = options.ensureCursorModelMetadataPatched || ensureCursorModelMetadataPatched;
|
|
208
|
+
const files = options.files || cursorModelMetadataFiles;
|
|
209
|
+
const patches = await Promise.all(files.map(async (file) => ({
|
|
210
|
+
file,
|
|
211
|
+
...await patchFile({ file }),
|
|
212
|
+
})));
|
|
213
|
+
const finalizePatch = options.finalizeCursorAppPatch || finalizeCursorAppPatch;
|
|
214
|
+
finalizePatch({ appPath });
|
|
215
|
+
return patches;
|
|
192
216
|
}
|
|
193
217
|
|
|
194
218
|
export async function updateService(options = {}) {
|
|
@@ -209,6 +233,21 @@ export async function updateService(options = {}) {
|
|
|
209
233
|
}
|
|
210
234
|
}
|
|
211
235
|
|
|
236
|
+
export function stopService(options = {}) {
|
|
237
|
+
bootout(serviceLabel, options.execFileSync || execFileSync);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export async function startService(options = {}) {
|
|
241
|
+
const plist = options.launchAgentFile || launchAgentFile;
|
|
242
|
+
const pathExists = options.exists || exists;
|
|
243
|
+
if (!await pathExists(plist)) {
|
|
244
|
+
throw new Error("OpenCodex Cursor Bridge is not installed; run ocx-cursor install first");
|
|
245
|
+
}
|
|
246
|
+
const execute = options.execFileSync || execFileSync;
|
|
247
|
+
bootout(serviceLabel, execute);
|
|
248
|
+
await bootstrap(plist, execute);
|
|
249
|
+
}
|
|
250
|
+
|
|
212
251
|
export async function uninstallService() {
|
|
213
252
|
bootout(serviceLabel);
|
|
214
253
|
await rm(launchAgentFile, { force: true });
|
package/src/paths.mjs
CHANGED
|
@@ -14,7 +14,8 @@ export const stderrFile = join(installRoot, "service.error.log");
|
|
|
14
14
|
export const launchAgentFile = join(homedir(), "Library", "LaunchAgents", `${serviceLabel}.plist`);
|
|
15
15
|
export const legacyLaunchAgentFile = join(homedir(), "Library", "LaunchAgents", `${legacyServiceLabel}.plist`);
|
|
16
16
|
export const cursorDatabaseFile = join(homedir(), "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
|
|
17
|
-
export const
|
|
17
|
+
export const cursorAppPath = "/Applications/Cursor.app";
|
|
18
|
+
export const cursorAppOutDirectory = join(cursorAppPath, "Contents", "Resources", "app", "out");
|
|
18
19
|
export const cursorWorkbenchFile = join(cursorAppOutDirectory, "vs", "workbench", "workbench.desktop.main.js");
|
|
19
20
|
export const cursorGlassWorkbenchFile = join(cursorAppOutDirectory, "vs", "workbench", "workbench.glass.main.js");
|
|
20
21
|
export const cursorModelMetadataFiles = [cursorWorkbenchFile, cursorGlassWorkbenchFile];
|
package/src/service.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { buildActiveCatalog } from "./catalog.mjs";
|
|
4
|
-
import { startCursorModelMetadataPatchMonitor } from "./cursor-patch.mjs";
|
|
4
|
+
import { finalizeCursorAppPatch, startCursorModelMetadataPatchMonitor } from "./cursor-patch.mjs";
|
|
5
5
|
import {
|
|
6
6
|
cursorIsRunning,
|
|
7
|
+
cursorUpdateIsRunning,
|
|
7
8
|
readPendingCatalog,
|
|
8
9
|
} from "./cursor-state.mjs";
|
|
9
10
|
import { startGateway } from "./gateway.mjs";
|
|
@@ -27,6 +28,7 @@ export async function runService(options = {}) {
|
|
|
27
28
|
let refreshPromise = null;
|
|
28
29
|
let cursorSyncPromise = null;
|
|
29
30
|
let wasCursorRunning = cursorIsRunning();
|
|
31
|
+
let cursorPatchReady = false;
|
|
30
32
|
let stopped = false;
|
|
31
33
|
|
|
32
34
|
const refresh = async () => {
|
|
@@ -37,7 +39,10 @@ export async function runService(options = {}) {
|
|
|
37
39
|
if (JSON.stringify(next) === JSON.stringify(catalog)) return;
|
|
38
40
|
catalog = next;
|
|
39
41
|
await saveCatalogSnapshot(catalog);
|
|
40
|
-
const result = await applyOrQueueCatalog(catalog, {
|
|
42
|
+
const result = await applyOrQueueCatalog(catalog, {
|
|
43
|
+
createBackup: false,
|
|
44
|
+
defer: !cursorPatchReady,
|
|
45
|
+
});
|
|
41
46
|
process.stdout.write(`${result.status === "queued" ? "Queued" : "Synced"} ${result.modelCount} Cursor models\n`);
|
|
42
47
|
} catch (error) {
|
|
43
48
|
process.stderr.write(`Catalog refresh failed: ${error.message}\n`);
|
|
@@ -61,19 +66,33 @@ export async function runService(options = {}) {
|
|
|
61
66
|
const server = startGateway({ secret, getCatalog: () => catalog, host: options.host, port: options.port });
|
|
62
67
|
const patchMonitor = startCursorModelMetadataPatchMonitor({
|
|
63
68
|
intervalMs: options.cursorPatchIntervalMs,
|
|
64
|
-
|
|
69
|
+
stableMs: options.cursorPatchStableMs ?? 5_000,
|
|
70
|
+
shouldPatch: () => !cursorIsRunning() && !cursorUpdateIsRunning(),
|
|
65
71
|
onResult: (result) => {
|
|
66
72
|
if (result.status === "patched") process.stdout.write(`Patched Cursor app file ${result.file} (backup: ${result.backupPath})\n`);
|
|
67
73
|
},
|
|
68
|
-
|
|
74
|
+
onPatched: () => {
|
|
75
|
+
finalizeCursorAppPatch();
|
|
76
|
+
},
|
|
77
|
+
onReady: (results) => {
|
|
78
|
+
if (results.every(({ status }) => status === "already-patched")) finalizeCursorAppPatch();
|
|
79
|
+
cursorPatchReady = true;
|
|
80
|
+
wasCursorRunning = true;
|
|
81
|
+
},
|
|
82
|
+
onError: (error, file) => {
|
|
83
|
+
cursorPatchReady = false;
|
|
84
|
+
process.stderr.write(`Cursor patch failed for ${file}: ${error.message}\n`);
|
|
85
|
+
},
|
|
69
86
|
});
|
|
70
87
|
const refreshTimer = setInterval(refresh, options.refreshIntervalMs || 15_000);
|
|
71
88
|
const pendingTimer = setInterval(() => {
|
|
72
89
|
if (cursorSyncPromise) return;
|
|
73
90
|
if (cursorIsRunning()) {
|
|
74
91
|
wasCursorRunning = true;
|
|
92
|
+
cursorPatchReady = false;
|
|
75
93
|
return;
|
|
76
94
|
}
|
|
95
|
+
if (!cursorPatchReady) return;
|
|
77
96
|
cursorSyncPromise = (async () => {
|
|
78
97
|
const pending = await readPendingCatalog();
|
|
79
98
|
const next = pending || (wasCursorRunning ? catalog : null);
|
package/src/sync.mjs
CHANGED
|
@@ -25,16 +25,20 @@ export async function saveCatalogSnapshot(catalog) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export async function applyOrQueueCatalog(catalog, options = {}) {
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
const running = options.cursorRunning ?? cursorIsRunning();
|
|
29
|
+
const queueCatalog = options.writePendingCatalog || writePendingCatalog;
|
|
30
|
+
if (options.defer || running) {
|
|
31
|
+
await queueCatalog(catalog);
|
|
30
32
|
return {
|
|
31
33
|
status: "queued",
|
|
32
34
|
modelCount: catalog.length,
|
|
33
35
|
effortCount: catalog.filter(({ reasoningEfforts }) => reasoningEfforts.length > 0).length,
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
+
const syncDatabase = options.syncCursorDatabase || syncCursorDatabase;
|
|
39
|
+
const clearPending = options.clearPendingCatalog || clearPendingCatalog;
|
|
40
|
+
const result = await syncDatabase(catalog, options);
|
|
41
|
+
await clearPending();
|
|
38
42
|
return { status: "synced", ...result };
|
|
39
43
|
}
|
|
40
44
|
|