@cursorpool-dev/cli 0.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/bin/cursor-pool.mjs +9 -0
  2. package/bin/cursor-pool.ts +169 -0
  3. package/node_modules/@cursor-pool/extension/dist/extension.js +2910 -0
  4. package/node_modules/@cursor-pool/extension/package.json +64 -0
  5. package/node_modules/@cursor-pool/extension/resources/cursor-pool.svg +6 -0
  6. package/node_modules/@cursor-pool/extension/src/api.ts +545 -0
  7. package/node_modules/@cursor-pool/extension/src/extension.ts +104 -0
  8. package/node_modules/@cursor-pool/extension/src/index.ts +1 -0
  9. package/node_modules/@cursor-pool/extension/src/panel.ts +569 -0
  10. package/node_modules/@cursor-pool/extension/src/runtime.ts +22 -0
  11. package/node_modules/@cursor-pool/extension/test/panel.test.ts +1785 -0
  12. package/node_modules/@cursor-pool/patcher/package.json +17 -0
  13. package/node_modules/@cursor-pool/patcher/src/alwaysLocalMarker.ts +86 -0
  14. package/node_modules/@cursor-pool/patcher/src/hash.ts +7 -0
  15. package/node_modules/@cursor-pool/patcher/src/index.ts +55 -0
  16. package/node_modules/@cursor-pool/patcher/src/marker.ts +159 -0
  17. package/node_modules/@cursor-pool/patcher/src/patchCursorAgentExec.ts +154 -0
  18. package/node_modules/@cursor-pool/patcher/src/patchCursorAlwaysLocal.ts +142 -0
  19. package/node_modules/@cursor-pool/patcher/src/patchCursorWorkbenchAuthGate.ts +140 -0
  20. package/node_modules/@cursor-pool/patcher/src/restoreCursorAgentExec.ts +52 -0
  21. package/node_modules/@cursor-pool/patcher/src/restoreCursorAlwaysLocal.ts +52 -0
  22. package/node_modules/@cursor-pool/patcher/src/restoreCursorWorkbenchAuthGate.ts +70 -0
  23. package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +243 -0
  24. package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +630 -0
  25. package/node_modules/@cursor-pool/patcher/test/patchCursorAlwaysLocal.test.ts +144 -0
  26. package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +770 -0
  27. package/node_modules/@cursor-pool/patcher/test/restoreCursorAgentExec.test.ts +139 -0
  28. package/node_modules/@cursor-pool/service/package.json +17 -0
  29. package/node_modules/@cursor-pool/service/src/canary.ts +61 -0
  30. package/node_modules/@cursor-pool/service/src/diagnostics.ts +385 -0
  31. package/node_modules/@cursor-pool/service/src/entry.ts +161 -0
  32. package/node_modules/@cursor-pool/service/src/health.ts +10 -0
  33. package/node_modules/@cursor-pool/service/src/index.ts +29 -0
  34. package/node_modules/@cursor-pool/service/src/metadata.ts +22 -0
  35. package/node_modules/@cursor-pool/service/src/platformSession.ts +1178 -0
  36. package/node_modules/@cursor-pool/service/src/requestCheck.ts +81 -0
  37. package/node_modules/@cursor-pool/service/src/requestGate.ts +100 -0
  38. package/node_modules/@cursor-pool/service/src/requestGateway.ts +441 -0
  39. package/node_modules/@cursor-pool/service/src/runtime.ts +48 -0
  40. package/node_modules/@cursor-pool/service/src/server.ts +939 -0
  41. package/node_modules/@cursor-pool/service/src/takeover.ts +111 -0
  42. package/node_modules/@cursor-pool/service/test/canary.test.ts +140 -0
  43. package/node_modules/@cursor-pool/service/test/diagnostics.test.ts +506 -0
  44. package/node_modules/@cursor-pool/service/test/metadata.test.ts +63 -0
  45. package/node_modules/@cursor-pool/service/test/platformSession.test.ts +2428 -0
  46. package/node_modules/@cursor-pool/service/test/requestCheck.test.ts +152 -0
  47. package/node_modules/@cursor-pool/service/test/requestGate.test.ts +207 -0
  48. package/node_modules/@cursor-pool/service/test/requestGateway.test.ts +466 -0
  49. package/node_modules/@cursor-pool/service/test/runtime.test.ts +47 -0
  50. package/node_modules/@cursor-pool/service/test/server.test.ts +2570 -0
  51. package/node_modules/@cursor-pool/shared/package.json +17 -0
  52. package/node_modules/@cursor-pool/shared/src/clientConfig.ts +49 -0
  53. package/node_modules/@cursor-pool/shared/src/index.ts +14 -0
  54. package/node_modules/@cursor-pool/shared/src/manifest.ts +36 -0
  55. package/node_modules/@cursor-pool/shared/src/metadata.ts +19 -0
  56. package/node_modules/@cursor-pool/shared/src/paths.ts +5 -0
  57. package/node_modules/@cursor-pool/shared/src/runtime.ts +3 -0
  58. package/node_modules/@cursor-pool/shared/test/index.test.ts +56 -0
  59. package/node_modules/@cursor-pool/shared/test/manifest.test.ts +65 -0
  60. package/node_modules/@cursor-pool/shared/test/metadata.test.ts +25 -0
  61. package/node_modules/@cursor-pool/shared/test/runtime.test.ts +8 -0
  62. package/package.json +28 -0
  63. package/src/adHocResign.ts +65 -0
  64. package/src/autostart.ts +240 -0
  65. package/src/compat.ts +282 -0
  66. package/src/confirm.ts +76 -0
  67. package/src/cursor.ts +94 -0
  68. package/src/diagnostics.ts +558 -0
  69. package/src/environment.ts +18 -0
  70. package/src/extensionBundle.ts +111 -0
  71. package/src/extensionLink.ts +168 -0
  72. package/src/index.ts +23 -0
  73. package/src/install.ts +614 -0
  74. package/src/installRecord.ts +105 -0
  75. package/src/launch.ts +182 -0
  76. package/src/patchSet.ts +182 -0
  77. package/src/platform.ts +132 -0
  78. package/src/repair.ts +383 -0
  79. package/src/restore.ts +153 -0
  80. package/src/serviceCommands.ts +79 -0
  81. package/src/serviceProcess.ts +188 -0
  82. package/src/status.ts +241 -0
  83. package/src/target.ts +37 -0
  84. package/src/trial.ts +133 -0
  85. package/src/uninstall.ts +213 -0
  86. package/test/autostart.test.ts +151 -0
  87. package/test/compat.test.ts +192 -0
  88. package/test/confirm.test.ts +114 -0
  89. package/test/cursor-pool-bin.test.ts +658 -0
  90. package/test/cursor.test.ts +20 -0
  91. package/test/diagnostics.test.ts +709 -0
  92. package/test/e2e-install.test.ts +773 -0
  93. package/test/extensionBundle.test.ts +161 -0
  94. package/test/extensionLink.test.ts +209 -0
  95. package/test/install.test.ts +862 -0
  96. package/test/installRecord.test.ts +107 -0
  97. package/test/launch.test.ts +138 -0
  98. package/test/platform.test.ts +226 -0
  99. package/test/repair.test.ts +575 -0
  100. package/test/restore.test.ts +211 -0
  101. package/test/serviceCommands.test.ts +135 -0
  102. package/test/serviceProcess.test.ts +280 -0
  103. package/test/status.test.ts +615 -0
  104. package/test/target.test.ts +49 -0
  105. package/test/trial.test.ts +146 -0
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "@cursor-pool/extension",
3
+ "version": "0.5.6",
4
+ "displayName": "Cursor Pool 平台模式",
5
+ "publisher": "cursor-pool",
6
+ "engines": {
7
+ "vscode": "^1.90.0"
8
+ },
9
+ "activationEvents": [
10
+ "onView:cursorPool.statusPanel",
11
+ "onCommand:cursorPool.openClient",
12
+ "onCommand:cursorPool.reportStatus"
13
+ ],
14
+ "type": "module",
15
+ "main": "./dist/extension.js",
16
+ "exports": {
17
+ "./package.json": "./package.json",
18
+ ".": "./src/extension.ts",
19
+ "./api": "./src/api.ts",
20
+ "./panel": "./src/panel.ts",
21
+ "./runtime": "./src/runtime.ts"
22
+ },
23
+ "dependencies": {
24
+ "@cursor-pool/service": "0.5.6",
25
+ "@cursor-pool/shared": "0.5.6"
26
+ },
27
+ "contributes": {
28
+ "viewsContainers": {
29
+ "activitybar": [
30
+ {
31
+ "id": "cursorPoolClient",
32
+ "title": "Cursor Pool 平台模式",
33
+ "icon": "resources/cursor-pool.svg"
34
+ }
35
+ ]
36
+ },
37
+ "views": {
38
+ "cursorPoolClient": [
39
+ {
40
+ "id": "cursorPool.statusPanel",
41
+ "name": "用户端",
42
+ "type": "webview"
43
+ }
44
+ ]
45
+ },
46
+ "commands": [
47
+ {
48
+ "command": "cursorPool.openClient",
49
+ "title": "Cursor Pool: 打开用户端"
50
+ },
51
+ {
52
+ "command": "cursorPool.openConsole",
53
+ "title": "Cursor Pool: Open Web Console"
54
+ },
55
+ {
56
+ "command": "cursorPool.reportStatus",
57
+ "title": "Cursor Pool: Report Status"
58
+ }
59
+ ]
60
+ },
61
+ "scripts": {
62
+ "test": "tsx --test test/*.test.ts"
63
+ }
64
+ }
@@ -0,0 +1,6 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
2
+ <path d="M4 7.5 12 3l8 4.5v9L12 21l-8-4.5v-9Z"/>
3
+ <path d="M8.5 9.5h7"/>
4
+ <path d="M8.5 14.5h4.5"/>
5
+ <path d="M16 13.5l2 2-2 2"/>
6
+ </svg>
@@ -0,0 +1,545 @@
1
+ import { arch, hostname, platform as osPlatform } from 'node:os';
2
+ import { readRuntimeInfo, writeRuntimeInfo, type RuntimeInfo } from './runtime';
3
+ import { startServer } from '@cursor-pool/service';
4
+ import { readClientConfig } from '@cursor-pool/shared/clientConfig';
5
+
6
+ const DEFAULT_SERVICE_PORT = 56393;
7
+
8
+ export type ServiceState = 'running' | 'stopped';
9
+ export type PlatformUserSummary = {
10
+ id: string;
11
+ email: string;
12
+ };
13
+ export type PlatformDeviceSummary = {
14
+ id: string;
15
+ status: string;
16
+ lastHeartbeatAt: string;
17
+ };
18
+ export type PlatformModeReleaseReason =
19
+ | 'product-missing'
20
+ | 'product-unavailable'
21
+ | 'insufficient-credits'
22
+ | 'invalid-token'
23
+ | 'device-inactive';
24
+ export type PlatformModeSnapshot =
25
+ | { state: 'inactive'; releaseReason?: PlatformModeReleaseReason; releasedAt?: string }
26
+ | { state: 'active'; productId: string; startedAt: string };
27
+ export type PlatformModeResult =
28
+ | PlatformModeSnapshot
29
+ | { state: 'logged-out' }
30
+ | { state: 'offline' }
31
+ | { state: 'invalid-token' }
32
+ | { state: 'product-not-selected' }
33
+ | { state: 'product-not-found'; productId: string }
34
+ | { state: 'product-unavailable'; productId: string }
35
+ | {
36
+ state: 'insufficient-credits';
37
+ productId: string;
38
+ requiredCredits: number;
39
+ currentCredits: number;
40
+ };
41
+ export type PlatformStatus =
42
+ | { state: 'logged-out' }
43
+ | {
44
+ state: 'logged-in';
45
+ user: PlatformUserSummary;
46
+ device: PlatformDeviceSummary;
47
+ mode?: PlatformModeSnapshot;
48
+ }
49
+ | {
50
+ state: 'offline';
51
+ user?: PlatformUserSummary;
52
+ device?: PlatformDeviceSummary;
53
+ mode?: PlatformModeSnapshot;
54
+ }
55
+ | {
56
+ state: 'invalid-token';
57
+ user?: PlatformUserSummary;
58
+ device?: PlatformDeviceSummary;
59
+ mode?: PlatformModeSnapshot;
60
+ };
61
+ export type PlatformProductSummary = {
62
+ id: string;
63
+ name: string;
64
+ description: string;
65
+ status: 'available' | 'unavailable';
66
+ minCredits: number;
67
+ usageLabel: string;
68
+ };
69
+ export type PlatformCatalog =
70
+ | { state: 'logged-out'; products: [] }
71
+ | {
72
+ state: 'logged-in';
73
+ account: { credits: number };
74
+ mode?: PlatformModeSnapshot;
75
+ selectedProductId?: string;
76
+ products: PlatformProductSummary[];
77
+ }
78
+ | { state: 'offline'; mode?: PlatformModeSnapshot; products: [] }
79
+ | { state: 'invalid-token'; mode?: PlatformModeSnapshot; products: [] };
80
+ export type PlatformSelectionResult =
81
+ | { state: 'selected'; selectedProductId: string }
82
+ | { state: 'logged-out' }
83
+ | { state: 'offline' }
84
+ | { state: 'invalid-token' }
85
+ | { state: 'product-not-found'; productId: string }
86
+ | { state: 'product-unavailable'; productId: string };
87
+ export type ExtensionServiceStatus = {
88
+ service: ServiceState;
89
+ runtime: RuntimeInfo | null;
90
+ };
91
+
92
+ export type PlatformLoginInput = {
93
+ runtimeFile?: string;
94
+ fallbackPort?: number;
95
+ email: string;
96
+ password: string;
97
+ };
98
+
99
+ export type PlatformSelectionInput = {
100
+ runtimeFile?: string;
101
+ fallbackPort?: number;
102
+ productId: string;
103
+ };
104
+
105
+ export type RuntimeResolutionOptions = {
106
+ runtimeFile?: string;
107
+ fallbackPort?: number;
108
+ };
109
+
110
+ const localExtensionServices = new Map<string, Awaited<ReturnType<typeof startServer>>>();
111
+
112
+ export function buildExtensionDeviceInfo() {
113
+ return {
114
+ name: hostname(),
115
+ os: osPlatform(),
116
+ arch: arch(),
117
+ extensionVersion: '0.5.6',
118
+ };
119
+ }
120
+
121
+ function serviceUrl(runtime: RuntimeInfo, path: string) {
122
+ return `http://${runtime.host}:${runtime.port}${path}`;
123
+ }
124
+
125
+ async function probeRuntime(runtime: RuntimeInfo): Promise<RuntimeInfo | null> {
126
+ try {
127
+ const response = await fetch(serviceUrl(runtime, '/health'));
128
+ const health = (await response.json()) as { ok?: unknown; runtimeId?: unknown };
129
+ if (!response.ok || health.ok !== true || typeof health.runtimeId !== 'string') {
130
+ return null;
131
+ }
132
+ if (runtime.runtimeId && health.runtimeId !== runtime.runtimeId) {
133
+ return null;
134
+ }
135
+ return { ...runtime, runtimeId: health.runtimeId };
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
141
+ async function resolveHealthyRuntime(options: RuntimeResolutionOptions = {}): Promise<RuntimeInfo | null> {
142
+ const runtime = await readRuntimeInfo({ runtimeFile: options.runtimeFile });
143
+ const allowFallback = options.runtimeFile === undefined || options.fallbackPort !== undefined;
144
+
145
+ if (runtime) {
146
+ const healthyRuntime = await probeRuntime(runtime);
147
+ if (healthyRuntime) {
148
+ return healthyRuntime;
149
+ }
150
+ }
151
+
152
+ if (!allowFallback) {
153
+ return null;
154
+ }
155
+
156
+ const fallbackRuntime: RuntimeInfo = {
157
+ host: '127.0.0.1',
158
+ port: options.fallbackPort ?? DEFAULT_SERVICE_PORT,
159
+ runtimeId: '',
160
+ };
161
+ const healthyFallback = await probeRuntime(fallbackRuntime);
162
+ if (!healthyFallback) {
163
+ return null;
164
+ }
165
+
166
+ await writeRuntimeInfo(healthyFallback, { runtimeFile: options.runtimeFile });
167
+ return healthyFallback;
168
+ }
169
+
170
+ export async function ensureLocalService(
171
+ runtimeFile?: string,
172
+ options: { platformApiBaseUrl?: string } = {},
173
+ ): Promise<RuntimeInfo> {
174
+ const healthy = await resolveHealthyRuntime({ runtimeFile });
175
+ if (healthy) {
176
+ return healthy;
177
+ }
178
+
179
+ const key = runtimeFile ?? '~/.cursor-pool/runtime.json';
180
+ const existing = localExtensionServices.get(key);
181
+ if (existing) {
182
+ return { host: existing.host, port: existing.port, runtimeId: existing.runtimeId };
183
+ }
184
+
185
+ const clientConfig = await readClientConfig();
186
+ const service = await startServer({
187
+ runtimeFile,
188
+ platformApiBaseUrl:
189
+ options.platformApiBaseUrl ?? process.env.CURSOR_POOL_API_BASE_URL ?? clientConfig.apiBaseUrl,
190
+ });
191
+ localExtensionServices.set(key, service);
192
+ return { host: service.host, port: service.port, runtimeId: service.runtimeId };
193
+ }
194
+
195
+ async function resolveRuntimeForRequest(options: RuntimeResolutionOptions = {}): Promise<RuntimeInfo | null> {
196
+ const runtime = await readRuntimeInfo({ runtimeFile: options.runtimeFile });
197
+ const allowFallback = options.runtimeFile === undefined || options.fallbackPort !== undefined;
198
+
199
+ if (runtime && !allowFallback) {
200
+ return runtime;
201
+ }
202
+
203
+ return resolveHealthyRuntime(options);
204
+ }
205
+
206
+ function isUserSummary(value: unknown): value is PlatformUserSummary {
207
+ const user = value as PlatformUserSummary;
208
+ return typeof user?.id === 'string' && typeof user.email === 'string';
209
+ }
210
+
211
+ function isDeviceSummary(value: unknown): value is PlatformDeviceSummary {
212
+ const device = value as PlatformDeviceSummary;
213
+ return (
214
+ typeof device?.id === 'string' &&
215
+ typeof device.status === 'string' &&
216
+ typeof device.lastHeartbeatAt === 'string'
217
+ );
218
+ }
219
+
220
+ function isProductSummary(value: unknown): value is PlatformProductSummary {
221
+ const product = value as PlatformProductSummary;
222
+ return (
223
+ typeof product?.id === 'string' &&
224
+ typeof product.name === 'string' &&
225
+ typeof product.description === 'string' &&
226
+ (product.status === 'available' || product.status === 'unavailable') &&
227
+ Number.isInteger(product.minCredits) &&
228
+ product.minCredits >= 0 &&
229
+ typeof product.usageLabel === 'string'
230
+ );
231
+ }
232
+
233
+ function isPlatformModeReleaseReason(value: unknown): value is PlatformModeReleaseReason {
234
+ return (
235
+ value === 'product-missing' ||
236
+ value === 'product-unavailable' ||
237
+ value === 'insufficient-credits' ||
238
+ value === 'invalid-token' ||
239
+ value === 'device-inactive'
240
+ );
241
+ }
242
+
243
+ function normalizePlatformMode(value: unknown): PlatformModeSnapshot | null {
244
+ const mode = value as PlatformModeSnapshot;
245
+ if (mode?.state === 'inactive') {
246
+ return {
247
+ state: 'inactive',
248
+ ...(isPlatformModeReleaseReason(mode.releaseReason) && typeof mode.releasedAt === 'string'
249
+ ? { releaseReason: mode.releaseReason, releasedAt: mode.releasedAt }
250
+ : {}),
251
+ };
252
+ }
253
+ if (
254
+ mode?.state === 'active' &&
255
+ typeof mode.productId === 'string' &&
256
+ typeof mode.startedAt === 'string'
257
+ ) {
258
+ return { state: 'active', productId: mode.productId, startedAt: mode.startedAt };
259
+ }
260
+ return null;
261
+ }
262
+
263
+ function normalizePlatformModeResult(value: unknown): PlatformModeResult {
264
+ const mode = normalizePlatformMode(value);
265
+ if (mode) {
266
+ return mode;
267
+ }
268
+
269
+ const result = value as PlatformModeResult;
270
+ if (
271
+ result?.state === 'logged-out' ||
272
+ result?.state === 'offline' ||
273
+ result?.state === 'invalid-token' ||
274
+ result?.state === 'product-not-selected'
275
+ ) {
276
+ return { state: result.state };
277
+ }
278
+ if (
279
+ (result?.state === 'product-not-found' || result?.state === 'product-unavailable') &&
280
+ typeof result.productId === 'string'
281
+ ) {
282
+ return { state: result.state, productId: result.productId };
283
+ }
284
+ if (
285
+ result?.state === 'insufficient-credits' &&
286
+ typeof result.productId === 'string' &&
287
+ Number.isFinite(result.requiredCredits) &&
288
+ Number.isFinite(result.currentCredits)
289
+ ) {
290
+ return {
291
+ state: 'insufficient-credits',
292
+ productId: result.productId,
293
+ requiredCredits: result.requiredCredits,
294
+ currentCredits: result.currentCredits,
295
+ };
296
+ }
297
+
298
+ return { state: 'offline' };
299
+ }
300
+
301
+ function isPlatformStatus(value: unknown): value is PlatformStatus {
302
+ const status = value as PlatformStatus;
303
+ if (status?.state === 'logged-out') {
304
+ return true;
305
+ }
306
+
307
+ if (status?.state === 'logged-in') {
308
+ return (
309
+ isUserSummary(status.user) &&
310
+ isDeviceSummary(status.device) &&
311
+ (status.mode === undefined || normalizePlatformMode(status.mode) !== null)
312
+ );
313
+ }
314
+
315
+ if (status?.state === 'offline' || status?.state === 'invalid-token') {
316
+ return (
317
+ (status.user === undefined || isUserSummary(status.user)) &&
318
+ (status.device === undefined || isDeviceSummary(status.device)) &&
319
+ (status.mode === undefined || normalizePlatformMode(status.mode) !== null)
320
+ );
321
+ }
322
+
323
+ return false;
324
+ }
325
+
326
+ function normalizePlatformCatalog(value: unknown): PlatformCatalog {
327
+ const catalog = value as PlatformCatalog;
328
+ if (catalog?.state === 'logged-out') {
329
+ return { state: 'logged-out', products: [] };
330
+ }
331
+
332
+ if (catalog?.state === 'offline') {
333
+ const mode = normalizePlatformMode(catalog.mode);
334
+ return { state: 'offline', ...(mode ? { mode } : {}), products: [] };
335
+ }
336
+
337
+ if (catalog?.state === 'invalid-token') {
338
+ const mode = normalizePlatformMode(catalog.mode);
339
+ return { state: 'invalid-token', ...(mode ? { mode } : {}), products: [] };
340
+ }
341
+
342
+ if (
343
+ catalog?.state === 'logged-in' &&
344
+ typeof catalog.account?.credits === 'number' &&
345
+ Number.isFinite(catalog.account.credits) &&
346
+ Array.isArray(catalog.products)
347
+ ) {
348
+ return {
349
+ state: 'logged-in',
350
+ account: { credits: catalog.account.credits },
351
+ ...(normalizePlatformMode(catalog.mode) ? { mode: normalizePlatformMode(catalog.mode) ?? undefined } : {}),
352
+ ...(typeof catalog.selectedProductId === 'string'
353
+ ? { selectedProductId: catalog.selectedProductId }
354
+ : {}),
355
+ products: catalog.products.filter(isProductSummary),
356
+ };
357
+ }
358
+
359
+ return { state: 'offline', products: [] };
360
+ }
361
+
362
+ function normalizePlatformSelectionResult(value: unknown): PlatformSelectionResult {
363
+ const result = value as PlatformSelectionResult;
364
+ if (result?.state === 'selected' && typeof result.selectedProductId === 'string') {
365
+ return { state: 'selected', selectedProductId: result.selectedProductId };
366
+ }
367
+ if (result?.state === 'logged-out' || result?.state === 'offline' || result?.state === 'invalid-token') {
368
+ return { state: result.state };
369
+ }
370
+ if (
371
+ (result?.state === 'product-not-found' || result?.state === 'product-unavailable') &&
372
+ typeof result.productId === 'string'
373
+ ) {
374
+ return { state: result.state, productId: result.productId };
375
+ }
376
+ return { state: 'offline' };
377
+ }
378
+
379
+ function platformLoginErrorMessage(code: unknown) {
380
+ if (code === 'PASSWORD_LOGIN_INVALID') {
381
+ return '登录失败:账号或密码不正确';
382
+ }
383
+ if (code === 'PASSWORD_LOGIN_NOT_CONFIGURED') {
384
+ return '登录失败:这个账号还没有设置网页登录密码,请先在用户端完成注册或重置密码';
385
+ }
386
+ if (code === 'DEVICE_LIMIT_REACHED') {
387
+ return '登录失败:设备数量已达上限,请先在用户端移除旧设备';
388
+ }
389
+ return '登录失败:本地服务没有连上平台 API,或账号密码不正确';
390
+ }
391
+
392
+ async function postJson<T>(
393
+ options: RuntimeResolutionOptions,
394
+ path: string,
395
+ body?: Record<string, unknown>,
396
+ ) {
397
+ const runtime = await resolveRuntimeForRequest(options);
398
+ if (!runtime) {
399
+ throw new Error('Cursor Pool service runtime is unavailable');
400
+ }
401
+
402
+ const response = await fetch(serviceUrl(runtime, path), {
403
+ method: 'POST',
404
+ headers: { 'content-type': 'application/json' },
405
+ body: JSON.stringify(body ?? {}),
406
+ });
407
+
408
+ if (!response.ok) {
409
+ if (response.status === 400 && path === '/platform/login') {
410
+ const errorBody = await response.json().catch(() => ({})) as { code?: unknown };
411
+ throw new Error(platformLoginErrorMessage(errorBody.code));
412
+ }
413
+ throw new Error(`本地服务请求失败:${response.status}`);
414
+ }
415
+
416
+ return (await response.json()) as T;
417
+ }
418
+
419
+ export async function getServiceStatus(
420
+ runtimeFile?: string,
421
+ options: Omit<RuntimeResolutionOptions, 'runtimeFile'> = {},
422
+ ): Promise<ExtensionServiceStatus> {
423
+ const runtime = await resolveHealthyRuntime({ runtimeFile, ...options });
424
+ if (!runtime) {
425
+ return { service: 'stopped', runtime: null };
426
+ }
427
+
428
+ try {
429
+ const response = await fetch(serviceUrl(runtime, '/extension/status'), {
430
+ method: 'POST',
431
+ headers: { 'content-type': 'application/json' },
432
+ body: JSON.stringify({ connected: true }),
433
+ });
434
+ if (!response.ok) {
435
+ return { service: 'stopped', runtime };
436
+ }
437
+ return { service: 'running', runtime };
438
+ } catch {
439
+ return { service: 'stopped', runtime };
440
+ }
441
+ }
442
+
443
+ export async function getPlatformStatus(
444
+ runtimeFile?: string,
445
+ options: Omit<RuntimeResolutionOptions, 'runtimeFile'> = {},
446
+ ): Promise<PlatformStatus> {
447
+ const runtime = await resolveRuntimeForRequest({ runtimeFile, ...options });
448
+ if (!runtime) {
449
+ return { state: 'logged-out' };
450
+ }
451
+
452
+ try {
453
+ const response = await fetch(serviceUrl(runtime, '/platform/status'));
454
+ if (!response.ok) {
455
+ return { state: 'offline' };
456
+ }
457
+
458
+ const status = (await response.json()) as unknown;
459
+ return isPlatformStatus(status) ? status : { state: 'offline' };
460
+ } catch {
461
+ return { state: 'offline' };
462
+ }
463
+ }
464
+
465
+ export async function getPlatformCatalog(
466
+ runtimeFile?: string,
467
+ options: Omit<RuntimeResolutionOptions, 'runtimeFile'> = {},
468
+ ): Promise<PlatformCatalog> {
469
+ const runtime = await resolveRuntimeForRequest({ runtimeFile, ...options });
470
+ if (!runtime) {
471
+ return { state: 'logged-out', products: [] };
472
+ }
473
+
474
+ try {
475
+ const response = await fetch(serviceUrl(runtime, '/platform/catalog'));
476
+ if (!response.ok) {
477
+ return { state: 'offline', products: [] };
478
+ }
479
+
480
+ return normalizePlatformCatalog(await response.json());
481
+ } catch {
482
+ return { state: 'offline', products: [] };
483
+ }
484
+ }
485
+
486
+ export async function startMode(
487
+ runtimeFile?: string,
488
+ options: Omit<RuntimeResolutionOptions, 'runtimeFile'> = {},
489
+ ): Promise<PlatformModeResult> {
490
+ try {
491
+ return normalizePlatformModeResult(await postJson<unknown>({ runtimeFile, ...options }, '/mode/start'));
492
+ } catch {
493
+ return { state: 'offline' };
494
+ }
495
+ }
496
+
497
+ export async function stopMode(
498
+ runtimeFile?: string,
499
+ options: Omit<RuntimeResolutionOptions, 'runtimeFile'> = {},
500
+ ): Promise<PlatformModeResult> {
501
+ try {
502
+ return normalizePlatformModeResult(await postJson<unknown>({ runtimeFile, ...options }, '/mode/stop'));
503
+ } catch {
504
+ return { state: 'offline' };
505
+ }
506
+ }
507
+
508
+ export async function loginPlatform(options: PlatformLoginInput): Promise<PlatformStatus> {
509
+ return postJson<PlatformStatus>(options, '/platform/login', {
510
+ email: options.email,
511
+ password: options.password,
512
+ device: buildExtensionDeviceInfo(),
513
+ });
514
+ }
515
+
516
+ export async function selectPlatformProduct(
517
+ options: PlatformSelectionInput,
518
+ ): Promise<PlatformSelectionResult> {
519
+ const runtime = await resolveRuntimeForRequest(options);
520
+ if (!runtime) {
521
+ return { state: 'offline' };
522
+ }
523
+
524
+ try {
525
+ const response = await fetch(serviceUrl(runtime, '/platform/selection'), {
526
+ method: 'POST',
527
+ headers: { 'content-type': 'application/json' },
528
+ body: JSON.stringify({ productId: options.productId }),
529
+ });
530
+ if (!response.ok) {
531
+ return { state: 'offline' };
532
+ }
533
+
534
+ return normalizePlatformSelectionResult(await response.json());
535
+ } catch {
536
+ return { state: 'offline' };
537
+ }
538
+ }
539
+
540
+ export async function logoutPlatform(
541
+ runtimeFile?: string,
542
+ options: Omit<RuntimeResolutionOptions, 'runtimeFile'> = {},
543
+ ): Promise<PlatformStatus> {
544
+ return postJson<PlatformStatus>({ runtimeFile, ...options }, '/platform/logout');
545
+ }