@tapicker/bridge 1.0.0 → 1.1.0

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 (49) hide show
  1. package/README.md +15 -15
  2. package/dist/bridge/bridge-service.d.ts +1 -1
  3. package/dist/bridge/bridge-service.js +7 -7
  4. package/dist/bridge/control.js +1 -1
  5. package/dist/bridge/export-store.d.ts +35 -0
  6. package/dist/bridge/export-store.js +195 -0
  7. package/dist/bridge/extension-session.d.ts +9 -2
  8. package/dist/bridge/extension-session.js +56 -8
  9. package/dist/cli.js +563 -148
  10. package/dist/commands.d.ts +8 -0
  11. package/dist/commands.js +276 -40
  12. package/dist/constants.d.ts +2 -2
  13. package/dist/constants.js +4 -2
  14. package/dist/http-client.d.ts +24 -0
  15. package/dist/http-client.js +101 -0
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +2 -2
  18. package/dist/mcp.d.ts +4 -3
  19. package/dist/mcp.js +221 -134
  20. package/dist/protocol/schema.d.ts +503 -34
  21. package/dist/protocol/schema.js +396 -15
  22. package/dist/server.d.ts +7 -4
  23. package/dist/server.js +70 -26
  24. package/dist/update-notifier-worker.d.ts +2 -0
  25. package/dist/update-notifier-worker.js +3 -0
  26. package/dist/update-notifier.d.ts +9 -0
  27. package/dist/update-notifier.js +178 -0
  28. package/dist/workflow-watch.d.ts +8 -7
  29. package/dist/workflow-watch.js +39 -29
  30. package/package.json +16 -6
  31. package/docs/cli.md +0 -112
  32. package/docs/http-api.md +0 -149
  33. package/docs/mcp.md +0 -90
  34. package/docs/operations.md +0 -85
  35. package/docs/workflow-watch.md +0 -86
  36. package/src/bridge/bridge-service.ts +0 -291
  37. package/src/bridge/control.ts +0 -281
  38. package/src/bridge/daemon.ts +0 -102
  39. package/src/bridge/extension-session.ts +0 -286
  40. package/src/bridge/pairing-state.ts +0 -92
  41. package/src/cli.ts +0 -1019
  42. package/src/commands.ts +0 -158
  43. package/src/constants.ts +0 -10
  44. package/src/errors.ts +0 -72
  45. package/src/index.ts +0 -35
  46. package/src/mcp.ts +0 -386
  47. package/src/protocol/schema.ts +0 -202
  48. package/src/server.ts +0 -623
  49. package/src/workflow-watch.ts +0 -364
package/dist/server.js CHANGED
@@ -7,13 +7,14 @@ import { join, resolve } from 'node:path';
7
7
  import { WebSocketServer } from 'ws';
8
8
  import { createMcpHandler } from '@modelcontextprotocol/server';
9
9
  import { localhostHostValidation, toNodeHandler } from '@modelcontextprotocol/node';
10
- import { DEFAULT_HOST, DEFAULT_MCP_PATH, DEFAULT_PORT, DEFAULT_PORT_MAX, DEFAULT_WS_PATH, DEFAULT_WORKFLOW_WATCH_WS_PATH, PROTOCOL_VERSION, VERSION, } from './constants.js';
10
+ import { DEFAULT_HOST, DEFAULT_MCP_PATH, DEFAULT_PORT, DEFAULT_PORT_MAX, DEFAULT_WS_PATH, DEFAULT_WORKFLOW_STATUS_WATCH_WS_PATH, PROTOCOL_VERSION, VERSION, } from './constants.js';
11
11
  import { BridgeService } from './bridge/bridge-service.js';
12
- import { PairingStateFile } from './bridge/pairing-state.js';
12
+ import { getDefaultBridgeDataDir, PairingStateFile } from './bridge/pairing-state.js';
13
+ import { ExportStore } from './bridge/export-store.js';
13
14
  import { getCommandCapabilities, dispatchCommand } from './commands.js';
14
15
  import { BridgeError, safeError, safeSuccess, toBridgeError } from './errors.js';
15
16
  import { createBridgeMcpApi, createTapickerMcpServer } from './mcp.js';
16
- import { WorkflowWatchRequestSchema, createEnvelope, parseEnvelope } from './protocol/schema.js';
17
+ import { WorkflowStatusWatchRequestSchema, createEnvelope, parseEnvelope } from './protocol/schema.js';
17
18
  const DEFAULT_CORS_ORIGINS = ['*'];
