@cursorpool-dev/cli 0.5.6 → 0.5.8

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 (108) hide show
  1. package/node_modules/@cursor-pool/extension/dist/extension.js +116 -46
  2. package/node_modules/@cursor-pool/extension/package.json +3 -3
  3. package/node_modules/@cursor-pool/extension/src/api.ts +17 -2
  4. package/node_modules/@cursor-pool/extension/src/panel.ts +26 -3
  5. package/node_modules/@cursor-pool/extension/test/panel.test.ts +34 -1
  6. package/node_modules/@cursor-pool/patcher/package.json +2 -2
  7. package/node_modules/@cursor-pool/patcher/src/marker.ts +5 -1
  8. package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +58 -7
  9. package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +20 -0
  10. package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +193 -2
  11. package/node_modules/@cursor-pool/service/package.json +2 -2
  12. package/node_modules/@cursor-pool/service/src/platformSession.ts +30 -7
  13. package/node_modules/@cursor-pool/service/src/server.ts +1 -0
  14. package/node_modules/@cursor-pool/service/test/platformSession.test.ts +5 -4
  15. package/node_modules/@cursor-pool/service/test/server.test.ts +130 -0
  16. package/node_modules/@cursor-pool/shared/package.json +1 -1
  17. package/node_modules/@cursor-pool/shared/src/manifest.ts +35 -0
  18. package/node_modules/@cursor-pool/shared/test/manifest.test.ts +43 -9
  19. package/node_modules/@cursor-pool/takeover-plans/package.json +12 -0
  20. package/node_modules/@cursor-pool/takeover-plans/src/index.ts +22 -0
  21. package/node_modules/@cursor-pool/takeover-plans/src/plans.ts +37 -0
  22. package/node_modules/@cursor-pool/takeover-plans/src/types.ts +9 -0
  23. package/node_modules/@cursor-pool/takeover-plans/test/registry.test.ts +23 -0
  24. package/node_modules/@esbuild/linux-x64/README.md +3 -0
  25. package/node_modules/@esbuild/linux-x64/bin/esbuild +0 -0
  26. package/node_modules/@esbuild/linux-x64/package.json +20 -0
  27. package/node_modules/esbuild/LICENSE.md +21 -0
  28. package/node_modules/esbuild/README.md +3 -0
  29. package/node_modules/esbuild/bin/esbuild +223 -0
  30. package/node_modules/esbuild/install.js +300 -0
  31. package/node_modules/esbuild/lib/main.d.ts +716 -0
  32. package/node_modules/esbuild/lib/main.js +2532 -0
  33. package/node_modules/esbuild/package.json +74 -0
  34. package/node_modules/tsx/LICENSE +21 -0
  35. package/node_modules/tsx/README.md +32 -0
  36. package/node_modules/tsx/dist/cjs/api/index.cjs +1 -0
  37. package/node_modules/tsx/dist/cjs/api/index.d.cts +35 -0
  38. package/node_modules/tsx/dist/cjs/api/index.d.mts +35 -0
  39. package/node_modules/tsx/dist/cjs/api/index.mjs +1 -0
  40. package/node_modules/tsx/dist/cjs/index.cjs +1 -0
  41. package/node_modules/tsx/dist/cjs/index.mjs +1 -0
  42. package/node_modules/tsx/dist/cli.cjs +54 -0
  43. package/node_modules/tsx/dist/cli.mjs +55 -0
  44. package/node_modules/tsx/dist/client-D3mGB526.cjs +1 -0
  45. package/node_modules/tsx/dist/client-D_mPDF5S.mjs +1 -0
  46. package/node_modules/tsx/dist/esm/api/index.cjs +1 -0
  47. package/node_modules/tsx/dist/esm/api/index.d.cts +35 -0
  48. package/node_modules/tsx/dist/esm/api/index.d.mts +35 -0
  49. package/node_modules/tsx/dist/esm/api/index.mjs +1 -0
  50. package/node_modules/tsx/dist/esm/index.cjs +1 -0
  51. package/node_modules/tsx/dist/esm/index.mjs +1 -0
  52. package/node_modules/tsx/dist/get-pipe-path-D4YM6rQt.cjs +1 -0
  53. package/node_modules/tsx/dist/get-pipe-path-_tAJyU_v.mjs +1 -0
  54. package/node_modules/tsx/dist/index-BWFBUo6r.cjs +1 -0
  55. package/node_modules/tsx/dist/index-D9F1FXzN.cjs +14 -0
  56. package/node_modules/tsx/dist/index-XurvG3JN.mjs +14 -0
  57. package/node_modules/tsx/dist/index-gbaejti9.mjs +1 -0
  58. package/node_modules/tsx/dist/lexer-DQCqS3nf.mjs +3 -0
  59. package/node_modules/tsx/dist/lexer-DgIbo0BU.cjs +3 -0
  60. package/node_modules/tsx/dist/loader.cjs +1 -0
  61. package/node_modules/tsx/dist/loader.mjs +1 -0
  62. package/node_modules/tsx/dist/node-features-B9BBLzwu.mjs +1 -0
  63. package/node_modules/tsx/dist/node-features-CQLdkVE6.cjs +1 -0
  64. package/node_modules/tsx/dist/package-CGdS2_oX.cjs +1 -0
  65. package/node_modules/tsx/dist/package-DyJMwVU5.mjs +1 -0
  66. package/node_modules/tsx/dist/patch-repl.cjs +1 -0
  67. package/node_modules/tsx/dist/patch-repl.mjs +1 -0
  68. package/node_modules/tsx/dist/preflight.cjs +1 -0
  69. package/node_modules/tsx/dist/preflight.mjs +1 -0
  70. package/node_modules/tsx/dist/register-BOkp8V6j.cjs +10 -0
  71. package/node_modules/tsx/dist/register-BnTWPeIB.mjs +10 -0
  72. package/node_modules/tsx/dist/register-CHVGxKtC.cjs +2 -0
  73. package/node_modules/tsx/dist/register-D_B8UL5H.mjs +2 -0
  74. package/node_modules/tsx/dist/repl.cjs +3 -0
  75. package/node_modules/tsx/dist/repl.mjs +3 -0
  76. package/node_modules/tsx/dist/require-CjvaJWEr.cjs +1 -0
  77. package/node_modules/tsx/dist/require-DzmC1hVr.mjs +1 -0
  78. package/node_modules/tsx/dist/suppress-warnings.cjs +1 -0
  79. package/node_modules/tsx/dist/suppress-warnings.mjs +1 -0
  80. package/node_modules/tsx/dist/temporary-directory-B83uKxJF.cjs +1 -0
  81. package/node_modules/tsx/dist/temporary-directory-BDDVQOvU.mjs +1 -0
  82. package/node_modules/tsx/dist/types-Cxp8y2TL.d.ts +5 -0
  83. package/node_modules/tsx/package.json +67 -0
  84. package/package.json +11 -6
  85. package/src/autostart.ts +5 -1
  86. package/src/compat.ts +193 -47
  87. package/src/cursor.ts +59 -3
  88. package/src/extensionBundle.ts +1 -1
  89. package/src/extensionLink.ts +28 -7
  90. package/src/install.ts +176 -24
  91. package/src/installRecord.ts +2 -0
  92. package/src/patchSet.ts +12 -6
  93. package/src/platform.ts +3 -3
  94. package/src/repair.ts +10 -1
  95. package/src/restore.ts +12 -4
  96. package/src/serviceProcess.ts +2 -1
  97. package/src/status.ts +6 -0
  98. package/src/trial.ts +1 -0
  99. package/test/autostart.test.ts +23 -2
  100. package/test/compat.test.ts +238 -3
  101. package/test/cursor-pool-bin.test.ts +1 -0
  102. package/test/cursor.test.ts +60 -1
  103. package/test/e2e-install.test.ts +53 -0
  104. package/test/extensionLink.test.ts +48 -2
  105. package/test/install.test.ts +191 -6
  106. package/test/repair.test.ts +1 -0
  107. package/test/serviceProcess.test.ts +10 -1
  108. package/test/status.test.ts +1 -0
