@axhub/genie 0.2.7 → 0.2.9

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 (131) hide show
  1. package/LICENSE +21 -675
  2. package/dist/api-docs.html +2 -2
  3. package/dist/assets/App-GBcTeeUS.js +460 -0
  4. package/dist/assets/App-qxJ8_QYu.css +32 -0
  5. package/dist/assets/ReviewApp-C9K--AQE.js +1 -0
  6. package/dist/assets/{_basePickBy-C19AekOu.js → _basePickBy-DR_8uFCo.js} +1 -1
  7. package/dist/assets/{_baseUniq-JsnevLw_.js → _baseUniq-D0njlQ_7.js} +1 -1
  8. package/dist/assets/{arc-BLpcuBlf.js → arc-CKlr_Rec.js} +1 -1
  9. package/dist/assets/architectureDiagram-2XIMDMQ5-BmO_uLUH.js +36 -0
  10. package/dist/assets/{blockDiagram-WCTKOSBZ-DQBLwsUS.js → blockDiagram-WCTKOSBZ-DhAeO-56.js} +3 -3
  11. package/dist/assets/c4Diagram-IC4MRINW-C67kFoXx.js +10 -0
  12. package/dist/assets/channel-V3MBjKys.js +1 -0
  13. package/dist/assets/{chunk-4BX2VUAB-De63kbgc.js → chunk-4BX2VUAB-mLLagvJi.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-DtTDDdM9.js → chunk-55IACEB6-Lx-hOjlM.js} +1 -1
  15. package/dist/assets/{chunk-FMBD7UC4-DHuwd8tw.js → chunk-FMBD7UC4-Bt-XmVUV.js} +1 -1
  16. package/dist/assets/{chunk-JSJVCQXG-BgytFtmO.js → chunk-JSJVCQXG-Cya6gaDV.js} +1 -1
  17. package/dist/assets/{chunk-KX2RTZJC-nZdp86aN.js → chunk-KX2RTZJC-Bd7Ig6tF.js} +1 -1
  18. package/dist/assets/chunk-NQ4KR5QH-5UAE0Vg-.js +220 -0
  19. package/dist/assets/{chunk-QZHKN3VN-DvUQ3mnO.js → chunk-QZHKN3VN-BAxZ8m7w.js} +1 -1
  20. package/dist/assets/chunk-WL4C6EOR-DjDPvUUP.js +189 -0
  21. package/dist/assets/classDiagram-VBA2DB6C-C790yYiY.js +1 -0
  22. package/dist/assets/classDiagram-v2-RAHNMMFH-C790yYiY.js +1 -0
  23. package/dist/assets/clone-BbMGfZwt.js +1 -0
  24. package/dist/assets/cose-bilkent-S5V4N54A-D-60XrkJ.js +1 -0
  25. package/dist/assets/cytoscape.esm-2ZfV8NB5.js +331 -0
  26. package/dist/assets/{dagre-KLK3FWXG-CHYIvW47.js → dagre-KLK3FWXG-bqu3ZS4K.js} +1 -1
  27. package/dist/assets/diagram-E7M64L7V-BueeqoYm.js +24 -0
  28. package/dist/assets/{diagram-IFDJBPK2-Dzsiln_C.js → diagram-IFDJBPK2-D4fDv2E7.js} +1 -1
  29. package/dist/assets/{diagram-P4PSJMXO-DKnGbUpE.js → diagram-P4PSJMXO-WqipY3fN.js} +1 -1
  30. package/dist/assets/erDiagram-INFDFZHY-D0oVnO-x.js +70 -0
  31. package/dist/assets/{flowDiagram-PKNHOUZH-BAZ2-jKp.js → flowDiagram-PKNHOUZH-DzbGyxrr.js} +4 -4
  32. package/dist/assets/ganttDiagram-A5KZAMGK-BwhbbgCP.js +292 -0
  33. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-BflpyjGy.js → gitGraphDiagram-K3NZZRJ6-DZgAh_KM.js} +1 -1
  34. package/dist/assets/{graph-suelaXFh.js → graph-DzKos-N0.js} +1 -1
  35. package/dist/assets/highlighted-body-TPN3WLV5-CKDMgz3X.js +1 -0
  36. package/dist/assets/index-DiQlHzGj.js +2 -0
  37. package/dist/assets/index-Drat2nB9.css +1 -0
  38. package/dist/assets/{infoDiagram-LFFYTUFH-pfD1FA3p.js → infoDiagram-LFFYTUFH-BFicZbTf.js} +1 -1
  39. package/dist/assets/ishikawaDiagram-PHBUUO56-CtihxDxl.js +70 -0
  40. package/dist/assets/journeyDiagram-4ABVD52K-Du00J8_d.js +139 -0
  41. package/dist/assets/{kanban-definition-K7BYSVSG-FWinmur1.js → kanban-definition-K7BYSVSG-BJi9S0iQ.js} +5 -5
  42. package/dist/assets/{layout-vcz43XvZ.js → layout-B80Sityu.js} +1 -1
  43. package/dist/assets/{linear-le4gc0vx.js → linear-sRQLOf5H.js} +1 -1
  44. package/dist/assets/mermaid-O7DHMXV3-CBuVs4eJ.js +1038 -0
  45. package/dist/assets/mindmap-definition-YRQLILUH-C5IL_xi-.js +68 -0
  46. package/dist/assets/{pieDiagram-SKSYHLDU-C7PKDh3b.js → pieDiagram-SKSYHLDU-CeTwlJ8z.js} +2 -2
  47. package/dist/assets/quadrantDiagram-337W2JSQ-COfUcLWt.js +7 -0
  48. package/dist/assets/requirementDiagram-Z7DCOOCP-DSb-CJ5B.js +73 -0
  49. package/dist/assets/{sankeyDiagram-WA2Y5GQK-4gulcOP4.js → sankeyDiagram-WA2Y5GQK-8jtuVb45.js} +3 -3
  50. package/dist/assets/sequenceDiagram-2WXFIKYE-C2VpkMwA.js +145 -0
  51. package/dist/assets/{stateDiagram-RAJIS63D-CB4Vl7qM.js → stateDiagram-RAJIS63D-fmwMqxxc.js} +1 -1
  52. package/dist/assets/stateDiagram-v2-FVOUBMTO-9GGXVWrR.js +1 -0
  53. package/dist/assets/timeline-definition-YZTLITO2-Dx1hP5lg.js +61 -0
  54. package/dist/assets/{treemap-KZPCXAKY-DZSEE6Hz.js → treemap-KZPCXAKY-CkLOdYCZ.js} +58 -58
  55. package/dist/assets/vendor-codemirror-BxPY6emf.js +39 -0
  56. package/dist/assets/vendor-react-xmA_f8ig.js +59 -0
  57. package/dist/assets/vendor-xterm-DfaPXD3y.js +66 -0
  58. package/dist/assets/{vennDiagram-LZ73GAT5-8E_G06fI.js → vennDiagram-LZ73GAT5-D6KWcnln.js} +4 -4
  59. package/dist/assets/xychartDiagram-JWTSCODW-6fh6qmzN.js +7 -0
  60. package/dist/index.html +5 -5
  61. package/package.json +36 -35
  62. package/server/acp-runtime/client.js +91 -17
  63. package/server/acp-runtime/index.js +5 -16
  64. package/server/acp-runtime/session-store.js +4 -4
  65. package/server/channels/runtime/AgentRuntimeAdapter.js +1 -10
  66. package/server/claude-sdk.js +1 -3
  67. package/server/cli.js +159 -2
  68. package/server/external-agent/service.js +24 -6
  69. package/server/external-agent/ws.js +63 -3
  70. package/server/gemini-cli.js +1 -3
  71. package/server/index.js +120 -19
  72. package/server/openai-codex.js +1 -3
  73. package/server/opencode-cli.js +1 -3
  74. package/server/projects.js +654 -236
  75. package/server/routes/cc-connect.js +1131 -0
  76. package/server/routes/cli-auth.js +1 -73
  77. package/server/routes/commands.js +4 -9
  78. package/server/routes/projects.js +45 -24
  79. package/server/routes/session-core.js +149 -86
  80. package/server/session-core/eventStore.js +45 -18
  81. package/server/session-core/providerAdapters.js +50 -13
  82. package/server/session-core/providerDiscovery.js +8 -3
  83. package/server/session-core/runtimeState.js +8 -0
  84. package/server/utils/ccConnectManager.js +390 -0
  85. package/server/utils/ccConnectState.js +575 -0
  86. package/server/utils/resolveCommandPath.js +71 -0
  87. package/server/utils/workspaceRoots.js +154 -0
  88. package/shared/conversationEvents.js +78 -14
  89. package/dist/assets/App-BWSqiXAT.js +0 -220
  90. package/dist/assets/App-DrlLKa8f.css +0 -1
  91. package/dist/assets/ReviewApp-nz3mbArg.js +0 -1
  92. package/dist/assets/architectureDiagram-2XIMDMQ5-CarjBOOv.js +0 -36
  93. package/dist/assets/c4Diagram-IC4MRINW-CGobwBIj.js +0 -10
  94. package/dist/assets/channel-DkFNxV_H.js +0 -1
  95. package/dist/assets/chunk-NQ4KR5QH-CMH6EDP2.js +0 -220
  96. package/dist/assets/chunk-WL4C6EOR-Dn7db_6t.js +0 -189
  97. package/dist/assets/classDiagram-VBA2DB6C-DtwCEe8S.js +0 -1
  98. package/dist/assets/classDiagram-v2-RAHNMMFH-DtwCEe8S.js +0 -1
  99. package/dist/assets/clone-C0lCEIEO.js +0 -1
  100. package/dist/assets/cose-bilkent-S5V4N54A-DD_nzqsz.js +0 -1
  101. package/dist/assets/cytoscape.esm-5J0xJHOV.js +0 -321
  102. package/dist/assets/diagram-E7M64L7V-TVdvHtGc.js +0 -24
  103. package/dist/assets/erDiagram-INFDFZHY-5Kw0bByo.js +0 -70
  104. package/dist/assets/ganttDiagram-A5KZAMGK-CsADFkcq.js +0 -292
  105. package/dist/assets/highlighted-body-OFNGDK62-CZrBMazC.js +0 -1
  106. package/dist/assets/index-B01NxbUv.css +0 -1
  107. package/dist/assets/index-DW5pGgQ_.js +0 -2
  108. package/dist/assets/ishikawaDiagram-PHBUUO56-ndm9snwO.js +0 -70
  109. package/dist/assets/journeyDiagram-4ABVD52K-HgF2t7z5.js +0 -139
  110. package/dist/assets/mermaid-GHXKKRXX-CK8m3lad.js +0 -870
  111. package/dist/assets/mindmap-definition-YRQLILUH-CNq9SKj4.js +0 -68
  112. package/dist/assets/quadrantDiagram-337W2JSQ-B7FnztNO.js +0 -7
  113. package/dist/assets/requirementDiagram-Z7DCOOCP-Bl_BM2Th.js +0 -73
  114. package/dist/assets/sequenceDiagram-2WXFIKYE-VEuJDwyJ.js +0 -145
  115. package/dist/assets/stateDiagram-v2-FVOUBMTO-C85ucl39.js +0 -1
  116. package/dist/assets/timeline-definition-YZTLITO2-BPGKhi7f.js +0 -61
  117. package/dist/assets/vendor-codemirror-CyOKkaQZ.js +0 -31
  118. package/dist/assets/vendor-react-CP4yFTs7.js +0 -8
  119. package/dist/assets/vendor-xterm-DfcmCpbH.js +0 -66
  120. package/dist/assets/xychartDiagram-JWTSCODW-CbBk50-O.js +0 -7
  121. package/server/_legacy-providers/README.md +0 -30
  122. package/server/_legacy-providers/claude-sdk.js +0 -956
  123. package/server/_legacy-providers/gemini-cli.js +0 -368
  124. package/server/_legacy-providers/openai-codex.js +0 -705
  125. package/server/_legacy-providers/opencode-cli.js +0 -674
  126. package/server/acp-runtime/client.test.js +0 -688
  127. package/server/acp-runtime/session-store.test.js +0 -89
  128. package/server/cli.test.js +0 -76
  129. package/server/external-agent/service.test.js +0 -53
  130. package/server/external-agent/ws.test.js +0 -289
  131. package/shared/conversationEvents.test.js +0 -403
