@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,107 @@
|
|
|
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 {
|
|
7
|
+
deleteInstallRecord,
|
|
8
|
+
installIdForAppPath,
|
|
9
|
+
installRecordPath,
|
|
10
|
+
isInstallRecordStale,
|
|
11
|
+
readInstallRecord,
|
|
12
|
+
writeInstallRecord,
|
|
13
|
+
type InstallRecord,
|
|
14
|
+
} from '../src/installRecord';
|
|
15
|
+
|
|
16
|
+
function installRecordFixture(appPath: string, overrides: Partial<InstallRecord> = {}) {
|
|
17
|
+
return {
|
|
18
|
+
installId: installIdForAppPath(appPath),
|
|
19
|
+
mode: 'real' as const,
|
|
20
|
+
appPath,
|
|
21
|
+
cursorVersion: '3.6.21',
|
|
22
|
+
cursorCommit: 'e7a7e93f4d75f8272503ecf33cedbaae10114a10',
|
|
23
|
+
targetRelativePath: 'Contents/Resources/app/extensions/cursor-agent-exec/dist/main.js',
|
|
24
|
+
originalSha256: 'abc123',
|
|
25
|
+
compatSupportStatus: 'supported' as const,
|
|
26
|
+
runtimeFile: join(appPath, 'runtime.json'),
|
|
27
|
+
backupDir: join(appPath, 'backups'),
|
|
28
|
+
extensionInstallPath: join(appPath, 'extensions/cursor-pool-status'),
|
|
29
|
+
extensionLinkedPath: join(appPath, 'Cursor-Pool-Extensions/cursor-pool.extension-0.0.0'),
|
|
30
|
+
extensionState: 'linked' as const,
|
|
31
|
+
cliVersion: '0.0.0',
|
|
32
|
+
extensionVersion: '0.0.0',
|
|
33
|
+
serviceVersion: '0.0.0',
|
|
34
|
+
lastOperation: 'install',
|
|
35
|
+
lastOperationStatus: 'ok',
|
|
36
|
+
createdAt: '2026-05-30T00:00:00.000Z',
|
|
37
|
+
updatedAt: '2026-05-30T00:00:01.000Z',
|
|
38
|
+
...overrides,
|
|
39
|
+
} satisfies InstallRecord;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
test('install records round-trip through an injected install record file', async () => {
|
|
43
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-install-record-'));
|
|
44
|
+
const installRecordFile = join(tempDir, 'nested/install.json');
|
|
45
|
+
const appPath = join(tempDir, 'Cursor.app');
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const record = installRecordFixture(appPath);
|
|
49
|
+
|
|
50
|
+
await writeInstallRecord(record, { installRecordFile });
|
|
51
|
+
|
|
52
|
+
const saved = await readInstallRecord({ installRecordFile });
|
|
53
|
+
assert.deepEqual(saved, record);
|
|
54
|
+
assert.deepEqual(JSON.parse(await readFile(installRecordFile, 'utf8')), record);
|
|
55
|
+
|
|
56
|
+
await deleteInstallRecord({ installRecordFile });
|
|
57
|
+
assert.equal(await readInstallRecord({ installRecordFile }), null);
|
|
58
|
+
} finally {
|
|
59
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('writeInstallRecord rejects install ids that do not match the app path', async () => {
|
|
64
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-install-record-'));
|
|
65
|
+
const installRecordFile = join(tempDir, 'install.json');
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await assert.rejects(
|
|
69
|
+
writeInstallRecord(
|
|
70
|
+
installRecordFixture(join(tempDir, 'Cursor.app'), {
|
|
71
|
+
installId: 'not-the-app-path-id',
|
|
72
|
+
}),
|
|
73
|
+
{ installRecordFile },
|
|
74
|
+
),
|
|
75
|
+
/Install record id does not match app path/,
|
|
76
|
+
);
|
|
77
|
+
} finally {
|
|
78
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('installRecordPath returns an explicit install record file path', () => {
|
|
83
|
+
assert.equal(
|
|
84
|
+
installRecordPath({ installRecordFile: '/tmp/cursor-pool/install.json' }),
|
|
85
|
+
'/tmp/cursor-pool/install.json',
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('isInstallRecordStale compares tracked install state fields', () => {
|
|
90
|
+
const current = installRecordFixture('/Users/example/Desktop/Cursor.app');
|
|
91
|
+
|
|
92
|
+
assert.equal(isInstallRecordStale(current, current), false);
|
|
93
|
+
assert.equal(
|
|
94
|
+
isInstallRecordStale(current, {
|
|
95
|
+
...current,
|
|
96
|
+
cursorCommit: 'different-cursor-commit',
|
|
97
|
+
}),
|
|
98
|
+
true,
|
|
99
|
+
);
|
|
100
|
+
assert.equal(
|
|
101
|
+
isInstallRecordStale(current, {
|
|
102
|
+
...current,
|
|
103
|
+
extensionState: 'missing',
|
|
104
|
+
}),
|
|
105
|
+
true,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
buildLaunchCommand,
|
|
5
|
+
inspectLaunchProcesses,
|
|
6
|
+
launchCommand,
|
|
7
|
+
launchStatus,
|
|
8
|
+
} from '../src/launch';
|
|
9
|
+
|
|
10
|
+
test('buildLaunchCommand prints a direct executable command with isolated paths', () => {
|
|
11
|
+
const appPath = '/Users/example/Desktop/Cursor-Pool-Trial.app';
|
|
12
|
+
const userDataDir = '/Users/example/Desktop/Cursor-Pool-Trial-UserData';
|
|
13
|
+
const cursorExtensionsDir = '/Users/example/Desktop/Cursor-Pool-Trial-Extensions';
|
|
14
|
+
|
|
15
|
+
const output = buildLaunchCommand({ appPath, userDataDir, cursorExtensionsDir });
|
|
16
|
+
|
|
17
|
+
assert.match(output, /Cursor-Pool-Trial\.app\/Contents\/MacOS\/Cursor/);
|
|
18
|
+
assert.match(output, /--user-data-dir/);
|
|
19
|
+
assert.match(output, /Cursor-Pool-Trial-UserData/);
|
|
20
|
+
assert.match(output, /--extensions-dir/);
|
|
21
|
+
assert.match(output, /Cursor-Pool-Trial-Extensions/);
|
|
22
|
+
assert.match(output, /--new-window/);
|
|
23
|
+
assert.doesNotMatch(output, /open -n/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('launchCommand rejects the real macOS Cursor app', async () => {
|
|
27
|
+
await assert.rejects(
|
|
28
|
+
launchCommand({
|
|
29
|
+
appPath: '/Applications/Cursor.app',
|
|
30
|
+
userDataDir: '/tmp/user-data',
|
|
31
|
+
cursorExtensionsDir: '/tmp/extensions',
|
|
32
|
+
}),
|
|
33
|
+
/Refusing to install into real Cursor app/,
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('launchCommand requires explicit user data and extensions paths', async () => {
|
|
38
|
+
await assert.rejects(
|
|
39
|
+
launchCommand({
|
|
40
|
+
appPath: '/Users/example/Desktop/Cursor-Pool-Trial.app',
|
|
41
|
+
cursorExtensionsDir: '/tmp/extensions',
|
|
42
|
+
}),
|
|
43
|
+
/Missing required --user-data-dir/,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
await assert.rejects(
|
|
47
|
+
launchCommand({
|
|
48
|
+
appPath: '/Users/example/Desktop/Cursor-Pool-Trial.app',
|
|
49
|
+
userDataDir: '/tmp/user-data',
|
|
50
|
+
}),
|
|
51
|
+
/Missing required --cursor-extensions-dir/,
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const appPath = '/Users/example/Desktop/Cursor-Pool-Trial.app';
|
|
56
|
+
const userDataDir = '/Users/example/Desktop/Cursor-Pool-Trial-UserData';
|
|
57
|
+
const cursorExtensionsDir = '/Users/example/Desktop/Cursor-Pool-Trial-Extensions';
|
|
58
|
+
|
|
59
|
+
test('inspectLaunchProcesses reports not-running when no target app process exists', () => {
|
|
60
|
+
const status = inspectLaunchProcesses({
|
|
61
|
+
psOutput: '1 0 /Applications/Cursor.app/Contents/MacOS/Cursor\n',
|
|
62
|
+
appPath,
|
|
63
|
+
userDataDir,
|
|
64
|
+
cursorExtensionsDir,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
assert.equal(status.launch, 'not-running');
|
|
68
|
+
assert.equal(status.app, 'missing');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('inspectLaunchProcesses detects isolated user data and extensions args', () => {
|
|
72
|
+
const status = inspectLaunchProcesses({
|
|
73
|
+
psOutput: [
|
|
74
|
+
`100 1 ${appPath}/Contents/MacOS/Cursor --user-data-dir ${userDataDir} --extensions-dir ${cursorExtensionsDir} --new-window`,
|
|
75
|
+
`101 100 ${appPath}/Contents/Frameworks/Cursor Helper.app/Contents/MacOS/Cursor Helper --user-data-dir=${userDataDir}`,
|
|
76
|
+
].join('\n'),
|
|
77
|
+
appPath,
|
|
78
|
+
userDataDir,
|
|
79
|
+
cursorExtensionsDir,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
assert.deepEqual(status, {
|
|
83
|
+
launch: 'running',
|
|
84
|
+
app: 'matched',
|
|
85
|
+
userData: 'isolated',
|
|
86
|
+
extensions: 'isolated',
|
|
87
|
+
realUserData: 'absent',
|
|
88
|
+
realExtensions: 'absent',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('inspectLaunchProcesses detects real Cursor user data pollution', () => {
|
|
93
|
+
const status = inspectLaunchProcesses({
|
|
94
|
+
psOutput: [
|
|
95
|
+
`${appPath}/Contents/MacOS/Cursor`,
|
|
96
|
+
`${appPath}/Contents/Frameworks/Cursor Helper.app/Contents/MacOS/Cursor Helper --user-data-dir=/Users/example/Library/Application Support/Cursor`,
|
|
97
|
+
].join('\n'),
|
|
98
|
+
appPath,
|
|
99
|
+
userDataDir,
|
|
100
|
+
cursorExtensionsDir,
|
|
101
|
+
homeDir: '/Users/example',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
assert.equal(status.launch, 'running');
|
|
105
|
+
assert.equal(status.userData, 'real-cursor');
|
|
106
|
+
assert.equal(status.realUserData, 'present');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('inspectLaunchProcesses ignores crashpad paths under real Cursor user data', () => {
|
|
110
|
+
const status = inspectLaunchProcesses({
|
|
111
|
+
psOutput: [
|
|
112
|
+
`100 1 ${appPath}/Contents/MacOS/Cursor --user-data-dir ${userDataDir} --extensions-dir ${cursorExtensionsDir} --new-window`,
|
|
113
|
+
`101 100 ${appPath}/Contents/Frameworks/Electron Framework.framework/Helpers/chrome_crashpad_handler --database=/Users/example/Library/Application Support/Cursor/Crashpad`,
|
|
114
|
+
`102 100 ${appPath}/Contents/Frameworks/Cursor Helper.app/Contents/MacOS/Cursor Helper --user-data-dir=${userDataDir}`,
|
|
115
|
+
].join('\n'),
|
|
116
|
+
appPath,
|
|
117
|
+
userDataDir,
|
|
118
|
+
cursorExtensionsDir,
|
|
119
|
+
homeDir: '/Users/example',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
assert.equal(status.userData, 'isolated');
|
|
123
|
+
assert.equal(status.realUserData, 'absent');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('launchStatus formats process inspection output', async () => {
|
|
127
|
+
const output = await launchStatus({
|
|
128
|
+
appPath,
|
|
129
|
+
userDataDir,
|
|
130
|
+
cursorExtensionsDir,
|
|
131
|
+
psOutput: `${appPath}/Contents/MacOS/Cursor --user-data-dir=${userDataDir}`,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
assert.match(output, /launch: running/);
|
|
135
|
+
assert.match(output, /app: matched/);
|
|
136
|
+
assert.match(output, /user-data: isolated/);
|
|
137
|
+
assert.match(output, /real-user-data: absent/);
|
|
138
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createServer, type IncomingMessage, type ServerResponse } from 'node:http';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import { writeRuntimeInfo } from '../../service/src/runtime';
|
|
8
|
+
import {
|
|
9
|
+
formatPlatformLogin,
|
|
10
|
+
formatPlatformStatus,
|
|
11
|
+
heartbeat,
|
|
12
|
+
login,
|
|
13
|
+
logout,
|
|
14
|
+
whoami,
|
|
15
|
+
} from '../src/platform';
|
|
16
|
+
|
|
17
|
+
function readBody(request: IncomingMessage) {
|
|
18
|
+
return new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
19
|
+
const chunks: Buffer[] = [];
|
|
20
|
+
request.on('data', (chunk: Buffer) => chunks.push(chunk));
|
|
21
|
+
request.on('error', reject);
|
|
22
|
+
request.on('end', () => {
|
|
23
|
+
resolve(
|
|
24
|
+
chunks.length === 0
|
|
25
|
+
? {}
|
|
26
|
+
: (JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>),
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeJson(response: ServerResponse, body: Record<string, unknown>) {
|
|
33
|
+
const payload = JSON.stringify(body);
|
|
34
|
+
response.writeHead(200, {
|
|
35
|
+
'content-type': 'application/json',
|
|
36
|
+
'content-length': Buffer.byteLength(payload),
|
|
37
|
+
});
|
|
38
|
+
response.end(payload);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function createLocalService(handler: (request: IncomingMessage, body: Record<string, unknown>) => void) {
|
|
42
|
+
const server = createServer(async (request, response) => {
|
|
43
|
+
const body = await readBody(request);
|
|
44
|
+
handler(request, body);
|
|
45
|
+
|
|
46
|
+
if (request.url === '/platform/status') {
|
|
47
|
+
writeJson(response, {
|
|
48
|
+
state: 'logged-in',
|
|
49
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
50
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:30Z' },
|
|
51
|
+
});
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (request.url === '/platform/heartbeat') {
|
|
56
|
+
writeJson(response, {
|
|
57
|
+
state: 'logged-in',
|
|
58
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
59
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:01:00Z' },
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (request.url === '/platform/logout') {
|
|
65
|
+
writeJson(response, { state: 'logged-out' });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (request.url === '/platform/login') {
|
|
70
|
+
writeJson(response, {
|
|
71
|
+
state: 'logged-in',
|
|
72
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
73
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
74
|
+
});
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
response.writeHead(404);
|
|
79
|
+
response.end();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
await new Promise<void>((resolve) => {
|
|
83
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
84
|
+
});
|
|
85
|
+
const address = server.address();
|
|
86
|
+
assert.equal(typeof address, 'object');
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
port: address?.port as number,
|
|
90
|
+
close: () => new Promise<void>((resolve, reject) => {
|
|
91
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
92
|
+
}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
test('formatPlatformStatus reports logged-in user, device, and heartbeat', () => {
|
|
97
|
+
assert.equal(
|
|
98
|
+
formatPlatformStatus({
|
|
99
|
+
state: 'logged-in',
|
|
100
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
101
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:30Z' },
|
|
102
|
+
}),
|
|
103
|
+
[
|
|
104
|
+
'platform: logged-in',
|
|
105
|
+
'user: dev@example.com',
|
|
106
|
+
'device: active dev_1',
|
|
107
|
+
'heartbeat: 2026-05-30T00:00:30Z',
|
|
108
|
+
].join('\n'),
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('formatPlatformStatus reports logged-out state', () => {
|
|
113
|
+
assert.equal(formatPlatformStatus({ state: 'logged-out' }), 'platform: logged-out');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('formatPlatformLogin matches platform status formatting', () => {
|
|
117
|
+
assert.equal(
|
|
118
|
+
formatPlatformLogin({
|
|
119
|
+
state: 'logged-in',
|
|
120
|
+
user: { id: 'usr_1', email: 'dev@example.com' },
|
|
121
|
+
device: { id: 'dev_1', status: 'active', lastHeartbeatAt: '2026-05-30T00:00:00Z' },
|
|
122
|
+
}),
|
|
123
|
+
[
|
|
124
|
+
'platform: logged-in',
|
|
125
|
+
'user: dev@example.com',
|
|
126
|
+
'device: active dev_1',
|
|
127
|
+
'heartbeat: 2026-05-30T00:00:00Z',
|
|
128
|
+
].join('\n'),
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('platform commands call local service platform routes', async () => {
|
|
133
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-cli-platform-'));
|
|
134
|
+
const runtimeFile = join(tempDir, 'runtime.json');
|
|
135
|
+
const calls: Array<{ method?: string; url?: string; body: Record<string, unknown> }> = [];
|
|
136
|
+
const service = await createLocalService((request, body) => {
|
|
137
|
+
calls.push({ method: request.method, url: request.url, body });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await writeRuntimeInfo(
|
|
142
|
+
{ host: '127.0.0.1', port: service.port, runtimeId: 'runtime-1' },
|
|
143
|
+
{ runtimeFile },
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const loginOutput = await login({
|
|
147
|
+
runtimeFile,
|
|
148
|
+
code: 'CODE-123',
|
|
149
|
+
apiBaseUrl: 'https://platform.example.test',
|
|
150
|
+
});
|
|
151
|
+
assert.match(loginOutput, /platform: logged-in/);
|
|
152
|
+
|
|
153
|
+
const whoamiOutput = await whoami({ runtimeFile });
|
|
154
|
+
assert.match(whoamiOutput, /heartbeat: 2026-05-30T00:00:30Z/);
|
|
155
|
+
|
|
156
|
+
const heartbeatOutput = await heartbeat({ runtimeFile });
|
|
157
|
+
assert.match(heartbeatOutput, /heartbeat: 2026-05-30T00:01:00Z/);
|
|
158
|
+
|
|
159
|
+
assert.equal(await logout({ runtimeFile }), 'platform: logged-out');
|
|
160
|
+
|
|
161
|
+
assert.deepEqual(
|
|
162
|
+
calls.map((call) => [call.method, call.url]),
|
|
163
|
+
[
|
|
164
|
+
['POST', '/platform/login'],
|
|
165
|
+
['GET', '/platform/status'],
|
|
166
|
+
['POST', '/platform/heartbeat'],
|
|
167
|
+
['POST', '/platform/logout'],
|
|
168
|
+
],
|
|
169
|
+
);
|
|
170
|
+
assert.equal(calls[0]?.body.code, 'CODE-123');
|
|
171
|
+
assert.equal(calls[0]?.body.apiBaseUrl, 'https://platform.example.test');
|
|
172
|
+
assert.equal(typeof (calls[0]?.body.device as Record<string, unknown>).name, 'string');
|
|
173
|
+
assert.equal((calls[2]?.body as Record<string, unknown>).serviceStatus, 'running');
|
|
174
|
+
assert.equal(JSON.stringify(calls), JSON.stringify(calls).replace(/deviceToken/g, ''));
|
|
175
|
+
} finally {
|
|
176
|
+
await service.close();
|
|
177
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('platform commands require service runtime', async () => {
|
|
182
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'cursor-pool-cli-platform-missing-'));
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
await assert.rejects(
|
|
186
|
+
whoami({ runtimeFile: join(tempDir, 'runtime.json') }),
|
|
187
|
+
/Cursor Pool service runtime is unavailable/,
|
|
188
|
+
);
|
|
189
|
+
} finally {
|
|
190
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('platform commands reject per-command session files', async () => {
|
|
195
|
+
await assert.rejects(
|
|
196
|
+
login({
|
|
197
|
+
code: 'CODE-123',
|
|
198
|
+
apiBaseUrl: 'https://platform.example.test',
|
|
199
|
+
sessionFile: '/tmp/session.json',
|
|
200
|
+
}),
|
|
201
|
+
/--session-file is managed by the local service/,
|
|
202
|
+
);
|
|
203
|
+
await assert.rejects(
|
|
204
|
+
whoami({ sessionFile: '/tmp/session.json' }),
|
|
205
|
+
/--session-file is managed by the local service/,
|
|
206
|
+
);
|
|
207
|
+
await assert.rejects(
|
|
208
|
+
heartbeat({ sessionFile: '/tmp/session.json' }),
|
|
209
|
+
/--session-file is managed by the local service/,
|
|
210
|
+
);
|
|
211
|
+
await assert.rejects(
|
|
212
|
+
logout({ sessionFile: '/tmp/session.json' }),
|
|
213
|
+
/--session-file is managed by the local service/,
|
|
214
|
+
);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('login requires code and apiBaseUrl', async () => {
|
|
218
|
+
await assert.rejects(
|
|
219
|
+
login({ code: 'CODE-123', apiBaseUrl: undefined as unknown as string }),
|
|
220
|
+
/--api-base-url is required/,
|
|
221
|
+
);
|
|
222
|
+
await assert.rejects(
|
|
223
|
+
login({ code: undefined as unknown as string, apiBaseUrl: 'https://platform.example.test' }),
|
|
224
|
+
/--code is required/,
|
|
225
|
+
);
|
|
226
|
+
});
|