@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,104 @@
1
+ import { getServiceStatus } from './api';
2
+ import { registerStatusPanel } from './panel';
3
+
4
+ async function appendDiagnostic(message: string) {
5
+ try {
6
+ const os = await new Function('specifier', 'return import(specifier)')('node:os');
7
+ const path = await new Function('specifier', 'return import(specifier)')('node:path');
8
+ const fs = await new Function('specifier', 'return import(specifier)')('node:fs/promises');
9
+ const logDir = path.join(os.homedir(), '.cursor-pool/logs');
10
+ await fs.mkdir(logDir, { recursive: true });
11
+ await fs.appendFile(
12
+ path.join(logDir, 'extension.log'),
13
+ `${new Date().toISOString()} ${message}\n`,
14
+ 'utf8',
15
+ );
16
+ } catch {
17
+ // Diagnostics must never block extension activation.
18
+ }
19
+ }
20
+
21
+ type ExtensionContext = {
22
+ subscriptions?: { push: (subscription: unknown) => unknown };
23
+ };
24
+
25
+ type VscodeLike = {
26
+ commands?: {
27
+ executeCommand?: (command: string) => unknown;
28
+ registerCommand?: (command: string, handler: () => unknown) => unknown;
29
+ };
30
+ window?: {
31
+ showInformationMessage?: (message: string) => unknown;
32
+ };
33
+ };
34
+
35
+ type CommandOptions = {
36
+ runtimeFile?: string;
37
+ getServiceStatus?: typeof getServiceStatus;
38
+ };
39
+
40
+ export function registerExtensionCommands(
41
+ context: ExtensionContext,
42
+ vscode: VscodeLike,
43
+ options: CommandOptions = {},
44
+ ) {
45
+ void appendDiagnostic('registerExtensionCommands');
46
+ const openClientSubscription = vscode.commands?.registerCommand?.('cursorPool.openClient', () => {
47
+ void appendDiagnostic('cursorPool.openClient invoked');
48
+ try {
49
+ const result = vscode.commands?.executeCommand?.('workbench.view.extension.cursorPoolClient');
50
+ void appendDiagnostic('workbench.view.extension.cursorPoolClient dispatched');
51
+ return result;
52
+ } catch (error) {
53
+ void appendDiagnostic(`workbench.view.extension.cursorPoolClient failed: ${error instanceof Error ? error.message : String(error)}`);
54
+ throw error;
55
+ }
56
+ });
57
+
58
+ if (openClientSubscription) {
59
+ void appendDiagnostic('cursorPool.openClient registered');
60
+ context.subscriptions?.push(openClientSubscription);
61
+ } else {
62
+ void appendDiagnostic('cursorPool.openClient not registered');
63
+ }
64
+
65
+ const openConsoleSubscription = vscode.commands?.registerCommand?.('cursorPool.openConsole', () => {
66
+ vscode.window?.showInformationMessage?.('请在网页端查看充值、记录、设备和账号信息。');
67
+ });
68
+
69
+ if (openConsoleSubscription) {
70
+ context.subscriptions?.push(openConsoleSubscription);
71
+ }
72
+
73
+ const reportStatusSubscription = vscode.commands?.registerCommand?.(
74
+ 'cursorPool.reportStatus',
75
+ async () => {
76
+ const readStatus = options.getServiceStatus ?? getServiceStatus;
77
+ const status = await readStatus(options.runtimeFile);
78
+ vscode.window?.showInformationMessage?.(`Cursor Pool service: ${status.service}`);
79
+ },
80
+ );
81
+
82
+ if (reportStatusSubscription) {
83
+ context.subscriptions?.push(reportStatusSubscription);
84
+ }
85
+ }
86
+
87
+ export async function activate(
88
+ context: ExtensionContext,
89
+ injectedVscode?: VscodeLike,
90
+ options: CommandOptions = {},
91
+ ) {
92
+ void appendDiagnostic('activate start');
93
+ try {
94
+ const vscode = injectedVscode ?? (await new Function('specifier', 'return import(specifier)')('vscode'));
95
+ registerExtensionCommands(context, vscode, options);
96
+ registerStatusPanel(context, vscode, options);
97
+ void appendDiagnostic('activate complete');
98
+ } catch (error) {
99
+ void appendDiagnostic(`activate failed: ${error instanceof Error ? error.message : String(error)}`);
100
+ return;
101
+ }
102
+ }
103
+
104
+ export function deactivate() {}
@@ -0,0 +1 @@
1
+ export const packageRole = 'extension';
@@ -0,0 +1,569 @@
1
+ import {
2
+ getPlatformCatalog,
3
+ getPlatformStatus,
4
+ getServiceStatus,
5
+ ensureLocalService,
6
+ loginPlatform,
7
+ logoutPlatform,
8
+ selectPlatformProduct,
9
+ startMode,
10
+ stopMode,
11
+ type PlatformCatalog,
12
+ type PlatformModeResult,
13
+ type PlatformProductSummary,
14
+ type PlatformSelectionResult,
15
+ type PlatformStatus,
16
+ } from './api';
17
+
18
+ export type PanelStatus = {
19
+ patch: string;
20
+ service: string;
21
+ compat: string;
22
+ mode: string;
23
+ platform?: string;
24
+ user?: string;
25
+ device?: string;
26
+ heartbeat?: string;
27
+ catalog?: string;
28
+ credits?: string;
29
+ selectedProductId?: string;
30
+ products?: PlatformProductSummary[];
31
+ message?: string;
32
+ error?: string;
33
+ };
34
+
35
+ export type PanelViewModel = {
36
+ title: string;
37
+ rows: { label: string; value: string }[];
38
+ platformControls: {
39
+ showLogin: boolean;
40
+ showLogout: boolean;
41
+ showRefresh: boolean;
42
+ showEnableTakeover: boolean;
43
+ showDisableTakeover: boolean;
44
+ };
45
+ consoleCommand: string;
46
+ selectedProductId?: string;
47
+ products: PlatformProductSummary[];
48
+ message?: string;
49
+ error?: string;
50
+ };
51
+
52
+ type ExtensionContext = {
53
+ subscriptions?: { push: (subscription: unknown) => unknown };
54
+ };
55
+
56
+ type WebviewView = {
57
+ webview: {
58
+ html: string;
59
+ options?: Record<string, unknown>;
60
+ onDidReceiveMessage?: (handler: (message: WebviewMessage) => unknown) => unknown;
61
+ };
62
+ };
63
+
64
+ type WebviewMessage = {
65
+ command?: string;
66
+ email?: string;
67
+ password?: string;
68
+ productId?: string;
69
+ };
70
+
71
+ type VscodeLike = {
72
+ window?: {
73
+ registerWebviewViewProvider?: (
74
+ id: string,
75
+ provider: { resolveWebviewView: (view: WebviewView) => unknown },
76
+ ) => unknown;
77
+ showInformationMessage?: (message: string) => unknown;
78
+ };
79
+ commands?: {
80
+ executeCommand?: (command: string) => unknown;
81
+ };
82
+ };
83
+
84
+ type RegisterStatusPanelOptions = {
85
+ runtimeFile?: string;
86
+ status?: PanelStatus;
87
+ getServiceStatus?: typeof getServiceStatus;
88
+ ensureService?: (runtimeFile?: string) => Promise<void>;
89
+ getPlatformStatus?: typeof getPlatformStatus;
90
+ getPlatformCatalog?: typeof getPlatformCatalog;
91
+ loginPlatform?: typeof loginPlatform;
92
+ logoutPlatform?: typeof logoutPlatform;
93
+ selectPlatformProduct?: typeof selectPlatformProduct;
94
+ startMode?: typeof startMode;
95
+ stopMode?: typeof stopMode;
96
+ };
97
+
98
+ export function createPanelViewModel(status: PanelStatus): PanelViewModel {
99
+ const rows = [
100
+ { label: 'patch', value: status.patch },
101
+ { label: 'service', value: status.service },
102
+ { label: 'compat', value: status.compat },
103
+ { label: 'mode', value: status.mode },
104
+ { label: 'platform', value: status.platform ?? 'logged-out' },
105
+ ];
106
+
107
+ if (status.user) {
108
+ rows.push({ label: 'user', value: status.user });
109
+ }
110
+
111
+ if (status.device) {
112
+ rows.push({ label: 'device', value: status.device });
113
+ }
114
+
115
+ if (status.heartbeat) {
116
+ rows.push({ label: 'heartbeat', value: status.heartbeat });
117
+ }
118
+
119
+ if (status.catalog) {
120
+ rows.push({ label: 'catalog', value: status.catalog });
121
+ }
122
+
123
+ if (status.credits) {
124
+ rows.push({ label: 'credits', value: status.credits });
125
+ }
126
+
127
+ if (status.selectedProductId) {
128
+ rows.push({ label: 'current product', value: status.selectedProductId });
129
+ }
130
+
131
+ const platformState = status.platform ?? 'logged-out';
132
+ const takeoverActive = status.mode.startsWith('active ');
133
+ const platformLoggedIn =
134
+ platformState === 'logged-in' || platformState === 'offline' || platformState === 'invalid-token';
135
+
136
+ return {
137
+ title: 'Cursor Pool 号池客户端',
138
+ rows,
139
+ platformControls: {
140
+ showLogin: platformState === 'logged-out' || platformState === 'offline',
141
+ showLogout:
142
+ platformLoggedIn,
143
+ showRefresh: true,
144
+ showEnableTakeover: platformState === 'logged-in' && !takeoverActive,
145
+ showDisableTakeover: platformLoggedIn && takeoverActive,
146
+ },
147
+ consoleCommand: 'cursorPool.openConsole',
148
+ selectedProductId: status.selectedProductId,
149
+ products: status.products ?? [],
150
+ message: status.message,
151
+ error: status.error,
152
+ };
153
+ }
154
+
155
+ function formatPanelMode(mode: PlatformStatus['mode']) {
156
+ if (mode?.state === 'active') {
157
+ return `active ${mode.productId}`;
158
+ }
159
+ if (mode?.state === 'inactive' && mode.releaseReason) {
160
+ return `inactive ${mode.releaseReason}`;
161
+ }
162
+ if (mode?.state === 'inactive') {
163
+ return 'inactive';
164
+ }
165
+ return undefined;
166
+ }
167
+
168
+ function formatPanelPlatformStatus(
169
+ status: PlatformStatus,
170
+ ): Pick<PanelStatus, 'platform' | 'user' | 'device' | 'heartbeat' | 'mode'> {
171
+ const mode = formatPanelMode(status.mode);
172
+ return {
173
+ platform: status.state,
174
+ user: status.user?.email,
175
+ device: status.device ? `${status.device.status} ${status.device.id}` : undefined,
176
+ heartbeat: status.device?.lastHeartbeatAt,
177
+ ...(mode ? { mode } : {}),
178
+ };
179
+ }
180
+
181
+ function formatPanelPlatformCatalog(
182
+ catalog: PlatformCatalog,
183
+ ): Pick<PanelStatus, 'catalog' | 'credits' | 'selectedProductId' | 'products' | 'mode'> {
184
+ const mode = catalog.state === 'logged-in' || catalog.state === 'offline' || catalog.state === 'invalid-token'
185
+ ? formatPanelMode(catalog.mode)
186
+ : undefined;
187
+ return {
188
+ catalog: catalog.state,
189
+ credits: catalog.state === 'logged-in' ? String(catalog.account.credits) : undefined,
190
+ selectedProductId: catalog.state === 'logged-in' ? catalog.selectedProductId : undefined,
191
+ products: catalog.state === 'logged-in' ? catalog.products : [],
192
+ ...(mode ? { mode } : {}),
193
+ };
194
+ }
195
+
196
+ function platformSelectionMessage(result: PlatformSelectionResult) {
197
+ if (result.state === 'selected') {
198
+ return { message: '商品已切换' };
199
+ }
200
+ if (result.state === 'logged-out') {
201
+ return { error: '请先登录平台' };
202
+ }
203
+ if (result.state === 'invalid-token') {
204
+ return { error: '平台登录已失效,请重新登录' };
205
+ }
206
+ if (result.state === 'product-not-found') {
207
+ return { error: '当前商品列表中没有这个商品' };
208
+ }
209
+ if (result.state === 'product-unavailable') {
210
+ return { error: '商品当前不可用' };
211
+ }
212
+ return { error: '平台服务或网页端 API 暂时不可用' };
213
+ }
214
+
215
+ function platformModeMessage(result: PlatformModeResult, action: 'start' | 'stop') {
216
+ if (result.state === 'active') {
217
+ return { message: '已开启平台接管' };
218
+ }
219
+ if (result.state === 'inactive') {
220
+ return { message: '已切回官方模式' };
221
+ }
222
+ if (result.state === 'logged-out') {
223
+ return { error: '请先登录平台' };
224
+ }
225
+ if (result.state === 'invalid-token') {
226
+ return { error: '平台登录已失效,请重新登录' };
227
+ }
228
+ if (result.state === 'product-not-selected') {
229
+ return { error: '开启平台接管前请先选择商品' };
230
+ }
231
+ if (result.state === 'product-not-found') {
232
+ return { error: '所选商品不在当前商品列表中' };
233
+ }
234
+ if (result.state === 'product-unavailable') {
235
+ return { error: '所选商品当前不可用' };
236
+ }
237
+ if (result.state === 'insufficient-credits') {
238
+ return { error: '积分不足,请打开网页端处理' };
239
+ }
240
+ return { error: action === 'start' ? '平台服务或网页端 API 暂时不可用' : '平台服务暂时不可用' };
241
+ }
242
+
243
+ function escapeHtml(value: string) {
244
+ return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
245
+ }
246
+
247
+ export function buildPanelHtml(viewModel: PanelViewModel): string {
248
+ const valueFor = (label: string, fallback = '-') =>
249
+ viewModel.rows.find((row) => row.label === label)?.value ?? fallback;
250
+ const statusItems = [
251
+ { label: '本地服务', value: valueFor('service') },
252
+ ...(valueFor('user', '') ? [{ label: '登录账号', value: valueFor('user') }] : []),
253
+ ...(valueFor('credits', '') ? [{ label: '积分余额', value: valueFor('credits') }] : []),
254
+ ...(valueFor('user', '')
255
+ ? [{
256
+ label: '当前模式',
257
+ value: valueFor('mode').startsWith('active ') ? '平台接管' : '官方模式',
258
+ }]
259
+ : []),
260
+ ];
261
+ const rows = statusItems
262
+ .map((row) => `<li><span>${escapeHtml(row.label)}</span><strong>${escapeHtml(row.value)}</strong></li>`)
263
+ .join('');
264
+ const currentProductName = viewModel.products.find((product) => product.id === viewModel.selectedProductId)?.name;
265
+ const currentProduct = currentProductName
266
+ ? `<li><span>当前使用</span><strong>${escapeHtml(currentProductName)}</strong></li>`
267
+ : '';
268
+ const platformControls = [
269
+ viewModel.platformControls.showLogin
270
+ ? `<section class="panel-section platform-login">
271
+ <h2>号池登录</h2>
272
+ <input name="email" placeholder="账号/邮箱" autocomplete="username">
273
+ <input name="password" type="password" placeholder="密码" autocomplete="current-password">
274
+ <button type="button" data-command="platform/login">登录</button>
275
+ </section>`
276
+ : '',
277
+ viewModel.platformControls.showLogout
278
+ ? '<button type="button" data-command="platform/logout">退出登录</button>'
279
+ : '',
280
+ viewModel.platformControls.showEnableTakeover
281
+ ? '<button type="button" data-command="takeover/enable">开启平台接管</button>'
282
+ : '',
283
+ viewModel.platformControls.showDisableTakeover
284
+ ? '<button type="button" data-command="takeover/disable">切回官方模式</button>'
285
+ : '',
286
+ viewModel.platformControls.showRefresh
287
+ ? '<button type="button" data-command="platform/refresh">刷新状态</button>'
288
+ : '',
289
+ ].join('');
290
+ const message = viewModel.message ? `<p class="message">${escapeHtml(viewModel.message)}</p>` : '';
291
+ const error = viewModel.error ? `<p class="error">${escapeHtml(viewModel.error)}</p>` : '';
292
+ const productItems = viewModel.products
293
+ .map(
294
+ (product) => {
295
+ const rowClass = product.id === viewModel.selectedProductId ? 'product-row is-selected' : 'product-row';
296
+ return `<li class="${rowClass}" title="${escapeHtml(product.description)}">
297
+ <div class="product-main">
298
+ <strong>${escapeHtml(product.name)}</strong>
299
+ <span class="product-description">${escapeHtml(product.description)}</span>
300
+ </div>
301
+ <div class="product-meta">
302
+ <span>${escapeHtml(product.status)}</span>
303
+ <span>准入 ${escapeHtml(String(product.minCredits))}</span>
304
+ <span>${escapeHtml(product.usageLabel)}</span>
305
+ </div>
306
+ <div class="product-action">
307
+ ${product.id === viewModel.selectedProductId
308
+ ? '<span class="current-badge">当前使用</span>'
309
+ : product.status === 'available'
310
+ ? `<button type="button" data-command="platform/select-product" data-product-id="${escapeHtml(product.id)}">使用</button>`
311
+ : ''}
312
+ </div>
313
+ </li>`;
314
+ },
315
+ )
316
+ .join('');
317
+ const products = viewModel.products.length > 0
318
+ ? `<section class="panel-section products"><h2>可用商品</h2><ul class="product-list">${productItems}</ul></section>`
319
+ : '';
320
+
321
+ return `<!doctype html>
322
+ <html lang="zh-CN">
323
+ <head>
324
+ <meta charset="utf-8">
325
+ <style>
326
+ body { font-family: var(--vscode-font-family, sans-serif); padding: 12px; color: var(--vscode-foreground); }
327
+ h1 { font-size: 15px; margin: 0 0 10px; }
328
+ h2 { font-size: 12px; margin: 0 0 8px; }
329
+ ul { list-style: none; margin: 0; padding: 0; }
330
+ li { margin: 0; }
331
+ .status-list { display: grid; gap: 6px; }
332
+ .status-list li { display: flex; justify-content: space-between; gap: 10px; min-width: 0; }
333
+ .status-list span { color: var(--vscode-descriptionForeground, #777); }
334
+ .status-list strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
335
+ strong { font-weight: 600; }
336
+ button { border: 1px solid var(--vscode-button-border, transparent); border-radius: 4px; padding: 3px 8px; color: var(--vscode-button-foreground); background: var(--vscode-button-background); }
337
+ input { box-sizing: border-box; display: block; width: 100%; margin: 6px 0; }
338
+ .panel-section { border-top: 1px solid var(--vscode-panel-border, #ddd); padding-top: 10px; margin-top: 10px; }
339
+ .products { margin-top: 10px; }
340
+ .product-list { max-height: 260px; overflow: auto; border: 1px solid var(--vscode-panel-border, #ddd); border-radius: 6px; }
341
+ .product-row { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; align-items: center; padding: 8px; border-top: 1px solid var(--vscode-panel-border, #ddd); }
342
+ .product-row:first-child { border-top: 0; }
343
+ .product-row.is-selected { background: var(--vscode-list-activeSelectionBackground, rgba(45, 91, 209, 0.14)); }
344
+ .product-main { min-width: 0; }
345
+ .product-main strong, .product-description { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
346
+ .product-description, .product-meta { color: var(--vscode-descriptionForeground, #777); font-size: 11px; }
347
+ .product-meta { display: grid; gap: 1px; white-space: nowrap; }
348
+ .product-action { min-width: 46px; text-align: right; }
349
+ .current-badge { color: var(--vscode-testing-iconPassed, #17633f); font-size: 12px; font-weight: 600; white-space: nowrap; }
350
+ .platform-controls { margin-top: 10px; }
351
+ .platform-controls button { margin-right: 8px; margin-bottom: 8px; }
352
+ .message { color: var(--vscode-descriptionForeground, #666); }
353
+ .error { color: var(--vscode-errorForeground, #b00020); }
354
+ </style>
355
+ </head>
356
+ <body>
357
+ <h1>${escapeHtml(viewModel.title)}</h1>
358
+ <section class="panel-section"><h2>运行状态</h2><ul class="status-list">${rows}${currentProduct}</ul></section>
359
+ ${message}
360
+ ${error}
361
+ ${products}
362
+ <div class="platform-controls">${platformControls}</div>
363
+ <script>
364
+ const vscode = acquireVsCodeApi();
365
+ document.addEventListener('click', (event) => {
366
+ const target = event.target;
367
+ if (!(target instanceof HTMLElement) || !target.dataset.command) {
368
+ return;
369
+ }
370
+
371
+ const payload = { command: target.dataset.command };
372
+ if (target.dataset.command === 'platform/login') {
373
+ payload.email = document.querySelector('[name="email"]')?.value || '';
374
+ payload.password = document.querySelector('[name="password"]')?.value || '';
375
+ }
376
+ if (target.dataset.productId) {
377
+ payload.productId = target.dataset.productId;
378
+ }
379
+
380
+ vscode.postMessage(payload);
381
+ });
382
+ </script>
383
+ </body>
384
+ </html>`;
385
+ }
386
+
387
+ export function registerStatusPanel(
388
+ context: ExtensionContext,
389
+ vscode: VscodeLike,
390
+ options: RegisterStatusPanelOptions = {},
391
+ ) {
392
+ const provider = {
393
+ async resolveWebviewView(view: WebviewView) {
394
+ const readServiceStatus = options.getServiceStatus ?? getServiceStatus;
395
+ const ensureService = options.ensureService ?? (async (runtimeFile?: string) => {
396
+ await ensureLocalService(runtimeFile);
397
+ });
398
+ const readPlatformStatus = options.getPlatformStatus ?? getPlatformStatus;
399
+ const readPlatformCatalog = options.getPlatformCatalog ?? getPlatformCatalog;
400
+ const doLoginPlatform = options.loginPlatform ?? loginPlatform;
401
+ const doLogoutPlatform = options.logoutPlatform ?? logoutPlatform;
402
+ const doSelectPlatformProduct = options.selectPlatformProduct ?? selectPlatformProduct;
403
+ const doStartMode = options.startMode ?? startMode;
404
+ const doStopMode = options.stopMode ?? stopMode;
405
+ let serviceEnsured = false;
406
+ let initialServiceStatus: Awaited<ReturnType<typeof readServiceStatus>> | undefined;
407
+
408
+ view.webview.options = {
409
+ ...view.webview.options,
410
+ enableScripts: true,
411
+ };
412
+
413
+ const render = async (
414
+ feedback: Pick<PanelStatus, 'message' | 'error'> = {},
415
+ serviceStatus?: Awaited<ReturnType<typeof readServiceStatus>>,
416
+ ) => {
417
+ const service = options.status
418
+ ? options.status.service
419
+ : (serviceStatus ?? await readServiceStatus(options.runtimeFile)).service;
420
+ const platform = options.status
421
+ ? {}
422
+ : formatPanelPlatformStatus(await readPlatformStatus(options.runtimeFile));
423
+ const catalog = options.status
424
+ ? {}
425
+ : formatPanelPlatformCatalog(await readPlatformCatalog(options.runtimeFile));
426
+ const status = options.status
427
+ ? { ...options.status, ...feedback }
428
+ : {
429
+ patch: 'unknown',
430
+ service,
431
+ compat: 'unknown',
432
+ mode: 'unknown',
433
+ ...platform,
434
+ ...catalog,
435
+ ...feedback,
436
+ };
437
+
438
+ view.webview.html = buildPanelHtml(createPanelViewModel(status));
439
+ };
440
+
441
+ const renderError = async (error: unknown) => {
442
+ try {
443
+ const message = error instanceof Error && error.message === 'fetch failed'
444
+ ? '本地服务未运行,请先启动 Cursor Pool 本地服务'
445
+ : error instanceof Error
446
+ ? error.message
447
+ : 'Cursor Pool service request failed';
448
+ await render({
449
+ error: message,
450
+ });
451
+ } catch {
452
+ // Avoid surfacing secondary render failures as unhandled WebView message rejections.
453
+ }
454
+ };
455
+
456
+ const ensureServiceForPanel = async () => {
457
+ if (serviceEnsured) {
458
+ return undefined;
459
+ }
460
+ const service = await readServiceStatus(options.runtimeFile);
461
+ if (service.service === 'stopped') {
462
+ await ensureService(options.runtimeFile);
463
+ serviceEnsured = true;
464
+ return undefined;
465
+ }
466
+ serviceEnsured = true;
467
+ return service;
468
+ };
469
+
470
+ if (!options.status) {
471
+ try {
472
+ initialServiceStatus = await ensureServiceForPanel();
473
+ } catch {
474
+ // The login form still renders; login/refresh will surface a user-facing error.
475
+ }
476
+ }
477
+ await render({}, initialServiceStatus);
478
+ view.webview.onDidReceiveMessage?.(async (message) => {
479
+ try {
480
+ if (message.command === 'mode/start' || message.command === 'takeover/enable') {
481
+ const result = await doStartMode(options.runtimeFile);
482
+ await render(platformModeMessage(result, 'start'));
483
+ if (result.state === 'active') {
484
+ await vscode.commands?.executeCommand?.('workbench.action.reloadWindow');
485
+ }
486
+ return;
487
+ }
488
+
489
+ if (message.command === 'mode/stop' || message.command === 'takeover/disable') {
490
+ const result = await doStopMode(options.runtimeFile);
491
+ await render(platformModeMessage(result, 'stop'));
492
+ if (result.state === 'inactive') {
493
+ await vscode.commands?.executeCommand?.('workbench.action.reloadWindow');
494
+ }
495
+ return;
496
+ }
497
+
498
+ if (message.command === 'platform/login') {
499
+ if (!message.email) {
500
+ await render({ error: '请输入账号/邮箱' });
501
+ return;
502
+ }
503
+
504
+ if (!message.password) {
505
+ await render({ error: '请输入密码' });
506
+ return;
507
+ }
508
+
509
+ await ensureServiceForPanel();
510
+ try {
511
+ await doLoginPlatform({
512
+ runtimeFile: options.runtimeFile,
513
+ email: message.email,
514
+ password: message.password,
515
+ });
516
+ } catch (error) {
517
+ const current = await readPlatformStatus(options.runtimeFile).catch(() => null);
518
+ if (current?.state === 'logged-in') {
519
+ await render({ message: '平台状态已刷新' });
520
+ return;
521
+ }
522
+ throw error;
523
+ }
524
+ await render({ message: '平台登录成功' });
525
+ return;
526
+ }
527
+
528
+ if (message.command === 'platform/logout') {
529
+ await doLogoutPlatform(options.runtimeFile);
530
+ await render({ message: '已退出平台登录' });
531
+ return;
532
+ }
533
+
534
+ if (message.command === 'platform/select-product') {
535
+ if (!message.productId) {
536
+ await render({ error: '请选择商品' });
537
+ return;
538
+ }
539
+
540
+ const result = await doSelectPlatformProduct({
541
+ runtimeFile: options.runtimeFile,
542
+ productId: message.productId,
543
+ });
544
+ await render(platformSelectionMessage(result));
545
+ return;
546
+ }
547
+
548
+ if (message.command === 'platform/refresh') {
549
+ await render({ message: '平台状态已刷新' });
550
+ return;
551
+ }
552
+
553
+ if (message.command === 'cursorPool.openConsole') {
554
+ await vscode.commands?.executeCommand?.('workbench.action.webview.openDeveloperTools');
555
+ }
556
+ } catch (error) {
557
+ await renderError(error);
558
+ }
559
+ });
560
+ },
561
+ };
562
+
563
+ const subscription = vscode.window?.registerWebviewViewProvider?.('cursorPool.statusPanel', provider);
564
+ if (subscription) {
565
+ context.subscriptions?.push(subscription);
566
+ }
567
+
568
+ return provider;
569
+ }
@@ -0,0 +1,22 @@
1
+ import {
2
+ readRuntimeInfo as readServiceRuntimeInfo,
3
+ resolveRuntimeFile,
4
+ writeRuntimeInfo as writeServiceRuntimeInfo,
5
+ type RuntimeInfo,
6
+ } from '@cursor-pool/service';
7
+
8
+ export type { RuntimeInfo };
9
+
10
+ export type RuntimeOptions = {
11
+ runtimeFile?: string;
12
+ };
13
+
14
+ export async function readRuntimeInfo(options: RuntimeOptions = {}): Promise<RuntimeInfo | null> {
15
+ return readServiceRuntimeInfo({ runtimeFile: options.runtimeFile });
16
+ }
17
+
18
+ export async function writeRuntimeInfo(runtime: RuntimeInfo, options: RuntimeOptions = {}) {
19
+ await writeServiceRuntimeInfo(runtime, { runtimeFile: options.runtimeFile });
20
+ }
21
+
22
+ export { resolveRuntimeFile };