@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.
- package/bin/cursor-pool.mjs +9 -0
- package/bin/cursor-pool.ts +169 -0
- package/node_modules/@cursor-pool/extension/dist/extension.js +2910 -0
- package/node_modules/@cursor-pool/extension/package.json +64 -0
- package/node_modules/@cursor-pool/extension/resources/cursor-pool.svg +6 -0
- package/node_modules/@cursor-pool/extension/src/api.ts +545 -0
- package/node_modules/@cursor-pool/extension/src/extension.ts +104 -0
- package/node_modules/@cursor-pool/extension/src/index.ts +1 -0
- package/node_modules/@cursor-pool/extension/src/panel.ts +569 -0
- package/node_modules/@cursor-pool/extension/src/runtime.ts +22 -0
- package/node_modules/@cursor-pool/extension/test/panel.test.ts +1785 -0
- package/node_modules/@cursor-pool/patcher/package.json +17 -0
- package/node_modules/@cursor-pool/patcher/src/alwaysLocalMarker.ts +86 -0
- package/node_modules/@cursor-pool/patcher/src/hash.ts +7 -0
- package/node_modules/@cursor-pool/patcher/src/index.ts +55 -0
- package/node_modules/@cursor-pool/patcher/src/marker.ts +159 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorAgentExec.ts +154 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorAlwaysLocal.ts +142 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorWorkbenchAuthGate.ts +140 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorAgentExec.ts +52 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorAlwaysLocal.ts +52 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorWorkbenchAuthGate.ts +70 -0
- package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +243 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +630 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorAlwaysLocal.test.ts +144 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +770 -0
- package/node_modules/@cursor-pool/patcher/test/restoreCursorAgentExec.test.ts +139 -0
- package/node_modules/@cursor-pool/service/package.json +17 -0
- package/node_modules/@cursor-pool/service/src/canary.ts +61 -0
- package/node_modules/@cursor-pool/service/src/diagnostics.ts +385 -0
- package/node_modules/@cursor-pool/service/src/entry.ts +161 -0
- package/node_modules/@cursor-pool/service/src/health.ts +10 -0
- package/node_modules/@cursor-pool/service/src/index.ts +29 -0
- package/node_modules/@cursor-pool/service/src/metadata.ts +22 -0
- package/node_modules/@cursor-pool/service/src/platformSession.ts +1178 -0
- package/node_modules/@cursor-pool/service/src/requestCheck.ts +81 -0
- package/node_modules/@cursor-pool/service/src/requestGate.ts +100 -0
- package/node_modules/@cursor-pool/service/src/requestGateway.ts +441 -0
- package/node_modules/@cursor-pool/service/src/runtime.ts +48 -0
- package/node_modules/@cursor-pool/service/src/server.ts +939 -0
- package/node_modules/@cursor-pool/service/src/takeover.ts +111 -0
- package/node_modules/@cursor-pool/service/test/canary.test.ts +140 -0
- package/node_modules/@cursor-pool/service/test/diagnostics.test.ts +506 -0
- package/node_modules/@cursor-pool/service/test/metadata.test.ts +63 -0
- package/node_modules/@cursor-pool/service/test/platformSession.test.ts +2428 -0
- package/node_modules/@cursor-pool/service/test/requestCheck.test.ts +152 -0
- package/node_modules/@cursor-pool/service/test/requestGate.test.ts +207 -0
- package/node_modules/@cursor-pool/service/test/requestGateway.test.ts +466 -0
- package/node_modules/@cursor-pool/service/test/runtime.test.ts +47 -0
- package/node_modules/@cursor-pool/service/test/server.test.ts +2570 -0
- package/node_modules/@cursor-pool/shared/package.json +17 -0
- package/node_modules/@cursor-pool/shared/src/clientConfig.ts +49 -0
- package/node_modules/@cursor-pool/shared/src/index.ts +14 -0
- package/node_modules/@cursor-pool/shared/src/manifest.ts +36 -0
- package/node_modules/@cursor-pool/shared/src/metadata.ts +19 -0
- package/node_modules/@cursor-pool/shared/src/paths.ts +5 -0
- package/node_modules/@cursor-pool/shared/src/runtime.ts +3 -0
- package/node_modules/@cursor-pool/shared/test/index.test.ts +56 -0
- package/node_modules/@cursor-pool/shared/test/manifest.test.ts +65 -0
- package/node_modules/@cursor-pool/shared/test/metadata.test.ts +25 -0
- package/node_modules/@cursor-pool/shared/test/runtime.test.ts +8 -0
- package/package.json +28 -0
- package/src/adHocResign.ts +65 -0
- package/src/autostart.ts +240 -0
- package/src/compat.ts +282 -0
- package/src/confirm.ts +76 -0
- package/src/cursor.ts +94 -0
- package/src/diagnostics.ts +558 -0
- package/src/environment.ts +18 -0
- package/src/extensionBundle.ts +111 -0
- package/src/extensionLink.ts +168 -0
- package/src/index.ts +23 -0
- package/src/install.ts +614 -0
- package/src/installRecord.ts +105 -0
- package/src/launch.ts +182 -0
- package/src/patchSet.ts +182 -0
- package/src/platform.ts +132 -0
- package/src/repair.ts +383 -0
- package/src/restore.ts +153 -0
- package/src/serviceCommands.ts +79 -0
- package/src/serviceProcess.ts +188 -0
- package/src/status.ts +241 -0
- package/src/target.ts +37 -0
- package/src/trial.ts +133 -0
- package/src/uninstall.ts +213 -0
- package/test/autostart.test.ts +151 -0
- package/test/compat.test.ts +192 -0
- package/test/confirm.test.ts +114 -0
- package/test/cursor-pool-bin.test.ts +658 -0
- package/test/cursor.test.ts +20 -0
- package/test/diagnostics.test.ts +709 -0
- package/test/e2e-install.test.ts +773 -0
- package/test/extensionBundle.test.ts +161 -0
- package/test/extensionLink.test.ts +209 -0
- package/test/install.test.ts +862 -0
- package/test/installRecord.test.ts +107 -0
- package/test/launch.test.ts +138 -0
- package/test/platform.test.ts +226 -0
- package/test/repair.test.ts +575 -0
- package/test/restore.test.ts +211 -0
- package/test/serviceCommands.test.ts +135 -0
- package/test/serviceProcess.test.ts +280 -0
- package/test/status.test.ts +615 -0
- package/test/target.test.ts +49 -0
- package/test/trial.test.ts +146 -0
package/src/uninstall.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { rm } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, normalize, resolve } from 'node:path';
|
|
3
|
+
import { sha256File } from '@cursor-pool/patcher/hash';
|
|
4
|
+
import { hasCursorPoolMarker } from '@cursor-pool/patcher/marker';
|
|
5
|
+
import { resolveCursorAgentExecPath } from '@cursor-pool/patcher';
|
|
6
|
+
import { resolveRuntimeFile } from '@cursor-pool/service';
|
|
7
|
+
import { DEFAULT_RUNTIME_FILE } from '@cursor-pool/shared/runtime';
|
|
8
|
+
import {
|
|
9
|
+
confirmRealOperation,
|
|
10
|
+
formatRealOperationSummary,
|
|
11
|
+
type ConfirmRealOperationOptions,
|
|
12
|
+
} from './confirm';
|
|
13
|
+
import { findCursor } from './cursor';
|
|
14
|
+
import { removeExtensionBundle } from './extensionBundle';
|
|
15
|
+
import {
|
|
16
|
+
LINKED_EXTENSION_DIRNAME,
|
|
17
|
+
linkedExtensionPathForDir,
|
|
18
|
+
removeLinkedExtensionBundle,
|
|
19
|
+
} from './extensionLink';
|
|
20
|
+
import {
|
|
21
|
+
deleteInstallRecord,
|
|
22
|
+
installIdForAppPath,
|
|
23
|
+
readInstallRecord,
|
|
24
|
+
type InstallRecord,
|
|
25
|
+
type InstallRecordOptions,
|
|
26
|
+
} from './installRecord';
|
|
27
|
+
import { restore } from './restore';
|
|
28
|
+
import type { RestoreOptions } from './restore';
|
|
29
|
+
import { stopRuntimeService } from './serviceProcess';
|
|
30
|
+
import { removeUserAutostart } from './autostart';
|
|
31
|
+
import { resolveCursorTarget, type CursorTargetOptions } from './target';
|
|
32
|
+
import {
|
|
33
|
+
assertDisposableCursorAppPath,
|
|
34
|
+
deleteTrialRecord,
|
|
35
|
+
readTrialRecord,
|
|
36
|
+
type TrialRecordOptions,
|
|
37
|
+
} from './trial';
|
|
38
|
+
|
|
39
|
+
export type UninstallOptions = RestoreOptions &
|
|
40
|
+
CursorTargetOptions &
|
|
41
|
+
TrialRecordOptions & {
|
|
42
|
+
runtimeFile?: string;
|
|
43
|
+
extensionInstallPath?: string;
|
|
44
|
+
cursorExtensionsDir?: string;
|
|
45
|
+
removeUserAutostart?: typeof removeUserAutostart;
|
|
46
|
+
} & InstallRecordOptions &
|
|
47
|
+
Pick<ConfirmRealOperationOptions, 'yes' | 'isInteractive' | 'askConfirmation'>;
|
|
48
|
+
|
|
49
|
+
function normalizeAppPath(path: string) {
|
|
50
|
+
return normalize(path).replace(/\/+$/, '');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assertRealUninstallInstallRecord(
|
|
54
|
+
record: InstallRecord | null,
|
|
55
|
+
cursorAppPath: string,
|
|
56
|
+
) {
|
|
57
|
+
if (!record) {
|
|
58
|
+
throw new Error('install-record: missing');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const normalizedCursorAppPath = normalizeAppPath(cursorAppPath);
|
|
62
|
+
const recordMatches =
|
|
63
|
+
record.mode === 'real' &&
|
|
64
|
+
normalizeAppPath(record.appPath) === normalizedCursorAppPath &&
|
|
65
|
+
record.installId === installIdForAppPath(cursorAppPath);
|
|
66
|
+
|
|
67
|
+
if (!recordMatches) {
|
|
68
|
+
throw new Error(`Install record does not match Cursor app: ${normalizedCursorAppPath}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return record;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function removeRecordedLinkedExtensionBundle(
|
|
75
|
+
linkedPath: string | undefined,
|
|
76
|
+
cursorExtensionsDir: string | undefined,
|
|
77
|
+
) {
|
|
78
|
+
if (!linkedPath) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (basename(resolve(linkedPath)) !== LINKED_EXTENSION_DIRNAME) {
|
|
83
|
+
throw new Error(`Unsafe linked extension path for recursive removal: ${linkedPath}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (cursorExtensionsDir) {
|
|
87
|
+
const expectedLinkedPath = resolve(linkedExtensionPathForDir(cursorExtensionsDir));
|
|
88
|
+
if (resolve(linkedPath) !== expectedLinkedPath) {
|
|
89
|
+
throw new Error(`Unsafe linked extension path: ${linkedPath}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await rm(linkedPath, { recursive: true, force: true });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await removeLinkedExtensionBundle(linkedPath);
|
|
98
|
+
return;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
const parentName = basename(dirname(resolve(linkedPath)));
|
|
101
|
+
if (!parentName.endsWith('Extensions')) {
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
await rm(linkedPath, { recursive: true, force: true });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function uninstall(options: UninstallOptions = {}) {
|
|
110
|
+
const target = resolveCursorTarget(options);
|
|
111
|
+
const appPath =
|
|
112
|
+
target.mode === 'disposable' ? assertDisposableCursorAppPath(target.appPath) : target.appPath;
|
|
113
|
+
const cursor = await findCursor({ ...options, appPath });
|
|
114
|
+
const installRecord =
|
|
115
|
+
target.mode === 'real'
|
|
116
|
+
? await readInstallRecord({ installRecordFile: options.installRecordFile })
|
|
117
|
+
: null;
|
|
118
|
+
const trialRecord =
|
|
119
|
+
target.mode === 'disposable'
|
|
120
|
+
? await readTrialRecord(cursor.appPath, {
|
|
121
|
+
trialRecordDir: options.trialRecordDir,
|
|
122
|
+
})
|
|
123
|
+
: null;
|
|
124
|
+
const realInstallRecord =
|
|
125
|
+
target.mode === 'real'
|
|
126
|
+
? assertRealUninstallInstallRecord(installRecord, cursor.appPath)
|
|
127
|
+
: null;
|
|
128
|
+
const backupDir =
|
|
129
|
+
target.mode === 'real'
|
|
130
|
+
? realInstallRecord?.backupDir
|
|
131
|
+
: options.backupDir ?? trialRecord?.backupDir;
|
|
132
|
+
const runtimeFile =
|
|
133
|
+
target.mode === 'real'
|
|
134
|
+
? realInstallRecord?.runtimeFile ?? DEFAULT_RUNTIME_FILE
|
|
135
|
+
: options.runtimeFile ?? trialRecord?.runtimeFile;
|
|
136
|
+
const extensionInstallPath =
|
|
137
|
+
target.mode === 'real'
|
|
138
|
+
? realInstallRecord?.extensionInstallPath
|
|
139
|
+
: options.extensionInstallPath ?? trialRecord?.extensionInstallPath;
|
|
140
|
+
const extensionLinkedPath =
|
|
141
|
+
target.mode === 'real'
|
|
142
|
+
? realInstallRecord?.extensionLinkedPath
|
|
143
|
+
: options.cursorExtensionsDir
|
|
144
|
+
? linkedExtensionPathForDir(options.cursorExtensionsDir)
|
|
145
|
+
: trialRecord?.extensionLinkedPath;
|
|
146
|
+
const targetRelativePath =
|
|
147
|
+
target.mode === 'real'
|
|
148
|
+
? realInstallRecord?.targetRelativePath
|
|
149
|
+
: trialRecord?.targetRelativePath;
|
|
150
|
+
const targetPath = resolveCursorAgentExecPath(cursor.appPath, targetRelativePath);
|
|
151
|
+
if (target.requiresConfirmation) {
|
|
152
|
+
const summary = {
|
|
153
|
+
operation: 'uninstall' as const,
|
|
154
|
+
appPath: cursor.appPath,
|
|
155
|
+
targetPath,
|
|
156
|
+
backupPath: backupDir,
|
|
157
|
+
runtimeFile,
|
|
158
|
+
extensionInstallPath,
|
|
159
|
+
extensionLinkedPath,
|
|
160
|
+
};
|
|
161
|
+
formatRealOperationSummary(summary);
|
|
162
|
+
await confirmRealOperation({
|
|
163
|
+
summary,
|
|
164
|
+
yes: options.yes,
|
|
165
|
+
isInteractive: options.isInteractive,
|
|
166
|
+
askConfirmation: options.askConfirmation,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const targetHash = await sha256File(targetPath);
|
|
171
|
+
const targetChangedFromRecordedOriginal =
|
|
172
|
+
target.mode === 'real'
|
|
173
|
+
? Boolean(realInstallRecord) && targetHash !== realInstallRecord?.originalSha256
|
|
174
|
+
: Boolean(trialRecord) && targetHash !== trialRecord?.originalSha256;
|
|
175
|
+
const shouldRestore =
|
|
176
|
+
(await hasCursorPoolMarker(targetPath)) || targetChangedFromRecordedOriginal;
|
|
177
|
+
const restoreOutput = shouldRestore
|
|
178
|
+
? await restore({ ...options, backupDir, yes: target.mode === 'real' ? true : options.yes })
|
|
179
|
+
: [
|
|
180
|
+
`Cursor ${cursor.version} (${cursor.commit})`,
|
|
181
|
+
`mode: ${target.mode}`,
|
|
182
|
+
`app: ${cursor.appPath}`,
|
|
183
|
+
'restore: skipped',
|
|
184
|
+
'patch: missing',
|
|
185
|
+
].join('\n');
|
|
186
|
+
const removeAutostart = options.removeUserAutostart ?? removeUserAutostart;
|
|
187
|
+
await removeAutostart();
|
|
188
|
+
if (runtimeFile) {
|
|
189
|
+
await stopRuntimeService(resolveRuntimeFile(runtimeFile));
|
|
190
|
+
await rm(resolveRuntimeFile(runtimeFile), { force: true });
|
|
191
|
+
}
|
|
192
|
+
await removeRecordedLinkedExtensionBundle(
|
|
193
|
+
extensionLinkedPath,
|
|
194
|
+
target.mode === 'real' ? undefined : options.cursorExtensionsDir,
|
|
195
|
+
);
|
|
196
|
+
if (extensionInstallPath) {
|
|
197
|
+
await removeExtensionBundle(extensionInstallPath);
|
|
198
|
+
}
|
|
199
|
+
if (target.mode === 'real') {
|
|
200
|
+
await deleteInstallRecord({ installRecordFile: options.installRecordFile });
|
|
201
|
+
} else {
|
|
202
|
+
await deleteTrialRecord(cursor.appPath, { trialRecordDir: options.trialRecordDir });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return [
|
|
206
|
+
restoreOutput,
|
|
207
|
+
extensionInstallPath || extensionLinkedPath ? 'extension: removed' : 'extension: skipped',
|
|
208
|
+
'autostart: removed',
|
|
209
|
+
runtimeFile ? 'service: stopped' : 'service: skipped',
|
|
210
|
+
target.mode === 'real' ? 'install-record: removed' : 'trial: removed',
|
|
211
|
+
'uninstall: ok',
|
|
212
|
+
].join('\n');
|
|
213
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import { installUserAutostart, removeUserAutostart } from '../src/autostart';
|
|
7
|
+
|
|
8
|
+
test('installUserAutostart writes a macOS user LaunchAgent without admin paths', async () => {
|
|
9
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-autostart-'));
|
|
10
|
+
const launchAgentFile = join(tempDir, 'home/Library/LaunchAgents/com.cursor-pool.service.plist');
|
|
11
|
+
const calls: string[][] = [];
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const result = await installUserAutostart({
|
|
15
|
+
platform: 'darwin',
|
|
16
|
+
launchAgentFile,
|
|
17
|
+
nodeCommand: '/usr/local/bin/node',
|
|
18
|
+
cliEntry: join(tempDir, 'home/.cursor-pool/client/node_modules/@cursorpool-dev/cli/bin/cursor-pool.mjs'),
|
|
19
|
+
tsxLoader: join(tempDir, 'home/.cursor-pool/client/node_modules/tsx/dist/esm/index.mjs'),
|
|
20
|
+
serviceEntry: join(tempDir, 'home/.cursor-pool/client/node_modules/@cursorpool-dev/cli/node_modules/@cursor-pool/service/src/entry.ts'),
|
|
21
|
+
runtimeFile: join(tempDir, 'home/.cursor-pool/runtime.json'),
|
|
22
|
+
serviceLogFile: join(tempDir, 'home/.cursor-pool/logs/service.log'),
|
|
23
|
+
configFile: join(tempDir, 'home/.cursor-pool/client-config.json'),
|
|
24
|
+
uid: 501,
|
|
25
|
+
execFile: async (_command, args) => {
|
|
26
|
+
calls.push(args);
|
|
27
|
+
return { stdout: '', stderr: '' };
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
assert.equal(result.state, 'installed');
|
|
32
|
+
const plist = await readFile(launchAgentFile, 'utf8');
|
|
33
|
+
assert.match(plist, /<key>Label<\/key>\s*<string>com\.cursor-pool\.service<\/string>/);
|
|
34
|
+
assert.match(plist, /<string>\/usr\/local\/bin\/node<\/string>/);
|
|
35
|
+
assert.match(plist, /<string>--import<\/string>/);
|
|
36
|
+
assert.match(plist, /<string>.*tsx\/dist\/esm\/index\.mjs<\/string>/);
|
|
37
|
+
assert.match(plist, /<string>.*service\/src\/entry\.ts<\/string>/);
|
|
38
|
+
assert.doesNotMatch(plist, /<string>start<\/string>/);
|
|
39
|
+
assert.match(plist, /<string>--runtime-file<\/string>/);
|
|
40
|
+
assert.match(plist, /<string>--service-log-file<\/string>/);
|
|
41
|
+
assert.match(plist, /<string>--config-file<\/string>/);
|
|
42
|
+
assert.doesNotMatch(plist, /sudo|\/Library\/LaunchDaemons/);
|
|
43
|
+
assert.deepEqual(calls, [
|
|
44
|
+
['bootstrap', 'gui/501', launchAgentFile],
|
|
45
|
+
['kickstart', '-k', 'gui/501/com.cursor-pool.service'],
|
|
46
|
+
]);
|
|
47
|
+
} finally {
|
|
48
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('removeUserAutostart removes the macOS user LaunchAgent idempotently', async () => {
|
|
53
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-autostart-'));
|
|
54
|
+
const launchAgentFile = join(tempDir, 'home/Library/LaunchAgents/com.cursor-pool.service.plist');
|
|
55
|
+
const calls: string[][] = [];
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await installUserAutostart({
|
|
59
|
+
platform: 'darwin',
|
|
60
|
+
launchAgentFile,
|
|
61
|
+
nodeCommand: '/usr/local/bin/node',
|
|
62
|
+
cliEntry: join(tempDir, 'cursor-pool.mjs'),
|
|
63
|
+
uid: 501,
|
|
64
|
+
execFile: async () => ({ stdout: '', stderr: '' }),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const result = await removeUserAutostart({
|
|
68
|
+
platform: 'darwin',
|
|
69
|
+
launchAgentFile,
|
|
70
|
+
uid: 501,
|
|
71
|
+
execFile: async (_command, args) => {
|
|
72
|
+
calls.push(args);
|
|
73
|
+
return { stdout: '', stderr: '' };
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
assert.equal(result.state, 'removed');
|
|
78
|
+
assert.deepEqual(calls, [['bootout', 'gui/501/com.cursor-pool.service']]);
|
|
79
|
+
await assert.rejects(readFile(launchAgentFile, 'utf8'), /ENOENT/);
|
|
80
|
+
} finally {
|
|
81
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('installUserAutostart writes a Windows user startup command', async () => {
|
|
86
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-autostart-'));
|
|
87
|
+
const startupFile = join(tempDir, 'Startup/CursorPoolService.cmd');
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const result = await installUserAutostart({
|
|
91
|
+
platform: 'win32',
|
|
92
|
+
windowsStartupFile: startupFile,
|
|
93
|
+
nodeCommand: 'C:\\Program Files\\nodejs\\node.exe',
|
|
94
|
+
cliEntry: 'C:\\Users\\demo\\.cursor-pool\\client\\node_modules\\@cursor-pool\\cli\\bin\\cursor-pool.mjs',
|
|
95
|
+
tsxLoader: 'C:\\Users\\demo\\.cursor-pool\\client\\node_modules\\tsx\\dist\\esm\\index.mjs',
|
|
96
|
+
serviceEntry: 'C:\\Users\\demo\\.cursor-pool\\client\\node_modules\\@cursor-pool\\cli\\node_modules\\@cursor-pool\\service\\src\\entry.ts',
|
|
97
|
+
runtimeFile: 'C:\\Users\\demo\\.cursor-pool\\runtime.json',
|
|
98
|
+
serviceLogFile: 'C:\\Users\\demo\\.cursor-pool\\logs\\service.log',
|
|
99
|
+
configFile: 'C:\\Users\\demo\\.cursor-pool\\client-config.json',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
assert.equal(result.state, 'installed');
|
|
103
|
+
const script = await readFile(startupFile, 'utf8');
|
|
104
|
+
assert.match(script, /start "" \/min/);
|
|
105
|
+
assert.match(script, /node\.exe/);
|
|
106
|
+
assert.match(script, /"--import"/);
|
|
107
|
+
assert.match(script, /tsx\\dist\\esm\\index\.mjs/);
|
|
108
|
+
assert.match(script, /service\\src\\entry\.ts/);
|
|
109
|
+
assert.doesNotMatch(script, /"start"/);
|
|
110
|
+
assert.match(script, /"--runtime-file"/);
|
|
111
|
+
assert.match(script, /"--config-file"/);
|
|
112
|
+
} finally {
|
|
113
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test('installUserAutostart writes a Linux user systemd service', async () => {
|
|
118
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-autostart-'));
|
|
119
|
+
const serviceFile = join(tempDir, 'systemd/cursor-pool.service');
|
|
120
|
+
const calls: string[][] = [];
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const result = await installUserAutostart({
|
|
124
|
+
platform: 'linux',
|
|
125
|
+
linuxServiceFile: serviceFile,
|
|
126
|
+
nodeCommand: '/usr/bin/node',
|
|
127
|
+
cliEntry: '/home/demo/.cursor-pool/client/node_modules/@cursorpool-dev/cli/bin/cursor-pool.mjs',
|
|
128
|
+
tsxLoader: '/home/demo/.cursor-pool/client/node_modules/tsx/dist/esm/index.mjs',
|
|
129
|
+
serviceEntry: '/home/demo/.cursor-pool/client/node_modules/@cursorpool-dev/cli/node_modules/@cursor-pool/service/src/entry.ts',
|
|
130
|
+
runtimeFile: '/home/demo/.cursor-pool/runtime.json',
|
|
131
|
+
serviceLogFile: '/home/demo/.cursor-pool/logs/service.log',
|
|
132
|
+
configFile: '/home/demo/.cursor-pool/client-config.json',
|
|
133
|
+
execFile: async (_command, args) => {
|
|
134
|
+
calls.push(args);
|
|
135
|
+
return { stdout: '', stderr: '' };
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
assert.equal(result.state, 'installed');
|
|
140
|
+
const service = await readFile(serviceFile, 'utf8');
|
|
141
|
+
assert.match(service, /ExecStart=\/usr\/bin\/node --import .*tsx\/dist\/esm\/index\.mjs .*service\/src\/entry\.ts --runtime-file/);
|
|
142
|
+
assert.match(service, /--config-file/);
|
|
143
|
+
assert.match(service, /Restart=always/);
|
|
144
|
+
assert.deepEqual(calls, [
|
|
145
|
+
['--user', 'daemon-reload'],
|
|
146
|
+
['--user', 'enable', '--now', 'cursor-pool.service'],
|
|
147
|
+
]);
|
|
148
|
+
} finally {
|
|
149
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
150
|
+
}
|
|
151
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { 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 { CURSOR_AGENT_EXEC_RELATIVE_PATH } from '../../patcher/src/patchCursorAgentExec';
|
|
7
|
+
import type { CompatibilityManifestEntry } from '../../shared/src/manifest';
|
|
8
|
+
import {
|
|
9
|
+
buildCompatManifestSignature,
|
|
10
|
+
compatManifestUrlFromApiBaseUrl,
|
|
11
|
+
DEFAULT_COMPAT_ENTRIES,
|
|
12
|
+
loadCompatEntries,
|
|
13
|
+
resolveCompatEntry,
|
|
14
|
+
verifyCompatManifestEnvelope,
|
|
15
|
+
} from '../src/compat';
|
|
16
|
+
|
|
17
|
+
function remoteRuleFor(cursorVersion: string, cursorCommit: string): CompatibilityManifestEntry {
|
|
18
|
+
return {
|
|
19
|
+
platform: 'darwin',
|
|
20
|
+
arch: 'arm64',
|
|
21
|
+
cursorVersion,
|
|
22
|
+
cursorCommit,
|
|
23
|
+
supportStatus: 'supported',
|
|
24
|
+
targetRelativePath: CURSOR_AGENT_EXEC_RELATIVE_PATH,
|
|
25
|
+
expectedSha256: 'remote-sha256',
|
|
26
|
+
structureSignature: 'remote-structure',
|
|
27
|
+
patchStrategy: 'cursor-agent-exec-snippet',
|
|
28
|
+
verifyMarker: 'cursor-pool',
|
|
29
|
+
restoreStrategy: 'external-backup',
|
|
30
|
+
minCliVersion: '0.0.0',
|
|
31
|
+
minExtensionVersion: '0.0.0',
|
|
32
|
+
minServiceVersion: '0.0.0',
|
|
33
|
+
requiresWritableAppBundle: true,
|
|
34
|
+
requiresAdHocResign: false,
|
|
35
|
+
userMessage: 'Remote Cursor build is supported.',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function signedEnvelope(version: number, rules: CompatibilityManifestEntry[]) {
|
|
40
|
+
return {
|
|
41
|
+
version,
|
|
42
|
+
signatureAlgorithm: 'hmac-sha256-dev',
|
|
43
|
+
signatureKeyId: 'dev-compatibility-key',
|
|
44
|
+
signature: buildCompatManifestSignature(version, rules),
|
|
45
|
+
rules,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test('default compatibility manifest supports Cursor 3.6.21 on macOS arm64', () => {
|
|
50
|
+
const entry = resolveCompatEntry(
|
|
51
|
+
{
|
|
52
|
+
appPath: '/Users/example/Desktop/Cursor-Pool-Agent-Canary.app',
|
|
53
|
+
version: '3.6.21',
|
|
54
|
+
commit: 'e7a7e93f4d75f8272503ecf33cedbaae10114a10',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
platform: 'darwin',
|
|
58
|
+
arch: 'arm64',
|
|
59
|
+
nodeVersion: process.version,
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
assert.equal(entry.supportStatus, 'supported');
|
|
64
|
+
assert.equal(entry.requiresAdHocResign, true);
|
|
65
|
+
assert.equal(entry.targetRelativePath, CURSOR_AGENT_EXEC_RELATIVE_PATH);
|
|
66
|
+
assert.equal(
|
|
67
|
+
entry.expectedSha256,
|
|
68
|
+
'222512631b78fddcdca3fa76c0dd458a7a86751dde19c998ceb31b3fe1905ebf',
|
|
69
|
+
);
|
|
70
|
+
assert.equal(
|
|
71
|
+
DEFAULT_COMPAT_ENTRIES.some(
|
|
72
|
+
(candidate) =>
|
|
73
|
+
candidate.cursorVersion === '3.6.21' &&
|
|
74
|
+
candidate.cursorCommit === 'e7a7e93f4d75f8272503ecf33cedbaae10114a10',
|
|
75
|
+
),
|
|
76
|
+
true,
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('default compatibility manifest supports Cursor 3.5.38 on macOS x64 for Rosetta-launched installers', () => {
|
|
81
|
+
const entry = resolveCompatEntry(
|
|
82
|
+
{
|
|
83
|
+
appPath: '/Applications/Cursor.app',
|
|
84
|
+
version: '3.5.38',
|
|
85
|
+
commit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
platform: 'darwin',
|
|
89
|
+
arch: 'x64',
|
|
90
|
+
nodeVersion: process.version,
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
assert.equal(entry.supportStatus, 'supported');
|
|
95
|
+
assert.equal(entry.requiresAdHocResign, true);
|
|
96
|
+
assert.equal(entry.targetRelativePath, CURSOR_AGENT_EXEC_RELATIVE_PATH);
|
|
97
|
+
assert.equal(
|
|
98
|
+
entry.expectedSha256,
|
|
99
|
+
'cb18f0237278884a39e2ce2b8664255e12689ad0803c20096c38e86c36acc51f',
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('verifies a remote compatibility manifest envelope with the development signature', () => {
|
|
104
|
+
const rules = [remoteRuleFor('3.7.0', 'remote-commit')];
|
|
105
|
+
const envelope = {
|
|
106
|
+
version: 7,
|
|
107
|
+
signatureAlgorithm: 'hmac-sha256-dev',
|
|
108
|
+
signatureKeyId: 'dev-compatibility-key',
|
|
109
|
+
signature: buildCompatManifestSignature(7, rules),
|
|
110
|
+
rules,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
assert.deepEqual(verifyCompatManifestEnvelope(envelope), rules);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('rejects remote compatibility manifest with an invalid signature', () => {
|
|
117
|
+
const rules = [remoteRuleFor('3.7.0', 'remote-commit')];
|
|
118
|
+
|
|
119
|
+
assert.throws(
|
|
120
|
+
() =>
|
|
121
|
+
verifyCompatManifestEnvelope({
|
|
122
|
+
version: 7,
|
|
123
|
+
signatureAlgorithm: 'hmac-sha256-dev',
|
|
124
|
+
signatureKeyId: 'dev-compatibility-key',
|
|
125
|
+
signature: 'bad',
|
|
126
|
+
rules,
|
|
127
|
+
}),
|
|
128
|
+
/compat manifest signature invalid/,
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('derives compatibility manifest url from api base url', () => {
|
|
133
|
+
assert.equal(
|
|
134
|
+
compatManifestUrlFromApiBaseUrl('https://platform.example.test/root/'),
|
|
135
|
+
'https://platform.example.test/root/api/client/compatibility/manifest',
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('loads remote compatibility entries from api base url when signature is valid', async () => {
|
|
140
|
+
const rules = [remoteRuleFor('3.7.0', 'remote-commit')];
|
|
141
|
+
const entries = await loadCompatEntries({
|
|
142
|
+
apiBaseUrl: 'https://platform.example.test/root',
|
|
143
|
+
fetchManifest: async (url) => {
|
|
144
|
+
assert.equal(url, 'https://platform.example.test/root/api/client/compatibility/manifest');
|
|
145
|
+
return { ok: true, json: async () => signedEnvelope(8, rules) };
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
assert.equal(entries[0]?.cursorVersion, '3.7.0');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('loads compatibility entries from a local file URL for disposable client validation', async () => {
|
|
153
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-compat-file-url-'));
|
|
154
|
+
const manifestPath = join(tempDir, 'compat.json');
|
|
155
|
+
const rules = [remoteRuleFor('3.9.0', 'file-commit')];
|
|
156
|
+
await writeFile(manifestPath, JSON.stringify(signedEnvelope(9, rules)), 'utf8');
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const entries = await loadCompatEntries({
|
|
160
|
+
compatManifestUrl: `file://${manifestPath}`,
|
|
161
|
+
fetchManifest: async () => {
|
|
162
|
+
throw new Error('fetch should not be called for file URLs');
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
assert.deepEqual(entries, rules);
|
|
167
|
+
} finally {
|
|
168
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('falls back to default compatibility entries when remote manifest is unavailable', async () => {
|
|
173
|
+
const entries = await loadCompatEntries({
|
|
174
|
+
apiBaseUrl: 'https://platform.example.test',
|
|
175
|
+
fetchManifest: async () => ({ ok: false, json: async () => ({}) }),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
assert.equal(entries, DEFAULT_COMPAT_ENTRIES);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('falls back to injected compatibility entries before fetching remote manifest', async () => {
|
|
182
|
+
const injected = [remoteRuleFor('3.8.0', 'injected-commit')];
|
|
183
|
+
const entries = await loadCompatEntries({
|
|
184
|
+
compatEntries: injected,
|
|
185
|
+
apiBaseUrl: 'https://platform.example.test',
|
|
186
|
+
fetchManifest: async () => {
|
|
187
|
+
throw new Error('fetch should not be called');
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
assert.equal(entries, injected);
|
|
192
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
confirmRealOperation,
|
|
5
|
+
formatRealOperationSummary,
|
|
6
|
+
} from '../src/confirm';
|
|
7
|
+
|
|
8
|
+
test('formatRealOperationSummary outputs provided operation paths', () => {
|
|
9
|
+
assert.equal(
|
|
10
|
+
formatRealOperationSummary({
|
|
11
|
+
operation: 'install',
|
|
12
|
+
appPath: '/Applications/Cursor.app',
|
|
13
|
+
targetPath: '/Applications/Cursor.app/Contents/Resources/app/out/main.js',
|
|
14
|
+
backupPath: '/tmp/backups/main.js',
|
|
15
|
+
runtimeFile: '/tmp/runtime.json',
|
|
16
|
+
extensionInstallPath: '/tmp/extensions/cursor-pool-status',
|
|
17
|
+
extensionLinkedPath: '/tmp/Cursor/extensions/cursor-pool-status',
|
|
18
|
+
restorePath: '/tmp/restore/main.js',
|
|
19
|
+
}),
|
|
20
|
+
[
|
|
21
|
+
'operation: install',
|
|
22
|
+
'app: /Applications/Cursor.app',
|
|
23
|
+
'target: /Applications/Cursor.app/Contents/Resources/app/out/main.js',
|
|
24
|
+
'backup: /tmp/backups/main.js',
|
|
25
|
+
'runtime: /tmp/runtime.json',
|
|
26
|
+
'extension-install: /tmp/extensions/cursor-pool-status',
|
|
27
|
+
'extension-linked: /tmp/Cursor/extensions/cursor-pool-status',
|
|
28
|
+
'restore: /tmp/restore/main.js',
|
|
29
|
+
].join('\n'),
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('formatRealOperationSummary omits optional paths when missing', () => {
|
|
34
|
+
assert.equal(
|
|
35
|
+
formatRealOperationSummary({
|
|
36
|
+
operation: 'restore',
|
|
37
|
+
appPath: '/Applications/Cursor.app',
|
|
38
|
+
targetPath: '/Applications/Cursor.app/target.js',
|
|
39
|
+
}),
|
|
40
|
+
[
|
|
41
|
+
'operation: restore',
|
|
42
|
+
'app: /Applications/Cursor.app',
|
|
43
|
+
'target: /Applications/Cursor.app/target.js',
|
|
44
|
+
].join('\n'),
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('confirmRealOperation allows yes without asking', async () => {
|
|
49
|
+
let asked = false;
|
|
50
|
+
|
|
51
|
+
await confirmRealOperation({
|
|
52
|
+
yes: true,
|
|
53
|
+
isInteractive: false,
|
|
54
|
+
summary: {
|
|
55
|
+
operation: 'install',
|
|
56
|
+
appPath: '/Applications/Cursor.app',
|
|
57
|
+
targetPath: '/Applications/Cursor.app/target.js',
|
|
58
|
+
},
|
|
59
|
+
askConfirmation: async () => {
|
|
60
|
+
asked = true;
|
|
61
|
+
return false;
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
assert.equal(asked, false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('confirmRealOperation rejects non-interactive real operation without yes', async () => {
|
|
69
|
+
await assert.rejects(
|
|
70
|
+
confirmRealOperation({
|
|
71
|
+
yes: false,
|
|
72
|
+
isInteractive: false,
|
|
73
|
+
summary: {
|
|
74
|
+
operation: 'uninstall',
|
|
75
|
+
appPath: '/Applications/Cursor.app',
|
|
76
|
+
targetPath: '/Applications/Cursor.app/target.js',
|
|
77
|
+
},
|
|
78
|
+
}),
|
|
79
|
+
/Pass --yes to confirm uninstall for real Cursor/,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('confirmRealOperation allows injected confirmation approval', async () => {
|
|
84
|
+
await confirmRealOperation({
|
|
85
|
+
yes: false,
|
|
86
|
+
isInteractive: true,
|
|
87
|
+
summary: {
|
|
88
|
+
operation: 'restore',
|
|
89
|
+
appPath: '/Applications/Cursor.app',
|
|
90
|
+
targetPath: '/Applications/Cursor.app/target.js',
|
|
91
|
+
},
|
|
92
|
+
askConfirmation: async (prompt) => {
|
|
93
|
+
assert.match(prompt, /operation: restore/);
|
|
94
|
+
assert.match(prompt, /Proceed\?/);
|
|
95
|
+
return true;
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('confirmRealOperation rejects injected confirmation denial', async () => {
|
|
101
|
+
await assert.rejects(
|
|
102
|
+
confirmRealOperation({
|
|
103
|
+
yes: false,
|
|
104
|
+
isInteractive: true,
|
|
105
|
+
summary: {
|
|
106
|
+
operation: 'repair',
|
|
107
|
+
appPath: '/Applications/Cursor.app',
|
|
108
|
+
targetPath: '/Applications/Cursor.app/target.js',
|
|
109
|
+
},
|
|
110
|
+
askConfirmation: async () => false,
|
|
111
|
+
}),
|
|
112
|
+
/repair cancelled/,
|
|
113
|
+
);
|
|
114
|
+
});
|