@cursorpool-dev/cli 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/bin/cursor-pool.mjs +9 -0
  2. package/bin/cursor-pool.ts +169 -0
  3. package/node_modules/@cursor-pool/extension/dist/extension.js +2910 -0
  4. package/node_modules/@cursor-pool/extension/package.json +64 -0
  5. package/node_modules/@cursor-pool/extension/resources/cursor-pool.svg +6 -0
  6. package/node_modules/@cursor-pool/extension/src/api.ts +545 -0
  7. package/node_modules/@cursor-pool/extension/src/extension.ts +104 -0
  8. package/node_modules/@cursor-pool/extension/src/index.ts +1 -0
  9. package/node_modules/@cursor-pool/extension/src/panel.ts +569 -0
  10. package/node_modules/@cursor-pool/extension/src/runtime.ts +22 -0
  11. package/node_modules/@cursor-pool/extension/test/panel.test.ts +1785 -0
  12. package/node_modules/@cursor-pool/patcher/package.json +17 -0
  13. package/node_modules/@cursor-pool/patcher/src/alwaysLocalMarker.ts +86 -0
  14. package/node_modules/@cursor-pool/patcher/src/hash.ts +7 -0
  15. package/node_modules/@cursor-pool/patcher/src/index.ts +55 -0
  16. package/node_modules/@cursor-pool/patcher/src/marker.ts +159 -0
  17. package/node_modules/@cursor-pool/patcher/src/patchCursorAgentExec.ts +154 -0
  18. package/node_modules/@cursor-pool/patcher/src/patchCursorAlwaysLocal.ts +142 -0
  19. package/node_modules/@cursor-pool/patcher/src/patchCursorWorkbenchAuthGate.ts +140 -0
  20. package/node_modules/@cursor-pool/patcher/src/restoreCursorAgentExec.ts +52 -0
  21. package/node_modules/@cursor-pool/patcher/src/restoreCursorAlwaysLocal.ts +52 -0
  22. package/node_modules/@cursor-pool/patcher/src/restoreCursorWorkbenchAuthGate.ts +70 -0
  23. package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +243 -0
  24. package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +630 -0
  25. package/node_modules/@cursor-pool/patcher/test/patchCursorAlwaysLocal.test.ts +144 -0
  26. package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +770 -0
  27. package/node_modules/@cursor-pool/patcher/test/restoreCursorAgentExec.test.ts +139 -0
  28. package/node_modules/@cursor-pool/service/package.json +17 -0
  29. package/node_modules/@cursor-pool/service/src/canary.ts +61 -0
  30. package/node_modules/@cursor-pool/service/src/diagnostics.ts +385 -0
  31. package/node_modules/@cursor-pool/service/src/entry.ts +161 -0
  32. package/node_modules/@cursor-pool/service/src/health.ts +10 -0
  33. package/node_modules/@cursor-pool/service/src/index.ts +29 -0
  34. package/node_modules/@cursor-pool/service/src/metadata.ts +22 -0
  35. package/node_modules/@cursor-pool/service/src/platformSession.ts +1178 -0
  36. package/node_modules/@cursor-pool/service/src/requestCheck.ts +81 -0
  37. package/node_modules/@cursor-pool/service/src/requestGate.ts +100 -0
  38. package/node_modules/@cursor-pool/service/src/requestGateway.ts +441 -0
  39. package/node_modules/@cursor-pool/service/src/runtime.ts +48 -0
  40. package/node_modules/@cursor-pool/service/src/server.ts +939 -0
  41. package/node_modules/@cursor-pool/service/src/takeover.ts +111 -0
  42. package/node_modules/@cursor-pool/service/test/canary.test.ts +140 -0
  43. package/node_modules/@cursor-pool/service/test/diagnostics.test.ts +506 -0
  44. package/node_modules/@cursor-pool/service/test/metadata.test.ts +63 -0
  45. package/node_modules/@cursor-pool/service/test/platformSession.test.ts +2428 -0
  46. package/node_modules/@cursor-pool/service/test/requestCheck.test.ts +152 -0
  47. package/node_modules/@cursor-pool/service/test/requestGate.test.ts +207 -0
  48. package/node_modules/@cursor-pool/service/test/requestGateway.test.ts +466 -0
  49. package/node_modules/@cursor-pool/service/test/runtime.test.ts +47 -0
  50. package/node_modules/@cursor-pool/service/test/server.test.ts +2570 -0
  51. package/node_modules/@cursor-pool/shared/package.json +17 -0
  52. package/node_modules/@cursor-pool/shared/src/clientConfig.ts +49 -0
  53. package/node_modules/@cursor-pool/shared/src/index.ts +14 -0
  54. package/node_modules/@cursor-pool/shared/src/manifest.ts +36 -0
  55. package/node_modules/@cursor-pool/shared/src/metadata.ts +19 -0
  56. package/node_modules/@cursor-pool/shared/src/paths.ts +5 -0
  57. package/node_modules/@cursor-pool/shared/src/runtime.ts +3 -0
  58. package/node_modules/@cursor-pool/shared/test/index.test.ts +56 -0
  59. package/node_modules/@cursor-pool/shared/test/manifest.test.ts +65 -0
  60. package/node_modules/@cursor-pool/shared/test/metadata.test.ts +25 -0
  61. package/node_modules/@cursor-pool/shared/test/runtime.test.ts +8 -0
  62. package/package.json +28 -0
  63. package/src/adHocResign.ts +65 -0
  64. package/src/autostart.ts +240 -0
  65. package/src/compat.ts +282 -0
  66. package/src/confirm.ts +76 -0
  67. package/src/cursor.ts +94 -0
  68. package/src/diagnostics.ts +558 -0
  69. package/src/environment.ts +18 -0
  70. package/src/extensionBundle.ts +111 -0
  71. package/src/extensionLink.ts +168 -0
  72. package/src/index.ts +23 -0
  73. package/src/install.ts +614 -0
  74. package/src/installRecord.ts +105 -0
  75. package/src/launch.ts +182 -0
  76. package/src/patchSet.ts +182 -0
  77. package/src/platform.ts +132 -0
  78. package/src/repair.ts +383 -0
  79. package/src/restore.ts +153 -0
  80. package/src/serviceCommands.ts +79 -0
  81. package/src/serviceProcess.ts +188 -0
  82. package/src/status.ts +241 -0
  83. package/src/target.ts +37 -0
  84. package/src/trial.ts +133 -0
  85. package/src/uninstall.ts +213 -0
  86. package/test/autostart.test.ts +151 -0
  87. package/test/compat.test.ts +192 -0
  88. package/test/confirm.test.ts +114 -0
  89. package/test/cursor-pool-bin.test.ts +658 -0
  90. package/test/cursor.test.ts +20 -0
  91. package/test/diagnostics.test.ts +709 -0
  92. package/test/e2e-install.test.ts +773 -0
  93. package/test/extensionBundle.test.ts +161 -0
  94. package/test/extensionLink.test.ts +209 -0
  95. package/test/install.test.ts +862 -0
  96. package/test/installRecord.test.ts +107 -0
  97. package/test/launch.test.ts +138 -0
  98. package/test/platform.test.ts +226 -0
  99. package/test/repair.test.ts +575 -0
  100. package/test/restore.test.ts +211 -0
  101. package/test/serviceCommands.test.ts +135 -0
  102. package/test/serviceProcess.test.ts +280 -0
  103. package/test/status.test.ts +615 -0
  104. package/test/target.test.ts +49 -0
  105. package/test/trial.test.ts +146 -0
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@cursor-pool/patcher",
3
+ "version": "0.5.6",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./hash": "./src/hash.ts",
9
+ "./marker": "./src/marker.ts"
10
+ },
11
+ "dependencies": {
12
+ "@cursor-pool/shared": "0.5.6"
13
+ },
14
+ "scripts": {
15
+ "test": "tsx --test test/*.test.ts"
16
+ }
17
+ }
@@ -0,0 +1,86 @@
1
+ import { readFile } from 'node:fs/promises';
2
+
3
+ export const CURSOR_POOL_ALWAYS_LOCAL_TAKEOVER_MARKER =
4
+ 'CURSOR_POOL_CURSOR_ALWAYS_LOCAL_TAKEOVER_PATCH';
5
+ export const CURSOR_POOL_ALWAYS_LOCAL_TAKEOVER_BEGIN_MARKER =
6
+ `/* ${CURSOR_POOL_ALWAYS_LOCAL_TAKEOVER_MARKER}: begin */`;
7
+ export const CURSOR_POOL_ALWAYS_LOCAL_TAKEOVER_END_MARKER =
8
+ `/* ${CURSOR_POOL_ALWAYS_LOCAL_TAKEOVER_MARKER}: end */`;
9
+ export const CURSOR_POOL_ALWAYS_LOCAL_TAKEOVER_SIGNATURE =
10
+ '__cursorPoolAlwaysLocalTakeover';
11
+
12
+ export function buildCursorAlwaysLocalTakeoverPatchSnippet() {
13
+ return `
14
+ ${CURSOR_POOL_ALWAYS_LOCAL_TAKEOVER_BEGIN_MARKER}
15
+ ;(() => {
16
+ try {
17
+ const fs = require('node:fs');
18
+ const os = require('node:os');
19
+ const path = require('node:path');
20
+ const http = require('node:http');
21
+ const runtimePath =
22
+ process.env.CURSOR_POOL_RUNTIME_FILE ||
23
+ path.join(os.homedir(), '.cursor-pool', 'runtime.json');
24
+ if (!fs.existsSync(runtimePath)) return;
25
+ const runtime = JSON.parse(fs.readFileSync(runtimePath, 'utf8'));
26
+ if (!runtime || runtime.host !== '127.0.0.1' || typeof runtime.port !== 'number') return;
27
+ const requestId = \`takeover-\${Date.now().toString(36)}-\${Math.random().toString(36).slice(2)}\`;
28
+ const payload = JSON.stringify({
29
+ requestId,
30
+ source: 'cursor-always-local',
31
+ model: 'unknown',
32
+ stream: true,
33
+ messagesShape: 'redacted',
34
+ });
35
+ const request = http.request(
36
+ {
37
+ hostname: '127.0.0.1',
38
+ port: runtime.port,
39
+ path: '/agent/takeover',
40
+ method: 'POST',
41
+ timeout: 1000,
42
+ headers: {
43
+ 'content-type': 'application/json',
44
+ 'content-length': Buffer.byteLength(payload),
45
+ },
46
+ },
47
+ (response) => {
48
+ response.resume();
49
+ },
50
+ );
51
+ request.on('error', () => {});
52
+ request.on('timeout', () => request.destroy());
53
+ request.end(payload);
54
+ globalThis.__cursorPoolAlwaysLocalTakeover = {
55
+ host: '127.0.0.1',
56
+ source: 'cursor-always-local',
57
+ };
58
+ } catch {}
59
+ })();
60
+ ${CURSOR_POOL_ALWAYS_LOCAL_TAKEOVER_END_MARKER}
61
+ `;
62
+ }
63
+
64
+ export function containsCursorAlwaysLocalTakeoverMarker(content: string) {
65
+ const beginIndex = content.indexOf(CURSOR_POOL_ALWAYS_LOCAL_TAKEOVER_BEGIN_MARKER);
66
+ const endIndex = content.indexOf(CURSOR_POOL_ALWAYS_LOCAL_TAKEOVER_END_MARKER);
67
+
68
+ return (
69
+ beginIndex >= 0 &&
70
+ endIndex > beginIndex &&
71
+ content
72
+ .slice(beginIndex, endIndex)
73
+ .includes(CURSOR_POOL_ALWAYS_LOCAL_TAKEOVER_SIGNATURE)
74
+ );
75
+ }
76
+
77
+ export function containsAnyCursorAlwaysLocalTakeoverMarkerText(content: string) {
78
+ return (
79
+ content.includes(CURSOR_POOL_ALWAYS_LOCAL_TAKEOVER_MARKER) ||
80
+ content.includes(CURSOR_POOL_ALWAYS_LOCAL_TAKEOVER_SIGNATURE)
81
+ );
82
+ }
83
+
84
+ export async function hasCursorAlwaysLocalTakeoverMarker(filePath: string) {
85
+ return containsCursorAlwaysLocalTakeoverMarker(await readFile(filePath, 'utf8'));
86
+ }
@@ -0,0 +1,7 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFile } from 'node:fs/promises';
3
+
4
+ export async function sha256File(filePath: string) {
5
+ const bytes = await readFile(filePath);
6
+ return createHash('sha256').update(bytes).digest('hex');
7
+ }
@@ -0,0 +1,55 @@
1
+ export const packageRole = 'patcher';
2
+ export { sha256File } from './hash';
3
+ export {
4
+ buildCursorAgentExecPatchSnippet,
5
+ containsCursorPoolMarker,
6
+ CURSOR_POOL_PATCH_MARKER,
7
+ hasCursorPoolMarker,
8
+ } from './marker';
9
+ export {
10
+ buildCursorAlwaysLocalTakeoverPatchSnippet,
11
+ containsCursorAlwaysLocalTakeoverMarker,
12
+ containsAnyCursorAlwaysLocalTakeoverMarkerText,
13
+ CURSOR_POOL_ALWAYS_LOCAL_TAKEOVER_MARKER,
14
+ hasCursorAlwaysLocalTakeoverMarker,
15
+ } from './alwaysLocalMarker';
16
+ export {
17
+ backupPathForCursorAgentExec,
18
+ CURSOR_AGENT_EXEC_RELATIVE_PATH,
19
+ patchCursorAgentExec,
20
+ resolveCursorAgentExecPath,
21
+ type PatchCursorAgentExecOptions,
22
+ type PatchCursorAgentExecResult,
23
+ } from './patchCursorAgentExec';
24
+ export {
25
+ backupPathForCursorAlwaysLocal,
26
+ CURSOR_ALWAYS_LOCAL_RELATIVE_PATH,
27
+ patchCursorAlwaysLocal,
28
+ resolveCursorAlwaysLocalPath,
29
+ type PatchCursorAlwaysLocalOptions,
30
+ type PatchCursorAlwaysLocalResult,
31
+ } from './patchCursorAlwaysLocal';
32
+ export {
33
+ backupPathForCursorWorkbenchAuthGate,
34
+ CURSOR_WORKBENCH_RELATIVE_PATH,
35
+ patchCursorWorkbenchAuthGate,
36
+ resolveCursorWorkbenchPath,
37
+ type PatchCursorWorkbenchAuthGateOptions,
38
+ type PatchCursorWorkbenchAuthGateResult,
39
+ } from './patchCursorWorkbenchAuthGate';
40
+ export {
41
+ containsCursorWorkbenchAuthGateMarker,
42
+ containsAnyCursorWorkbenchAuthGateMarkerText,
43
+ } from './workbenchAuthGateMarker';
44
+ export {
45
+ restoreCursorAgentExec,
46
+ type RestoreCursorAgentExecResult,
47
+ } from './restoreCursorAgentExec';
48
+ export {
49
+ restoreCursorAlwaysLocal,
50
+ type RestoreCursorAlwaysLocalResult,
51
+ } from './restoreCursorAlwaysLocal';
52
+ export {
53
+ restoreCursorWorkbenchAuthGate,
54
+ type RestoreCursorWorkbenchAuthGateResult,
55
+ } from './restoreCursorWorkbenchAuthGate';
@@ -0,0 +1,159 @@
1
+ import { readFile } from 'node:fs/promises';
2
+
3
+ export const CURSOR_POOL_PATCH_MARKER = 'CURSOR_POOL_MVP0_CURSOR_AGENT_EXEC_PATCH_V3_FAIL_CLOSED';
4
+ export const CURSOR_POOL_PATCH_BEGIN_MARKER = `/* ${CURSOR_POOL_PATCH_MARKER}: begin */`;
5
+ export const CURSOR_POOL_PATCH_END_MARKER = `/* ${CURSOR_POOL_PATCH_MARKER}: end */`;
6
+ export const CURSOR_POOL_PATCH_SIGNATURE = '__cursorPoolAgentExecTakeover';
7
+ export const CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR =
8
+ 'const ht=F.cursor.registerAgentExecProvider(mt);';
9
+ export const CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHORS = [
10
+ CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR,
11
+ 'const ht=c.cursor.registerAgentExecProvider(mt);',
12
+ ];
13
+
14
+ export function buildCursorAgentExecPatchSnippet() {
15
+ return `
16
+ ${CURSOR_POOL_PATCH_BEGIN_MARKER}
17
+ ;(() => {
18
+ try {
19
+ const fs = require('node:fs');
20
+ const os = require('node:os');
21
+ const path = require('node:path');
22
+ const http = require('node:http');
23
+ const runtimePath =
24
+ process.env.CURSOR_POOL_RUNTIME_FILE ||
25
+ path.join(os.homedir(), '.cursor-pool', 'runtime.json');
26
+ if (!fs.existsSync(runtimePath)) return;
27
+ const runtime = JSON.parse(fs.readFileSync(runtimePath, 'utf8'));
28
+ if (
29
+ !runtime ||
30
+ runtime.host !== '127.0.0.1' ||
31
+ typeof runtime.port !== 'number' ||
32
+ typeof runtime.runtimeId !== 'string'
33
+ ) {
34
+ return;
35
+ }
36
+ if (!globalThis.__cursorPoolAgentExecTakeover) {
37
+ globalThis.__cursorPoolAgentExecTakeover = {
38
+ host: '127.0.0.1',
39
+ source: 'cursor-agent-exec',
40
+ runtimeId: runtime.runtimeId,
41
+ };
42
+ }
43
+ const requestCursorPoolTakeover = (context) =>
44
+ new Promise((resolve) => {
45
+ const requestId =
46
+ context?.requestId ||
47
+ \`agent-\${Date.now().toString(36)}-\${Math.random().toString(36).slice(2)}\`;
48
+ const body = JSON.stringify({
49
+ requestId,
50
+ source: 'cursor-agent-exec',
51
+ model: context?.modelName || 'unknown',
52
+ stream: true,
53
+ bodyShape: 'redacted',
54
+ });
55
+ const request = http.request(
56
+ {
57
+ hostname: '127.0.0.1',
58
+ port: runtime.port,
59
+ path: '/agent/takeover',
60
+ method: 'POST',
61
+ timeout: 1000,
62
+ headers: {
63
+ 'content-type': 'application/json',
64
+ 'content-length': Buffer.byteLength(body),
65
+ },
66
+ },
67
+ (response) => {
68
+ let text = '';
69
+ response.setEncoding?.('utf8');
70
+ response.on('data', (chunk) => {
71
+ text += chunk;
72
+ });
73
+ response.on('end', () => {
74
+ try {
75
+ resolve(JSON.parse(text));
76
+ } catch {
77
+ resolve({ state: 'rejected', requestId, reason: 'invalid-session' });
78
+ }
79
+ });
80
+ },
81
+ );
82
+ request.on('error', () => resolve({ state: 'rejected', requestId, reason: 'invalid-session' }));
83
+ request.on('timeout', () => {
84
+ request.destroy();
85
+ resolve({ state: 'rejected', requestId, reason: 'invalid-session' });
86
+ });
87
+ request.end(body);
88
+ });
89
+ const wrapCursorPoolProvider = (provider) => {
90
+ if (!provider || provider.__cursorPoolWrappedAgentExecProvider) {
91
+ return provider;
92
+ }
93
+ const wrapped = Object.create(Object.getPrototypeOf(provider));
94
+ Object.assign(wrapped, provider);
95
+ if (typeof provider.runLocalAgent === 'function') {
96
+ wrapped.runLocalAgent = provider.runLocalAgent.bind(provider);
97
+ }
98
+ if (typeof provider.createSession === 'function') {
99
+ wrapped.createSession = provider.createSession.bind(provider);
100
+ }
101
+ if (typeof provider.fetchLocalProviderModels === 'function') {
102
+ wrapped.fetchLocalProviderModels = provider.fetchLocalProviderModels.bind(provider);
103
+ }
104
+ wrapped.__cursorPoolWrappedAgentExecProvider = true;
105
+ return wrapped;
106
+ };
107
+ const cursorApi =
108
+ typeof F !== 'undefined' && F.cursor?.registerAgentExecProvider
109
+ ? F
110
+ : typeof c !== 'undefined' && c.cursor?.registerAgentExecProvider
111
+ ? c
112
+ : undefined;
113
+ if (cursorApi?.cursor?.registerAgentExecProvider) {
114
+ const originalRegisterAgentExecProvider = cursorApi.cursor.registerAgentExecProvider.bind(cursorApi.cursor);
115
+ cursorApi.cursor.registerAgentExecProvider = (provider) =>
116
+ originalRegisterAgentExecProvider(wrapCursorPoolProvider(provider));
117
+ }
118
+ } catch {}
119
+ })();
120
+ ${CURSOR_POOL_PATCH_END_MARKER}
121
+ `;
122
+ }
123
+
124
+ export function injectCursorAgentExecPatchSnippet(content: string) {
125
+ if (containsCursorPoolMarker(content)) {
126
+ return content;
127
+ }
128
+ const snippet = buildCursorAgentExecPatchSnippet();
129
+ const anchor = CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHORS.find((candidate) =>
130
+ content.includes(candidate)
131
+ );
132
+ const anchorIndex = anchor ? content.indexOf(anchor) : -1;
133
+ if (anchorIndex < 0) {
134
+ throw new Error('Cursor Agent Exec provider registration anchor was not found.');
135
+ }
136
+ return `${content.slice(0, anchorIndex)}${snippet}${content.slice(anchorIndex)}`;
137
+ }
138
+
139
+ export function containsCursorPoolMarker(content: string) {
140
+ const beginIndex = content.indexOf(CURSOR_POOL_PATCH_BEGIN_MARKER);
141
+ const endIndex = content.indexOf(CURSOR_POOL_PATCH_END_MARKER);
142
+
143
+ return (
144
+ beginIndex >= 0 &&
145
+ endIndex > beginIndex &&
146
+ content.slice(beginIndex, endIndex).includes(CURSOR_POOL_PATCH_SIGNATURE)
147
+ );
148
+ }
149
+
150
+ export function containsAnyCursorPoolMarkerText(content: string) {
151
+ return (
152
+ content.includes(CURSOR_POOL_PATCH_MARKER) ||
153
+ content.includes(CURSOR_POOL_PATCH_SIGNATURE)
154
+ );
155
+ }
156
+
157
+ export async function hasCursorPoolMarker(filePath: string) {
158
+ return containsCursorPoolMarker(await readFile(filePath, 'utf8'));
159
+ }
@@ -0,0 +1,154 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { basename, dirname, join } from 'node:path';
5
+ import { joinCursorAppPath } from '@cursor-pool/shared/paths';
6
+ import { DEFAULT_RUNTIME_DIR } from '@cursor-pool/shared/runtime';
7
+ import { sha256File } from './hash';
8
+ import {
9
+ buildCursorAgentExecPatchSnippet,
10
+ containsAnyCursorPoolMarkerText,
11
+ containsCursorPoolMarker,
12
+ injectCursorAgentExecPatchSnippet,
13
+ } from './marker';
14
+
15
+ export const CURSOR_AGENT_EXEC_RELATIVE_PATH =
16
+ 'Contents/Resources/app/extensions/cursor-agent-exec/dist/main.js';
17
+
18
+ export type PatchCursorAgentExecOptions = {
19
+ backupDir?: string;
20
+ targetRelativePath?: string;
21
+ patchContent?: (content: string) => string;
22
+ };
23
+
24
+ export type PatchCursorAgentExecResult = {
25
+ targetPath: string;
26
+ backupPath: string;
27
+ beforeHash: string;
28
+ afterHash: string;
29
+ markerPresent: boolean;
30
+ };
31
+
32
+ function resolveHomePath(path: string) {
33
+ if (path.startsWith('~/')) {
34
+ return join(homedir(), path.slice(2));
35
+ }
36
+ return path;
37
+ }
38
+
39
+ function resolveBackupDir(backupDir = join(DEFAULT_RUNTIME_DIR, 'backups')) {
40
+ return resolveHomePath(backupDir);
41
+ }
42
+
43
+ export function resolveCursorAgentExecPath(
44
+ appPath: string,
45
+ targetRelativePath = CURSOR_AGENT_EXEC_RELATIVE_PATH,
46
+ ) {
47
+ return joinCursorAppPath(appPath, targetRelativePath);
48
+ }
49
+
50
+ export async function backupPathForCursorAgentExec(
51
+ appPath: string,
52
+ targetPath = resolveCursorAgentExecPath(appPath),
53
+ backupDir?: string,
54
+ ) {
55
+ const appPathHash = createHash('sha256').update(appPath).digest('hex').slice(0, 16);
56
+ const targetPathHash = createHash('sha256')
57
+ .update(targetPath)
58
+ .digest('hex')
59
+ .slice(0, 16);
60
+ return join(
61
+ resolveBackupDir(backupDir),
62
+ `${basename(appPath).replaceAll('/', '_')}-${appPathHash}-${targetPathHash}.main.js.bak`,
63
+ );
64
+ }
65
+
66
+ function defaultPatchContent(content: string) {
67
+ if (containsCursorPoolMarker(content)) {
68
+ return content;
69
+ }
70
+ return injectCursorAgentExecPatchSnippet(content);
71
+ }
72
+
73
+ function containsLegacyCursorPoolMarker(content: string) {
74
+ return containsAnyCursorPoolMarkerText(content) && !containsCursorPoolMarker(content);
75
+ }
76
+
77
+ async function readBackupContent(backupPath: string) {
78
+ try {
79
+ return await readFile(backupPath, 'utf8');
80
+ } catch (error) {
81
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
82
+ throw new Error(`Patch backup is missing: ${backupPath}`);
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ export async function validateUnpatchedBackup(backupPath: string) {
89
+ const backupContent = await readBackupContent(backupPath);
90
+ if (containsAnyCursorPoolMarkerText(backupContent)) {
91
+ throw new Error(`Patch backup contains patch marker and is unsafe: ${backupPath}`);
92
+ }
93
+ return backupContent;
94
+ }
95
+
96
+ export async function patchCursorAgentExec(
97
+ appPath: string,
98
+ options: PatchCursorAgentExecOptions = {},
99
+ ): Promise<PatchCursorAgentExecResult> {
100
+ const targetPath = resolveCursorAgentExecPath(appPath, options.targetRelativePath);
101
+ const beforeHash = await sha256File(targetPath);
102
+ const backupPath = await backupPathForCursorAgentExec(
103
+ appPath,
104
+ targetPath,
105
+ options.backupDir,
106
+ );
107
+ const content = await readFile(targetPath, 'utf8');
108
+
109
+ if (containsCursorPoolMarker(content)) {
110
+ await validateUnpatchedBackup(backupPath);
111
+ return {
112
+ targetPath,
113
+ backupPath,
114
+ beforeHash,
115
+ afterHash: beforeHash,
116
+ markerPresent: true,
117
+ };
118
+ }
119
+
120
+ await mkdir(dirname(backupPath), { recursive: true });
121
+ if (containsLegacyCursorPoolMarker(content)) {
122
+ await validateUnpatchedBackup(backupPath);
123
+ } else {
124
+ await copyFile(targetPath, backupPath);
125
+ }
126
+
127
+ try {
128
+ const patchBaseContent = containsLegacyCursorPoolMarker(content)
129
+ ? await readBackupContent(backupPath)
130
+ : content;
131
+ const patchedContent = (options.patchContent ?? defaultPatchContent)(patchBaseContent);
132
+ await writeFile(targetPath, patchedContent, 'utf8');
133
+
134
+ const afterHash = await sha256File(targetPath);
135
+ const markerPresent = containsCursorPoolMarker(
136
+ await readFile(targetPath, 'utf8'),
137
+ );
138
+
139
+ if (!markerPresent) {
140
+ throw new Error('Patch did not write a valid patch marker pair.');
141
+ }
142
+
143
+ return {
144
+ targetPath,
145
+ backupPath,
146
+ beforeHash,
147
+ afterHash,
148
+ markerPresent,
149
+ };
150
+ } catch (error) {
151
+ await copyFile(backupPath, targetPath);
152
+ throw error;
153
+ }
154
+ }
@@ -0,0 +1,142 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { copyFile, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { basename, dirname, join } from 'node:path';
5
+ import { joinCursorAppPath } from '@cursor-pool/shared/paths';
6
+ import { DEFAULT_RUNTIME_DIR } from '@cursor-pool/shared/runtime';
7
+ import {
8
+ buildCursorAlwaysLocalTakeoverPatchSnippet,
9
+ containsAnyCursorAlwaysLocalTakeoverMarkerText,
10
+ containsCursorAlwaysLocalTakeoverMarker,
11
+ } from './alwaysLocalMarker';
12
+ import { sha256File } from './hash';
13
+
14
+ export const CURSOR_ALWAYS_LOCAL_RELATIVE_PATH =
15
+ 'Contents/Resources/app/extensions/cursor-always-local/dist/main.js';
16
+
17
+ export type PatchCursorAlwaysLocalOptions = {
18
+ backupDir?: string;
19
+ patchContent?: (content: string) => string;
20
+ targetRelativePath?: string;
21
+ };
22
+
23
+ export type PatchCursorAlwaysLocalResult = {
24
+ targetPath: string;
25
+ backupPath: string;
26
+ beforeHash: string;
27
+ afterHash: string;
28
+ markerPresent: boolean;
29
+ };
30
+
31
+ function resolveHomePath(path: string) {
32
+ if (path.startsWith('~/')) {
33
+ return join(homedir(), path.slice(2));
34
+ }
35
+ return path;
36
+ }
37
+
38
+ function resolveBackupDir(backupDir = join(DEFAULT_RUNTIME_DIR, 'backups')) {
39
+ return resolveHomePath(backupDir);
40
+ }
41
+
42
+ export function resolveCursorAlwaysLocalPath(
43
+ appPath: string,
44
+ targetRelativePath = CURSOR_ALWAYS_LOCAL_RELATIVE_PATH,
45
+ ) {
46
+ return joinCursorAppPath(appPath, targetRelativePath);
47
+ }
48
+
49
+ export async function backupPathForCursorAlwaysLocal(
50
+ appPath: string,
51
+ targetPath = resolveCursorAlwaysLocalPath(appPath),
52
+ backupDir?: string,
53
+ ) {
54
+ const appPathHash = createHash('sha256').update(appPath).digest('hex').slice(0, 16);
55
+ const targetPathHash = createHash('sha256')
56
+ .update(targetPath)
57
+ .digest('hex')
58
+ .slice(0, 16);
59
+ return join(
60
+ resolveBackupDir(backupDir),
61
+ `${basename(appPath).replaceAll('/', '_')}-${appPathHash}-${targetPathHash}.always-local.main.js.bak`,
62
+ );
63
+ }
64
+
65
+ function defaultPatchContent(content: string) {
66
+ if (containsCursorAlwaysLocalTakeoverMarker(content)) {
67
+ return content;
68
+ }
69
+ return `${content}${buildCursorAlwaysLocalTakeoverPatchSnippet()}`;
70
+ }
71
+
72
+ async function readBackupContent(backupPath: string) {
73
+ try {
74
+ return await readFile(backupPath, 'utf8');
75
+ } catch (error) {
76
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
77
+ throw new Error(`Patch backup is missing: ${backupPath}`);
78
+ }
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ export async function validateUnpatchedAlwaysLocalBackup(backupPath: string) {
84
+ const backupContent = await readBackupContent(backupPath);
85
+ if (containsAnyCursorAlwaysLocalTakeoverMarkerText(backupContent)) {
86
+ throw new Error(`Patch backup contains patch marker and is unsafe: ${backupPath}`);
87
+ }
88
+ return backupContent;
89
+ }
90
+
91
+ export async function patchCursorAlwaysLocal(
92
+ appPath: string,
93
+ options: PatchCursorAlwaysLocalOptions = {},
94
+ ): Promise<PatchCursorAlwaysLocalResult> {
95
+ const targetPath = resolveCursorAlwaysLocalPath(appPath, options.targetRelativePath);
96
+ const beforeHash = await sha256File(targetPath);
97
+ const backupPath = await backupPathForCursorAlwaysLocal(
98
+ appPath,
99
+ targetPath,
100
+ options.backupDir,
101
+ );
102
+ const content = await readFile(targetPath, 'utf8');
103
+
104
+ if (containsCursorAlwaysLocalTakeoverMarker(content)) {
105
+ await validateUnpatchedAlwaysLocalBackup(backupPath);
106
+ return {
107
+ targetPath,
108
+ backupPath,
109
+ beforeHash,
110
+ afterHash: beforeHash,
111
+ markerPresent: true,
112
+ };
113
+ }
114
+
115
+ await mkdir(dirname(backupPath), { recursive: true });
116
+ await copyFile(targetPath, backupPath);
117
+
118
+ try {
119
+ const patchedContent = (options.patchContent ?? defaultPatchContent)(content);
120
+ await writeFile(targetPath, patchedContent, 'utf8');
121
+
122
+ const afterHash = await sha256File(targetPath);
123
+ const markerPresent = containsCursorAlwaysLocalTakeoverMarker(
124
+ await readFile(targetPath, 'utf8'),
125
+ );
126
+
127
+ if (!markerPresent) {
128
+ throw new Error('Patch did not write a valid patch marker pair.');
129
+ }
130
+
131
+ return {
132
+ targetPath,
133
+ backupPath,
134
+ beforeHash,
135
+ afterHash,
136
+ markerPresent,
137
+ };
138
+ } catch (error) {
139
+ await copyFile(backupPath, targetPath);
140
+ throw error;
141
+ }
142
+ }