@szc-ft/mcp-szcd-client 0.32.1 → 0.34.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@szc-ft/mcp-szcd-client",
3
- "version": "0.32.1",
3
+ "version": "0.34.0",
4
4
  "description": "MCP client for szcd component library - auto-configures AI coding tools with MCP server, skills, agents and commands",
5
5
  "keywords": [
6
6
  "mcp",
@@ -22,6 +22,7 @@
22
22
  "bin": {
23
23
  "szcd-mcp-proxy": "mcp-proxy.js",
24
24
  "szcd-mcp-setup": "scripts/postinstall.js",
25
+ "szcd-mcp-install-shortcuts": "scripts/install-dev-shortcuts.js",
25
26
  "szcd-mcp-update-url": "scripts/update-mcp-url.js",
26
27
  "szcd-mcp-coding-config": "scripts/update-coding-config.js",
27
28
  "szcd-mcp-api-config": "scripts/update-api-config.js",
@@ -32,6 +33,7 @@
32
33
  "local-browser-executor.js",
33
34
  "lib/",
34
35
  "scripts/postinstall.js",
36
+ "scripts/install-dev-shortcuts.js",
35
37
  "scripts/update-mcp-url.js",
36
38
  "scripts/update-coding-config.js",
37
39
  "scripts/update-api-config.js",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "szcd-component-helper",
3
- "version": "0.32.1",
3
+ "version": "0.34.0",
4
4
  "description": "szcd 组件库 MCP 助手 — 查询组件信息、匹配需求、生成代码",
5
5
  "mcpServers": {
6
6
  "szcd-component-helper": {
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * install-dev-shortcuts.js — 手动重跑 dev-browser 快捷方式安装的 CLI
4
+ *
5
+ * 用途:postinstall 静默失败时(特别是 Windows 中文系统、AV 拦截、ExecutionPolicy 等)
6
+ * 用户跑 `npx szcd-mcp-install-shortcuts` 单独重试并看到完整错误信息。
7
+ *
8
+ * 与 postinstall 调用同一个 setupDevBrowserShortcut;本 CLI 区别在于:
9
+ * 1. 进程退出码:失败时 exit 1(postinstall 是 exit 0 不阻塞 npm install)
10
+ * 2. 输出更详细:直接传入 process 的 stdout / stderr,不被 npm install 折叠
11
+ * 3. 文末汇总:成功/失败计数 + 每条失败的完整错误,便于贴回 issue
12
+ */
13
+
14
+ import { setupDevBrowserShortcut } from "./lib/dev-browser-shortcut.js";
15
+
16
+ console.log("============================================================");
17
+ console.log(" szcd-mcp dev-browser DevSession shortcut installer");
18
+ console.log("============================================================");
19
+ console.log(`Platform: ${process.platform}`);
20
+ console.log(`Node: ${process.version}`);
21
+ console.log("");
22
+
23
+ const result = setupDevBrowserShortcut({});
24
+
25
+ console.log("");
26
+ console.log("============================================================");
27
+ console.log(" Summary");
28
+ console.log("============================================================");
29
+ if (result.skipped) {
30
+ console.log("⚠️ No Chrome/Edge detected on this machine.");
31
+ console.log(" Install Google Chrome or Microsoft Edge, then re-run this command.");
32
+ process.exit(1);
33
+ }
34
+
35
+ const okCount = result.results.filter(r => r.ok).length;
36
+ const total = result.results.length;
37
+ console.log(`Installed: ${okCount}/${total}`);
38
+ for (const r of result.results) {
39
+ if (r.ok) {
40
+ console.log(` ✓ ${r.name}: ${r.path}`);
41
+ } else {
42
+ console.log(` ✗ ${r.name}:`);
43
+ for (const line of String(r.error || "(unknown error)").split("\n")) {
44
+ console.log(` ${line}`);
45
+ }
46
+ }
47
+ }
48
+ console.log("");
49
+
50
+ process.exit(result.ok ? 0 : 1);
@@ -0,0 +1,260 @@
1
+ /**
2
+ * codex.js — OpenAI Codex CLI 兼容逻辑
3
+ *
4
+ * 导出 setupCodex(deps) 供 postinstall.js 调用。
5
+ *
6
+ * Codex CLI 关键事实(来源:github.com/openai/codex codex-rs/cli/src/mcp_cmd.rs +
7
+ * developers.openai.ac.cn/codex/skills + 本机 codex-cli 0.142.0 实测):
8
+ * - 安装:npm i -g @openai/codex(提供 `codex` 命令)
9
+ * - 配置文件:$CODEX_HOME/config.toml(默认 ~/.codex/config.toml),TOML 格式
10
+ * - MCP server 节段:[mcp_servers.<NAME>],扁平字段,无 transport 子表、无 type/enabled:
11
+ * · stdio:command = "...",可选 args/env
12
+ * · streamable_http:url = "...",可选 bearer_token_env_var
13
+ * CLI 通过是否存在 url/command 自动识别 transport 类型
14
+ * - 管理子命令:codex mcp { list | get | add | remove }
15
+ * - Skill 目录(用户级):$HOME/.agents/skills/<skill-name>/SKILL.md(不是 ~/.codex/skills/,
16
+ * 与 Codex CLI 自己的 home 分离;同一 .agents/skills/ 也被其它走 "open agent skills 规范"
17
+ * 的客户端共享)。Codex 还会读仓库级 .agents/skills,但 postinstall 不应碰用户仓库。
18
+ * - Skill 格式:目录形式,必含 SKILL.md(YAML frontmatter: name/description + Markdown 正文),
19
+ * 可选 scripts/、references/、assets/、agents/openai.yaml 等子目录。无需 CLI 注册,
20
+ * 放文件即生效;变更后重启 Codex。
21
+ *
22
+ * 本实现策略:
23
+ * 1. MCP:优先 spawn `codex mcp remove` 再 `codex mcp add`(CLI 自己做 TOML 合并 + 幂等)
24
+ * 2. MCP fallback:找不到 `codex` 时直接写 config.toml(扁平 TOML,与 CLI 序列化一致)
25
+ * 3. Skill:调用 deps.copySkill(...) 把 standard-skill/* 整目录拷到 ~/.agents/skills/
26
+ */
27
+
28
+ import fs from "node:fs";
29
+ import path from "node:path";
30
+ import os from "node:os";
31
+ import { execSync } from "node:child_process";
32
+ import { getSketchMcpServerUrl, ADDITIONAL_MCP_SERVERS } from "./common.js";
33
+
34
+ // ==================== 路径工具 ====================
35
+
36
+ function getHomeDir() {
37
+ if (os.platform() === "win32") {
38
+ return process.env.USERPROFILE || process.env.HOME || os.homedir();
39
+ }
40
+ return process.env.HOME || os.homedir();
41
+ }
42
+
43
+ function getCodexHome() {
44
+ return process.env.CODEX_HOME || path.join(getHomeDir(), ".codex");
45
+ }
46
+
47
+ function getCodexConfigPath() {
48
+ return path.join(getCodexHome(), "config.toml");
49
+ }
50
+
51
+ function isCodexCliAvailable() {
52
+ try {
53
+ execSync("codex --version", { stdio: "pipe", timeout: 5000 });
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+
60
+ // ==================== TOML 序列化(最小子集,够本场景用) ====================
61
+
62
+ /**
63
+ * 把单个 mcp server 渲染成 TOML(与 codex CLI 实际序列化格式一致:扁平、无 transport 子表)。
64
+ *
65
+ * stdio:
66
+ * [mcp_servers.NAME]
67
+ * command = "..."
68
+ * args = ["...", "..."]
69
+ *
70
+ * streamable_http:
71
+ * [mcp_servers.NAME]
72
+ * url = "https://..."
73
+ *
74
+ * 注:codex CLI 0.142 实测产物不写 `type`、`enabled`、`transport`,
75
+ * 由它根据是否有 url/command 字段自动识别。手写 TOML 必须与 CLI 一致,
76
+ * 否则 CLI 下次 `mcp add/remove` 重写时会丢失字段。
77
+ */
78
+ function renderStdioServerToml(name, command, args = []) {
79
+ const lines = [
80
+ `[mcp_servers.${name}]`,
81
+ `command = ${tomlString(command)}`,
82
+ ];
83
+ if (args.length) {
84
+ lines.push(`args = [${args.map(tomlString).join(", ")}]`);
85
+ }
86
+ return lines.join("\n");
87
+ }
88
+
89
+ function renderHttpServerToml(name, url, bearerTokenEnvVar) {
90
+ const lines = [
91
+ `[mcp_servers.${name}]`,
92
+ `url = ${tomlString(url)}`,
93
+ ];
94
+ if (bearerTokenEnvVar) {
95
+ lines.push(`bearer_token_env_var = ${tomlString(bearerTokenEnvVar)}`);
96
+ }
97
+ return lines.join("\n");
98
+ }
99
+
100
+ function tomlString(s) {
101
+ // basic TOML string: 双引号 + 转义反斜杠/双引号/换行
102
+ return '"' + String(s).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n") + '"';
103
+ }
104
+
105
+ // ==================== 文件级合并(CLI 不可用时回退) ====================
106
+
107
+ /**
108
+ * 读取已有 config.toml,移除本包负责的 [mcp_servers.<name>] / [mcp_servers.<name>.transport]
109
+ * 整段(直到下一个 [...]),然后追加新的渲染结果。
110
+ *
111
+ * 不引入完整 TOML 解析器(用户其它 server / 顶层 settings 用纯文本保留,最稳)。
112
+ */
113
+ function upsertCodexConfig(configPath, ourServers) {
114
+ let existing = "";
115
+ if (fs.existsSync(configPath)) {
116
+ existing = fs.readFileSync(configPath, "utf8");
117
+ }
118
+
119
+ // 移除我们要写的每个 server 的旧 [mcp_servers.NAME] 整段(直到下一个 [...] 或文件末)
120
+ for (const s of ourServers) {
121
+ const sectionRegex = new RegExp(
122
+ `(^|\\n)\\[mcp_servers\\.${escapeRegex(s.name)}\\][^\\n]*\\n` +
123
+ `(?:(?!\\n\\[)[\\s\\S])*?(?=\\n\\[|$)`,
124
+ "g"
125
+ );
126
+ existing = existing.replace(sectionRegex, "");
127
+ }
128
+ // 收尾:折叠多余空行 / 保证末尾有换行
129
+ existing = existing.replace(/\n{3,}/g, "\n\n").replace(/^\n+/, "").trimEnd();
130
+
131
+ const newBlocks = ourServers.map(s => {
132
+ if (s.kind === "stdio") return renderStdioServerToml(s.name, s.command, s.args);
133
+ return renderHttpServerToml(s.name, s.url, s.bearerTokenEnvVar);
134
+ }).join("\n\n");
135
+
136
+ const sep = existing.length ? "\n\n" : "";
137
+ return existing + sep + newBlocks + "\n";
138
+ }
139
+
140
+ function escapeRegex(s) {
141
+ return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
142
+ }
143
+
144
+ // ==================== CLI 优先路径 ====================
145
+
146
+ function addServerViaCli(server) {
147
+ // codex mcp add NAME --url URL (HTTP)
148
+ // codex mcp add NAME [--env K=V]... -- COMMAND ARGS (stdio)
149
+ let cmd;
150
+ if (server.kind === "http") {
151
+ cmd = `codex mcp add ${shellEscape(server.name)} --url ${shellEscape(server.url)}`;
152
+ } else {
153
+ const args = (server.args || []).map(shellEscape).join(" ");
154
+ cmd = `codex mcp add ${shellEscape(server.name)} -- ${shellEscape(server.command)}${args ? " " + args : ""}`;
155
+ }
156
+ try {
157
+ // 先 remove 再 add,保证幂等(add 在已存在时会报错)
158
+ execSync(`codex mcp remove ${shellEscape(server.name)}`, { stdio: "pipe", timeout: 8000 });
159
+ } catch { /* 没有就忽略 */ }
160
+ try {
161
+ execSync(cmd, { stdio: "pipe", timeout: 10000 });
162
+ return { ok: true, via: "cli" };
163
+ } catch (e) {
164
+ return { ok: false, via: "cli", error: String(e.stderr || e.message).slice(0, 300) };
165
+ }
166
+ }
167
+
168
+ function shellEscape(s) {
169
+ // 简单版:含空格/特殊字符的串用单引号包,单引号本身转义为 '"'"'
170
+ if (/^[A-Za-z0-9_\-./:@=]+$/.test(s)) return s;
171
+ return `'${String(s).replace(/'/g, `'"'"'`)}'`;
172
+ }
173
+
174
+ // ==================== Skill 复制 ====================
175
+
176
+ function getCodexSkillsDirectory() {
177
+ return path.join(getHomeDir(), ".agents", "skills");
178
+ }
179
+
180
+ function copySkillsToCodex(deps) {
181
+ if (typeof deps.discoverSkills !== "function" || typeof deps.copySkill !== "function") {
182
+ console.log("ℹ️ deps.discoverSkills / deps.copySkill not provided — skipping Codex skill copy");
183
+ return { ok: false, skipped: true };
184
+ }
185
+ const skillsRoot = getCodexSkillsDirectory();
186
+ const skills = deps.discoverSkills();
187
+ const results = [];
188
+ for (const skill of skills) {
189
+ const skillDir = path.join(skillsRoot, skill.name);
190
+ try {
191
+ deps.ensureDirectory?.(skillDir) ?? fs.mkdirSync(skillDir, { recursive: true });
192
+ const dest = path.join(skillDir, "SKILL.md");
193
+ deps.copySkill(skill.sourcePath, dest);
194
+ console.log(`✓ Copied skill "${skill.name}" to Codex (${skillDir})`);
195
+ results.push({ name: skill.name, ok: true, path: skillDir });
196
+ } catch (e) {
197
+ console.warn(`⚠️ Failed to copy skill "${skill.name}" to Codex: ${e.message}`);
198
+ results.push({ name: skill.name, ok: false, error: e.message });
199
+ }
200
+ }
201
+ return { ok: results.every(r => r.ok), results, skillsRoot };
202
+ }
203
+
204
+ // ==================== 主入口 ====================
205
+
206
+ export function setupCodex(deps) {
207
+ // 1) 构造我们要注册的两个 server
208
+ const httpUrl = (deps.getMcpServerUrl?.() || "http://localhost:3456") + "/mcp";
209
+ const serverName = deps.getMcpServerName?.() || "szcd-component-helper";
210
+
211
+ const ourServers = [
212
+ // szcd 主 server:HTTP(与其它 IDE 一致;本地 server 无鉴权,故不带 bearer)
213
+ { kind: "http", name: serverName, url: httpUrl },
214
+ // sketch-mcp-server:stdio
215
+ ...ADDITIONAL_MCP_SERVERS.filter(s => s.type === "stdio").map(s => ({
216
+ kind: "stdio", name: s.name, command: s.command, args: s.args || [],
217
+ })),
218
+ ];
219
+
220
+ // 2) MCP 注册
221
+ const mcpResult = registerMcpServers(ourServers);
222
+
223
+ // 3) Skill 复制(独立于 MCP 注册成败,文件操作本身基本不会失败)
224
+ const skillResult = copySkillsToCodex(deps);
225
+
226
+ return { mcp: mcpResult, skill: skillResult };
227
+ }
228
+
229
+ function registerMcpServers(ourServers) {
230
+ const cliAvailable = isCodexCliAvailable();
231
+ if (cliAvailable) {
232
+ console.log("✓ codex CLI detected — registering MCP servers via `codex mcp add`");
233
+ const cliResults = [];
234
+ for (const s of ourServers) {
235
+ const r = addServerViaCli(s);
236
+ cliResults.push({ name: s.name, ...r });
237
+ if (r.ok) console.log(` ✓ codex mcp add ${s.name} (${s.kind})`);
238
+ else console.warn(` ⚠️ codex mcp add ${s.name} failed: ${r.error}`);
239
+ }
240
+ const allOk = cliResults.every(r => r.ok);
241
+ if (allOk) return { ok: true, via: "cli", servers: cliResults };
242
+ console.log("→ CLI partial failure, falling back to direct config.toml edit");
243
+ } else {
244
+ console.log("ℹ️ codex CLI not installed (you can install via `npm i -g @openai/codex`)");
245
+ console.log(" Writing config.toml directly so it works once Codex is installed.");
246
+ }
247
+
248
+ // 回退:直接写 config.toml
249
+ const configPath = getCodexConfigPath();
250
+ try {
251
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
252
+ const merged = upsertCodexConfig(configPath, ourServers);
253
+ fs.writeFileSync(configPath, merged);
254
+ console.log(`✓ Wrote Codex MCP config: ${configPath}`);
255
+ return { ok: true, via: "config-toml", configPath, servers: ourServers.map(s => s.name) };
256
+ } catch (e) {
257
+ console.warn(`⚠️ Failed to write ${configPath}: ${e.message}`);
258
+ return { ok: false, error: e.message };
259
+ }
260
+ }
@@ -19,7 +19,7 @@
19
19
  import fs from "node:fs";
20
20
  import path from "node:path";
21
21
  import os from "node:os";
22
- import { execSync } from "node:child_process";
22
+ import { execSync, spawnSync } from "node:child_process";
23
23
 
24
24
  // 与 SKILL.md 第 186-191 行保持一致
25
25
  const CDP_PORT = 9222;
@@ -117,12 +117,40 @@ function installWindows(browser) {
117
117
  ].join("; ");
118
118
  // -EncodedCommand 要求脚本块本身是 UTF-16LE base64
119
119
  const encoded = Buffer.from(psScript, "utf16le").toString("base64");
120
- try {
121
- execSync(`powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand ${encoded}`, { stdio: "ignore" });
122
- return { ok: true, path: lnkPath };
123
- } catch (e) {
124
- return { ok: false, error: e.message };
120
+
121
+ // 反馈 #18(2026-06-24, accuracy=1):原先用
122
+ // execSync('powershell ... -EncodedCommand ...', { stdio: 'ignore' })
123
+ // 在中文 Windows + 全局 npm install 场景下静默失败:execSync 经 cmd.exe /d /s /c
124
+ // 调用 PowerShell,中文 Windows PowerShell 启动期向 stderr 写 CLIXML
125
+ // (progress/information record),stdio:'ignore' 导致 stderr 写入失败,
126
+ // PowerShell 以 exit code 1 终止,CreateShortcut COM 调用未执行;
127
+ // 外层 try/catch 仅 console.warn,postinstall 整体仍成功,用户无法察觉。
128
+ //
129
+ // 修复:用 spawnSync 直接调 powershell.exe,绕开 cmd.exe;stderr/stdout 用 pipe
130
+ // 而不是 ignore,捕获真实失败信息以便排查。
131
+ const result = spawnSync(
132
+ "powershell.exe",
133
+ ["-NoProfile", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded],
134
+ { stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
135
+ );
136
+ if (result.error) {
137
+ return { ok: false, error: `spawn failed: ${result.error.message}` };
125
138
  }
139
+ if (result.status !== 0) {
140
+ const stderr = (result.stderr?.toString() || "").trim();
141
+ const stdout = (result.stdout?.toString() || "").trim();
142
+ return {
143
+ ok: false,
144
+ error: `powershell exit code ${result.status}` +
145
+ (stderr ? `\n stderr: ${stderr.slice(0, 400)}` : "") +
146
+ (stdout ? `\n stdout: ${stdout.slice(0, 200)}` : ""),
147
+ };
148
+ }
149
+ // 二次校验:文件实际存在再算成功
150
+ if (!fs.existsSync(lnkPath)) {
151
+ return { ok: false, error: `powershell exit 0 but ${lnkPath} not created` };
152
+ }
153
+ return { ok: true, path: lnkPath };
126
154
  }
127
155
 
128
156
  // 字符串 → UTF-8 bytes → base64,给 PowerShell 内联反序列化用
@@ -216,14 +244,15 @@ export function setupDevBrowserShortcut(deps = {}) {
216
244
  installLinux;
217
245
  for (const b of browsers) {
218
246
  const r = install(b);
219
- results.push({ browser: b.id, ...r });
247
+ results.push({ browser: b.id, name: b.name, ...r });
220
248
  if (r.ok) {
221
249
  console.log(`✓ Created ${b.name} DevSession shortcut: ${r.path}`);
222
250
  } else {
223
- console.warn(`⚠️ Failed to create ${b.name} DevSession shortcut: ${r.error}`);
251
+ console.warn(`⚠️ Failed to create ${b.name} DevSession shortcut:\n ${r.error}`);
224
252
  }
225
253
  }
226
254
  const okCount = results.filter(r => r.ok).length;
255
+ const failCount = results.length - okCount;
227
256
  if (okCount > 0) {
228
257
  const dirHint = platform === "win32" ? "桌面 (Desktop)"
229
258
  : platform === "darwin" ? "~/Applications/ (Launchpad / Spotlight)"
@@ -231,7 +260,16 @@ export function setupDevBrowserShortcut(deps = {}) {
231
260
  console.log(`\n💡 Double-click the "DevSession" shortcut in ${dirHint} to start a CDP-enabled browser`);
232
261
  console.log(` on port ${CDP_PORT} with an isolated profile, then re-run your local-browser-test plan.`);
233
262
  }
234
- return { ok: okCount > 0, results };
263
+ if (failCount > 0) {
264
+ // 醒目告警 + 手动重跑命令(反馈 #18 要求)
265
+ console.warn("");
266
+ console.warn("⚠️ ============================================================");
267
+ console.warn(`⚠️ ${failCount}/${results.length} dev-browser shortcut(s) failed to install.`);
268
+ console.warn("⚠️ Re-run the dedicated CLI to retry with full diagnostics:");
269
+ console.warn("⚠️ npx szcd-mcp-install-shortcuts");
270
+ console.warn("⚠️ ============================================================");
271
+ }
272
+ return { ok: okCount > 0 && failCount === 0, results, platform };
235
273
  } catch (e) {
236
274
  console.warn(`⚠️ setupDevBrowserShortcut error: ${e.message}`);
237
275
  return { ok: false, results, error: e.message };
@@ -24,6 +24,7 @@ import { setupClaudeCode } from "./lib/claude-code.js";
24
24
  import { setupOpenCode } from "./lib/opencode.js";
25
25
  import { setupQwenCode } from "./lib/qwen-code.js";
26
26
  import { setupQoder } from "./lib/qoder.js";
27
+ import { setupCodex } from "./lib/codex.js";
27
28
  import { setupDevBrowserShortcut } from "./lib/dev-browser-shortcut.js";
28
29
 
29
30
  // 递归守卫在 common.js 加载时已执行
@@ -228,6 +229,10 @@ function main() {
228
229
  console.log("\n🔧 Setting up Qoder CLI compatibility...\n");
229
230
  const qoderResult = setupQoder(deps);
230
231
 
232
+ // ---- OpenAI Codex CLI ----
233
+ console.log("\n🔧 Setting up Codex CLI compatibility...\n");
234
+ setupCodex(deps);
235
+
231
236
  // ---- Dev Browser Shortcut(local-browser-test 一键启动) ----
232
237
  console.log("\n🔧 Setting up dev-browser shortcut for local-browser-test...\n");
233
238
  setupDevBrowserShortcut(deps);