18
19
  const CORS_ALLOWED_METHODS = 'GET, POST, DELETE, OPTIONS';
19
20
  const CORS_ALLOWED_HEADERS = 'Accept, Authorization, Content-Type, Last-Event-ID, MCP-Protocol-Version, Mcp-Session-Id';
@@ -25,6 +26,8 @@ export class BridgeServer {
25
26
  requestedPort;
26
27
  portMax;
27
28
  token;
29
+ exports;
30
+ exportCleanupTimer;
28
31
  corsOrigins;
29
32
  tls;
30
33
  mcpHandler;
@@ -41,28 +44,41 @@ export class BridgeServer {
41
44
  this.token = options.token;
42
45
  this.corsOrigins = normalizeCorsOrigins(options.corsOrigins ?? DEFAULT_CORS_ORIGINS);
43
46
  this.tls = options.tls;
47
+ this.exports = new ExportStore(join(resolve(options.dataDir ?? getDefaultBridgeDataDir()), 'exports'));
44
48
  const pairingState = options.dataDir
45
49
  ? new PairingStateFile(join(resolve(options.dataDir), 'paired-clients.json'))
46
50
  : undefined;
47
51
  this.bridge = new BridgeService({
48
52
  pairingCode: options.pairingCode ?? String(randomInt(0, 10_000)).padStart(4, '0'),
49
53
  capabilities: [
54
+ 'workflow.guide',
55
+ 'workflow.schema',
56
+ 'workflow.validate',
57
+ 'workflow.list',
58
+ 'workflow.get',
59
+ 'workflow.create',
60
+ 'workflow.update',
50
61
  'workflow.start',
51
62
  'workflow.pause',
52
63
  'workflow.resume',
53
64
  'workflow.stop',
54
65
  'workflow.status',
55
- 'workflow.status.get',
66
+ 'workflow.logs',
67
+ 'workflow.log',
68
+ 'data.list',
69
+ 'data.preview',
56
70
  'data.get',
71
+ 'data.export',
57
72
  'data.delete',
73
+ 'user.me',
58
74
  ],
59
75
  pairingState,
60
76
  });
61
- this.mcpHandler = createMcpHandler(() => createTapickerMcpServer(createBridgeMcpApi(this.bridge)));
77
+ this.mcpHandler = createMcpHandler(() => createTapickerMcpServer(createBridgeMcpApi(this.bridge, this.exports)));
62
78
  this.mcpNodeHandler = toNodeHandler(this.mcpHandler, {
63
79
  onerror: (error) => console.error('Tapicker Bridge MCP request failed:', error),
64
80
  });
65
- this.bridge.on('workflow.status', (event) => this.publishWorkflowStatus(event));
81
+ this.bridge.on('workflow.status.changed', (event) => this.publishWorkflowStatus(event));
66
82
  }
67
83
  getPairingCode() {
68
84
  return this.bridge.getPairingCode();
@@ -84,6 +100,9 @@ export class BridgeServer {
84
100
  this.server = runtime.server;
85
101
  this.webSocketServer = runtime.webSocketServer;
86
102
  this.port = actualPort;
103
+ this.cleanExports();
104
+ this.exportCleanupTimer = setInterval(() => this.cleanExports(), 60_000);
105
+ this.exportCleanupTimer.unref();
87
106
  return this.getAddress();
88
107
  }
89
108
  catch (error) {
@@ -99,6 +118,7 @@ export class BridgeServer {
99
118
  throw toBridgeError(lastError, 'PortUnavailable', 503);
100
119
  }
101
120
  async close() {
121
+ clearInterval(this.exportCleanupTimer);
102
122
  this.bridge.close();
103
123
  this.workflowSubscriptions.clear();
104
124
  await this.mcpHandler.close();
@@ -122,7 +142,7 @@ export class BridgeServer {
122
142
  port: this.port,
123
143
  httpUrl: `${protocol}://${displayHost}:${this.port}`,
124
144
  wsUrl: `${webSocketProtocol}://${displayHost}:${this.port}${DEFAULT_WS_PATH}`,
125
- workflowWatchWsUrl: `${webSocketProtocol}://${displayHost}:${this.port}${DEFAULT_WORKFLOW_WATCH_WS_PATH}`,
145
+ workflowStatusWatchWsUrl: `${webSocketProtocol}://${displayHost}:${this.port}${DEFAULT_WORKFLOW_STATUS_WATCH_WS_PATH}`,
126
146
  mcpUrl: `${protocol}://${displayHost}:${this.port}${DEFAULT_MCP_PATH}`,
127
147
  };
128
148
  }
@@ -134,7 +154,7 @@ export class BridgeServer {
134
154
  }
135
155
  createRuntime() {
136
156
  const requestHandler = (request, response) => {
137
- void this.handleRequest(request, response);
157
+ this.handleRequest(request, response);
138
158
  };
139
159
  const server = this.tls
140
160
  ? createHttpsServer({
@@ -145,7 +165,7 @@ export class BridgeServer {
145
165
  const webSocketServer = new WebSocketServer({ noServer: true });
146
166
  server.on('upgrade', (request, socket, head) => {
147
167
  const requestUrl = new URL(request.url || '/', this.getOrigin(request));
148
- if (requestUrl.pathname === DEFAULT_WORKFLOW_WATCH_WS_PATH) {
168
+ if (requestUrl.pathname === DEFAULT_WORKFLOW_STATUS_WATCH_WS_PATH) {
149
169
  try {
150
170
  this.assertAuthorized(request);
151
171
  }
@@ -153,7 +173,7 @@ export class BridgeServer {
153
173
  rejectUpgrade(socket, toBridgeError(error));
154
174
  return;
155
175
  }
156
- webSocketServer.handleUpgrade(request, socket, head, (client) => this.attachWorkflowWatcher(client));
176
+ webSocketServer.handleUpgrade(request, socket, head, (client) => this.attachWorkflowStatusWatcher(client));
157
177
  return;
158
178
  }
159
179
  if (requestUrl.pathname !== DEFAULT_WS_PATH) {
@@ -166,10 +186,21 @@ export class BridgeServer {
166
186
  });
167
187
  return { server, webSocketServer };
168
188
  }
189
+ cleanExports() {
190
+ this.exports.cleanup().catch((error) => console.warn('Failed to clean expired exports:', error));
191
+ }
169
192
  async handleRequest(request, response) {
170
193
  try {
171
194
  const requestUrl = new URL(request.url || '/', this.getOrigin(request));
172
195
  const method = request.method || 'GET';
196
+ const exportRoute = /^\/v1\/exports\/([0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12})(\/upload)?$/.exec(requestUrl.pathname);
197
+ // 上传由当前导出的一次性凭证授权,与浏览器页面的 CORS 配置独立。
198
+ if (method === 'POST' && exportRoute?.[2]) {
199
+ await this.exports.upload(exportRoute[1], request);
200
+ response.statusCode = 204;
201
+ response.end();
202
+ return;
203
+ }
173
204
  if (!this.applyCors(request, response)) {
174
205
  this.sendJson(response, 403, safeError(new BridgeError('CorsOriginNotAllowed', 'Cross-origin requests from this Origin are not allowed', {
175
206
  status: 403,
@@ -189,6 +220,11 @@ export class BridgeServer {
189
220
  await this.mcpNodeHandler(request, response);
190
221
  return;
191
222
  }
223
+ if (method === 'GET' && exportRoute && !exportRoute[2]) {
224
+ this.assertAuthorized(request);
225
+ await this.exports.download(exportRoute[1], response);
226
+ return;
227
+ }
192
228
  if (method === 'GET' && requestUrl.pathname === '/v1/health') {
193
229
  this.sendJson(response, 200, {
194
230
  ok: true,
@@ -206,7 +242,7 @@ export class BridgeServer {
206
242
  protocolVersion: PROTOCOL_VERSION,
207
243
  bridgeId: this.bridgeId,
208
244
  wsPath: DEFAULT_WS_PATH,
209
- workflowWatchPath: DEFAULT_WORKFLOW_WATCH_WS_PATH,
245
+ workflowStatusWatchPath: DEFAULT_WORKFLOW_STATUS_WATCH_WS_PATH,
210
246
  requiresAuthentication: Boolean(this.token),
211
247
  });
212
248
  return;
@@ -225,22 +261,30 @@ export class BridgeServer {
225
261
  });
226
262
  return;
227
263
  }
228
- if (requestUrl.pathname === DEFAULT_WORKFLOW_WATCH_WS_PATH) {
264
+ if (requestUrl.pathname === DEFAULT_WORKFLOW_STATUS_WATCH_WS_PATH) {
229
265
  this.assertAuthorized(request);
230
- this.sendJson(response, 405, safeError(new BridgeError('WebSocketRequired', 'workflow.watch requires a WebSocket connection', { status: 405 })));
266
+ this.sendJson(response, 405, safeError(new BridgeError('WebSocketRequired', 'workflow.status.watch requires a WebSocket connection', {
267
+ status: 405,
268
+ })));
231
269
  return;
232
270
  }
233
271
  if (method === 'POST' && requestUrl.pathname.startsWith('/v1/commands/')) {
234
272
  this.assertAuthorized(request);
235
273
  const commandName = decodeURIComponent(requestUrl.pathname.slice('/v1/commands/'.length));
236
274
  const body = await readJson(request);
237
- const data = await dispatchCommand(commandName, body, { bridge: this.bridge });
275
+ const data = await dispatchCommand(commandName, body, { bridge: this.bridge, exports: this.exports });
238
276
  this.sendJson(response, 200, safeSuccess(data));
239
277
  return;
240
278
  }
241
279
  this.sendJson(response, 404, safeError(new BridgeError('NotFound', 'Route not found', { status: 404 })));
242
280
  }
243
281
  catch (error) {
282
+ if (response.destroyed)
283
+ return;
284
+ if (response.headersSent) {
285
+ response.destroy();
286
+ return;
287
+ }
244
288
  const bridgeError = toBridgeError(error);
245
289
  this.sendJson(response, bridgeError.status, safeError(bridgeError));
246
290
  }
@@ -255,33 +299,33 @@ export class BridgeServer {
255
299
  throw new BridgeError('Unauthorized', 'Bridge authentication is required', { status: 401 });
256
300
  }
257
301
  }
258
- attachWorkflowWatcher(socket) {
302
+ attachWorkflowStatusWatcher(socket) {
259
303
  const remove = () => this.workflowSubscriptions.delete(socket);
260
304
  socket.on('close', remove);
261
305
  socket.on('error', remove);
262
306
  socket.on('message', (raw) => {
263
307
  const parsedEnvelope = parseEnvelope(raw);
264
308
  if (!parsedEnvelope.success) {
265
- socket.close(1007, 'Invalid Workflow watch message');
309
+ socket.close(1007, 'Invalid Workflow status watch message');
266
310
  return;
267
311
  }
268
312
  const request = parsedEnvelope.data;
269
- if (request.type !== 'workflow.watch') {
270
- this.sendWorkflowWatchResult(socket, request.id, safeError(new BridgeError('UnsupportedMessage', 'Only workflow.watch is supported on this socket', {
313
+ if (request.type !== 'workflow.status.watch') {
314
+ this.sendWorkflowStatusWatchResult(socket, request.id, safeError(new BridgeError('UnsupportedMessage', 'Only workflow.status.watch is supported on this socket', {
271
315
  status: 400,
272
316
  })));
273
317
  return;
274
318
  }
275
- const parsedRequest = WorkflowWatchRequestSchema.safeParse(request.data);
319
+ const parsedRequest = WorkflowStatusWatchRequestSchema.safeParse(request.data);
276
320
  if (!parsedRequest.success) {
277
- this.sendWorkflowWatchResult(socket, request.id, safeError(new BridgeError('InvalidArguments', 'Invalid Workflow watch arguments', {
321
+ this.sendWorkflowStatusWatchResult(socket, request.id, safeError(new BridgeError('InvalidArguments', 'Invalid Workflow status watch arguments', {
278
322
  status: 400,
279
323
  details: parsedRequest.error.issues,
280
324
  })));
281
325
  return;
282
326
  }
283
327
  if (this.workflowSubscriptions.has(socket)) {
284
- this.sendWorkflowWatchResult(socket, request.id, safeError(new BridgeError('WorkflowWatchAlreadyStarted', 'This socket is already watching a Workflow', {
328
+ this.sendWorkflowStatusWatchResult(socket, request.id, safeError(new BridgeError('WorkflowStatusWatchAlreadyStarted', 'This socket is already watching a Workflow', {
285
329
  status: 409,
286
330
  })));
287
331
  return;
@@ -291,17 +335,17 @@ export class BridgeServer {
291
335
  const clientId = this.bridge.resolveCommandClientId('workflow.status', watchRequest.clientId);
292
336
  const subscription = { clientId, workflowId: watchRequest.input.workflowId };
293
337
  this.workflowSubscriptions.set(socket, subscription);
294
- this.sendWorkflowWatchResult(socket, request.id, safeSuccess(subscription));
338
+ this.sendWorkflowStatusWatchResult(socket, request.id, safeSuccess(subscription));
295
339
  }
296
340
  catch (error) {
297
- this.sendWorkflowWatchResult(socket, request.id, safeError(error));
341
+ this.sendWorkflowStatusWatchResult(socket, request.id, safeError(error));
298
342
  }
299
343
  });
300
344
  }
301
- sendWorkflowWatchResult(socket, id, data) {
345
+ sendWorkflowStatusWatchResult(socket, id, data) {
302
346
  if (socket.readyState === socket.OPEN) {
303
347
  try {
304
- socket.send(JSON.stringify(createEnvelope('workflow.watch:res', data, { id, callback: false })));
348
+ socket.send(JSON.stringify(createEnvelope('workflow.status.watch:res', data, { id, callback: false })));
305
349
  }
306
350
  catch {
307
351
  this.workflowSubscriptions.delete(socket);
@@ -309,7 +353,7 @@ export class BridgeServer {
309
353
  }
310
354
  }
311
355
  publishWorkflowStatus(event) {
312
- const envelope = JSON.stringify(createEnvelope('workflow.status', event, { callback: false }));
356
+ const envelope = JSON.stringify(createEnvelope('workflow.status.changed', event, { callback: false }));
313
357
  for (const [socket, subscription] of this.workflowSubscriptions) {
314
358
  if (socket.readyState === socket.OPEN && subscriptionKey(subscription) === subscriptionKey(event)) {
315
359
  try {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=update-notifier-worker.d.ts.map
@@ -0,0 +1,3 @@
1
+ import { refreshUpdateCache } from './update-notifier.js';
2
+ await refreshUpdateCache(process.argv[2]).catch(() => undefined);
3
+ //# sourceMappingURL=update-notifier-worker.js.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 从缓存显示提醒,然后按需启动独立的检查进程。
3
+ */
4
+ export declare function showCachedUpdate(directory?: string): Promise<void>;
5
+ /**
6
+ * 在后台进程中刷新 npm 缓存。失败也记录检查时间,避免离线时反复请求。
7
+ */
8
+ export declare function refreshUpdateCache(directory?: string): Promise<void>;
9
+ //# sourceMappingURL=update-notifier.d.ts.map
@@ -0,0 +1,178 @@
1
+ import { exec, spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdir, readFile, rename, rmdir, stat, unlink, writeFile } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { promisify } from 'node:util';
7
+ import { z } from 'zod';
8
+ import { getDefaultBridgeDataDir } from './bridge/pairing-state.js';
9
+ import { VERSION } from './constants.js';
10
+ const UPDATE_INTERVAL = 24 * 60 * 60 * 1000;
11
+ const REQUEST_TIMEOUT = 5000;
12
+ const LOCK_TIMEOUT = 60_000;
13
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org/';
14
+ const execAsync = promisify(exec);
15
+ const CacheSchema = z.object({
16
+ checkedAt: z.number().optional(),
17
+ latestVersion: z.string().optional(),
18
+ notifiedAt: z.number().optional(),
19
+ });
20
+ const RegistrySchema = z.object({ 'dist-tags': z.object({ latest: z.string() }) });
21
+ const numericVersion = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
22
+ /**
23
+ * 仅比较发布流程使用的三段数字版本。
24
+ */
25
+ function isNewerVersion(latest, current) {
26
+ if (!numericVersion.test(latest) || !numericVersion.test(current))
27
+ return false;
28
+ const latestParts = latest.split('.').map(BigInt);
29
+ const currentParts = current.split('.').map(BigInt);
30
+ const difference = latestParts.findIndex((part, index) => part !== currentParts[index]);
31
+ return difference !== -1 && latestParts[difference] > currentParts[difference];
32
+ }
33
+ function isRecent(timestamp) {
34
+ return timestamp !== undefined && Date.now() - timestamp < UPDATE_INTERVAL;
35
+ }
36
+ /**
37
+ * 使用与全局升级命令相同的 npm 配置,并保留镜像地址的路径前缀。
38
+ */
39
+ async function getRegistryPackageUrl() {
40
+ const config = await execAsync('npm config get @tapicker:registry registry --global', {
41
+ timeout: 2000,
42
+ windowsHide: true,
43
+ }).catch(() => undefined);
44
+ const settings = config?.stdout.trim().split(/\r?\n/) ?? [];
45
+ const registry = ['@tapicker:registry', 'registry']
46
+ .map((key) => settings
47
+ .find((line) => line.startsWith(`${key}=`))
48
+ ?.slice(key.length + 1)
49
+ .trim())
50
+ .find((value) => value && value !== 'undefined' && value !== 'null');
51
+ const url = new URL(registry ?? DEFAULT_REGISTRY);
52
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
53
+ throw new Error('Unsupported npm registry protocol');
54
+ }
55
+ url.pathname = `${url.pathname.replace(/\/+$/, '')}/@tapicker%2Fbridge`;
56
+ return url;
57
+ }
58
+ async function readCache(path) {
59
+ try {
60
+ return CacheSchema.safeParse(JSON.parse(await readFile(path, 'utf8'))).data ?? {};
61
+ }
62
+ catch (error) {
63
+ if (error instanceof SyntaxError || error.code === 'ENOENT')
64
+ return {};
65
+ throw error;
66
+ }
67
+ }
68
+ async function writeCache(path, cache) {
69
+ const temporaryPath = `${path}.${randomUUID()}.tmp`;
70
+ try {
71
+ await writeFile(temporaryPath, JSON.stringify(cache), { flag: 'wx', mode: 0o600 });
72
+ await rename(temporaryPath, path);
73
+ }
74
+ finally {
75
+ await unlink(temporaryPath).catch(() => undefined);
76
+ }
77
+ }
78
+ /**
79
+ * 尝试获取短期文件锁。已有进程持锁时直接跳过,避免 CLI 等待网络请求。
80
+ */
81
+ async function withCacheLock(directory, action) {
82
+ const path = join(directory, 'update-notifier.json');
83
+ const lockPath = `${path}.lock`;
84
+ await mkdir(directory, { recursive: true, mode: 0o700 });
85
+ try {
86
+ await mkdir(lockPath);
87
+ }
88
+ catch (error) {
89
+ if (error.code !== 'EEXIST')
90
+ throw error;
91
+ const lock = await stat(lockPath).catch(() => undefined);
92
+ if (!lock || Date.now() - lock.mtimeMs < LOCK_TIMEOUT)
93
+ return;
94
+ await rmdir(lockPath);
95
+ await mkdir(lockPath);
96
+ }
97
+ try {
98
+ await action(path);
99
+ }
100
+ finally {
101
+ await rmdir(lockPath);
102
+ }
103
+ }
104
+ function printUpdate(latest) {
105
+ const useColor = !('NO_COLOR' in process.env) && process.stderr.hasColors?.();
106
+ const title = `Update Available: ${VERSION} → `;
107
+ const lines = [`${title}${latest}`, 'npm i -g @tapicker/bridge@latest', 'Restart Bridge to Apply if Running'];
108
+ const width = Math.max(...lines.map((line) => line.length));
109
+ const border = '─'.repeat(width + 4);
110
+ const rows = lines.map((line, index) => {
111
+ const padding = width - line.length;
112
+ const left = Math.floor(padding / 2);
113
+ const right = padding - left;
114
+ const label = index === 0 && useColor ? `${title}\x1b[36m${latest}\x1b[0m` : line;
115
+ return `│${' '.repeat(left + 2)}${label}${' '.repeat(right + 2)}│`;
116
+ });
117
+ console.error([`╭${border}╮`, ...rows, `╰${border}╯`].join('\n'));
118
+ }
119
+ /**
120
+ * 从缓存显示提醒,然后按需启动独立的检查进程。
121
+ */
122
+ export async function showCachedUpdate(directory = getDefaultBridgeDataDir()) {
123
+ try {
124
+ const cache = await readCache(join(directory, 'update-notifier.json'));
125
+ const latestVersion = cache.latestVersion;
126
+ if (latestVersion && isNewerVersion(latestVersion, VERSION) && !isRecent(cache.notifiedAt)) {
127
+ await withCacheLock(directory, async (path) => {
128
+ const current = await readCache(path);
129
+ if (current.latestVersion !== latestVersion || isRecent(current.notifiedAt))
130
+ return;
131
+ await writeCache(path, { ...current, notifiedAt: Date.now() });
132
+ printUpdate(latestVersion);
133
+ });
134
+ }
135
+ if (!isRecent(cache.checkedAt)) {
136
+ const worker = fileURLToPath(new URL('./update-notifier-worker.js', import.meta.url));
137
+ const child = spawn(process.execPath, [worker, directory], {
138
+ detached: true,
139
+ stdio: 'ignore',
140
+ windowsHide: true,
141
+ });
142
+ child.on('error', () => undefined);
143
+ child.unref();
144
+ }
145
+ }
146
+ catch {
147
+ // 缓存或进程创建失败时,继续执行 CLI 命令。
148
+ }
149
+ }
150
+ /**
151
+ * 在后台进程中刷新 npm 缓存。失败也记录检查时间,避免离线时反复请求。
152
+ */
153
+ export async function refreshUpdateCache(directory = getDefaultBridgeDataDir()) {
154
+ await withCacheLock(directory, async (path) => {
155
+ const cache = await readCache(path);
156
+ if (isRecent(cache.checkedAt))
157
+ return;
158
+ const checkedAt = Date.now();
159
+ let latestVersion;
160
+ try {
161
+ const response = await fetch(await getRegistryPackageUrl(), {
162
+ headers: { Accept: 'application/vnd.npm.install-v1+json' },
163
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT),
164
+ });
165
+ if (response.ok) {
166
+ const result = RegistrySchema.safeParse(await response.json());
167
+ const version = result.data?.['dist-tags'].latest;
168
+ if (version && numericVersion.test(version))
169
+ latestVersion = version;
170
+ }
171
+ }
172
+ catch {
173
+ // 网络错误与超时都不需要提示。
174
+ }
175
+ await writeCache(path, { ...cache, checkedAt, latestVersion });
176
+ });
177
+ }
178
+ //# sourceMappingURL=update-notifier.js.map
@@ -1,14 +1,14 @@
1
1
  import { BridgeError } from './errors.js';
2
2
  import type { WorkflowStatusEvent } from './protocol/schema.js';
3
- export type WorkflowWatchInput = {
3
+ export type WorkflowStatusWatchInput = {
4
4
  clientId?: string;
5
5
  workflowId: string;
6
6
  timeoutMs?: number;
7
7
  };
8
- export type WorkflowWatchResult = WorkflowStatusEvent & {
9
- timedOut: boolean;
8
+ export type WorkflowStatusWatchResult = WorkflowStatusEvent & {
9
+ isTimedOut: boolean;
10
10
  };
11
- export type WorkflowWatchConnection = {
11
+ export type WorkflowStatusWatchConnection = {
12
12
  clientId: string;
13
13
  workflowId: string;
14
14
  onStatus: (listener: (event: WorkflowStatusEvent) => void) => () => void;
@@ -18,14 +18,15 @@ export type WorkflowWatchConnection = {
18
18
  type WaitForWorkflowStatusOptions = {
19
19
  clientId: string;
20
20
  workflowId: string;
21
+ runAt?: string;
21
22
  subscribe: (onStatus: (event: WorkflowStatusEvent) => void, onClose: (error: BridgeError) => void) => () => void;
22
23
  readStatus: () => Promise<WorkflowStatusEvent>;
23
24
  onStatus?: (event: WorkflowStatusEvent) => void;
24
25
  timeoutMs?: number;
25
26
  signal?: AbortSignal;
26
27
  };
27
- export declare function workflowWatchWebSocketUrl(bridgeUrl: URL): URL;
28
- export declare function connectWorkflowWatch(url: string | URL, input: Omit<WorkflowWatchInput, 'timeoutMs'>, token?: string, signal?: AbortSignal): Promise<WorkflowWatchConnection>;
29
- export declare function waitForWorkflowStatus(options: WaitForWorkflowStatusOptions): Promise<WorkflowWatchResult>;
28
+ export declare function workflowStatusWatchWebSocketUrl(bridgeUrl: URL): import("url").URL;
29
+ export declare function connectWorkflowStatusWatch(url: string | URL, input: Omit<WorkflowStatusWatchInput, 'timeoutMs'>, token?: string, signal?: AbortSignal): Promise<WorkflowStatusWatchConnection>;
30
+ export declare function waitForWorkflowStatus(options: WaitForWorkflowStatusOptions): Promise<WorkflowStatusWatchResult>;
30
31
  export {};
31
32
  //# sourceMappingURL=workflow-watch.d.ts.map