@szc-ft/mcp-szcd-client 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,426 @@
1
+ /**
2
+ * qoder.js — Qoder CLI 兼容逻辑
3
+ *
4
+ * 导出 setupQoder(deps) 供 postinstall.js 调用。
5
+ * deps 需包含: { getMcpServerUrl, getMcpServerName, safeExecSync, isCommandAvailable, ensureDirectory, copyFile, writeFile, SKILL_SOURCE, PROJECT_ROOT, PACKAGE_ROOT }
6
+ *
7
+ * Qoder CLI 配置方式(SSE 模式,与其他客户端一致):
8
+ * - 优先使用 CLI: qoder mcp add <name> -s user -- <sse-url>
9
+ * - 回退方案: 直接修改 ~/.qoder/settings.json
10
+ * - Skill 复制: ~/.qoder/skills/<serverName>/SKILL.md
11
+ * - Agent 复制: ~/.qoder/agents/<agentName>.md
12
+ * - Command 复制: ~/.qoder/commands/<commandName>.md
13
+ *
14
+ * Qoder 目录结构:
15
+ * ~/.qoder/
16
+ * ├── settings.json # 权限/Hook/MCP 配置
17
+ * ├── agents/ # 子代理
18
+ * │ └── <agentName>.md
19
+ * ├── skills/ # Skills
20
+ * │ └── <skillName>/
21
+ * │ └── SKILL.md
22
+ * ├── commands/ # 自定义命令
23
+ * │ └── <commandName>.md
24
+ * └── hooks/ # Hook 脚本
25
+ */
26
+
27
+ import fs from "node:fs";
28
+ import path from "node:path";
29
+ import os from "node:os";
30
+ import { execSync } from "node:child_process";
31
+
32
+ // ==================== 路径工具 ====================
33
+
34
+ function getHomeDir() {
35
+ if (os.platform() === "win32") {
36
+ return process.env.USERPROFILE || process.env.HOME || os.homedir();
37
+ }
38
+ return process.env.HOME || os.homedir();
39
+ }
40
+
41
+ function getQoderUserDirectory() {
42
+ const homeDir = getHomeDir();
43
+ if (!homeDir) throw new Error("Home directory not found");
44
+ return path.join(homeDir, ".qoder");
45
+ }
46
+
47
+ function getQoderSkillsDirectory() {
48
+ return path.join(getQoderUserDirectory(), "skills");
49
+ }
50
+
51
+ function getQoderAgentsDirectory() {
52
+ return path.join(getQoderUserDirectory(), "agents");
53
+ }
54
+
55
+ function getQoderCommandsDirectory() {
56
+ return path.join(getQoderUserDirectory(), "commands");
57
+ }
58
+
59
+ function getQoderSettingsPath() {
60
+ return path.join(getQoderUserDirectory(), "settings.json");
61
+ }
62
+
63
+ function getQoderProjectSettingsPath(projectRoot) {
64
+ return path.join(projectRoot, ".qoder", "settings.json");
65
+ }
66
+
67
+ // ==================== JSON 读写 ====================
68
+
69
+ function readJsonFile(filePath) {
70
+ if (!fs.existsSync(filePath)) return {};
71
+ try {
72
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
73
+ } catch {
74
+ return {};
75
+ }
76
+ }
77
+
78
+ function writeJsonFile(filePath, data) {
79
+ const dir = path.dirname(filePath);
80
+ if (!fs.existsSync(dir)) {
81
+ fs.mkdirSync(dir, { recursive: true });
82
+ }
83
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
84
+ }
85
+
86
+ // ==================== MCP 配置(SSE 模式) ====================
87
+
88
+ /**
89
+ * 方式1: 通过 CLI 配置 MCP 服务器(推荐)
90
+ * qoder mcp add <name> -s user -- <sse-url>
91
+ */
92
+ function addMcpViaCLI(deps, scope = "user") {
93
+ if (!deps.isCommandAvailable("qoder")) {
94
+ console.log(`⏭️ Skipping Qoder CLI MCP setup (${scope}): qoder command not found`);
95
+ return false;
96
+ }
97
+
98
+ const serverName = deps.getMcpServerName();
99
+ const sseUrl = `${deps.getMcpServerUrl()}/sse`;
100
+ const scopeFlag = scope === "project" ? "-s project" : "-s user";
101
+
102
+ const result = deps.safeExecSync(
103
+ `qoder mcp add ${serverName} ${scopeFlag} -- ${sseUrl}`,
104
+ );
105
+
106
+ if (result === true) {
107
+ console.log(`✓ Qoder CLI MCP server added (${scope}): ${serverName} (${sseUrl})`);
108
+ return true;
109
+ }
110
+
111
+ if (result === "already_exists") {
112
+ console.log(`✓ Qoder CLI MCP server already configured (${scope}): ${serverName}`);
113
+ return true;
114
+ }
115
+
116
+ // 尝试先移除再添加
117
+ if (
118
+ deps.safeExecSync(`qoder mcp remove ${serverName} ${scopeFlag} 2>/dev/null`) &&
119
+ deps.safeExecSync(`qoder mcp add ${serverName} ${scopeFlag} -- ${sseUrl}`)
120
+ ) {
121
+ console.log(`✓ Qoder CLI MCP server updated (${scope}): ${serverName} (${sseUrl})`);
122
+ return true;
123
+ }
124
+
125
+ console.warn(`⚠️ Failed to configure Qoder CLI MCP server (settings.json will be updated directly)`);
126
+ return false;
127
+ }
128
+
129
+ /**
130
+ * 方式2: 直接修改 ~/.qoder/settings.json
131
+ * 在 mcpServers 中添加 SSE 类型配置
132
+ */
133
+ function syncQoderSettings(deps) {
134
+ const settingsPath = getQoderSettingsPath();
135
+ const serverName = deps.getMcpServerName();
136
+ const sseUrl = `${deps.getMcpServerUrl()}/sse`;
137
+
138
+ let settings = readJsonFile(settingsPath);
139
+
140
+ if (!settings.mcpServers) settings.mcpServers = {};
141
+
142
+ const desiredConfig = {
143
+ type: "sse",
144
+ url: sseUrl,
145
+ };
146
+
147
+ const current = settings.mcpServers[serverName];
148
+ if (current && current.type === "sse" && current.url === sseUrl) {
149
+ console.log(`✓ Qoder ~/.qoder/settings.json already up-to-date: ${serverName} (${sseUrl})`);
150
+ return;
151
+ }
152
+
153
+ settings.mcpServers[serverName] = desiredConfig;
154
+
155
+ writeJsonFile(settingsPath, settings);
156
+ console.log(`✓ Updated Qoder ~/.qoder/settings.json: ${serverName} (${sseUrl})`);
157
+ }
158
+
159
+ // ==================== Skill 复制 ====================
160
+
161
+ function copySkillToQoder(deps, isProjectLevel = false) {
162
+ const skillName = deps.getMcpServerName();
163
+ let qoderSkillsDir;
164
+
165
+ if (isProjectLevel) {
166
+ qoderSkillsDir = path.join(deps.PROJECT_ROOT, ".qoder", "skills");
167
+ } else {
168
+ qoderSkillsDir = getQoderSkillsDirectory();
169
+ }
170
+
171
+ const skillDir = path.join(qoderSkillsDir, skillName);
172
+
173
+ try {
174
+ deps.ensureDirectory(skillDir);
175
+ const skillDest = path.join(skillDir, "SKILL.md");
176
+ deps.copyFile(deps.SKILL_SOURCE, skillDest);
177
+ console.log(`✓ Copied skill to Qoder ${isProjectLevel ? "project" : "user"} directory: ${skillDir}`);
178
+ return true;
179
+ } catch (error) {
180
+ console.log(`⚠️ Failed to copy skill to Qoder ${isProjectLevel ? "project" : "user"} directory: ${error.message}`);
181
+ return false;
182
+ }
183
+ }
184
+
185
+ // ==================== Agent 安装 ====================
186
+
187
+ function copyAgentsToQoder(deps, isProjectLevel = false) {
188
+ const agentsSourceDir = path.join(deps.PACKAGE_ROOT, "agents");
189
+ if (!fs.existsSync(agentsSourceDir)) {
190
+ console.log("⏭️ Skipping agent install: agents source directory not found");
191
+ return false;
192
+ }
193
+
194
+ let qoderAgentsDir;
195
+ if (isProjectLevel) {
196
+ qoderAgentsDir = path.join(deps.PROJECT_ROOT, ".qoder", "agents");
197
+ } else {
198
+ qoderAgentsDir = getQoderAgentsDirectory();
199
+ }
200
+
201
+ // Qoder 只复制不含 .trae. 的 .md 文件(原生格式,同 Claude Code)
202
+ const agentFiles = fs.readdirSync(agentsSourceDir).filter(
203
+ (f) => f.endsWith(".md") && !f.includes(".trae."),
204
+ );
205
+
206
+ let copied = 0;
207
+ for (const agentFile of agentFiles) {
208
+ const sourcePath = path.join(agentsSourceDir, agentFile);
209
+ const destPath = path.join(qoderAgentsDir, agentFile);
210
+
211
+ try {
212
+ deps.ensureDirectory(qoderAgentsDir);
213
+ deps.copyFile(sourcePath, destPath);
214
+ copied++;
215
+ } catch (error) {
216
+ console.log(`⚠️ Failed to copy agent ${agentFile}: ${error.message}`);
217
+ }
218
+ }
219
+
220
+ if (copied > 0) {
221
+ console.log(`✓ Copied ${copied} agent(s) to Qoder ${isProjectLevel ? "project" : "user"} directory: ${qoderAgentsDir}`);
222
+ return true;
223
+ }
224
+ return false;
225
+ }
226
+
227
+ // ==================== Command 安装 ====================
228
+
229
+ function copyCommandsToQoder(deps, isProjectLevel = false) {
230
+ const commandsSourceDir = path.join(deps.PACKAGE_ROOT, "commands");
231
+ if (!fs.existsSync(commandsSourceDir)) {
232
+ console.log("⏭️ Skipping command install: commands source directory not found");
233
+ return false;
234
+ }
235
+
236
+ let qoderCommandsDir;
237
+ if (isProjectLevel) {
238
+ qoderCommandsDir = path.join(deps.PROJECT_ROOT, ".qoder", "commands");
239
+ } else {
240
+ qoderCommandsDir = getQoderCommandsDirectory();
241
+ }
242
+
243
+ const commandFiles = fs.readdirSync(commandsSourceDir).filter((f) => f.endsWith(".md"));
244
+ let copied = 0;
245
+
246
+ for (const cmdFile of commandFiles) {
247
+ const sourcePath = path.join(commandsSourceDir, cmdFile);
248
+ const destPath = path.join(qoderCommandsDir, cmdFile);
249
+
250
+ try {
251
+ deps.ensureDirectory(qoderCommandsDir);
252
+
253
+ // 如果目标已存在且内容不同,跳过(用户可能已修改)
254
+ if (fs.existsSync(destPath)) {
255
+ const existingContent = fs.readFileSync(destPath, "utf8").trim();
256
+ const sourceContent = fs.readFileSync(sourcePath, "utf8").trim();
257
+ if (existingContent !== sourceContent) {
258
+ console.log(`⏭️ Skipping Qoder command ${cmdFile}: already exists (user modified)`);
259
+ continue;
260
+ }
261
+ }
262
+
263
+ deps.copyFile(sourcePath, destPath);
264
+ copied++;
265
+ } catch (error) {
266
+ console.log(`⚠️ Failed to copy command ${cmdFile}: ${error.message}`);
267
+ }
268
+ }
269
+
270
+ if (copied > 0) {
271
+ console.log(`✓ Copied ${copied} command(s) to Qoder ${isProjectLevel ? "project" : "user"} directory: ${qoderCommandsDir}`);
272
+ return true;
273
+ }
274
+ return false;
275
+ }
276
+
277
+ // ==================== 项目级配置 ====================
278
+
279
+ function setupQoderProjectConfig(deps) {
280
+ const serverName = deps.getMcpServerName();
281
+ const sseUrl = `${deps.getMcpServerUrl()}/sse`;
282
+
283
+ // 优先 CLI
284
+ if (addMcpViaCLI(deps, "project")) {
285
+ return true;
286
+ }
287
+
288
+ // 回退到直接修改项目级 .qoder/settings.json
289
+ const projectSettingsPath = getQoderProjectSettingsPath(deps.PROJECT_ROOT);
290
+ const projectQoderDir = path.dirname(projectSettingsPath);
291
+
292
+ if (!fs.existsSync(projectQoderDir)) {
293
+ console.log("⏭️ Skipping Qoder project settings: .qoder/ directory not found in project");
294
+ return false;
295
+ }
296
+
297
+ let settings = readJsonFile(projectSettingsPath);
298
+ if (!settings.mcpServers) settings.mcpServers = {};
299
+
300
+ settings.mcpServers[serverName] = {
301
+ type: "sse",
302
+ url: sseUrl,
303
+ };
304
+
305
+ writeJsonFile(projectSettingsPath, settings);
306
+ console.log(`✓ Updated Qoder project .qoder/settings.json: ${serverName} (${sseUrl})`);
307
+ return true;
308
+ }
309
+
310
+ // ==================== MCP URL 同步(供 update-mcp-url.js 调用) ====================
311
+
312
+ /**
313
+ * 不依赖 deps 的 CLI 可用性检测
314
+ */
315
+ function isCommandAvailable(cmd) {
316
+ try {
317
+ execSync(`which ${cmd} 2>/dev/null`, { stdio: "pipe", timeout: 3000 });
318
+ return true;
319
+ } catch {
320
+ return false;
321
+ }
322
+ }
323
+
324
+ /**
325
+ * 不依赖 deps 的安全命令执行
326
+ */
327
+ function safeExecSync(command) {
328
+ try {
329
+ execSync(command, { stdio: "pipe", timeout: 10000 });
330
+ return true;
331
+ } catch (e) {
332
+ const stderr = e.stderr?.toString() || "";
333
+ if (stderr.includes("already exists")) return "already_exists";
334
+ return false;
335
+ }
336
+ }
337
+
338
+ /**
339
+ * 同步 Qoder 配置中的 MCP 服务器 URL
340
+ * @param {string} targetUrl - 新的 MCP 服务器基础 URL(如 http://localhost:3456)
341
+ * @param {string} serverName - MCP 服务器名称
342
+ */
343
+ export function syncMcpUrl(targetUrl, serverName) {
344
+ const sseUrl = `${targetUrl}/sse`;
345
+
346
+ // 1. CLI 命令同步
347
+ if (isCommandAvailable("qoder")) {
348
+ safeExecSync(`qoder mcp remove ${serverName} -s user 2>/dev/null`);
349
+ const result = safeExecSync(
350
+ `qoder mcp add ${serverName} -s user -- ${sseUrl}`,
351
+ );
352
+ if (result === true || result === "already_exists") {
353
+ console.log(`✓ Qoder CLI synced: ${serverName} (${sseUrl})`);
354
+ } else {
355
+ console.warn(`⚠️ Qoder CLI sync failed (settings.json will be updated directly)`);
356
+ }
357
+ } else {
358
+ console.log("⏭️ Skipping Qoder CLI sync: qoder not found");
359
+ }
360
+
361
+ // 2. 直接文件操作同步 ~/.qoder/settings.json
362
+ syncQoderSettingsDirect(targetUrl, serverName);
363
+ }
364
+
365
+ /**
366
+ * 直接文件操作同步 ~/.qoder/settings.json(独立于 deps)
367
+ */
368
+ function syncQoderSettingsDirect(targetUrl, serverName) {
369
+ const settingsPath = getQoderSettingsPath();
370
+ const sseUrl = `${targetUrl}/sse`;
371
+
372
+ let settings = readJsonFile(settingsPath);
373
+
374
+ if (!settings.mcpServers) settings.mcpServers = {};
375
+
376
+ const current = settings.mcpServers[serverName];
377
+ if (current && current.type === "sse" && current.url === sseUrl) {
378
+ console.log(`⏭️ Qoder ~/.qoder/settings.json already up-to-date: ${serverName} (${sseUrl})`);
379
+ return;
380
+ }
381
+
382
+ settings.mcpServers[serverName] = {
383
+ type: "sse",
384
+ url: sseUrl,
385
+ };
386
+
387
+ writeJsonFile(settingsPath, settings);
388
+ console.log(`✓ Updated Qoder ~/.qoder/settings.json: ${serverName} (${sseUrl})`);
389
+ }
390
+
391
+ // ==================== 导出 ====================
392
+
393
+ export function setupQoder(deps) {
394
+ console.log("\n📦 Setting up Qoder CLI compatibility...\n");
395
+
396
+ // ---- MCP 配置(SSE 模式) ----
397
+ addMcpViaCLI(deps, "user");
398
+ syncQoderSettings(deps);
399
+
400
+ // ---- 用户级安装 ----
401
+ copySkillToQoder(deps, false);
402
+ copyAgentsToQoder(deps, false);
403
+ copyCommandsToQoder(deps, false);
404
+
405
+ // ---- 项目级安装(暂不启用,用户级足够) ----
406
+ // let qoderProjectInstalled = false;
407
+ // if (deps.PROJECT_ROOT && fs.existsSync(deps.PROJECT_ROOT)) {
408
+ // try {
409
+ // qoderProjectInstalled = setupQoderProjectConfig(deps);
410
+ // if (qoderProjectInstalled) {
411
+ // copySkillToQoder(deps, true);
412
+ // copyAgentsToQoder(deps, true);
413
+ // copyCommandsToQoder(deps, true);
414
+ // }
415
+ // } catch (e) {
416
+ // console.log(`⚠️ Failed to install Qoder project config: ${e.message}`);
417
+ // }
418
+ // }
419
+
420
+ return {
421
+ qoderProjectInstalled: false,
422
+ skillsDirectory: getQoderSkillsDirectory(),
423
+ agentsDirectory: getQoderAgentsDirectory(),
424
+ commandsDirectory: getQoderCommandsDirectory(),
425
+ };
426
+ }