@axhub/genie 0.2.7 → 0.2.8
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.
- package/dist/api-docs.html +2 -2
- package/dist/assets/App-CTKZtqB1.js +460 -0
- package/dist/assets/App-qxJ8_QYu.css +32 -0
- package/dist/assets/ReviewApp-DM6BNAzR.js +1 -0
- package/dist/assets/{_basePickBy-C19AekOu.js → _basePickBy-CqJbRZ9y.js} +1 -1
- package/dist/assets/{_baseUniq-JsnevLw_.js → _baseUniq-BS8YH8jO.js} +1 -1
- package/dist/assets/{arc-BLpcuBlf.js → arc-BBmKEN-S.js} +1 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-N5lcb82R.js +36 -0
- package/dist/assets/{blockDiagram-WCTKOSBZ-DQBLwsUS.js → blockDiagram-WCTKOSBZ-DTMwHuLn.js} +3 -3
- package/dist/assets/c4Diagram-IC4MRINW-BTKlkXI9.js +10 -0
- package/dist/assets/channel-1oJBvF-0.js +1 -0
- package/dist/assets/{chunk-4BX2VUAB-De63kbgc.js → chunk-4BX2VUAB-DUdoTxAc.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DtTDDdM9.js → chunk-55IACEB6-Bm_92xe4.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DHuwd8tw.js → chunk-FMBD7UC4-CGW0g62g.js} +1 -1
- package/dist/assets/{chunk-JSJVCQXG-BgytFtmO.js → chunk-JSJVCQXG-DYkTH3w1.js} +1 -1
- package/dist/assets/{chunk-KX2RTZJC-nZdp86aN.js → chunk-KX2RTZJC-C9oTlISU.js} +1 -1
- package/dist/assets/chunk-NQ4KR5QH-CM50ygWP.js +220 -0
- package/dist/assets/{chunk-QZHKN3VN-DvUQ3mnO.js → chunk-QZHKN3VN-7dzpYeNJ.js} +1 -1
- package/dist/assets/chunk-WL4C6EOR-Cm9nQrsr.js +189 -0
- package/dist/assets/classDiagram-VBA2DB6C-d5TeKFM4.js +1 -0
- package/dist/assets/classDiagram-v2-RAHNMMFH-d5TeKFM4.js +1 -0
- package/dist/assets/clone-CinxIlEu.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-Ccp_p0JZ.js +1 -0
- package/dist/assets/cytoscape.esm-2ZfV8NB5.js +331 -0
- package/dist/assets/{dagre-KLK3FWXG-CHYIvW47.js → dagre-KLK3FWXG-fBwTLUp9.js} +1 -1
- package/dist/assets/diagram-E7M64L7V-CeNVmFUp.js +24 -0
- package/dist/assets/{diagram-IFDJBPK2-Dzsiln_C.js → diagram-IFDJBPK2-CtavyLGa.js} +1 -1
- package/dist/assets/{diagram-P4PSJMXO-DKnGbUpE.js → diagram-P4PSJMXO-CpQTjQwc.js} +1 -1
- package/dist/assets/erDiagram-INFDFZHY-B8R5vwhd.js +70 -0
- package/dist/assets/{flowDiagram-PKNHOUZH-BAZ2-jKp.js → flowDiagram-PKNHOUZH-BvkVVwIQ.js} +4 -4
- package/dist/assets/ganttDiagram-A5KZAMGK-DOu3hSNa.js +292 -0
- package/dist/assets/{gitGraphDiagram-K3NZZRJ6-BflpyjGy.js → gitGraphDiagram-K3NZZRJ6-C7zT67YE.js} +1 -1
- package/dist/assets/{graph-suelaXFh.js → graph-D11wiwHo.js} +1 -1
- package/dist/assets/highlighted-body-TPN3WLV5-Babpthg-.js +1 -0
- package/dist/assets/index-DFxzgWoO.js +2 -0
- package/dist/assets/index-YCFGDVKw.css +1 -0
- package/dist/assets/{infoDiagram-LFFYTUFH-pfD1FA3p.js → infoDiagram-LFFYTUFH-BmA7IpQG.js} +1 -1
- package/dist/assets/ishikawaDiagram-PHBUUO56-BEquZd3E.js +70 -0
- package/dist/assets/journeyDiagram-4ABVD52K-BfemGz7f.js +139 -0
- package/dist/assets/{kanban-definition-K7BYSVSG-FWinmur1.js → kanban-definition-K7BYSVSG-CWja3mln.js} +5 -5
- package/dist/assets/{layout-vcz43XvZ.js → layout-BLUNf-PJ.js} +1 -1
- package/dist/assets/{linear-le4gc0vx.js → linear-DukIV_Xv.js} +1 -1
- package/dist/assets/mermaid-O7DHMXV3-SgtM28qI.js +1038 -0
- package/dist/assets/mindmap-definition-YRQLILUH-4UjqXITU.js +68 -0
- package/dist/assets/{pieDiagram-SKSYHLDU-C7PKDh3b.js → pieDiagram-SKSYHLDU-8AxqJd0M.js} +2 -2
- package/dist/assets/quadrantDiagram-337W2JSQ-D60m8V8r.js +7 -0
- package/dist/assets/requirementDiagram-Z7DCOOCP-zqh9jBVf.js +73 -0
- package/dist/assets/{sankeyDiagram-WA2Y5GQK-4gulcOP4.js → sankeyDiagram-WA2Y5GQK-CDZILTLI.js} +3 -3
- package/dist/assets/sequenceDiagram-2WXFIKYE-7BReFd0L.js +145 -0
- package/dist/assets/{stateDiagram-RAJIS63D-CB4Vl7qM.js → stateDiagram-RAJIS63D-HPTVdIG4.js} +1 -1
- package/dist/assets/stateDiagram-v2-FVOUBMTO-DTUf5_gC.js +1 -0
- package/dist/assets/timeline-definition-YZTLITO2-CTVllFgr.js +61 -0
- package/dist/assets/{treemap-KZPCXAKY-DZSEE6Hz.js → treemap-KZPCXAKY-BtyxboJZ.js} +58 -58
- package/dist/assets/vendor-codemirror-Dz7_EqNA.js +39 -0
- package/dist/assets/vendor-react-Cpt6D04s.js +59 -0
- package/dist/assets/vendor-xterm-DfaPXD3y.js +66 -0
- package/dist/assets/{vennDiagram-LZ73GAT5-8E_G06fI.js → vennDiagram-LZ73GAT5-D96ZI6Mg.js} +4 -4
- package/dist/assets/xychartDiagram-JWTSCODW-eRk-39YO.js +7 -0
- package/dist/index.html +4 -4
- package/package.json +34 -33
- package/server/acp-runtime/client.js +82 -15
- package/server/acp-runtime/index.js +5 -16
- package/server/channels/runtime/AgentRuntimeAdapter.js +1 -10
- package/server/claude-sdk.js +1 -3
- package/server/cli.js +136 -0
- package/server/gemini-cli.js +1 -3
- package/server/index.js +86 -14
- package/server/openai-codex.js +1 -3
- package/server/opencode-cli.js +1 -3
- package/server/projects.js +128 -85
- package/server/routes/cc-connect.js +1131 -0
- package/server/routes/cli-auth.js +1 -73
- package/server/routes/commands.js +4 -9
- package/server/routes/projects.js +45 -24
- package/server/session-core/providerDiscovery.js +8 -3
- package/server/utils/ccConnectManager.js +390 -0
- package/server/utils/ccConnectState.js +575 -0
- package/server/utils/resolveCommandPath.js +71 -0
- package/server/utils/workspaceRoots.js +154 -0
- package/dist/assets/App-BWSqiXAT.js +0 -220
- package/dist/assets/App-DrlLKa8f.css +0 -1
- package/dist/assets/ReviewApp-nz3mbArg.js +0 -1
- package/dist/assets/architectureDiagram-2XIMDMQ5-CarjBOOv.js +0 -36
- package/dist/assets/c4Diagram-IC4MRINW-CGobwBIj.js +0 -10
- package/dist/assets/channel-DkFNxV_H.js +0 -1
- package/dist/assets/chunk-NQ4KR5QH-CMH6EDP2.js +0 -220
- package/dist/assets/chunk-WL4C6EOR-Dn7db_6t.js +0 -189
- package/dist/assets/classDiagram-VBA2DB6C-DtwCEe8S.js +0 -1
- package/dist/assets/classDiagram-v2-RAHNMMFH-DtwCEe8S.js +0 -1
- package/dist/assets/clone-C0lCEIEO.js +0 -1
- package/dist/assets/cose-bilkent-S5V4N54A-DD_nzqsz.js +0 -1
- package/dist/assets/cytoscape.esm-5J0xJHOV.js +0 -321
- package/dist/assets/diagram-E7M64L7V-TVdvHtGc.js +0 -24
- package/dist/assets/erDiagram-INFDFZHY-5Kw0bByo.js +0 -70
- package/dist/assets/ganttDiagram-A5KZAMGK-CsADFkcq.js +0 -292
- package/dist/assets/highlighted-body-OFNGDK62-CZrBMazC.js +0 -1
- package/dist/assets/index-B01NxbUv.css +0 -1
- package/dist/assets/index-DW5pGgQ_.js +0 -2
- package/dist/assets/ishikawaDiagram-PHBUUO56-ndm9snwO.js +0 -70
- package/dist/assets/journeyDiagram-4ABVD52K-HgF2t7z5.js +0 -139
- package/dist/assets/mermaid-GHXKKRXX-CK8m3lad.js +0 -870
- package/dist/assets/mindmap-definition-YRQLILUH-CNq9SKj4.js +0 -68
- package/dist/assets/quadrantDiagram-337W2JSQ-B7FnztNO.js +0 -7
- package/dist/assets/requirementDiagram-Z7DCOOCP-Bl_BM2Th.js +0 -73
- package/dist/assets/sequenceDiagram-2WXFIKYE-VEuJDwyJ.js +0 -145
- package/dist/assets/stateDiagram-v2-FVOUBMTO-C85ucl39.js +0 -1
- package/dist/assets/timeline-definition-YZTLITO2-BPGKhi7f.js +0 -61
- package/dist/assets/vendor-codemirror-CyOKkaQZ.js +0 -31
- package/dist/assets/vendor-react-CP4yFTs7.js +0 -8
- package/dist/assets/vendor-xterm-DfcmCpbH.js +0 -66
- package/dist/assets/xychartDiagram-JWTSCODW-CbBk50-O.js +0 -7
- package/server/acp-runtime/client.test.js +0 -688
- package/server/acp-runtime/session-store.test.js +0 -89
- package/server/cli.test.js +0 -76
- package/server/external-agent/service.test.js +0 -53
- package/server/external-agent/ws.test.js +0 -289
- 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;
|