rush-ai 0.18.1 → 0.19.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/README.md CHANGED
@@ -124,7 +124,7 @@ rush-ai task status <id> --json
124
124
  - **多环境 profile**:在不同 Rush 环境之间切换
125
125
  - **shell 补全**:bash / zsh / fish
126
126
  - **CI 友好**:`--json` 输出、`--ci` 模式、出错返回非 0 退出码
127
- - **插件分发**:`marketplace` + `plugin install` 一条命令把 Rush 生态的 skill / command / rule / MCP 装到 Claude Code、Codex、Cursor 三家 IDEmarketplace 注册后 Codex 桌面端 Plugins 页直接浏览整个目录(含未装插件)
127
+ - **插件分发**:`marketplace` + `plugin install` 一条命令把 Rush 生态的 skill / command / rule / MCP 装到 Claude Code、Codex、Cursor 三家 IDE;首次 `plugin install --target codex` 自动把 marketplace 完整目录镜像到 Codex 桌面端 Plugins 页(含未装的插件,无需先 `marketplace add`)
128
128
 
129
129
  ## 命令一览
130
130
 
@@ -220,9 +220,9 @@ registry,仍然可以直接使用独立的 `reskill` CLI。
220
220
 
221
221
  #### Codex 桌面端浏览体验
222
222
 
223
- `marketplace add` / `marketplace sync` 完成后,Codex 桌面端 Plugins 页可以
224
- 直接浏览整个 marketplace 目录(含未装的插件)—— 不需要先知道 plugin 名字
225
- 再装。展示形式:
223
+ 只要做了 `marketplace add` / `marketplace sync` / `plugin install --target codex`
224
+ 任意一个,Codex 桌面端 Plugins 页就会出现该 marketplace 的完整目录(含未装
225
+ 的插件)—— 不需要先知道 plugin 名字再装。展示形式:
226
226
 
227
227
  - **未装插件**:列表/详情卡片可见,描述里附完整安装命令;点 "添加" 会弹
228
228
  toast 失败(避免空壳被错误启用),引导用户走终端安装
@@ -230,6 +230,11 @@ registry,仍然可以直接使用独立的 `reskill` CLI。
230
230
  工具可在对话中直接调用
231
231
  - **卸载后**:插件回到未装状态,依然在列表里作为重装入口
232
232
 
233
+ `plugin install` 触发的同步发生在装完目标 plugin 之后(仅 `--target codex`
234
+ 成功时),把 marketplace 里其它插件也一并镜像为 stub。所以从干净状态直接
235
+ 跑一条 `rush-ai plugin install context7@claude-plugins-official --target codex`
236
+ 就能在 Codex Plugins 页同时拿到 225 个 plugin 的完整目录。
237
+
233
238
  ### 认证 / 配置 / 其他
234
239
 
