@szc-ft/mcp-szcd-client 0.19.1 → 0.20.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.
- package/agents/build.js +118 -0
- package/agents/opencode-extension/agents/szcd-component-expert.md +224 -0
- package/agents/platforms.json +15 -0
- package/agents/qwen-extension/agents/szcd-component-expert.md +8 -0
- package/agents/src/szcd-component-expert.md +8 -0
- package/agents/src/tools.json +5 -0
- package/agents/szcd-component-expert.md +8 -0
- package/agents/szcd-component-expert.qoder.md +9 -0
- package/agents/szcd-component-expert.trae.md +8 -0
- package/commands/szcd-mcp-coding-config.md +4 -6
- package/lib/browser-engine.js +313 -46
- package/lib/shared-deps.js +97 -0
- package/lib/visual-compare.js +7 -4
- package/local-browser-executor.js +17 -5
- package/package.json +1 -7
- package/qwen-extension/agents/szcd-component-expert.md +8 -0
- package/qwen-extension/commands/szcd-mcp-coding-config.md +4 -6
- package/qwen-extension/qwen-extension.json +1 -1
- package/qwen-extension/skills/local-browser-test/SKILL.md +101 -5
- package/scripts/lib/opencode.js +510 -22
- package/scripts/postinstall.js +14 -3
- package/scripts/update-mcp-url.js +3 -0
- package/standard-skill/local-browser-test/SKILL.md +101 -5
package/scripts/lib/opencode.js
CHANGED
|
@@ -1,37 +1,525 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* opencode.js — OpenCode
|
|
2
|
+
* opencode.js — OpenCode 全部兼容逻辑
|
|
3
3
|
*
|
|
4
4
|
* 导出 setupOpenCode(deps) 供 postinstall.js 调用。
|
|
5
|
-
*
|
|
5
|
+
* 导出 syncMcpUrl(targetUrl, serverName) 供 update-mcp-url.js 调用。
|
|
6
|
+
*
|
|
7
|
+
* deps 需包含: { getMcpServerUrl, getMcpServerName, getApiKey, getClientConfigHeader,
|
|
8
|
+
* safeExecSync, isCommandAvailable, ensureDirectory, copyFile, writeFile,
|
|
9
|
+
* discoverSkills, PROJECT_ROOT, PACKAGE_ROOT, fs }
|
|
10
|
+
*
|
|
11
|
+
* OpenCode 配置方式(HTTP 直连模式):
|
|
12
|
+
* - 优先使用 CLI: opencode mcp add <name> --scope user --url <http-url>
|
|
13
|
+
* - 回退方案: 直接修改 ~/.config/opencode/opencode.jsonc
|
|
14
|
+
* - Skill 复制: ~/.config/opencode/skills/<serverName>/SKILL.md
|
|
15
|
+
* - Agent 复制: ~/.config/opencode/agents/<agentName>.md
|
|
16
|
+
* - Command 复制: ~/.config/opencode/commands/<commandName>.md
|
|
17
|
+
*
|
|
18
|
+
* OpenCode 目录结构:
|
|
19
|
+
* ~/.config/opencode/
|
|
20
|
+
* ├── opencode.jsonc # 用户级配置(MCP servers 等)
|
|
21
|
+
* ├── agents/ # 子代理
|
|
22
|
+
* │ └── <agentName>.md
|
|
23
|
+
* ├── skills/ # Skills
|
|
24
|
+
* │ └── <skillName>/
|
|
25
|
+
* │ └── SKILL.md
|
|
26
|
+
* └── commands/ # 自定义命令
|
|
27
|
+
* └── <commandName>.md
|
|
28
|
+
*
|
|
29
|
+
* 项目级配置(项目根目录):
|
|
30
|
+
* <project>/
|
|
31
|
+
* ├── opencode.json # 项目级 MCP 配置
|
|
32
|
+
* ├── .opencode/
|
|
33
|
+
* │ ├── agents/
|
|
34
|
+
* │ ├── skills/
|
|
35
|
+
* │ └── commands/
|
|
6
36
|
*/
|
|
7
37
|
|
|
8
38
|
import fs from "node:fs";
|
|
9
39
|
import path from "node:path";
|
|
40
|
+
import os from "node:os";
|
|
41
|
+
import { execSync } from "node:child_process";
|
|
42
|
+
import { getClientConfigHeader as _getClientConfigHeader, getApiKey as _getApiKey } from "./common.js";
|
|
10
43
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
44
|
+
// ==================== 路径工具 ====================
|
|
45
|
+
|
|
46
|
+
function getHomeDir() {
|
|
47
|
+
if (os.platform() === "win32") {
|
|
48
|
+
return process.env.USERPROFILE || process.env.HOME || os.homedir();
|
|
49
|
+
}
|
|
50
|
+
return process.env.HOME || os.homedir();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getOpenCodeConfigDir() {
|
|
54
|
+
return path.join(getHomeDir(), ".config", "opencode");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getOpenCodeConfigPath() {
|
|
58
|
+
// opencode.jsonc 优先,回退 opencode.json
|
|
59
|
+
return path.join(getOpenCodeConfigDir(), "opencode.jsonc");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getOpenCodeConfigPathJson() {
|
|
63
|
+
return path.join(getOpenCodeConfigDir(), "opencode.json");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function getOpenCodeSkillsDirectory() {
|
|
67
|
+
return path.join(getOpenCodeConfigDir(), "skills");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getOpenCodeAgentsDirectory() {
|
|
71
|
+
return path.join(getOpenCodeConfigDir(), "agents");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getOpenCodeCommandsDirectory() {
|
|
75
|
+
return path.join(getOpenCodeConfigDir(), "commands");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getOpenCodeProjectConfigPath(projectRoot) {
|
|
79
|
+
return path.join(projectRoot, "opencode.json");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function getOpenCodeProjectSkillsDirectory(projectRoot) {
|
|
83
|
+
return path.join(projectRoot, ".opencode", "skills");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function getOpenCodeProjectAgentsDirectory(projectRoot) {
|
|
87
|
+
return path.join(projectRoot, ".opencode", "agents");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getOpenCodeProjectCommandsDirectory(projectRoot) {
|
|
91
|
+
return path.join(projectRoot, ".opencode", "commands");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ==================== JSON 读写 ====================
|
|
95
|
+
|
|
96
|
+
function readJsonFile(filePath) {
|
|
97
|
+
if (!fs.existsSync(filePath)) return {};
|
|
98
|
+
try {
|
|
99
|
+
const content = fs.readFileSync(filePath, "utf8");
|
|
100
|
+
// jsonc 支持:简单去除注释(// 和 /* */)
|
|
101
|
+
const cleaned = content
|
|
102
|
+
.replace(/\/\*[\s\S]*?\*\//g, "")
|
|
103
|
+
.replace(/\/\/.*$/gm, "")
|
|
104
|
+
.replace(/,(\s*[}\]])/g, "$1")
|
|
105
|
+
.trim();
|
|
106
|
+
return JSON.parse(cleaned);
|
|
107
|
+
} catch {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function writeJsonFile(filePath, data) {
|
|
113
|
+
const dir = path.dirname(filePath);
|
|
114
|
+
if (!fs.existsSync(dir)) {
|
|
115
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
116
|
+
}
|
|
117
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ==================== MCP 配置 ====================
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 方式1: 通过 CLI 配置 MCP 服务器(推荐)
|
|
124
|
+
* opencode mcp add <name> --scope user --url <http-url>
|
|
125
|
+
*/
|
|
126
|
+
function createOpenCodeUserConfig(deps) {
|
|
127
|
+
if (deps.isCommandAvailable("opencode")) {
|
|
128
|
+
const mcpUrl = `${deps.getMcpServerUrl()}/mcp`;
|
|
129
|
+
const serverName = deps.getMcpServerName();
|
|
130
|
+
|
|
131
|
+
const result = deps.safeExecSync(`opencode mcp add ${serverName} --scope user --url ${mcpUrl}`);
|
|
132
|
+
if (result === true) {
|
|
133
|
+
console.log(`✓ OpenCode user MCP server added via CLI: ${serverName} (${mcpUrl})`);
|
|
134
|
+
} else if (result === "already_exists") {
|
|
135
|
+
console.log(`✓ OpenCode user MCP server already configured: ${serverName} (${mcpUrl})`);
|
|
136
|
+
} else {
|
|
137
|
+
console.warn(`⚠️ Failed to configure OpenCode MCP server via CLI (config file will be updated directly)`);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
console.log("⏭️ Skipping OpenCode CLI config: opencode command not found");
|
|
141
|
+
}
|
|
14
142
|
|
|
15
|
-
|
|
16
|
-
|
|
143
|
+
syncOpenCodeConfig(deps);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 方式2: 直接文件操作同步 ~/.config/opencode/opencode.jsonc
|
|
148
|
+
*/
|
|
149
|
+
function syncOpenCodeConfig(deps) {
|
|
150
|
+
const configPath = getOpenCodeConfigPath();
|
|
151
|
+
const fallbackPath = getOpenCodeConfigPathJson();
|
|
152
|
+
const mcpUrl = `${deps.getMcpServerUrl()}/mcp`;
|
|
153
|
+
const serverName = deps.getMcpServerName();
|
|
154
|
+
|
|
155
|
+
// 优先使用 jsonc,不存在则使用 json
|
|
156
|
+
let actualPath = fs.existsSync(configPath) ? configPath : fallbackPath;
|
|
157
|
+
|
|
158
|
+
let config = {};
|
|
159
|
+
if (fs.existsSync(actualPath)) {
|
|
160
|
+
config = readJsonFile(actualPath);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!config.mcp) config.mcp = {};
|
|
164
|
+
|
|
165
|
+
const current = config.mcp[serverName];
|
|
166
|
+
const apiKey = deps.getApiKey ? deps.getApiKey() : "";
|
|
167
|
+
const currentHeaders = current && current.headers ? current.headers : {};
|
|
168
|
+
const needsApiKeyUpdate = apiKey && (!current || currentHeaders["Authorization"] !== `Bearer ${apiKey}`);
|
|
169
|
+
|
|
170
|
+
if (current && current.type === "remote" && current.url === mcpUrl && !needsApiKeyUpdate) {
|
|
171
|
+
console.log(`✓ OpenCode ${path.basename(actualPath)} already up-to-date: ${mcpUrl}`);
|
|
17
172
|
return;
|
|
18
173
|
}
|
|
19
174
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
command: ["npx", "szcd-mcp-proxy"],
|
|
28
|
-
environment: {
|
|
29
|
-
MCP_SERVER_URL: deps.getMcpServerUrl(),
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
},
|
|
175
|
+
const headers = {};
|
|
176
|
+
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
177
|
+
|
|
178
|
+
config.mcp[serverName] = {
|
|
179
|
+
type: "remote",
|
|
180
|
+
url: mcpUrl,
|
|
181
|
+
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
33
182
|
};
|
|
34
183
|
|
|
35
|
-
|
|
36
|
-
|
|
184
|
+
// 确保 $schema 存在
|
|
185
|
+
if (!config.$schema) {
|
|
186
|
+
config.$schema = "https://opencode.ai/config.json";
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
writeJsonFile(actualPath, config);
|
|
190
|
+
console.log(`✓ Updated OpenCode ${path.basename(actualPath)}: ${mcpUrl}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 项目级 MCP 配置
|
|
195
|
+
*/
|
|
196
|
+
function createOpenCodeProjectConfig(deps) {
|
|
197
|
+
if (!deps.PROJECT_ROOT || !fs.existsSync(deps.PROJECT_ROOT)) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const projectConfigPath = getOpenCodeProjectConfigPath(deps.PROJECT_ROOT);
|
|
202
|
+
const mcpUrl = `${deps.getMcpServerUrl()}/mcp`;
|
|
203
|
+
const serverName = deps.getMcpServerName();
|
|
204
|
+
|
|
205
|
+
// 如果项目已有 opencode.json,检查是否需要更新
|
|
206
|
+
let config = {};
|
|
207
|
+
if (fs.existsSync(projectConfigPath)) {
|
|
208
|
+
config = readJsonFile(projectConfigPath);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!config.mcp) config.mcp = {};
|
|
212
|
+
|
|
213
|
+
const current = config.mcp[serverName];
|
|
214
|
+
const apiKey = deps.getApiKey ? deps.getApiKey() : "";
|
|
215
|
+
const currentHeaders = current && current.headers ? current.headers : {};
|
|
216
|
+
const needsUpdate = !current || current.type !== "remote" || current.url !== mcpUrl || (apiKey && currentHeaders["Authorization"] !== `Bearer ${apiKey}`);
|
|
217
|
+
|
|
218
|
+
if (!needsUpdate) {
|
|
219
|
+
console.log(`✓ OpenCode project opencode.json already up-to-date: ${mcpUrl}`);
|
|
220
|
+
return true;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const headers = {};
|
|
224
|
+
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
225
|
+
|
|
226
|
+
config.mcp[serverName] = {
|
|
227
|
+
type: "remote",
|
|
228
|
+
url: mcpUrl,
|
|
229
|
+
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
if (!config.$schema) {
|
|
233
|
+
config.$schema = "https://opencode.ai/config.json";
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
writeJsonFile(projectConfigPath, config);
|
|
237
|
+
console.log(`✓ Created/Updated OpenCode project config: ${projectConfigPath}`);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ==================== Skill 复制 ====================
|
|
242
|
+
|
|
243
|
+
function copySkillToOpenCode(deps, isProjectLevel = false) {
|
|
244
|
+
let openCodeSkillsDir;
|
|
245
|
+
|
|
246
|
+
if (isProjectLevel) {
|
|
247
|
+
openCodeSkillsDir = getOpenCodeProjectSkillsDirectory(deps.PROJECT_ROOT);
|
|
248
|
+
} else {
|
|
249
|
+
openCodeSkillsDir = getOpenCodeSkillsDirectory();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const skills = deps.discoverSkills();
|
|
253
|
+
let allOk = true;
|
|
254
|
+
|
|
255
|
+
for (const skill of skills) {
|
|
256
|
+
const skillDir = path.join(openCodeSkillsDir, skill.name);
|
|
257
|
+
try {
|
|
258
|
+
deps.ensureDirectory(skillDir);
|
|
259
|
+
const skillDest = path.join(skillDir, "SKILL.md");
|
|
260
|
+
deps.copyFile(skill.sourcePath, skillDest);
|
|
261
|
+
console.log(`✓ Copied skill "${skill.name}" to OpenCode ${isProjectLevel ? 'project' : 'user'} directory: ${skillDir}`);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.log(`⚠️ Failed to copy skill "${skill.name}" to OpenCode ${isProjectLevel ? 'project' : 'user'} directory: ${error.message}`);
|
|
264
|
+
allOk = false;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return allOk;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ==================== Agent 安装 ====================
|
|
272
|
+
|
|
273
|
+
function copyAgentToOpenCode(deps, isProjectLevel = false) {
|
|
274
|
+
// 优先使用构建产物,回退到 agents/ 源目录
|
|
275
|
+
const agentsSourceDir = path.join(deps.PACKAGE_ROOT, "opencode-extension", "agents");
|
|
276
|
+
const fallbackSourceDir = path.join(deps.PACKAGE_ROOT, "agents");
|
|
277
|
+
|
|
278
|
+
let actualSourceDir;
|
|
279
|
+
if (fs.existsSync(agentsSourceDir)) {
|
|
280
|
+
actualSourceDir = agentsSourceDir;
|
|
281
|
+
} else if (fs.existsSync(fallbackSourceDir)) {
|
|
282
|
+
actualSourceDir = fallbackSourceDir;
|
|
283
|
+
} else {
|
|
284
|
+
console.log("⏭️ Skipping agent install: agents source directory not found");
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let openCodeAgentsDir;
|
|
289
|
+
if (isProjectLevel) {
|
|
290
|
+
openCodeAgentsDir = getOpenCodeProjectAgentsDirectory(deps.PROJECT_ROOT);
|
|
291
|
+
} else {
|
|
292
|
+
openCodeAgentsDir = getOpenCodeAgentsDirectory();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 只复制 OpenCode 构建产物中的 agent 文件
|
|
296
|
+
const agentFiles = fs.readdirSync(actualSourceDir).filter(f => f.endsWith(".md"));
|
|
297
|
+
let copied = 0;
|
|
298
|
+
|
|
299
|
+
for (const agentFile of agentFiles) {
|
|
300
|
+
const sourcePath = path.join(actualSourceDir, agentFile);
|
|
301
|
+
const destPath = path.join(openCodeAgentsDir, agentFile);
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
deps.ensureDirectory(openCodeAgentsDir);
|
|
305
|
+
deps.copyFile(sourcePath, destPath);
|
|
306
|
+
copied++;
|
|
307
|
+
} catch (error) {
|
|
308
|
+
console.log(`⚠️ Failed to copy agent ${agentFile}: ${error.message}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (copied > 0) {
|
|
313
|
+
console.log(`✓ Copied ${copied} agent(s) to OpenCode ${isProjectLevel ? 'project' : 'user'} directory: ${openCodeAgentsDir}`);
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// ==================== Slash Command 安装 ====================
|
|
320
|
+
|
|
321
|
+
function installOpenCodeSlashCommand(deps) {
|
|
322
|
+
const commandsSourceDir = path.join(deps.PACKAGE_ROOT, "commands");
|
|
323
|
+
if (!fs.existsSync(commandsSourceDir)) {
|
|
324
|
+
console.log("⚠️ Commands source directory not found, skipping OpenCode");
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const commandFiles = fs.readdirSync(commandsSourceDir).filter(f => f.endsWith(".md"));
|
|
329
|
+
if (commandFiles.length === 0) {
|
|
330
|
+
console.log("⚠️ No command files found, skipping OpenCode");
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// 全局命令:~/.config/opencode/commands/
|
|
335
|
+
const globalCommandsDir = getOpenCodeCommandsDirectory();
|
|
336
|
+
try {
|
|
337
|
+
deps.ensureDirectory(globalCommandsDir);
|
|
338
|
+
|
|
339
|
+
for (const file of commandFiles) {
|
|
340
|
+
const commandSource = path.join(commandsSourceDir, file);
|
|
341
|
+
const commandDest = path.join(globalCommandsDir, file);
|
|
342
|
+
|
|
343
|
+
if (fs.existsSync(commandDest)) {
|
|
344
|
+
const existingContent = fs.readFileSync(commandDest, "utf8").trim();
|
|
345
|
+
const sourceContent = fs.readFileSync(commandSource, "utf8").trim();
|
|
346
|
+
if (existingContent !== sourceContent) {
|
|
347
|
+
console.log(`⏭️ Skipping OpenCode global command ${file}: already exists (user modified)`);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
deps.copyFile(commandSource, commandDest);
|
|
353
|
+
console.log(`✓ Installed OpenCode global command: /${file.replace(".md", "")}`);
|
|
354
|
+
}
|
|
355
|
+
} catch (error) {
|
|
356
|
+
console.log(`⚠️ Failed to install OpenCode global commands: ${error.message}`);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 项目级命令暂不启用,全局命令已覆盖所有项目
|
|
360
|
+
/*
|
|
361
|
+
if (!deps.PROJECT_ROOT || !fs.existsSync(deps.PROJECT_ROOT)) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const openCodeDir = path.join(deps.PROJECT_ROOT, ".opencode");
|
|
366
|
+
const projectCommandsDir = path.join(openCodeDir, "commands");
|
|
367
|
+
|
|
368
|
+
if (fs.existsSync(openCodeDir) && !fs.statSync(openCodeDir).isDirectory()) {
|
|
369
|
+
console.log(`⚠️ Skipping OpenCode project commands: ${openCodeDir} exists but is not a directory`);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
deps.ensureDirectory(projectCommandsDir);
|
|
375
|
+
|
|
376
|
+
for (const file of commandFiles) {
|
|
377
|
+
const commandSource = path.join(commandsSourceDir, file);
|
|
378
|
+
const commandDest = path.join(projectCommandsDir, file);
|
|
379
|
+
|
|
380
|
+
if (fs.existsSync(commandDest)) {
|
|
381
|
+
const existingContent = fs.readFileSync(commandDest, "utf8").trim();
|
|
382
|
+
const sourceContent = fs.readFileSync(commandSource, "utf8").trim();
|
|
383
|
+
if (existingContent !== sourceContent) {
|
|
384
|
+
console.log(`⏭️ Skipping OpenCode project command ${file}: already exists (user modified)`);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
deps.copyFile(commandSource, commandDest);
|
|
390
|
+
console.log(`✓ Installed OpenCode project command: /${file.replace(".md", "")}`);
|
|
391
|
+
}
|
|
392
|
+
} catch (error) {
|
|
393
|
+
console.log(`⚠️ Failed to install OpenCode project commands: ${error.message}`);
|
|
394
|
+
}
|
|
395
|
+
*/
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ==================== MCP URL 同步(供 update-mcp-url.js 调用) ====================
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* 不依赖 deps 的 CLI 可用性检测
|
|
402
|
+
*/
|
|
403
|
+
function isCommandAvailable(cmd) {
|
|
404
|
+
try {
|
|
405
|
+
execSync(`which ${cmd} 2>/dev/null`, { stdio: "pipe", timeout: 3000 });
|
|
406
|
+
return true;
|
|
407
|
+
} catch {
|
|
408
|
+
return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* 不依赖 deps 的安全命令执行
|
|
414
|
+
*/
|
|
415
|
+
function safeExecSync(command) {
|
|
416
|
+
try {
|
|
417
|
+
execSync(command, { stdio: "pipe", timeout: 10000 });
|
|
418
|
+
return true;
|
|
419
|
+
} catch (e) {
|
|
420
|
+
const stderr = e.stderr?.toString() || "";
|
|
421
|
+
if (stderr.includes("already exists")) return "already_exists";
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* 同步 OpenCode 配置中的 MCP 服务器 URL
|
|
428
|
+
* @param {string} targetUrl - 新的 MCP 服务器基础 URL(如 http://localhost:3456)
|
|
429
|
+
* @param {string} serverName - MCP 服务器名称
|
|
430
|
+
*/
|
|
431
|
+
export function syncMcpUrl(targetUrl, serverName) {
|
|
432
|
+
const mcpUrl = `${targetUrl}/mcp`;
|
|
433
|
+
|
|
434
|
+
// 1. CLI 命令同步
|
|
435
|
+
if (isCommandAvailable("opencode")) {
|
|
436
|
+
safeExecSync(`opencode mcp remove ${serverName} --scope user 2>/dev/null`);
|
|
437
|
+
const result = safeExecSync(`opencode mcp add ${serverName} --scope user --url ${mcpUrl}`);
|
|
438
|
+
if (result === true || result === "already_exists") {
|
|
439
|
+
console.log(`✓ OpenCode CLI synced: ${mcpUrl}`);
|
|
440
|
+
} else {
|
|
441
|
+
console.warn(`⚠️ OpenCode CLI sync failed (config file will be updated directly)`);
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
console.log("⏭️ Skipping OpenCode CLI sync: opencode not found");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 2. 直接文件操作同步(确保最终状态正确)
|
|
448
|
+
syncOpenCodeConfigDirect(targetUrl, serverName);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* 直接文件操作同步 opencode.jsonc(独立于 deps)
|
|
453
|
+
*/
|
|
454
|
+
function syncOpenCodeConfigDirect(targetUrl, serverName) {
|
|
455
|
+
const configPath = getOpenCodeConfigPath();
|
|
456
|
+
const fallbackPath = getOpenCodeConfigPathJson();
|
|
457
|
+
const mcpUrl = `${targetUrl}/mcp`;
|
|
458
|
+
|
|
459
|
+
// 优先使用 jsonc,不存在则使用 json
|
|
460
|
+
let actualPath = fs.existsSync(configPath) ? configPath : fallbackPath;
|
|
461
|
+
|
|
462
|
+
let config = {};
|
|
463
|
+
if (fs.existsSync(actualPath)) {
|
|
464
|
+
config = readJsonFile(actualPath);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!config.mcp) config.mcp = {};
|
|
468
|
+
|
|
469
|
+
const current = config.mcp[serverName];
|
|
470
|
+
const apiKey = _getApiKey();
|
|
471
|
+
const currentHeaders = current && current.headers ? current.headers : {};
|
|
472
|
+
const needsApiKeyUpdate = apiKey && (!current || currentHeaders["Authorization"] !== `Bearer ${apiKey}`);
|
|
473
|
+
|
|
474
|
+
if (current && current.type === "remote" && current.url === mcpUrl && !needsApiKeyUpdate) {
|
|
475
|
+
console.log(`⏭️ OpenCode ${path.basename(actualPath)} already up-to-date: ${mcpUrl}`);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const headers = {};
|
|
480
|
+
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
|
481
|
+
|
|
482
|
+
config.mcp[serverName] = {
|
|
483
|
+
type: "remote",
|
|
484
|
+
url: mcpUrl,
|
|
485
|
+
...(Object.keys(headers).length > 0 ? { headers } : {}),
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
if (!config.$schema) {
|
|
489
|
+
config.$schema = "https://opencode.ai/config.json";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
writeJsonFile(actualPath, config);
|
|
493
|
+
console.log(`✓ Updated OpenCode ${path.basename(actualPath)}: ${mcpUrl}`);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ==================== 导出 ====================
|
|
497
|
+
|
|
498
|
+
export function setupOpenCode(deps) {
|
|
499
|
+
createOpenCodeUserConfig(deps);
|
|
500
|
+
copySkillToOpenCode(deps, false);
|
|
501
|
+
copyAgentToOpenCode(deps, false);
|
|
502
|
+
installOpenCodeSlashCommand(deps);
|
|
503
|
+
|
|
504
|
+
// 项目级配置暂不启用,用户级已足够
|
|
505
|
+
/*
|
|
506
|
+
let openCodeProjectInstalled = false;
|
|
507
|
+
if (deps.PROJECT_ROOT && fs.existsSync(deps.PROJECT_ROOT)) {
|
|
508
|
+
try {
|
|
509
|
+
openCodeProjectInstalled = createOpenCodeProjectConfig(deps);
|
|
510
|
+
if (openCodeProjectInstalled) {
|
|
511
|
+
copySkillToOpenCode(deps, true);
|
|
512
|
+
copyAgentToOpenCode(deps, true);
|
|
513
|
+
}
|
|
514
|
+
} catch (e) {
|
|
515
|
+
console.log(`⚠️ Failed to install OpenCode project config: ${e.message}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
*/
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
openCodeProjectInstalled: false,
|
|
522
|
+
skillsDirectory: getOpenCodeSkillsDirectory(),
|
|
523
|
+
agentsDirectory: getOpenCodeAgentsDirectory(),
|
|
524
|
+
};
|
|
37
525
|
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -82,7 +82,7 @@ function createConfigTemplate() {
|
|
|
82
82
|
|
|
83
83
|
// ==================== 成功信息 ====================
|
|
84
84
|
|
|
85
|
-
function showSuccessMessage(traeResult, claudeResult, qwenResult, qoderResult) {
|
|
85
|
+
function showSuccessMessage(traeResult, claudeResult, qwenResult, qoderResult, openCodeResult) {
|
|
86
86
|
const configPath = common.getConfigFilePath();
|
|
87
87
|
const projectRoot = common.PROJECT_ROOT;
|
|
88
88
|
|
|
@@ -97,6 +97,7 @@ function showSuccessMessage(traeResult, claudeResult, qwenResult, qoderResult) {
|
|
|
97
97
|
console.log(` Claude Code: ${path.join(claudeResult.skillsDirectory, common.getMcpServerName(), "SKILL.md")}`);
|
|
98
98
|
console.log(` Qwen Code: ${path.join(qwenResult.skillsDirectory, common.getMcpServerName(), "SKILL.md")}`);
|
|
99
99
|
console.log(` Qoder CLI: ${path.join(qoderResult.skillsDirectory, common.getMcpServerName(), "SKILL.md")}`);
|
|
100
|
+
console.log(` OpenCode: ${path.join(openCodeResult.skillsDirectory, common.getMcpServerName(), "SKILL.md")}`);
|
|
100
101
|
if (traeResult.traeProjectInstalled) {
|
|
101
102
|
console.log(` Trae Project: ${path.join(projectRoot, ".trae", "skills", common.getMcpServerName(), "SKILL.md")}`);
|
|
102
103
|
}
|
|
@@ -109,6 +110,12 @@ function showSuccessMessage(traeResult, claudeResult, qwenResult, qoderResult) {
|
|
|
109
110
|
if (qoderResult.qoderProjectInstalled) {
|
|
110
111
|
console.log(` Qoder Project: ${path.join(projectRoot, ".qoder", "skills", common.getMcpServerName(), "SKILL.md")}`);
|
|
111
112
|
}
|
|
113
|
+
// 项目级配置暂不启用
|
|
114
|
+
/*
|
|
115
|
+
if (openCodeResult.openCodeProjectInstalled) {
|
|
116
|
+
console.log(` OpenCode Project: ${path.join(projectRoot, ".opencode", "skills", common.getMcpServerName(), "SKILL.md")}`);
|
|
117
|
+
}
|
|
118
|
+
*/
|
|
112
119
|
|
|
113
120
|
console.log("\n⚙️ Configuration Files:");
|
|
114
121
|
console.log(` 1. Config file: ${configPath}`);
|
|
@@ -116,6 +123,7 @@ function showSuccessMessage(traeResult, claudeResult, qwenResult, qoderResult) {
|
|
|
116
123
|
console.log(` 3. Claude Code: via 'claude mcp add'`);
|
|
117
124
|
console.log(` 4. Qwen Code: via 'qwen extensions install'`);
|
|
118
125
|
console.log(` 5. Qoder CLI: via 'qoder mcp add'`);
|
|
126
|
+
console.log(` 6. OpenCode: via 'opencode mcp add'`);
|
|
119
127
|
|
|
120
128
|
console.log("\n📝 Edit the config file to set your MCP server URL:");
|
|
121
129
|
console.log(` "MCP_SERVER_URL": "http://YOUR_SERVER_IP:3456"`);
|
|
@@ -131,6 +139,8 @@ function showSuccessMessage(traeResult, claudeResult, qwenResult, qoderResult) {
|
|
|
131
139
|
console.log(` qwen mcp add --transport http --scope user ${common.getMcpServerName()} ${common.getMcpServerUrl()}/mcp`);
|
|
132
140
|
console.log("\n For Qoder CLI (via CLI):");
|
|
133
141
|
console.log(` qoder mcp add ${common.getMcpServerName()} -s user -- ${common.getMcpServerUrl()}/mcp`);
|
|
142
|
+
console.log("\n For OpenCode (via CLI):");
|
|
143
|
+
console.log(` opencode mcp add ${common.getMcpServerName()} --scope user --url ${common.getMcpServerUrl()}/mcp`);
|
|
134
144
|
|
|
135
145
|
console.log("\n💡 Configuration Priority:");
|
|
136
146
|
console.log(" 1. Environment variables (highest priority)");
|
|
@@ -169,7 +179,8 @@ function main() {
|
|
|
169
179
|
|
|
170
180
|
// ---- 配置文件 ----
|
|
171
181
|
createConfigTemplate();
|
|
172
|
-
|
|
182
|
+
console.log("\n🔧 Setting up OpenCode compatibility...\n");
|
|
183
|
+
const openCodeResult = setupOpenCode(deps);
|
|
173
184
|
|
|
174
185
|
// ---- Trae CLI ----
|
|
175
186
|
console.log("\n🔧 Setting up Trae CLI compatibility...\n");
|
|
@@ -187,7 +198,7 @@ function main() {
|
|
|
187
198
|
console.log("\n🔧 Setting up Qoder CLI compatibility...\n");
|
|
188
199
|
const qoderResult = setupQoder(deps);
|
|
189
200
|
|
|
190
|
-
showSuccessMessage(traeResult, claudeResult, qwenResult, qoderResult);
|
|
201
|
+
showSuccessMessage(traeResult, claudeResult, qwenResult, qoderResult, openCodeResult);
|
|
191
202
|
} catch (error) {
|
|
192
203
|
console.error("\n❌ Error during installation:", error.message);
|
|
193
204
|
console.error("Please report this issue at: https://github.com/szc-ft/szcd/issues");
|
|
@@ -32,6 +32,7 @@ import { syncMcpUrl as syncTraeIde } from "./lib/trae-ide.js";
|
|
|
32
32
|
import { syncMcpUrl as syncClaudeCode } from "./lib/claude-code.js";
|
|
33
33
|
import { syncMcpUrl as syncQwenCode } from "./lib/qwen-code.js";
|
|
34
34
|
import { syncMcpUrl as syncQoder } from "./lib/qoder.js";
|
|
35
|
+
import { syncMcpUrl as syncOpenCode } from "./lib/opencode.js";
|
|
35
36
|
|
|
36
37
|
const DEFAULT_MCP_SERVER_NAME = "szcd-component-helper";
|
|
37
38
|
const DEFAULT_MCP_SERVER_URL = "http://localhost:3456";
|
|
@@ -183,6 +184,7 @@ function main() {
|
|
|
183
184
|
syncClaudeCode(targetUrl, serverName);
|
|
184
185
|
syncQwenCode(targetUrl, serverName);
|
|
185
186
|
syncQoder(targetUrl, serverName);
|
|
187
|
+
syncOpenCode(targetUrl, serverName);
|
|
186
188
|
|
|
187
189
|
console.log(`\n✅ Done! MCP_SERVER_URL = ${targetUrl}`);
|
|
188
190
|
console.log(`\n📋 Updated configuration files:`);
|
|
@@ -192,6 +194,7 @@ function main() {
|
|
|
192
194
|
console.log(` ${path.join(getHomeDir(), ".claude.json")}`);
|
|
193
195
|
console.log(` ${path.join(getHomeDir(), ".qwen", "settings.json")}`);
|
|
194
196
|
console.log(` ${path.join(getHomeDir(), ".qoder", "settings.json")}`);
|
|
197
|
+
console.log(` ${path.join(getHomeDir(), ".config", "opencode", "opencode.jsonc")}`);
|
|
195
198
|
console.log(`\n💡 Restart your IDE/CLI for changes to take effect.\n`);
|
|
196
199
|
}
|
|
197
200
|
|