@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/repair.ts ADDED
@@ -0,0 +1,383 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join, normalize } from 'node:path';
3
+ import {
4
+ patchCursorAgentExec,
5
+ patchCursorWorkbenchAuthGate,
6
+ } from '@cursor-pool/patcher';
7
+ import { sha256File } from '@cursor-pool/patcher/hash';
8
+ import {
9
+ containsAnyCursorPoolMarkerText,
10
+ containsCursorPoolMarker,
11
+ } from '@cursor-pool/patcher/marker';
12
+ import { readRuntimeInfo, type RuntimeInfo } from '@cursor-pool/service';
13
+ import type { CompatibilityManifestEntry } from '@cursor-pool/shared/manifest';
14
+ import { DEFAULT_RUNTIME_FILE } from '@cursor-pool/shared/runtime';
15
+ import { DEFAULT_COMPAT_ENTRIES, loadCompatEntries, type CompatManifestFetchResponse } from './compat';
16
+ import {
17
+ confirmRealOperation,
18
+ formatRealOperationSummary,
19
+ type ConfirmRealOperationOptions,
20
+ } from './confirm';
21
+ import { findCursor, type FindCursorOptions } from './cursor';
22
+ import { detectEnvironment, type DetectEnvironmentOptions } from './environment';
23
+ import {
24
+ bundleExtension,
25
+ getExtensionState,
26
+ resolveExtensionInstallPath,
27
+ } from './extensionBundle';
28
+ import {
29
+ getLinkedExtensionState,
30
+ linkExtensionBundle,
31
+ linkedExtensionPathForDir,
32
+ } from './extensionLink';
33
+ import {
34
+ installIdForAppPath,
35
+ readInstallRecord,
36
+ writeInstallRecord,
37
+ type InstallRecord,
38
+ type InstallRecordOptions,
39
+ } from './installRecord';
40
+ import { startDetachedService, stopRuntimeService } from './serviceProcess';
41
+ import { resolveCursorTarget, type CursorTargetOptions } from './target';
42
+ import { adHocResignApp } from './adHocResign';
43
+ import {
44
+ formatCursorPatchSetState,
45
+ patchCursorSet,
46
+ readCursorPatchSetState,
47
+ } from './patchSet';
48
+
49
+ export type RepairOptions = FindCursorOptions &
50
+ CursorTargetOptions &
51
+ DetectEnvironmentOptions &
52
+ InstallRecordOptions &
53
+ Pick<ConfirmRealOperationOptions, 'yes' | 'isInteractive' | 'askConfirmation'> & {
54
+ runtimeFile?: string;
55
+ backupDir?: string;
56
+ extensionInstallPath?: string;
57
+ cursorExtensionsDir?: string;
58
+ serviceLogFile?: string;
59
+ compatEntries?: CompatibilityManifestEntry[];
60
+ apiBaseUrl?: string;
61
+ compatManifestUrl?: string;
62
+ fetchCompatManifest?: (url: string) => Promise<CompatManifestFetchResponse>;
63
+ stopServiceAfterRepair?: boolean;
64
+ startDetachedService?: typeof startDetachedService;
65
+ bundleExtension?: typeof bundleExtension;
66
+ linkExtensionBundle?: typeof linkExtensionBundle;
67
+ patchCursorAgentExec?: typeof patchCursorAgentExec;
68
+ patchCursorWorkbenchAuthGate?: typeof patchCursorWorkbenchAuthGate;
69
+ adHocResignApp?: typeof adHocResignApp;
70
+ };
71
+
72
+ const PACKAGE_VERSION = '0.5.6';
73
+
74
+ function normalizeAppPath(path: string) {
75
+ return normalize(path).replace(/\/+$/, '');
76
+ }
77
+
78
+ function assertRealRepairInstallRecord(
79
+ record: InstallRecord | null,
80
+ cursorAppPath: string,
81
+ ) {
82
+ if (!record) {
83
+ throw new Error('install-record: missing');
84
+ }
85
+
86
+ const normalizedCursorAppPath = normalizeAppPath(cursorAppPath);
87
+ const recordMatches =
88
+ record.mode === 'real' &&
89
+ normalizeAppPath(record.appPath) === normalizedCursorAppPath &&
90
+ record.installId === installIdForAppPath(cursorAppPath);
91
+
92
+ if (!recordMatches) {
93
+ throw new Error(`Install record does not match Cursor app: ${normalizedCursorAppPath}`);
94
+ }
95
+
96
+ return record;
97
+ }
98
+
99
+ function resolveRepairCompatEntry({
100
+ cursorVersion,
101
+ cursorCommit,
102
+ platform,
103
+ arch,
104
+ entries,
105
+ }: {
106
+ cursorVersion: string;
107
+ cursorCommit: string;
108
+ platform: string;
109
+ arch: string;
110
+ entries?: CompatibilityManifestEntry[];
111
+ }) {
112
+ const compatEntries = entries ?? DEFAULT_COMPAT_ENTRIES;
113
+ const entry = compatEntries.find(
114
+ (candidate) =>
115
+ candidate.platform === platform &&
116
+ candidate.arch === arch &&
117
+ candidate.cursorVersion === cursorVersion &&
118
+ candidate.cursorCommit === cursorCommit,
119
+ );
120
+
121
+ if (!entry) {
122
+ throw new Error(
123
+ `No compatibility entry for Cursor ${cursorVersion} ${cursorCommit} on ${platform}/${arch}`,
124
+ );
125
+ }
126
+
127
+ return entry;
128
+ }
129
+
130
+ async function isRuntimeHealthy(runtimeFile: string) {
131
+ const runtime = await readRuntimeInfo({ runtimeFile });
132
+ if (!runtime) {
133
+ return false;
134
+ }
135
+
136
+ try {
137
+ const response = await fetch(`http://${runtime.host}:${runtime.port}/health`);
138
+ const health = (await response.json()) as { ok?: unknown; runtimeId?: unknown };
139
+ return response.ok && health.ok === true && health.runtimeId === runtime.runtimeId;
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ function sameRuntime(a: RuntimeInfo | null | undefined, b: RuntimeInfo | null | undefined) {
146
+ return Boolean(
147
+ a &&
148
+ b &&
149
+ a.host === b.host &&
150
+ a.port === b.port &&
151
+ a.runtimeId === b.runtimeId,
152
+ );
153
+ }
154
+
155
+ async function stopRuntimeServiceStartedByRepair(
156
+ runtimeFile: string,
157
+ startedRuntimeByRepair: RuntimeInfo | undefined,
158
+ ) {
159
+ if (!startedRuntimeByRepair) {
160
+ return;
161
+ }
162
+
163
+ const currentRuntime = await readRuntimeInfo({ runtimeFile });
164
+ if (sameRuntime(currentRuntime, startedRuntimeByRepair)) {
165
+ await stopRuntimeService(runtimeFile);
166
+ }
167
+ }
168
+
169
+ async function readPatchState(targetPath: string) {
170
+ const content = await readFile(targetPath, 'utf8');
171
+ const markerPresent = containsCursorPoolMarker(content);
172
+ return {
173
+ markerPresent,
174
+ legacyMarkerPresent: !markerPresent && containsAnyCursorPoolMarkerText(content),
175
+ currentHash: await sha256File(targetPath),
176
+ };
177
+ }
178
+
179
+ async function maybeAdHocResign({
180
+ appPath,
181
+ platform,
182
+ requiresAdHocResign,
183
+ resign,
184
+ }: {
185
+ appPath: string;
186
+ platform: NodeJS.Platform;
187
+ requiresAdHocResign: boolean;
188
+ resign: typeof adHocResignApp;
189
+ }) {
190
+ if (platform !== 'darwin' || !requiresAdHocResign) {
191
+ return 'skipped' as const;
192
+ }
193
+
194
+ await resign(appPath);
195
+ return 'ad-hoc' as const;
196
+ }
197
+
198
+ export async function repair(options: RepairOptions = {}) {
199
+ const target = resolveCursorTarget(options);
200
+ if (target.mode !== 'real') {
201
+ throw new Error('repair is only supported for real Cursor installs in MVP-1');
202
+ }
203
+
204
+ const cursor = await findCursor({ ...options, appPath: target.appPath });
205
+ const environment = detectEnvironment(options);
206
+ const compatEntries = await loadCompatEntries({
207
+ compatEntries: options.compatEntries,
208
+ apiBaseUrl: options.apiBaseUrl,
209
+ compatManifestUrl: options.compatManifestUrl,
210
+ fetchManifest: options.fetchCompatManifest,
211
+ });
212
+ const installRecord = assertRealRepairInstallRecord(
213
+ await readInstallRecord({ installRecordFile: options.installRecordFile }),
214
+ cursor.appPath,
215
+ );
216
+ const compat = resolveRepairCompatEntry({
217
+ cursorVersion: cursor.version,
218
+ cursorCommit: cursor.commit,
219
+ platform: environment.platform,
220
+ arch: environment.arch,
221
+ entries: compatEntries,
222
+ });
223
+ if (compat.supportStatus === 'blocked' || compat.supportStatus === 'unknown') {
224
+ throw new Error(`compat: ${compat.supportStatus}`);
225
+ }
226
+ const targetPath = join(cursor.appPath, compat.targetRelativePath);
227
+ const runtimeFile = options.runtimeFile ?? installRecord.runtimeFile ?? DEFAULT_RUNTIME_FILE;
228
+ const backupDir = options.backupDir ?? installRecord.backupDir;
229
+ const extensionInstallPath =
230
+ options.extensionInstallPath ?? installRecord.extensionInstallPath;
231
+ const resolvedExtensionInstallPath = resolveExtensionInstallPath(extensionInstallPath);
232
+ const extensionLinkedPath = options.cursorExtensionsDir
233
+ ? linkedExtensionPathForDir(options.cursorExtensionsDir)
234
+ : installRecord.extensionLinkedPath;
235
+
236
+ const summary = {
237
+ operation: 'repair' as const,
238
+ appPath: cursor.appPath,
239
+ targetPath,
240
+ backupPath: backupDir,
241
+ runtimeFile,
242
+ extensionInstallPath: resolvedExtensionInstallPath,
243
+ extensionLinkedPath,
244
+ };
245
+ formatRealOperationSummary(summary);
246
+ await confirmRealOperation({
247
+ summary,
248
+ yes: options.yes,
249
+ isInteractive: options.isInteractive,
250
+ askConfirmation: options.askConfirmation,
251
+ });
252
+
253
+ const patchState = await readPatchState(targetPath);
254
+ if (!patchState.markerPresent) {
255
+ const matchesInstallOriginal =
256
+ installRecord.originalSha256 && patchState.currentHash === installRecord.originalSha256;
257
+ if (
258
+ !patchState.legacyMarkerPresent &&
259
+ patchState.currentHash !== compat.expectedSha256 &&
260
+ !matchesInstallOriginal
261
+ ) {
262
+ throw new Error('patch: changed');
263
+ }
264
+ }
265
+
266
+ const output = [
267
+ `Cursor ${cursor.version} (${cursor.commit})`,
268
+ 'mode: real',
269
+ `app: ${cursor.appPath}`,
270
+ `compat: ${compat.supportStatus}`,
271
+ ];
272
+
273
+ let serviceStatus = 'running';
274
+ let startedRuntimeByRepair: RuntimeInfo | undefined;
275
+ if (!(await isRuntimeHealthy(runtimeFile))) {
276
+ const startService = options.startDetachedService ?? startDetachedService;
277
+ startedRuntimeByRepair = await startService({
278
+ runtimeFile,
279
+ logFile: options.serviceLogFile ?? '~/.cursor-pool/logs/service.log',
280
+ });
281
+ serviceStatus = 'repaired';
282
+ }
283
+ output.push(`service: ${serviceStatus}`);
284
+
285
+ let extensionState = await getExtensionState(extensionInstallPath);
286
+ let extensionStatus = 'bundled';
287
+ if (extensionState === 'missing') {
288
+ const repairBundle = options.bundleExtension ?? bundleExtension;
289
+ const bundled = await repairBundle({ installPath: extensionInstallPath });
290
+ if (bundled.state === 'missing') {
291
+ throw new Error('Extension bundle is missing packages/extension/dist/extension.js');
292
+ }
293
+ extensionState = bundled.state;
294
+ extensionStatus = 'repaired';
295
+ }
296
+ output.push(`extension: ${extensionStatus}`);
297
+
298
+ if (options.cursorExtensionsDir) {
299
+ const linkedPath = linkedExtensionPathForDir(options.cursorExtensionsDir);
300
+ const linkedState = await getLinkedExtensionState(linkedPath);
301
+ if (linkedState === 'missing') {
302
+ const repairLink = options.linkExtensionBundle ?? linkExtensionBundle;
303
+ const linked = await repairLink({
304
+ sourceBundlePath: resolvedExtensionInstallPath,
305
+ cursorExtensionsDir: options.cursorExtensionsDir,
306
+ });
307
+ if (linked.state === 'missing') {
308
+ throw new Error('Linked extension source bundle is missing');
309
+ }
310
+ extensionState = linked.state;
311
+ output.push('linked-extension: repaired');
312
+ } else {
313
+ extensionState = linkedState;
314
+ output.push('linked-extension: linked');
315
+ }
316
+ } else if (extensionLinkedPath) {
317
+ const linkedState = await getLinkedExtensionState(extensionLinkedPath);
318
+ if (linkedState === 'linked') {
319
+ extensionState = linkedState;
320
+ output.push('linked-extension: linked');
321
+ }
322
+ }
323
+
324
+ const patchSetState = await readCursorPatchSetState(cursor.appPath, {
325
+ agentExecTargetRelativePath: compat.targetRelativePath,
326
+ });
327
+
328
+ if (patchSetState.allApplied) {
329
+ output.push('patch: applied');
330
+ output.push('resign: skipped');
331
+ } else {
332
+ const repairedPatchSet = await patchCursorSet(cursor.appPath, {
333
+ backupDir,
334
+ agentExecTargetRelativePath: compat.targetRelativePath,
335
+ patchCursorAgentExec: options.patchCursorAgentExec,
336
+ patchCursorWorkbenchAuthGate: options.patchCursorWorkbenchAuthGate,
337
+ });
338
+ output.push(
339
+ `patch: ${repairedPatchSet.before.appliedCount === 0 ? 'repaired' : `repaired ${formatCursorPatchSetState(repairedPatchSet.after)}`}`,
340
+ );
341
+ const resignState = await maybeAdHocResign({
342
+ appPath: cursor.appPath,
343
+ platform: environment.platform,
344
+ requiresAdHocResign: compat.requiresAdHocResign,
345
+ resign: options.adHocResignApp ?? adHocResignApp,
346
+ });
347
+ output.push(`resign: ${resignState}`);
348
+ }
349
+
350
+ const now = new Date().toISOString();
351
+ await writeInstallRecord(
352
+ {
353
+ ...installRecord,
354
+ installId: installIdForAppPath(cursor.appPath),
355
+ mode: 'real',
356
+ appPath: cursor.appPath,
357
+ cursorVersion: cursor.version,
358
+ cursorCommit: cursor.commit,
359
+ targetRelativePath: compat.targetRelativePath,
360
+ compatSupportStatus: compat.supportStatus,
361
+ runtimeFile,
362
+ backupDir,
363
+ extensionInstallPath: resolvedExtensionInstallPath,
364
+ extensionLinkedPath,
365
+ extensionState,
366
+ cliVersion: installRecord.cliVersion ?? PACKAGE_VERSION,
367
+ extensionVersion: installRecord.extensionVersion ?? PACKAGE_VERSION,
368
+ serviceVersion: installRecord.serviceVersion ?? PACKAGE_VERSION,
369
+ lastOperation: 'repair',
370
+ lastOperationStatus: 'ok',
371
+ updatedAt: now,
372
+ },
373
+ { installRecordFile: options.installRecordFile },
374
+ );
375
+
376
+ if (options.stopServiceAfterRepair) {
377
+ await stopRuntimeServiceStartedByRepair(runtimeFile, startedRuntimeByRepair);
378
+ }
379
+
380
+ output.push('install-record: recorded');
381
+ output.push('repair: ok');
382
+ return output.join('\n');
383
+ }
package/src/restore.ts ADDED
@@ -0,0 +1,153 @@
1
+ import { normalize } from 'node:path';
2
+ import { restoreCursorAgentExec, resolveCursorAgentExecPath } from '@cursor-pool/patcher';
3
+ import { DEFAULT_RUNTIME_FILE } from '@cursor-pool/shared/runtime';
4
+ import {
5
+ confirmRealOperation,
6
+ formatRealOperationSummary,
7
+ type ConfirmRealOperationOptions,
8
+ } from './confirm';
9
+ import { findCursor, type FindCursorOptions } from './cursor';
10
+ import {
11
+ installIdForAppPath,
12
+ readInstallRecord,
13
+ type InstallRecord,
14
+ type InstallRecordOptions,
15
+ } from './installRecord';
16
+ import { stopRuntimeService } from './serviceProcess';
17
+ import { resolveCursorTarget, type CursorTargetOptions } from './target';
18
+ import { assertDisposableCursorAppPath, readTrialRecord, type TrialRecordOptions } from './trial';
19
+
20
+ export type RestoreOptions = FindCursorOptions &
21
+ CursorTargetOptions &
22
+ TrialRecordOptions &
23
+ InstallRecordOptions & {
24
+ backupDir?: string;
25
+ runtimeFile?: string;
26
+ } & Pick<ConfirmRealOperationOptions, 'yes' | 'isInteractive' | 'askConfirmation'>;
27
+
28
+ async function stopRuntimeServiceIfRecorded(runtimeFile: string | undefined) {
29
+ if (!runtimeFile) {
30
+ return;
31
+ }
32
+
33
+ await stopRuntimeService(runtimeFile);
34
+ }
35
+
36
+ function realRuntimeFile(options: RestoreOptions, recordRuntimeFile: string | undefined) {
37
+ return recordRuntimeFile ?? options.runtimeFile ?? DEFAULT_RUNTIME_FILE;
38
+ }
39
+
40
+ function normalizeAppPath(path: string) {
41
+ return normalize(path).replace(/\/+$/, '');
42
+ }
43
+
44
+ function assertRealRestoreInstallRecord(
45
+ record: InstallRecord | null,
46
+ cursorAppPath: string,
47
+ ) {
48
+ if (!record) {
49
+ throw new Error('install-record: missing');
50
+ }
51
+
52
+ const normalizedCursorAppPath = normalizeAppPath(cursorAppPath);
53
+ const recordMatches =
54
+ record.mode === 'real' &&
55
+ normalizeAppPath(record.appPath) === normalizedCursorAppPath &&
56
+ record.installId === installIdForAppPath(cursorAppPath);
57
+
58
+ if (!recordMatches) {
59
+ throw new Error(`Install record does not match Cursor app: ${normalizedCursorAppPath}`);
60
+ }
61
+
62
+ return record;
63
+ }
64
+
65
+ function formatRestoreConfirmation({
66
+ cursorAppPath,
67
+ targetRelativePath,
68
+ backupDir,
69
+ runtimeFile,
70
+ }: {
71
+ cursorAppPath: string;
72
+ targetRelativePath?: string;
73
+ backupDir: string | undefined;
74
+ runtimeFile: string | undefined;
75
+ }) {
76
+ const summary = {
77
+ operation: 'restore' as const,
78
+ appPath: cursorAppPath,
79
+ targetPath: resolveCursorAgentExecPath(cursorAppPath, targetRelativePath),
80
+ backupPath: backupDir,
81
+ runtimeFile,
82
+ };
83
+ formatRealOperationSummary(summary);
84
+ return summary;
85
+ }
86
+
87
+ export async function restore(options: RestoreOptions = {}) {
88
+ const target = resolveCursorTarget(options);
89
+ const appPath =
90
+ target.mode === 'disposable' ? assertDisposableCursorAppPath(target.appPath) : target.appPath;
91
+ const cursor = await findCursor({ ...options, appPath });
92
+ const installRecord =
93
+ target.mode === 'real'
94
+ ? await readInstallRecord({ installRecordFile: options.installRecordFile })
95
+ : null;
96
+ const trialRecord =
97
+ target.mode === 'disposable'
98
+ ? await readTrialRecord(cursor.appPath, {
99
+ trialRecordDir: options.trialRecordDir,
100
+ })
101
+ : null;
102
+ const realInstallRecord =
103
+ target.mode === 'real'
104
+ ? assertRealRestoreInstallRecord(installRecord, cursor.appPath)
105
+ : null;
106
+
107
+ const backupDir =
108
+ target.mode === 'real'
109
+ ? realInstallRecord?.backupDir ?? options.backupDir
110
+ : options.backupDir ?? trialRecord?.backupDir;
111
+ const runtimeFile =
112
+ target.mode === 'real'
113
+ ? realRuntimeFile(options, realInstallRecord?.runtimeFile)
114
+ : options.runtimeFile ?? trialRecord?.runtimeFile;
115
+ const targetRelativePath =
116
+ target.mode === 'real'
117
+ ? realInstallRecord?.targetRelativePath
118
+ : trialRecord?.targetRelativePath;
119
+
120
+ if (target.requiresConfirmation) {
121
+ const summary = formatRestoreConfirmation({
122
+ cursorAppPath: cursor.appPath,
123
+ targetRelativePath,
124
+ backupDir,
125
+ runtimeFile,
126
+ });
127
+ await confirmRealOperation({
128
+ summary,
129
+ yes: options.yes,
130
+ isInteractive: options.isInteractive,
131
+ askConfirmation: options.askConfirmation,
132
+ });
133
+ }
134
+
135
+ const result = await restoreCursorAgentExec(cursor.appPath, {
136
+ backupDir,
137
+ targetRelativePath,
138
+ });
139
+ if (target.mode === 'real') {
140
+ await stopRuntimeServiceIfRecorded(runtimeFile);
141
+ }
142
+
143
+ return [
144
+ `Cursor ${cursor.version} (${cursor.commit})`,
145
+ `mode: ${target.mode}`,
146
+ `app: ${cursor.appPath}`,
147
+ 'restore: ok',
148
+ `patch: ${result.markerPresent ? 'applied' : 'missing'}`,
149
+ target.mode === 'real'
150
+ ? `install-record: ${realInstallRecord ? 'recorded' : 'missing'}`
151
+ : `trial: ${trialRecord ? 'recorded' : 'missing'}`,
152
+ ].join('\n');
153
+ }
@@ -0,0 +1,79 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { DEFAULT_RUNTIME_FILE } from '@cursor-pool/shared/runtime';
4
+ import { readClientConfig } from '@cursor-pool/shared/clientConfig';
5
+ import { readRuntimeInfo, resolveRuntimeFile } from '@cursor-pool/service';
6
+ import {
7
+ startDetachedService,
8
+ stopRuntimeService,
9
+ waitForRuntimeHealth,
10
+ } from './serviceProcess';
11
+
12
+ export type LocalServiceCommandOptions = {
13
+ runtimeFile?: string;
14
+ serviceLogFile?: string;
15
+ apiBaseUrl?: string;
16
+ configFile?: string;
17
+ startDetachedService?: typeof startDetachedService;
18
+ stopRuntimeService?: typeof stopRuntimeService;
19
+ };
20
+
21
+ const DEFAULT_SERVICE_LOG_FILE = '~/.cursor-pool/logs/service.log';
22
+
23
+ function resolveTildePath(path: string) {
24
+ if (path.startsWith('~/')) {
25
+ return join(homedir(), path.slice(2));
26
+ }
27
+ return path;
28
+ }
29
+
30
+ function formatRuntimeAddress(runtime: { host: string; port: number }) {
31
+ return `${runtime.host}:${runtime.port}`;
32
+ }
33
+
34
+ export async function startLocalServiceCommand(options: LocalServiceCommandOptions = {}) {
35
+ const runtimeFile = options.runtimeFile ?? DEFAULT_RUNTIME_FILE;
36
+ const logFile = options.serviceLogFile ?? DEFAULT_SERVICE_LOG_FILE;
37
+ const resolvedRuntimeFile = resolveRuntimeFile(runtimeFile);
38
+ const resolvedLogFile = resolveTildePath(logFile);
39
+ const existingRuntime = await readRuntimeInfo({ runtimeFile });
40
+ const clientConfig = await readClientConfig({ configFile: options.configFile });
41
+
42
+ if (existingRuntime && await waitForRuntimeHealth(runtimeFile, 250)) {
43
+ const runtime = await readRuntimeInfo({ runtimeFile });
44
+ if (!runtime) {
45
+ throw new Error('Runtime became unavailable after health check');
46
+ }
47
+ return [
48
+ `service: already-running ${formatRuntimeAddress(runtime)}`,
49
+ `runtime: ${resolvedRuntimeFile}`,
50
+ `log: ${resolvedLogFile}`,
51
+ ].join('\n');
52
+ }
53
+
54
+ const startDetached = options.startDetachedService ?? startDetachedService;
55
+ const runtime = await startDetached({
56
+ runtimeFile,
57
+ logFile,
58
+ configFile: options.configFile,
59
+ apiBaseUrl: options.apiBaseUrl ?? clientConfig.apiBaseUrl,
60
+ });
61
+
62
+ return [
63
+ `service: started ${formatRuntimeAddress(runtime)}`,
64
+ `runtime: ${resolvedRuntimeFile}`,
65
+ `log: ${resolvedLogFile}`,
66
+ ].join('\n');
67
+ }
68
+
69
+ export async function stopLocalServiceCommand(options: LocalServiceCommandOptions = {}) {
70
+ const runtimeFile = options.runtimeFile ?? DEFAULT_RUNTIME_FILE;
71
+ const resolvedRuntimeFile = resolveRuntimeFile(runtimeFile);
72
+ const stopRuntime = options.stopRuntimeService ?? stopRuntimeService;
73
+ const stopped = await stopRuntime(runtimeFile);
74
+
75
+ return [
76
+ `service: ${stopped ? 'stopped' : 'not-running'}`,
77
+ `runtime: ${resolvedRuntimeFile}`,
78
+ ].join('\n');
79
+ }