@cursorpool-dev/cli 0.5.8 → 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.
Files changed (49) hide show
  1. package/node_modules/@cursor-pool/extension/dist/extension.js +46 -116
  2. package/node_modules/@cursor-pool/extension/package.json +3 -3
  3. package/node_modules/@cursor-pool/extension/src/api.ts +2 -17
  4. package/node_modules/@cursor-pool/extension/src/panel.ts +3 -26
  5. package/node_modules/@cursor-pool/extension/test/panel.test.ts +1 -34
  6. package/node_modules/@cursor-pool/patcher/package.json +2 -2
  7. package/node_modules/@cursor-pool/patcher/src/marker.ts +72 -7
  8. package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +102 -19
  9. package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +88 -13
  10. package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +162 -149
  11. package/node_modules/@cursor-pool/service/package.json +2 -2
  12. package/node_modules/@cursor-pool/service/src/platformSession.ts +7 -30
  13. package/node_modules/@cursor-pool/service/src/server.ts +1 -1
  14. package/node_modules/@cursor-pool/service/test/platformSession.test.ts +4 -5
  15. package/node_modules/@cursor-pool/service/test/server.test.ts +1 -130
  16. package/node_modules/@cursor-pool/shared/package.json +1 -1
  17. package/node_modules/@cursor-pool/shared/src/manifest.ts +0 -35
  18. package/node_modules/@cursor-pool/shared/test/manifest.test.ts +9 -43
  19. package/package.json +5 -7
  20. package/src/compat.ts +201 -194
  21. package/src/cursor.ts +45 -4
  22. package/src/extensionBundle.ts +1 -1
  23. package/src/extensionLink.ts +8 -29
  24. package/src/install.ts +10 -62
  25. package/src/installRecord.ts +0 -2
  26. package/src/patchSet.ts +49 -13
  27. package/src/platform.ts +3 -3
  28. package/src/repair.ts +9 -13
  29. package/src/restore.ts +11 -12
  30. package/src/status.ts +5 -9
  31. package/src/target.ts +12 -0
  32. package/src/trial.ts +2 -3
  33. package/src/uninstall.ts +6 -2
  34. package/test/compat.test.ts +146 -192
  35. package/test/cursor.test.ts +54 -0
  36. package/test/e2e-install.test.ts +29 -46
  37. package/test/extensionLink.test.ts +26 -49
  38. package/test/install.test.ts +4 -64
  39. package/test/patchSet.test.ts +71 -0
  40. package/test/repair.test.ts +131 -1
  41. package/test/restore.test.ts +59 -3
  42. package/test/status.test.ts +0 -1
  43. package/test/target.test.ts +28 -0
  44. package/test/trial.test.ts +15 -1
  45. package/node_modules/@cursor-pool/takeover-plans/package.json +0 -12
  46. package/node_modules/@cursor-pool/takeover-plans/src/index.ts +0 -22
  47. package/node_modules/@cursor-pool/takeover-plans/src/plans.ts +0 -37
  48. package/node_modules/@cursor-pool/takeover-plans/src/types.ts +0 -9
  49. package/node_modules/@cursor-pool/takeover-plans/test/registry.test.ts +0 -23
@@ -1,7 +1,7 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
3
3
  import { tmpdir } from 'node:os';
4
- import { join } from 'node:path';
4
+ import { isAbsolute, join, relative } from 'node:path';
5
5
  import test from 'node:test';
