@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,105 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join, normalize } from 'node:path';
5
+ import type { CompatibilityManifestEntry } from '@cursor-pool/shared/manifest';
6
+ import type { ExtensionState } from './trial';
7
+
8
+ export type InstallMode = 'real' | 'disposable';
9
+
10
+ export type InstallRecord = {
11
+ installId: string;
12
+ mode: InstallMode;
13
+ appPath: string;
14
+ cursorVersion: string;
15
+ cursorCommit: string;
16
+ targetRelativePath: string;
17
+ originalSha256: string;
18
+ compatSupportStatus: CompatibilityManifestEntry['supportStatus'];
19
+ runtimeFile: string;
20
+ backupDir: string;
21
+ extensionInstallPath: string;
22
+ extensionLinkedPath?: string;
23
+ extensionState: ExtensionState;
24
+ cliVersion: string;
25
+ extensionVersion: string;
26
+ serviceVersion: string;
27
+ lastOperation: 'install' | 'repair' | 'restore' | 'uninstall';
28
+ lastOperationStatus: 'ok' | 'failed' | 'rolled-back';
29
+ createdAt: string;
30
+ updatedAt: string;
31
+ };
32
+
33
+ export type InstallRecordOptions = {
34
+ installRecordFile?: string;
35
+ };
36
+
37
+ export const DEFAULT_INSTALL_RECORD_FILE = '~/.cursor-pool/install.json';
38
+
39
+ function resolveHomePath(path: string) {
40
+ return path.startsWith('~/') ? join(homedir(), path.slice(2)) : path;
41
+ }
42
+
43
+ export function installRecordPath(options: InstallRecordOptions = {}) {
44
+ return resolveHomePath(options.installRecordFile ?? DEFAULT_INSTALL_RECORD_FILE);
45
+ }
46
+
47
+ export function installIdForAppPath(appPath: string) {
48
+ return createHash('sha256').update(normalize(appPath)).digest('hex').slice(0, 16);
49
+ }
50
+
51
+ export async function writeInstallRecord(
52
+ record: InstallRecord,
53
+ options: InstallRecordOptions = {},
54
+ ) {
55
+ const expectedInstallId = installIdForAppPath(record.appPath);
56
+ if (record.installId !== expectedInstallId) {
57
+ throw new Error(
58
+ `Install record id does not match app path. Expected ${expectedInstallId} for ${record.appPath}.`,
59
+ );
60
+ }
61
+
62
+ const recordPath = installRecordPath(options);
63
+ await mkdir(dirname(recordPath), { recursive: true });
64
+ await writeFile(recordPath, `${JSON.stringify(record, null, 2)}\n`, 'utf8');
65
+ }
66
+
67
+ export async function readInstallRecord(
68
+ options: InstallRecordOptions = {},
69
+ ): Promise<InstallRecord | null> {
70
+ try {
71
+ return JSON.parse(await readFile(installRecordPath(options), 'utf8')) as InstallRecord;
72
+ } catch (error) {
73
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
74
+ return null;
75
+ }
76
+ throw error;
77
+ }
78
+ }
79
+
80
+ export async function deleteInstallRecord(options: InstallRecordOptions = {}) {
81
+ await rm(installRecordPath(options), { force: true });
82
+ }
83
+
84
+ export function isInstallRecordStale(record: InstallRecord | null, actual: InstallRecord | null) {
85
+ if (!record || !actual) {
86
+ return Boolean(record) !== Boolean(actual);
87
+ }
88
+
89
+ const trackedFields: Array<keyof InstallRecord> = [
90
+ 'mode',
91
+ 'appPath',
92
+ 'cursorVersion',
93
+ 'cursorCommit',
94
+ 'targetRelativePath',
95
+ 'originalSha256',
96
+ 'compatSupportStatus',
97
+ 'runtimeFile',
98
+ 'backupDir',
99
+ 'extensionInstallPath',
100
+ 'extensionLinkedPath',
101
+ 'extensionState',
102
+ ];
103
+
104
+ return trackedFields.some((field) => record[field] !== actual[field]);
105
+ }
package/src/launch.ts ADDED
@@ -0,0 +1,182 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { homedir } from 'node:os';
3
+ import { join, resolve } from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ import { assertDisposableCursorAppPath } from './trial';
6
+
7
+ const execFileAsync = promisify(execFile);
8
+
9
+ export type LaunchOptions = {
10
+ appPath?: string;
11
+ userDataDir?: string;
12
+ cursorExtensionsDir?: string;
13
+ };
14
+
15
+ export type LaunchStatusOptions = LaunchOptions & {
16
+ psOutput?: string;
17
+ execFile?: typeof execFileAsync;
18
+ };
19
+
20
+ function expandHome(path: string) {
21
+ if (path === '~') {
22
+ return homedir();
23
+ }
24
+ if (path.startsWith('~/')) {
25
+ return join(homedir(), path.slice(2));
26
+ }
27
+ return path;
28
+ }
29
+
30
+ function quoteShell(value: string) {
31
+ return JSON.stringify(value);
32
+ }
33
+
34
+ function requirePath(value: string | undefined, flag: string) {
35
+ if (!value) {
36
+ throw new Error(`Missing required ${flag}`);
37
+ }
38
+ return resolve(expandHome(value));
39
+ }
40
+
41
+ export function buildLaunchCommand(options: {
42
+ appPath: string;
43
+ userDataDir: string;
44
+ cursorExtensionsDir: string;
45
+ }) {
46
+ const executable = join(options.appPath, 'Contents/MacOS/Cursor');
47
+ return [
48
+ `${quoteShell(executable)} \\`,
49
+ ` --user-data-dir ${quoteShell(options.userDataDir)} \\`,
50
+ ` --extensions-dir ${quoteShell(options.cursorExtensionsDir)} \\`,
51
+ ' --new-window',
52
+ ].join('\n');
53
+ }
54
+
55
+ export async function launchCommand(options: LaunchOptions = {}) {
56
+ const appPath = assertDisposableCursorAppPath(options.appPath);
57
+ const userDataDir = requirePath(options.userDataDir, '--user-data-dir');
58
+ const cursorExtensionsDir = requirePath(
59
+ options.cursorExtensionsDir,
60
+ '--cursor-extensions-dir',
61
+ );
62
+
63
+ return buildLaunchCommand({ appPath, userDataDir, cursorExtensionsDir });
64
+ }
65
+
66
+ type LaunchInspectionInput = {
67
+ psOutput: string;
68
+ appPath: string;
69
+ userDataDir: string;
70
+ cursorExtensionsDir: string;
71
+ homeDir?: string;
72
+ };
73
+
74
+ export type LaunchInspection = {
75
+ launch: 'running' | 'not-running';
76
+ app: 'matched' | 'missing';
77
+ userData: 'isolated' | 'real-cursor' | 'unknown';
78
+ extensions: 'isolated' | 'unknown';
79
+ realUserData: 'present' | 'absent';
80
+ realExtensions: 'present' | 'absent';
81
+ };
82
+
83
+ function normalizePath(path: string) {
84
+ return resolve(expandHome(path));
85
+ }
86
+
87
+ function realUserDataDir(homeDir = homedir()) {
88
+ return join(homeDir, 'Library/Application Support/Cursor');
89
+ }
90
+
91
+ function realExtensionsDir(homeDir = homedir()) {
92
+ return join(homeDir, '.cursor/extensions');
93
+ }
94
+
95
+ function commandHasPath(command: string, path: string) {
96
+ return command.includes(path);
97
+ }
98
+
99
+ function commandHasArgPath(command: string, flag: string, path: string) {
100
+ return command.includes(`${flag} ${path}`) || command.includes(`${flag}=${path}`);
101
+ }
102
+
103
+ export function inspectLaunchProcesses(input: LaunchInspectionInput): LaunchInspection {
104
+ const appPath = normalizePath(input.appPath);
105
+ const userDataDir = normalizePath(input.userDataDir);
106
+ const cursorExtensionsDir = normalizePath(input.cursorExtensionsDir);
107
+ const targetCommands = input.psOutput
108
+ .split('\n')
109
+ .map((line) => line.trim())
110
+ .filter((line) => commandHasPath(line, `${appPath}/Contents/`));
111
+
112
+ if (targetCommands.length === 0) {
113
+ return {
114
+ launch: 'not-running',
115
+ app: 'missing',
116
+ userData: 'unknown',
117
+ extensions: 'unknown',
118
+ realUserData: 'absent',
119
+ realExtensions: 'absent',
120
+ };
121
+ }
122
+
123
+ const realUserDataPath = realUserDataDir(input.homeDir);
124
+ const realExtensionsPath = realExtensionsDir(input.homeDir);
125
+ const hasIsolatedUserData = targetCommands.some((command) =>
126
+ commandHasArgPath(command, '--user-data-dir', userDataDir),
127
+ );
128
+ const hasIsolatedExtensions = targetCommands.some((command) =>
129
+ commandHasArgPath(command, '--extensions-dir', cursorExtensionsDir),
130
+ );
131
+ const hasRealUserData = targetCommands.some((command) =>
132
+ commandHasArgPath(command, '--user-data-dir', realUserDataPath),
133
+ );
134
+ const hasRealExtensions = targetCommands.some((command) =>
135
+ commandHasPath(command, realExtensionsPath),
136
+ );
137
+
138
+ return {
139
+ launch: 'running',
140
+ app: 'matched',
141
+ userData: hasRealUserData
142
+ ? 'real-cursor'
143
+ : hasIsolatedUserData
144
+ ? 'isolated'
145
+ : 'unknown',
146
+ extensions: hasIsolatedExtensions ? 'isolated' : 'unknown',
147
+ realUserData: hasRealUserData ? 'present' : 'absent',
148
+ realExtensions: hasRealExtensions ? 'present' : 'absent',
149
+ };
150
+ }
151
+
152
+ export function formatLaunchStatus(status: LaunchInspection) {
153
+ return [
154
+ `launch: ${status.launch}`,
155
+ `app: ${status.app}`,
156
+ `user-data: ${status.userData}`,
157
+ `extensions: ${status.extensions}`,
158
+ `real-user-data: ${status.realUserData}`,
159
+ `real-extensions: ${status.realExtensions}`,
160
+ ].join('\n');
161
+ }
162
+
163
+ export async function launchStatus(options: LaunchStatusOptions = {}) {
164
+ const appPath = assertDisposableCursorAppPath(options.appPath);
165
+ const userDataDir = requirePath(options.userDataDir, '--user-data-dir');
166
+ const cursorExtensionsDir = requirePath(
167
+ options.cursorExtensionsDir,
168
+ '--cursor-extensions-dir',
169
+ );
170
+ const psOutput =
171
+ options.psOutput ??
172
+ (await (options.execFile ?? execFileAsync)('ps', ['-axo', 'pid,ppid,command'])).stdout;
173
+
174
+ return formatLaunchStatus(
175
+ inspectLaunchProcesses({
176
+ psOutput,
177
+ appPath,
178
+ userDataDir,
179
+ cursorExtensionsDir,
180
+ }),
181
+ );
182
+ }
@@ -0,0 +1,182 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import {
3
+ CURSOR_AGENT_EXEC_RELATIVE_PATH,
4
+ CURSOR_WORKBENCH_RELATIVE_PATH,
5
+ patchCursorAgentExec,
6
+ patchCursorWorkbenchAuthGate,
7
+ restoreCursorAgentExec,
8
+ restoreCursorAlwaysLocal,
9
+ restoreCursorWorkbenchAuthGate,
10
+ resolveCursorAgentExecPath,
11
+ resolveCursorAlwaysLocalPath,
12
+ resolveCursorWorkbenchPath,
13
+ containsCursorAlwaysLocalTakeoverMarker,
14
+ containsCursorWorkbenchAuthGateMarker,
15
+ CURSOR_ALWAYS_LOCAL_RELATIVE_PATH,
16
+ } from '@cursor-pool/patcher';
17
+ import { containsCursorPoolMarker } from '@cursor-pool/patcher/marker';
18
+
19
+ export type CursorPatchName = 'agent-exec' | 'workbench';
20
+
21
+ export type CursorPatchState = {
22
+ name: CursorPatchName;
23
+ markerPresent: boolean;
24
+ targetPath: string;
25
+ };
26
+
27
+ export type CursorPatchSetState = {
28
+ allApplied: boolean;
29
+ appliedCount: number;
30
+ patches: CursorPatchState[];
31
+ };
32
+
33
+ export type PatchCursorSetOptions = {
34
+ backupDir?: string;
35
+ platform?: NodeJS.Platform;
36
+ agentExecTargetRelativePath?: string;
37
+ alwaysLocalTargetRelativePath?: string;
38
+ workbenchTargetRelativePath?: string;
39
+ patchCursorAgentExec?: typeof patchCursorAgentExec;
40
+ patchCursorWorkbenchAuthGate?: typeof patchCursorWorkbenchAuthGate;
41
+ };
42
+
43
+ export type RestoreCursorSetOptions = {
44
+ backupDir?: string;
45
+ platform?: NodeJS.Platform;
46
+ alwaysLocalTargetRelativePath?: string;
47
+ workbenchTargetRelativePath?: string;
48
+ restoreCursorAgentExec?: typeof restoreCursorAgentExec;
49
+ restoreCursorAlwaysLocal?: typeof restoreCursorAlwaysLocal;
50
+ restoreCursorWorkbenchAuthGate?: typeof restoreCursorWorkbenchAuthGate;
51
+ };
52
+
53
+ const LINUX_WORKBENCH_RELATIVE_PATH =
54
+ 'usr/share/cursor/resources/app/out/vs/workbench/workbench.desktop.main.js';
55
+ const LINUX_ALWAYS_LOCAL_RELATIVE_PATH =
56
+ 'usr/share/cursor/resources/app/extensions/cursor-always-local/dist/main.js';
57
+
58
+ function workbenchRelativePath(options: { platform?: NodeJS.Platform; workbenchTargetRelativePath?: string }) {
59
+ return options.workbenchTargetRelativePath ?? (options.platform === 'linux' ? LINUX_WORKBENCH_RELATIVE_PATH : CURSOR_WORKBENCH_RELATIVE_PATH);
60
+ }
61
+
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);
64
+ }
65
+
66
+ async function fileContainsMarker(
67
+ targetPath: string,
68
+ predicate: (content: string) => boolean,
69
+ ) {
70
+ try {
71
+ return predicate(await readFile(targetPath, 'utf8'));
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ export async function readCursorPatchSetState(
78
+ appPath: string,
79
+ options: { agentExecTargetRelativePath?: string; platform?: NodeJS.Platform; workbenchTargetRelativePath?: string } = {},
80
+ ): Promise<CursorPatchSetState> {
81
+ const agentExecPath = resolveCursorAgentExecPath(
82
+ appPath,
83
+ options.agentExecTargetRelativePath ?? CURSOR_AGENT_EXEC_RELATIVE_PATH,
84
+ );
85
+ const workbenchPath = resolveCursorWorkbenchPath(appPath, workbenchRelativePath(options));
86
+ const patches: CursorPatchState[] = [
87
+ {
88
+ name: 'agent-exec',
89
+ targetPath: agentExecPath,
90
+ markerPresent: await fileContainsMarker(agentExecPath, containsCursorPoolMarker),
91
+ },
92
+ {
93
+ name: 'workbench',
94
+ targetPath: workbenchPath,
95
+ markerPresent: await fileContainsMarker(
96
+ workbenchPath,
97
+ containsCursorWorkbenchAuthGateMarker,
98
+ ),
99
+ },
100
+ ];
101
+ const appliedCount = patches.filter((patch) => patch.markerPresent).length;
102
+ return {
103
+ allApplied: appliedCount === patches.length,
104
+ appliedCount,
105
+ patches,
106
+ };
107
+ }
108
+
109
+ export function formatCursorPatchSetState(state: CursorPatchSetState) {
110
+ if (state.allApplied) {
111
+ return 'applied';
112
+ }
113
+ if (state.appliedCount === 0) {
114
+ return 'missing';
115
+ }
116
+ const missing = state.patches
117
+ .filter((patch) => !patch.markerPresent)
118
+ .map((patch) => patch.name)
119
+ .join(',');
120
+ return `partial missing=${missing}`;
121
+ }
122
+
123
+ export async function patchCursorSet(appPath: string, options: PatchCursorSetOptions = {}) {
124
+ const before = await readCursorPatchSetState(appPath, {
125
+ agentExecTargetRelativePath: options.agentExecTargetRelativePath,
126
+ platform: options.platform,
127
+ workbenchTargetRelativePath: options.workbenchTargetRelativePath,
128
+ });
129
+ await (options.patchCursorAgentExec ?? patchCursorAgentExec)(appPath, {
130
+ backupDir: options.backupDir,
131
+ targetRelativePath: options.agentExecTargetRelativePath,
132
+ });
133
+ await (options.patchCursorWorkbenchAuthGate ?? patchCursorWorkbenchAuthGate)(appPath, {
134
+ backupDir: options.backupDir,
135
+ targetRelativePath: workbenchRelativePath(options),
136
+ });
137
+ const after = await readCursorPatchSetState(appPath, {
138
+ agentExecTargetRelativePath: options.agentExecTargetRelativePath,
139
+ platform: options.platform,
140
+ workbenchTargetRelativePath: options.workbenchTargetRelativePath,
141
+ });
142
+ return { before, after };
143
+ }
144
+
145
+ export async function restoreCursorSet(appPath: string, options: RestoreCursorSetOptions = {}) {
146
+ const restoreErrors: unknown[] = [];
147
+ const restore = async (operation: () => Promise<unknown>) => {
148
+ try {
149
+ await operation();
150
+ } catch (error) {
151
+ restoreErrors.push(error);
152
+ }
153
+ };
154
+
155
+ await restore(() =>
156
+ (options.restoreCursorWorkbenchAuthGate ?? restoreCursorWorkbenchAuthGate)(appPath, {
157
+ backupDir: options.backupDir,
158
+ targetRelativePath: workbenchRelativePath(options),
159
+ }),
160
+ );
161
+ const alwaysLocalPath = resolveCursorAlwaysLocalPath(appPath, alwaysLocalRelativePath(options));
162
+ const shouldRestoreAlwaysLocal =
163
+ Boolean(options.restoreCursorAlwaysLocal) ||
164
+ await fileContainsMarker(alwaysLocalPath, containsCursorAlwaysLocalTakeoverMarker);
165
+ if (shouldRestoreAlwaysLocal) {
166
+ await restore(() =>
167
+ (options.restoreCursorAlwaysLocal ?? restoreCursorAlwaysLocal)(appPath, {
168
+ backupDir: options.backupDir,
169
+ targetRelativePath: alwaysLocalRelativePath(options),
170
+ }),
171
+ );
172
+ }
173
+ await restore(() =>
174
+ (options.restoreCursorAgentExec ?? restoreCursorAgentExec)(appPath, {
175
+ backupDir: options.backupDir,
176
+ }),
177
+ );
178
+
179
+ if (restoreErrors.length > 0) {
180
+ throw new AggregateError(restoreErrors, 'Failed to restore one or more Cursor patches.');
181
+ }
182
+ }
@@ -0,0 +1,132 @@
1
+ import { arch, hostname, platform as osPlatform } from 'node:os';
2
+ import { readRuntimeInfo } from '@cursor-pool/service';
3
+ import type { PlatformStatus } from '@cursor-pool/service/platformSession';
4
+
5
+ export type PlatformCommandOptions = {
6
+ runtimeFile?: string;
7
+ sessionFile?: string;
8
+ };
9
+
10
+ export type PlatformLoginOptions = PlatformCommandOptions & {
11
+ code?: string;
12
+ apiBaseUrl?: string;
13
+ };
14
+
15
+ function formatUser(status: Extract<PlatformStatus, { state: 'logged-in' | 'offline' | 'invalid-token' }>) {
16
+ return status.user ? [`user: ${status.user.email}`] : [];
17
+ }
18
+
19
+ function formatDevice(status: Extract<PlatformStatus, { state: 'logged-in' | 'offline' | 'invalid-token' }>) {
20
+ if (!status.device) {
21
+ return [];
22
+ }
23
+
24
+ return [
25
+ `device: ${status.device.status} ${status.device.id}`,
26
+ `heartbeat: ${status.device.lastHeartbeatAt}`,
27
+ ];
28
+ }
29
+
30
+ function assertNoSessionFileOverride(options: PlatformCommandOptions) {
31
+ if (options.sessionFile) {
32
+ throw new Error('--session-file is managed by the local service; restart the service with a platform session file instead');
33
+ }
34
+ }
35
+
36
+ export function formatPlatformStatus(status: PlatformStatus) {
37
+ if (status.state === 'logged-out') {
38
+ return 'platform: logged-out';
39
+ }
40
+
41
+ return [
42
+ `platform: ${status.state}`,
43
+ ...formatUser(status),
44
+ ...formatDevice(status),
45
+ ].join('\n');
46
+ }
47
+
48
+ export function formatPlatformLogin(status: PlatformStatus) {
49
+ return formatPlatformStatus(status);
50
+ }
51
+
52
+ async function serviceBaseUrl(options: PlatformCommandOptions) {
53
+ const runtime = await readRuntimeInfo({ runtimeFile: options.runtimeFile });
54
+ if (!runtime) {
55
+ throw new Error('Cursor Pool service runtime is unavailable');
56
+ }
57
+
58
+ return `http://${runtime.host}:${runtime.port}`;
59
+ }
60
+
61
+ async function requestPlatform(
62
+ options: PlatformCommandOptions,
63
+ path: string,
64
+ requestOptions: { method?: string; body?: Record<string, unknown> } = {},
65
+ ) {
66
+ const baseUrl = await serviceBaseUrl(options);
67
+ const response = await fetch(`${baseUrl}${path}`, {
68
+ method: requestOptions.method ?? 'GET',
69
+ headers: requestOptions.body ? { 'content-type': 'application/json' } : undefined,
70
+ body: requestOptions.body ? JSON.stringify(requestOptions.body) : undefined,
71
+ });
72
+
73
+ if (!response.ok) {
74
+ throw new Error(`Cursor Pool service request failed with status ${response.status}`);
75
+ }
76
+
77
+ return (await response.json()) as PlatformStatus;
78
+ }
79
+
80
+ function buildDeviceInfo() {
81
+ return {
82
+ name: hostname(),
83
+ os: osPlatform(),
84
+ arch: arch(),
85
+ cliVersion: '0.5.6',
86
+ serviceVersion: '0.5.6',
87
+ extensionVersion: '0.5.6',
88
+ };
89
+ }
90
+
91
+ export async function login(options: PlatformLoginOptions = {}) {
92
+ assertNoSessionFileOverride(options);
93
+ if (!options.code) {
94
+ throw new Error('--code is required');
95
+ }
96
+ if (!options.apiBaseUrl) {
97
+ throw new Error('--api-base-url is required');
98
+ }
99
+
100
+ const status = await requestPlatform(options, '/platform/login', {
101
+ method: 'POST',
102
+ body: {
103
+ code: options.code,
104
+ apiBaseUrl: options.apiBaseUrl,
105
+ device: buildDeviceInfo(),
106
+ },
107
+ });
108
+
109
+ return formatPlatformLogin(status);
110
+ }
111
+
112
+ export async function whoami(options: PlatformCommandOptions = {}) {
113
+ assertNoSessionFileOverride(options);
114
+ return formatPlatformStatus(await requestPlatform(options, '/platform/status'));
115
+ }
116
+
117
+ export async function heartbeat(options: PlatformCommandOptions = {}) {
118
+ assertNoSessionFileOverride(options);
119
+ return formatPlatformStatus(
120
+ await requestPlatform(options, '/platform/heartbeat', {
121
+ method: 'POST',
122
+ body: { serviceStatus: 'running' },
123
+ }),
124
+ );
125
+ }
126
+
127
+ export async function logout(options: PlatformCommandOptions = {}) {
128
+ assertNoSessionFileOverride(options);
129
+ return formatPlatformStatus(
130
+ await requestPlatform(options, '/platform/logout', { method: 'POST' }),
131
+ );
132
+ }