@@ -0,0 +1,1131 @@
1
+ import express from 'express';
2
+ import fs from 'fs';
3
+ import os from 'os';
4
+ import path from 'path';
5
+ import { spawnSync } from 'child_process';
6
+ import crossSpawn from 'cross-spawn';
7
+ import TOML from '@iarna/toml';
8
+ import { detectProviderInstallationStatus } from './cli-auth.js';
9
+ import { resolveCommandPath } from '../utils/resolveCommandPath.js';
10
+ import { spawnCommand } from '../utils/spawnCommand.js';
11
+ import {
12
+ CC_CONNECT_CONFIG_PATH,
13
+ CC_CONNECT_PLATFORMS,
14
+ CC_CONNECT_PROVIDERS,
15
+ createSetupConfigContent,
16
+ extractPlatformOptionsFromConfigContent,
17
+ getCcConnectManagedProjectName,
18
+ getPlatformConnection,
19
+ listCcConnectPlatformSummaries,
20
+ readCcConnectState,
21
+ removePlatformConnection,
22
+ upsertPlatformConnection,
23
+ writeCcConnectConfig,
24
+ writeCcConnectState
25
+ } from '../utils/ccConnectState.js';
26
+
27
+ const router = express.Router();
28
+
29
+ const CC_CONNECT_COMMAND = 'cc-connect';
30
+ const COMMAND_TIMEOUT_MS = 5000;
31
+ const INSTALL_TIMEOUT_MS = 120000;
32
+ const DAEMON_COMMAND_TIMEOUT_MS = 20000;
33
+ const LOG_PREFIX = '[cc-connect]';
34
+ const SESSION_TERMINAL_GRACE_MS = 60000;
35
+ const setupSessions = new Map();
36
+
37
+ const PROVIDER_ORDER = CC_CONNECT_PROVIDERS.map((provider) => provider.id);
38
+ const PLATFORM_MAP = Object.fromEntries(CC_CONNECT_PLATFORMS.map((platform) => [platform.id, platform]));
39
+
40
+ function nowIso() {
41
+ return new Date().toISOString();
42
+ }
43
+
44
+ function mergeAvailableProviders(configuredProviders = [], installedProviderIds = [], activeProvider = null) {
45
+ const available = new Set();
46
+
47
+ for (const providerId of configuredProviders) {
48
+ if (PROVIDER_ORDER.includes(providerId)) {
49
+ available.add(providerId);
50
+ }
51
+ }
52
+
53
+ for (const providerId of installedProviderIds) {
54
+ if (PROVIDER_ORDER.includes(providerId)) {
55
+ available.add(providerId);
56
+ }
57
+ }
58
+
59
+ if (activeProvider && PROVIDER_ORDER.includes(activeProvider)) {
60
+ available.add(activeProvider);
61
+ }
62
+
63
+ return PROVIDER_ORDER.filter((providerId) => available.has(providerId));
64
+ }
65
+
66
+ function withAvailableProviders(connection, installedProviderIds) {
67
+ if (!connection) {
68
+ return null;
69
+ }
70
+
71
+ const configuredProviders = mergeAvailableProviders(
72
+ connection.configuredProviders,
73
+ installedProviderIds,
74
+ connection.activeProvider
75
+ );
76
+
77
+ return {
78
+ ...connection,
79
+ configuredProviders
80
+ };
81
+ }
82
+
83
+ function buildPlatformSummaries(state, installedProviderIds) {
84
+ return listCcConnectPlatformSummaries(state).map((platform) => {
85
+ if (!platform.connected) {
86
+ return platform;
87
+ }
88
+
89
+ const connection = getPlatformConnection(state, platform.id);
90
+ const mergedProviders = mergeAvailableProviders(
91
+ connection?.configuredProviders || [],
92
+ installedProviderIds,
93
+ connection?.activeProvider || null
94
+ );
95
+
96
+ return {
97
+ ...platform,
98
+ configuredProviders: mergedProviders
99
+ };
100
+ });
101
+ }
102
+
103
+ function runCommandSync({ command, args = [], timeoutMs = COMMAND_TIMEOUT_MS, cwd = process.cwd(), env = process.env }) {
104
+ const runner = process.platform === 'win32' ? crossSpawn.sync : spawnSync;
105
+ const result = runner(command, args, {
106
+ cwd,
107
+ env,
108
+ encoding: 'utf8',
109
+ timeout: timeoutMs
110
+ });
111
+
112
+ return {
113
+ status: result.status,
114
+ stdout: result.stdout || '',
115
+ stderr: result.stderr || '',
116
+ error: result.error || null
117
+ };
118
+ }
119
+
120
+ function normalizePlatformId(value) {
121
+ const platformId = String(value || '').trim().toLowerCase();
122
+ return PLATFORM_MAP[platformId] ? platformId : null;
123
+ }
124
+
125
+ function normalizeProviderId(value) {
126
+ const providerId = String(value || '').trim().toLowerCase();
127
+ return PROVIDER_ORDER.includes(providerId) ? providerId : null;
128
+ }
129
+
130
+ function buildCommandEnv() {
131
+ const env = { ...process.env };
132
+ delete env.CLAUDECODE;
133
+ return env;
134
+ }
135
+
136
+ function getNpmCommand() {
137
+ return process.platform === 'win32' ? 'npm.cmd' : 'npm';
138
+ }
139
+
140
+ async function getCcConnectStatus() {
141
+ const resolved = await resolveCommandPath(CC_CONNECT_COMMAND);
142
+ if (!resolved.found) {
143
+ return {
144
+ installed: false,
145
+ version: null,
146
+ isBeta: false,
147
+ resolvedPath: null
148
+ };
149
+ }
150
+
151
+ const result = runCommandSync({
152
+ command: resolved.resolvedPath || CC_CONNECT_COMMAND,
153
+ args: ['--version'],
154
+ timeoutMs: COMMAND_TIMEOUT_MS,
155
+ env: buildCommandEnv()
156
+ });
157
+ const version = String(result.stdout || '').trim() || null;
158
+
159
+ return {
160
+ installed: true,
161
+ version,
162
+ isBeta: Boolean(version && /beta|alpha|rc|pre/i.test(version)),
163
+ resolvedPath: resolved.resolvedPath || null
164
+ };
165
+ }
166
+
167
+ async function getProviderStatuses() {
168
+ const statuses = await Promise.all(
169
+ PROVIDER_ORDER.map(async (providerId) => {
170
+ try {
171
+ const status = await detectProviderInstallationStatus(providerId);
172
+ return {
173
+ id: providerId,
174
+ label: CC_CONNECT_PROVIDERS.find((provider) => provider.id === providerId)?.label || providerId,
175
+ ...status
176
+ };
177
+ } catch (error) {
178
+ return {
179
+ id: providerId,
180
+ label: CC_CONNECT_PROVIDERS.find((provider) => provider.id === providerId)?.label || providerId,
181
+ success: false,
182
+ installed: false,
183
+ error: error.message
184
+ };
185
+ }
186
+ })
187
+ );
188
+
189
+ return statuses;
190
+ }
191
+
192
+ async function getInstalledProviderIds() {
193
+ const statuses = await getProviderStatuses();
194
+ return statuses.filter((status) => status.installed).map((status) => status.id);
195
+ }
196
+
197
+ function cleanupSessionArtifacts(session) {
198
+ if (!session) {
199
+ return;
200
+ }
201
+
202
+ for (const filePath of [session.tempConfigPath, session.qrImagePath]) {
203
+ if (!filePath) {
204
+ continue;
205
+ }
206
+
207
+ try {
208
+ if (fs.existsSync(filePath)) {
209
+ fs.unlinkSync(filePath);
210
+ }
211
+ } catch {
212
+ // Best-effort cleanup.
213
+ }
214
+ }
215
+ }
216
+
217
+ function cleanupSession(sessionId) {
218
+ const session = setupSessions.get(sessionId);
219
+ if (!session) {
220
+ return;
221
+ }
222
+
223
+ if (session.process && !session.process.killed) {
224
+ try {
225
+ session.process.kill('SIGTERM');
226
+ } catch {
227
+ // Ignore child cleanup failures.
228
+ }
229
+ }
230
+
231
+ cleanupSessionArtifacts(session);
232
+ setupSessions.delete(sessionId);
233
+ }
234
+
235
+ function scheduleFinishedSessionCleanup(sessionId) {
236
+ setTimeout(() => {
237
+ cleanupSession(sessionId);
238
+ }, SESSION_TERMINAL_GRACE_MS);
239
+ }
240
+
241
+ function parseQrStatusOutput(session, output) {
242
+ const combinedOutput = String(output || '');
243
+
244
+ const urlMatch = combinedOutput.match(/https?:\/\/[^\s"'<>]+/);
245
+ if (urlMatch && !session.qrUrl) {
246
+ session.qrUrl = urlMatch[0];
247
+ session.status = 'pending';
248
+ session.message = 'Scan the QR code to continue.';
249
+ }
250
+
251
+ if (session.qrImagePath && !session.qrImageBase64 && fs.existsSync(session.qrImagePath)) {
252
+ try {
253
+ session.qrImageBase64 = fs.readFileSync(session.qrImagePath).toString('base64');
254
+ session.status = 'pending';
255
+ session.message = 'Scan the QR code to continue.';
256
+ } catch {
257
+ // Ignore partially-written image files and retry on next chunk.
258
+ }
259
+ }
260
+
261
+ if (/已扫码|scann?ed|confirm on (?:your )?phone|confirm/i.test(combinedOutput) && session.status === 'pending') {
262
+ session.status = 'scanned';
263
+ session.message = 'QR code scanned. Confirm on your device to finish setup.';
264
+ }
265
+
266
+ if (/token.*written|setup.*complete|登录成功|绑定成功|success/i.test(combinedOutput)) {
267
+ session.message = 'Connection completed. Applying runtime configuration...';
268
+ }
269
+ }
270
+
271
+ function buildShortErrorMessage(result) {
272
+ const stderr = String(result?.stderr || '').trim();
273
+ if (stderr) {
274
+ return stderr.replace(/\s+/g, ' ').slice(0, 260);
275
+ }
276
+
277
+ const stdout = String(result?.stdout || '').trim();
278
+ if (stdout) {
279
+ return stdout.replace(/\s+/g, ' ').slice(0, 260);
280
+ }
281
+
282
+ if (result?.error?.message) {
283
+ return result.error.message;
284
+ }
285
+
286
+ return 'Unknown error';
287
+ }
288
+
289
+ function runCcConnectDaemonCommand(args, timeoutMs = DAEMON_COMMAND_TIMEOUT_MS) {
290
+ return runCommandSync({
291
+ command: CC_CONNECT_COMMAND,
292
+ args,
293
+ timeoutMs,
294
+ env: buildCommandEnv()
295
+ });
296
+ }
297
+
298
+ function startCcConnectViaDaemon(configPath) {
299
+ const statusResult = runCcConnectDaemonCommand(['daemon', 'status']);
300
+ const isRunning = statusResult.status === 0 && /status:\s+running/i.test(statusResult.stdout || '');
301
+
302
+ if (isRunning) {
303
+ const restartResult = runCcConnectDaemonCommand(['daemon', 'restart']);
304
+ if (restartResult.status === 0) {
305
+ return { ok: true, detail: 'cc-connect daemon restarted with the latest configuration.' };
306
+ }
307
+
308
+ return {
309
+ ok: false,
310
+ detail: `Configuration written, but daemon restart failed: ${buildShortErrorMessage(restartResult)}`
311
+ };
312
+ }
313
+
314
+ const startResult = runCcConnectDaemonCommand(['daemon', 'start']);
315
+ if (startResult.status === 0) {
316
+ return { ok: true, detail: 'cc-connect daemon started with the latest configuration.' };
317
+ }
318
+
319
+ const installResult = runCcConnectDaemonCommand(['daemon', 'install', '--config', configPath], 60000);
320
+ if (installResult.status === 0) {
321
+ return { ok: true, detail: 'cc-connect daemon installed and started.' };
322
+ }
323
+
324
+ return {
325
+ ok: false,
326
+ detail: `Configuration written, but daemon start failed: ${buildShortErrorMessage(installResult)}`
327
+ };
328
+ }
329
+
330
+ function startCcConnectDetached(configPath) {
331
+ try {
332
+ const child = spawnCommand(CC_CONNECT_COMMAND, ['--config', configPath], {
333
+ detached: true,
334
+ stdio: 'ignore',
335
+ env: buildCommandEnv(),
336
+ windowsHide: true
337
+ });
338
+ child.unref();
339
+ return { ok: true, detail: 'cc-connect started in the background.' };
340
+ } catch (error) {
341
+ return {
342
+ ok: false,
343
+ detail: `Configuration written, but background start failed: ${error.message}`
344
+ };
345
+ }
346
+ }
347
+
348
+ function ensureCcConnectDaemonRunning(configPath) {
349
+ if (process.platform === 'win32') {
350
+ return startCcConnectDetached(configPath);
351
+ }
352
+
353
+ return startCcConnectViaDaemon(configPath);
354
+ }
355
+
356
+ function stopCcConnectDaemon() {
357
+ try {
358
+ runCcConnectDaemonCommand(['daemon', 'stop']);
359
+ } catch {
360
+ // Ignore stop failures.
361
+ }
362
+
363
+ try {
364
+ runCcConnectDaemonCommand(['daemon', 'uninstall']);
365
+ } catch {
366
+ // Ignore uninstall failures.
367
+ }
368
+ }
369
+
370
+ function countConfiguredProjects(configContent) {
371
+ try {
372
+ const parsed = TOML.parse(configContent);
373
+ return Array.isArray(parsed?.projects) ? parsed.projects.length : 0;
374
+ } catch {
375
+ return 0;
376
+ }
377
+ }
378
+
379
+ function buildSetupOptions(platformId, mode, credentials = {}) {
380
+ if (platformId === 'weixin') {
381
+ if (mode === 'token') {
382
+ return {
383
+ token: String(credentials.token || '').trim()
384
+ };
385
+ }
386
+
387
+ return {
388
+ token: ''
389
+ };
390
+ }
391
+
392
+ if (platformId === 'feishu') {
393
+ if (mode === 'credentials') {
394
+ return {
395
+ app_id: String(credentials.app_id || credentials.appId || '').trim(),
396
+ app_secret: String(credentials.app_secret || credentials.appSecret || '').trim()
397
+ };
398
+ }
399
+
400
+ return {
401
+ app_id: '',
402
+ app_secret: ''
403
+ };
404
+ }
405
+
406
+ return {};
407
+ }
408
+
409
+ function buildSetupArgs({ platformId, mode, tempConfigPath, projectName, qrImagePath, credentials = {} }) {
410
+ if (platformId === 'weixin') {
411
+ if (mode === 'token') {
412
+ const token = String(credentials.token || '').trim();
413
+ const args = ['weixin', 'setup', '--project', projectName, '--config', tempConfigPath, '--token', token];
414
+ if (credentials.api_url || credentials.apiUrl) {
415
+ args.push('--api-url', String(credentials.api_url || credentials.apiUrl).trim());
416
+ }
417
+ return args;
418
+ }
419
+
420
+ return [
421
+ 'weixin',
422
+ 'setup',
423
+ '--project',
424
+ projectName,
425
+ '--config',
426
+ tempConfigPath,
427
+ '--qr-image',
428
+ qrImagePath,
429
+ '--timeout',
430
+ String(PLATFORM_MAP[platformId].timeoutSeconds)
431
+ ];
432
+ }
433
+
434
+ if (platformId === 'feishu') {
435
+ const baseArgs = ['feishu', 'setup', '--project', projectName, '--config', tempConfigPath, '--platform-type', 'feishu'];
436
+ if (mode === 'credentials') {
437
+ return [
438
+ ...baseArgs,
439
+ '--app-id',
440
+ String(credentials.app_id || credentials.appId || '').trim(),
441
+ '--app-secret',
442
+ String(credentials.app_secret || credentials.appSecret || '').trim()
443
+ ];
444
+ }
445
+
446
+ return [
447
+ ...baseArgs,
448
+ '--qr-image',
449
+ qrImagePath,
450
+ '--timeout',
451
+ String(PLATFORM_MAP[platformId].timeoutSeconds)
452
+ ];
453
+ }
454
+
455
+ throw new Error(`Unsupported platform: ${platformId}`);
456
+ }
457
+
458
+ function validateSetupPayload(platformId, body) {
459
+ const projectPath = typeof body?.projectPath === 'string' ? body.projectPath.trim() : '';
460
+ if (!projectPath) {
461
+ return { ok: false, error: 'projectPath is required.' };
462
+ }
463
+
464
+ if (!fs.existsSync(projectPath)) {
465
+ return { ok: false, error: 'Selected project path does not exist.' };
466
+ }
467
+
468
+ const provider = typeof body?.provider === 'undefined'
469
+ ? null
470
+ : normalizeProviderId(body.provider);
471
+
472
+ if (typeof body?.provider !== 'undefined' && !provider) {
473
+ return { ok: false, error: 'Unsupported provider.' };
474
+ }
475
+
476
+ if (platformId === 'weixin') {
477
+ const mode = body?.mode === 'token' ? 'token' : 'qr';
478
+ if (mode === 'token' && !String(body?.credentials?.token || '').trim()) {
479
+ return { ok: false, error: 'A Weixin token is required for token mode.' };
480
+ }
481
+
482
+ return {
483
+ ok: true,
484
+ payload: {
485
+ mode,
486
+ projectPath,
487
+ provider,
488
+ credentials: body?.credentials || {}
489
+ }
490
+ };
491
+ }
492
+
493
+ if (platformId === 'feishu') {
494
+ const mode = body?.mode === 'credentials' ? 'credentials' : 'qr';
495
+ const credentials = body?.credentials || {};
496
+ if (mode === 'credentials') {
497
+ const appId = String(credentials.app_id || credentials.appId || '').trim();
498
+ const appSecret = String(credentials.app_secret || credentials.appSecret || '').trim();
499
+ if (!appId || !appSecret) {
500
+ return { ok: false, error: 'app_id and app_secret are required for Feishu credentials mode.' };
501
+ }
502
+ }
503
+
504
+ return {
505
+ ok: true,
506
+ payload: {
507
+ mode,
508
+ projectPath,
509
+ provider,
510
+ credentials
511
+ }
512
+ };
513
+ }
514
+
515
+ return { ok: false, error: 'Unsupported platform.' };
516
+ }
517
+
518
+ async function finalizeSetupSession(session) {
519
+ const configContent = fs.readFileSync(session.tempConfigPath, 'utf8');
520
+ const connectionOptions = extractPlatformOptionsFromConfigContent(
521
+ configContent,
522
+ session.primaryProjectName,
523
+ session.platformId
524
+ );
525
+
526
+ if (!connectionOptions || !Object.keys(connectionOptions).length) {
527
+ throw new Error('Setup completed, but no connection options were written to the temporary config.');
528
+ }
529
+
530
+ const currentState = readCcConnectState();
531
+ const previousConnection = getPlatformConnection(currentState, session.platformId);
532
+ const preferredProvider = session.primaryProvider && session.configuredProviders.includes(session.primaryProvider)
533
+ ? session.primaryProvider
534
+ : previousConnection?.activeProvider && session.configuredProviders.includes(previousConnection.activeProvider)
535
+ ? previousConnection.activeProvider
536
+ : session.configuredProviders[0] || null;
537
+
538
+ const nextState = upsertPlatformConnection(currentState, session.platformId, {
539
+ configuredProviders: session.configuredProviders,
540
+ activeProvider: preferredProvider,
541
+ projectPath: session.projectPath,
542
+ connectionOptions,
543
+ platformType: session.platformId,
544
+ connectedAt: previousConnection?.connectedAt || nowIso(),
545
+ updatedAt: nowIso()
546
+ });
547
+
548
+ writeCcConnectState(nextState);
549
+ const { configPath } = writeCcConnectConfig(nextState);
550
+ const daemonResult = ensureCcConnectDaemonRunning(configPath);
551
+
552
+ if (!daemonResult.ok) {
553
+ throw new Error(daemonResult.detail);
554
+ }
555
+
556
+ session.status = 'confirmed';
557
+ session.message = daemonResult.detail;
558
+ }
559
+
560
+ async function createSetupSession({ platformId, mode, projectPath, credentials, provider }) {
561
+ const ccStatus = await getCcConnectStatus();
562
+ if (!ccStatus.installed) {
563
+ throw new Error('cc-connect is not installed.');
564
+ }
565
+
566
+ const configuredProviders = await getInstalledProviderIds();
567
+ if (!configuredProviders.length) {
568
+ throw new Error('No supported AI providers are installed. Install Claude, Codex, Gemini, or OpenCode first.');
569
+ }
570
+
571
+ const existingConnection = getPlatformConnection(readCcConnectState(), platformId);
572
+ const requestedProvider = normalizeProviderId(provider);
573
+ if (requestedProvider && !configuredProviders.includes(requestedProvider)) {
574
+ throw new Error(`Selected provider is not installed: ${requestedProvider}`);
575
+ }
576
+
577
+ const primaryProvider = requestedProvider && configuredProviders.includes(requestedProvider)
578
+ ? requestedProvider
579
+ : existingConnection?.activeProvider && configuredProviders.includes(existingConnection.activeProvider)
580
+ ? existingConnection.activeProvider
581
+ : configuredProviders[0];
582
+ const sessionId = `${platformId}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
583
+ const primaryProjectName = getCcConnectManagedProjectName(platformId, primaryProvider);
584
+ const tempConfigPath = path.join(os.tmpdir(), `cc-connect-${platformId}-${sessionId}.toml`);
585
+ const qrImagePath = mode === 'qr' ? path.join(os.tmpdir(), `cc-connect-${platformId}-${sessionId}.png`) : null;
586
+ const setupOptions = buildSetupOptions(platformId, mode, credentials);
587
+ const session = {
588
+ id: sessionId,
589
+ platformId,
590
+ mode,
591
+ status: 'starting',
592
+ qrUrl: null,
593
+ qrImageBase64: null,
594
+ startedAt: Date.now(),
595
+ expiresAt: Date.now() + (PLATFORM_MAP[platformId].timeoutSeconds * 1000),
596
+ message: 'Starting connection setup...',
597
+ process: null,
598
+ projectPath,
599
+ configuredProviders,
600
+ primaryProvider,
601
+ primaryProjectName,
602
+ tempConfigPath,
603
+ qrImagePath,
604
+ credentials
605
+ };
606
+
607
+ fs.writeFileSync(
608
+ tempConfigPath,
609
+ createSetupConfigContent({
610
+ platformId,
611
+ providerId: primaryProvider,
612
+ projectName: primaryProjectName,
613
+ projectPath,
614
+ setupOptions
615
+ }),
616
+ 'utf8'
617
+ );
618
+
619
+ setupSessions.set(sessionId, session);
620
+
621
+ const args = buildSetupArgs({
622
+ platformId,
623
+ mode,
624
+ tempConfigPath,
625
+ projectName: primaryProjectName,
626
+ qrImagePath,
627
+ credentials
628
+ });
629
+
630
+ let child = null;
631
+ try {
632
+ child = spawnCommand(CC_CONNECT_COMMAND, args, {
633
+ cwd: projectPath,
634
+ env: buildCommandEnv(),
635
+ stdio: ['ignore', 'pipe', 'pipe'],
636
+ windowsHide: process.platform === 'win32'
637
+ });
638
+ session.process = child;
639
+ } catch (error) {
640
+ cleanupSessionArtifacts(session);
641
+ setupSessions.delete(session.id);
642
+ throw error;
643
+ }
644
+
645
+ let stdoutBuffer = '';
646
+ let stderrBuffer = '';
647
+
648
+ child.stdout?.on('data', (chunk) => {
649
+ const text = chunk.toString('utf8');
650
+ stdoutBuffer += text;
651
+ parseQrStatusOutput(session, stdoutBuffer);
652
+ });
653
+
654
+ child.stderr?.on('data', (chunk) => {
655
+ const text = chunk.toString('utf8');
656
+ stderrBuffer += text;
657
+ parseQrStatusOutput(session, stderrBuffer);
658
+ });
659
+
660
+ child.once('error', (error) => {
661
+ session.status = 'error';
662
+ session.message = `Failed to start setup: ${error.message}`;
663
+ scheduleFinishedSessionCleanup(session.id);
664
+ });
665
+
666
+ child.once('close', async (code) => {
667
+ try {
668
+ if (code === 0) {
669
+ await finalizeSetupSession(session);
670
+ } else if (session.status !== 'error') {
671
+ session.status = 'error';
672
+ session.message = `Setup failed: ${String(stderrBuffer || stdoutBuffer || `exit code ${code}`).trim().slice(0, 260)}`;
673
+ }
674
+ } catch (error) {
675
+ session.status = 'error';
676
+ session.message = error.message;
677
+ } finally {
678
+ cleanupSessionArtifacts(session);
679
+ scheduleFinishedSessionCleanup(session.id);
680
+ }
681
+ });
682
+
683
+ return session;
684
+ }
685
+
686
+ setInterval(() => {
687
+ const now = Date.now();
688
+ for (const [sessionId, session] of setupSessions.entries()) {
689
+ if (session.status === 'confirmed' || session.status === 'error' || session.status === 'expired') {
690
+ continue;
691
+ }
692
+
693
+ if (now > session.expiresAt) {
694
+ session.status = 'expired';
695
+ session.message = 'The setup session expired. Start again to generate a new QR code.';
696
+ if (session.process && !session.process.killed) {
697
+ try {
698
+ session.process.kill('SIGTERM');
699
+ } catch {
700
+ // Ignore timeout cleanup failures.
701
+ }
702
+ }
703
+ cleanupSessionArtifacts(session);
704
+ scheduleFinishedSessionCleanup(sessionId);
705
+ }
706
+ }
707
+ }, 10000);
708
+
709
+ async function buildSummaryResponse() {
710
+ const ccConnect = await getCcConnectStatus();
711
+ const providers = await getProviderStatuses();
712
+ const state = readCcConnectState();
713
+ const installedProviderIds = providers.filter((provider) => provider.installed).map((provider) => provider.id);
714
+
715
+ return {
716
+ success: true,
717
+ ccConnect,
718
+ providers,
719
+ platforms: buildPlatformSummaries(state, installedProviderIds)
720
+ };
721
+ }
722
+
723
+ function buildPlatformSessionPayload(session) {
724
+ return {
725
+ sessionId: session.id,
726
+ platform: session.platformId,
727
+ mode: session.mode,
728
+ provider: session.primaryProvider,
729
+ providers: session.configuredProviders,
730
+ status: session.status,
731
+ qrUrl: session.qrUrl,
732
+ qrImageBase64: session.qrImageBase64,
733
+ expiresAt: session.expiresAt,
734
+ message: session.message
735
+ };
736
+ }
737
+
738
+ router.get('/status', async (req, res) => {
739
+ try {
740
+ res.json(await getCcConnectStatus());
741
+ } catch (error) {
742
+ res.status(500).json({ success: false, error: error.message });
743
+ }
744
+ });
745
+
746
+ router.get('/providers', async (req, res) => {
747
+ try {
748
+ const providers = await getProviderStatuses();
749
+ res.json({ success: true, providers });
750
+ } catch (error) {
751
+ res.status(500).json({ success: false, error: error.message });
752
+ }
753
+ });
754
+
755
+ router.get('/agents', async (req, res) => {
756
+ try {
757
+ const providers = await getProviderStatuses();
758
+ res.json({ success: true, agents: providers, providers });
759
+ } catch (error) {
760
+ res.status(500).json({ success: false, error: error.message });
761
+ }
762
+ });
763
+
764
+ router.get('/platforms', async (req, res) => {
765
+ try {
766
+ res.json(await buildSummaryResponse());
767
+ } catch (error) {
768
+ res.status(500).json({ success: false, error: error.message });
769
+ }
770
+ });
771
+
772
+ router.post('/install', async (req, res) => {
773
+ try {
774
+ const existing = await getCcConnectStatus();
775
+ if (existing.installed && existing.isBeta) {
776
+ return res.json({
777
+ success: true,
778
+ version: existing.version,
779
+ message: 'cc-connect@beta is already installed.'
780
+ });
781
+ }
782
+
783
+ const result = runCommandSync({
784
+ command: getNpmCommand(),
785
+ args: ['install', '-g', 'cc-connect@beta'],
786
+ timeoutMs: INSTALL_TIMEOUT_MS,
787
+ env: buildCommandEnv()
788
+ });
789
+
790
+ if (result.status !== 0) {
791
+ return res.status(500).json({
792
+ success: false,
793
+ error: `Installation failed: ${buildShortErrorMessage(result)}`
794
+ });
795
+ }
796
+
797
+ const installed = await getCcConnectStatus();
798
+ return res.json({
799
+ success: true,
800
+ version: installed.version,
801
+ message: 'cc-connect@beta installed successfully.'
802
+ });
803
+ } catch (error) {
804
+ return res.status(500).json({ success: false, error: error.message });
805
+ }
806
+ });
807
+
808
+ router.get('/active-agent', (req, res) => {
809
+ void (async () => {
810
+ const state = readCcConnectState();
811
+ const installedProviderIds = await getInstalledProviderIds();
812
+ const connection = withAvailableProviders(getPlatformConnection(state, 'weixin'), installedProviderIds);
813
+ res.json({
814
+ agent: connection?.activeProvider || null,
815
+ availableAgents: connection?.configuredProviders || []
816
+ });
817
+ })().catch((error) => {
818
+ res.status(500).json({ success: false, error: error.message });
819
+ });
820
+ });
821
+
822
+ router.post('/switch-agent', (req, res) => {
823
+ void (async () => {
824
+ const provider = String(req.body?.agent || '').trim().toLowerCase();
825
+ if (!provider) {
826
+ return res.status(400).json({ success: false, error: 'agent is required.' });
827
+ }
828
+
829
+ const currentState = readCcConnectState();
830
+ const installedProviderIds = await getInstalledProviderIds();
831
+ const connection = getPlatformConnection(currentState, 'weixin');
832
+ if (!connection) {
833
+ return res.status(404).json({ success: false, error: 'Weixin is not connected.' });
834
+ }
835
+
836
+ const availableProviders = mergeAvailableProviders(
837
+ connection.configuredProviders,
838
+ installedProviderIds,
839
+ connection.activeProvider
840
+ );
841
+
842
+ if (!availableProviders.includes(provider)) {
843
+ return res.status(400).json({ success: false, error: `Unsupported provider: ${provider}` });
844
+ }
845
+
846
+ const nextState = upsertPlatformConnection(currentState, 'weixin', {
847
+ ...connection,
848
+ configuredProviders: availableProviders,
849
+ activeProvider: provider,
850
+ updatedAt: nowIso()
851
+ });
852
+ writeCcConnectState(nextState);
853
+ const { configPath } = writeCcConnectConfig(nextState);
854
+ const daemonResult = ensureCcConnectDaemonRunning(configPath);
855
+ if (!daemonResult.ok) {
856
+ return res.status(500).json({ success: false, error: daemonResult.detail });
857
+ }
858
+
859
+ return res.json({ success: true, agent: provider, message: daemonResult.detail });
860
+ })().catch((error) => {
861
+ return res.status(500).json({ success: false, error: error.message });
862
+ });
863
+ });
864
+
865
+ router.post('/unbind', (req, res) => {
866
+ const currentState = readCcConnectState();
867
+ const nextState = removePlatformConnection(currentState, 'weixin');
868
+ writeCcConnectState(nextState);
869
+ const { content } = writeCcConnectConfig(nextState);
870
+ if (countConfiguredProjects(content) > 0) {
871
+ const daemonResult = ensureCcConnectDaemonRunning(CC_CONNECT_CONFIG_PATH);
872
+ if (!daemonResult.ok) {
873
+ return res.status(500).json({ success: false, error: daemonResult.detail });
874
+ }
875
+ return res.json({ success: true, message: 'Weixin connection removed.' });
876
+ }
877
+
878
+ stopCcConnectDaemon();
879
+ return res.json({ success: true, message: 'Weixin connection removed.' });
880
+ });
881
+
882
+ router.post('/:platform/setup', async (req, res) => {
883
+ try {
884
+ const platformId = normalizePlatformId(req.params.platform);
885
+ if (!platformId) {
886
+ return res.status(404).json({ success: false, error: 'Unsupported platform.' });
887
+ }
888
+
889
+ const validation = validateSetupPayload(platformId, req.body);
890
+ if (!validation.ok) {
891
+ return res.status(400).json({ success: false, error: validation.error });
892
+ }
893
+
894
+ const session = await createSetupSession({
895
+ platformId,
896
+ ...validation.payload
897
+ });
898
+
899
+ return res.json({
900
+ success: true,
901
+ ...buildPlatformSessionPayload(session),
902
+ providers: session.configuredProviders
903
+ });
904
+ } catch (error) {
905
+ return res.status(500).json({ success: false, error: error.message });
906
+ }
907
+ });
908
+
909
+ router.get('/:platform/status', (req, res) => {
910
+ const platformId = normalizePlatformId(req.params.platform);
911
+ if (!platformId) {
912
+ return res.status(404).json({ success: false, error: 'Unsupported platform.' });
913
+ }
914
+
915
+ const sessionId = String(req.query.sessionId || '').trim();
916
+ if (!sessionId) {
917
+ return res.status(400).json({ success: false, error: 'sessionId is required.' });
918
+ }
919
+
920
+ const session = setupSessions.get(sessionId);
921
+ if (!session || session.platformId !== platformId) {
922
+ return res.status(404).json({ success: false, error: 'Setup session not found.' });
923
+ }
924
+
925
+ return res.json(buildPlatformSessionPayload(session));
926
+ });
927
+
928
+ router.post('/:platform/refresh', async (req, res) => {
929
+ try {
930
+ const platformId = normalizePlatformId(req.params.platform);
931
+ if (!platformId) {
932
+ return res.status(404).json({ success: false, error: 'Unsupported platform.' });
933
+ }
934
+
935
+ const sessionId = String(req.body?.sessionId || req.query.sessionId || '').trim();
936
+ if (!sessionId) {
937
+ return res.status(400).json({ success: false, error: 'sessionId is required.' });
938
+ }
939
+
940
+ const previousSession = setupSessions.get(sessionId);
941
+ if (!previousSession || previousSession.platformId !== platformId) {
942
+ return res.status(404).json({ success: false, error: 'Setup session not found.' });
943
+ }
944
+
945
+ if (previousSession.mode !== 'qr') {
946
+ return res.status(400).json({ success: false, error: 'Only QR setup sessions can be refreshed.' });
947
+ }
948
+
949
+ cleanupSession(sessionId);
950
+ const session = await createSetupSession({
951
+ platformId,
952
+ mode: 'qr',
953
+ projectPath: previousSession.projectPath,
954
+ credentials: previousSession.credentials,
955
+ provider: previousSession.primaryProvider
956
+ });
957
+
958
+ return res.json({
959
+ success: true,
960
+ ...buildPlatformSessionPayload(session),
961
+ providers: session.configuredProviders
962
+ });
963
+ } catch (error) {
964
+ return res.status(500).json({ success: false, error: error.message });
965
+ }
966
+ });
967
+
968
+ router.delete('/:platform/session', (req, res) => {
969
+ const platformId = normalizePlatformId(req.params.platform);
970
+ if (!platformId) {
971
+ return res.status(404).json({ success: false, error: 'Unsupported platform.' });
972
+ }
973
+
974
+ const sessionId = String(req.query.sessionId || '').trim();
975
+ if (sessionId) {
976
+ const session = setupSessions.get(sessionId);
977
+ if (session?.platformId === platformId) {
978
+ cleanupSession(sessionId);
979
+ }
980
+ }
981
+
982
+ res.json({ success: true });
983
+ });
984
+
985
+ router.get('/:platform/active-provider', (req, res) => {
986
+ void (async () => {
987
+ const platformId = normalizePlatformId(req.params.platform);
988
+ if (!platformId) {
989
+ return res.status(404).json({ success: false, error: 'Unsupported platform.' });
990
+ }
991
+
992
+ const state = readCcConnectState();
993
+ const installedProviderIds = await getInstalledProviderIds();
994
+ const connection = withAvailableProviders(getPlatformConnection(state, platformId), installedProviderIds);
995
+ return res.json({
996
+ success: true,
997
+ provider: connection?.activeProvider || null,
998
+ availableProviders: connection?.configuredProviders || [],
999
+ projectPath: connection?.projectPath || null
1000
+ });
1001
+ })().catch((error) => {
1002
+ return res.status(500).json({ success: false, error: error.message });
1003
+ });
1004
+ });
1005
+
1006
+ router.post('/:platform/switch-provider', (req, res) => {
1007
+ void (async () => {
1008
+ const platformId = normalizePlatformId(req.params.platform);
1009
+ if (!platformId) {
1010
+ return res.status(404).json({ success: false, error: 'Unsupported platform.' });
1011
+ }
1012
+
1013
+ const provider = String(req.body?.provider || '').trim().toLowerCase();
1014
+ const currentState = readCcConnectState();
1015
+ const installedProviderIds = await getInstalledProviderIds();
1016
+ const connection = getPlatformConnection(currentState, platformId);
1017
+
1018
+ if (!provider) {
1019
+ return res.status(400).json({ success: false, error: 'provider is required.' });
1020
+ }
1021
+
1022
+ if (!connection) {
1023
+ return res.status(404).json({ success: false, error: `${PLATFORM_MAP[platformId].label} is not connected.` });
1024
+ }
1025
+
1026
+ const availableProviders = mergeAvailableProviders(
1027
+ connection.configuredProviders,
1028
+ installedProviderIds,
1029
+ connection.activeProvider
1030
+ );
1031
+
1032
+ if (!availableProviders.includes(provider)) {
1033
+ return res.status(400).json({ success: false, error: `Unsupported provider: ${provider}` });
1034
+ }
1035
+
1036
+ const nextState = upsertPlatformConnection(currentState, platformId, {
1037
+ ...connection,
1038
+ configuredProviders: availableProviders,
1039
+ activeProvider: provider,
1040
+ updatedAt: nowIso()
1041
+ });
1042
+ writeCcConnectState(nextState);
1043
+ const { configPath } = writeCcConnectConfig(nextState);
1044
+ const daemonResult = ensureCcConnectDaemonRunning(configPath);
1045
+ if (!daemonResult.ok) {
1046
+ return res.status(500).json({ success: false, error: daemonResult.detail });
1047
+ }
1048
+
1049
+ return res.json({ success: true, provider, message: daemonResult.detail });
1050
+ })().catch((error) => {
1051
+ return res.status(500).json({ success: false, error: error.message });
1052
+ });
1053
+ });
1054
+
1055
+ router.post('/:platform/project', (req, res) => {
1056
+ void (async () => {
1057
+ const platformId = normalizePlatformId(req.params.platform);
1058
+ if (!platformId) {
1059
+ return res.status(404).json({ success: false, error: 'Unsupported platform.' });
1060
+ }
1061
+
1062
+ const projectPath = typeof req.body?.projectPath === 'string' ? req.body.projectPath.trim() : '';
1063
+ if (!projectPath) {
1064
+ return res.status(400).json({ success: false, error: 'projectPath is required.' });
1065
+ }
1066
+
1067
+ if (!fs.existsSync(projectPath)) {
1068
+ return res.status(400).json({ success: false, error: 'Selected project path does not exist.' });
1069
+ }
1070
+
1071
+ const currentState = readCcConnectState();
1072
+ const installedProviderIds = await getInstalledProviderIds();
1073
+ const connection = getPlatformConnection(currentState, platformId);
1074
+
1075
+ if (!connection) {
1076
+ return res.status(404).json({ success: false, error: `${PLATFORM_MAP[platformId].label} is not connected.` });
1077
+ }
1078
+
1079
+ const availableProviders = mergeAvailableProviders(
1080
+ connection.configuredProviders,
1081
+ installedProviderIds,
1082
+ connection.activeProvider
1083
+ );
1084
+
1085
+ const nextState = upsertPlatformConnection(currentState, platformId, {
1086
+ ...connection,
1087
+ configuredProviders: availableProviders,
1088
+ projectPath,
1089
+ updatedAt: nowIso()
1090
+ });
1091
+ writeCcConnectState(nextState);
1092
+ const { configPath } = writeCcConnectConfig(nextState);
1093
+ const daemonResult = ensureCcConnectDaemonRunning(configPath);
1094
+
1095
+ if (!daemonResult.ok) {
1096
+ return res.status(500).json({ success: false, error: daemonResult.detail });
1097
+ }
1098
+
1099
+ return res.json({
1100
+ success: true,
1101
+ projectPath,
1102
+ message: daemonResult.detail
1103
+ });
1104
+ })().catch((error) => {
1105
+ return res.status(500).json({ success: false, error: error.message });
1106
+ });
1107
+ });
1108
+
1109
+ router.post('/:platform/unbind', (req, res) => {
1110
+ const platformId = normalizePlatformId(req.params.platform);
1111
+ if (!platformId) {
1112
+ return res.status(404).json({ success: false, error: 'Unsupported platform.' });
1113
+ }
1114
+
1115
+ const currentState = readCcConnectState();
1116
+ const nextState = removePlatformConnection(currentState, platformId);
1117
+ writeCcConnectState(nextState);
1118
+ const { content } = writeCcConnectConfig(nextState);
1119
+ if (countConfiguredProjects(content) > 0) {
1120
+ const daemonResult = ensureCcConnectDaemonRunning(CC_CONNECT_CONFIG_PATH);
1121
+ if (!daemonResult.ok) {
1122
+ return res.status(500).json({ success: false, error: daemonResult.detail });
1123
+ }
1124
+ return res.json({ success: true, message: `${PLATFORM_MAP[platformId].label} connection removed.` });
1125
+ }
1126
+
1127
+ stopCcConnectDaemon();
1128
+ return res.json({ success: true, message: `${PLATFORM_MAP[platformId].label} connection removed.` });
1129
+ });
1130
+
1131
+ export default router;