235
240
  | 命令 | 说明 |
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ buildLocalCachePluginContentLoader,
4
+ buildRushPluginContentLoader,
5
+ pathExists,
6
+ pickContentLoader
7
+ } from "./chunk-2AICQRQP.js";
8
+ import "./chunk-OIKYNVKO.js";
9
+ import "./chunk-T5S6NCHZ.js";
10
+ export {
11
+ buildLocalCachePluginContentLoader,
12
+ buildRushPluginContentLoader,
13
+ pathExists,
14
+ pickContentLoader
15
+ };
16
+ //# sourceMappingURL=_codex-content-loader-Q7KBWZSB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,447 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getAuthToken
4
+ } from "./chunk-OIKYNVKO.js";
5
+ import {
6
+ output
7
+ } from "./chunk-T5S6NCHZ.js";
8
+
9
+ // src/commands/marketplace/_codex-content-loader.ts
10
+ import { constants as fsConstants } from "fs";
11
+ import { access, readFile, stat as stat2 } from "fs/promises";
12
+ import { resolve as pathResolve, sep } from "path";
13
+
14
+ // src/marketplaces/rush.ts
15
+ import { execFileSync } from "child_process";
16
+ import { randomUUID } from "crypto";
17
+ import { mkdir, rename, rm, stat, writeFile } from "fs/promises";
18
+ import { resolve } from "path";
19
+ async function fetchRushMarketplace(source, opts = {}) {
20
+ const fetchFn = opts.fetchFn ?? fetch;
21
+ const url = `${inferProtocol(source.host)}${source.host}/api/marketplace`;
22
+ const headers = {
23
+ Accept: "application/json"
24
+ };
25
+ if (opts.token) {
26
+ headers.Authorization = `Bearer ${opts.token}`;
27
+ }
28
+ const response = await fetchFn(url, { headers });
29
+ if (!response.ok) {
30
+ throw new Error(
31
+ `Failed to fetch rush marketplace from ${url}: ${response.status} ${response.statusText}`
32
+ );
33
+ }
34
+ const data = await response.json();
35
+ if (!data.plugins || !Array.isArray(data.plugins)) {
36
+ throw new Error(
37
+ `Invalid marketplace response from ${url}: missing 'plugins' array`
38
+ );
39
+ }
40
+ return data;
41
+ }
42
+ async function fetchRushPlugin(source, pluginSlug, opts = {}) {
43
+ const fetchFn = opts.fetchFn ?? fetch;
44
+ const url = `${inferProtocol(source.host)}${source.host}/api/marketplace/${encodeURIComponent(pluginSlug)}`;
45
+ const headers = {
46
+ Accept: "application/json"
47
+ };
48
+ if (opts.token) {
49
+ headers.Authorization = `Bearer ${opts.token}`;
50
+ }
51
+ const response = await fetchFn(url, { headers });
52
+ if (response.status === 401) {
53
+ throw new Error(
54
+ `Plugin '${pluginSlug}' requires authentication. Run 'rush-ai login' first.`
55
+ );
56
+ }
57
+ if (response.status === 404) {
58
+ throw new Error(
59
+ `Plugin '${pluginSlug}' not found in rush marketplace at ${source.host}`
60
+ );
61
+ }
62
+ if (!response.ok) {
63
+ throw new Error(
64
+ `Failed to fetch plugin '${pluginSlug}' from ${url}: ${response.status} ${response.statusText}`
65
+ );
66
+ }
67
+ return await response.json();
68
+ }
69
+ async function materializeRushPlugin(source, pluginSlug, targetDir, opts = {}) {
70
+ validateSlug(pluginSlug, "plugin slug");
71
+ const manifest = await fetchRushPlugin(source, pluginSlug, opts);
72
+ let mcpServers = {};
73
+ if (manifest.mcpServers && Object.keys(manifest.mcpServers).length > 0) {
74
+ mcpServers = { ...manifest.mcpServers };
75
+ if (opts.secrets && Object.keys(opts.secrets).length > 0) {
76
+ mcpServers = substituteMcpSecrets(mcpServers, opts.secrets);
77
+ }
78
+ }
79
+ const pluginJson = {
80
+ name: manifest.name,
81
+ version: manifest.version || "1.0.0",
82
+ description: manifest.description,
83
+ ...Object.keys(mcpServers).length > 0 ? { mcpServers } : {}
84
+ };
85
+ const tmpDir = `${targetDir}.${randomUUID()}.tmp`;
86
+ try {
87
+ const pluginJsonDir = resolve(tmpDir, ".claude-plugin");
88
+ await mkdir(pluginJsonDir, { recursive: true });
89
+ await writeFile(
90
+ resolve(pluginJsonDir, "plugin.json"),
91
+ `${JSON.stringify(pluginJson, null, 2)}
92
+ `,
93
+ "utf8"
94
+ );
95
+ if (manifest.skills && manifest.skills.length > 0) {
96
+ await writeFile(
97
+ resolve(tmpDir, "skills.json"),
98
+ '{"skills":{}}\n',
99
+ "utf8"
100
+ );
101
+ const registryUrl = `${inferProtocol(source.host)}${source.host}`;
102
+ for (const skill of manifest.skills) {
103
+ try {
104
+ const args = [
105
+ "-y",
106
+ "reskill@latest",
107
+ "install",
108
+ skill.name,
109
+ "--no-save",
110
+ "--skip-manifest",
111
+ "-y",
112
+ "-f",
113
+ "-r",
114
+ registryUrl,
115
+ ...opts.token ? ["-t", opts.token] : []
116
+ ];
117
+ execFileSync("npx", args, {
118
+ cwd: tmpDir,
119
+ timeout: 6e4,
120
+ stdio: "pipe"
121
+ });
122
+ } catch (err) {
123
+ const stderr = err && typeof err === "object" && "stderr" in err ? err.stderr?.toString() ?? "" : "";
124
+ if (stderr.includes("401") || stderr.includes("403") || stderr.includes("Unauthorized") || stderr.includes("Forbidden")) {
125
+ throw new SkillAuthError(skill.name, 401);
126
+ }
127
+ output.warn(
128
+ ` Warning: failed to install skill '${skill.name}': ${err instanceof Error ? err.message : String(err)}`
129
+ );
130
+ }
131
+ }
132
+ }
133
+ const dotSkillsDir = resolve(tmpDir, ".skills");
134
+ const skillsDir = resolve(tmpDir, "skills");
135
+ if (await stat(dotSkillsDir).then((s) => s.isDirectory()).catch(() => false)) {
136
+ if (!await stat(skillsDir).then((s) => s.isDirectory()).catch(() => false)) {
137
+ await rename(dotSkillsDir, skillsDir);
138
+ }
139
+ }
140
+ await rm(resolve(tmpDir, "skills.json"), { force: true });
141
+ await rm(resolve(tmpDir, ".cursor"), { recursive: true, force: true });
142
+ await rm(resolve(tmpDir, ".claude"), { recursive: true, force: true });
143
+ await rm(resolve(tmpDir, ".codex"), { recursive: true, force: true });
144
+ await rm(resolve(tmpDir, ".github"), { recursive: true, force: true });
145
+ await rm(resolve(tmpDir, ".opencode"), { recursive: true, force: true });
146
+ await rm(targetDir, { recursive: true, force: true });
147
+ await mkdir(resolve(targetDir, ".."), { recursive: true });
148
+ await rename(tmpDir, targetDir);
149
+ } catch (err) {
150
+ await rm(tmpDir, { recursive: true, force: true }).catch(() => {
151
+ });
152
+ throw err;
153
+ }
154
+ return manifest;
155
+ }
156
+ var SkillAuthError = class extends Error {
157
+ constructor(skillName, status) {
158
+ super(
159
+ `Skill '${skillName}' requires authentication (HTTP ${status}). Run 'rush-ai auth login' first.`
160
+ );
161
+ this.name = "SkillAuthError";
162
+ }
163
+ };
164
+ function substituteMcpSecrets(mcpServers, secrets) {
165
+ const result = {};
166
+ for (const [serverName, serverConfig] of Object.entries(mcpServers)) {
167
+ if (!serverConfig || typeof serverConfig !== "object") {
168
+ result[serverName] = serverConfig;
169
+ continue;
170
+ }
171
+ const cfg = { ...serverConfig };
172
+ if (cfg.env && typeof cfg.env === "object") {
173
+ const env = {};
174
+ for (const [k, v] of Object.entries(cfg.env)) {
175
+ env[k] = substituteValue(v, secrets);
176
+ }
177
+ cfg.env = env;
178
+ }
179
+ if (cfg.headers && typeof cfg.headers === "object") {
180
+ const headers = {};
181
+ for (const [k, v] of Object.entries(
182
+ cfg.headers
183
+ )) {
184
+ headers[k] = substituteValue(v, secrets);
185
+ }
186
+ cfg.headers = headers;
187
+ }
188
+ result[serverName] = cfg;
189
+ }
190
+ return result;
191
+ }
192
+ function substituteValue(value, secrets) {
193
+ return value.replace(/\$\{([^}]+)\}/g, (match, key) => {
194
+ return key in secrets ? secrets[key] : match;
195
+ });
196
+ }
197
+ function validateSlug(value, label) {
198
+ if (!value || value.includes("..") || value.startsWith("/") || value.includes("\0")) {
199
+ throw new Error(
200
+ `Invalid ${label} '${value}': must not be empty, contain '..', start with '/', or contain null bytes.`
201
+ );
202
+ }
203
+ }
204
+ function inferProtocol(host) {
205
+ const hostname = host.split(":")[0];
206
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0") {
207
+ return "http://";
208
+ }
209
+ return "https://";
210
+ }
211
+
212
+ // src/commands/marketplace/_codex-content-loader.ts
213
+ function pickContentLoader(resolved, opts = {}) {
214
+ switch (resolved.source.kind) {
215
+ case "rush":
216
+ return buildRushPluginContentLoader(resolved.source, opts);
217
+ case "directory":
218
+ case "github":
219
+ return buildLocalCachePluginContentLoader(resolved);
220
+ default:
221
+ return async () => ({});
222
+ }
223
+ }
224
+ function buildRushPluginContentLoader(source, opts = {}) {
225
+ const token = opts.token ?? getAuthToken();
226
+ const fetchOpts = {};
227
+ if (opts.fetchFn !== void 0) fetchOpts.fetchFn = opts.fetchFn;
228
+ if (token !== void 0 && token !== null) fetchOpts.token = token;
229
+ const webBase = inferWebBase(source.host);
230
+ const fetchFn = opts.fetchFn ?? globalThis.fetch;
231
+ return async (entry) => {
232
+ if (typeof entry.name !== "string" || entry.name.length === 0) return {};
233
+ try {
234
+ const detail = await fetchRushPlugin(source, entry.name, fetchOpts);
235
+ const manifest = pickManifestFields(
236
+ detail
237
+ );
238
+ const hasMcp = detail.mcpServers && Object.keys(detail.mcpServers).length > 0;
239
+ const pluginWebUrl = `${webBase}/next/plugins/${encodeURIComponent(
240
+ entry.name
241
+ )}`;
242
+ const skillEntries = Array.isArray(detail.skills) && detail.skills.length > 0 ? detail.skills.filter(
243
+ (s) => !!s && typeof s.name === "string" && s.name.length > 0
244
+ ) : [];
245
+ const skillsPlaceholders = [];
246
+ for (const s of skillEntries) {
247
+ const url = `${webBase}/next/skills/${encodeURIComponent(s.name)}`;
248
+ const mdUrl = `${url}.md`;
249
+ let rawSkillMd;
250
+ try {
251
+ const headers = {
252
+ Accept: "text/markdown,text/plain,*/*"
253
+ };
254
+ if (token) headers.Authorization = `Bearer ${token}`;
255
+ const res = await fetchFn(mdUrl, { headers });
256
+ if (res.ok) {
257
+ const text = await res.text();
258
+ if (text.length > 0) rawSkillMd = text;
259
+ }
260
+ } catch {
261
+ }
262
+ skillsPlaceholders.push({
263
+ name: s.name,
264
+ url,
265
+ ...rawSkillMd !== void 0 ? { rawSkillMd } : {}
266
+ });
267
+ }
268
+ return {
269
+ ...manifest !== void 0 ? { manifest } : {},
270
+ ...hasMcp ? { mcpServers: detail.mcpServers } : {},
271
+ ...skillsPlaceholders.length > 0 ? { skillsPlaceholders } : {},
272
+ pluginWebUrl
273
+ };
274
+ } catch {
275
+ return {};
276
+ }
277
+ };
278
+ }
279
+ function inferWebBase(host) {
280
+ const hostname = host.split(":")[0];
281
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0") {
282
+ return `http://${host}`;
283
+ }
284
+ return `https://${host}`;
285
+ }
286
+ function buildLocalCachePluginContentLoader(resolved) {
287
+ return async (entry) => {
288
+ const relPath = extractLocalPluginPath(entry);
289
+ if (relPath === null) return {};
290
+ const pluginDir = pathResolve(resolved.rootDir, relPath);
291
+ const root = pathResolve(resolved.rootDir);
292
+ const rootWithSep = root.endsWith(sep) ? root : root + sep;
293
+ const safe = pluginDir === root || pluginDir.startsWith(rootWithSep);
294
+ if (!safe) return {};
295
+ let manifest;
296
+ let mcpServers;
297
+ let skillsSourceDir;
298
+ try {
299
+ const pj = await readFile(
300
+ pathResolve(pluginDir, ".claude-plugin", "plugin.json"),
301
+ "utf8"
302
+ );
303
+ const parsed = JSON.parse(pj);
304
+ manifest = pickManifestFields(parsed);
305
+ const inline = parsed.mcpServers;
306
+ if (inline && typeof inline === "object" && !Array.isArray(inline) && Object.keys(inline).length > 0) {
307
+ mcpServers = inline;
308
+ }
309
+ } catch {
310
+ }
311
+ try {
312
+ const raw = await readFile(pathResolve(pluginDir, ".mcp.json"), "utf8");
313
+ const parsed = JSON.parse(raw);
314
+ let servers = null;
315
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && "mcpServers" in parsed && typeof parsed.mcpServers === "object") {
316
+ servers = parsed.mcpServers;
317
+ } else if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
318
+ servers = parsed;
319
+ }
320
+ if (servers && Object.keys(servers).length > 0) {
321
+ mcpServers = servers;
322
+ }
323
+ } catch {
324
+ }
325
+ const skillsDir = pathResolve(pluginDir, "skills");
326
+ if (await isDir(skillsDir)) {
327
+ skillsSourceDir = skillsDir;
328
+ }
329
+ return {
330
+ ...manifest !== void 0 ? { manifest } : {},
331
+ ...mcpServers !== void 0 ? { mcpServers } : {},
332
+ ...skillsSourceDir !== void 0 ? { skillsSourceDir } : {}
333
+ };
334
+ };
335
+ }
336
+ function extractLocalPluginPath(entry) {
337
+ if (typeof entry.source === "string") {
338
+ return entry.source;
339
+ }
340
+ if (entry.source && typeof entry.source === "object" && !Array.isArray(entry.source)) {
341
+ const obj = entry.source;
342
+ if (typeof obj.url === "string" && obj.url.length > 0) {
343
+ return null;
344
+ }
345
+ if (typeof obj.path === "string" && obj.path.length > 0) {
346
+ return obj.path;
347
+ }
348
+ }
349
+ if (entry.source === void 0 && typeof entry.name === "string" && entry.name.length > 0) {
350
+ return `plugins/${entry.name}`;
351
+ }
352
+ return null;
353
+ }
354
+ async function isDir(p) {
355
+ try {
356
+ const s = await stat2(p);
357
+ return s.isDirectory();
358
+ } catch {
359
+ return false;
360
+ }
361
+ }
362
+ async function pathExists(p) {
363
+ try {
364
+ await access(p, fsConstants.F_OK);
365
+ return true;
366
+ } catch {
367
+ return false;
368
+ }
369
+ }
370
+ function pickManifestFields(raw) {
371
+ const m = {};
372
+ if (typeof raw.description === "string" && raw.description.length > 0) {
373
+ m.description = raw.description;
374
+ }
375
+ if (typeof raw.version === "string" && raw.version.length > 0) {
376
+ m.version = raw.version;
377
+ }
378
+ if (typeof raw.homepage === "string" && raw.homepage.length > 0) {
379
+ m.homepage = raw.homepage;
380
+ }
381
+ if (typeof raw.license === "string" && raw.license.length > 0) {
382
+ m.license = raw.license;
383
+ }
384
+ if (Array.isArray(raw.keywords)) {
385
+ const kw = raw.keywords.filter(
386
+ (x) => typeof x === "string" && x.length > 0
387
+ );
388
+ if (kw.length > 0) m.keywords = kw;
389
+ }
390
+ if (raw.author && typeof raw.author === "object" && !Array.isArray(raw.author)) {
391
+ const a = raw.author;
392
+ const author = {};
393
+ if (typeof a.name === "string") author.name = a.name;
394
+ if (typeof a.email === "string") author.email = a.email;
395
+ if (typeof a.url === "string") author.url = a.url;
396
+ if (Object.keys(author).length > 0) m.author = author;
397
+ }
398
+ if (raw.interface && typeof raw.interface === "object" && !Array.isArray(raw.interface)) {
399
+ const iface = pickInterfaceFields(raw.interface);
400
+ if (iface !== void 0) m.interface = iface;
401
+ }
402
+ return Object.keys(m).length > 0 ? m : void 0;
403
+ }
404
+ function pickInterfaceFields(raw) {
405
+ const out = {};
406
+ const stringKeys = [
407
+ "displayName",
408
+ "shortDescription",
409
+ "longDescription",
410
+ "developerName",
411
+ "category",
412
+ "websiteURL",
413
+ "privacyPolicyURL",
414
+ "termsOfServiceURL",
415
+ "brandColor",
416
+ "composerIcon",
417
+ "logo"
418
+ ];
419
+ for (const key of stringKeys) {
420
+ const v = raw[key];
421
+ if (typeof v === "string" && v.length > 0) {
422
+ out[key] = v;
423
+ }
424
+ }
425
+ for (const key of ["capabilities", "defaultPrompt", "screenshots"]) {
426
+ const v = raw[key];
427
+ if (Array.isArray(v)) {
428
+ const arr = v.filter(
429
+ (x) => typeof x === "string" && x.length > 0
430
+ );
431
+ if (arr.length > 0) out[key] = arr;
432
+ }
433
+ }
434
+ return Object.keys(out).length > 0 ? out : void 0;
435
+ }
436
+
437
+ export {
438
+ fetchRushMarketplace,
439
+ fetchRushPlugin,
440
+ materializeRushPlugin,
441
+ SkillAuthError,
442
+ pickContentLoader,
443
+ buildRushPluginContentLoader,
444
+ buildLocalCachePluginContentLoader,
445
+ pathExists
446
+ };
447
+ //# sourceMappingURL=chunk-2AICQRQP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/marketplace/_codex-content-loader.ts","../src/marketplaces/rush.ts"],"sourcesContent":["/**\n * Codex marketplace mirror —— `PluginContentLoader` 的具体实现集合。\n *\n * Spec: `specs/rush-v2/web/plugins/codex-marketplace-mirror.spec.md` §6.1\n *\n * 不同 source kind 的 marketplace 用不同 loader 拉取 plugin 内容:\n *\n * - `rush://` → `buildRushPluginContentLoader`:调 `/api/marketplace/:slug` 拿 mcpServers\n * - `directory:`/`github:` → `buildLocalCachePluginContentLoader`:从 cache 目录里读\n * `<rootDir>/<entry.source.path>/.claude-plugin/plugin.json` + 同目录的 `.mcp.json`\n * + `skills/` 子目录\n * - `git`/`npm` → 暂未实现,调用方走默认空 loader\n *\n * 所有 loader 都**不应抛错**:失败时返回 `{}`(空载荷),让 sync 退化到最简 stub。\n */\n\nimport { constants as fsConstants } from 'node:fs';\nimport { access, readFile, stat } from 'node:fs/promises';\nimport { resolve as pathResolve, sep } from 'node:path';\nimport type {\n PluginContent,\n PluginContentInterface,\n PluginContentLoader,\n PluginContentManifest,\n} from '../../installers/codex/index.js';\nimport { fetchRushPlugin } from '../../marketplaces/rush.js';\nimport type {\n MarketplacePluginEntry,\n ResolvedMarketplace,\n RushMarketplaceSource,\n} from '../../marketplaces/types.js';\nimport { getAuthToken } from '../../util/auth.js';\n\nexport interface BuildLoaderOptions {\n /** 测试用 fetch 注入;默认走 global fetch */\n fetchFn?: typeof fetch;\n /** 测试用 token 注入;默认从 keychain 读 */\n token?: string | null;\n}\n\n/**\n * 按 `resolved.source.kind` 选合适的 loader。\n * 未知 kind / Phase 2 source → 返回返回空载荷的 fallback loader。\n */\nexport function pickContentLoader(\n resolved: ResolvedMarketplace,\n opts: BuildLoaderOptions = {}\n): PluginContentLoader {\n switch (resolved.source.kind) {\n case 'rush':\n return buildRushPluginContentLoader(resolved.source, opts);\n case 'directory':\n case 'github':\n return buildLocalCachePluginContentLoader(resolved);\n default:\n return async () => ({});\n }\n}\n\n// ---------------------------------------------------------------------------\n// rush:// loader\n// ---------------------------------------------------------------------------\n\n/**\n * 调 `https://<host>/api/marketplace/:slug` 拿单个 plugin 详情。\n *\n * 注意:每个 plugin 一次 HTTP 调用。rush 当前 ≤ 3 个 plugin 时无压力;\n * 100+ plugin 时未来需要批量接口或并发限流(当前**串行**调用避免雪崩)。\n */\nexport function buildRushPluginContentLoader(\n source: RushMarketplaceSource,\n opts: BuildLoaderOptions = {}\n): PluginContentLoader {\n const token = opts.token ?? getAuthToken();\n const fetchOpts: { fetchFn?: typeof fetch; token?: string | null } = {};\n if (opts.fetchFn !== undefined) fetchOpts.fetchFn = opts.fetchFn;\n if (token !== undefined && token !== null) fetchOpts.token = token;\n // 拼 web 站点 base URL(与 fetchRushPlugin 内的 inferProtocol 同构 ——\n // localhost 用 http://,其他 https://)\n const webBase = inferWebBase(source.host);\n const fetchFn = opts.fetchFn ?? globalThis.fetch;\n return async (entry: MarketplacePluginEntry): Promise<PluginContent> => {\n if (typeof entry.name !== 'string' || entry.name.length === 0) return {};\n try {\n const detail = await fetchRushPlugin(source, entry.name, fetchOpts);\n const manifest = pickManifestFields(\n detail as unknown as Record<string, unknown>\n );\n const hasMcp =\n detail.mcpServers && Object.keys(detail.mcpServers).length > 0;\n // 路径与 web 路由 `/next/plugins/[slug]` 对齐\n const pluginWebUrl = `${webBase}/next/plugins/${encodeURIComponent(\n entry.name\n )}`;\n\n // 真实 skills:拉 `<host>/next/skills/<name>.md` 端点拿 SKILL.md 全文。\n // 串行 fetch 避免对后端打太狠(rush 当前每 plugin ≤ ~10 skills);\n // 单个 skill fetch 失败 → 该 skill 退化到占位(不阻塞别的 skill)。\n const skillEntries =\n Array.isArray(detail.skills) && detail.skills.length > 0\n ? detail.skills.filter(\n (s): s is { name: string; version: string } =>\n !!s && typeof s.name === 'string' && s.name.length > 0\n )\n : [];\n const skillsPlaceholders: Array<{\n name: string;\n url: string;\n rawSkillMd?: string;\n }> = [];\n for (const s of skillEntries) {\n const url = `${webBase}/next/skills/${encodeURIComponent(s.name)}`;\n const mdUrl = `${url}.md`;\n let rawSkillMd: string | undefined;\n try {\n const headers: Record<string, string> = {\n Accept: 'text/markdown,text/plain,*/*',\n };\n if (token) headers.Authorization = `Bearer ${token}`;\n const res = await fetchFn(mdUrl, { headers });\n if (res.ok) {\n const text = await res.text();\n if (text.length > 0) rawSkillMd = text;\n }\n } catch {\n // skill md fetch 失败 → 占位流程接管\n }\n skillsPlaceholders.push({\n name: s.name,\n url,\n ...(rawSkillMd !== undefined ? { rawSkillMd } : {}),\n });\n }\n\n // 透传 mcpServers,env / args 中的 ${KEY} 占位符**不替换** ——\n // sync 阶段不能拿 secret(与 plugin install 路径分离)。\n return {\n ...(manifest !== undefined ? { manifest } : {}),\n ...(hasMcp\n ? { mcpServers: detail.mcpServers as Record<string, unknown> }\n : {}),\n ...(skillsPlaceholders.length > 0 ? { skillsPlaceholders } : {}),\n pluginWebUrl,\n };\n } catch {\n // 单个 plugin 详情拉失败 → 退化到空载荷(sync 写最简 stub)\n return {};\n }\n };\n}\n\nfunction inferWebBase(host: string): string {\n const hostname = host.split(':')[0];\n if (\n hostname === 'localhost' ||\n hostname === '127.0.0.1' ||\n hostname === '0.0.0.0'\n ) {\n return `http://${host}`;\n }\n return `https://${host}`;\n}\n\n// ---------------------------------------------------------------------------\n// directory:/github: loader(从本地 cache 读盘)\n// ---------------------------------------------------------------------------\n\n/**\n * 从 `resolved.rootDir/<entry.source.path>` 目录读取 plugin 内容:\n * - `.claude-plugin/plugin.json` → manifest(取 description / version)\n * - `.mcp.json` → mcpServers(直接 parse)\n * - `skills/` → 整目录 copy 到镜像\n *\n * source 字段两种形态:\n * - 字符串:`\"./plugins/foo\"` —— 相对 marketplace rootDir 的路径\n * - 对象:`{ source: \"git-subdir\", url, path, ref }` 等 —— v1 不读,返回 {}\n *\n * 路径穿越守护:拒绝 `../` 逃出 rootDir 的相对路径。\n */\nexport function buildLocalCachePluginContentLoader(\n resolved: ResolvedMarketplace\n): PluginContentLoader {\n return async (entry: MarketplacePluginEntry): Promise<PluginContent> => {\n const relPath = extractLocalPluginPath(entry);\n if (relPath === null) return {};\n\n const pluginDir = pathResolve(resolved.rootDir, relPath);\n // 必须用 path.sep 边界比较,否则 `../market-evil/plugin` 会被\n // `startsWith('/tmp/market')` 误判为安全(sibling path 共享前缀)。\n const root = pathResolve(resolved.rootDir);\n const rootWithSep = root.endsWith(sep) ? root : root + sep;\n const safe = pluginDir === root || pluginDir.startsWith(rootWithSep);\n if (!safe) return {};\n\n let manifest: PluginContent['manifest'] | undefined;\n let mcpServers: Record<string, unknown> | undefined;\n let skillsSourceDir: string | undefined;\n\n // 1. .claude-plugin/plugin.json —— 透传 description / version / author /\n // homepage / license / keywords / interface(让 Codex 详情页字段丰富)\n // + 顺便提取 inline mcpServers(plugin resolver 也接受 plugin.json\n // 内联 mcpServers 对象,没有独立 .mcp.json 时也得镜像走)\n try {\n const pj = await readFile(\n pathResolve(pluginDir, '.claude-plugin', 'plugin.json'),\n 'utf8'\n );\n const parsed = JSON.parse(pj) as Record<string, unknown>;\n manifest = pickManifestFields(parsed);\n // inline mcpServers\n const inline = (parsed as { mcpServers?: unknown }).mcpServers;\n if (\n inline &&\n typeof inline === 'object' &&\n !Array.isArray(inline) &&\n Object.keys(inline as Record<string, unknown>).length > 0\n ) {\n mcpServers = inline as Record<string, unknown>;\n }\n } catch {\n // 缺失 / 损坏 → 不带 manifest\n }\n\n // 2. .mcp.json(独立文件,优先级高于 plugin.json 内联)\n try {\n const raw = await readFile(pathResolve(pluginDir, '.mcp.json'), 'utf8');\n const parsed = JSON.parse(raw) as\n | Record<string, unknown>\n | { mcpServers?: Record<string, unknown> };\n // 两种形态都接受:\n // { mcpServers: {...} }(rush-ai 自己写的,包一层)\n // { foo: {...} } (claude-plugins-official 的 external_plugins/* 不包一层)\n let servers: Record<string, unknown> | null = null;\n if (\n parsed &&\n typeof parsed === 'object' &&\n !Array.isArray(parsed) &&\n 'mcpServers' in parsed &&\n typeof (parsed as { mcpServers?: unknown }).mcpServers === 'object'\n ) {\n servers = (parsed as { mcpServers: Record<string, unknown> })\n .mcpServers;\n } else if (\n parsed &&\n typeof parsed === 'object' &&\n !Array.isArray(parsed)\n ) {\n servers = parsed as Record<string, unknown>;\n }\n if (servers && Object.keys(servers).length > 0) {\n mcpServers = servers;\n }\n } catch {\n // 缺失 / 损坏 → 不影响已经从 plugin.json 内联拿到的 mcpServers\n }\n\n // 3. skills/\n const skillsDir = pathResolve(pluginDir, 'skills');\n if (await isDir(skillsDir)) {\n skillsSourceDir = skillsDir;\n }\n\n return {\n ...(manifest !== undefined ? { manifest } : {}),\n ...(mcpServers !== undefined ? { mcpServers } : {}),\n ...(skillsSourceDir !== undefined ? { skillsSourceDir } : {}),\n };\n };\n}\n\n/**\n * 从 marketplace plugin entry 推导出本地 plugin 目录的相对路径。\n *\n * 与 plugin resolver 的本地路径接受策略对齐(参考 `plugins/resolver.ts`):\n *\n * - `source: \"./plugins/foo\"` → `./plugins/foo` (字符串路径)\n * - `source: { path: \"plugins/foo\" }` → `plugins/foo` (任意 object,无 url)\n * - `source: { source: \"local\", path }` → `path` (rush-ai 自己写的)\n * - 完全无 `source` 字段 → `plugins/<entry.name>` (Claude Code 默认布局)\n * - 含 `url` 的 object(git-subdir/url/github)→ `null` (远端 source,本地没盘)\n *\n * 路径穿越的最终守护在调用方:用 path.sep 边界比较 pluginDir vs rootDir。\n */\nfunction extractLocalPluginPath(entry: MarketplacePluginEntry): string | null {\n if (typeof entry.source === 'string') {\n return entry.source;\n }\n if (\n entry.source &&\n typeof entry.source === 'object' &&\n !Array.isArray(entry.source)\n ) {\n const obj = entry.source as {\n path?: unknown;\n source?: unknown;\n url?: unknown;\n };\n // 含 url → 远端 source,本地没盘\n if (typeof obj.url === 'string' && obj.url.length > 0) {\n return null;\n }\n if (typeof obj.path === 'string' && obj.path.length > 0) {\n return obj.path;\n }\n }\n // 完全没写 source —— Claude Code 默认布局 plugins/<name>/\n if (\n entry.source === undefined &&\n typeof entry.name === 'string' &&\n entry.name.length > 0\n ) {\n return `plugins/${entry.name}`;\n }\n return null;\n}\n\nasync function isDir(p: string): Promise<boolean> {\n try {\n const s = await stat(p);\n return s.isDirectory();\n } catch {\n return false;\n }\n}\n\n// 保留:未来若需要先确认 plugin 目录存在再发 IO,可用 access\nexport async function pathExists(p: string): Promise<boolean> {\n try {\n await access(p, fsConstants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * 从 plugin.json 解析对象里挑选 `PluginContentManifest` 关心的字段。\n *\n * 透传策略:\n * - 字符串类字段(description / version / homepage / license)→ 仅当非空字符串\n * - author → 接受 `{ name?, email?, url? }` 形态,子字段做类型校验\n * - keywords → 仅接受字符串数组\n * - interface → 嵌套 picker,子字段同 PluginContentInterface\n *\n * 缺失 / 类型不符的字段全部丢弃;不抛错。返回的 manifest 中至少含一个字段才返回,\n * 否则返回 undefined(让 sync 走\"无 manifest\"分支)。\n */\nfunction pickManifestFields(\n raw: Record<string, unknown>\n): PluginContentManifest | undefined {\n const m: {\n description?: string;\n version?: string;\n author?: { name?: string; email?: string; url?: string };\n homepage?: string;\n license?: string;\n keywords?: string[];\n interface?: PluginContentInterface;\n } = {};\n\n if (typeof raw.description === 'string' && raw.description.length > 0) {\n m.description = raw.description;\n }\n if (typeof raw.version === 'string' && raw.version.length > 0) {\n m.version = raw.version;\n }\n if (typeof raw.homepage === 'string' && raw.homepage.length > 0) {\n m.homepage = raw.homepage;\n }\n if (typeof raw.license === 'string' && raw.license.length > 0) {\n m.license = raw.license;\n }\n if (Array.isArray(raw.keywords)) {\n const kw = raw.keywords.filter(\n (x): x is string => typeof x === 'string' && x.length > 0\n );\n if (kw.length > 0) m.keywords = kw;\n }\n if (\n raw.author &&\n typeof raw.author === 'object' &&\n !Array.isArray(raw.author)\n ) {\n const a = raw.author as Record<string, unknown>;\n const author: { name?: string; email?: string; url?: string } = {};\n if (typeof a.name === 'string') author.name = a.name;\n if (typeof a.email === 'string') author.email = a.email;\n if (typeof a.url === 'string') author.url = a.url;\n if (Object.keys(author).length > 0) m.author = author;\n }\n if (\n raw.interface &&\n typeof raw.interface === 'object' &&\n !Array.isArray(raw.interface)\n ) {\n const iface = pickInterfaceFields(raw.interface as Record<string, unknown>);\n if (iface !== undefined) m.interface = iface;\n }\n\n return Object.keys(m).length > 0 ? m : undefined;\n}\n\nfunction pickInterfaceFields(\n raw: Record<string, unknown>\n): PluginContentInterface | undefined {\n const out: {\n displayName?: string;\n shortDescription?: string;\n longDescription?: string;\n developerName?: string;\n category?: string;\n capabilities?: string[];\n websiteURL?: string;\n privacyPolicyURL?: string;\n termsOfServiceURL?: string;\n defaultPrompt?: string[];\n brandColor?: string;\n composerIcon?: string;\n logo?: string;\n screenshots?: string[];\n } = {};\n\n const stringKeys = [\n 'displayName',\n 'shortDescription',\n 'longDescription',\n 'developerName',\n 'category',\n 'websiteURL',\n 'privacyPolicyURL',\n 'termsOfServiceURL',\n 'brandColor',\n 'composerIcon',\n 'logo',\n ] as const;\n for (const key of stringKeys) {\n const v = raw[key];\n if (typeof v === 'string' && v.length > 0) {\n out[key] = v;\n }\n }\n for (const key of ['capabilities', 'defaultPrompt', 'screenshots'] as const) {\n const v = raw[key];\n if (Array.isArray(v)) {\n const arr = v.filter(\n (x): x is string => typeof x === 'string' && x.length > 0\n );\n if (arr.length > 0) out[key] = arr;\n }\n }\n\n return Object.keys(out).length > 0 ? out : undefined;\n}\n","/**\n * `rush://` marketplace source 实现。\n *\n * 从 Rush 平台 Web API 获取 marketplace manifest 和 plugin 详情。\n *\n * Spec: specs/rush-v2/web/plugins/cli-rush-source.spec.md\n */\n\nimport { execFileSync } from 'node:child_process';\nimport { randomUUID } from 'node:crypto';\nimport { mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { output } from '../output/logger.js';\nimport type { RushMarketplaceSource } from './types.js';\n\nexport interface RushMarketplaceManifestResponse {\n name: string;\n plugins: Array<{\n name: string;\n version: string;\n description: string;\n }>;\n}\n\nexport interface RushPluginManifestResponse {\n name: string;\n version: string;\n description: string;\n mcpServers: Record<\n string,\n {\n command?: string;\n args?: string[];\n url?: string;\n env?: Record<string, string>;\n [key: string]: unknown;\n }\n >;\n requiredSecrets: Array<{\n key: string;\n type: string;\n description: string;\n helpUrl?: string;\n }>;\n skills: Array<{\n name: string;\n version: string;\n }>;\n}\n\nexport interface FetchRushOptions {\n /** Auth token (Bearer) for private plugins */\n token?: string | null;\n /** Override fetch for testing */\n fetchFn?: typeof fetch;\n}\n\n/**\n * Fetch the full marketplace listing from a rush:// source.\n *\n * Calls: GET https://<host>/api/marketplace\n */\nexport async function fetchRushMarketplace(\n source: RushMarketplaceSource,\n opts: FetchRushOptions = {}\n): Promise<RushMarketplaceManifestResponse> {\n const fetchFn = opts.fetchFn ?? fetch;\n const url = `${inferProtocol(source.host)}${source.host}/api/marketplace`;\n\n const headers: Record<string, string> = {\n Accept: 'application/json',\n };\n if (opts.token) {\n headers.Authorization = `Bearer ${opts.token}`;\n }\n\n const response = await fetchFn(url, { headers });\n\n if (!response.ok) {\n throw new Error(\n `Failed to fetch rush marketplace from ${url}: ${response.status} ${response.statusText}`\n );\n }\n\n const data = (await response.json()) as RushMarketplaceManifestResponse;\n\n if (!data.plugins || !Array.isArray(data.plugins)) {\n throw new Error(\n `Invalid marketplace response from ${url}: missing 'plugins' array`\n );\n }\n\n return data;\n}\n\n/**\n * Fetch a single plugin's manifest from a rush:// source.\n *\n * Calls: GET https://<host>/api/marketplace/:slug\n */\nexport async function fetchRushPlugin(\n source: RushMarketplaceSource,\n pluginSlug: string,\n opts: FetchRushOptions = {}\n): Promise<RushPluginManifestResponse> {\n const fetchFn = opts.fetchFn ?? fetch;\n const url = `${inferProtocol(source.host)}${source.host}/api/marketplace/${encodeURIComponent(pluginSlug)}`;\n\n const headers: Record<string, string> = {\n Accept: 'application/json',\n };\n if (opts.token) {\n headers.Authorization = `Bearer ${opts.token}`;\n }\n\n const response = await fetchFn(url, { headers });\n\n if (response.status === 401) {\n throw new Error(\n `Plugin '${pluginSlug}' requires authentication. Run 'rush-ai login' first.`\n );\n }\n if (response.status === 404) {\n throw new Error(\n `Plugin '${pluginSlug}' not found in rush marketplace at ${source.host}`\n );\n }\n if (!response.ok) {\n throw new Error(\n `Failed to fetch plugin '${pluginSlug}' from ${url}: ${response.status} ${response.statusText}`\n );\n }\n\n return (await response.json()) as RushPluginManifestResponse;\n}\n\n// ---------------------------------------------------------------------------\n// Materialize: download plugin manifest + skills to local cache directory\n// ---------------------------------------------------------------------------\n\nexport interface MaterializeRushPluginOptions extends FetchRushOptions {\n /**\n * Pre-resolved secrets to substitute into mcpServers placeholders.\n * If provided, `${KEY}` patterns in mcpServers env/headers are replaced.\n */\n secrets?: Record<string, string>;\n}\n\n/**\n * Materialize a rush:// plugin to a local cache directory.\n *\n * Downloads the plugin manifest from API, fetches SKILL.md for each skill,\n * and writes the standard `.claude-plugin/plugin.json` + `skills/<name>/SKILL.md`\n * directory structure that `resolvePlugin()` can consume.\n *\n * Uses atomic write (tmp dir + rename) to avoid half-written state on failure.\n *\n * @param source The rush:// marketplace source (for API host)\n * @param pluginSlug The plugin slug/name to fetch\n * @param targetDir Final directory (e.g. ~/.rush/marketplaces/rush-marketplace/plugins/<slug>/)\n * @param opts Auth token, fetch override, and pre-resolved secrets\n */\nexport async function materializeRushPlugin(\n source: RushMarketplaceSource,\n pluginSlug: string,\n targetDir: string,\n opts: MaterializeRushPluginOptions = {}\n): Promise<RushPluginManifestResponse> {\n // Validate pluginSlug to prevent path traversal\n validateSlug(pluginSlug, 'plugin slug');\n\n // 1. Fetch full plugin manifest from API\n const manifest = await fetchRushPlugin(source, pluginSlug, opts);\n\n // 2. Build plugin.json content (PluginManifest shape)\n let mcpServers: Record<string, unknown> = {};\n if (manifest.mcpServers && Object.keys(manifest.mcpServers).length > 0) {\n mcpServers = { ...manifest.mcpServers };\n // Apply secret substitution if secrets provided\n if (opts.secrets && Object.keys(opts.secrets).length > 0) {\n mcpServers = substituteMcpSecrets(mcpServers, opts.secrets);\n }\n }\n\n const pluginJson: Record<string, unknown> = {\n name: manifest.name,\n version: manifest.version || '1.0.0',\n description: manifest.description,\n ...(Object.keys(mcpServers).length > 0 ? { mcpServers } : {}),\n };\n\n // 3. Atomic write: prepare in tmp dir, then rename\n const tmpDir = `${targetDir}.${randomUUID()}.tmp`;\n\n try {\n // Write .claude-plugin/plugin.json\n const pluginJsonDir = resolve(tmpDir, '.claude-plugin');\n await mkdir(pluginJsonDir, { recursive: true });\n await writeFile(\n resolve(pluginJsonDir, 'plugin.json'),\n `${JSON.stringify(pluginJson, null, 2)}\\n`,\n 'utf8'\n );\n\n // 4. Install skills via reskill CLI (downloads complete skill directory)\n if (manifest.skills && manifest.skills.length > 0) {\n // reskill requires skills.json to exist in cwd\n await writeFile(\n resolve(tmpDir, 'skills.json'),\n '{\"skills\":{}}\\n',\n 'utf8'\n );\n const registryUrl = `${inferProtocol(source.host)}${source.host}`;\n for (const skill of manifest.skills) {\n try {\n // Use execFileSync (not execSync) to avoid shell injection —\n // skill.name is untrusted input from the API.\n const args = [\n '-y',\n 'reskill@latest',\n 'install',\n skill.name,\n '--no-save',\n '--skip-manifest',\n '-y',\n '-f',\n '-r',\n registryUrl,\n ...(opts.token ? ['-t', opts.token] : []),\n ];\n execFileSync('npx', args, {\n cwd: tmpDir,\n timeout: 60_000,\n stdio: 'pipe',\n });\n } catch (err) {\n const stderr =\n err && typeof err === 'object' && 'stderr' in err\n ? ((err as { stderr: Buffer }).stderr?.toString() ?? '')\n : '';\n // Auth errors should block install\n if (\n stderr.includes('401') ||\n stderr.includes('403') ||\n stderr.includes('Unauthorized') ||\n stderr.includes('Forbidden')\n ) {\n throw new SkillAuthError(skill.name, 401);\n }\n // Other failures are non-blocking — warn and continue\n output.warn(\n ` Warning: failed to install skill '${skill.name}': ${\n err instanceof Error ? err.message : String(err)\n }`\n );\n }\n }\n }\n\n // 4b. Normalize: reskill installs to `.skills/` (Claude style) but\n // ClaudeCodeInstaller copies from `skills/` (CAPABILITY_DIRS).\n // Rename `.skills/` → `skills/` if needed.\n const dotSkillsDir = resolve(tmpDir, '.skills');\n const skillsDir = resolve(tmpDir, 'skills');\n if (\n await stat(dotSkillsDir)\n .then((s) => s.isDirectory())\n .catch(() => false)\n ) {\n if (\n !(await stat(skillsDir)\n .then((s) => s.isDirectory())\n .catch(() => false))\n ) {\n await rename(dotSkillsDir, skillsDir);\n }\n }\n\n // Remove reskill artifacts that we don't need in the plugin directory\n await rm(resolve(tmpDir, 'skills.json'), { force: true });\n await rm(resolve(tmpDir, '.cursor'), { recursive: true, force: true });\n await rm(resolve(tmpDir, '.claude'), { recursive: true, force: true });\n await rm(resolve(tmpDir, '.codex'), { recursive: true, force: true });\n await rm(resolve(tmpDir, '.github'), { recursive: true, force: true });\n await rm(resolve(tmpDir, '.opencode'), { recursive: true, force: true });\n\n // 5. Atomic rename: tmp → target (remove target first if exists)\n await rm(targetDir, { recursive: true, force: true });\n await mkdir(resolve(targetDir, '..'), { recursive: true });\n await rename(tmpDir, targetDir);\n } catch (err) {\n // Cleanup tmp on failure\n await rm(tmpDir, { recursive: true, force: true }).catch(() => {});\n throw err;\n }\n\n return manifest;\n}\n\n/**\n * Error thrown when skill download fails due to auth issues (401/403).\n * This should block the install — the user needs to fix auth.\n */\nexport class SkillAuthError extends Error {\n constructor(skillName: string, status: number) {\n super(\n `Skill '${skillName}' requires authentication (HTTP ${status}). Run 'rush-ai auth login' first.`\n );\n this.name = 'SkillAuthError';\n }\n}\n\n/**\n * Substitute `${KEY}` placeholders in mcpServers config with actual secret values.\n *\n * Replaces patterns in `env` values and top-level string values of each server config.\n */\nfunction substituteMcpSecrets(\n mcpServers: Record<string, unknown>,\n secrets: Record<string, string>\n): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n for (const [serverName, serverConfig] of Object.entries(mcpServers)) {\n if (!serverConfig || typeof serverConfig !== 'object') {\n result[serverName] = serverConfig;\n continue;\n }\n const cfg = { ...(serverConfig as Record<string, unknown>) };\n\n // Substitute in env values\n if (cfg.env && typeof cfg.env === 'object') {\n const env: Record<string, string> = {};\n for (const [k, v] of Object.entries(cfg.env as Record<string, string>)) {\n env[k] = substituteValue(v, secrets);\n }\n cfg.env = env;\n }\n\n // Substitute in headers values (for HTTP/SSE transports)\n if (cfg.headers && typeof cfg.headers === 'object') {\n const headers: Record<string, string> = {};\n for (const [k, v] of Object.entries(\n cfg.headers as Record<string, string>\n )) {\n headers[k] = substituteValue(v, secrets);\n }\n cfg.headers = headers;\n }\n\n result[serverName] = cfg;\n }\n return result;\n}\n\nfunction substituteValue(\n value: string,\n secrets: Record<string, string>\n): string {\n return value.replace(/\\$\\{([^}]+)\\}/g, (match, key: string) => {\n return key in secrets ? secrets[key] : match;\n });\n}\n\n/**\n * Validate a slug/name used in file paths to prevent path traversal.\n * Rejects: empty, contains `..`, starts with `/`, or contains null bytes.\n * Allows: alphanumeric, hyphens, underscores, dots, `@`, `/` (for scoped names like @scope/name).\n */\nfunction validateSlug(value: string, label: string): void {\n if (\n !value ||\n value.includes('..') ||\n value.startsWith('/') ||\n value.includes('\\0')\n ) {\n throw new Error(\n `Invalid ${label} '${value}': must not be empty, contain '..', start with '/', or contain null bytes.`\n );\n }\n}\n\n/**\n * Infer protocol from host. Localhost/127.0.0.1 use http://, others https://.\n */\nfunction inferProtocol(host: string): string {\n const hostname = host.split(':')[0];\n if (\n hostname === 'localhost' ||\n hostname === '127.0.0.1' ||\n hostname === '0.0.0.0'\n ) {\n return 'http://';\n }\n return 'https://';\n}\n"],"mappings":"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","names":["stat","stat"]}