@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
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFile, spawn } from 'node:child_process';
|
|
3
|
+
import { mkdir, mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
import { parseCommonOptions } from '../bin/cursor-pool';
|
|
10
|
+
|
|
11
|
+
const cliRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
const repoRoot = resolve(cliRoot, '../..');
|
|
13
|
+
const binPath = resolve(cliRoot, 'bin/cursor-pool.ts');
|
|
14
|
+
const execFileAsync = promisify(execFile);
|
|
15
|
+
|
|
16
|
+
test('CLI common options include target, record, trial, extension, and service log paths', () => {
|
|
17
|
+
assert.deepEqual(
|
|
18
|
+
parseCommonOptions([
|
|
19
|
+
'--app-path',
|
|
20
|
+
'/tmp/Cursor.app',
|
|
21
|
+
'--real-app-path',
|
|
22
|
+
'/Applications/Cursor.app',
|
|
23
|
+
'--runtime-file',
|
|
24
|
+
'/tmp/runtime.json',
|
|
25
|
+
'--backup-dir',
|
|
26
|
+
'/tmp/backups',
|
|
27
|
+
'--install-record-file',
|
|
28
|
+
'/tmp/install-record.json',
|
|
29
|
+
'--trial-record-dir',
|
|
30
|
+
'/tmp/trials',
|
|
31
|
+
'--extension-install-path',
|
|
32
|
+
'/tmp/extensions/cursor-pool-status',
|
|
33
|
+
'--cursor-extensions-dir',
|
|
34
|
+
'/tmp/Cursor-Pool-Trial-Extensions',
|
|
35
|
+
'--user-data-dir',
|
|
36
|
+
'/tmp/Cursor-Pool-Trial-UserData',
|
|
37
|
+
'--service-log-file',
|
|
38
|
+
'/tmp/service.log',
|
|
39
|
+
'--diagnostics-file',
|
|
40
|
+
'/tmp/diagnostics.jsonl',
|
|
41
|
+
'--session-file',
|
|
42
|
+
'/tmp/session.json',
|
|
43
|
+
'--code',
|
|
44
|
+
'CODE-123',
|
|
45
|
+
'--api-base-url',
|
|
46
|
+
'https://platform.example.test',
|
|
47
|
+
'--compat-manifest-url',
|
|
48
|
+
'https://cdn.example.test/compat.json',
|
|
49
|
+
'--yes',
|
|
50
|
+
]),
|
|
51
|
+
{
|
|
52
|
+
appPath: '/tmp/Cursor.app',
|
|
53
|
+
realAppPath: '/Applications/Cursor.app',
|
|
54
|
+
runtimeFile: '/tmp/runtime.json',
|
|
55
|
+
backupDir: '/tmp/backups',
|
|
56
|
+
installRecordFile: '/tmp/install-record.json',
|
|
57
|
+
trialRecordDir: '/tmp/trials',
|
|
58
|
+
extensionInstallPath: '/tmp/extensions/cursor-pool-status',
|
|
59
|
+
cursorExtensionsDir: '/tmp/Cursor-Pool-Trial-Extensions',
|
|
60
|
+
userDataDir: '/tmp/Cursor-Pool-Trial-UserData',
|
|
61
|
+
serviceLogFile: '/tmp/service.log',
|
|
62
|
+
diagnosticsFile: '/tmp/diagnostics.jsonl',
|
|
63
|
+
sessionFile: '/tmp/session.json',
|
|
64
|
+
code: 'CODE-123',
|
|
65
|
+
apiBaseUrl: 'https://platform.example.test',
|
|
66
|
+
compatManifestUrl: 'https://cdn.example.test/compat.json',
|
|
67
|
+
yes: true,
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('CLI common options reject missing values before command execution', () => {
|
|
73
|
+
assert.throws(
|
|
74
|
+
() => parseCommonOptions(['--runtime-file', '--service-log-file', '/tmp/service.log']),
|
|
75
|
+
/runtime_file_missing_value/,
|
|
76
|
+
);
|
|
77
|
+
assert.throws(
|
|
78
|
+
() => parseCommonOptions(['--code', '--api-base-url', 'https://platform.example.test']),
|
|
79
|
+
/code_missing_value/,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('CLI login command is recognized', async () => {
|
|
84
|
+
const child = spawn(
|
|
85
|
+
'pnpm',
|
|
86
|
+
[
|
|
87
|
+
'exec',
|
|
88
|
+
'tsx',
|
|
89
|
+
binPath,
|
|
90
|
+
'login',
|
|
91
|
+
'--code',
|
|
92
|
+
'CODE-123',
|
|
93
|
+
'--api-base-url',
|
|
94
|
+
'https://platform.example.test',
|
|
95
|
+
'--runtime-file',
|
|
96
|
+
'/tmp/cursor-pool-missing-runtime.json',
|
|
97
|
+
],
|
|
98
|
+
{
|
|
99
|
+
cwd: repoRoot,
|
|
100
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const stderrChunks: Buffer[] = [];
|
|
105
|
+
child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
|
|
106
|
+
|
|
107
|
+
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
108
|
+
child.once('error', reject);
|
|
109
|
+
child.once('exit', resolve);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
113
|
+
|
|
114
|
+
assert.equal(exitCode, 1);
|
|
115
|
+
assert.match(stderr, /Cursor Pool service runtime is unavailable/);
|
|
116
|
+
assert.doesNotMatch(stderr, /Unknown command: login/);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('CLI platform status commands are recognized', async () => {
|
|
120
|
+
for (const command of ['whoami', 'heartbeat', 'logout']) {
|
|
121
|
+
const child = spawn(
|
|
122
|
+
'pnpm',
|
|
123
|
+
[
|
|
124
|
+
'exec',
|
|
125
|
+
'tsx',
|
|
126
|
+
binPath,
|
|
127
|
+
command,
|
|
128
|
+
'--runtime-file',
|
|
129
|
+
`/tmp/cursor-pool-missing-${command}-runtime.json`,
|
|
130
|
+
],
|
|
131
|
+
{
|
|
132
|
+
cwd: repoRoot,
|
|
133
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const stderrChunks: Buffer[] = [];
|
|
138
|
+
child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
|
|
139
|
+
|
|
140
|
+
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
141
|
+
child.once('error', reject);
|
|
142
|
+
child.once('exit', resolve);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
146
|
+
|
|
147
|
+
assert.equal(exitCode, 1);
|
|
148
|
+
assert.match(stderr, /Cursor Pool service runtime is unavailable/);
|
|
149
|
+
assert.doesNotMatch(stderr, new RegExp(`Unknown command: ${command}`));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test('CLI stop command is recognized and idempotent without runtime', async () => {
|
|
154
|
+
const child = spawn(
|
|
155
|
+
'pnpm',
|
|
156
|
+
[
|
|
157
|
+
'exec',
|
|
158
|
+
'tsx',
|
|
159
|
+
binPath,
|
|
160
|
+
'stop',
|
|
161
|
+
'--runtime-file',
|
|
162
|
+
'/tmp/cursor-pool-missing-stop-runtime.json',
|
|
163
|
+
],
|
|
164
|
+
{
|
|
165
|
+
cwd: repoRoot,
|
|
166
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const stdoutChunks: Buffer[] = [];
|
|
171
|
+
const stderrChunks: Buffer[] = [];
|
|
172
|
+
child.stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));
|
|
173
|
+
child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
|
|
174
|
+
|
|
175
|
+
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
176
|
+
child.once('error', reject);
|
|
177
|
+
child.once('exit', resolve);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf8');
|
|
181
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
182
|
+
|
|
183
|
+
assert.equal(exitCode, 0, stderr);
|
|
184
|
+
assert.match(stdout, /service: not-running/);
|
|
185
|
+
assert.doesNotMatch(stderr, /Unknown command: stop/);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('CLI start command is recognized and starts a disposable runtime service', async () => {
|
|
189
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-bin-start-'));
|
|
190
|
+
const runtimeFile = join(tempDir, 'runtime.json');
|
|
191
|
+
const serviceLogFile = join(tempDir, 'service.log');
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const child = spawn(
|
|
195
|
+
'pnpm',
|
|
196
|
+
[
|
|
197
|
+
'exec',
|
|
198
|
+
'tsx',
|
|
199
|
+
binPath,
|
|
200
|
+
'start',
|
|
201
|
+
'--runtime-file',
|
|
202
|
+
runtimeFile,
|
|
203
|
+
'--service-log-file',
|
|
204
|
+
serviceLogFile,
|
|
205
|
+
],
|
|
206
|
+
{
|
|
207
|
+
cwd: repoRoot,
|
|
208
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const stdoutChunks: Buffer[] = [];
|
|
213
|
+
const stderrChunks: Buffer[] = [];
|
|
214
|
+
child.stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));
|
|
215
|
+
child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
|
|
216
|
+
|
|
217
|
+
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
218
|
+
child.once('error', reject);
|
|
219
|
+
child.once('exit', resolve);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf8');
|
|
223
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
224
|
+
|
|
225
|
+
assert.equal(exitCode, 0, stderr);
|
|
226
|
+
assert.match(stdout, /service: started 127\.0\.0\.1:\d+/);
|
|
227
|
+
assert.doesNotMatch(stderr, /Unknown command: start/);
|
|
228
|
+
} finally {
|
|
229
|
+
const stop = spawn(
|
|
230
|
+
'pnpm',
|
|
231
|
+
['exec', 'tsx', binPath, 'stop', '--runtime-file', runtimeFile],
|
|
232
|
+
{
|
|
233
|
+
cwd: repoRoot,
|
|
234
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
await new Promise<number | null>((resolve, reject) => {
|
|
238
|
+
stop.once('error', reject);
|
|
239
|
+
stop.once('exit', resolve);
|
|
240
|
+
});
|
|
241
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test('CLI start rejects missing runtime file value before starting service', async () => {
|
|
246
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-bin-missing-runtime-'));
|
|
247
|
+
const runtimeFile = join(tempDir, 'runtime.json');
|
|
248
|
+
const serviceLogFile = join(tempDir, 'service.log');
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const child = spawn(
|
|
252
|
+
'pnpm',
|
|
253
|
+
[
|
|
254
|
+
'exec',
|
|
255
|
+
'tsx',
|
|
256
|
+
binPath,
|
|
257
|
+
'start',
|
|
258
|
+
'--runtime-file',
|
|
259
|
+
'--service-log-file',
|
|
260
|
+
serviceLogFile,
|
|
261
|
+
],
|
|
262
|
+
{
|
|
263
|
+
cwd: repoRoot,
|
|
264
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
265
|
+
},
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const stderrChunks: Buffer[] = [];
|
|
269
|
+
child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
|
|
270
|
+
|
|
271
|
+
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
272
|
+
child.once('error', reject);
|
|
273
|
+
child.once('exit', resolve);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
277
|
+
assert.equal(exitCode, 1);
|
|
278
|
+
assert.match(stderr, /runtime_file_missing_value/);
|
|
279
|
+
await assert.rejects(readFile(runtimeFile, 'utf8'), /ENOENT/);
|
|
280
|
+
} finally {
|
|
281
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('CLI bin executes launch-command with isolated paths', async () => {
|
|
286
|
+
const child = spawn(
|
|
287
|
+
'pnpm',
|
|
288
|
+
[
|
|
289
|
+
'exec',
|
|
290
|
+
'tsx',
|
|
291
|
+
binPath,
|
|
292
|
+
'launch-command',
|
|
293
|
+
'--app-path',
|
|
294
|
+
'/Users/example/Desktop/Cursor-Pool-Trial.app',
|
|
295
|
+
'--user-data-dir',
|
|
296
|
+
'/Users/example/Desktop/Cursor-Pool-Trial-UserData',
|
|
297
|
+
'--cursor-extensions-dir',
|
|
298
|
+
'/Users/example/Desktop/Cursor-Pool-Trial-Extensions',
|
|
299
|
+
],
|
|
300
|
+
{
|
|
301
|
+
cwd: repoRoot,
|
|
302
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
303
|
+
},
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
const stdoutChunks: Buffer[] = [];
|
|
307
|
+
const stderrChunks: Buffer[] = [];
|
|
308
|
+
child.stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));
|
|
309
|
+
child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
|
|
310
|
+
|
|
311
|
+
const exitCode = await new Promise<number | null>((resolveExit, reject) => {
|
|
312
|
+
child.once('error', reject);
|
|
313
|
+
child.once('exit', resolveExit);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf8');
|
|
317
|
+
assert.equal(exitCode, 0, Buffer.concat(stderrChunks).toString('utf8'));
|
|
318
|
+
assert.match(stdout, /Contents\/MacOS\/Cursor/);
|
|
319
|
+
assert.match(stdout, /--user-data-dir/);
|
|
320
|
+
assert.match(stdout, /--extensions-dir/);
|
|
321
|
+
assert.match(stdout, /--new-window/);
|
|
322
|
+
assert.doesNotMatch(stdout, /open -n/);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test('CLI bin executes when spawned through tsx', async () => {
|
|
326
|
+
const child = spawn(
|
|
327
|
+
'pnpm',
|
|
328
|
+
[
|
|
329
|
+
'exec',
|
|
330
|
+
'tsx',
|
|
331
|
+
binPath,
|
|
332
|
+
'status',
|
|
333
|
+
'--app-path',
|
|
334
|
+
'/Applications/Cursor.app',
|
|
335
|
+
],
|
|
336
|
+
{
|
|
337
|
+
cwd: repoRoot,
|
|
338
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
339
|
+
},
|
|
340
|
+
);
|
|
341
|
+
|
|
342
|
+
const stdoutChunks: Buffer[] = [];
|
|
343
|
+
const stderrChunks: Buffer[] = [];
|
|
344
|
+
child.stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));
|
|
345
|
+
child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
|
|
346
|
+
|
|
347
|
+
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
348
|
+
child.once('error', reject);
|
|
349
|
+
child.once('exit', resolve);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
assert.equal(exitCode, 1);
|
|
353
|
+
assert.equal(Buffer.concat(stdoutChunks).toString('utf8'), '');
|
|
354
|
+
assert.match(
|
|
355
|
+
Buffer.concat(stderrChunks).toString('utf8'),
|
|
356
|
+
/Refusing to install into real Cursor app/,
|
|
357
|
+
);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test('CLI status ignores diagnostics-only limit option', async () => {
|
|
361
|
+
const child = spawn(
|
|
362
|
+
'pnpm',
|
|
363
|
+
[
|
|
364
|
+
'exec',
|
|
365
|
+
'tsx',
|
|
366
|
+
binPath,
|
|
367
|
+
'status',
|
|
368
|
+
'--app-path',
|
|
369
|
+
'/Applications/Cursor.app',
|
|
370
|
+
'--limit',
|
|
371
|
+
'0',
|
|
372
|
+
],
|
|
373
|
+
{
|
|
374
|
+
cwd: repoRoot,
|
|
375
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
376
|
+
},
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
const stderrChunks: Buffer[] = [];
|
|
380
|
+
child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
|
|
381
|
+
|
|
382
|
+
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
383
|
+
child.once('error', reject);
|
|
384
|
+
child.once('exit', resolve);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
388
|
+
|
|
389
|
+
assert.equal(exitCode, 1);
|
|
390
|
+
assert.match(stderr, /Refusing to install into real Cursor app/);
|
|
391
|
+
assert.doesNotMatch(stderr, /--limit must be a positive integer/);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test('CLI diagnostics rejects missing limit value', async () => {
|
|
395
|
+
const child = spawn(
|
|
396
|
+
'pnpm',
|
|
397
|
+
['exec', 'tsx', binPath, 'diagnostics', '--limit', '--json'],
|
|
398
|
+
{
|
|
399
|
+
cwd: repoRoot,
|
|
400
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
401
|
+
},
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
const stderrChunks: Buffer[] = [];
|
|
405
|
+
child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
|
|
406
|
+
|
|
407
|
+
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
408
|
+
child.once('error', reject);
|
|
409
|
+
child.once('exit', resolve);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
413
|
+
assert.equal(exitCode, 1);
|
|
414
|
+
assert.match(stderr, /limit_missing_value/);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
test('CLI repair command is recognized', async () => {
|
|
418
|
+
const child = spawn(
|
|
419
|
+
'pnpm',
|
|
420
|
+
[
|
|
421
|
+
'exec',
|
|
422
|
+
'tsx',
|
|
423
|
+
binPath,
|
|
424
|
+
'repair',
|
|
425
|
+
'--app-path',
|
|
426
|
+
'/Applications/Cursor.app',
|
|
427
|
+
],
|
|
428
|
+
{
|
|
429
|
+
cwd: repoRoot,
|
|
430
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
431
|
+
},
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
const stderrChunks: Buffer[] = [];
|
|
435
|
+
child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
|
|
436
|
+
|
|
437
|
+
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
438
|
+
child.once('error', reject);
|
|
439
|
+
child.once('exit', resolve);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
443
|
+
|
|
444
|
+
assert.equal(exitCode, 1);
|
|
445
|
+
assert.doesNotMatch(stderr, /Unknown command: repair/);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test('CLI bin executes when spawned through a symlinked path', async () => {
|
|
449
|
+
const tempDir = await mkdtemp(resolve(tmpdir(), 'cursor-pool-bin-symlink-'));
|
|
450
|
+
const symlinkPath = resolve(tempDir, 'cursor-pool.ts');
|
|
451
|
+
await symlink(binPath, symlinkPath);
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
const child = spawn(
|
|
455
|
+
'pnpm',
|
|
456
|
+
[
|
|
457
|
+
'exec',
|
|
458
|
+
'tsx',
|
|
459
|
+
symlinkPath,
|
|
460
|
+
'status',
|
|
461
|
+
'--app-path',
|
|
462
|
+
'/Applications/Cursor.app',
|
|
463
|
+
],
|
|
464
|
+
{
|
|
465
|
+
cwd: repoRoot,
|
|
466
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
467
|
+
},
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
const stdoutChunks: Buffer[] = [];
|
|
471
|
+
const stderrChunks: Buffer[] = [];
|
|
472
|
+
child.stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));
|
|
473
|
+
child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
|
|
474
|
+
|
|
475
|
+
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
476
|
+
child.once('error', reject);
|
|
477
|
+
child.once('exit', resolve);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
assert.equal(exitCode, 1);
|
|
481
|
+
assert.equal(Buffer.concat(stdoutChunks).toString('utf8'), '');
|
|
482
|
+
assert.match(
|
|
483
|
+
Buffer.concat(stderrChunks).toString('utf8'),
|
|
484
|
+
/Refusing to install into real Cursor app/,
|
|
485
|
+
);
|
|
486
|
+
} finally {
|
|
487
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test('CLI bin executes diagnostics with an injected diagnostics file', async () => {
|
|
492
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-bin-diagnostics-'));
|
|
493
|
+
const diagnosticsFile = join(tempDir, 'diagnostics.jsonl');
|
|
494
|
+
|
|
495
|
+
try {
|
|
496
|
+
await mkdir(dirname(diagnosticsFile), { recursive: true });
|
|
497
|
+
await writeFile(
|
|
498
|
+
diagnosticsFile,
|
|
499
|
+
`${JSON.stringify({
|
|
500
|
+
requestId: 'req-1',
|
|
501
|
+
requestType: 'agent',
|
|
502
|
+
source: 'cursor-agent-exec',
|
|
503
|
+
model: 'unknown',
|
|
504
|
+
receivedAt: '2026-05-30T00:00:01.000Z',
|
|
505
|
+
runtimeId: 'runtime-1',
|
|
506
|
+
prompt: 'do not print',
|
|
507
|
+
apiKey: 'secret',
|
|
508
|
+
})}\n`,
|
|
509
|
+
'utf8',
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
const child = spawn(
|
|
513
|
+
'pnpm',
|
|
514
|
+
[
|
|
515
|
+
'exec',
|
|
516
|
+
'tsx',
|
|
517
|
+
binPath,
|
|
518
|
+
'diagnostics',
|
|
519
|
+
'--diagnostics-file',
|
|
520
|
+
diagnosticsFile,
|
|
521
|
+
],
|
|
522
|
+
{
|
|
523
|
+
cwd: repoRoot,
|
|
524
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
525
|
+
},
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const stdoutChunks: Buffer[] = [];
|
|
529
|
+
const stderrChunks: Buffer[] = [];
|
|
530
|
+
child.stdout.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));
|
|
531
|
+
child.stderr.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
|
|
532
|
+
|
|
533
|
+
const exitCode = await new Promise<number | null>((resolve, reject) => {
|
|
534
|
+
child.once('error', reject);
|
|
535
|
+
child.once('exit', resolve);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const stdout = Buffer.concat(stdoutChunks).toString('utf8');
|
|
539
|
+
const stderr = Buffer.concat(stderrChunks).toString('utf8');
|
|
540
|
+
|
|
541
|
+
assert.equal(exitCode, 0, stderr);
|
|
542
|
+
assert.match(stdout, /diagnostics: 1 entry/);
|
|
543
|
+
assert.match(stdout, /requestId=req-1/);
|
|
544
|
+
assert.doesNotMatch(stdout, /prompt|secret/);
|
|
545
|
+
} finally {
|
|
546
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
test('packed npm client runs from a clean user project without global tsx', async () => {
|
|
551
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-packed-client-'));
|
|
552
|
+
const packDir = join(tempDir, 'packages');
|
|
553
|
+
const userDir = join(tempDir, 'user');
|
|
554
|
+
await mkdir(packDir, { recursive: true });
|
|
555
|
+
await mkdir(userDir, { recursive: true });
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
const packageDirs = ['shared', 'patcher', 'service', 'extension', 'cli'];
|
|
559
|
+
const tarballs: string[] = [];
|
|
560
|
+
for (const name of packageDirs) {
|
|
561
|
+
const { stdout } = await execFileAsync(
|
|
562
|
+
'pnpm',
|
|
563
|
+
['pack', '--pack-destination', packDir],
|
|
564
|
+
{ cwd: join(repoRoot, 'packages', name) },
|
|
565
|
+
);
|
|
566
|
+
tarballs.push(stdout.trim().split('\n').at(-1) ?? '');
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
await writeFile(join(userDir, 'package.json'), '{"private":true}\n', 'utf8');
|
|
570
|
+
await execFileAsync('npm', ['install', ...tarballs], { cwd: userDir });
|
|
571
|
+
|
|
572
|
+
let result:
|
|
573
|
+
| { code?: number; stdout: string; stderr: string }
|
|
574
|
+
| { code: number; stdout?: string; stderr?: string };
|
|
575
|
+
try {
|
|
576
|
+
result = await execFileAsync(
|
|
577
|
+
join(userDir, 'node_modules/.bin/cursor-pool'),
|
|
578
|
+
['status', '--app-path', '/Applications/Cursor.app'],
|
|
579
|
+
{ cwd: userDir },
|
|
580
|
+
);
|
|
581
|
+
} catch (error) {
|
|
582
|
+
result = error as { code: number; stdout?: string; stderr?: string };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
assert.equal(result.code, 1);
|
|
586
|
+
assert.match(result.stderr ?? '', /Refusing to install into real Cursor app/);
|
|
587
|
+
assert.doesNotMatch(result.stderr ?? '', /tsx: No such file or directory/);
|
|
588
|
+
assert.doesNotMatch(result.stderr ?? '', /Cannot find module/);
|
|
589
|
+
} finally {
|
|
590
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
test('single packed npm client installs from a clean user project', async () => {
|
|
595
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-single-packed-client-'));
|
|
596
|
+
const packDir = join(tempDir, 'packages');
|
|
597
|
+
const userDir = join(tempDir, 'user');
|
|
598
|
+
await mkdir(packDir, { recursive: true });
|
|
599
|
+
await mkdir(userDir, { recursive: true });
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
const { stdout } = await execFileAsync(
|
|
603
|
+
'pnpm',
|
|
604
|
+
['pack', '--pack-destination', packDir],
|
|
605
|
+
{ cwd: cliRoot },
|
|
606
|
+
);
|
|
607
|
+
const tarball = stdout.trim().split('\n').at(-1) ?? '';
|
|
608
|
+
|
|
609
|
+
await writeFile(join(userDir, 'package.json'), '{"private":true}\n', 'utf8');
|
|
610
|
+
await execFileAsync('npm', ['install', tarball], { cwd: userDir });
|
|
611
|
+
const runtimeFile = join(userDir, 'runtime.json');
|
|
612
|
+
const serviceLogFile = join(userDir, 'service.log');
|
|
613
|
+
const clientBin = join(userDir, 'node_modules/.bin/cursor-pool');
|
|
614
|
+
|
|
615
|
+
try {
|
|
616
|
+
const start = await execFileAsync(
|
|
617
|
+
clientBin,
|
|
618
|
+
['start', '--runtime-file', runtimeFile, '--service-log-file', serviceLogFile],
|
|
619
|
+
{ cwd: userDir },
|
|
620
|
+
);
|
|
621
|
+
assert.match(start.stdout, /service: started 127\.0\.0\.1:\d+/);
|
|
622
|
+
assert.doesNotMatch(start.stderr, /Cannot find module|tsx: No such file or directory/);
|
|
623
|
+
|
|
624
|
+
const runningStatus = await execFileAsync(
|
|
625
|
+
clientBin,
|
|
626
|
+
['status', '--runtime-file', runtimeFile],
|
|
627
|
+
{ cwd: userDir },
|
|
628
|
+
);
|
|
629
|
+
assert.match(runningStatus.stdout, /service: running/);
|
|
630
|
+
} finally {
|
|
631
|
+
await execFileAsync(
|
|
632
|
+
clientBin,
|
|
633
|
+
['stop', '--runtime-file', runtimeFile],
|
|
634
|
+
{ cwd: userDir },
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
let result:
|
|
639
|
+
| { code?: number; stdout: string; stderr: string }
|
|
640
|
+
| { code: number; stdout?: string; stderr?: string };
|
|
641
|
+
try {
|
|
642
|
+
result = await execFileAsync(
|
|
643
|
+
clientBin,
|
|
644
|
+
['status', '--app-path', '/Applications/Cursor.app'],
|
|
645
|
+
{ cwd: userDir },
|
|
646
|
+
);
|
|
647
|
+
} catch (error) {
|
|
648
|
+
result = error as { code: number; stdout?: string; stderr?: string };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
assert.equal(result.code, 1);
|
|
652
|
+
assert.match(result.stderr ?? '', /Refusing to install into real Cursor app/);
|
|
653
|
+
assert.doesNotMatch(result.stderr ?? '', /tsx: No such file or directory/);
|
|
654
|
+
assert.doesNotMatch(result.stderr ?? '', /Cannot find module/);
|
|
655
|
+
} finally {
|
|
656
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
657
|
+
}
|
|
658
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { defaultCursorAppPath } from '../src/cursor';
|
|
4
|
+
|
|
5
|
+
test('defaultCursorAppPath resolves the user-level Windows Cursor install directory', () => {
|
|
6
|
+
assert.equal(
|
|
7
|
+
defaultCursorAppPath({
|
|
8
|
+
platform: 'win32',
|
|
9
|
+
env: { LOCALAPPDATA: 'C:\\Users\\dev\\AppData\\Local' },
|
|
10
|
+
}),
|
|
11
|
+
'C:\\Users\\dev\\AppData\\Local\\Programs\\Cursor',
|
|
12
|
+
);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('defaultCursorAppPath gives a helpful Windows error when LOCALAPPDATA is unavailable', () => {
|
|
16
|
+
assert.throws(
|
|
17
|
+
() => defaultCursorAppPath({ platform: 'win32', env: {} }),
|
|
18
|
+
/LOCALAPPDATA is required to auto-detect Cursor on Windows/,
|
|
19
|
+
);
|
|
20
|
+
});
|