@double-coding/dsh-connectors 0.0.0-stage → 0.1.0-test.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 (125) hide show
  1. package/CONTRIBUTING.md +173 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +156 -0
  4. package/README.md +153 -2
  5. package/assets/readme/architecture.webp +0 -0
  6. package/assets/readme/hero-en.svg +62 -0
  7. package/assets/readme/hero.svg +62 -0
  8. package/assets/readme/panel.webp +0 -0
  9. package/cordis.patch.yml +8 -0
  10. package/dist/client.js +5385 -0
  11. package/dist/client.js.map +7 -0
  12. package/dist/host/catalog.d.ts +7 -0
  13. package/dist/host/catalog.js +15 -0
  14. package/dist/host/catalog.js.map +1 -0
  15. package/dist/host/cli/adapters.d.ts +69 -0
  16. package/dist/host/cli/adapters.js +577 -0
  17. package/dist/host/cli/adapters.js.map +1 -0
  18. package/dist/host/cli/exec.d.ts +34 -0
  19. package/dist/host/cli/exec.js +186 -0
  20. package/dist/host/cli/exec.js.map +1 -0
  21. package/dist/host/cli/service.d.ts +44 -0
  22. package/dist/host/cli/service.js +711 -0
  23. package/dist/host/cli/service.js.map +1 -0
  24. package/dist/host/cli/version.d.ts +1 -0
  25. package/dist/host/cli/version.js +16 -0
  26. package/dist/host/cli/version.js.map +1 -0
  27. package/dist/host/connections.d.ts +106 -0
  28. package/dist/host/connections.js +1207 -0
  29. package/dist/host/connections.js.map +1 -0
  30. package/dist/host/errors.d.ts +4 -0
  31. package/dist/host/errors.js +8 -0
  32. package/dist/host/errors.js.map +1 -0
  33. package/dist/host/instanceUrl.d.ts +1 -0
  34. package/dist/host/instanceUrl.js +27 -0
  35. package/dist/host/instanceUrl.js.map +1 -0
  36. package/dist/host/mcpServers.d.ts +19 -0
  37. package/dist/host/mcpServers.js +243 -0
  38. package/dist/host/mcpServers.js.map +1 -0
  39. package/dist/host/oauth.d.ts +49 -0
  40. package/dist/host/oauth.js +215 -0
  41. package/dist/host/oauth.js.map +1 -0
  42. package/dist/host/openExternal.d.ts +1 -0
  43. package/dist/host/openExternal.js +45 -0
  44. package/dist/host/openExternal.js.map +1 -0
  45. package/dist/host/providers.d.ts +105 -0
  46. package/dist/host/providers.js +199 -0
  47. package/dist/host/providers.js.map +1 -0
  48. package/dist/host/routes.d.ts +18 -0
  49. package/dist/host/routes.js +514 -0
  50. package/dist/host/routes.js.map +1 -0
  51. package/dist/host/sse-bridge.d.ts +1 -0
  52. package/dist/host/sse-bridge.js +168 -0
  53. package/dist/host/sse-bridge.js.map +1 -0
  54. package/dist/host/store.d.ts +184 -0
  55. package/dist/host/store.js +98 -0
  56. package/dist/host/store.js.map +1 -0
  57. package/dist/index.d.ts +5 -0
  58. package/dist/index.js +35 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/shared/categories.d.ts +12 -0
  61. package/dist/shared/categories.js +59 -0
  62. package/dist/shared/categories.js.map +1 -0
  63. package/dist/shared/customConfig.d.ts +1 -0
  64. package/dist/shared/customConfig.js +2 -0
  65. package/dist/shared/customConfig.js.map +1 -0
  66. package/dist/shared/types.d.ts +222 -0
  67. package/dist/shared/types.js +2 -0
  68. package/dist/shared/types.js.map +1 -0
  69. package/package.json +96 -4
  70. package/providers/README.md +44 -0
  71. package/providers/amap/icon.png +0 -0
  72. package/providers/amap/provider.json +34 -0
  73. package/providers/baidu-netdisk/icon.png +0 -0
  74. package/providers/baidu-netdisk/provider.json +36 -0
  75. package/providers/circleci/icon.svg +1 -0
  76. package/providers/circleci/provider.json +29 -0
  77. package/providers/cloudflare-docs/icon.svg +1 -0
  78. package/providers/cloudflare-docs/provider.json +26 -0
  79. package/providers/context7/icon.png +0 -0
  80. package/providers/context7/provider.json +25 -0
  81. package/providers/deepwiki/icon.svg +5 -0
  82. package/providers/deepwiki/provider.json +26 -0
  83. package/providers/firecrawl/icon.png +0 -0
  84. package/providers/firecrawl/provider.json +40 -0
  85. package/providers/gitee/icon.svg +1 -0
  86. package/providers/gitee/provider.json +32 -0
  87. package/providers/github/icon.svg +1 -0
  88. package/providers/github/provider.json +33 -0
  89. package/providers/gitlab/icon.svg +1 -0
  90. package/providers/gitlab/provider.json +31 -0
  91. package/providers/gitlab-self-hosted/icon.svg +1 -0
  92. package/providers/gitlab-self-hosted/provider.json +39 -0
  93. package/providers/grafana/icon.svg +1 -0
  94. package/providers/grafana/provider.json +32 -0
  95. package/providers/linear/icon.svg +1 -0
  96. package/providers/linear/provider.json +41 -0
  97. package/providers/microsoft-learn/icon.svg +6 -0
  98. package/providers/microsoft-learn/provider.json +26 -0
  99. package/providers/neon/icon.svg +1 -0
  100. package/providers/neon/provider.json +41 -0
  101. package/providers/notion/icon.svg +1 -0
  102. package/providers/notion/provider.json +29 -0
  103. package/providers/posthog/icon.svg +1 -0
  104. package/providers/posthog/provider.json +38 -0
  105. package/providers/postman/icon.svg +1 -0
  106. package/providers/postman/provider.json +29 -0
  107. package/providers/qcc/icon.png +0 -0
  108. package/providers/qcc/provider.json +50 -0
  109. package/providers/sentry/icon.svg +1 -0
  110. package/providers/sentry/provider.json +30 -0
  111. package/providers/sourcegraph/icon.png +0 -0
  112. package/providers/sourcegraph/provider.json +32 -0
  113. package/providers/supabase/icon.svg +1 -0
  114. package/providers/supabase/provider.json +52 -0
  115. package/providers/tavily/icon.svg +4 -0
  116. package/providers/tavily/provider.json +41 -0
  117. package/providers/tencent-docs/icon.png +0 -0
  118. package/providers/tencent-docs/provider.json +34 -0
  119. package/providers/tencent-yuanbao-search/icon.png +0 -0
  120. package/providers/tencent-yuanbao-search/provider.json +32 -0
  121. package/providers/tianyancha/icon.png +0 -0
  122. package/providers/tianyancha/provider.json +34 -0
  123. package/providers/yunxiao/icon.svg +1 -0
  124. package/providers/yunxiao/provider.json +30 -0
  125. package/schema/provider.schema.json +325 -0
