@cursorpool-dev/cli 0.5.6 → 0.5.9

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 (93) hide show
  1. package/node_modules/@cursor-pool/extension/dist/extension.js +1 -1
  2. package/node_modules/@cursor-pool/extension/package.json +3 -3
  3. package/node_modules/@cursor-pool/extension/src/api.ts +1 -1
  4. package/node_modules/@cursor-pool/extension/test/panel.test.ts +1 -1
  5. package/node_modules/@cursor-pool/patcher/package.json +2 -2
  6. package/node_modules/@cursor-pool/patcher/src/marker.ts +72 -3
  7. package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +128 -14
  8. package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +102 -7
  9. package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +193 -0
  10. package/node_modules/@cursor-pool/service/package.json +2 -2
  11. package/node_modules/@cursor-pool/service/src/server.ts +1 -0
  12. package/node_modules/@cursor-pool/service/test/server.test.ts +1 -0
  13. package/node_modules/@cursor-pool/shared/package.json +1 -1
  14. package/node_modules/@cursor-pool/shared/test/manifest.test.ts +6 -6
  15. package/node_modules/@esbuild/linux-x64/README.md +3 -0
  16. package/node_modules/@esbuild/linux-x64/bin/esbuild +0 -0
  17. package/node_modules/@esbuild/linux-x64/package.json +20 -0
  18. package/node_modules/esbuild/LICENSE.md +21 -0
  19. package/node_modules/esbuild/README.md +3 -0
  20. package/node_modules/esbuild/bin/esbuild +223 -0
  21. package/node_modules/esbuild/install.js +300 -0
  22. package/node_modules/esbuild/lib/main.d.ts +716 -0
  23. package/node_modules/esbuild/lib/main.js +2532 -0
  24. package/node_modules/esbuild/package.json +74 -0
  25. package/node_modules/tsx/LICENSE +21 -0
  26. package/node_modules/tsx/README.md +32 -0
  27. package/node_modules/tsx/dist/cjs/api/index.cjs +1 -0
  28. package/node_modules/tsx/dist/cjs/api/index.d.cts +35 -0
  29. package/node_modules/tsx/dist/cjs/api/index.d.mts +35 -0
  30. package/node_modules/tsx/dist/cjs/api/index.mjs +1 -0
  31. package/node_modules/tsx/dist/cjs/index.cjs +1 -0
  32. package/node_modules/tsx/dist/cjs/index.mjs +1 -0
  33. package/node_modules/tsx/dist/cli.cjs +54 -0
  34. package/node_modules/tsx/dist/cli.mjs +55 -0
  35. package/node_modules/tsx/dist/client-D3mGB526.cjs +1 -0
  36. package/node_modules/tsx/dist/client-D_mPDF5S.mjs +1 -0
  37. package/node_modules/tsx/dist/esm/api/index.cjs +1 -0
  38. package/node_modules/tsx/dist/esm/api/index.d.cts +35 -0
  39. package/node_modules/tsx/dist/esm/api/index.d.mts +35 -0
  40. package/node_modules/tsx/dist/esm/api/index.mjs +1 -0
  41. package/node_modules/tsx/dist/esm/index.cjs +1 -0
  42. package/node_modules/tsx/dist/esm/index.mjs +1 -0
  43. package/node_modules/tsx/dist/get-pipe-path-D4YM6rQt.cjs +1 -0
  44. package/node_modules/tsx/dist/get-pipe-path-_tAJyU_v.mjs +1 -0
  45. package/node_modules/tsx/dist/index-BWFBUo6r.cjs +1 -0
  46. package/node_modules/tsx/dist/index-D9F1FXzN.cjs +14 -0
  47. package/node_modules/tsx/dist/index-XurvG3JN.mjs +14 -0
  48. package/node_modules/tsx/dist/index-gbaejti9.mjs +1 -0
  49. package/node_modules/tsx/dist/lexer-DQCqS3nf.mjs +3 -0
  50. package/node_modules/tsx/dist/lexer-DgIbo0BU.cjs +3 -0
  51. package/node_modules/tsx/dist/loader.cjs +1 -0
  52. package/node_modules/tsx/dist/loader.mjs +1 -0
  53. package/node_modules/tsx/dist/node-features-B9BBLzwu.mjs +1 -0
  54. package/node_modules/tsx/dist/node-features-CQLdkVE6.cjs +1 -0
  55. package/node_modules/tsx/dist/package-CGdS2_oX.cjs +1 -0
  56. package/node_modules/tsx/dist/package-DyJMwVU5.mjs +1 -0
  57. package/node_modules/tsx/dist/patch-repl.cjs +1 -0
  58. package/node_modules/tsx/dist/patch-repl.mjs +1 -0
  59. package/node_modules/tsx/dist/preflight.cjs +1 -0
  60. package/node_modules/tsx/dist/preflight.mjs +1 -0
  61. package/node_modules/tsx/dist/register-BOkp8V6j.cjs +10 -0
  62. package/node_modules/tsx/dist/register-BnTWPeIB.mjs +10 -0
  63. package/node_modules/tsx/dist/register-CHVGxKtC.cjs +2 -0
  64. package/node_modules/tsx/dist/register-D_B8UL5H.mjs +2 -0
  65. package/node_modules/tsx/dist/repl.cjs +3 -0
  66. package/node_modules/tsx/dist/repl.mjs +3 -0
  67. package/node_modules/tsx/dist/require-CjvaJWEr.cjs +1 -0
  68. package/node_modules/tsx/dist/require-DzmC1hVr.mjs +1 -0
  69. package/node_modules/tsx/dist/suppress-warnings.cjs +1 -0
  70. package/node_modules/tsx/dist/suppress-warnings.mjs +1 -0
  71. package/node_modules/tsx/dist/temporary-directory-B83uKxJF.cjs +1 -0
  72. package/node_modules/tsx/dist/temporary-directory-BDDVQOvU.mjs +1 -0
  73. package/node_modules/tsx/dist/types-Cxp8y2TL.d.ts +5 -0
  74. package/node_modules/tsx/package.json +67 -0
  75. package/package.json +9 -6
  76. package/src/autostart.ts +5 -1
  77. package/src/compat.ts +103 -29
  78. package/src/cursor.ts +59 -3
  79. package/src/extensionBundle.ts +1 -1
  80. package/src/extensionLink.ts +3 -3
  81. package/src/install.ts +118 -19
  82. package/src/platform.ts +3 -3
  83. package/src/repair.ts +2 -1
  84. package/src/serviceProcess.ts +2 -1
  85. package/src/trial.ts +2 -2
  86. package/test/autostart.test.ts +23 -2
  87. package/test/compat.test.ts +108 -9
  88. package/test/cursor-pool-bin.test.ts +1 -0
  89. package/test/cursor.test.ts +60 -1
  90. package/test/extensionLink.test.ts +24 -1
  91. package/test/install.test.ts +127 -2
  92. package/test/serviceProcess.test.ts +10 -1
  93. package/test/trial.test.ts +15 -1
