@cursorpool-dev/cli 0.5.6

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.
Files changed (105) hide show
  1. package/bin/cursor-pool.mjs +9 -0
  2. package/bin/cursor-pool.ts +169 -0
  3. package/node_modules/@cursor-pool/extension/dist/extension.js +2910 -0
  4. package/node_modules/@cursor-pool/extension/package.json +64 -0
  5. package/node_modules/@cursor-pool/extension/resources/cursor-pool.svg +6 -0
  6. package/node_modules/@cursor-pool/extension/src/api.ts +545 -0
  7. package/node_modules/@cursor-pool/extension/src/extension.ts +104 -0
  8. package/node_modules/@cursor-pool/extension/src/index.ts +1 -0
  9. package/node_modules/@cursor-pool/extension/src/panel.ts +569 -0
  10. package/node_modules/@cursor-pool/extension/src/runtime.ts +22 -0
  11. package/node_modules/@cursor-pool/extension/test/panel.test.ts +1785 -0
  12. package/node_modules/@cursor-pool/patcher/package.json +17 -0
  13. package/node_modules/@cursor-pool/patcher/src/alwaysLocalMarker.ts +86 -0
  14. package/node_modules/@cursor-pool/patcher/src/hash.ts +7 -0
  15. package/node_modules/@cursor-pool/patcher/src/index.ts +55 -0
  16. package/node_modules/@cursor-pool/patcher/src/marker.ts +159 -0
  17. package/node_modules/@cursor-pool/patcher/src/patchCursorAgentExec.ts +154 -0
  18. package/node_modules/@cursor-pool/patcher/src/patchCursorAlwaysLocal.ts +142 -0
  19. package/node_modules/@cursor-pool/patcher/src/patchCursorWorkbenchAuthGate.ts +140 -0
  20. package/node_modules/@cursor-pool/patcher/src/restoreCursorAgentExec.ts +52 -0
  21. package/node_modules/@cursor-pool/patcher/src/restoreCursorAlwaysLocal.ts +52 -0
  22. package/node_modules/@cursor-pool/patcher/src/restoreCursorWorkbenchAuthGate.ts +70 -0
  23. package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +243 -0
  24. package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +630 -0
  25. package/node_modules/@cursor-pool/patcher/test/patchCursorAlwaysLocal.test.ts +144 -0
  26. package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +770 -0
  27. package/node_modules/@cursor-pool/patcher/test/restoreCursorAgentExec.test.ts +139 -0
  28. package/node_modules/@cursor-pool/service/package.json +17 -0
  29. package/node_modules/@cursor-pool/service/src/canary.ts +61 -0
  30. package/node_modules/@cursor-pool/service/src/diagnostics.ts +385 -0
  31. package/node_modules/@cursor-pool/service/src/entry.ts +161 -0
  32. package/node_modules/@cursor-pool/service/src/health.ts +10 -0
  33. package/node_modules/@cursor-pool/service/src/index.ts +29 -0
  34. package/node_modules/@cursor-pool/service/src/metadata.ts +22 -0
  35. package/node_modules/@cursor-pool/service/src/platformSession.ts +1178 -0
  36. package/node_modules/@cursor-pool/service/src/requestCheck.ts +81 -0
  37. package/node_modules/@cursor-pool/service/src/requestGate.ts +100 -0
  38. package/node_modules/@cursor-pool/service/src/requestGateway.ts +441 -0
  39. package/node_modules/@cursor-pool/service/src/runtime.ts +48 -0
  40. package/node_modules/@cursor-pool/service/src/server.ts +939 -0
  41. package/node_modules/@cursor-pool/service/src/takeover.ts +111 -0
  42. package/node_modules/@cursor-pool/service/test/canary.test.ts +140 -0
  43. package/node_modules/@cursor-pool/service/test/diagnostics.test.ts +506 -0
  44. package/node_modules/@cursor-pool/service/test/metadata.test.ts +63 -0
  45. package/node_modules/@cursor-pool/service/test/platformSession.test.ts +2428 -0
  46. package/node_modules/@cursor-pool/service/test/requestCheck.test.ts +152 -0
  47. package/node_modules/@cursor-pool/service/test/requestGate.test.ts +207 -0
  48. package/node_modules/@cursor-pool/service/test/requestGateway.test.ts +466 -0
  49. package/node_modules/@cursor-pool/service/test/runtime.test.ts +47 -0
  50. package/node_modules/@cursor-pool/service/test/server.test.ts +2570 -0
  51. package/node_modules/@cursor-pool/shared/package.json +17 -0
  52. package/node_modules/@cursor-pool/shared/src/clientConfig.ts +49 -0
  53. package/node_modules/@cursor-pool/shared/src/index.ts +14 -0
  54. package/node_modules/@cursor-pool/shared/src/manifest.ts +36 -0
  55. package/node_modules/@cursor-pool/shared/src/metadata.ts +19 -0
  56. package/node_modules/@cursor-pool/shared/src/paths.ts +5 -0
  57. package/node_modules/@cursor-pool/shared/src/runtime.ts +3 -0
  58. package/node_modules/@cursor-pool/shared/test/index.test.ts +56 -0
  59. package/node_modules/@cursor-pool/shared/test/manifest.test.ts +65 -0
  60. package/node_modules/@cursor-pool/shared/test/metadata.test.ts +25 -0
  61. package/node_modules/@cursor-pool/shared/test/runtime.test.ts +8 -0
  62. package/package.json +28 -0
  63. package/src/adHocResign.ts +65 -0
  64. package/src/autostart.ts +240 -0
  65. package/src/compat.ts +282 -0
  66. package/src/confirm.ts +76 -0
  67. package/src/cursor.ts +94 -0
  68. package/src/diagnostics.ts +558 -0
  69. package/src/environment.ts +18 -0
  70. package/src/extensionBundle.ts +111 -0
  71. package/src/extensionLink.ts +168 -0
  72. package/src/index.ts +23 -0
  73. package/src/install.ts +614 -0
  74. package/src/installRecord.ts +105 -0
  75. package/src/launch.ts +182 -0
  76. package/src/patchSet.ts +182 -0
  77. package/src/platform.ts +132 -0
  78. package/src/repair.ts +383 -0
  79. package/src/restore.ts +153 -0
  80. package/src/serviceCommands.ts +79 -0
  81. package/src/serviceProcess.ts +188 -0
  82. package/src/status.ts +241 -0
  83. package/src/target.ts +37 -0
  84. package/src/trial.ts +133 -0
  85. package/src/uninstall.ts +213 -0
  86. package/test/autostart.test.ts +151 -0
  87. package/test/compat.test.ts +192 -0
  88. package/test/confirm.test.ts +114 -0
  89. package/test/cursor-pool-bin.test.ts +658 -0
  90. package/test/cursor.test.ts +20 -0
  91. package/test/diagnostics.test.ts +709 -0
  92. package/test/e2e-install.test.ts +773 -0
  93. package/test/extensionBundle.test.ts +161 -0
  94. package/test/extensionLink.test.ts +209 -0
  95. package/test/install.test.ts +862 -0
  96. package/test/installRecord.test.ts +107 -0
  97. package/test/launch.test.ts +138 -0
  98. package/test/platform.test.ts +226 -0
  99. package/test/repair.test.ts +575 -0
  100. package/test/restore.test.ts +211 -0
  101. package/test/serviceCommands.test.ts +135 -0
  102. package/test/serviceProcess.test.ts +280 -0
  103. package/test/status.test.ts +615 -0
  104. package/test/target.test.ts +49 -0
  105. package/test/trial.test.ts +146 -0
