@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,615 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { createServer, type Server } from 'node:http';
|
|
4
|
+
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import test from 'node:test';
|
|
8
|
+
import { CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR } from '../../patcher/src/marker';
|
|
9
|
+
import { patchCursorAgentExec } from '../../patcher/src/patchCursorAgentExec';
|
|
10
|
+
import { patchCursorAlwaysLocal } from '../../patcher/src/patchCursorAlwaysLocal';
|
|
11
|
+
import { patchCursorWorkbenchAuthGate } from '../../patcher/src/patchCursorWorkbenchAuthGate';
|
|
12
|
+
import { startServer } from '../../service/src/server';
|
|
13
|
+
import { writeRuntimeInfo } from '../../service/src/runtime';
|
|
14
|
+
import type { CompatibilityManifestEntry } from '../../shared/src/manifest';
|
|
15
|
+
import { bundleExtension } from '../src/extensionBundle';
|
|
16
|
+
import { linkExtensionBundle, linkedExtensionPathForDir } from '../src/extensionLink';
|
|
17
|
+
import {
|
|
18
|
+
installIdForAppPath,
|
|
19
|
+
writeInstallRecord,
|
|
20
|
+
type InstallRecord,
|
|
21
|
+
} from '../src/installRecord';
|
|
22
|
+
import { status } from '../src/status';
|
|
23
|
+
import { trialIdForAppPath, writeTrialRecord } from '../src/trial';
|
|
24
|
+
|
|
25
|
+
const targetRelativePath =
|
|
26
|
+
'Contents/Resources/app/extensions/cursor-agent-exec/dist/main.js';
|
|
27
|
+
const alwaysLocalRelativePath =
|
|
28
|
+
'Contents/Resources/app/extensions/cursor-always-local/dist/main.js';
|
|
29
|
+
const workbenchRelativePath = 'Contents/Resources/app/out/vs/workbench/workbench.desktop.main.js';
|
|
30
|
+
const composerAuthGateAnchor =
|
|
31
|
+
'get when(){return p()&&!fzC},get fallback(){return he(DGC,{})}';
|
|
32
|
+
const composerSubmitAuthGateAnchor =
|
|
33
|
+
'if(!p()){e.cursorAuthenticationService.login(),e.commandService.executeCommand(wV,"general");return}';
|
|
34
|
+
const agentLoopRunAnchor = 'await this.agentClientService.run(te,H,$e,Ne,Ce,T,ze,me,we,[],ct)';
|
|
35
|
+
const buildFlagsLocalModeAnchor = 'localMode:!1';
|
|
36
|
+
const localProviderConfigAnchor =
|
|
37
|
+
'async getLocalAgentProviderConfig(e){const t="[AgentClientService][getLocalAgentProviderConfig]",i=L0.localMode?await this.shellEnvironmentService.getShellEnv():{},r=e?.credentials,s=r?.case==="apiKeyCredentials"?r.value:void 0,o=this.reactiveStorageService.applicationUserPersistentStorage,a=o.useOpenAIKey===!0?this.cursorAuthenticationService.openAIKey()??void 0:void 0,u=xgS({apiKeyCandidates:[{value:s?.apiKey,source:"modelDetails.apiKeyCredentials.apiKey"},{value:a,source:"storage.openAIKey"}],baseUrlCandidates:[{value:s?.baseUrl,source:"modelDetails.apiKeyCredentials.baseUrl"},{value:o.openAIBaseUrl,source:"storage.openAIBaseUrl"}]});return{baseUrl:u.baseUrl,apiKey:u.apiKey}}createDefaultLocalModel(e){return "default"}';
|
|
38
|
+
const agentClientRunLocalModeAnchor =
|
|
39
|
+
'if(L0.localMode){try{g.onNetworkPhaseStart?.()}catch(b){this.logService.warn("[AgentClientService] onNetworkPhaseStart callback failed in local mode",b)}return this.runLocalAgentInExtensionHost(e,t,i,r,s,a,d,h,g)}return this.client.run(e,t,i,r,s,o,a,u,d,h,g)';
|
|
40
|
+
|
|
41
|
+
function workbenchFixture() {
|
|
42
|
+
return [
|
|
43
|
+
`function composer(){return he(Mt,{${composerAuthGateAnchor},get children(){return "controls"}})}`,
|
|
44
|
+
`async function submit(){${composerSubmitAuthGateAnchor};return "submitted";}`,
|
|
45
|
+
`const flags={${buildFlagsLocalModeAnchor}}`,
|
|
46
|
+
localProviderConfigAnchor,
|
|
47
|
+
`async function runAgentLoop(){${agentLoopRunAnchor}}`,
|
|
48
|
+
`async function agentClientRun(){const g={};${agentClientRunLocalModeAnchor}}`,
|
|
49
|
+
].join(';');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function patchFullSet(appPath: string, backupDir: string) {
|
|
53
|
+
await patchCursorAgentExec(appPath, { backupDir });
|
|
54
|
+
await patchCursorAlwaysLocal(appPath, { backupDir });
|
|
55
|
+
await patchCursorWorkbenchAuthGate(appPath, { backupDir });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function listen(server: Server) {
|
|
59
|
+
await new Promise<void>((resolve, reject) => {
|
|
60
|
+
server.once('error', reject);
|
|
61
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const address = server.address();
|
|
65
|
+
assert.equal(typeof address, 'object');
|
|
66
|
+
assert.ok(address);
|
|
67
|
+
return address.port;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function close(server: Server) {
|
|
71
|
+
await new Promise<void>((resolve, reject) => {
|
|
72
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function startStatusRuntime(
|
|
77
|
+
latestTakeover: unknown,
|
|
78
|
+
runtimeId = 'status-runtime',
|
|
79
|
+
) {
|
|
80
|
+
const server = createServer((request, response) => {
|
|
81
|
+
if (request.method === 'GET' && request.url === '/health') {
|
|
82
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
83
|
+
response.end(JSON.stringify({ ok: true, runtimeId }));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (request.method === 'GET' && request.url === '/agent/takeover/latest') {
|
|
88
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
89
|
+
response.end(JSON.stringify(latestTakeover));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (request.method === 'GET' && request.url === '/platform/status') {
|
|
94
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
95
|
+
response.end(JSON.stringify({ state: 'logged-out' }));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
response.writeHead(404, { 'content-type': 'application/json' });
|
|
100
|
+
response.end(JSON.stringify({ ok: false }));
|
|
101
|
+
});
|
|
102
|
+
const port = await listen(server);
|
|
103
|
+
|
|
104
|
+
return { server, port, runtimeId };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function createFixtureApp() {
|
|
108
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-cli-status-'));
|
|
109
|
+
const appPath = join(tempDir, 'Cursor.app');
|
|
110
|
+
const targetPath = join(appPath, targetRelativePath);
|
|
111
|
+
const targetContent = `function main() { return "agent"; }\n${CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR}\nmain();\n`;
|
|
112
|
+
await mkdir(join(appPath, 'Contents/Resources/app/extensions/cursor-agent-exec/dist'), {
|
|
113
|
+
recursive: true,
|
|
114
|
+
});
|
|
115
|
+
await mkdir(join(appPath, 'Contents/Resources/app/extensions/cursor-always-local/dist'), {
|
|
116
|
+
recursive: true,
|
|
117
|
+
});
|
|
118
|
+
await mkdir(join(appPath, 'Contents/Resources/app/out/vs/workbench'), {
|
|
119
|
+
recursive: true,
|
|
120
|
+
});
|
|
121
|
+
await writeFile(
|
|
122
|
+
join(appPath, 'Contents/Resources/app/product.json'),
|
|
123
|
+
JSON.stringify({
|
|
124
|
+
version: '3.5.38',
|
|
125
|
+
commit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
|
|
126
|
+
}),
|
|
127
|
+
'utf8',
|
|
128
|
+
);
|
|
129
|
+
await writeFile(targetPath, targetContent, 'utf8');
|
|
130
|
+
await writeFile(join(appPath, alwaysLocalRelativePath), 'function alwaysLocal(){}\n', 'utf8');
|
|
131
|
+
await writeFile(join(appPath, workbenchRelativePath), workbenchFixture(), 'utf8');
|
|
132
|
+
|
|
133
|
+
const compatEntry: CompatibilityManifestEntry = {
|
|
134
|
+
platform: process.platform,
|
|
135
|
+
arch: process.arch,
|
|
136
|
+
cursorVersion: '3.5.38',
|
|
137
|
+
cursorCommit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
|
|
138
|
+
supportStatus: 'supported',
|
|
139
|
+
targetRelativePath,
|
|
140
|
+
expectedSha256: createHash('sha256').update(targetContent).digest('hex'),
|
|
141
|
+
structureSignature: 'fixture',
|
|
142
|
+
patchStrategy: 'cursor-agent-exec-snippet',
|
|
143
|
+
verifyMarker: 'cursor-pool',
|
|
144
|
+
restoreStrategy: 'external-backup',
|
|
145
|
+
minCliVersion: '0.0.0',
|
|
146
|
+
minExtensionVersion: '0.0.0',
|
|
147
|
+
minServiceVersion: '0.0.0',
|
|
148
|
+
requiresWritableAppBundle: true,
|
|
149
|
+
requiresAdHocResign: false,
|
|
150
|
+
userMessage: 'fixture supported',
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
appPath,
|
|
155
|
+
tempDir,
|
|
156
|
+
runtimeFile: join(tempDir, 'runtime.json'),
|
|
157
|
+
backupDir: join(tempDir, 'backups'),
|
|
158
|
+
compatEntry,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
test('status reports Cursor version, compatibility, patch, and runtime service state', async () => {
|
|
163
|
+
const fixture = await createFixtureApp();
|
|
164
|
+
const service = await startServer({ runtimeFile: fixture.runtimeFile });
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
168
|
+
|
|
169
|
+
const output = await status({
|
|
170
|
+
appPath: fixture.appPath,
|
|
171
|
+
runtimeFile: fixture.runtimeFile,
|
|
172
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
173
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
174
|
+
compatEntries: [fixture.compatEntry],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
assert.match(output, /Cursor 3\.5\.38/);
|
|
178
|
+
assert.match(output, /mode: disposable/);
|
|
179
|
+
assert.match(output, /app: .*Cursor\.app/);
|
|
180
|
+
assert.match(output, /compat: supported/);
|
|
181
|
+
assert.match(output, /patch: applied/);
|
|
182
|
+
assert.match(output, /service: running/);
|
|
183
|
+
assert.match(output, /extension: missing/);
|
|
184
|
+
assert.match(output, /runtime: /);
|
|
185
|
+
assert.match(output, /trial: missing/);
|
|
186
|
+
assert.match(output, /takeover: missing/);
|
|
187
|
+
assert.doesNotMatch(output, /canary:|request-check:|request-gateway:/);
|
|
188
|
+
} finally {
|
|
189
|
+
await service.stop();
|
|
190
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('status reports logged-out platform state without requiring runtime service', async () => {
|
|
195
|
+
const fixture = await createFixtureApp();
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
199
|
+
|
|
200
|
+
const output = await status({
|
|
201
|
+
appPath: fixture.appPath,
|
|
202
|
+
runtimeFile: fixture.runtimeFile,
|
|
203
|
+
platformSessionFile: join(fixture.tempDir, 'missing-platform-session.json'),
|
|
204
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
205
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
206
|
+
compatEntries: [fixture.compatEntry],
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
assert.match(output, /service: stopped/);
|
|
210
|
+
assert.match(output, /platform: logged-out/);
|
|
211
|
+
} finally {
|
|
212
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('status reports logged-in platform user, device, and heartbeat', async () => {
|
|
217
|
+
const fixture = await createFixtureApp();
|
|
218
|
+
const platformSessionFile = join(fixture.tempDir, 'platform-session.json');
|
|
219
|
+
let service: Awaited<ReturnType<typeof startServer>> | undefined;
|
|
220
|
+
const server = createServer((request, response) => {
|
|
221
|
+
if (request.method === 'GET' && request.url === '/me') {
|
|
222
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
223
|
+
response.end(
|
|
224
|
+
JSON.stringify({
|
|
225
|
+
user: { id: 'user-1', email: 'user@example.test' },
|
|
226
|
+
device: {
|
|
227
|
+
id: 'device-1',
|
|
228
|
+
status: 'active',
|
|
229
|
+
lastHeartbeatAt: '2026-05-30T08:00:00.000Z',
|
|
230
|
+
},
|
|
231
|
+
}),
|
|
232
|
+
);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
response.writeHead(404, { 'content-type': 'application/json' });
|
|
237
|
+
response.end(JSON.stringify({ ok: false }));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const port = await listen(server);
|
|
242
|
+
await writeFile(
|
|
243
|
+
platformSessionFile,
|
|
244
|
+
JSON.stringify({
|
|
245
|
+
apiBaseUrl: `http://127.0.0.1:${port}`,
|
|
246
|
+
deviceToken: 'device-token',
|
|
247
|
+
createdAt: '2026-05-30T00:00:00.000Z',
|
|
248
|
+
user: { id: 'old-user', email: 'old@example.test' },
|
|
249
|
+
device: {
|
|
250
|
+
id: 'old-device',
|
|
251
|
+
status: 'pending',
|
|
252
|
+
lastHeartbeatAt: '2026-05-29T00:00:00.000Z',
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
'utf8',
|
|
256
|
+
);
|
|
257
|
+
service = await startServer({
|
|
258
|
+
runtimeFile: fixture.runtimeFile,
|
|
259
|
+
platformSessionFile,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const output = await status({
|
|
263
|
+
appPath: fixture.appPath,
|
|
264
|
+
runtimeFile: fixture.runtimeFile,
|
|
265
|
+
platformSessionFile,
|
|
266
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
267
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
268
|
+
compatEntries: [fixture.compatEntry],
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
assert.match(output, /platform: logged-in/);
|
|
272
|
+
assert.match(output, /user: user@example\.test/);
|
|
273
|
+
assert.match(output, /device: active device-1/);
|
|
274
|
+
assert.match(output, /heartbeat: 2026-05-30T08:00:00\.000Z/);
|
|
275
|
+
} finally {
|
|
276
|
+
await service?.stop();
|
|
277
|
+
await close(server);
|
|
278
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('status uses local platform session cache without calling platform API when service is stopped', async () => {
|
|
283
|
+
const fixture = await createFixtureApp();
|
|
284
|
+
const platformSessionFile = join(fixture.tempDir, 'platform-session.json');
|
|
285
|
+
let requestedMe = false;
|
|
286
|
+
const server = createServer((request, response) => {
|
|
287
|
+
if (request.method === 'GET' && request.url === '/me') {
|
|
288
|
+
requestedMe = true;
|
|
289
|
+
response.writeHead(200, { 'content-type': 'application/json' });
|
|
290
|
+
response.end(JSON.stringify({ ok: true }));
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
response.writeHead(404, { 'content-type': 'application/json' });
|
|
295
|
+
response.end(JSON.stringify({ ok: false }));
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const port = await listen(server);
|
|
300
|
+
await writeFile(
|
|
301
|
+
platformSessionFile,
|
|
302
|
+
JSON.stringify({
|
|
303
|
+
apiBaseUrl: `http://127.0.0.1:${port}`,
|
|
304
|
+
deviceToken: 'device-token',
|
|
305
|
+
createdAt: '2026-05-30T00:00:00.000Z',
|
|
306
|
+
user: { id: 'user-1', email: 'cached@example.test' },
|
|
307
|
+
device: {
|
|
308
|
+
id: 'device-1',
|
|
309
|
+
status: 'active',
|
|
310
|
+
lastHeartbeatAt: '2026-05-30T08:00:00.000Z',
|
|
311
|
+
},
|
|
312
|
+
}),
|
|
313
|
+
'utf8',
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
const output = await status({
|
|
317
|
+
appPath: fixture.appPath,
|
|
318
|
+
runtimeFile: fixture.runtimeFile,
|
|
319
|
+
platformSessionFile,
|
|
320
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
321
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
322
|
+
compatEntries: [fixture.compatEntry],
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
assert.equal(requestedMe, false);
|
|
326
|
+
assert.match(output, /service: stopped/);
|
|
327
|
+
assert.match(output, /platform: offline/);
|
|
328
|
+
assert.match(output, /user: cached@example\.test/);
|
|
329
|
+
assert.match(output, /device: active device-1/);
|
|
330
|
+
} finally {
|
|
331
|
+
await close(server);
|
|
332
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('status reports stopped when runtime file points at an unreachable service', async () => {
|
|
337
|
+
const fixture = await createFixtureApp();
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
341
|
+
await writeRuntimeInfo(
|
|
342
|
+
{ host: '127.0.0.1', port: 45123, runtimeId: 'stale-runtime-fixture' },
|
|
343
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const output = await status({
|
|
347
|
+
appPath: fixture.appPath,
|
|
348
|
+
runtimeFile: fixture.runtimeFile,
|
|
349
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
350
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
351
|
+
compatEntries: [fixture.compatEntry],
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
assert.match(output, /patch: applied/);
|
|
355
|
+
assert.match(output, /service: stopped/);
|
|
356
|
+
assert.match(output, /extension: missing/);
|
|
357
|
+
assert.match(output, /mode: disposable/);
|
|
358
|
+
assert.match(output, /trial: missing/);
|
|
359
|
+
assert.match(output, /takeover: unavailable/);
|
|
360
|
+
assert.doesNotMatch(output, /canary:|request-check:|request-gateway:/);
|
|
361
|
+
assert.doesNotMatch(output, /service: running/);
|
|
362
|
+
} finally {
|
|
363
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test('status reports latest answered takeover without sensitive response content', async () => {
|
|
368
|
+
const fixture = await createFixtureApp();
|
|
369
|
+
const runtime = await startStatusRuntime({
|
|
370
|
+
ok: true,
|
|
371
|
+
takeover: {
|
|
372
|
+
state: 'answered',
|
|
373
|
+
requestId: 'req-takeover-1',
|
|
374
|
+
source: 'cursor-agent-exec',
|
|
375
|
+
model: 'gpt-test',
|
|
376
|
+
content: '来自 Cursor Pool 本地测试 provider',
|
|
377
|
+
prompt: 'must not appear',
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
383
|
+
await writeRuntimeInfo(
|
|
384
|
+
{ host: '127.0.0.1', port: runtime.port, runtimeId: runtime.runtimeId },
|
|
385
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
const output = await status({
|
|
389
|
+
appPath: fixture.appPath,
|
|
390
|
+
runtimeFile: fixture.runtimeFile,
|
|
391
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
392
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
393
|
+
compatEntries: [fixture.compatEntry],
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
assert.match(
|
|
397
|
+
output,
|
|
398
|
+
/takeover: answered requestId=req-takeover-1 source=cursor-agent-exec model=gpt-test content=present/,
|
|
399
|
+
);
|
|
400
|
+
assert.doesNotMatch(output, /来自 Cursor Pool 本地测试 provider|must not appear|prompt/);
|
|
401
|
+
} finally {
|
|
402
|
+
await close(runtime.server);
|
|
403
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('status reports latest rejected takeover reason', async () => {
|
|
408
|
+
const fixture = await createFixtureApp();
|
|
409
|
+
const runtime = await startStatusRuntime({
|
|
410
|
+
ok: true,
|
|
411
|
+
takeover: {
|
|
412
|
+
state: 'rejected',
|
|
413
|
+
requestId: 'req-takeover-2',
|
|
414
|
+
reason: 'mode-released',
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
420
|
+
await writeRuntimeInfo(
|
|
421
|
+
{ host: '127.0.0.1', port: runtime.port, runtimeId: runtime.runtimeId },
|
|
422
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
const output = await status({
|
|
426
|
+
appPath: fixture.appPath,
|
|
427
|
+
runtimeFile: fixture.runtimeFile,
|
|
428
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
429
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
430
|
+
compatEntries: [fixture.compatEntry],
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
assert.match(
|
|
434
|
+
output,
|
|
435
|
+
/takeover: rejected requestId=req-takeover-2 reason=mode-released/,
|
|
436
|
+
);
|
|
437
|
+
} finally {
|
|
438
|
+
await close(runtime.server);
|
|
439
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test('status rejects malicious takeover values before output', async () => {
|
|
444
|
+
const fixture = await createFixtureApp();
|
|
445
|
+
const runtime = await startStatusRuntime({
|
|
446
|
+
ok: true,
|
|
447
|
+
takeover: {
|
|
448
|
+
state: 'answered',
|
|
449
|
+
requestId: 'req takeover apiKey=secret',
|
|
450
|
+
source: 'cursor-agent-exec apiKey=secret',
|
|
451
|
+
model: 'sk-secret-token',
|
|
452
|
+
content: 'secret prompt leak',
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
458
|
+
await writeRuntimeInfo(
|
|
459
|
+
{ host: '127.0.0.1', port: runtime.port, runtimeId: runtime.runtimeId },
|
|
460
|
+
{ runtimeFile: fixture.runtimeFile },
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
const output = await status({
|
|
464
|
+
appPath: fixture.appPath,
|
|
465
|
+
runtimeFile: fixture.runtimeFile,
|
|
466
|
+
trialRecordDir: join(fixture.tempDir, 'trials'),
|
|
467
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
468
|
+
compatEntries: [fixture.compatEntry],
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
assert.match(
|
|
472
|
+
output,
|
|
473
|
+
/takeover: answered requestId=unknown source=unknown model=unknown content=present/,
|
|
474
|
+
);
|
|
475
|
+
assert.doesNotMatch(output, /apiKey|secret|prompt|leak|sk-secret-token|req takeover/);
|
|
476
|
+
} finally {
|
|
477
|
+
await close(runtime.server);
|
|
478
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test('status reports linked extension state from the trial record', async () => {
|
|
483
|
+
const fixture = await createFixtureApp();
|
|
484
|
+
const trialRecordDir = join(fixture.tempDir, 'trials');
|
|
485
|
+
const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
|
|
486
|
+
const cursorExtensionsDir = join(fixture.tempDir, 'Extensions');
|
|
487
|
+
const linkedPath = linkedExtensionPathForDir(cursorExtensionsDir);
|
|
488
|
+
const now = '2026-05-30T00:00:00.000Z';
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
await bundleExtension({ installPath: extensionInstallPath });
|
|
492
|
+
await linkExtensionBundle({ sourceBundlePath: extensionInstallPath, cursorExtensionsDir });
|
|
493
|
+
await writeTrialRecord(
|
|
494
|
+
{
|
|
495
|
+
trialId: trialIdForAppPath(fixture.appPath),
|
|
496
|
+
appPath: fixture.appPath,
|
|
497
|
+
cursorVersion: '3.5.38',
|
|
498
|
+
cursorCommit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
|
|
499
|
+
targetRelativePath,
|
|
500
|
+
originalSha256: fixture.compatEntry.expectedSha256,
|
|
501
|
+
compatSupportStatus: 'supported',
|
|
502
|
+
runtimeFile: fixture.runtimeFile,
|
|
503
|
+
backupDir: fixture.backupDir,
|
|
504
|
+
extensionInstallPath,
|
|
505
|
+
extensionLinkedPath: linkedPath,
|
|
506
|
+
extensionState: 'linked',
|
|
507
|
+
createdAt: now,
|
|
508
|
+
updatedAt: now,
|
|
509
|
+
},
|
|
510
|
+
{ trialRecordDir },
|
|
511
|
+
);
|
|
512
|
+
|
|
513
|
+
const output = await status({
|
|
514
|
+
appPath: fixture.appPath,
|
|
515
|
+
trialRecordDir,
|
|
516
|
+
compatEntries: [fixture.compatEntry],
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
assert.match(output, /extension: linked/);
|
|
520
|
+
assert.match(output, /mode: disposable/);
|
|
521
|
+
assert.match(output, /trial: recorded/);
|
|
522
|
+
} finally {
|
|
523
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
function createInstallRecordFixture(
|
|
528
|
+
fixture: Awaited<ReturnType<typeof createFixtureApp>>,
|
|
529
|
+
overrides: Partial<InstallRecord> = {},
|
|
530
|
+
): InstallRecord {
|
|
531
|
+
const now = '2026-05-30T00:00:00.000Z';
|
|
532
|
+
return {
|
|
533
|
+
installId: installIdForAppPath(fixture.appPath),
|
|
534
|
+
mode: 'real',
|
|
535
|
+
appPath: fixture.appPath,
|
|
536
|
+
cursorVersion: '3.5.38',
|
|
537
|
+
cursorCommit: '009bb5a3600dd98fe1c1f25798f767f686e14750',
|
|
538
|
+
targetRelativePath,
|
|
539
|
+
originalSha256: fixture.compatEntry.expectedSha256,
|
|
540
|
+
compatSupportStatus: 'supported',
|
|
541
|
+
runtimeFile: fixture.runtimeFile,
|
|
542
|
+
backupDir: fixture.backupDir,
|
|
543
|
+
extensionInstallPath: join(fixture.tempDir, 'extensions/cursor-pool-status'),
|
|
544
|
+
extensionLinkedPath: linkedExtensionPathForDir(join(fixture.tempDir, 'extensions')),
|
|
545
|
+
extensionState: 'linked',
|
|
546
|
+
cliVersion: '0.0.0',
|
|
547
|
+
extensionVersion: '0.0.0',
|
|
548
|
+
serviceVersion: '0.0.0',
|
|
549
|
+
lastOperation: 'install',
|
|
550
|
+
lastOperationStatus: 'ok',
|
|
551
|
+
createdAt: now,
|
|
552
|
+
updatedAt: now,
|
|
553
|
+
...overrides,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
test('status reports real mode from the install record', async () => {
|
|
558
|
+
const fixture = await createFixtureApp();
|
|
559
|
+
const installRecordFile = join(fixture.tempDir, 'install.json');
|
|
560
|
+
const cursorExtensionsDir = join(fixture.tempDir, 'extensions');
|
|
561
|
+
const extensionInstallPath = join(fixture.tempDir, 'extensions/cursor-pool-status');
|
|
562
|
+
const linkedPath = linkedExtensionPathForDir(cursorExtensionsDir);
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
await patchFullSet(fixture.appPath, fixture.backupDir);
|
|
566
|
+
await bundleExtension({ installPath: extensionInstallPath });
|
|
567
|
+
await linkExtensionBundle({ sourceBundlePath: extensionInstallPath, cursorExtensionsDir });
|
|
568
|
+
await writeInstallRecord(
|
|
569
|
+
createInstallRecordFixture(fixture, {
|
|
570
|
+
extensionInstallPath,
|
|
571
|
+
extensionLinkedPath: linkedPath,
|
|
572
|
+
}),
|
|
573
|
+
{ installRecordFile },
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
const output = await status({
|
|
577
|
+
realAppPath: fixture.appPath,
|
|
578
|
+
installRecordFile,
|
|
579
|
+
compatEntries: [fixture.compatEntry],
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
assert.match(output, /mode: real/);
|
|
583
|
+
assert.match(output, /extension: linked/);
|
|
584
|
+
assert.match(output, /runtime: /);
|
|
585
|
+
assert.match(output, /install-record: recorded/);
|
|
586
|
+
assert.doesNotMatch(output, /trial:/);
|
|
587
|
+
} finally {
|
|
588
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
test('status reports stale install record when recorded cursor commit differs', async () => {
|
|
593
|
+
const fixture = await createFixtureApp();
|
|
594
|
+
const installRecordFile = join(fixture.tempDir, 'install.json');
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
await writeInstallRecord(
|
|
598
|
+
createInstallRecordFixture(fixture, {
|
|
599
|
+
cursorCommit: 'different-recorded-commit',
|
|
600
|
+
}),
|
|
601
|
+
{ installRecordFile },
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
const output = await status({
|
|
605
|
+
realAppPath: fixture.appPath,
|
|
606
|
+
installRecordFile,
|
|
607
|
+
compatEntries: [fixture.compatEntry],
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
assert.match(output, /mode: real/);
|
|
611
|
+
assert.match(output, /install-record: stale/);
|
|
612
|
+
} finally {
|
|
613
|
+
await rm(fixture.tempDir, { recursive: true, force: true });
|
|
614
|
+
}
|
|
615
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { resolveCursorTarget } from '../src/target';
|
|
4
|
+
|
|
5
|
+
test('resolveCursorTarget treats explicit appPath as disposable mode', () => {
|
|
6
|
+
assert.deepEqual(
|
|
7
|
+
resolveCursorTarget({
|
|
8
|
+
appPath: '/Users/example/Desktop/Cursor-Pool-Trial.app',
|
|
9
|
+
defaultRealAppPath: '/Applications/Cursor.app',
|
|
10
|
+
}),
|
|
11
|
+
{
|
|
12
|
+
mode: 'disposable',
|
|
13
|
+
appPath: '/Users/example/Desktop/Cursor-Pool-Trial.app',
|
|
14
|
+
requiresConfirmation: false,
|
|
15
|
+
},
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('resolveCursorTarget rejects the real Cursor app as disposable appPath', () => {
|
|
20
|
+
assert.throws(
|
|
21
|
+
() => resolveCursorTarget({ appPath: '/Applications/Cursor.app' }),
|
|
22
|
+
/Refusing to install into real Cursor app/,
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('resolveCursorTarget uses real mode when appPath is not provided', () => {
|
|
27
|
+
assert.deepEqual(
|
|
28
|
+
resolveCursorTarget({ defaultRealAppPath: '/Applications/Cursor-Real.app' }),
|
|
29
|
+
{
|
|
30
|
+
mode: 'real',
|
|
31
|
+
appPath: '/Applications/Cursor-Real.app',
|
|
32
|
+
requiresConfirmation: true,
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('resolveCursorTarget prefers injected realAppPath for real mode', () => {
|
|
38
|
+
assert.deepEqual(
|
|
39
|
+
resolveCursorTarget({
|
|
40
|
+
realAppPath: '/Volumes/Apps/Cursor.app',
|
|
41
|
+
defaultRealAppPath: '/Applications/Cursor.app',
|
|
42
|
+
}),
|
|
43
|
+
{
|
|
44
|
+
mode: 'real',
|
|
45
|
+
appPath: '/Volumes/Apps/Cursor.app',
|
|
46
|
+
requiresConfirmation: true,
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
});
|