package/src/cursor.ts CHANGED
@@ -1,7 +1,12 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { join, win32 } from 'node:path';
1
+ import { execFile } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { promisify } from 'node:util';
4
+ import { chmod, mkdir, readFile, rm, stat } from 'node:fs/promises';
5
+ import { basename, join, win32 } from 'node:path';
6
+ import { homedir } from 'node:os';
3
7
 
4
8
  export const DEFAULT_MACOS_CURSOR_APP_PATH = '/Applications/Cursor.app';
9
+ const execFileAsync = promisify(execFile);
5
10
 
6
11
  export type CursorInfo = {
7
12
  appPath: string;
@@ -14,6 +19,7 @@ export type FindCursorOptions = {
14
19
  productRelativePath?: string;
15
20
  platform?: NodeJS.Platform;
16
21
  env?: NodeJS.ProcessEnv;
22
+ appImageExtract?: (appImagePath: string, outputRoot: string) => Promise<string>;
17
23
  };
18
24
 
19
25
  type CursorProductJson = {
@@ -43,7 +49,8 @@ function readString(value: unknown) {
43
49
  }
44
50
 
45
51
  export async function findCursor(options: FindCursorOptions = {}): Promise<CursorInfo> {
46
- const appPath = options.appPath ?? defaultCursorAppPath(options);
52
+ const rawAppPath = options.appPath ?? defaultCursorAppPath(options);
53
+ const appPath = await resolveCursorAppPath(rawAppPath, options);
47
54
  const productJsonPath = await resolveProductJsonPath(appPath, options);
48
55
  const product = JSON.parse(await readFile(productJsonPath, 'utf8')) as CursorProductJson;
49
56
  const version = readString(product.version);
@@ -66,6 +73,55 @@ export async function findCursor(options: FindCursorOptions = {}): Promise<Curso
66
73
  };
67
74
  }
68
75
 
76
+ async function pathIsFile(path: string) {
77
+ try {
78
+ return (await stat(path)).isFile();
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+
84
+ function linuxAppImageExtractRoot(appImagePath: string) {
85
+ const hash = createHash('sha256').update(appImagePath).digest('hex').slice(0, 16);
86
+ const name = basename(appImagePath).replace(/\.AppImage$/i, '').replace(/[^A-Za-z0-9._-]/g, '-');
87
+ return join(homedir(), '.cursor-pool/appimages', `${name}-${hash}`);
88
+ }
89
+
90
+ async function defaultExtractLinuxAppImage(appImagePath: string, outputRoot: string) {
91
+ await rm(outputRoot, { recursive: true, force: true });
92
+ await mkdir(outputRoot, { recursive: true });
93
+ await chmod(appImagePath, 0o755).catch(() => undefined);
94
+ await execFileAsync(appImagePath, ['--appimage-extract'], {
95
+ cwd: outputRoot,
96
+ maxBuffer: 1024 * 1024 * 20,
97
+ });
98
+ return join(outputRoot, 'squashfs-root');
99
+ }
100
+
101
+ async function resolveCursorAppPath(appPath: string, options: FindCursorOptions) {
102
+ if (
103
+ (options.platform ?? process.platform) !== 'linux' ||
104
+ !/\.AppImage$/i.test(appPath) ||
105
+ !(await pathIsFile(appPath))
106
+ ) {
107
+ return appPath;
108
+ }
109
+
110
+ const outputRoot = linuxAppImageExtractRoot(appPath);
111
+ const extractedPath = join(outputRoot, 'squashfs-root');
112
+ try {
113
+ await readFile(join(extractedPath, 'usr/share/cursor/resources/app/product.json'), 'utf8');
114
+ return extractedPath;
115
+ } catch (error) {
116
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
117
+ throw error;
118
+ }
119
+ }
120
+
121
+ const extract = options.appImageExtract ?? defaultExtractLinuxAppImage;
122
+ return extract(appPath, outputRoot);
123
+ }
124
+
69
125
  async function resolveProductJsonPath(appPath: string, options: FindCursorOptions) {
70
126
  if (options.productRelativePath) {
71
127
  return join(appPath, options.productRelativePath);
@@ -55,7 +55,7 @@ async function exists(path: string) {
55
55
  function buildRuntimeManifest(sourceManifest: Record<string, unknown>) {
56
56
  return {
57
57
  name: 'cursorpool',
58
- version: typeof sourceManifest.version === 'string' ? sourceManifest.version : '0.5.6',
58
+ version: typeof sourceManifest.version === 'string' ? sourceManifest.version : '0.5.9',
59
59
  displayName: 'Cursor Pool 平台模式',
60
60
  publisher: 'cursor-pool',
61
61
  type: sourceManifest.type,
@@ -4,7 +4,7 @@ import { basename, dirname, join, resolve } from 'node:path';
4
4
  import { resolveExtensionInstallPath } from './extensionBundle';
5
5
  import type { ExtensionState } from './trial';
6
6
 
7
- export const LINKED_EXTENSION_DIRNAME = 'cursor-pool.extension-0.5.6';
7
+ export const LINKED_EXTENSION_DIRNAME = 'cursor-pool.extension-0.5.9';
8
8
  const RUNTIME_EXTENSION_ID = 'cursor-pool.cursorpool';
9
9
  const STALE_EXTENSION_IDS = new Set([
10
10
  RUNTIME_EXTENSION_ID,
@@ -48,7 +48,7 @@ async function readRuntimeManifest(linkedPath: string) {
48
48
  };
49
49
  const publisher = typeof manifest.publisher === 'string' ? manifest.publisher : 'cursor-pool';
50
50
  const name = typeof manifest.name === 'string' ? manifest.name : 'cursorpool';
51
- const version = typeof manifest.version === 'string' ? manifest.version : '0.5.6';
51
+ const version = typeof manifest.version === 'string' ? manifest.version : '0.5.9';
52
52
  return { id: `${publisher}.${name}`, version };
53
53
  }
54
54
 
@@ -100,7 +100,7 @@ function assertSafeLinkedExtensionPath(linkedPath: string) {
100
100
  }
101
101
 
102
102
  export function linkedExtensionPathForDir(cursorExtensionsDir: string) {
103
- return join(resolveExtensionInstallPath(cursorExtensionsDir), LINKED_EXTENSION_DIRNAME);
103
+ return join(resolve(resolveExtensionInstallPath(cursorExtensionsDir)), LINKED_EXTENSION_DIRNAME);
104
104
  }
105
105
 
106
106
  export async function getLinkedExtensionState(linkedPath: string | undefined): Promise<LinkedExtensionState> {
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,
@@ -100,10 +100,16 @@ export type InstallOptions = FindCursorOptions &
100
100
  restoreCursorWorkbenchAuthGate?: typeof restoreCursorWorkbenchAuthGate;
101
101
  adHocResignApp?: typeof adHocResignApp;
102
102
  fetchHealth?: (url: string) => Promise<{ ok: boolean; healthy: boolean }>;
103
+ linuxLauncherFile?: string;
103
104
  };
104
105
 
105
106
  const REAL_CURSOR_EXTENSIONS_DIR = '~/.cursor/extensions';
106
- const PACKAGE_VERSION = '0.5.6';
107
+ const PACKAGE_VERSION = '0.5.9';
108
+ const AUTOSTART_SERVICE_STARTUP_TIMEOUT_MS = 30_000;
109
+
110
+ function delay(ms: number) {
111
+ return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
112
+ }
107
113
 
108
114
  async function maybeAdHocResign({
109
115
  appPath,
@@ -135,27 +141,73 @@ async function assertExpectedHash(targetPath: string, expectedSha256: string) {
135
141
  }
136
142
  }
137
143
 
138
- async function startInstallService(options: InstallOptions) {
144
+ async function startServiceFromAutostart(options: InstallOptions, targetMode: 'real' | 'disposable') {
145
+ const autostart = await installAutostart(options, targetMode);
146
+ const runtimeFile = options.runtimeFile ?? DEFAULT_RUNTIME_FILE;
147
+ const fetchHealthFn = options.fetchHealth ?? fetchHealth;
148
+ const startedAt = Date.now();
149
+ let runtime: RuntimeInfo | null = null;
150
+
151
+ while (Date.now() - startedAt < AUTOSTART_SERVICE_STARTUP_TIMEOUT_MS) {
152
+ runtime = await readRuntimeInfo({ runtimeFile });
153
+ const health = runtime
154
+ ? await fetchHealthFn(`http://${runtime.host}:${runtime.port}/health`)
155
+ : null;
156
+ if (runtime && health?.ok && health.healthy) {
157
+ return {
158
+ service: {
159
+ ...runtime,
160
+ stop: () => stopRuntimeService(runtimeFile),
161
+ },
162
+ autostartState: autostart.state,
163
+ };
164
+ }
165
+ await delay(50);
166
+ }
167
+
168
+ throw new Error('Autostart service failed to become healthy');
169
+ }
170
+
171
+ async function startInstallService(
172
+ options: InstallOptions,
173
+ targetMode: 'real' | 'disposable',
174
+ platform: NodeJS.Platform,
175
+ ) {
139
176
  if (options.stopServiceAfterInstall) {
140
177
  const service = await startServer({ runtimeFile: options.runtimeFile });
141
178
  return {
142
- host: service.host,
143
- port: service.port,
144
- runtimeId: service.runtimeId,
145
- stop: service.stop,
179
+ service: {
180
+ host: service.host,
181
+ port: service.port,
182
+ runtimeId: service.runtimeId,
183
+ stop: service.stop,
184
+ },
185
+ autostartState: 'skipped' as const,
146
186
  };
147
187
  }
148
188
 
149
189
  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
- });
190
+ let runtime: RuntimeInfo;
191
+ try {
192
+ runtime = await startDetached({
193
+ runtimeFile: options.runtimeFile ?? DEFAULT_RUNTIME_FILE,
194
+ logFile: options.serviceLogFile ?? '~/.cursor-pool/logs/service.log',
195
+ configFile: options.clientConfigFile,
196
+ apiBaseUrl: options.apiBaseUrl,
197
+ });
198
+ } catch (error) {
199
+ if (platform === 'linux' && targetMode === 'real') {
200
+ return startServiceFromAutostart(options, targetMode);
201
+ }
202
+ throw error;
203
+ }
204
+
156
205
  return {
157
- ...runtime,
158
- stop: () => stopRuntimeService(options.runtimeFile ?? DEFAULT_RUNTIME_FILE),
206
+ service: {
207
+ ...runtime,
208
+ stop: () => stopRuntimeService(options.runtimeFile ?? DEFAULT_RUNTIME_FILE),
209
+ },
210
+ autostartState: undefined,
159
211
  };
160
212
  }
161
213
 
@@ -184,6 +236,43 @@ async function installAutostart(options: InstallOptions, targetMode: 'real' | 'd
184
236
  });
185
237
  }
186
238
 
239
+ function defaultLinuxLauncherFile() {
240
+ return join(homedir(), '.local/bin/cursor-pool-cursor');
241
+ }
242
+
243
+ function shellSingleQuote(value: string) {
244
+ return `'${value.replaceAll("'", "'\\''")}'`;
245
+ }
246
+
247
+ async function installLinuxLauncher({
248
+ appPath,
249
+ platform,
250
+ launcherFile,
251
+ }: {
252
+ appPath: string;
253
+ platform: NodeJS.Platform;
254
+ launcherFile?: string;
255
+ }) {
256
+ if (platform !== 'linux') {
257
+ return 'skipped';
258
+ }
259
+
260
+ const targetFile = launcherFile ?? defaultLinuxLauncherFile();
261
+ await mkdir(dirname(targetFile), { recursive: true });
262
+ await writeFile(
263
+ targetFile,
264
+ [
265
+ '#!/usr/bin/env sh',
266
+ 'set -eu',
267
+ `exec ${shellSingleQuote(join(appPath, 'AppRun'))} --no-sandbox "$@"`,
268
+ '',
269
+ ].join('\n'),
270
+ 'utf8',
271
+ );
272
+ await chmod(targetFile, 0o755);
273
+ return targetFile;
274
+ }
275
+
187
276
  function sameRuntime(a: RuntimeInfo | null | undefined, b: RuntimeInfo | null | undefined) {
188
277
  return Boolean(
189
278
  a &&
@@ -317,7 +406,9 @@ export async function install(options: InstallOptions = {}) {
317
406
  const compat = resolveCompatEntry(cursor, environment, { entries: compatEntries });
318
407
  const targetPath = join(cursor.appPath, compat.targetRelativePath);
319
408
  const originalSha256 = await sha256File(targetPath);
320
- await assertExpectedHash(targetPath, compat.expectedSha256);
409
+ if (compat.expectedSha256 !== '*') {
410
+ await assertExpectedHash(targetPath, compat.expectedSha256);
411
+ }
321
412
  const patchSetBeforeInstall = await readCursorPatchSetState(cursor.appPath, {
322
413
  agentExecTargetRelativePath: compat.targetRelativePath,
323
414
  platform: environment.platform,
@@ -348,6 +439,7 @@ export async function install(options: InstallOptions = {}) {
348
439
  let resignState: 'ad-hoc' | 'skipped' = 'skipped';
349
440
  let patchState = 'skipped';
350
441
  let autostartState = 'skipped';
442
+ let launcherState = 'skipped';
351
443
 
352
444
  try {
353
445
  if (target.requiresConfirmation) {
@@ -404,8 +496,9 @@ export async function install(options: InstallOptions = {}) {
404
496
  }
405
497
 
406
498
  await writeInstallClientConfig(options);
407
- service = await startInstallService(options);
408
- autostartState = (await installAutostart(options, target.mode)).state;
499
+ const started = await startInstallService(options, target.mode, environment.platform);
500
+ service = started.service;
501
+ autostartState = started.autostartState ?? (await installAutostart(options, target.mode)).state;
409
502
  const patchSet = await patchCursorSet(cursor.appPath, {
410
503
  backupDir: options.backupDir,
411
504
  platform: environment.platform,
@@ -421,6 +514,11 @@ export async function install(options: InstallOptions = {}) {
421
514
  requiresAdHocResign: compat.requiresAdHocResign,
422
515
  resign: options.adHocResignApp ?? adHocResignApp,
423
516
  });
517
+ launcherState = await installLinuxLauncher({
518
+ appPath: cursor.appPath,
519
+ platform: environment.platform,
520
+ launcherFile: options.linuxLauncherFile,
521
+ });
424
522
 
425
523
  const health = await (options.fetchHealth ?? fetchHealth)(
426
524
  `http://${service.host}:${service.port}/health`,
@@ -483,6 +581,7 @@ export async function install(options: InstallOptions = {}) {
483
581
  `autostart: ${autostartState}`,
484
582
  `patch: ${patchState}`,
485
583
  `resign: ${resignState}`,
584
+ `launcher: ${launcherState}`,
486
585
  'health: ok',
487
586
  target.mode === 'real' ? 'install-record: recorded' : 'trial: recorded',
488
587
  ].join('\n');
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.9',
86
+ serviceVersion: '0.5.9',
87
+ extensionVersion: '0.5.9',
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.9';
73
73
 
74
74
  function normalizeAppPath(path: string) {
75
75
  return normalize(path).replace(/\/+$/, '');
@@ -256,6 +256,7 @@ export async function repair(options: RepairOptions = {}) {
256
256
  installRecord.originalSha256 && patchState.currentHash === installRecord.originalSha256;
257
257
  if (
258
258
  !patchState.legacyMarkerPresent &&
259
+ compat.expectedSha256 !== '*' &&
259
260
  patchState.currentHash !== compat.expectedSha256 &&
260
261
  !matchesInstallOriginal
261
262
  ) {
@@ -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/trial.ts CHANGED
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import { realpathSync } from 'node:fs';
4
4
  import { homedir } from 'node:os';
5
- import { dirname, join, normalize } from 'node:path';
5
+ import { dirname, join, normalize, resolve } from 'node:path';
6
6
  import { DEFAULT_MACOS_CURSOR_APP_PATH } from './cursor';
7
7
 
8
8
  export type ExtensionState = 'bundled' | 'linked' | 'manual-step-required' | 'missing';
@@ -39,7 +39,7 @@ function resolveHomePath(path: string) {
39
39
  }
40
40
 
41
41
  function normalizeAppPath(path: string) {
42
- return normalize(path).replace(/\/+$/, '');
42
+ return resolve(normalize(path).replace(/\/+$/, ''));
43
43
  }
44
44
 
45
45
  function isRealCursorAppPath(path: string) {
@@ -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
+ });
@@ -46,7 +46,7 @@ function signedEnvelope(version: number, rules: CompatibilityManifestEntry[]) {
46
46
  };
47
47
  }
48
48
 
49
- test('default compatibility manifest supports Cursor 3.6.21 on macOS arm64', () => {
49
+ test('default compatibility manifest supports the Cursor 3.6 macOS arm64 version family', () => {
50
50
  const entry = resolveCompatEntry(
51
51
  {
52
52
  appPath: '/Users/example/Desktop/Cursor-Pool-Agent-Canary.app',
@@ -63,20 +63,120 @@ test('default compatibility manifest supports Cursor 3.6.21 on macOS arm64', ()
63
63
  assert.equal(entry.supportStatus, 'supported');
64
64
  assert.equal(entry.requiresAdHocResign, true);
65
65
  assert.equal(entry.targetRelativePath, CURSOR_AGENT_EXEC_RELATIVE_PATH);
66
+ assert.equal(entry.cursorVersion, '3.6');
67
+ assert.equal(entry.cursorCommit, '*');
68
+ assert.equal(entry.expectedSha256, '*');
66
69
  assert.equal(
67
- entry.expectedSha256,
68
- '222512631b78fddcdca3fa76c0dd458a7a86751dde19c998ceb31b3fe1905ebf',
70
+ DEFAULT_COMPAT_ENTRIES.some(
71
+ (candidate) =>
72
+ candidate.platform === 'darwin' &&
73
+ candidate.arch === 'arm64' &&
74
+ candidate.cursorVersion === '3.6' &&
75
+ candidate.cursorCommit === '*',
76
+ ),
77
+ true,
69
78
  );
79
+ });
80
+
81
+ test('default compatibility manifest supports the Cursor 3.4 macOS arm64 version family', () => {
82
+ const entry = resolveCompatEntry(
83
+ {
84
+ appPath: '/Users/example/Desktop/Cursor-3.4.0.app',
85
+ version: '3.4.0',
86
+ commit: 'cursor-3-4-family-commit',
87
+ },
88
+ {
89
+ platform: 'darwin',
90
+ arch: 'arm64',
91
+ nodeVersion: process.version,
92
+ },
93
+ );
94
+
95
+ assert.equal(entry.supportStatus, 'supported');
96
+ assert.equal(entry.requiresAdHocResign, true);
97
+ assert.equal(entry.targetRelativePath, CURSOR_AGENT_EXEC_RELATIVE_PATH);
98
+ assert.equal(entry.cursorVersion, '3.4');
99
+ assert.equal(entry.cursorCommit, '*');
100
+ assert.equal(entry.expectedSha256, '*');
70
101
  assert.equal(
71
102
  DEFAULT_COMPAT_ENTRIES.some(
72
103
  (candidate) =>
73
- candidate.cursorVersion === '3.6.21' &&
74
- candidate.cursorCommit === 'e7a7e93f4d75f8272503ecf33cedbaae10114a10',
104
+ candidate.platform === 'darwin' &&
105
+ candidate.arch === 'arm64' &&
106
+ candidate.cursorVersion === '3.4' &&
107
+ candidate.cursorCommit === '*',
75
108
  ),
76
109
  true,
77
110
  );
78
111
  });
79
112
 
113
+ test('default compatibility manifest accepts later Cursor 3.6 patch versions on macOS arm64', () => {
114
+ const entry = resolveCompatEntry(
115
+ {
116
+ appPath: '/Users/example/Desktop/Cursor-3.6.31.app',
117
+ version: '3.6.31',
118
+ commit: '81fcf2931d7687b4ff3f3017858d0c6dee7e2a60',
119
+ },
120
+ {
121
+ platform: 'darwin',
122
+ arch: 'arm64',
123
+ nodeVersion: process.version,
124
+ },
125
+ );
126
+
127
+ assert.equal(entry.supportStatus, 'supported');
128
+ assert.equal(entry.cursorVersion, '3.6');
129
+ assert.equal(entry.cursorCommit, '*');
130
+ assert.equal(entry.expectedSha256, '*');
131
+ });
132
+
133
+ test('default compatibility manifest supports the Cursor 3.7 macOS arm64 version family', () => {
134
+ const entry = resolveCompatEntry(
135
+ {
136
+ appPath: '/Users/example/Desktop/Cursor-3.7.12.app',
137
+ version: '3.7.12',
138
+ commit: 'b887a26c4f70bd8136bfffeda812b24194ec9ce0',
139
+ },
140
+ {
141
+ platform: 'darwin',
142
+ arch: 'arm64',
143
+ nodeVersion: process.version,
144
+ },
145
+ );
146
+
147
+ assert.equal(entry.supportStatus, 'supported');
148
+ assert.equal(entry.targetRelativePath, CURSOR_AGENT_EXEC_RELATIVE_PATH);
149
+ assert.equal(entry.cursorVersion, '3.7');
150
+ assert.equal(entry.cursorCommit, '*');
151
+ assert.equal(entry.expectedSha256, '*');
152
+ });
153
+
154
+ test('default compatibility manifest supports Cursor 3.6.31 Linux x64 AppImage', () => {
155
+ const entry = resolveCompatEntry(
156
+ {
157
+ appPath: '/home/xubuntu/.cursor-pool/appimages/Cursor-89aa899ccc46fb58/squashfs-root',
158
+ version: '3.6.31',
159
+ commit: '81fcf2931d7687b4ff3f3017858d0c6dee7e2a60',
160
+ },
161
+ {
162
+ platform: 'linux',
163
+ arch: 'x64',
164
+ nodeVersion: process.version,
165
+ },
166
+ );
167
+
168
+ assert.equal(entry.supportStatus, 'supported');
169
+ assert.equal(entry.requiresAdHocResign, false);
170
+ assert.equal(
171
+ entry.targetRelativePath,
172
+ 'usr/share/cursor/resources/app/extensions/cursor-agent-exec/dist/main.js',
173
+ );
174
+ assert.equal(
175
+ entry.expectedSha256,
176
+ '05bfa29eacb8271c378765ead4bf881f806b97549dd13367183aa7a9331c1131',
177
+ );
178
+ });
179
+
80
180
  test('default compatibility manifest supports Cursor 3.5.38 on macOS x64 for Rosetta-launched installers', () => {
81
181
  const entry = resolveCompatEntry(
82
182
  {
@@ -94,10 +194,9 @@ test('default compatibility manifest supports Cursor 3.5.38 on macOS x64 for Ros
94
194
  assert.equal(entry.supportStatus, 'supported');
95
195
  assert.equal(entry.requiresAdHocResign, true);
96
196
  assert.equal(entry.targetRelativePath, CURSOR_AGENT_EXEC_RELATIVE_PATH);
97
- assert.equal(
98
- entry.expectedSha256,
99
- 'cb18f0237278884a39e2ce2b8664255e12689ad0803c20096c38e86c36acc51f',
100
- );
197
+ assert.equal(entry.cursorVersion, '3.5');
198
+ assert.equal(entry.cursorCommit, '*');
199
+ assert.equal(entry.expectedSha256, '*');
101
200
  });
102
201
 
103
202
  test('verifies a remote compatibility manifest envelope with the development signature', () => {
@@ -59,6 +59,7 @@ test('CLI common options include target, record, trial, extension, and service l
59
59
  cursorExtensionsDir: '/tmp/Cursor-Pool-Trial-Extensions',
60
60
  userDataDir: '/tmp/Cursor-Pool-Trial-UserData',
61
61
  serviceLogFile: '/tmp/service.log',
62
+ configFile: undefined,
62
63
  diagnosticsFile: '/tmp/diagnostics.jsonl',
63
64
  sessionFile: '/tmp/session.json',
64
65
  code: 'CODE-123',
@@ -1,6 +1,9 @@
1
1
  import assert from 'node:assert/strict';
2
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
2
5
  import test from 'node:test';
3
- import { defaultCursorAppPath } from '../src/cursor';
6
+ import { defaultCursorAppPath, findCursor } from '../src/cursor';
4
7
 
5
8
  test('defaultCursorAppPath resolves the user-level Windows Cursor install directory', () => {
6
9
  assert.equal(
@@ -18,3 +21,59 @@ test('defaultCursorAppPath gives a helpful Windows error when LOCALAPPDATA is un
18
21
  /LOCALAPPDATA is required to auto-detect Cursor on Windows/,
19
22
  );
20
23
  });
24
+
25
+ test('findCursor extracts Linux AppImage files before reading product metadata', async () => {
26
+ const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-linux-appimage-'));
27
+ const appImagePath = join(tempDir, 'Cursor.AppImage');
28
+ const extractedRoots: string[] = [];
29
+
30
+ try {
31
+ await writeFile(appImagePath, '#!/bin/sh\n', { mode: 0o755 });
32
+
33
+ const cursor = await findCursor({
34
+ appPath: appImagePath,
35
+ platform: 'linux',
36
+ appImageExtract: async (path, outputRoot) => {
37
+ assert.equal(path, appImagePath);
38
+ extractedRoots.push(outputRoot);
39
+ const extractedPath = join(outputRoot, 'squashfs-root');
40
+ await mkdir(join(extractedPath, 'usr/share/cursor/resources/app'), { recursive: true });
41
+ await writeFile(
42
+ join(extractedPath, 'usr/share/cursor/resources/app/product.json'),
43
+ JSON.stringify({ version: '3.6.31', commit: 'linux-commit' }),
44
+ 'utf8',
45
+ );
46
+ return extractedPath;
47
+ },
48
+ });
49
+
50
+ assert.equal(cursor.version, '3.6.31');
51
+ assert.equal(cursor.commit, 'linux-commit');
52
+ assert.match(cursor.appPath, /squashfs-root$/);
53
+ assert.equal(extractedRoots.length, 1);
54
+ } finally {
55
+ await rm(tempDir, { recursive: true, force: true });
56
+ }
57
+ });
58
+
59
+ test('findCursor keeps macOS app bundle product path unchanged', async () => {
60
+ const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-macos-app-'));
61
+ const appPath = join(tempDir, 'Cursor.app');
62
+
63
+ try {
64
+ await mkdir(join(appPath, 'Contents/Resources/app'), { recursive: true });
65
+ await writeFile(
66
+ join(appPath, 'Contents/Resources/app/product.json'),
67
+ JSON.stringify({ version: '3.5.38', commit: 'mac-commit' }),
68
+ 'utf8',
69
+ );
70
+
71
+ const cursor = await findCursor({ appPath, platform: 'darwin' });
72
+
73
+ assert.equal(cursor.appPath, appPath);
74
+ assert.equal(cursor.version, '3.5.38');
75
+ assert.equal(cursor.commit, 'mac-commit');
76
+ } finally {
77
+ await rm(tempDir, { recursive: true, force: true });
78
+ }
79
+ });