6
6
  import {
7
7
  getLinkedExtensionState,
@@ -12,12 +12,12 @@ import {
12
12
  snapshotLinkedExtensionBundle,
13
13
  } from '../src/extensionLink';
14
14
 
15
- async function createSourceBundle(tempDir: string, version = '0.0.0') {
15
+ async function createSourceBundle(tempDir: string) {
16
16
  const sourceBundlePath = join(tempDir, 'source/extensions/cursor-pool-status');
17
17
  await mkdir(join(sourceBundlePath, 'dist'), { recursive: true });
18
18
  await writeFile(
19
19
  join(sourceBundlePath, 'package.json'),
20
- JSON.stringify({ name: 'cursorpool', publisher: 'cursor-pool', version }),
20
+ JSON.stringify({ name: 'cursorpool', publisher: 'cursor-pool', version: '0.0.0' }),
21
21
  'utf8',
22
22
  );
23
23
  await writeFile(join(sourceBundlePath, 'dist/extension.js'), 'export function activate() {}\n', 'utf8');
@@ -54,52 +54,6 @@ test('linkExtensionBundle copies source bundle into Cursor extensions directory'
54
54
  }
55
55
  });
56
56
 
57
- test('linkExtensionBundle uses the source manifest version for the linked directory and index', async () => {
58
- const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-extension-link-version-'));
59
- const sourceBundlePath = await createSourceBundle(tempDir, '0.5.6');
60
- const cursorExtensionsDir = join(tempDir, 'Extensions');
61
- await mkdir(cursorExtensionsDir, { recursive: true });
62
- await writeFile(join(cursorExtensionsDir, 'extensions.json'), '[]', 'utf8');
63
-
64
- try {
65
- const result = await linkExtensionBundle({ sourceBundlePath, cursorExtensionsDir });
66
- const index = JSON.parse(await readFile(join(cursorExtensionsDir, 'extensions.json'), 'utf8'));
67
-
68
- assert.deepEqual(result, {
69
- state: 'linked',
70
- linkedPath: join(cursorExtensionsDir, 'cursor-pool.extension-0.5.6'),
71
- });
72
- assert.equal(await getLinkedExtensionState(result.linkedPath), 'linked');
73
- assert.deepEqual(index.at(-1), {
74
- identifier: { id: 'cursor-pool.cursorpool' },
75
- location: {
76
- $mid: 1,
77
- path: result.linkedPath,
78
- scheme: 'file',
79
- },
80
- relativeLocation: 'cursor-pool.extension-0.5.6',
81
- version: '0.5.6',
82
- });
83
- } finally {
84
- await rm(tempDir, { recursive: true, force: true });
85
- }
86
- });
87
-
88
- test('linkExtensionBundle rejects unsafe source manifest versions before linking', async () => {
89
- const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-extension-link-version-unsafe-'));
90
- const sourceBundlePath = await createSourceBundle(tempDir, '../../bad');
91
- const cursorExtensionsDir = join(tempDir, 'Extensions');
92
-
93
- try {
94
- await assert.rejects(
95
- linkExtensionBundle({ sourceBundlePath, cursorExtensionsDir }),
96
- /Unsafe extension version/,
97
- );
98
- } finally {
99
- await rm(tempDir, { recursive: true, force: true });
100
- }
101
- });
102
-
103
57
  test('linkExtensionBundle refreshes Cursor extensions index with runtime extension id', async () => {
104
58
  const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-extension-link-index-'));
105
59
  const sourceBundlePath = await createSourceBundle(tempDir);
@@ -169,6 +123,29 @@ test('linkExtensionBundle refreshes Cursor extensions index with runtime extensi
169
123
  }
170
124
  });
171
125
 
126
+ test('linkExtensionBundle writes an absolute linked extension location for relative cursor extensions dir', async () => {
127
+ const tempDir = await mkdtemp(join(process.cwd(), '.cursor-pool-extension-link-relative-'));
128
+ const sourceBundlePath = await createSourceBundle(tempDir);
129
+ const cursorExtensionsDir = join(tempDir, 'Extensions');
130
+ const relativeCursorExtensionsDir = relative(process.cwd(), cursorExtensionsDir);
131
+ await mkdir(cursorExtensionsDir, { recursive: true });
132
+ await writeFile(join(cursorExtensionsDir, 'extensions.json'), '[]\n', 'utf8');
133
+
134
+ try {
135
+ const result = await linkExtensionBundle({
136
+ sourceBundlePath,
137
+ cursorExtensionsDir: relativeCursorExtensionsDir,
138
+ });
139
+ const index = JSON.parse(await readFile(join(cursorExtensionsDir, 'extensions.json'), 'utf8'));
140
+
141
+ assert.equal(isAbsolute(result.linkedPath), true);
142
+ assert.equal(isAbsolute(index.at(-1).location.path), true);
143
+ assert.equal(index.at(-1).location.path, result.linkedPath);
144
+ } finally {
145
+ await rm(tempDir, { recursive: true, force: true });
146
+ }
147
+ });
148
+
172
149
  test('linkExtensionBundle reports missing when source bundle is absent', async () => {
173
150
  const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-extension-link-missing-'));
174
151
  const cursorExtensionsDir = join(tempDir, 'Extensions');
@@ -69,36 +69,15 @@ async function createFixtureApp() {
69
69
  );
70
70
  await writeFile(targetPath, targetContent, 'utf8');
71
71
  await writeFile(join(appPath, alwaysLocalRelativePath), 'function alwaysLocal(){}\n', 'utf8');
72
- const workbenchContent = workbenchFixture();
73
- await writeFile(join(appPath, workbenchRelativePath), workbenchContent, 'utf8');
72
+ await writeFile(join(appPath, workbenchRelativePath), workbenchFixture(), 'utf8');
74
73
 
75
74
  const expectedSha256 = createHash('sha256').update(targetContent).digest('hex');
76
- const workbenchSha256 = createHash('sha256').update(workbenchContent).digest('hex');
77
75
  const compatEntry: CompatibilityManifestEntry = {
78
76
  platform: process.platform,
79
77
  arch: process.arch,
80
- cursorVersion: '3.5',
78
+ cursorVersion: '3.5.38',
81
79
  cursorCommit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
82
80
  supportStatus: 'supported',
83
- adapterVersion: '0.5.8',
84
- takeoverPlanId: 'cursor-3.5-mac-agent-f-workbench-p-l0',
85
- structureFamily: 'mac-agent-F-workbench-p-L0',
86
- patchTargets: [
87
- {
88
- name: 'agent-exec',
89
- targetRelativePath,
90
- expectedSha256,
91
- patchStrategy: 'cursor-agent-exec-snippet',
92
- verifyMarker: 'cursor-pool',
93
- },
94
- {
95
- name: 'workbench',
96
- targetRelativePath: workbenchRelativePath,
97
- expectedSha256: workbenchSha256,
98
- patchStrategy: 'cursor-workbench-auth-gate',
99
- verifyMarker: 'cursor-pool-workbench',
100
- },
101
- ],
102
81
  targetRelativePath,
103
82
  expectedSha256,
104
83
  structureSignature: 'fixture',
@@ -162,10 +141,9 @@ async function createLinuxFixtureApp() {
162
141
  const compatEntry: CompatibilityManifestEntry = {
163
142
  platform: 'linux',
164
143
  arch: process.arch,
165
- cursorVersion: '3.6',
144
+ cursorVersion: '3.6.31',
166
145
  cursorCommit: 'linux-commit',
167
146
  supportStatus: 'supported',
168
- takeoverPlanId: 'cursor-3.6-mac-agent-c-workbench-h-uv',
169
147
  targetRelativePath,
170
148
  expectedSha256,
171
149
  structureSignature: 'linux-fixture',
@@ -210,7 +188,7 @@ async function createRemoteOnlyFixtureApp() {
210
188
  );
211
189
  const compatEntry = {
212
190
  ...fixture.compatEntry,
213
- cursorVersion: '3.7',
191
+ cursorVersion: '3.7.0',
214
192
  cursorCommit: 'remote-commit',
215
193
  userMessage: 'remote fixture supported',
216
194
  };
@@ -234,7 +212,6 @@ test('install reports Cursor version, simulated extension, service, patch, and h
234
212
  assert.match(output, /Cursor 3\.5\.38/);
235
213
  assert.match(output, /mode: disposable/);
236
214
  assert.match(output, /app: .*Cursor\.app/);
237
- assert.match(output, /takeover-plan: cursor-3\.5-mac-agent-f-workbench-p-l0/);
238
215
  assert.match(output, /extension: bundled/);
239
216
  assert.match(output, /trial: recorded/);
240
217
  assert.match(output, /service: running/);
@@ -310,36 +287,6 @@ test('install can use a signed remote compatibility manifest from api base url',
310
287
  }
311
288
  });
312
289
 
313
- test('install rejects when a secondary patch target hash does not match', async () => {
314
- const fixture = await createFixtureApp();
315
- const compatEntry: CompatibilityManifestEntry = {
316
- ...fixture.compatEntry,
317
- patchTargets: fixture.compatEntry.patchTargets?.map((target) =>
318
- target.name === 'workbench'
319
- ? { ...target, expectedSha256: 'f'.repeat(64) }
320
- : target,
321
- ),
322
- };
323
-
324
- try {
325
- await assert.rejects(
326
- () =>
327
- install({
328
- appPath: fixture.appPath,
329
- runtimeFile: fixture.runtimeFile,
330
- backupDir: fixture.backupDir,
331
- trialRecordDir: join(fixture.tempDir, 'trials'),
332
- extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
333
- compatEntries: [compatEntry],
334
- stopServiceAfterInstall: true,
335
- }),
336
- /Patch target hash mismatch/,
337
- );
338
- } finally {
339
- await rm(fixture.tempDir, { recursive: true, force: true });
340
- }
341
- });
342
-
343
290
  test('install passes API base URL to detached service startup', async () => {
344
291
  const fixture = await createFixtureApp();
345
292
  const serviceCalls: Record<string, unknown>[] = [];
@@ -548,7 +495,6 @@ test('real-mode install writes install record and reports real mode', async () =
548
495
  });
549
496
 
550
497
  assert.match(output, /mode: real/);
551
- assert.match(output, /takeover-plan: cursor-3\.5-mac-agent-f-workbench-p-l0/);
552
498
  assert.match(output, /patch: applied/);
553
499
  assert.match(output, /install-record: recorded/);
554
500
  assert.doesNotMatch(output, /trial: recorded/);
@@ -559,7 +505,6 @@ test('real-mode install writes install record and reports real mode', async () =
559
505
  assert.equal(record.mode, 'real');
560
506
  assert.equal(record.appPath, fixture.appPath);
561
507
  assert.equal(record.cursorVersion, '3.5.38');
562
- assert.equal(record.takeoverPlanId, 'cursor-3.5-mac-agent-f-workbench-p-l0');
563
508
  assert.equal(record.extensionInstallPath, extensionInstallPath);
564
509
  assert.equal(record.extensionLinkedPath, linkedExtensionPathForDir(cursorExtensionsDir));
565
510
  assert.equal(record.lastOperation, 'install');
@@ -573,7 +518,6 @@ test('real-mode install rolls back install record after health failure', async (
573
518
  const fixture = await createFixtureApp();
574
519
  const installRecordFile = join(fixture.tempDir, 'install.json');
575
520
  const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
576
- const cursorExtensionsDir = join(fixture.tempDir, 'real-extensions');
577
521
 
578
522
  try {
579
523
  await assert.rejects(
@@ -584,7 +528,6 @@ test('real-mode install rolls back install record after health failure', async (
584
528
  backupDir: fixture.backupDir,
585
529
  installRecordFile,
586
530
  extensionInstallPath,
587
- cursorExtensionsDir,
588
531
  compatEntries: [fixture.compatEntry],
589
532
  stopServiceAfterInstall: true,
590
533
  fetchHealth: async () => ({ ok: false, healthy: false }),
@@ -603,7 +546,6 @@ test('real-mode install requires confirmation when yes is missing', async () =>
603
546
  const originalTargetContent = await readFile(fixture.targetPath, 'utf8');
604
547
  const installRecordFile = join(fixture.tempDir, 'install.json');
605
548
  const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
606
- const cursorExtensionsDir = join(fixture.tempDir, 'real-extensions');
607
549
 
608
550
  try {
609
551
  await assert.rejects(
@@ -614,7 +556,6 @@ test('real-mode install requires confirmation when yes is missing', async () =>
614
556
  backupDir: fixture.backupDir,
615
557
  installRecordFile,
616
558
  extensionInstallPath,
617
- cursorExtensionsDir,
618
559
  compatEntries: [fixture.compatEntry],
619
560
  stopServiceAfterInstall: true,
620
561
  }),
@@ -653,7 +594,6 @@ test('install links extension into an explicit Cursor extensions directory', asy
653
594
  assert.equal(await getLinkedExtensionState(linkedPath), 'linked');
654
595
 
655
596
  const trialRecord = await readTrialRecord(fixture.appPath, { trialRecordDir });
656
- assert.equal(trialRecord?.takeoverPlanId, 'cursor-3.5-mac-agent-f-workbench-p-l0');
657
597
  assert.equal(trialRecord?.extensionState, 'linked');
658
598
  assert.equal(trialRecord?.extensionInstallPath, extensionInstallPath);
659
599
  assert.equal(trialRecord?.extensionLinkedPath, linkedPath);
@@ -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
+ });
@@ -89,7 +89,6 @@ async function createFixtureApp() {
89
89
  cursorVersion: '3.5.38',
90
90
  cursorCommit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
91
91
  supportStatus: 'supported',
92
- takeoverPlanId: 'cursor-3.6-mac-agent-c-workbench-h-uv',
93
92
  targetRelativePath,
94
93
  expectedSha256: createHash('sha256').update(targetContent).digest('hex'),
95
94
  structureSignature: 'fixture',
@@ -208,6 +207,137 @@ test('repair restarts stopped service and updates install record', async () => {
208
207
  }
209
208
  });
210
209
 
210
+ test('repair accepts version-family compatibility entries with wildcard commits', async () => {
211
+ const fixture = await createFixtureApp();
212
+ const familyCompatEntry: CompatibilityManifestEntry = {
213
+ ...fixture.compatEntry,
214
+ cursorVersion: '3.5',
215
+ cursorCommit: '*',
216
+ };
217
+
218
+ try {
219
+ await patchFullSet(fixture.appPath, fixture.backupDir);
220
+ await bundleExtension({ installPath: fixture.extensionInstallPath });
221
+ await seedInstallRecord(fixture);
222
+
223
+ const output = await repair({
224
+ realAppPath: fixture.appPath,
225
+ yes: true,
226
+ runtimeFile: fixture.runtimeFile,
227
+ backupDir: fixture.backupDir,
228
+ installRecordFile: fixture.installRecordFile,
229
+ extensionInstallPath: fixture.extensionInstallPath,
230
+ compatEntries: [familyCompatEntry],
231
+ startDetachedService: async () => {
232
+ await writeRuntimeInfo(
233
+ { host: '127.0.0.1', port: 45207, runtimeId: 'runtime-repaired-family' },
234
+ { runtimeFile: fixture.runtimeFile },
235
+ );
236
+ return { host: '127.0.0.1', port: 45207, runtimeId: 'runtime-repaired-family' };
237
+ },
238
+ });
239
+
240
+ assert.match(output, /compat: supported/);
241
+ assert.match(output, /repair: ok/);
242
+ } finally {
243
+ await rm(fixture.tempDir, { recursive: true, force: true });
244
+ }
245
+ });
246
+
247
+ test('repair passes platform through when checking and repairing the full patch set', async () => {
248
+ const fixture = await createFixtureApp();
249
+ let workbenchTargetRelativePath: string | undefined;
250
+ const linuxDebTargetRelativePath =
251
+ 'resources/app/extensions/cursor-agent-exec/dist/main.js';
252
+ const linuxDebWorkbenchRelativePath =
253
+ 'resources/app/out/vs/workbench/workbench.desktop.main.js';
254
+
255
+ try {
256
+ await mkdir(join(fixture.appPath, 'resources/app/extensions/cursor-agent-exec/dist'), {
257
+ recursive: true,
258
+ });
259
+ await mkdir(join(fixture.appPath, 'resources/app/out/vs/workbench'), { recursive: true });
260
+ await writeFile(
261
+ join(fixture.appPath, 'resources/app/product.json'),
262
+ JSON.stringify({
263
+ version: '3.5.38',
264
+ commit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
265
+ }),
266
+ 'utf8',
267
+ );
268
+ await writeFile(
269
+ join(fixture.appPath, linuxDebTargetRelativePath),
270
+ `${fixture.targetContent}${CURSOR_POOL_PATCH_MARKER}\n`,
271
+ 'utf8',
272
+ );
273
+ await writeFile(
274
+ join(fixture.appPath, linuxDebWorkbenchRelativePath),
275
+ 'function workbenchWithoutMarker(){}\n',
276
+ 'utf8',
277
+ );
278
+ await bundleExtension({ installPath: fixture.extensionInstallPath });
279
+ await writeInstallRecord(
280
+ createInstallRecordFixture(fixture, {
281
+ targetRelativePath: linuxDebTargetRelativePath,
282
+ originalSha256: '*',
283
+ }),
284
+ { installRecordFile: fixture.installRecordFile },
285
+ );
286
+
287
+ await repair({
288
+ realAppPath: fixture.appPath,
289
+ productRelativePath: 'resources/app/product.json',
290
+ platform: 'linux',
291
+ arch: process.arch,
292
+ yes: true,
293
+ runtimeFile: fixture.runtimeFile,
294
+ backupDir: fixture.backupDir,
295
+ installRecordFile: fixture.installRecordFile,
296
+ extensionInstallPath: fixture.extensionInstallPath,
297
+ compatEntries: [
298
+ {
299
+ ...fixture.compatEntry,
300
+ platform: 'linux',
301
+ arch: process.arch,
302
+ targetRelativePath: linuxDebTargetRelativePath,
303
+ expectedSha256: '*',
304
+ },
305
+ ],
306
+ startDetachedService: async () => {
307
+ await writeRuntimeInfo(
308
+ { host: '127.0.0.1', port: 45208, runtimeId: 'runtime-repaired-linux-platform' },
309
+ { runtimeFile: fixture.runtimeFile },
310
+ );
311
+ return { host: '127.0.0.1', port: 45208, runtimeId: 'runtime-repaired-linux-platform' };
312
+ },
313
+ patchCursorAgentExec: async (_appPath, options = {}) => ({
314
+ targetPath: join(fixture.appPath, options.targetRelativePath ?? targetRelativePath),
315
+ backupPath: join(fixture.backupDir, 'agent.bak'),
316
+ beforeHash: 'before',
317
+ afterHash: 'after',
318
+ markerPresent: true,
319
+ }),
320
+ patchCursorWorkbenchAuthGate: async (_appPath, options = {}) => {
321
+ workbenchTargetRelativePath = options.targetRelativePath;
322
+ return {
323
+ targetPath: join(fixture.appPath, options.targetRelativePath ?? workbenchRelativePath),
324
+ backupPath: join(fixture.backupDir, 'workbench.bak'),
325
+ beforeHash: 'before',
326
+ afterHash: 'after',
327
+ markerPresent: true,
328
+ };
329
+ },
330
+ });
331
+
332
+ assert.equal(
333
+ workbenchTargetRelativePath,
334
+ linuxDebWorkbenchRelativePath,
335
+ );
336
+ } finally {
337
+ await rm(fixture.tempDir, { recursive: true, force: true });
338
+ }
339
+ });
340
+
211
341
  test('repair restores missing extension bundle', async () => {
212
342
  const fixture = await createFixtureApp();
213
343
 
@@ -5,6 +5,8 @@ import { join, normalize } from 'node:path';
5
5
  import test from 'node:test';
6
6
  import { CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR } from '../../patcher/src/marker';
7
7
  import { patchCursorAgentExec } from '../../patcher/src/patchCursorAgentExec';
8
+ import { patchCursorAlwaysLocal } from '../../patcher/src/patchCursorAlwaysLocal';
9
+ import { patchCursorWorkbenchAuthGate } from '../../patcher/src/patchCursorWorkbenchAuthGate';
8
10
  import {
9
11
  installIdForAppPath,
10
12
  readInstallRecord,
@@ -16,15 +18,57 @@ import { trialIdForAppPath, writeTrialRecord } from '../src/trial';
16
18
 
17
19
  const targetRelativePath =
18
20
  'Contents/Resources/app/extensions/cursor-agent-exec/dist/main.js';
21
+ const alwaysLocalRelativePath =
22
+ 'Contents/Resources/app/extensions/cursor-always-local/dist/main.js';
23
+ const workbenchRelativePath =
24
+ 'Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js';
25
+ const composerAuthGateAnchor =
26
+ 'get when(){return p()&&!fzC},get fallback(){return he(DGC,{})}';
27
+ const composerSubmitAuthGateAnchor =
28
+ 'if(!p()){e.cursorAuthenticationService.login(),e.commandService.executeCommand(wV,"general");return}';
29
+ const agentLoopRunAnchor =
30
+ 'await this.agentClientService.run(te,H,$e,Ne,Ce,T,ze,me,we,[],ct)';
31
+ const buildFlagsLocalModeAnchor = 'localMode:!1';
32
+ const localProviderConfigAnchor =
33
+ '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"}';
34
+ const agentClientRunLocalModeAnchor =
35
+ '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)';
36
+
37
+ function workbenchFixture() {
38
+ return [
39
+ `function composer(){return he(Mt,{${composerAuthGateAnchor},get children(){return "controls"}})}`,
40
+ `async function submit(){${composerSubmitAuthGateAnchor};return "submitted";}`,
41
+ `const flags={${buildFlagsLocalModeAnchor}}`,
42
+ localProviderConfigAnchor,
43
+ `async function runAgentLoop(){${agentLoopRunAnchor}}`,
44
+ `async function agentClientRun(){const g={};${agentClientRunLocalModeAnchor}}`,
45
+ ].join(';');
46
+ }
47
+
48
+ async function patchFullSet(appPath: string, backupDir: string) {
49
+ await patchCursorAgentExec(appPath, { backupDir });
50
+ await patchCursorAlwaysLocal(appPath, { backupDir });
51
+ await patchCursorWorkbenchAuthGate(appPath, { backupDir });
52
+ }
19
53
 
20
54
  async function createFixtureApp() {
21
55
  const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-cli-restore-'));
22
56
  const appPath = join(tempDir, 'Cursor.app');
23
57
  const targetPath = join(appPath, targetRelativePath);
58
+ const alwaysLocalPath = join(appPath, alwaysLocalRelativePath);
59
+ const workbenchPath = join(appPath, workbenchRelativePath);
24
60
  const targetContent = `function main() { return "agent"; }\n${CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR}\nmain();\n`;
61
+ const alwaysLocalContent = 'function alwaysLocal(){}\n';
62
+ const workbenchContent = workbenchFixture();
25
63
  await mkdir(join(appPath, 'Contents/Resources/app/extensions/cursor-agent-exec/dist'), {
26
64
  recursive: true,
27
65
  });
66
+ await mkdir(join(appPath, 'Contents/Resources/app/extensions/cursor-always-local/dist'), {
67
+ recursive: true,
68
+ });
69
+ await mkdir(join(appPath, 'Contents/Resources/app/out/vs/workbench'), {
70
+ recursive: true,
71
+ });
28
72
  await writeFile(
29
73
  join(appPath, 'Contents/Resources/app/product.json'),
30
74
  JSON.stringify({
@@ -34,12 +78,18 @@ async function createFixtureApp() {
34
78
  'utf8',
35
79
  );
36
80
  await writeFile(targetPath, targetContent, 'utf8');
81
+ await writeFile(alwaysLocalPath, alwaysLocalContent, 'utf8');
82
+ await writeFile(workbenchPath, workbenchContent, 'utf8');
37
83
 
38
84
  return {
39
85
  appPath,
40
86
  tempDir,
41
87
  targetPath,
88
+ alwaysLocalPath,
89
+ workbenchPath,
42
90
  targetContent,
91
+ alwaysLocalContent,
92
+ workbenchContent,
43
93
  backupDir: join(tempDir, 'backups'),
44
94
  trialRecordDir: join(tempDir, 'trials'),
45
95
  };
@@ -49,7 +99,7 @@ test('restore reports ok and replaces patched fixture bytes with backup bytes',
49
99
  const fixture = await createFixtureApp();
50
100
 
51
101
  try {
52
- await patchCursorAgentExec(fixture.appPath, { backupDir: fixture.backupDir });
102
+ await patchFullSet(fixture.appPath, fixture.backupDir);
53
103
 
54
104
  const output = await restore({
55
105
  appPath: fixture.appPath,
@@ -59,6 +109,8 @@ test('restore reports ok and replaces patched fixture bytes with backup bytes',
59
109
  assert.match(output, /mode: disposable/);
60
110
  assert.match(output, /restore: ok/);
61
111
  assert.equal(await readFile(fixture.targetPath, 'utf8'), fixture.targetContent);
112
+ assert.equal(await readFile(fixture.alwaysLocalPath, 'utf8'), fixture.alwaysLocalContent);
113
+ assert.equal(await readFile(fixture.workbenchPath, 'utf8'), fixture.workbenchContent);
62
114
  } finally {
63
115
  await rm(fixture.tempDir, { recursive: true, force: true });
64
116
  }
@@ -68,7 +120,7 @@ test('restore discovers backup directory from the trial record', async () => {
68
120
  const fixture = await createFixtureApp();
69
121
 
70
122
  try {
71
- await patchCursorAgentExec(fixture.appPath, { backupDir: fixture.backupDir });
123
+ await patchFullSet(fixture.appPath, fixture.backupDir);
72
124
  await writeTrialRecord(
73
125
  {
74
126
  trialId: trialIdForAppPath(fixture.appPath),
@@ -96,6 +148,8 @@ test('restore discovers backup directory from the trial record', async () => {
96
148
  assert.match(output, /mode: disposable/);
97
149
  assert.match(output, /restore: ok/);
98
150
  assert.equal(await readFile(fixture.targetPath, 'utf8'), fixture.targetContent);
151
+ assert.equal(await readFile(fixture.alwaysLocalPath, 'utf8'), fixture.alwaysLocalContent);
152
+ assert.equal(await readFile(fixture.workbenchPath, 'utf8'), fixture.workbenchContent);
99
153
  } finally {
100
154
  await rm(fixture.tempDir, { recursive: true, force: true });
101
155
  }
@@ -136,7 +190,7 @@ test('real-mode restore uses install record backup and keeps install record', as
136
190
  const runtimeFile = join(fixture.tempDir, 'runtime.json');
137
191
 
138
192
  try {
139
- await patchCursorAgentExec(fixture.appPath, { backupDir: fixture.backupDir });
193
+ await patchFullSet(fixture.appPath, fixture.backupDir);
140
194
  const record = createInstallRecordFixture(fixture, { runtimeFile });
141
195
  await writeInstallRecord(record, { installRecordFile });
142
196
 
@@ -150,6 +204,8 @@ test('real-mode restore uses install record backup and keeps install record', as
150
204
  assert.match(output, /install-record: recorded/);
151
205
  assert.match(output, /restore: ok/);
152
206
  assert.equal(await readFile(fixture.targetPath, 'utf8'), fixture.targetContent);
207
+ assert.equal(await readFile(fixture.alwaysLocalPath, 'utf8'), fixture.alwaysLocalContent);
208
+ assert.equal(await readFile(fixture.workbenchPath, 'utf8'), fixture.workbenchContent);
153
209
  assert.deepEqual(await readInstallRecord({ installRecordFile }), record);
154
210
  } finally {
155
211
  await rm(fixture.tempDir, { recursive: true, force: true });
@@ -136,7 +136,6 @@ async function createFixtureApp() {
136
136
  cursorVersion: '3.5.38',
137
137
  cursorCommit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
138
138
  supportStatus: 'supported',
139
- takeoverPlanId: 'cursor-3.6-mac-agent-c-workbench-h-uv',
140
139
  targetRelativePath,
141
140
  expectedSha256: createHash('sha256').update(targetContent).digest('hex'),
142
141
  structureSignature: 'fixture',
@@ -16,6 +16,34 @@ test('resolveCursorTarget treats explicit appPath as disposable mode', () => {
16
16
  );
17
17
  });
18
18
 
19
+ test('resolveCursorTarget treats Linux deb Cursor root as real mode', () => {
20
+ assert.deepEqual(
21
+ resolveCursorTarget({
22
+ appPath: '/usr/share/cursor/',
23
+ platform: 'linux',
24
+ }),
25
+ {
26
+ mode: 'real',
27
+ appPath: '/usr/share/cursor',
28
+ requiresConfirmation: true,
29
+ },
30
+ );
31
+ });
32
+
33
+ test('resolveCursorTarget keeps Linux AppImage extraction roots disposable', () => {
34
+ assert.deepEqual(
35
+ resolveCursorTarget({
36
+ appPath: '/home/debian/.cursor-pool/appimages/Cursor-abc/squashfs-root',
37
+ platform: 'linux',
38
+ }),
39
+ {
40
+ mode: 'disposable',
41
+ appPath: '/home/debian/.cursor-pool/appimages/Cursor-abc/squashfs-root',
42
+ requiresConfirmation: false,
43
+ },
44
+ );
45
+ });
46
+
19
47
  test('resolveCursorTarget rejects the real Cursor app as disposable appPath', () => {
20
48
  assert.throws(
21
49
  () => resolveCursorTarget({ appPath: '/Applications/Cursor.app' }),