@@ -0,0 +1,711 @@
1
+ import { existsSync, readdirSync, rmSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { ConnectError } from '../errors.js';
5
+ import { openExternal } from '../openExternal.js';
6
+ import { defineCliDomain } from '../store.js';
7
+ import { getAdapter, listAdapters } from './adapters.js';
8
+ import { compareVersions } from './version.js';
9
+ import { defaultExec, killProcessTree, redactOutput, resolveBinary, spawnStreaming } from './exec.js';
10
+ const VERSION_TIMEOUT_MS = 10_000;
11
+ const STATUS_TIMEOUT_MS = 15_000;
12
+ const CAPABILITY_TIMEOUT_MS = 20_000;
13
+ const CAPABILITY_TTL_MS = 10 * 60 * 1000;
14
+ const INSTALL_TIMEOUT_MS = 600_000;
15
+ const PREPARE_TIMEOUT_MS = 300_000;
16
+ const LOGIN_TIMEOUT_MS = 120_000;
17
+ const MAX_OUTPUT = 128 * 1024;
18
+ function skillDirs() {
19
+ return [join(homedir(), '.agents', 'skills'), join(homedir(), '.dsh', 'skills')];
20
+ }
21
+ function trimOutput(current, chunk) {
22
+ const next = current + chunk;
23
+ return next.length > MAX_OUTPUT ? next.slice(-MAX_OUTPUT) : next;
24
+ }
25
+ function versionLine(raw) {
26
+ const line = raw
27
+ .split(/\r?\n/)
28
+ .map(item => item.trim())
29
+ .find(item => item !== '');
30
+ if (line === undefined)
31
+ return undefined;
32
+ const semver = /(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)/.exec(line)?.[1];
33
+ if (semver !== undefined)
34
+ return semver;
35
+ return line.replace(/^v/i, '').slice(0, 32);
36
+ }
37
+ function installHint(adapter) {
38
+ return adapter.install.map(step => [step.command, ...step.args].join(' ').replace(/\.cmd\b/gi, ''));
39
+ }
40
+ function commandEnv(adapter) {
41
+ if (adapter.unsetEnv === undefined || adapter.unsetEnv.length === 0)
42
+ return undefined;
43
+ const env = { ...process.env };
44
+ for (const key of adapter.unsetEnv)
45
+ delete env[key];
46
+ return env;
47
+ }
48
+ export class CliConnectorService {
49
+ ctx;
50
+ exec;
51
+ domainPromise;
52
+ jobs = new Map();
53
+ cache = new Map();
54
+ capabilityCache = new Map();
55
+ constructor(ctx, exec = defaultExec) {
56
+ this.ctx = ctx;
57
+ this.exec = exec;
58
+ if (ctx.storageDomain === undefined)
59
+ throw new Error('storageDomain service is unavailable');
60
+ this.domainPromise = ctx.storageDomain.open(defineCliDomain());
61
+ }
62
+ async table() {
63
+ const domain = await this.domainPromise;
64
+ return domain.table('cli_connectors');
65
+ }
66
+ async dispose() {
67
+ for (const job of this.jobs.values())
68
+ if (job.child !== undefined)
69
+ killProcessTree(job.child);
70
+ const domain = await this.domainPromise;
71
+ await domain.close();
72
+ }
73
+ async remember(view) {
74
+ const table = await this.table();
75
+ const previous = table.get(view.id);
76
+ const now = Date.now();
77
+ const record = {
78
+ key: view.id,
79
+ connectorId: view.id,
80
+ installed: view.installed,
81
+ enabled: view.enabled,
82
+ ...(view.version === undefined ? {} : { installedVersion: view.version }),
83
+ authenticated: view.authenticated,
84
+ ...(view.account === undefined ? {} : { account: view.account }),
85
+ skillsCount: view.skills.count,
86
+ ...(view.lastError === undefined ? {} : { lastError: view.lastError }),
87
+ lastCheckedAt: now,
88
+ createdAt: previous?.createdAt ?? now,
89
+ updatedAt: now,
90
+ };
91
+ await table.put(view.id, record);
92
+ }
93
+ async capabilitiesFor(adapter, binPath) {
94
+ if (adapter.capabilities === undefined)
95
+ return undefined;
96
+ const cached = this.capabilityCache.get(adapter.id);
97
+ if (cached !== undefined && Date.now() - cached.at < CAPABILITY_TTL_MS)
98
+ return cached.value;
99
+ const env = commandEnv(adapter);
100
+ const result = await this.exec(binPath, adapter.capabilities.args, {
101
+ timeoutMs: CAPABILITY_TIMEOUT_MS,
102
+ acceptFailure: true,
103
+ ...(env === undefined ? {} : { env }),
104
+ });
105
+ let value;
106
+ if (result.code === 0) {
107
+ try {
108
+ const parsed = adapter.capabilities.parse(result.stdout || result.stderr);
109
+ if (parsed.items.length > 0)
110
+ value = parsed;
111
+ }
112
+ catch {
113
+ // 能力目录解析失败不影响连接状态
114
+ }
115
+ }
116
+ this.capabilityCache.set(adapter.id, { at: Date.now(), value: value ?? cached?.value });
117
+ return value ?? cached?.value;
118
+ }
119
+ baseView(adapter, record) {
120
+ const installed = record?.installed === true || record?.installedVersion !== undefined;
121
+ return {
122
+ id: adapter.id,
123
+ name: adapter.name,
124
+ vendor: adapter.vendor,
125
+ description: adapter.description,
126
+ category: adapter.category,
127
+ docsUrl: adapter.docsUrl,
128
+ examples: adapter.examples,
129
+ installHint: installHint(adapter),
130
+ installed,
131
+ enabled: record?.enabled !== false,
132
+ ...(record?.installedVersion === undefined ? {} : { version: record.installedVersion }),
133
+ authenticated: record?.authenticated === true,
134
+ ...(record?.account === undefined ? {} : { account: record.account }),
135
+ skills: { installed: (record?.skillsCount ?? 0) > 0, count: record?.skillsCount ?? 0, names: [] },
136
+ phase: record?.lastError === undefined ? 'idle' : 'error',
137
+ ...(record?.lastError === undefined ? {} : { lastError: record.lastError }),
138
+ ...(record?.updatedAt === undefined ? {} : { updatedAt: record.updatedAt }),
139
+ };
140
+ }
141
+ scanSkills(adapter) {
142
+ const names = [];
143
+ let dir;
144
+ for (const root of skillDirs()) {
145
+ if (!existsSync(root))
146
+ continue;
147
+ let entries;
148
+ try {
149
+ entries = readdirSync(root, { withFileTypes: true });
150
+ }
151
+ catch {
152
+ continue;
153
+ }
154
+ for (const entry of entries) {
155
+ if (!entry.isDirectory())
156
+ continue;
157
+ if (!entry.name.startsWith(adapter.skills.expectedPrefix))
158
+ continue;
159
+ if (!existsSync(join(root, entry.name, 'SKILL.md')))
160
+ continue;
161
+ if (!names.includes(entry.name))
162
+ names.push(entry.name);
163
+ }
164
+ if (dir === undefined && names.length > 0)
165
+ dir = root;
166
+ }
167
+ names.sort((left, right) => left.localeCompare(right, 'zh-CN'));
168
+ return { installed: names.length > 0, count: names.length, names, ...(dir === undefined ? {} : { dir }) };
169
+ }
170
+ removeSkills(adapter) {
171
+ let removed = 0;
172
+ for (const root of skillDirs()) {
173
+ if (!existsSync(root))
174
+ continue;
175
+ let entries;
176
+ try {
177
+ entries = readdirSync(root, { withFileTypes: true });
178
+ }
179
+ catch {
180
+ continue;
181
+ }
182
+ for (const entry of entries) {
183
+ if (!entry.isDirectory())
184
+ continue;
185
+ if (!entry.name.startsWith(adapter.skills.expectedPrefix))
186
+ continue;
187
+ const dir = join(root, entry.name);
188
+ if (!existsSync(join(dir, 'SKILL.md')))
189
+ continue;
190
+ try {
191
+ rmSync(dir, { recursive: true, force: true });
192
+ removed += 1;
193
+ }
194
+ catch {
195
+ // 单个 Skills 删除失败不阻断其余清理
196
+ }
197
+ }
198
+ }
199
+ return removed;
200
+ }
201
+ mergeJob(view) {
202
+ const job = this.jobs.get(view.id);
203
+ if (job === undefined)
204
+ return view;
205
+ const output = job.output === '' ? {} : { output: redactOutput(job.output.slice(-8000)) };
206
+ if (job.done) {
207
+ const staleLoginError = job.kind === 'login' && view.authenticated && job.error !== undefined;
208
+ if (job.error !== undefined && !staleLoginError) {
209
+ return { ...view, ...output, lastError: job.error, phase: 'error' };
210
+ }
211
+ if (!job.settled && !job.cancel) {
212
+ return { ...view, ...output, phase: job.phase, stage: job.stage };
213
+ }
214
+ return { ...view, ...output };
215
+ }
216
+ return {
217
+ ...view,
218
+ phase: job.phase,
219
+ stage: job.stage,
220
+ ...output,
221
+ ...(job.link === undefined ? {} : { link: job.link }),
222
+ ...(job.code === undefined ? {} : { code: job.code }),
223
+ ...(job.error === undefined ? {} : { lastError: job.error }),
224
+ };
225
+ }
226
+ async list() {
227
+ const table = await this.table();
228
+ return listAdapters().map(adapter => {
229
+ const cached = this.cache.get(adapter.id) ?? this.baseView(adapter, table.get(adapter.id));
230
+ return this.mergeJob(cached);
231
+ });
232
+ }
233
+ async view(id) {
234
+ const adapter = getAdapter(id);
235
+ if (adapter === undefined)
236
+ throw new ConnectError('not_found', '未知的 CLI 连接器');
237
+ const table = await this.table();
238
+ return this.mergeJob(this.cache.get(adapter.id) ?? this.baseView(adapter, table.get(adapter.id)));
239
+ }
240
+ async setEnabled(id, enabled) {
241
+ const adapter = getAdapter(id);
242
+ if (adapter === undefined)
243
+ throw new ConnectError('not_found', '未知的 CLI 连接器');
244
+ this.assertIdle(adapter.id);
245
+ const current = this.cache.get(adapter.id) ?? await this.refresh(adapter.id);
246
+ const view = { ...current, enabled, updatedAt: Date.now() };
247
+ this.cache.set(adapter.id, view);
248
+ await this.remember(view);
249
+ return view;
250
+ }
251
+ async refresh(id) {
252
+ const adapter = getAdapter(id);
253
+ if (adapter === undefined)
254
+ throw new ConnectError('not_found', '未知的 CLI 连接器');
255
+ const running = this.jobs.get(adapter.id);
256
+ if (running !== undefined && !running.done)
257
+ return this.view(adapter.id);
258
+ const table = await this.table();
259
+ const previous = table.get(adapter.id);
260
+ const skills = this.scanSkills(adapter);
261
+ const binPath = await resolveBinary(adapter.bin, this.exec, adapter.binCandidates ?? []);
262
+ if (binPath === null) {
263
+ const view = {
264
+ id: adapter.id,
265
+ name: adapter.name,
266
+ vendor: adapter.vendor,
267
+ description: adapter.description,
268
+ category: adapter.category,
269
+ docsUrl: adapter.docsUrl,
270
+ examples: adapter.examples,
271
+ installHint: installHint(adapter),
272
+ installed: false,
273
+ enabled: previous?.enabled !== false,
274
+ authenticated: false,
275
+ skills,
276
+ phase: 'idle',
277
+ updatedAt: Date.now(),
278
+ };
279
+ this.cache.set(adapter.id, view);
280
+ await this.remember(view);
281
+ return view;
282
+ }
283
+ const env = commandEnv(adapter);
284
+ const [versionResult, statusResult, capabilities] = await Promise.all([
285
+ this.exec(binPath, adapter.versionArgs, {
286
+ timeoutMs: VERSION_TIMEOUT_MS,
287
+ acceptFailure: true,
288
+ ...(env === undefined ? {} : { env }),
289
+ }),
290
+ this.exec(binPath, adapter.auth.status.args, {
291
+ timeoutMs: STATUS_TIMEOUT_MS,
292
+ acceptFailure: true,
293
+ ...(env === undefined ? {} : { env }),
294
+ }),
295
+ this.capabilitiesFor(adapter, binPath),
296
+ ]);
297
+ const rawStatus = `${statusResult.stdout}\n${statusResult.stderr}`;
298
+ const parsed = adapter.parseStatus(rawStatus);
299
+ const authenticated = statusResult.timedOut ? previous?.authenticated === true : parsed.authenticated;
300
+ if (authenticated) {
301
+ const job = this.jobs.get(adapter.id);
302
+ if (job !== undefined && job.kind === 'login') {
303
+ job.error = undefined;
304
+ if (job.phase === 'error')
305
+ job.phase = 'idle';
306
+ }
307
+ }
308
+ const versionOk = versionResult.code === 0 && !versionResult.timedOut;
309
+ const version = versionLine(`${versionResult.stdout}\n${versionResult.stderr}`)
310
+ ?? (versionOk ? versionLine(previous?.installedVersion ?? '') : undefined);
311
+ const error = statusResult.timedOut
312
+ ? '获取登录状态超时'
313
+ : !versionOk
314
+ ? 'CLI 无法执行,请重新安装或查看官方文档'
315
+ : undefined;
316
+ const view = {
317
+ id: adapter.id,
318
+ name: adapter.name,
319
+ vendor: adapter.vendor,
320
+ description: adapter.description,
321
+ category: adapter.category,
322
+ docsUrl: adapter.docsUrl,
323
+ examples: adapter.examples,
324
+ installHint: installHint(adapter),
325
+ installed: true,
326
+ enabled: previous?.enabled !== false,
327
+ ...(version === undefined ? {} : { version }),
328
+ authenticated,
329
+ ...(parsed.account === undefined ? {} : { account: parsed.account }),
330
+ skills,
331
+ ...(capabilities === undefined ? {} : { capabilities }),
332
+ ...(adapter.minVersion === undefined ? {} : { minVersion: adapter.minVersion }),
333
+ ...(adapter.minVersion !== undefined && version !== undefined && compareVersions(version, adapter.minVersion) < 0
334
+ ? { versionOutdated: true }
335
+ : {}),
336
+ phase: error === undefined ? 'idle' : 'error',
337
+ ...(error === undefined ? {} : { lastError: error }),
338
+ updatedAt: Date.now(),
339
+ };
340
+ this.cache.set(adapter.id, view);
341
+ await this.remember(view);
342
+ return view;
343
+ }
344
+ async detectAll() {
345
+ await Promise.all(listAdapters().map(adapter => this.refresh(adapter.id).catch(() => undefined)));
346
+ }
347
+ async startInstall(id) {
348
+ const adapter = getAdapter(id);
349
+ if (adapter === undefined)
350
+ throw new ConnectError('not_found', '未知的 CLI 连接器');
351
+ this.assertIdle(adapter.id);
352
+ const job = {
353
+ connectorId: adapter.id,
354
+ kind: 'install',
355
+ phase: 'installing',
356
+ stage: adapter.install[0]?.label ?? '安装 CLI',
357
+ output: '',
358
+ cancel: false,
359
+ done: false,
360
+ settled: false,
361
+ };
362
+ this.jobs.set(adapter.id, job);
363
+ void (async () => {
364
+ const env = commandEnv(adapter);
365
+ try {
366
+ for (const step of adapter.install) {
367
+ if (job.cancel)
368
+ return;
369
+ job.stage = step.label;
370
+ const handle = spawnStreaming(step.command, step.args, {
371
+ timeoutMs: INSTALL_TIMEOUT_MS,
372
+ onOutput: chunk => {
373
+ job.output = trimOutput(job.output, chunk.text);
374
+ },
375
+ ...(env === undefined ? {} : { env }),
376
+ });
377
+ job.child = handle.child;
378
+ const result = await handle.done;
379
+ if (job.cancel)
380
+ return;
381
+ if (result.code !== 0) {
382
+ throw new Error(`${step.label}失败:${redactOutput(result.stderr || result.stdout).trim().slice(0, 300) || `退出码 ${String(result.code)}`}`);
383
+ }
384
+ }
385
+ }
386
+ catch (error) {
387
+ job.error = error instanceof Error ? error.message : String(error);
388
+ job.phase = 'error';
389
+ }
390
+ finally {
391
+ job.done = true;
392
+ }
393
+ if (job.cancel)
394
+ return;
395
+ this.capabilityCache.delete(adapter.id);
396
+ const settledView = await this.refresh(adapter.id).catch(() => undefined);
397
+ // CLI 与 Skills 都就绪时,失败只发生在中途某一步,不保留红色错误
398
+ if (job.error !== undefined && settledView !== undefined && settledView.installed && settledView.skills.installed) {
399
+ job.error = undefined;
400
+ job.phase = 'idle';
401
+ }
402
+ job.settled = true;
403
+ })();
404
+ return this.view(adapter.id);
405
+ }
406
+ async startUpdate(id) {
407
+ const adapter = getAdapter(id);
408
+ if (adapter === undefined)
409
+ throw new ConnectError('not_found', '未知的 CLI 连接器');
410
+ this.assertIdle(adapter.id);
411
+ const current = await this.view(adapter.id);
412
+ if (!current.installed)
413
+ throw new ConnectError('cli_not_installed', '请先安装 CLI');
414
+ const job = {
415
+ connectorId: adapter.id,
416
+ kind: 'update',
417
+ phase: 'updating',
418
+ stage: '更新 CLI',
419
+ output: '',
420
+ cancel: false,
421
+ done: false,
422
+ settled: false,
423
+ };
424
+ this.jobs.set(adapter.id, job);
425
+ void (async () => {
426
+ const env = commandEnv(adapter);
427
+ try {
428
+ for (const step of adapter.install) {
429
+ if (job.cancel)
430
+ return;
431
+ job.stage = step.label;
432
+ const handle = spawnStreaming(step.command, step.args, {
433
+ timeoutMs: INSTALL_TIMEOUT_MS,
434
+ onOutput: chunk => {
435
+ job.output = trimOutput(job.output, chunk.text);
436
+ },
437
+ ...(env === undefined ? {} : { env }),
438
+ });
439
+ job.child = handle.child;
440
+ const result = await handle.done;
441
+ if (job.cancel)
442
+ return;
443
+ if (result.code !== 0) {
444
+ throw new Error(`${step.label}失败:${redactOutput(result.stderr || result.stdout).trim().slice(0, 300) || `退出码 ${String(result.code)}`}`);
445
+ }
446
+ }
447
+ }
448
+ catch (error) {
449
+ job.error = error instanceof Error ? error.message : String(error);
450
+ job.phase = 'error';
451
+ }
452
+ finally {
453
+ job.done = true;
454
+ }
455
+ if (job.cancel)
456
+ return;
457
+ this.capabilityCache.delete(adapter.id);
458
+ const settledView = await this.refresh(adapter.id).catch(() => undefined);
459
+ if (job.error !== undefined && settledView !== undefined && settledView.installed && settledView.skills.installed) {
460
+ job.error = undefined;
461
+ job.phase = 'idle';
462
+ }
463
+ job.settled = true;
464
+ })();
465
+ return this.view(adapter.id);
466
+ }
467
+ async startLogin(id) {
468
+ const adapter = getAdapter(id);
469
+ if (adapter === undefined)
470
+ throw new ConnectError('not_found', '未知的 CLI 连接器');
471
+ this.assertIdle(adapter.id);
472
+ const current = await this.view(adapter.id);
473
+ if (!current.installed)
474
+ throw new ConnectError('cli_not_installed', '请先安装 CLI');
475
+ const binPath = await resolveBinary(adapter.bin, this.exec, adapter.binCandidates ?? []);
476
+ if (binPath === null)
477
+ throw new ConnectError('cli_not_installed', 'CLI 可执行文件未找到,请重新安装');
478
+ const job = {
479
+ connectorId: adapter.id,
480
+ kind: 'login',
481
+ phase: adapter.auth.prepare === undefined ? 'logging_in' : 'configuring',
482
+ stage: adapter.auth.prepare === undefined ? '登录' : '配置应用',
483
+ output: '',
484
+ cancel: false,
485
+ done: false,
486
+ settled: false,
487
+ };
488
+ this.jobs.set(adapter.id, job);
489
+ void (async () => {
490
+ try {
491
+ if (adapter.auth.prepare !== undefined) {
492
+ const prepared = await this.runLoginCommand(job, adapter, binPath, adapter.auth.prepare.args, PREPARE_TIMEOUT_MS);
493
+ if (job.cancel)
494
+ return;
495
+ if (prepared.code !== 0)
496
+ throw new Error(`配置失败:${this.tail(prepared.stderr || prepared.stdout)}`);
497
+ job.link = undefined;
498
+ job.code = undefined;
499
+ }
500
+ job.phase = 'logging_in';
501
+ job.stage = '登录';
502
+ const logged = await this.runLoginCommand(job, adapter, binPath, adapter.auth.login.args, adapter.loginTimeoutMs ?? LOGIN_TIMEOUT_MS);
503
+ if (job.cancel)
504
+ return;
505
+ const view = await this.refresh(adapter.id).catch(() => undefined);
506
+ if (view?.authenticated === true) {
507
+ job.link = undefined;
508
+ job.code = undefined;
509
+ return;
510
+ }
511
+ if (logged.code !== 0)
512
+ throw new Error(`登录失败:${this.tail(logged.stderr || logged.stdout)}`);
513
+ throw new Error('登录未完成:请确认已在浏览器完成授权后重试');
514
+ }
515
+ catch (error) {
516
+ job.error = error instanceof Error ? error.message : String(error);
517
+ job.phase = 'error';
518
+ }
519
+ finally {
520
+ job.done = true;
521
+ }
522
+ if (!job.cancel)
523
+ await this.refresh(adapter.id).catch(() => undefined);
524
+ job.settled = true;
525
+ })();
526
+ return this.view(adapter.id);
527
+ }
528
+ async runLoginCommand(job, adapter, binPath, args, timeoutMs) {
529
+ const env = commandEnv(adapter);
530
+ const capture = (text) => {
531
+ const hint = adapter.parseLoginHint(text);
532
+ if (job.link === undefined && hint.link !== undefined) {
533
+ job.link = hint.link;
534
+ void openExternal(job.link);
535
+ }
536
+ if (job.code === undefined && hint.code !== undefined)
537
+ job.code = hint.code;
538
+ };
539
+ const handle = spawnStreaming(binPath, args, {
540
+ timeoutMs,
541
+ onOutput: chunk => {
542
+ job.output = trimOutput(job.output, chunk.text);
543
+ capture(chunk.text);
544
+ },
545
+ ...(env === undefined ? {} : { env }),
546
+ });
547
+ job.child = handle.child;
548
+ const result = await handle.done;
549
+ if (result.timedOut)
550
+ throw new Error('登录超时:请重新发起并在浏览器完成授权');
551
+ capture(`${result.stdout}\n${result.stderr}`);
552
+ return result;
553
+ }
554
+ tail(raw) {
555
+ return redactOutput(raw).trim().slice(-300) || 'CLI 未返回更多信息';
556
+ }
557
+ async logout(id) {
558
+ const adapter = getAdapter(id);
559
+ if (adapter === undefined)
560
+ throw new ConnectError('not_found', '未知的 CLI 连接器');
561
+ this.assertIdle(adapter.id);
562
+ const logout = adapter.auth.logout;
563
+ if (logout !== undefined) {
564
+ const binPath = await resolveBinary(adapter.bin, this.exec, adapter.binCandidates ?? []);
565
+ if (binPath === null)
566
+ throw new ConnectError('cli_not_installed', 'CLI 可执行文件未找到');
567
+ const env = commandEnv(adapter);
568
+ const result = await this.exec(binPath, logout.args, {
569
+ timeoutMs: STATUS_TIMEOUT_MS,
570
+ acceptFailure: true,
571
+ ...(env === undefined ? {} : { env }),
572
+ });
573
+ if (result.code !== 0)
574
+ throw new ConnectError('cli_logout_failed', this.tail(result.stderr || result.stdout));
575
+ }
576
+ else if (adapter.logoutFiles !== undefined) {
577
+ this.clearCredentials(adapter);
578
+ }
579
+ else {
580
+ throw new ConnectError('cli_logout_unsupported', '该 CLI 不支持解绑');
581
+ }
582
+ this.jobs.delete(adapter.id);
583
+ return this.refresh(adapter.id);
584
+ }
585
+ clearCredentials(adapter) {
586
+ const spec = adapter.logoutFiles;
587
+ if (spec === undefined)
588
+ return;
589
+ const override = spec.envDir === undefined ? undefined : process.env[spec.envDir];
590
+ const dir = override !== undefined && override !== '' ? override : spec.dir.replace(/^~/, homedir());
591
+ for (const file of spec.files)
592
+ rmSync(join(dir, file), { force: true });
593
+ }
594
+ async startUninstall(id) {
595
+ const adapter = getAdapter(id);
596
+ if (adapter === undefined)
597
+ throw new ConnectError('not_found', '未知的 CLI 连接器');
598
+ this.assertIdle(adapter.id);
599
+ const job = {
600
+ connectorId: adapter.id,
601
+ kind: 'uninstall',
602
+ phase: 'uninstalling',
603
+ stage: '准备卸载',
604
+ output: '',
605
+ cancel: false,
606
+ done: false,
607
+ settled: false,
608
+ };
609
+ this.jobs.set(adapter.id, job);
610
+ void (async () => {
611
+ try {
612
+ const binPath = await resolveBinary(adapter.bin, this.exec, adapter.binCandidates ?? []);
613
+ if (binPath !== null) {
614
+ job.stage = '退出登录';
615
+ const env = commandEnv(adapter);
616
+ const logout = adapter.auth.logout;
617
+ if (logout !== undefined) {
618
+ await this.exec(binPath, logout.args, {
619
+ timeoutMs: STATUS_TIMEOUT_MS,
620
+ acceptFailure: true,
621
+ ...(env === undefined ? {} : { env }),
622
+ }).catch(() => undefined);
623
+ }
624
+ else {
625
+ this.clearCredentials(adapter);
626
+ }
627
+ if (job.cancel)
628
+ return;
629
+ for (const step of adapter.uninstall) {
630
+ if (job.cancel)
631
+ return;
632
+ job.stage = step.label;
633
+ const handle = spawnStreaming(step.command, step.args, {
634
+ timeoutMs: INSTALL_TIMEOUT_MS,
635
+ onOutput: chunk => {
636
+ job.output = trimOutput(job.output, chunk.text);
637
+ },
638
+ ...(env === undefined ? {} : { env }),
639
+ });
640
+ job.child = handle.child;
641
+ const result = await handle.done;
642
+ if (job.cancel)
643
+ return;
644
+ if (result.code !== 0) {
645
+ throw new Error(`${step.label}失败:${this.tail(result.stderr || result.stdout)}`);
646
+ }
647
+ }
648
+ }
649
+ }
650
+ catch (error) {
651
+ job.error = error instanceof Error ? error.message : String(error);
652
+ job.phase = 'error';
653
+ }
654
+ finally {
655
+ job.done = true;
656
+ }
657
+ if (job.cancel) {
658
+ await this.refresh(adapter.id).catch(() => undefined);
659
+ job.settled = true;
660
+ return;
661
+ }
662
+ if (job.error !== undefined) {
663
+ job.settled = true;
664
+ return;
665
+ }
666
+ try {
667
+ job.stage = '删除 Skills';
668
+ this.removeSkills(adapter);
669
+ this.clearCredentials(adapter);
670
+ this.capabilityCache.delete(adapter.id);
671
+ this.cache.delete(adapter.id);
672
+ const table = await this.table();
673
+ await table.delete(adapter.id);
674
+ job.stage = '完成';
675
+ job.settled = true;
676
+ }
677
+ catch (error) {
678
+ job.error = error instanceof Error ? error.message : String(error);
679
+ job.phase = 'error';
680
+ job.settled = true;
681
+ }
682
+ })();
683
+ return this.view(adapter.id);
684
+ }
685
+ async cancel(id) {
686
+ const adapter = getAdapter(id);
687
+ if (adapter === undefined)
688
+ throw new ConnectError('not_found', '未知的 CLI 连接器');
689
+ const job = this.jobs.get(adapter.id);
690
+ if (job === undefined || job.done)
691
+ return this.view(adapter.id);
692
+ job.cancel = true;
693
+ job.done = true;
694
+ job.settled = true;
695
+ if (job.child !== undefined)
696
+ killProcessTree(job.child);
697
+ if (job.kind === 'uninstall')
698
+ return this.refresh(adapter.id).catch(() => this.view(adapter.id));
699
+ return this.view(adapter.id);
700
+ }
701
+ log(id) {
702
+ const job = this.jobs.get(id);
703
+ return job === undefined ? '' : redactOutput(job.output.slice(-8000));
704
+ }
705
+ assertIdle(id) {
706
+ const job = this.jobs.get(id);
707
+ if (job !== undefined && !job.done)
708
+ throw new ConnectError('cli_busy', '当前连接器正在安装、更新、登录或卸载,请稍候');
709
+ }
710
+ }
711
+ //# sourceMappingURL=service.js.map