package/src/install.ts ADDED
@@ -0,0 +1,614 @@
1
+ import { cp, mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import {
5
+ patchCursorAgentExec,
6
+ patchCursorWorkbenchAuthGate,
7
+ restoreCursorAgentExec,
8
+ restoreCursorAlwaysLocal,
9
+ restoreCursorWorkbenchAuthGate,
10
+ } from '@cursor-pool/patcher';
11
+ import { sha256File } from '@cursor-pool/patcher/hash';
12
+ import { startServer } from '@cursor-pool/service';
13
+ import {
14
+ readRuntimeInfo,
15
+ resolveRuntimeFile,
16
+ writeRuntimeInfo,
17
+ type RuntimeInfo,
18
+ } from '@cursor-pool/service';
19
+ import type { CompatibilityManifestEntry } from '@cursor-pool/shared/manifest';
20
+ import { DEFAULT_RUNTIME_FILE } from '@cursor-pool/shared/runtime';
21
+ import { writeClientConfig } from '@cursor-pool/shared/clientConfig';
22
+ import { containsCursorPoolMarker } from '@cursor-pool/patcher/marker';
23
+ import { detectEnvironment, type DetectEnvironmentOptions } from './environment';
24
+ import { findCursor, type FindCursorOptions } from './cursor';
25
+ import { loadCompatEntries, resolveCompatEntry, type CompatManifestFetchResponse } from './compat';
26
+ import {
27
+ bundleExtension,
28
+ getExtensionState,
29
+ removeExtensionBundle,
30
+ resolveExtensionInstallPath,
31
+ } from './extensionBundle';
32
+ import {
33
+ getLinkedExtensionState,
34
+ linkExtensionBundle,
35
+ linkedExtensionPathForDir,
36
+ refreshCursorExtensionsIndex,
37
+ removeLinkedExtensionBundle,
38
+ restoreLinkedExtensionSnapshot,
39
+ snapshotLinkedExtensionBundle,
40
+ type LinkedExtensionSnapshot,
41
+ } from './extensionLink';
42
+ import { startDetachedService, stopRuntimeService } from './serviceProcess';
43
+ import { installUserAutostart } from './autostart';
44
+ import {
45
+ assertDisposableCursorAppPath,
46
+ deleteTrialRecord,
47
+ readTrialRecord,
48
+ trialIdForAppPath,
49
+ writeTrialRecord,
50
+ type TrialRecord,
51
+ type TrialRecordOptions,
52
+ } from './trial';
53
+ import { resolveCursorTarget, type CursorTargetOptions } from './target';
54
+ import {
55
+ confirmRealOperation,
56
+ formatRealOperationSummary,
57
+ type ConfirmRealOperationOptions,
58
+ } from './confirm';
59
+ import {
60
+ deleteInstallRecord,
61
+ installIdForAppPath,
62
+ readInstallRecord,
63
+ writeInstallRecord,
64
+ type InstallRecord,
65
+ type InstallRecordOptions,
66
+ } from './installRecord';
67
+ import { adHocResignApp } from './adHocResign';
68
+ import {
69
+ formatCursorPatchSetState,
70
+ patchCursorSet,
71
+ readCursorPatchSetState,
72
+ restoreCursorSet,
73
+ } from './patchSet';
74
+
75
+ export type InstallOptions = FindCursorOptions &
76
+ DetectEnvironmentOptions &
77
+ CursorTargetOptions &
78
+ TrialRecordOptions &
79
+ InstallRecordOptions &
80
+ Pick<ConfirmRealOperationOptions, 'yes' | 'isInteractive' | 'askConfirmation'> & {
81
+ runtimeFile?: string;
82
+ backupDir?: string;
83
+ extensionInstallPath?: string;
84
+ cursorExtensionsDir?: string;
85
+ serviceLogFile?: string;
86
+ clientConfigFile?: string;
87
+ startDetachedService?: typeof startDetachedService;
88
+ installUserAutostart?: typeof installUserAutostart;
89
+ bundleExtension?: typeof bundleExtension;
90
+ linkExtensionBundle?: typeof linkExtensionBundle;
91
+ compatEntries?: CompatibilityManifestEntry[];
92
+ apiBaseUrl?: string;
93
+ compatManifestUrl?: string;
94
+ fetchCompatManifest?: (url: string) => Promise<CompatManifestFetchResponse>;
95
+ stopServiceAfterInstall?: boolean;
96
+ patchCursorAgentExec?: typeof patchCursorAgentExec;
97
+ patchCursorWorkbenchAuthGate?: typeof patchCursorWorkbenchAuthGate;
98
+ restoreCursorAgentExec?: typeof restoreCursorAgentExec;
99
+ restoreCursorAlwaysLocal?: typeof restoreCursorAlwaysLocal;
100
+ restoreCursorWorkbenchAuthGate?: typeof restoreCursorWorkbenchAuthGate;
101
+ adHocResignApp?: typeof adHocResignApp;
102
+ fetchHealth?: (url: string) => Promise<{ ok: boolean; healthy: boolean }>;
103
+ };
104
+
105
+ const REAL_CURSOR_EXTENSIONS_DIR = '~/.cursor/extensions';
106
+ const PACKAGE_VERSION = '0.5.6';
107
+
108
+ async function maybeAdHocResign({
109
+ appPath,
110
+ platform,
111
+ requiresAdHocResign,
112
+ resign,
113
+ }: {
114
+ appPath: string;
115
+ platform: NodeJS.Platform;
116
+ requiresAdHocResign: boolean;
117
+ resign: typeof adHocResignApp;
118
+ }) {
119
+ if (platform !== 'darwin' || !requiresAdHocResign) {
120
+ return 'skipped' as const;
121
+ }
122
+
123
+ await resign(appPath);
124
+ return 'ad-hoc' as const;
125
+ }
126
+
127
+ async function assertExpectedHash(targetPath: string, expectedSha256: string) {
128
+ const currentHash = await sha256File(targetPath);
129
+ if (currentHash !== expectedSha256) {
130
+ const content = await readFile(targetPath, 'utf8');
131
+ if (containsCursorPoolMarker(content)) {
132
+ return;
133
+ }
134
+ throw new Error(`Patch target hash mismatch: expected ${expectedSha256}, got ${currentHash}`);
135
+ }
136
+ }
137
+
138
+ async function startInstallService(options: InstallOptions) {
139
+ if (options.stopServiceAfterInstall) {
140
+ const service = await startServer({ runtimeFile: options.runtimeFile });
141
+ return {
142
+ host: service.host,
143
+ port: service.port,
144
+ runtimeId: service.runtimeId,
145
+ stop: service.stop,
146
+ };
147
+ }
148
+
149
+ const startDetached = options.startDetachedService ?? startDetachedService;
150
+ const runtime = await startDetached({
151
+ runtimeFile: options.runtimeFile ?? DEFAULT_RUNTIME_FILE,
152
+ logFile: options.serviceLogFile ?? '~/.cursor-pool/logs/service.log',
153
+ configFile: options.clientConfigFile,
154
+ apiBaseUrl: options.apiBaseUrl,
155
+ });
156
+ return {
157
+ ...runtime,
158
+ stop: () => stopRuntimeService(options.runtimeFile ?? DEFAULT_RUNTIME_FILE),
159
+ };
160
+ }
161
+
162
+ async function writeInstallClientConfig(options: InstallOptions) {
163
+ if (!options.apiBaseUrl) {
164
+ return;
165
+ }
166
+ await writeClientConfig(
167
+ { apiBaseUrl: options.apiBaseUrl },
168
+ { configFile: options.clientConfigFile },
169
+ );
170
+ }
171
+
172
+ async function installAutostart(options: InstallOptions, targetMode: 'real' | 'disposable') {
173
+ if (options.stopServiceAfterInstall && !options.installUserAutostart) {
174
+ return { state: 'skipped' as const };
175
+ }
176
+ if (targetMode !== 'real' && !options.installUserAutostart) {
177
+ return { state: 'skipped' as const };
178
+ }
179
+ const installAutostartFn = options.installUserAutostart ?? installUserAutostart;
180
+ return installAutostartFn({
181
+ runtimeFile: options.runtimeFile ?? DEFAULT_RUNTIME_FILE,
182
+ serviceLogFile: options.serviceLogFile,
183
+ configFile: options.clientConfigFile,
184
+ });
185
+ }
186
+
187
+ function sameRuntime(a: RuntimeInfo | null | undefined, b: RuntimeInfo | null | undefined) {
188
+ return Boolean(
189
+ a &&
190
+ b &&
191
+ a.host === b.host &&
192
+ a.port === b.port &&
193
+ a.runtimeId === b.runtimeId,
194
+ );
195
+ }
196
+
197
+ async function restoreRuntimeState(
198
+ runtimeFile: string | undefined,
199
+ existingRuntime: RuntimeInfo | null,
200
+ service: Awaited<ReturnType<typeof startInstallService>> | undefined,
201
+ ) {
202
+ const resolvedRuntimeFile = resolveRuntimeFile(runtimeFile);
203
+ const currentRuntime = await readRuntimeInfo({ runtimeFile: resolvedRuntimeFile });
204
+ if (service && !sameRuntime(currentRuntime, service)) {
205
+ return;
206
+ }
207
+
208
+ if (existingRuntime) {
209
+ await writeRuntimeInfo(existingRuntime, { runtimeFile: resolvedRuntimeFile });
210
+ return;
211
+ }
212
+
213
+ await rm(resolvedRuntimeFile, { force: true });
214
+ }
215
+
216
+ async function snapshotExtensionBundle(installPath: string | undefined) {
217
+ if ((await getExtensionState(installPath)) === 'missing') {
218
+ return undefined;
219
+ }
220
+
221
+ const resolvedInstallPath = resolveExtensionInstallPath(installPath);
222
+ const snapshotRoot = await mkdtemp(join(tmpdir(), 'cursor-pool-extension-snapshot-'));
223
+ const snapshotPath = join(snapshotRoot, 'cursor-pool-status');
224
+ await cp(resolvedInstallPath, snapshotPath, { recursive: true });
225
+ return { root: snapshotRoot, path: snapshotPath };
226
+ }
227
+
228
+ async function restoreExtensionBundle(
229
+ installPath: string,
230
+ snapshot: Awaited<ReturnType<typeof snapshotExtensionBundle>>,
231
+ ) {
232
+ if (!snapshot) {
233
+ return;
234
+ }
235
+
236
+ await rm(installPath, { recursive: true, force: true });
237
+ await mkdir(dirname(installPath), { recursive: true });
238
+ await cp(snapshot.path, installPath, { recursive: true });
239
+ }
240
+
241
+ function assertLinkedPathInsideCursorExtensionsDir(linkedPath: string, cursorExtensionsDir: string) {
242
+ const expectedLinkedPath = resolve(linkedExtensionPathForDir(cursorExtensionsDir));
243
+ const resolvedLinkedPath = resolve(linkedPath);
244
+ if (resolvedLinkedPath !== expectedLinkedPath) {
245
+ throw new Error(`Unsafe linked extension path: ${linkedPath}`);
246
+ }
247
+ }
248
+
249
+ async function removeInstallLinkedExtensionBundle(
250
+ linkedPath: string,
251
+ cursorExtensionsDir: string | undefined,
252
+ ) {
253
+ if (!cursorExtensionsDir) {
254
+ await removeLinkedExtensionBundle(linkedPath);
255
+ return;
256
+ }
257
+
258
+ assertLinkedPathInsideCursorExtensionsDir(linkedPath, cursorExtensionsDir);
259
+ await rm(linkedPath, { recursive: true, force: true });
260
+ }
261
+
262
+ async function restoreInstallLinkedExtensionSnapshot(
263
+ linkedPath: string,
264
+ snapshot: LinkedExtensionSnapshot | null | undefined,
265
+ cursorExtensionsDir: string | undefined,
266
+ ) {
267
+ if (!snapshot) {
268
+ return;
269
+ }
270
+
271
+ if (!cursorExtensionsDir) {
272
+ await restoreLinkedExtensionSnapshot(linkedPath, snapshot);
273
+ return;
274
+ }
275
+
276
+ assertLinkedPathInsideCursorExtensionsDir(linkedPath, cursorExtensionsDir);
277
+ await rm(linkedPath, { recursive: true, force: true });
278
+ await mkdir(dirname(linkedPath), { recursive: true });
279
+ await cp(snapshot.root, linkedPath, { recursive: true });
280
+ }
281
+
282
+ async function linkInstallExtensionBundle({
283
+ sourceBundlePath,
284
+ cursorExtensionsDir,
285
+ }: {
286
+ sourceBundlePath: string;
287
+ cursorExtensionsDir: string;
288
+ }): ReturnType<typeof linkExtensionBundle> {
289
+ const linkedPath = linkedExtensionPathForDir(cursorExtensionsDir);
290
+ if ((await getLinkedExtensionState(sourceBundlePath)) === 'missing') {
291
+ return { state: 'missing', linkedPath };
292
+ }
293
+
294
+ await removeInstallLinkedExtensionBundle(linkedPath, cursorExtensionsDir);
295
+ await mkdir(dirname(linkedPath), { recursive: true });
296
+ await cp(sourceBundlePath, linkedPath, { recursive: true });
297
+ await refreshCursorExtensionsIndex(cursorExtensionsDir, linkedPath);
298
+ return { state: 'linked', linkedPath };
299
+ }
300
+
301
+ export async function install(options: InstallOptions = {}) {
302
+ const environment = detectEnvironment(options);
303
+ const target = resolveCursorTarget({ ...options, platform: environment.platform });
304
+ const appPath =
305
+ target.mode === 'disposable'
306
+ ? assertDisposableCursorAppPath(target.appPath, { platform: environment.platform })
307
+ : target.appPath;
308
+ const cursorExtensionsDir =
309
+ options.cursorExtensionsDir ?? (target.mode === 'real' ? REAL_CURSOR_EXTENSIONS_DIR : undefined);
310
+ const cursor = await findCursor({ ...options, appPath });
311
+ const compatEntries = await loadCompatEntries({
312
+ compatEntries: options.compatEntries,
313
+ apiBaseUrl: options.apiBaseUrl,
314
+ compatManifestUrl: options.compatManifestUrl,
315
+ fetchManifest: options.fetchCompatManifest,
316
+ });
317
+ const compat = resolveCompatEntry(cursor, environment, { entries: compatEntries });
318
+ const targetPath = join(cursor.appPath, compat.targetRelativePath);
319
+ const originalSha256 = await sha256File(targetPath);
320
+ await assertExpectedHash(targetPath, compat.expectedSha256);
321
+ const patchSetBeforeInstall = await readCursorPatchSetState(cursor.appPath, {
322
+ agentExecTargetRelativePath: compat.targetRelativePath,
323
+ platform: environment.platform,
324
+ });
325
+ const wasPatchedBeforeInstall = patchSetBeforeInstall.allApplied;
326
+ const existingTrialRecord = await readTrialRecord(cursor.appPath, {
327
+ trialRecordDir: options.trialRecordDir,
328
+ });
329
+ const existingInstallRecord = await readInstallRecord({
330
+ installRecordFile: options.installRecordFile,
331
+ });
332
+ const existingRuntime = await readRuntimeInfo({ runtimeFile: options.runtimeFile });
333
+ const existingExtensionState = await getExtensionState(options.extensionInstallPath);
334
+
335
+ let service: Awaited<ReturnType<typeof startInstallService>> | undefined;
336
+ let extension: Awaited<ReturnType<typeof bundleExtension>> | undefined;
337
+ let extensionInstallPath: string | undefined;
338
+ let extensionLinkedPath: string | undefined;
339
+ let previousTrialRecord: TrialRecord | null = existingTrialRecord;
340
+ let extensionSnapshot: Awaited<ReturnType<typeof snapshotExtensionBundle>> | undefined;
341
+ let linkedExtensionSnapshot: LinkedExtensionSnapshot | null | undefined;
342
+ let extensionCreatedByInstall = false;
343
+ let linkedExtensionCreatedByInstall = false;
344
+ let patchApplied = false;
345
+ let wroteTrialRecord = false;
346
+ let previousInstallRecord: InstallRecord | null = existingInstallRecord;
347
+ let wroteInstallRecord = false;
348
+ let resignState: 'ad-hoc' | 'skipped' = 'skipped';
349
+ let patchState = 'skipped';
350
+ let autostartState = 'skipped';
351
+
352
+ try {
353
+ if (target.requiresConfirmation) {
354
+ const summary = {
355
+ operation: 'install' as const,
356
+ appPath: cursor.appPath,
357
+ targetPath,
358
+ backupPath: options.backupDir ?? '~/.cursor-pool/backups',
359
+ runtimeFile: options.runtimeFile ?? DEFAULT_RUNTIME_FILE,
360
+ extensionInstallPath: resolveExtensionInstallPath(options.extensionInstallPath),
361
+ extensionLinkedPath: cursorExtensionsDir
362
+ ? linkedExtensionPathForDir(cursorExtensionsDir)
363
+ : undefined,
364
+ };
365
+ formatRealOperationSummary(summary);
366
+ await confirmRealOperation({
367
+ summary,
368
+ yes: options.yes,
369
+ isInteractive: options.isInteractive,
370
+ askConfirmation: options.askConfirmation,
371
+ });
372
+ }
373
+ previousTrialRecord = await readTrialRecord(cursor.appPath, {
374
+ trialRecordDir: options.trialRecordDir,
375
+ });
376
+ previousInstallRecord = await readInstallRecord({
377
+ installRecordFile: options.installRecordFile,
378
+ });
379
+ extensionInstallPath = resolveExtensionInstallPath(options.extensionInstallPath);
380
+ extensionSnapshot = await snapshotExtensionBundle(options.extensionInstallPath);
381
+ const bundle = options.bundleExtension ?? bundleExtension;
382
+ extension = await bundle({
383
+ installPath: options.extensionInstallPath,
384
+ });
385
+ if (extension.state === 'missing') {
386
+ throw new Error('Extension bundle is missing packages/extension/dist/extension.js');
387
+ }
388
+ extensionCreatedByInstall = existingExtensionState === 'missing';
389
+ if (cursorExtensionsDir) {
390
+ extensionLinkedPath = linkedExtensionPathForDir(cursorExtensionsDir);
391
+ linkedExtensionSnapshot = await snapshotLinkedExtensionBundle(extensionLinkedPath);
392
+ const existingLinkedState = await getLinkedExtensionState(extensionLinkedPath);
393
+ linkedExtensionCreatedByInstall = existingLinkedState === 'missing';
394
+ const linkBundle = options.linkExtensionBundle ?? linkInstallExtensionBundle;
395
+ const linked = await linkBundle({
396
+ sourceBundlePath: extension.installPath,
397
+ cursorExtensionsDir,
398
+ });
399
+ if (linked.state === 'missing') {
400
+ throw new Error('Linked extension source bundle is missing');
401
+ }
402
+ extensionLinkedPath = linked.linkedPath;
403
+ extension.state = linked.state;
404
+ }
405
+
406
+ await writeInstallClientConfig(options);
407
+ service = await startInstallService(options);
408
+ autostartState = (await installAutostart(options, target.mode)).state;
409
+ const patchSet = await patchCursorSet(cursor.appPath, {
410
+ backupDir: options.backupDir,
411
+ platform: environment.platform,
412
+ agentExecTargetRelativePath: compat.targetRelativePath,
413
+ patchCursorAgentExec: options.patchCursorAgentExec,
414
+ patchCursorWorkbenchAuthGate: options.patchCursorWorkbenchAuthGate,
415
+ });
416
+ patchApplied = !patchSet.before.allApplied;
417
+ patchState = patchSet.before.allApplied ? 'already applied' : formatCursorPatchSetState(patchSet.after);
418
+ resignState = await maybeAdHocResign({
419
+ appPath: cursor.appPath,
420
+ platform: environment.platform,
421
+ requiresAdHocResign: compat.requiresAdHocResign,
422
+ resign: options.adHocResignApp ?? adHocResignApp,
423
+ });
424
+
425
+ const health = await (options.fetchHealth ?? fetchHealth)(
426
+ `http://${service.host}:${service.port}/health`,
427
+ );
428
+ if (!health.ok || !health.healthy) {
429
+ throw new Error('Service health check failed');
430
+ }
431
+
432
+ const now = new Date().toISOString();
433
+ const installState = {
434
+ appPath: cursor.appPath,
435
+ cursorVersion: cursor.version,
436
+ cursorCommit: cursor.commit,
437
+ targetRelativePath: compat.targetRelativePath,
438
+ originalSha256,
439
+ compatSupportStatus: compat.supportStatus,
440
+ runtimeFile: options.runtimeFile ?? DEFAULT_RUNTIME_FILE,
441
+ backupDir: options.backupDir ?? '~/.cursor-pool/backups',
442
+ extensionInstallPath: extension.installPath,
443
+ extensionLinkedPath,
444
+ extensionState: extension.state,
445
+ };
446
+ if (target.mode === 'real') {
447
+ await writeInstallRecord(
448
+ {
449
+ installId: installIdForAppPath(cursor.appPath),
450
+ mode: 'real',
451
+ ...installState,
452
+ cliVersion: PACKAGE_VERSION,
453
+ extensionVersion: PACKAGE_VERSION,
454
+ serviceVersion: PACKAGE_VERSION,
455
+ lastOperation: 'install',
456
+ lastOperationStatus: 'ok',
457
+ createdAt: previousInstallRecord?.createdAt ?? now,
458
+ updatedAt: now,
459
+ },
460
+ { installRecordFile: options.installRecordFile },
461
+ );
462
+ wroteInstallRecord = true;
463
+ } else {
464
+ await writeTrialRecord(
465
+ {
466
+ trialId: trialIdForAppPath(cursor.appPath),
467
+ ...installState,
468
+ createdAt: now,
469
+ updatedAt: now,
470
+ },
471
+ { trialRecordDir: options.trialRecordDir },
472
+ );
473
+ wroteTrialRecord = true;
474
+ }
475
+
476
+ return [
477
+ `Cursor ${cursor.version} (${cursor.commit})`,
478
+ `mode: ${target.mode}`,
479
+ `app: ${cursor.appPath}`,
480
+ `compat: ${compat.supportStatus}`,
481
+ `extension: ${extension.state}`,
482
+ `service: running ${service.host}:${service.port}`,
483
+ `autostart: ${autostartState}`,
484
+ `patch: ${patchState}`,
485
+ `resign: ${resignState}`,
486
+ 'health: ok',
487
+ target.mode === 'real' ? 'install-record: recorded' : 'trial: recorded',
488
+ ].join('\n');
489
+ } catch (error) {
490
+ const rollbackErrors: unknown[] = [];
491
+ let shouldRestorePatch = patchApplied;
492
+ try {
493
+ shouldRestorePatch =
494
+ shouldRestorePatch ||
495
+ (!wasPatchedBeforeInstall &&
496
+ (await readCursorPatchSetState(cursor.appPath, {
497
+ agentExecTargetRelativePath: compat.targetRelativePath,
498
+ platform: environment.platform,
499
+ })).appliedCount > 0);
500
+ } catch (rollbackError) {
501
+ rollbackErrors.push(rollbackError);
502
+ }
503
+ if (shouldRestorePatch) {
504
+ try {
505
+ await restoreCursorSet(cursor.appPath, {
506
+ backupDir: options.backupDir,
507
+ platform: environment.platform,
508
+ restoreCursorAgentExec: options.restoreCursorAgentExec,
509
+ restoreCursorAlwaysLocal: options.restoreCursorAlwaysLocal,
510
+ restoreCursorWorkbenchAuthGate: options.restoreCursorWorkbenchAuthGate,
511
+ });
512
+ } catch (rollbackError) {
513
+ rollbackErrors.push(rollbackError);
514
+ }
515
+ }
516
+ try {
517
+ await service?.stop();
518
+ } catch (rollbackError) {
519
+ rollbackErrors.push(rollbackError);
520
+ }
521
+ try {
522
+ await restoreRuntimeState(options.runtimeFile, existingRuntime, service);
523
+ } catch (rollbackError) {
524
+ rollbackErrors.push(rollbackError);
525
+ }
526
+ if (extensionLinkedPath && linkedExtensionSnapshot) {
527
+ try {
528
+ await restoreInstallLinkedExtensionSnapshot(
529
+ extensionLinkedPath,
530
+ linkedExtensionSnapshot,
531
+ cursorExtensionsDir,
532
+ );
533
+ } catch (rollbackError) {
534
+ rollbackErrors.push(rollbackError);
535
+ }
536
+ } else if (linkedExtensionCreatedByInstall && extensionLinkedPath) {
537
+ try {
538
+ await removeInstallLinkedExtensionBundle(extensionLinkedPath, cursorExtensionsDir);
539
+ } catch (rollbackError) {
540
+ rollbackErrors.push(rollbackError);
541
+ }
542
+ }
543
+ if (extensionCreatedByInstall && extension) {
544
+ try {
545
+ await removeExtensionBundle(extension.installPath);
546
+ } catch (rollbackError) {
547
+ rollbackErrors.push(rollbackError);
548
+ }
549
+ } else if (extensionSnapshot) {
550
+ try {
551
+ const restoreInstallPath =
552
+ extension?.installPath ??
553
+ extensionInstallPath ??
554
+ resolveExtensionInstallPath(options.extensionInstallPath);
555
+ await restoreExtensionBundle(restoreInstallPath, extensionSnapshot);
556
+ } catch (rollbackError) {
557
+ rollbackErrors.push(rollbackError);
558
+ }
559
+ }
560
+ if (!previousTrialRecord && (extension || wroteTrialRecord)) {
561
+ try {
562
+ await deleteTrialRecord(cursor.appPath, { trialRecordDir: options.trialRecordDir });
563
+ } catch (rollbackError) {
564
+ rollbackErrors.push(rollbackError);
565
+ }
566
+ } else if (previousTrialRecord && wroteTrialRecord) {
567
+ try {
568
+ await writeTrialRecord(previousTrialRecord, { trialRecordDir: options.trialRecordDir });
569
+ } catch (rollbackError) {
570
+ rollbackErrors.push(rollbackError);
571
+ }
572
+ }
573
+ if (!previousInstallRecord && wroteInstallRecord) {
574
+ try {
575
+ await deleteInstallRecord({ installRecordFile: options.installRecordFile });
576
+ } catch (rollbackError) {
577
+ rollbackErrors.push(rollbackError);
578
+ }
579
+ } else if (previousInstallRecord && wroteInstallRecord) {
580
+ try {
581
+ await writeInstallRecord(previousInstallRecord, {
582
+ installRecordFile: options.installRecordFile,
583
+ });
584
+ } catch (rollbackError) {
585
+ rollbackErrors.push(rollbackError);
586
+ }
587
+ }
588
+ if (rollbackErrors.length > 0 && error instanceof Error) {
589
+ throw new Error(error.message, {
590
+ cause: new AggregateError(rollbackErrors, 'Install rollback cleanup failed'),
591
+ });
592
+ }
593
+ throw error;
594
+ } finally {
595
+ if (extensionSnapshot) {
596
+ await rm(extensionSnapshot.root, { recursive: true, force: true });
597
+ }
598
+ if (linkedExtensionSnapshot) {
599
+ await rm(linkedExtensionSnapshot.root, { recursive: true, force: true });
600
+ }
601
+ if (options.stopServiceAfterInstall) {
602
+ await service?.stop();
603
+ }
604
+ }
605
+ }
606
+
607
+ async function fetchHealth(url: string) {
608
+ const response = await fetch(url);
609
+ const health = (await response.json()) as { ok?: unknown };
610
+ return {
611
+ ok: response.ok,
612
+ healthy: health.ok === true,
613
+ };
614
+ }