@cursorpool-dev/cli 0.5.9 → 0.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/patchSet.ts CHANGED
@@ -54,13 +54,43 @@ const LINUX_WORKBENCH_RELATIVE_PATH =
54
54
  'usr/share/cursor/resources/app/out/vs/workbench/workbench.desktop.main.js';
55
55
  const LINUX_ALWAYS_LOCAL_RELATIVE_PATH =
56
56
  'usr/share/cursor/resources/app/extensions/cursor-always-local/dist/main.js';
57
+ const LINUX_DEB_WORKBENCH_RELATIVE_PATH =
58
+ 'resources/app/out/vs/workbench/workbench.desktop.main.js';
59
+ const LINUX_DEB_ALWAYS_LOCAL_RELATIVE_PATH =
60
+ 'resources/app/extensions/cursor-always-local/dist/main.js';
57
61
 
58
- function workbenchRelativePath(options: { platform?: NodeJS.Platform; workbenchTargetRelativePath?: string }) {
59
- return options.workbenchTargetRelativePath ?? (options.platform === 'linux' ? LINUX_WORKBENCH_RELATIVE_PATH : CURSOR_WORKBENCH_RELATIVE_PATH);
62
+ function linuxUsesDebRoot(options: { agentExecTargetRelativePath?: string }) {
63
+ return options.agentExecTargetRelativePath?.startsWith('resources/app/') ?? false;
60
64
  }
61
65
 
62
- function alwaysLocalRelativePath(options: { platform?: NodeJS.Platform; alwaysLocalTargetRelativePath?: string }) {
63
- return options.alwaysLocalTargetRelativePath ?? (options.platform === 'linux' ? LINUX_ALWAYS_LOCAL_RELATIVE_PATH : CURSOR_ALWAYS_LOCAL_RELATIVE_PATH);
66
+ function workbenchRelativePath(options: {
67
+ platform?: NodeJS.Platform;
68
+ agentExecTargetRelativePath?: string;
69
+ workbenchTargetRelativePath?: string;
70
+ }) {
71
+ if (options.workbenchTargetRelativePath) {
72
+ return options.workbenchTargetRelativePath;
73
+ }
74
+ if (options.platform === 'linux') {
75
+ return linuxUsesDebRoot(options) ? LINUX_DEB_WORKBENCH_RELATIVE_PATH : LINUX_WORKBENCH_RELATIVE_PATH;
76
+ }
77
+ return CURSOR_WORKBENCH_RELATIVE_PATH;
78
+ }
79
+
80
+ function alwaysLocalRelativePath(options: {
81
+ platform?: NodeJS.Platform;
82
+ agentExecTargetRelativePath?: string;
83
+ alwaysLocalTargetRelativePath?: string;
84
+ }) {
85
+ if (options.alwaysLocalTargetRelativePath) {
86
+ return options.alwaysLocalTargetRelativePath;
87
+ }
88
+ if (options.platform === 'linux') {
89
+ return linuxUsesDebRoot(options)
90
+ ? LINUX_DEB_ALWAYS_LOCAL_RELATIVE_PATH
91
+ : LINUX_ALWAYS_LOCAL_RELATIVE_PATH;
92
+ }
93
+ return CURSOR_ALWAYS_LOCAL_RELATIVE_PATH;
64
94
  }
65
95
 
66
96
  async function fileContainsMarker(
@@ -143,6 +173,11 @@ export async function patchCursorSet(appPath: string, options: PatchCursorSetOpt
143
173
  }
144
174
 
145
175
  export async function restoreCursorSet(appPath: string, options: RestoreCursorSetOptions = {}) {
176
+ const before = await readCursorPatchSetState(appPath, {
177
+ agentExecTargetRelativePath: options.agentExecTargetRelativePath,
178
+ platform: options.platform,
179
+ workbenchTargetRelativePath: options.workbenchTargetRelativePath,
180
+ });
146
181
  const restoreErrors: unknown[] = [];
147
182
  const restore = async (operation: () => Promise<unknown>) => {
148
183
  try {
@@ -173,10 +208,17 @@ export async function restoreCursorSet(appPath: string, options: RestoreCursorSe
173
208
  await restore(() =>
174
209
  (options.restoreCursorAgentExec ?? restoreCursorAgentExec)(appPath, {
175
210
  backupDir: options.backupDir,
211
+ targetRelativePath: options.agentExecTargetRelativePath,
176
212
  }),
177
213
  );
178
214
 
179
215
  if (restoreErrors.length > 0) {
180
216
  throw new AggregateError(restoreErrors, 'Failed to restore one or more Cursor patches.');
181
217
  }
218
+ const after = await readCursorPatchSetState(appPath, {
219
+ agentExecTargetRelativePath: options.agentExecTargetRelativePath,
220
+ platform: options.platform,
221
+ workbenchTargetRelativePath: options.workbenchTargetRelativePath,
222
+ });
223
+ return { before, after };
182
224
  }
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.9',
86
- serviceVersion: '0.5.9',
87
- extensionVersion: '0.5.9',
85
+ cliVersion: '0.5.10',
86
+ serviceVersion: '0.5.10',
87
+ extensionVersion: '0.5.10',
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.9';
72
+ const PACKAGE_VERSION = '0.5.10';
73
73
 
74
74
  function normalizeAppPath(path: string) {
75
75
  return normalize(path).replace(/\/+$/, '');
@@ -110,12 +110,14 @@ function resolveRepairCompatEntry({
110
110
  entries?: CompatibilityManifestEntry[];
111
111
  }) {
112
112
  const compatEntries = entries ?? DEFAULT_COMPAT_ENTRIES;
113
+ const versionFamily = cursorVersion.match(/^(\d+\.\d+)(?:\.|$)/)?.[1];
113
114
  const entry = compatEntries.find(
114
115
  (candidate) =>
115
116
  candidate.platform === platform &&
116
117
  candidate.arch === arch &&
117
- candidate.cursorVersion === cursorVersion &&
118
- candidate.cursorCommit === cursorCommit,
118
+ (candidate.cursorVersion === cursorVersion ||
119
+ candidate.cursorVersion === versionFamily) &&
120
+ (candidate.cursorCommit === '*' || candidate.cursorCommit === cursorCommit),
119
121
  );
120
122
 
121
123
  if (!entry) {
@@ -196,13 +198,13 @@ async function maybeAdHocResign({
196
198
  }
197
199
 
198
200
  export async function repair(options: RepairOptions = {}) {
199
- const target = resolveCursorTarget(options);
201
+ const environment = detectEnvironment(options);
202
+ const target = resolveCursorTarget({ ...options, platform: environment.platform });
200
203
  if (target.mode !== 'real') {
201
204
  throw new Error('repair is only supported for real Cursor installs in MVP-1');
202
205
  }
203
206
 
204
207
  const cursor = await findCursor({ ...options, appPath: target.appPath });
205
- const environment = detectEnvironment(options);
206
208
  const compatEntries = await loadCompatEntries({
207
209
  compatEntries: options.compatEntries,
208
210
  apiBaseUrl: options.apiBaseUrl,
@@ -323,6 +325,7 @@ export async function repair(options: RepairOptions = {}) {
323
325
  }
324
326
 
325
327
  const patchSetState = await readCursorPatchSetState(cursor.appPath, {
328
+ platform: environment.platform,
326
329
  agentExecTargetRelativePath: compat.targetRelativePath,
327
330
  });
328
331
 
@@ -332,6 +335,7 @@ export async function repair(options: RepairOptions = {}) {
332
335
  } else {
333
336
  const repairedPatchSet = await patchCursorSet(cursor.appPath, {
334
337
  backupDir,
338
+ platform: environment.platform,
335
339
  agentExecTargetRelativePath: compat.targetRelativePath,
336
340
  patchCursorAgentExec: options.patchCursorAgentExec,
337
341
  patchCursorWorkbenchAuthGate: options.patchCursorWorkbenchAuthGate,
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,
@@ -7,6 +7,7 @@ import {
7
7
  type ConfirmRealOperationOptions,
8
8
  } from './confirm';
9
9
  import { findCursor, type FindCursorOptions } from './cursor';
10
+ import { detectEnvironment, type DetectEnvironmentOptions } from './environment';
10
11
  import {
11
12
  installIdForAppPath,
12
13
  readInstallRecord,
@@ -16,8 +17,10 @@ import {
16
17
  import { stopRuntimeService } from './serviceProcess';
17
18
  import { resolveCursorTarget, type CursorTargetOptions } from './target';
18
19
  import { assertDisposableCursorAppPath, readTrialRecord, type TrialRecordOptions } from './trial';
20
+ import { formatCursorPatchSetState, restoreCursorSet } from './patchSet';
19
21
 
20
22
  export type RestoreOptions = FindCursorOptions &
23
+ DetectEnvironmentOptions &
21
24
  CursorTargetOptions &
22
25
  TrialRecordOptions &
23
26
  InstallRecordOptions & {
@@ -85,9 +88,12 @@ function formatRestoreConfirmation({
85
88
  }
86
89
 
87
90
  export async function restore(options: RestoreOptions = {}) {
88
- const target = resolveCursorTarget(options);
91
+ const environment = detectEnvironment(options);
92
+ const target = resolveCursorTarget({ ...options, platform: environment.platform });
89
93
  const appPath =
90
- target.mode === 'disposable' ? assertDisposableCursorAppPath(target.appPath) : target.appPath;
94
+ target.mode === 'disposable'
95
+ ? assertDisposableCursorAppPath(target.appPath, { platform: environment.platform })
96
+ : target.appPath;
91
97
  const cursor = await findCursor({ ...options, appPath });
92
98
  const installRecord =
93
99
  target.mode === 'real'
@@ -132,9 +138,10 @@ export async function restore(options: RestoreOptions = {}) {
132
138
  });
133
139
  }
134
140
 
135
- const result = await restoreCursorAgentExec(cursor.appPath, {
141
+ const result = await restoreCursorSet(cursor.appPath, {
136
142
  backupDir,
137
- targetRelativePath,
143
+ agentExecTargetRelativePath: targetRelativePath,
144
+ platform: environment.platform,
138
145
  });
139
146
  if (target.mode === 'real') {
140
147
  await stopRuntimeServiceIfRecorded(runtimeFile);
@@ -145,7 +152,7 @@ export async function restore(options: RestoreOptions = {}) {
145
152
  `mode: ${target.mode}`,
146
153
  `app: ${cursor.appPath}`,
147
154
  'restore: ok',
148
- `patch: ${result.markerPresent ? 'applied' : 'missing'}`,
155
+ `patch: ${formatCursorPatchSetState(result.after)}`,
149
156
  target.mode === 'real'
150
157
  ? `install-record: ${realInstallRecord ? 'recorded' : 'missing'}`
151
158
  : `trial: ${trialRecord ? 'recorded' : 'missing'}`,
package/src/status.ts CHANGED
@@ -146,10 +146,12 @@ async function getTakeoverStatus(
146
146
  }
147
147
 
148
148
  export async function status(options: StatusOptions = {}) {
149
- const target = resolveCursorTarget(options);
150
- const appPath =
151
- target.mode === 'disposable' ? assertDisposableCursorAppPath(target.appPath) : target.appPath;
152
149
  const environment = detectEnvironment(options);
150
+ const target = resolveCursorTarget({ ...options, platform: environment.platform });
151
+ const appPath =
152
+ target.mode === 'disposable'
153
+ ? assertDisposableCursorAppPath(target.appPath, { platform: environment.platform })
154
+ : target.appPath;
153
155
  const cursor = await findCursor({ ...options, appPath });
154
156
  const compatEntries = await loadCompatEntries({
155
157
  compatEntries: options.compatEntries,
package/src/target.ts CHANGED
@@ -20,8 +20,20 @@ export type CursorTarget =
20
20
  requiresConfirmation: true;
21
21
  };
22
22
 
23
+ function isLinuxRealCursorPath(appPath: string, platform: NodeJS.Platform | undefined) {
24
+ return platform === 'linux' && appPath.replace(/\/+$/, '') === '/usr/share/cursor';
25
+ }
26
+
23
27
  export function resolveCursorTarget(options: CursorTargetOptions = {}): CursorTarget {
24
28
  if (options.appPath) {
29
+ if (isLinuxRealCursorPath(options.appPath, options.platform)) {
30
+ return {
31
+ mode: 'real',
32
+ appPath: options.appPath.replace(/\/+$/, ''),
33
+ requiresConfirmation: true,
34
+ };
35
+ }
36
+
25
37
  return {
26
38
  mode: 'disposable',
27
39
  appPath: assertDisposableCursorAppPath(options.appPath, { platform: options.platform }),
package/src/uninstall.ts CHANGED
@@ -28,6 +28,7 @@ import { restore } from './restore';
28
28
  import type { RestoreOptions } from './restore';
29
29
  import { stopRuntimeService } from './serviceProcess';
30
30
  import { removeUserAutostart } from './autostart';
31
+ import { detectEnvironment } from './environment';
31
32
  import { resolveCursorTarget, type CursorTargetOptions } from './target';
32
33
  import {
33
34
  assertDisposableCursorAppPath,
@@ -107,9 +108,12 @@ async function removeRecordedLinkedExtensionBundle(
107
108
  }
108
109
 
109
110
  export async function uninstall(options: UninstallOptions = {}) {
110
- const target = resolveCursorTarget(options);
111
+ const environment = detectEnvironment(options);
112
+ const target = resolveCursorTarget({ ...options, platform: environment.platform });
111
113
  const appPath =
112
- target.mode === 'disposable' ? assertDisposableCursorAppPath(target.appPath) : target.appPath;
114
+ target.mode === 'disposable'
115
+ ? assertDisposableCursorAppPath(target.appPath, { platform: environment.platform })
116
+ : target.appPath;
113
117
  const cursor = await findCursor({ ...options, appPath });
114
118
  const installRecord =
115
119
  target.mode === 'real'
@@ -177,6 +177,96 @@ test('default compatibility manifest supports Cursor 3.6.31 Linux x64 AppImage',
177
177
  );
178
178
  });
179
179
 
180
+ test('default compatibility manifest supports Cursor 3.7.12 Linux arm64 deb installs', () => {
181
+ const entry = resolveCompatEntry(
182
+ {
183
+ appPath: '/usr/share/cursor',
184
+ version: '3.7.12',
185
+ commit: 'b887a26c4f70bd8136bfffeda812b24194ec9ce0',
186
+ },
187
+ {
188
+ platform: 'linux',
189
+ arch: 'arm64',
190
+ nodeVersion: process.version,
191
+ },
192
+ );
193
+
194
+ assert.equal(entry.supportStatus, 'supported');
195
+ assert.equal(entry.requiresAdHocResign, false);
196
+ assert.equal(
197
+ entry.targetRelativePath,
198
+ 'resources/app/extensions/cursor-agent-exec/dist/main.js',
199
+ );
200
+ assert.equal(
201
+ entry.expectedSha256,
202
+ '9ce7a2f40a98a27eb1b609a79e0e1707bad5fbb02493693f6f18945a7640dde4',
203
+ );
204
+ });
205
+
206
+ test('default compatibility manifest supports verified Cursor 3.4 Linux arm64 deb installs', () => {
207
+ const entry = resolveCompatEntry(
208
+ {
209
+ appPath: '/usr/share/cursor',
210
+ version: '3.4.20',
211
+ commit: '0cf8b06883f54e26bb4f0fb8647c9500ccb43310',
212
+ },
213
+ {
214
+ platform: 'linux',
215
+ arch: 'arm64',
216
+ nodeVersion: process.version,
217
+ },
218
+ );
219
+
220
+ assert.equal(entry.supportStatus, 'supported');
221
+ assert.equal(entry.requiresAdHocResign, false);
222
+ assert.equal(
223
+ entry.targetRelativePath,
224
+ 'resources/app/extensions/cursor-agent-exec/dist/main.js',
225
+ );
226
+ assert.equal(
227
+ entry.expectedSha256,
228
+ '4ccb7526e5ece8f6a98a99724a048977698cbf52cb2033ec06f2b5a0a085fe71',
229
+ );
230
+ });
231
+
232
+ test('default compatibility manifest supports verified Cursor 3.5 and 3.6 Linux arm64 deb installs', () => {
233
+ const cases = [
234
+ {
235
+ version: '3.5.38',
236
+ commit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
237
+ expectedSha256: 'cb18f0237278884a39e2ce2b8664255e12689ad0803c20096c38e86c36acc51f',
238
+ },
239
+ {
240
+ version: '3.6.31',
241
+ commit: '81fcf2931d7687b4ff3f3017858d0c6dee7e2a60',
242
+ expectedSha256: '05bfa29eacb8271c378765ead4bf881f806b97549dd13367183aa7a9331c1131',
243
+ },
244
+ ];
245
+
246
+ for (const fixture of cases) {
247
+ const entry = resolveCompatEntry(
248
+ {
249
+ appPath: '/usr/share/cursor',
250
+ version: fixture.version,
251
+ commit: fixture.commit,
252
+ },
253
+ {
254
+ platform: 'linux',
255
+ arch: 'arm64',
256
+ nodeVersion: process.version,
257
+ },
258
+ );
259
+
260
+ assert.equal(entry.supportStatus, 'supported');
261
+ assert.equal(entry.requiresAdHocResign, false);
262
+ assert.equal(
263
+ entry.targetRelativePath,
264
+ 'resources/app/extensions/cursor-agent-exec/dist/main.js',
265
+ );
266
+ assert.equal(entry.expectedSha256, fixture.expectedSha256);
267
+ }
268
+ });
269
+
180
270
  test('default compatibility manifest supports Cursor 3.5.38 on macOS x64 for Rosetta-launched installers', () => {
181
271
  const entry = resolveCompatEntry(
182
272
  {
@@ -22,6 +22,60 @@ test('defaultCursorAppPath gives a helpful Windows error when LOCALAPPDATA is un
22
22
  );
23
23
  });
24
24
 
25
+ test('defaultCursorAppPath auto-detects Linux deb installs from the cursor launcher on PATH', () => {
26
+ assert.equal(
27
+ defaultCursorAppPath({
28
+ platform: 'linux',
29
+ execFileSync: (command, args) => {
30
+ if (command === 'which' && args[0] === 'cursor') {
31
+ return '/usr/bin/cursor\n';
32
+ }
33
+ if (command === 'readlink' && args.join(' ') === '-f /usr/bin/cursor') {
34
+ return '/usr/share/cursor/bin/cursor\n';
35
+ }
36
+ throw new Error(`unexpected command: ${command} ${args.join(' ')}`);
37
+ },
38
+ }),
39
+ '/usr/share/cursor',
40
+ );
41
+ });
42
+
43
+ test('findCursor auto-detects Linux deb installs from the cursor launcher on PATH', async () => {
44
+ const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-linux-deb-'));
45
+ const appPath = join(tempDir, 'usr/share/cursor');
46
+ const launcherPath = join(appPath, 'bin/cursor');
47
+
48
+ try {
49
+ await mkdir(join(appPath, 'resources/app'), { recursive: true });
50
+ await mkdir(join(appPath, 'bin'), { recursive: true });
51
+ await writeFile(launcherPath, '#!/bin/sh\n', 'utf8');
52
+ await writeFile(
53
+ join(appPath, 'resources/app/product.json'),
54
+ JSON.stringify({ version: '3.7.12', commit: 'linux-arm64-commit' }),
55
+ 'utf8',
56
+ );
57
+
58
+ const cursor = await findCursor({
59
+ platform: 'linux',
60
+ execFile: async (command, args) => {
61
+ if (command === 'which' && args[0] === 'cursor') {
62
+ return { stdout: '/usr/bin/cursor\n' };
63
+ }
64
+ if (command === 'readlink' && args.join(' ') === '-f /usr/bin/cursor') {
65
+ return { stdout: `${launcherPath}\n` };
66
+ }
67
+ throw new Error(`unexpected command: ${command} ${args.join(' ')}`);
68
+ },
69
+ });
70
+
71
+ assert.equal(cursor.appPath, appPath);
72
+ assert.equal(cursor.version, '3.7.12');
73
+ assert.equal(cursor.commit, 'linux-arm64-commit');
74
+ } finally {
75
+ await rm(tempDir, { recursive: true, force: true });
76
+ }
77
+ });
78
+
25
79
  test('findCursor extracts Linux AppImage files before reading product metadata', async () => {
26
80
  const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-linux-appimage-'));
27
81
  const appImagePath = join(tempDir, 'Cursor.AppImage');
@@ -32,23 +32,59 @@ const cursorVersion = '3.5.38';
32
32
  const cursorCommit = '009bb5a3600dd98fe1c1f25798f767f686e14750';
33
33
  const targetRelativePath =
34
34
  'Contents/Resources/app/extensions/cursor-agent-exec/dist/main.js';
35
+ const alwaysLocalRelativePath =
36
+ 'Contents/Resources/app/extensions/cursor-always-local/dist/main.js';
37
+ const workbenchRelativePath =
38
+ 'Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js';
35
39
  const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../../..');
36
40
  const serviceServerPath = resolve(repoRoot, 'packages/service/src/server.ts');
41
+ const composerAuthGateAnchor =
42
+ 'get when(){return p()&&!fzC},get fallback(){return he(DGC,{})}';
43
+ const composerSubmitAuthGateAnchor =
44
+ 'if(!p()){e.cursorAuthenticationService.login(),e.commandService.executeCommand(wV,"general");return}';
45
+ const agentLoopRunAnchor =
46
+ 'await this.agentClientService.run(te,H,$e,Ne,Ce,T,ze,me,we,[],ct)';
47
+ const buildFlagsLocalModeAnchor = 'localMode:!1';
48
+ const localProviderConfigAnchor =
49
+ 'async getLocalAgentProviderConfig(e){const t="[AgentClientService][getLocalAgentProviderConfig]",i=L0.localMode?await this.shellEnvironmentService.getShellEnv():{},r=e?.credentials,s=r?.case==="apiKeyCredentials"?r.value:void 0,o=this.reactiveStorageService.applicationUserPersistentStorage,a=o.useOpenAIKey===!0?this.cursorAuthenticationService.openAIKey()??void 0:void 0,u=xgS({apiKeyCandidates:[{value:s?.apiKey,source:"modelDetails.apiKeyCredentials.apiKey"},{value:a,source:"storage.openAIKey"}],baseUrlCandidates:[{value:s?.baseUrl,source:"modelDetails.apiKeyCredentials.baseUrl"},{value:o.openAIBaseUrl,source:"storage.openAIBaseUrl"}]});return{baseUrl:u.baseUrl,apiKey:u.apiKey}}createDefaultLocalModel(e){return "default"}';
50
+ const agentClientRunLocalModeAnchor =
51
+ 'if(L0.localMode){try{g.onNetworkPhaseStart?.()}catch(b){this.logService.warn("[AgentClientService] onNetworkPhaseStart callback failed in local mode",b)}return this.runLocalAgentInExtensionHost(e,t,i,r,s,a,d,h,g)}return this.client.run(e,t,i,r,s,o,a,u,d,h,g)';
52
+
53
+ function workbenchFixture() {
54
+ return [
55
+ `function composer(){return he(Mt,{${composerAuthGateAnchor},get children(){return "controls"}})}`,
56
+ `async function submit(){${composerSubmitAuthGateAnchor};return "submitted";}`,
57
+ `const flags={${buildFlagsLocalModeAnchor}}`,
58
+ localProviderConfigAnchor,
59
+ `async function runAgentLoop(){${agentLoopRunAnchor}}`,
60
+ `async function agentClientRun(){const g={};${agentClientRunLocalModeAnchor}}`,
61
+ ].join(';');
62
+ }
37
63
 
38
64
  async function createFixtureApp(prefix: string) {
39
65
  const tempDir = await mkdtemp(join(tmpdir(), prefix));
40
66
  const appPath = join(tempDir, 'Cursor.app');
41
67
  const targetPath = join(appPath, targetRelativePath);
68
+ const alwaysLocalPath = join(appPath, alwaysLocalRelativePath);
69
+ const workbenchPath = join(appPath, workbenchRelativePath);
42
70
  const targetContent = `function main() { return "agent"; }\n${CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR}\nmain();\n`;
43
71
  await mkdir(join(appPath, 'Contents/Resources/app/extensions/cursor-agent-exec/dist'), {
44
72
  recursive: true,
45
73
  });
74
+ await mkdir(join(appPath, 'Contents/Resources/app/extensions/cursor-always-local/dist'), {
75
+ recursive: true,
76
+ });
77
+ await mkdir(join(appPath, 'Contents/Resources/app/out/vs/workbench'), {
78
+ recursive: true,
79
+ });
46
80
  await writeFile(
47
81
  join(appPath, 'Contents/Resources/app/product.json'),
48
82
  JSON.stringify({ version: cursorVersion, commit: cursorCommit }),
49
83
  'utf8',
50
84
  );
51
85
  await writeFile(targetPath, targetContent, 'utf8');
86
+ await writeFile(alwaysLocalPath, 'function alwaysLocal(){}\n', 'utf8');
87
+ await writeFile(workbenchPath, workbenchFixture(), 'utf8');
52
88
 
53
89
  const originalHash = createHash('sha256').update(targetContent).digest('hex');
54
90
  const compatEntry: CompatibilityManifestEntry = {
@@ -0,0 +1,71 @@
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';
5
+ import test from 'node:test';
6
+ import { readCursorPatchSetState } from '../src/patchSet';
7
+
8
+ async function withLinuxCursorApp(rootKind: 'deb' | 'appimage') {
9
+ const tempDir = await mkdtemp(join(tmpdir(), `cursor-pool-patchset-${rootKind}-`));
10
+ const appPath = rootKind === 'deb' ? join(tempDir, 'cursor') : join(tempDir, 'squashfs-root');
11
+ const resourceRoot =
12
+ rootKind === 'deb' ? join(appPath, 'resources/app') : join(appPath, 'usr/share/cursor/resources/app');
13
+ await mkdir(join(resourceRoot, 'extensions/cursor-agent-exec/dist'), { recursive: true });
14
+ await mkdir(join(resourceRoot, 'out/vs/workbench'), { recursive: true });
15
+ await writeFile(
16
+ join(resourceRoot, 'extensions/cursor-agent-exec/dist/main.js'),
17
+ 'function agent(){}\n',
18
+ 'utf8',
19
+ );
20
+ await writeFile(
21
+ join(resourceRoot, 'out/vs/workbench/workbench.desktop.main.js'),
22
+ 'function workbench(){}\n',
23
+ 'utf8',
24
+ );
25
+ return { appPath, tempDir, resourceRoot };
26
+ }
27
+
28
+ test('readCursorPatchSetState resolves Linux deb install patch targets from /usr/share/cursor root', async () => {
29
+ const fixture = await withLinuxCursorApp('deb');
30
+
31
+ try {
32
+ const state = await readCursorPatchSetState(fixture.appPath, {
33
+ platform: 'linux',
34
+ agentExecTargetRelativePath: 'resources/app/extensions/cursor-agent-exec/dist/main.js',
35
+ });
36
+
37
+ assert.equal(
38
+ state.patches.find((patch) => patch.name === 'agent-exec')?.targetPath,
39
+ join(fixture.resourceRoot, 'extensions/cursor-agent-exec/dist/main.js'),
40
+ );
41
+ assert.equal(
42
+ state.patches.find((patch) => patch.name === 'workbench')?.targetPath,
43
+ join(fixture.resourceRoot, 'out/vs/workbench/workbench.desktop.main.js'),
44
+ );
45
+ } finally {
46
+ await rm(fixture.tempDir, { recursive: true, force: true });
47
+ }
48
+ });
49
+
50
+ test('readCursorPatchSetState keeps Linux AppImage squashfs-root patch targets unchanged', async () => {
51
+ const fixture = await withLinuxCursorApp('appimage');
52
+
53
+ try {
54
+ const state = await readCursorPatchSetState(fixture.appPath, {
55
+ platform: 'linux',
56
+ agentExecTargetRelativePath:
57
+ 'usr/share/cursor/resources/app/extensions/cursor-agent-exec/dist/main.js',
58
+ });
59
+
60
+ assert.equal(
61
+ state.patches.find((patch) => patch.name === 'agent-exec')?.targetPath,
62
+ join(fixture.resourceRoot, 'extensions/cursor-agent-exec/dist/main.js'),
63
+ );
64
+ assert.equal(
65
+ state.patches.find((patch) => patch.name === 'workbench')?.targetPath,
66
+ join(fixture.resourceRoot, 'out/vs/workbench/workbench.desktop.main.js'),
67
+ );
68
+ } finally {
69
+ await rm(fixture.tempDir, { recursive: true, force: true });
70
+ }
71
+ });