package/src/install.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { cp, mkdir, mkdtemp, readFile, rm } from 'node:fs/promises';
2
- import { tmpdir } from 'node:os';
1
+ import { chmod, cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { homedir, tmpdir } from 'node:os';
3
3
  import { dirname, join, resolve } from 'node:path';
4
4
  import {
5
5
  patchCursorAgentExec,
@@ -7,16 +7,18 @@ import {
7
7
  restoreCursorAgentExec,
8
8
  restoreCursorAlwaysLocal,
9
9
  restoreCursorWorkbenchAuthGate,
10
+ containsCursorWorkbenchAuthGateMarker,
10
11
  } from '@cursor-pool/patcher';
11
12
  import { sha256File } from '@cursor-pool/patcher/hash';
12
13
  import { startServer } from '@cursor-pool/service';
14
+ import { assertBundledTakeoverPlan } from '@cursor-pool/takeover-plans';
13
15
  import {
14
16
  readRuntimeInfo,
15
17
  resolveRuntimeFile,
16
18
  writeRuntimeInfo,
17
19
  type RuntimeInfo,
18
20
  } from '@cursor-pool/service';
19
- import type { CompatibilityManifestEntry } from '@cursor-pool/shared/manifest';
21
+ import type { CompatibilityManifestEntry, CompatibilityPatchTarget } from '@cursor-pool/shared/manifest';
20
22
  import { DEFAULT_RUNTIME_FILE } from '@cursor-pool/shared/runtime';
21
23
  import { writeClientConfig } from '@cursor-pool/shared/clientConfig';
22
24
  import { containsCursorPoolMarker } from '@cursor-pool/patcher/marker';
@@ -32,6 +34,7 @@ import {
32
34
  import {
33
35
  getLinkedExtensionState,
34
36
  linkExtensionBundle,
37
+ linkedExtensionPathForSource,
35
38
  linkedExtensionPathForDir,
36
39
  refreshCursorExtensionsIndex,
37
40
  removeLinkedExtensionBundle,
@@ -100,10 +103,16 @@ export type InstallOptions = FindCursorOptions &
100
103
  restoreCursorWorkbenchAuthGate?: typeof restoreCursorWorkbenchAuthGate;
101
104
  adHocResignApp?: typeof adHocResignApp;
102
105
  fetchHealth?: (url: string) => Promise<{ ok: boolean; healthy: boolean }>;
106
+ linuxLauncherFile?: string;
103
107
  };
104
108
 
105
109
  const REAL_CURSOR_EXTENSIONS_DIR = '~/.cursor/extensions';
106
- const PACKAGE_VERSION = '0.5.6';
110
+ const PACKAGE_VERSION = '0.5.8';
111
+ const AUTOSTART_SERVICE_STARTUP_TIMEOUT_MS = 30_000;
112
+
113
+ function delay(ms: number) {
114
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
115
+ }
107
116
 
108
117
  async function maybeAdHocResign({
109
118
  appPath,
@@ -124,38 +133,124 @@ async function maybeAdHocResign({
124
133
  return 'ad-hoc' as const;
125
134
  }
126
135
 
127
- async function assertExpectedHash(targetPath: string, expectedSha256: string) {
136
+ function markerPredicateForPatchTarget(target: CompatibilityPatchTarget) {
137
+ if (target.name === 'workbench' || target.verifyMarker === 'cursor-pool-workbench') {
138
+ return containsCursorWorkbenchAuthGateMarker;
139
+ }
140
+ return containsCursorPoolMarker;
141
+ }
142
+
143
+ async function assertExpectedHash(
144
+ targetPath: string,
145
+ expectedSha256: string,
146
+ containsExpectedMarker = containsCursorPoolMarker,
147
+ ) {
128
148
  const currentHash = await sha256File(targetPath);
129
149
  if (currentHash !== expectedSha256) {
130
150
  const content = await readFile(targetPath, 'utf8');
131
- if (containsCursorPoolMarker(content)) {
151
+ if (containsExpectedMarker(content)) {
132
152
  return;
133
153
  }
134
154
  throw new Error(`Patch target hash mismatch: expected ${expectedSha256}, got ${currentHash}`);
135
155
  }
136
156
  }
137
157
 
138
- async function startInstallService(options: InstallOptions) {
158
+ function compatPatchTargets(compat: CompatibilityManifestEntry): CompatibilityPatchTarget[] {
159
+ if (compat.patchTargets?.length) {
160
+ return compat.patchTargets;
161
+ }
162
+ return [
163
+ {
164
+ name: 'agent-exec',
165
+ targetRelativePath: compat.targetRelativePath,
166
+ expectedSha256: compat.expectedSha256,
167
+ patchStrategy: compat.patchStrategy,
168
+ verifyMarker: compat.verifyMarker,
169
+ },
170
+ ];
171
+ }
172
+
173
+ function workbenchTargetRelativePathFromCompat(compat: CompatibilityManifestEntry) {
174
+ return compat.patchTargets?.find((target) => target.name === 'workbench')?.targetRelativePath;
175
+ }
176
+
177
+ async function assertExpectedPatchTargetHashes(appPath: string, compat: CompatibilityManifestEntry) {
178
+ for (const target of compatPatchTargets(compat)) {
179
+ await assertExpectedHash(
180
+ join(appPath, target.targetRelativePath),
181
+ target.expectedSha256,
182
+ markerPredicateForPatchTarget(target),
183
+ );
184
+ }
185
+ }
186
+
187
+ async function startServiceFromAutostart(options: InstallOptions, targetMode: 'real' | 'disposable') {
188
+ const autostart = await installAutostart(options, targetMode);
189
+ const runtimeFile = options.runtimeFile ?? DEFAULT_RUNTIME_FILE;
190
+ const fetchHealthFn = options.fetchHealth ?? fetchHealth;
191
+ const startedAt = Date.now();
192
+ let runtime: RuntimeInfo | null = null;
193
+
194
+ while (Date.now() - startedAt < AUTOSTART_SERVICE_STARTUP_TIMEOUT_MS) {
195
+ runtime = await readRuntimeInfo({ runtimeFile });
196
+ const health = runtime
197
+ ? await fetchHealthFn(`http://${runtime.host}:${runtime.port}/health`)
198
+ : null;
199
+ if (runtime && health?.ok && health.healthy) {
200
+ return {
201
+ service: {
202
+ ...runtime,
203
+ stop: () => stopRuntimeService(runtimeFile),
204
+ },
205
+ autostartState: autostart.state,
206
+ };
207
+ }
208
+ await delay(50);
209
+ }
210
+
211
+ throw new Error('Autostart service failed to become healthy');
212
+ }
213
+
214
+ async function startInstallService(
215
+ options: InstallOptions,
216
+ targetMode: 'real' | 'disposable',
217
+ platform: NodeJS.Platform,
218
+ ) {
139
219
  if (options.stopServiceAfterInstall) {
140
220
  const service = await startServer({ runtimeFile: options.runtimeFile });
141
221
  return {
142
- host: service.host,
143
- port: service.port,
144
- runtimeId: service.runtimeId,
145
- stop: service.stop,
222
+ service: {
223
+ host: service.host,
224
+ port: service.port,
225
+ runtimeId: service.runtimeId,
226
+ stop: service.stop,
227
+ },
228
+ autostartState: 'skipped' as const,
146
229
  };
147
230
  }
148
231
 
149
232
  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
- });
233
+ let runtime: RuntimeInfo;
234
+ try {
235
+ runtime = await startDetached({
236
+ runtimeFile: options.runtimeFile ?? DEFAULT_RUNTIME_FILE,
237
+ logFile: options.serviceLogFile ?? '~/.cursor-pool/logs/service.log',
238
+ configFile: options.clientConfigFile,
239
+ apiBaseUrl: options.apiBaseUrl,
240
+ });
241
+ } catch (error) {
242
+ if (platform === 'linux' && targetMode === 'real') {
243
+ return startServiceFromAutostart(options, targetMode);
244
+ }
245
+ throw error;
246
+ }
247
+
156
248
  return {
157
- ...runtime,
158
- stop: () => stopRuntimeService(options.runtimeFile ?? DEFAULT_RUNTIME_FILE),
249
+ service: {
250
+ ...runtime,
251
+ stop: () => stopRuntimeService(options.runtimeFile ?? DEFAULT_RUNTIME_FILE),
252
+ },
253
+ autostartState: undefined,
159
254
  };
160
255
  }
161
256
 
@@ -184,6 +279,43 @@ async function installAutostart(options: InstallOptions, targetMode: 'real' | 'd
184
279
  });
185
280
  }
186
281
 
282
+ function defaultLinuxLauncherFile() {
283
+ return join(homedir(), '.local/bin/cursor-pool-cursor');
284
+ }
285
+
286
+ function shellSingleQuote(value: string) {
287
+ return `'${value.replaceAll("'", "'\\''")}'`;
288
+ }
289
+
290
+ async function installLinuxLauncher({
291
+ appPath,
292
+ platform,
293
+ launcherFile,
294
+ }: {
295
+ appPath: string;
296
+ platform: NodeJS.Platform;
297
+ launcherFile?: string;
298
+ }) {
299
+ if (platform !== 'linux') {
300
+ return 'skipped';
301
+ }
302
+
303
+ const targetFile = launcherFile ?? defaultLinuxLauncherFile();
304
+ await mkdir(dirname(targetFile), { recursive: true });
305
+ await writeFile(
306
+ targetFile,
307
+ [
308
+ '#!/usr/bin/env sh',
309
+ 'set -eu',
310
+ `exec ${shellSingleQuote(join(appPath, 'AppRun'))} --no-sandbox "$@"`,
311
+ '',
312
+ ].join('\n'),
313
+ 'utf8',
314
+ );
315
+ await chmod(targetFile, 0o755);
316
+ return targetFile;
317
+ }
318
+
187
319
  function sameRuntime(a: RuntimeInfo | null | undefined, b: RuntimeInfo | null | undefined) {
188
320
  return Boolean(
189
321
  a &&
@@ -286,11 +418,12 @@ async function linkInstallExtensionBundle({
286
418
  sourceBundlePath: string;
287
419
  cursorExtensionsDir: string;
288
420
  }): ReturnType<typeof linkExtensionBundle> {
289
- const linkedPath = linkedExtensionPathForDir(cursorExtensionsDir);
290
421
  if ((await getLinkedExtensionState(sourceBundlePath)) === 'missing') {
422
+ const linkedPath = linkedExtensionPathForDir(cursorExtensionsDir);
291
423
  return { state: 'missing', linkedPath };
292
424
  }
293
425
 
426
+ const linkedPath = await linkedExtensionPathForSource(cursorExtensionsDir, sourceBundlePath);
294
427
  await removeInstallLinkedExtensionBundle(linkedPath, cursorExtensionsDir);
295
428
  await mkdir(dirname(linkedPath), { recursive: true });
296
429
  await cp(sourceBundlePath, linkedPath, { recursive: true });
@@ -314,13 +447,19 @@ export async function install(options: InstallOptions = {}) {
314
447
  compatManifestUrl: options.compatManifestUrl,
315
448
  fetchManifest: options.fetchCompatManifest,
316
449
  });
317
- const compat = resolveCompatEntry(cursor, environment, { entries: compatEntries });
450
+ const adapterVersion = PACKAGE_VERSION === '0.5.8'
451
+ ? undefined
452
+ : PACKAGE_VERSION.split('-', 1)[0].split('+', 1)[0];
453
+ const compat = resolveCompatEntry(cursor, environment, { entries: compatEntries, adapterVersion });
454
+ assertBundledTakeoverPlan(compat.takeoverPlanId);
455
+ const workbenchTargetRelativePath = workbenchTargetRelativePathFromCompat(compat);
318
456
  const targetPath = join(cursor.appPath, compat.targetRelativePath);
319
457
  const originalSha256 = await sha256File(targetPath);
320
- await assertExpectedHash(targetPath, compat.expectedSha256);
458
+ await assertExpectedPatchTargetHashes(cursor.appPath, compat);
321
459
  const patchSetBeforeInstall = await readCursorPatchSetState(cursor.appPath, {
322
460
  agentExecTargetRelativePath: compat.targetRelativePath,
323
461
  platform: environment.platform,
462
+ workbenchTargetRelativePath,
324
463
  });
325
464
  const wasPatchedBeforeInstall = patchSetBeforeInstall.allApplied;
326
465
  const existingTrialRecord = await readTrialRecord(cursor.appPath, {
@@ -348,6 +487,7 @@ export async function install(options: InstallOptions = {}) {
348
487
  let resignState: 'ad-hoc' | 'skipped' = 'skipped';
349
488
  let patchState = 'skipped';
350
489
  let autostartState = 'skipped';
490
+ let launcherState = 'skipped';
351
491
 
352
492
  try {
353
493
  if (target.requiresConfirmation) {
@@ -404,12 +544,14 @@ export async function install(options: InstallOptions = {}) {
404
544
  }
405
545
 
406
546
  await writeInstallClientConfig(options);
407
- service = await startInstallService(options);
408
- autostartState = (await installAutostart(options, target.mode)).state;
547
+ const started = await startInstallService(options, target.mode, environment.platform);
548
+ service = started.service;
549
+ autostartState = started.autostartState ?? (await installAutostart(options, target.mode)).state;
409
550
  const patchSet = await patchCursorSet(cursor.appPath, {
410
551
  backupDir: options.backupDir,
411
552
  platform: environment.platform,
412
553
  agentExecTargetRelativePath: compat.targetRelativePath,
554
+ workbenchTargetRelativePath,
413
555
  patchCursorAgentExec: options.patchCursorAgentExec,
414
556
  patchCursorWorkbenchAuthGate: options.patchCursorWorkbenchAuthGate,
415
557
  });
@@ -421,6 +563,11 @@ export async function install(options: InstallOptions = {}) {
421
563
  requiresAdHocResign: compat.requiresAdHocResign,
422
564
  resign: options.adHocResignApp ?? adHocResignApp,
423
565
  });
566
+ launcherState = await installLinuxLauncher({
567
+ appPath: cursor.appPath,
568
+ platform: environment.platform,
569
+ launcherFile: options.linuxLauncherFile,
570
+ });
424
571
 
425
572
  const health = await (options.fetchHealth ?? fetchHealth)(
426
573
  `http://${service.host}:${service.port}/health`,
@@ -434,6 +581,7 @@ export async function install(options: InstallOptions = {}) {
434
581
  appPath: cursor.appPath,
435
582
  cursorVersion: cursor.version,
436
583
  cursorCommit: cursor.commit,
584
+ takeoverPlanId: compat.takeoverPlanId,
437
585
  targetRelativePath: compat.targetRelativePath,
438
586
  originalSha256,
439
587
  compatSupportStatus: compat.supportStatus,
@@ -478,11 +626,13 @@ export async function install(options: InstallOptions = {}) {
478
626
  `mode: ${target.mode}`,
479
627
  `app: ${cursor.appPath}`,
480
628
  `compat: ${compat.supportStatus}`,
629
+ `takeover-plan: ${compat.takeoverPlanId}`,
481
630
  `extension: ${extension.state}`,
482
631
  `service: running ${service.host}:${service.port}`,
483
632
  `autostart: ${autostartState}`,
484
633
  `patch: ${patchState}`,
485
634
  `resign: ${resignState}`,
635
+ `launcher: ${launcherState}`,
486
636
  'health: ok',
487
637
  target.mode === 'real' ? 'install-record: recorded' : 'trial: recorded',
488
638
  ].join('\n');
@@ -496,6 +646,7 @@ export async function install(options: InstallOptions = {}) {
496
646
  (await readCursorPatchSetState(cursor.appPath, {
497
647
  agentExecTargetRelativePath: compat.targetRelativePath,
498
648
  platform: environment.platform,
649
+ workbenchTargetRelativePath,
499
650
  })).appliedCount > 0);
500
651
  } catch (rollbackError) {
501
652
  rollbackErrors.push(rollbackError);
@@ -505,6 +656,7 @@ export async function install(options: InstallOptions = {}) {
505
656
  await restoreCursorSet(cursor.appPath, {
506
657
  backupDir: options.backupDir,
507
658
  platform: environment.platform,
659
+ workbenchTargetRelativePath,
508
660
  restoreCursorAgentExec: options.restoreCursorAgentExec,
509
661
  restoreCursorAlwaysLocal: options.restoreCursorAlwaysLocal,
510
662
  restoreCursorWorkbenchAuthGate: options.restoreCursorWorkbenchAuthGate,
@@ -13,6 +13,7 @@ export type InstallRecord = {
13
13
  appPath: string;
14
14
  cursorVersion: string;
15
15
  cursorCommit: string;
16
+ takeoverPlanId: string;
16
17
  targetRelativePath: string;
17
18
  originalSha256: string;
18
19
  compatSupportStatus: CompatibilityManifestEntry['supportStatus'];
@@ -91,6 +92,7 @@ export function isInstallRecordStale(record: InstallRecord | null, actual: Insta
91
92
  'appPath',
92
93
  'cursorVersion',
93
94
  'cursorCommit',
95
+ 'takeoverPlanId',
94
96
  'targetRelativePath',
95
97
  'originalSha256',
96
98
  'compatSupportStatus',
package/src/patchSet.ts CHANGED
@@ -144,6 +144,10 @@ export async function patchCursorSet(appPath: string, options: PatchCursorSetOpt
144
144
 
145
145
  export async function restoreCursorSet(appPath: string, options: RestoreCursorSetOptions = {}) {
146
146
  const restoreErrors: unknown[] = [];
147
+ const before = await readCursorPatchSetState(appPath, {
148
+ platform: options.platform,
149
+ workbenchTargetRelativePath: options.workbenchTargetRelativePath,
150
+ });
147
151
  const restore = async (operation: () => Promise<unknown>) => {
148
152
  try {
149
153
  await operation();
@@ -152,12 +156,14 @@ export async function restoreCursorSet(appPath: string, options: RestoreCursorSe
152
156
  }
153
157
  };
154
158
 
155
- await restore(() =>
156
- (options.restoreCursorWorkbenchAuthGate ?? restoreCursorWorkbenchAuthGate)(appPath, {
157
- backupDir: options.backupDir,
158
- targetRelativePath: workbenchRelativePath(options),
159
- }),
160
- );
159
+ if (before.patches.some((patch) => patch.name === 'workbench' && patch.markerPresent)) {
160
+ await restore(() =>
161
+ (options.restoreCursorWorkbenchAuthGate ?? restoreCursorWorkbenchAuthGate)(appPath, {
162
+ backupDir: options.backupDir,
163
+ targetRelativePath: workbenchRelativePath(options),
164
+ }),
165
+ );
166
+ }
161
167
  const alwaysLocalPath = resolveCursorAlwaysLocalPath(appPath, alwaysLocalRelativePath(options));
162
168
  const shouldRestoreAlwaysLocal =
163
169
  Boolean(options.restoreCursorAlwaysLocal) ||
package/src/platform.ts CHANGED
@@ -82,9 +82,9 @@ function buildDeviceInfo() {
82
82
  name: hostname(),
83
83
  os: osPlatform(),
84
84
  arch: arch(),
85
- cliVersion: '0.5.6',
86
- serviceVersion: '0.5.6',
87
- extensionVersion: '0.5.6',
85
+ cliVersion: '0.5.8',
86
+ serviceVersion: '0.5.8',
87
+ extensionVersion: '0.5.8',
88
88
  };
89
89
  }
90
90
 
package/src/repair.ts CHANGED
@@ -69,7 +69,7 @@ export type RepairOptions = FindCursorOptions &
69
69
  adHocResignApp?: typeof adHocResignApp;
70
70
  };
71
71
 
72
- const PACKAGE_VERSION = '0.5.6';
72
+ const PACKAGE_VERSION = '0.5.8';
73
73
 
74
74
  function normalizeAppPath(path: string) {
75
75
  return normalize(path).replace(/\/+$/, '');
@@ -96,6 +96,10 @@ function assertRealRepairInstallRecord(
96
96
  return record;
97
97
  }
98
98
 
99
+ function workbenchTargetRelativePathFromCompat(compat: CompatibilityManifestEntry) {
100
+ return compat.patchTargets?.find((target) => target.name === 'workbench')?.targetRelativePath;
101
+ }
102
+
99
103
  function resolveRepairCompatEntry({
100
104
  cursorVersion,
101
105
  cursorCommit,
@@ -223,6 +227,7 @@ export async function repair(options: RepairOptions = {}) {
223
227
  if (compat.supportStatus === 'blocked' || compat.supportStatus === 'unknown') {
224
228
  throw new Error(`compat: ${compat.supportStatus}`);
225
229
  }
230
+ const workbenchTargetRelativePath = workbenchTargetRelativePathFromCompat(compat);
226
231
  const targetPath = join(cursor.appPath, compat.targetRelativePath);
227
232
  const runtimeFile = options.runtimeFile ?? installRecord.runtimeFile ?? DEFAULT_RUNTIME_FILE;
228
233
  const backupDir = options.backupDir ?? installRecord.backupDir;
@@ -323,6 +328,8 @@ export async function repair(options: RepairOptions = {}) {
323
328
 
324
329
  const patchSetState = await readCursorPatchSetState(cursor.appPath, {
325
330
  agentExecTargetRelativePath: compat.targetRelativePath,
331
+ platform: environment.platform,
332
+ workbenchTargetRelativePath,
326
333
  });
327
334
 
328
335
  if (patchSetState.allApplied) {
@@ -331,7 +338,9 @@ export async function repair(options: RepairOptions = {}) {
331
338
  } else {
332
339
  const repairedPatchSet = await patchCursorSet(cursor.appPath, {
333
340
  backupDir,
341
+ platform: environment.platform,
334
342
  agentExecTargetRelativePath: compat.targetRelativePath,
343
+ workbenchTargetRelativePath,
335
344
  patchCursorAgentExec: options.patchCursorAgentExec,
336
345
  patchCursorWorkbenchAuthGate: options.patchCursorWorkbenchAuthGate,
337
346
  });
package/src/restore.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { normalize } from 'node:path';
2
- import { restoreCursorAgentExec, resolveCursorAgentExecPath } from '@cursor-pool/patcher';
2
+ import { resolveCursorAgentExecPath } from '@cursor-pool/patcher';
3
3
  import { DEFAULT_RUNTIME_FILE } from '@cursor-pool/shared/runtime';
4
4
  import {
5
5
  confirmRealOperation,
@@ -14,6 +14,11 @@ import {
14
14
  type InstallRecordOptions,
15
15
  } from './installRecord';
16
16
  import { stopRuntimeService } from './serviceProcess';
17
+ import {
18
+ formatCursorPatchSetState,
19
+ readCursorPatchSetState,
20
+ restoreCursorSet,
21
+ } from './patchSet';
17
22
  import { resolveCursorTarget, type CursorTargetOptions } from './target';
18
23
  import { assertDisposableCursorAppPath, readTrialRecord, type TrialRecordOptions } from './trial';
19
24
 
@@ -132,9 +137,12 @@ export async function restore(options: RestoreOptions = {}) {
132
137
  });
133
138
  }
134
139
 
135
- const result = await restoreCursorAgentExec(cursor.appPath, {
140
+ await restoreCursorSet(cursor.appPath, {
136
141
  backupDir,
137
- targetRelativePath,
142
+ agentExecTargetRelativePath: targetRelativePath,
143
+ });
144
+ const patchSetState = await readCursorPatchSetState(cursor.appPath, {
145
+ agentExecTargetRelativePath: targetRelativePath,
138
146
  });
139
147
  if (target.mode === 'real') {
140
148
  await stopRuntimeServiceIfRecorded(runtimeFile);
@@ -145,7 +153,7 @@ export async function restore(options: RestoreOptions = {}) {
145
153
  `mode: ${target.mode}`,
146
154
  `app: ${cursor.appPath}`,
147
155
  'restore: ok',
148
- `patch: ${result.markerPresent ? 'applied' : 'missing'}`,
156
+ `patch: ${formatCursorPatchSetState(patchSetState)}`,
149
157
  target.mode === 'real'
150
158
  ? `install-record: ${realInstallRecord ? 'recorded' : 'missing'}`
151
159
  : `trial: ${trialRecord ? 'recorded' : 'missing'}`,
@@ -18,6 +18,7 @@ export type StartDetachedServiceOptions = {
18
18
  const require = createRequire(import.meta.url);
19
19
  const DEFAULT_SERVICE_ENTRY = require.resolve('@cursor-pool/service/entry');
20
20
  const TSX_ESM_LOADER = require.resolve('tsx/esm');
21
+ export const DEFAULT_DETACHED_SERVICE_STARTUP_TIMEOUT_MS = 30_000;
21
22
  function resolveTildePath(path: string) {
22
23
  if (path.startsWith('~/')) {
23
24
  return join(homedir(), path.slice(2));
@@ -132,7 +133,7 @@ export async function startDetachedService(
132
133
  let healthy = false;
133
134
  try {
134
135
  const startedAt = Date.now();
135
- const timeoutMs = options.startupTimeoutMs ?? 5000;
136
+ const timeoutMs = options.startupTimeoutMs ?? DEFAULT_DETACHED_SERVICE_STARTUP_TIMEOUT_MS;
136
137
  while (Date.now() - startedAt < timeoutMs) {
137
138
  if (startupError) {
138
139
  throw startupError;
package/src/status.ts CHANGED
@@ -49,6 +49,10 @@ type LatestTakeoverResponse = {
49
49
  };
50
50
  };
51
51
 
52
+ function workbenchTargetRelativePathFromCompat(compat: CompatibilityManifestEntry) {
53
+ return compat.patchTargets?.find((target) => target.name === 'workbench')?.targetRelativePath;
54
+ }
55
+
52
56
  const MAX_CANARY_FIELD_LENGTH = 256;
53
57
 
54
58
  function isSafeCanaryString(value: unknown): value is string {
@@ -158,6 +162,7 @@ export async function status(options: StatusOptions = {}) {
158
162
  fetchManifest: options.fetchCompatManifest,
159
163
  });
160
164
  const compat = resolveCompatEntry(cursor, environment, { entries: compatEntries });
165
+ const workbenchTargetRelativePath = workbenchTargetRelativePathFromCompat(compat);
161
166
  const installRecord =
162
167
  target.mode === 'real'
163
168
  ? await readInstallRecord({ installRecordFile: options.installRecordFile })
@@ -189,6 +194,7 @@ export async function status(options: StatusOptions = {}) {
189
194
  const patchSetState = await readCursorPatchSetState(cursor.appPath, {
190
195
  agentExecTargetRelativePath: compat.targetRelativePath,
191
196
  platform: environment.platform,
197
+ workbenchTargetRelativePath,
192
198
  });
193
199
  const serviceRunning = await isRuntimeHealthy(runtime);
194
200
  const takeoverStatus = await getTakeoverStatus(runtime, serviceRunning);
package/src/trial.ts CHANGED
@@ -12,6 +12,7 @@ export type TrialRecord = {
12
12
  appPath: string;
13
13
  cursorVersion: string;
14
14
  cursorCommit: string;
15
+ takeoverPlanId: string;
15
16
  targetRelativePath: string;
16
17
  originalSha256: string;
17
18
  compatSupportStatus: 'supported' | 'canary' | 'warning' | 'blocked' | 'unknown';
@@ -125,7 +125,7 @@ test('installUserAutostart writes a Linux user systemd service', async () => {
125
125
  linuxServiceFile: serviceFile,
126
126
  nodeCommand: '/usr/bin/node',
127
127
  cliEntry: '/home/demo/.cursor-pool/client/node_modules/@cursorpool-dev/cli/bin/cursor-pool.mjs',
128
- tsxLoader: '/home/demo/.cursor-pool/client/node_modules/tsx/dist/esm/index.mjs',
128
+ tsxLoader: '/home/demo/.cursor-pool/client/node_modules/@cursorpool-dev/cli/node_modules/tsx/dist/esm/index.mjs',
129
129
  serviceEntry: '/home/demo/.cursor-pool/client/node_modules/@cursorpool-dev/cli/node_modules/@cursor-pool/service/src/entry.ts',
130
130
  runtimeFile: '/home/demo/.cursor-pool/runtime.json',
131
131
  serviceLogFile: '/home/demo/.cursor-pool/logs/service.log',
@@ -138,10 +138,11 @@ test('installUserAutostart writes a Linux user systemd service', async () => {
138
138
 
139
139
  assert.equal(result.state, 'installed');
140
140
  const service = await readFile(serviceFile, 'utf8');
141
- assert.match(service, /ExecStart=\/usr\/bin\/node --import .*tsx\/dist\/esm\/index\.mjs .*service\/src\/entry\.ts --runtime-file/);
141
+ assert.match(service, /ExecStart=\/usr\/bin\/node --import .*@cursorpool-dev\/cli\/node_modules\/tsx\/dist\/esm\/index\.mjs .*service\/src\/entry\.ts --runtime-file/);
142
142
  assert.match(service, /--config-file/);
143
143
  assert.match(service, /Restart=always/);
144
144
  assert.deepEqual(calls, [
145
+ ['--user', 'unmask', 'cursor-pool.service'],
145
146
  ['--user', 'daemon-reload'],
146
147
  ['--user', 'enable', '--now', 'cursor-pool.service'],
147
148
  ]);
@@ -149,3 +150,23 @@ test('installUserAutostart writes a Linux user systemd service', async () => {
149
150
  await rm(tempDir, { recursive: true, force: true });
150
151
  }
151
152
  });
153
+
154
+ test('installUserAutostart resolves the default tsx loader from the packaged CLI dependency tree', async () => {
155
+ const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-autostart-default-tsx-'));
156
+ const serviceFile = join(tempDir, 'systemd/cursor-pool.service');
157
+
158
+ try {
159
+ await installUserAutostart({
160
+ platform: 'linux',
161
+ linuxServiceFile: serviceFile,
162
+ nodeCommand: '/usr/bin/node',
163
+ execFile: async () => ({ stdout: '', stderr: '' }),
164
+ });
165
+
166
+ const service = await readFile(serviceFile, 'utf8');
167
+ assert.match(service, /--import .*tsx\/dist\/esm\/index\.mjs/);
168
+ assert.doesNotMatch(service, /\.cursor-pool\/client\/node_modules\/tsx\/dist\/esm\/index\.mjs/);
169
+ } finally {
170
+ await rm(tempDir, { recursive: true, force: true });
171
+ }
172
+ });