rush-ai 0.20.0 → 0.21.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 +18 -9
- package/dist/{_codex-content-loader-EXSX7ZGI.js → _codex-content-loader-WAPA56U3.js} +2 -2
- package/dist/{chunk-X45FKY3L.js → chunk-22YQT6XF.js} +18 -7
- package/dist/chunk-22YQT6XF.js.map +1 -0
- package/dist/{chunk-E2CKW6JZ.js → chunk-C7KJUHEF.js} +66 -7
- package/dist/chunk-C7KJUHEF.js.map +1 -0
- package/dist/index.js +1414 -273
- package/dist/index.js.map +1 -1
- package/dist/{mcp-UZYID3GG.js → mcp-TROKQRBF.js} +2 -2
- package/package.json +1 -1
- package/dist/chunk-E2CKW6JZ.js.map +0 -1
- package/dist/chunk-X45FKY3L.js.map +0 -1
- /package/dist/{_codex-content-loader-EXSX7ZGI.js.map → _codex-content-loader-WAPA56U3.js.map} +0 -0
- /package/dist/{mcp-UZYID3GG.js.map → mcp-TROKQRBF.js.map} +0 -0
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
isRushOwnPlugin,
|
|
8
8
|
normalizeClaudeMcpServers,
|
|
9
9
|
readExternalMcpServers
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-22YQT6XF.js";
|
|
11
11
|
export {
|
|
12
12
|
RUSH_AI_MARKETPLACE_NAME,
|
|
13
13
|
RUSH_AI_PLUGIN_NAME,
|
|
@@ -17,4 +17,4 @@ export {
|
|
|
17
17
|
normalizeClaudeMcpServers,
|
|
18
18
|
readExternalMcpServers
|
|
19
19
|
};
|
|
20
|
-
//# sourceMappingURL=mcp-
|
|
20
|
+
//# sourceMappingURL=mcp-TROKQRBF.js.map
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
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 {\n fetchRushOfficialFile,\n fetchRushPlugin,\n} 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 // II-3 起 SKILL.md 取法分流:\n // - rush 用户 plugin(detail.source 缺省):走 `<host>/next/skills/<name>.md`\n // - 官方 local plugin(detail.source === 'claude-plugins-official'):走\n // OSS 文件代理 `/api/marketplace/<slug>/files/<skill.path>`\n // - 官方外部 plugin(含 redirectTo):sync 不下载真实内容(与现状 url/git-subdir 一致)\n const isOfficial = detail.source === 'claude-plugins-official';\n const isExternalRedirect = isOfficial && !!detail.redirectTo;\n\n const skillEntries =\n Array.isArray(detail.skills) && detail.skills.length > 0\n ? detail.skills.filter(\n (s): s is { name: string; version: string; path?: 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 // url 始终指向 web 详情页(plugin),不是 skill 单页 — 因为官方 skill 没有 reskill 单页\n const url = `${webBase}/next/plugins/${encodeURIComponent(entry.name)}`;\n let rawSkillMd: string | undefined;\n\n if (isExternalRedirect) {\n // 外部 plugin sync 阶段不取真内容(现状语义)\n } else if (\n isOfficial &&\n typeof s.path === 'string' &&\n s.path.length > 0\n ) {\n // 官方 local:走 OSS 文件代理\n const skillMdPath = ensureSkillMdSuffix(s.path);\n try {\n const file = await fetchRushOfficialFile(\n source,\n entry.name,\n skillMdPath,\n fetchOpts\n );\n if (typeof file.content === 'string' && file.content.length > 0) {\n rawSkillMd = file.content;\n }\n } catch {\n // 单个 skill md fetch 失败 → 占位流程接管\n }\n } else {\n // rush 用户 plugin:走 reskill skill md 端点\n const mdUrl = `${webBase}/next/skills/${encodeURIComponent(s.name)}.md`;\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 }\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/**\n * II-3:把 skill 的 path 规范成\"指向 SKILL.md 文件\"的相对路径。\n *\n * `manifest.skills[].path` 在官方 plugin 中通常是目录(如 `skills/code-review`),\n * 真实的 markdown 是 `skills/code-review/SKILL.md`。如果上游已经直接给了\n * `<dir>/SKILL.md`,则不再追加。\n *\n * 不依赖 fs.stat(loader 在 sync 阶段,目录还没落盘);纯字符串规则。\n */\nfunction ensureSkillMdSuffix(p: string): string {\n // 已经是 .md 文件结尾 → 原样返回\n if (p.endsWith('.md') || p.endsWith('.MD')) return p;\n // 移除尾部 / 后追加\n const trimmed = p.endsWith('/') ? p.slice(0, -1) : p;\n return `${trimmed}/SKILL.md`;\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, spawn } from 'node:child_process';\nimport { randomUUID } from 'node:crypto';\nimport { mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';\nimport { resolve as pathResolve, 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 * II-3:分类(业务串如 'database' / 'security' / 'productivity'),\n * 由 server `/api/marketplace` 下发。CLI 透传到本地 marketplace.json,\n * 在 codex mirror sync 时被 `normalizeCategory()` 映射成 Codex UI 大类。\n */\n category?: string;\n /**\n * II-3:作者元数据。CLI 透传到 codex mirror,让详情页\"开发者\"区块显示。\n * 字段子集任意可缺失(rush 用户 plugin 仅有 name = ldap)。\n */\n author?: { name?: string; email?: string; url?: string };\n /**\n * II-3:产品官网 URL。codex mirror 详情页\"网站\"链接的 fallback。\n */\n homepage?: string;\n /**\n * II-3:plugin 源代码位置。\n * - 字符串:rush 用户 plugin 时为 web 详情页 URL(codex 详情\"网站\"目标)\n * - 对象:官方外部 plugin 的原始 source.url + sha + ref(透传给 mirror\n * 提取 websiteURL;不影响本地 cache,cache 仍用 `./plugins/<name>`)\n */\n source?:\n | string\n | { url?: string; source?: string; sha?: string; ref?: string };\n }>;\n}\n\n/**\n * II-3 起 manifest 跨表 union:\n * - rush 用户 plugin(默认形态,无 `source` 字段)\n * - 官方 local plugin:`source: 'claude-plugins-official'`,含 mcpServers /\n * skills / agents 等;CLI 通过 `/api/marketplace/<slug>/files/<path>` 拉文件\n * - 官方外部 plugin:`source: 'claude-plugins-official'` + `redirectTo`,\n * CLI 临时 git clone 当 directory source 装\n */\nexport interface RushPluginManifestResponse {\n name: string;\n version: string;\n description: string;\n /** II-3:标识业务来源;缺省=rush 用户 plugin */\n source?: 'claude-plugins-official';\n /**\n * II-3:作者元数据(CLI 写到 plugin.json 顶层 + interface.developerName)。\n * 字段子集任意可缺失;rush 用户 plugin 仅有 name = ldap。\n */\n author?: { name?: string; email?: string; url?: string };\n /**\n * II-3:产品官网 / 仓库地址。CLI 写到 plugin.json 顶层 + interface.websiteURL。\n * 缺失 + 是外部 plugin → CLI 用 redirectTo 推导 GitHub URL。\n */\n homepage?: string;\n /**\n * 官方外部 plugin 才有;CLI 收到后切到 git clone 链路。\n * 当前仅支持 `kind: 'github'`(白名单 — server 端也只下发 github)。\n */\n redirectTo?: {\n kind: 'github';\n owner: string;\n repo: string;\n ref?: string;\n path?: string;\n };\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 /**\n * skills 列表。II-3 起官方 plugin 的 entry 多带 `path` 字段;\n * rush plugin 不带(CLI 走 /next/skills/<name>.md 端点)。\n */\n skills: Array<{\n name: string;\n version: string;\n /** 仅官方 plugin 携带 — 用于 OSS 文件代理拉 SKILL.md */\n path?: string;\n }>;\n /** II-3 官方 local plugin 才有;rush 不下发 */\n agents?: Array<{ name: string; path: string }>;\n hooks?: Array<{ name: string; path: string }>;\n commands?: Array<{ name: string; path: string }>;\n}\n\n/**\n * 官方 local plugin 的文件代理响应(GET /api/marketplace/<slug>/files/<path>)。\n *\n * 三种情况(互斥):\n * - 小文本 inline:`encoding='utf-8'` + `content`\n * - 二进制 / 大文件:`signedUrl` + `expiresAt`(CLI 直连 OSS)\n * - 截断:`truncated: true` + 无内容(CLI 跳过 + 提示)\n */\nexport interface RushOfficialFileResponse {\n path: string;\n size: number;\n mime: string | null;\n isBinary: boolean;\n truncated: boolean;\n encoding?: 'utf-8';\n content?: string;\n signedUrl?: string;\n expiresAt?: string;\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 * 拉官方 plugin 的单个文件(II-3:通过 rush API + OSS 代理,不连 GitHub)。\n *\n * Calls: GET https://<host>/api/marketplace/:slug/files/<path...>\n *\n * `path` 由 `manifest.skills[].path` / `manifest.agents[].path` 等字段提供 ——\n * 已经过 server 端 `claude_marketplace_plugin_files` 索引校验(防穿越)。\n *\n * 返回三种状态(见 `RushOfficialFileResponse`):\n * - inline content(小文本)\n * - signedUrl(二进制 / 大文件)\n * - truncated(同步阶段未传 OSS)\n *\n * 失败语义:\n * - 404:path 不在该 plugin 的文件索引(CLI 应跳过该文件 + warn)\n * - 5xx:server / OSS 异常(CLI 抛错让上层处理)\n */\nexport async function fetchRushOfficialFile(\n source: RushMarketplaceSource,\n pluginSlug: string,\n filePath: string,\n opts: FetchRushOptions = {}\n): Promise<RushOfficialFileResponse> {\n const fetchFn = opts.fetchFn ?? fetch;\n // path 用 / 分隔;逐段 encodeURIComponent 防特殊字符\n const encoded = filePath\n .split('/')\n .filter((s) => s.length > 0)\n .map((s) => encodeURIComponent(s))\n .join('/');\n const url = `${inferProtocol(source.host)}${source.host}/api/marketplace/${encodeURIComponent(pluginSlug)}/files/${encoded}`;\n\n const headers: Record<string, string> = { Accept: 'application/json' };\n if (opts.token) headers.Authorization = `Bearer ${opts.token}`;\n\n const response = await fetchFn(url, { headers });\n if (response.status === 404) {\n throw new Error(\n `File '${filePath}' not found in official plugin '${pluginSlug}'`\n );\n }\n if (!response.ok) {\n throw new Error(\n `Failed to fetch file '${filePath}' from ${url}: ${response.status} ${response.statusText}`\n );\n }\n return (await response.json()) as RushOfficialFileResponse;\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 // II-3:根据 manifest 形态分流。\n if (manifest.source === 'claude-plugins-official') {\n if (manifest.redirectTo) {\n // 1a) 外部 plugin → git clone redirectTo 到 targetDir\n await materializeOfficialExternal(manifest, targetDir);\n return manifest;\n }\n // 1b) 官方 local plugin → 通过 OSS 文件代理拉文件\n await materializeOfficialLocal(\n source,\n pluginSlug,\n manifest,\n targetDir,\n opts\n );\n return manifest;\n }\n\n // 1c) Rush 用户 plugin(默认链路) → 走原 reskill 流程\n return materializeRushUserPlugin(\n source,\n pluginSlug,\n manifest,\n targetDir,\n opts\n );\n}\n\n/**\n * Rush 用户 plugin 的 materialize 实现(II-3 之前的现状逻辑)。\n *\n * 抽出独立函数让 `materializeRushPlugin` 顶层只做分流。\n */\nasync function materializeRushUserPlugin(\n source: RushMarketplaceSource,\n pluginSlug: string,\n manifest: RushPluginManifestResponse,\n targetDir: string,\n opts: MaterializeRushPluginOptions\n): Promise<RushPluginManifestResponse> {\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> = buildPluginJsonShape(manifest, {\n 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 // 3 分钟 — 覆盖 npx 首次下载 reskill 包的冷启动场景\n // (warm cache 时 reskill 实际运行只需 ~7s)\n timeout: 180_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// II-3: 官方 plugin materialize(local + 外部两种分支)\n// ---------------------------------------------------------------------------\n\n/**\n * 官方 local plugin:从 rush API 的 OSS 文件代理拉取所有组件文件,\n * 在 `targetDir` 下还原成标准 Claude plugin 目录结构。\n *\n * 写出文件清单:\n * - `.claude-plugin/plugin.json`(含 name / version / description / mcpServers)\n * - `.mcp.json`(manifest.mcpServers 已包含 — 但写一份兜底,让 resolver 的隐式\n * mcp 兜底逻辑也能命中)\n * - `skills/<name>/SKILL.md`(每个 skill)\n * - `agents/<path>` / `hooks/<path>` / `commands/<path>`\n *\n * 失败语义:单文件下载失败 → warn 后跳过;plugin.json 写失败 → 抛错(致命)。\n *\n * 文件路径全部走 manifest 提供的字符串,**不做拼接**:server 端\n * `/api/marketplace/<slug>/files/<path>` 严格在 `claude_marketplace_plugin_files`\n * 索引中匹配 path,CLI 拿到的 path 已是合法文件位置。\n */\nasync function materializeOfficialLocal(\n source: RushMarketplaceSource,\n pluginSlug: string,\n manifest: RushPluginManifestResponse,\n targetDir: string,\n opts: MaterializeRushPluginOptions\n): Promise<void> {\n // mcpServers 接受 secrets 替换(与 rush 用户 plugin 保持一致语义)\n let mcpServers: Record<string, unknown> = {};\n if (manifest.mcpServers && Object.keys(manifest.mcpServers).length > 0) {\n mcpServers = { ...manifest.mcpServers };\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> = buildPluginJsonShape(manifest, {\n mcpServers,\n });\n\n const tmpDir = `${targetDir}.${randomUUID()}.tmp`;\n\n try {\n await mkdir(resolve(tmpDir, '.claude-plugin'), { recursive: true });\n await writeFile(\n resolve(tmpDir, '.claude-plugin', 'plugin.json'),\n `${JSON.stringify(pluginJson, null, 2)}\\n`,\n 'utf8'\n );\n\n // 写一份独立 .mcp.json(兼容某些 installer 强制读独立文件)\n if (Object.keys(mcpServers).length > 0) {\n await writeFile(\n resolve(tmpDir, '.mcp.json'),\n `${JSON.stringify({ mcpServers }, null, 2)}\\n`,\n 'utf8'\n );\n }\n\n // skills / agents / hooks / commands —— 全部通过 OSS 代理拉\n await fetchAndWriteOfficialFiles(\n source,\n pluginSlug,\n manifest,\n tmpDir,\n opts\n );\n\n // 原子 rename\n await rm(targetDir, { recursive: true, force: true });\n await mkdir(resolve(targetDir, '..'), { recursive: true });\n await rename(tmpDir, targetDir);\n } catch (err) {\n await rm(tmpDir, { recursive: true, force: true }).catch(() => {});\n throw err;\n }\n}\n\n/**\n * 拉取 manifest 里所有组件文件(skills / agents / hooks / commands)→ 写到 targetDir。\n *\n * 串行限流(per-plugin ≤ ~30 个文件,可接受)。\n * 单个文件下载失败仅 warn,不阻塞其他。\n */\nasync function fetchAndWriteOfficialFiles(\n source: RushMarketplaceSource,\n pluginSlug: string,\n manifest: RushPluginManifestResponse,\n rootDir: string,\n opts: MaterializeRushPluginOptions\n): Promise<void> {\n type Entry = { kind: string; name: string; path: string };\n const entries: Entry[] = [];\n for (const s of manifest.skills ?? []) {\n if (s.path) {\n // skill 的 manifest.path 通常是目录(如 'skills/review'),实际 markdown\n // 在 `<dir>/SKILL.md`。如果上游已经直接给了 .md 文件,原样使用。\n const skillFilePath =\n s.path.endsWith('.md') || s.path.endsWith('.MD')\n ? s.path\n : `${s.path.endsWith('/') ? s.path.slice(0, -1) : s.path}/SKILL.md`;\n entries.push({ kind: 'skill', name: s.name, path: skillFilePath });\n }\n }\n for (const a of manifest.agents ?? []) {\n entries.push({ kind: 'agent', name: a.name, path: a.path });\n }\n for (const h of manifest.hooks ?? []) {\n entries.push({ kind: 'hook', name: h.name, path: h.path });\n }\n for (const c of manifest.commands ?? []) {\n entries.push({ kind: 'command', name: c.name, path: c.path });\n }\n if (entries.length === 0) return;\n\n const fetchOpts: { fetchFn?: typeof fetch; token?: string | null } = {};\n if (opts.fetchFn !== undefined) fetchOpts.fetchFn = opts.fetchFn;\n if (opts.token !== undefined && opts.token !== null) {\n fetchOpts.token = opts.token;\n }\n\n for (const e of entries) {\n try {\n const file = await fetchRushOfficialFile(\n source,\n pluginSlug,\n e.path,\n fetchOpts\n );\n if (file.truncated) {\n output.warn(\n ` Warning: ${e.kind} '${e.name}' file '${e.path}' was truncated during sync; please view on web`\n );\n continue;\n }\n // 二进制走 signedUrl —— 需要再下载一次(CLI 不持久化 OSS 凭证;signedUrl\n // 是预签名一次性 URL,直接 GET 即可)\n let body: Buffer | string | null = null;\n if (typeof file.content === 'string') {\n body = file.content;\n } else if (typeof file.signedUrl === 'string') {\n const fetchFn = opts.fetchFn ?? fetch;\n const res = await fetchFn(file.signedUrl);\n if (res.ok) {\n const arr = await res.arrayBuffer();\n body = Buffer.from(arr);\n } else {\n output.warn(\n ` Warning: failed to download signed URL for ${e.kind} '${e.name}': ${res.status}`\n );\n continue;\n }\n } else {\n output.warn(\n ` Warning: ${e.kind} '${e.name}' has no content or signedUrl`\n );\n continue;\n }\n\n // 落盘到 rootDir/<path>。path 已经过 server 严格校验(在\n // claude_marketplace_plugin_files 索引内),不会含 `..` 等危险序列;\n // 这里再用 path.resolve 边界防御。\n const dest = resolve(rootDir, e.path);\n const root = pathResolve(rootDir);\n const rootWithSep = root.endsWith('/') ? root : `${root}/`;\n if (dest !== root && !dest.startsWith(rootWithSep)) {\n output.warn(\n ` Warning: refusing to write ${e.kind} '${e.name}' outside plugin dir: ${e.path}`\n );\n continue;\n }\n await mkdir(resolve(dest, '..'), { recursive: true });\n await writeFile(dest, body);\n } catch (err) {\n output.warn(\n ` Warning: failed to fetch ${e.kind} '${e.name}' (${e.path}): ${\n err instanceof Error ? err.message : String(err)\n }`\n );\n }\n }\n}\n\n/**\n * 官方外部 plugin:根据 `manifest.redirectTo` git clone 目标 GitHub 仓库到 plugin 目录。\n *\n * 流程:\n * 1. tmp 目录 git clone(深度 1,可选 ref)\n * 2. 如果 redirectTo.path 存在 → 把子目录的内容 move 到 targetDir\n * 3. 否则整个仓库就是 plugin 根 → 直接 rename\n *\n * 安全约束(spec §7.10):\n * - 只接受 redirectTo.kind === 'github'\n * - URL 由 owner/repo 拼成 `https://github.com/<owner>/<repo>.git`,\n * 不接受 server 直接下发完整 URL(避免任意主机被信任)\n *\n * 失败语义:clone / cp 失败抛错,由调用方上抛 RushError(2)。\n */\nasync function materializeOfficialExternal(\n manifest: RushPluginManifestResponse,\n targetDir: string\n): Promise<void> {\n const r = manifest.redirectTo;\n if (!r || r.kind !== 'github') {\n throw new Error(\n `Plugin '${manifest.name}' has unsupported redirectTo (kind=${r?.kind ?? 'undefined'}). ` +\n `External plugins must redirect to GitHub.`\n );\n }\n if (!r.owner || !r.repo) {\n throw new Error(\n `Plugin '${manifest.name}' has invalid redirectTo: missing owner/repo`\n );\n }\n\n const url = `https://github.com/${r.owner}/${r.repo}.git`;\n const cloneTmp = `${targetDir}.${randomUUID()}.clone.tmp`;\n\n const args = ['clone', '--depth', '1'];\n if (r.ref) args.push('--branch', r.ref);\n args.push('--', url, cloneTmp);\n\n try {\n await runGitClone(args);\n\n // 决定用整个仓库还是子目录作为 plugin 根\n let sourceDir: string;\n if (r.path && r.path.length > 0) {\n // 防穿越:path 不能逃出 cloneTmp\n const candidate = resolve(cloneTmp, r.path);\n const cloneTmpAbs = pathResolve(cloneTmp);\n const cloneTmpWithSep = cloneTmpAbs.endsWith('/')\n ? cloneTmpAbs\n : `${cloneTmpAbs}/`;\n if (candidate !== cloneTmpAbs && !candidate.startsWith(cloneTmpWithSep)) {\n throw new Error(`redirectTo.path escapes repo root: '${r.path}'`);\n }\n const st = await stat(candidate).catch(() => null);\n if (!st || !st.isDirectory()) {\n throw new Error(\n `redirectTo.path '${r.path}' is not a directory in ${url}`\n );\n }\n sourceDir = candidate;\n } else {\n sourceDir = cloneTmp;\n }\n\n // 原子 rename\n await rm(targetDir, { recursive: true, force: true });\n await mkdir(resolve(targetDir, '..'), { recursive: true });\n await rename(sourceDir, targetDir);\n\n // II-3:补齐上游 plugin.json 缺失的 homepage / author / interface\n // 让 codex sync 把它复制过去后详情页\"作者 / 网站\"能正常渲染。\n // 上游仓库的 plugin.json 通常无 homepage / interface 字段。\n await enrichExternalPluginJson(targetDir, manifest);\n } catch (err) {\n await rm(cloneTmp, { recursive: true, force: true }).catch(() => {});\n // 如果 sourceDir 已经 rename 出去再失败 — 几乎不可能,但 cleanup 兜底\n throw err;\n } finally {\n // 子目录方式:还有 cloneTmp 残留(其余文件没用),清理\n await rm(cloneTmp, { recursive: true, force: true }).catch(() => {});\n }\n}\n\n/**\n * II-3:合并 manifest 里的 author / homepage / interface 字段到 git clone 下来\n * 的 plugin.json,**仅填补缺失字段**,不覆盖上游已有声明。\n *\n * 安全:plugin.json 不存在 → 创建最小一份(name/version/description from manifest);\n * 读 / parse 失败 → silent skip,不阻塞安装(plugin 主体已落盘)。\n */\nexport async function enrichExternalPluginJson(\n pluginDir: string,\n manifest: RushPluginManifestResponse\n): Promise<void> {\n const pjPath = resolve(pluginDir, '.claude-plugin', 'plugin.json');\n let existing: Record<string, unknown> = {};\n try {\n const raw = await import('node:fs/promises').then((m) =>\n m.readFile(pjPath, 'utf8')\n );\n const parsed = JSON.parse(raw);\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n existing = parsed as Record<string, unknown>;\n }\n } catch {\n // 缺失 / 损坏 → 用最小 plugin.json 代替\n await mkdir(resolve(pluginDir, '.claude-plugin'), { recursive: true });\n existing = {\n name: manifest.name,\n version: manifest.version || '1.0.0',\n description: manifest.description || '',\n };\n }\n\n // 推导 homepage:优先 manifest.homepage > redirectTo 推导的 GitHub URL\n let derivedHomepage = manifest.homepage;\n if (!derivedHomepage && manifest.redirectTo?.kind === 'github') {\n const r = manifest.redirectTo;\n derivedHomepage = `https://github.com/${r.owner}/${r.repo}`;\n if (r.path) derivedHomepage += `/tree/${r.ref ?? 'main'}/${r.path}`;\n }\n\n // 仅填补缺失:不覆盖上游已有\n if (!existing.author && manifest.author) {\n existing.author = { ...manifest.author };\n }\n if (!existing.homepage && derivedHomepage) {\n existing.homepage = derivedHomepage;\n }\n\n // interface 区块:合并补充字段(codex 详情页\"开发者 / 网站\")\n const ifaceExisting = (existing.interface as Record<string, unknown>) ?? {};\n const ifaceOut = { ...ifaceExisting };\n if (!ifaceOut.developerName && manifest.author?.name) {\n ifaceOut.developerName = manifest.author.name;\n }\n if (!ifaceOut.websiteURL && derivedHomepage) {\n ifaceOut.websiteURL = derivedHomepage;\n }\n if (Object.keys(ifaceOut).length > 0) {\n existing.interface = ifaceOut;\n }\n\n await writeFile(pjPath, `${JSON.stringify(existing, null, 2)}\\n`, 'utf8');\n}\n\n/** 简单 git clone runner(与 plugins/resolver.ts 的 defaultGitRunner 形态一致) */\nasync function runGitClone(args: string[]): Promise<void> {\n await new Promise<void>((res, rej) => {\n const child = spawn('git', args, {\n stdio: ['ignore', 'pipe', 'pipe'],\n });\n let stderr = '';\n child.stderr?.on('data', (chunk: Buffer | string) => {\n stderr += typeof chunk === 'string' ? chunk : chunk.toString();\n });\n child.on('error', (err) => rej(err));\n child.on('close', (code) => {\n if (code === 0) res();\n else\n rej(\n new Error(`git ${args.join(' ')} exited ${code}: ${stderr.trim()}`)\n );\n });\n });\n}\n\n/**\n * 把 manifest 拼成 plugin.json 形态(写到 `.claude-plugin/plugin.json`)。\n *\n * II-3:除了基础字段,还把 author / homepage / interface(含 developerName /\n * websiteURL / category)写进来,让 codex mirror 复制 versionDir 后的 full plugin.json\n * 也含详情页要的元数据(不再依赖 sync stub 的 buildPluginJson 二次处理)。\n *\n * 字段优先级:\n * - websiteURL:homepage > redirectTo 推导的 GitHub URL(外部 plugin)\n * - developerName:author.name\n * - category:作为业务串原样落盘;codex mirror 的 normalizeCategory() 会再\n * 映射成 Codex UI 大类(这里不映射,避免 rush 用户 plugin 走不同路径出现不一致)\n */\nexport function buildPluginJsonShape(\n manifest: RushPluginManifestResponse,\n context: { mcpServers: Record<string, unknown> }\n): Record<string, unknown> {\n const out: Record<string, unknown> = {\n name: manifest.name,\n version: manifest.version || '1.0.0',\n description: manifest.description,\n };\n if (Object.keys(context.mcpServers).length > 0) {\n out.mcpServers = context.mcpServers;\n }\n\n // 顶层 author / homepage(codex 详情页\"信息\"区块读取)\n if (manifest.author && Object.keys(manifest.author).length > 0) {\n out.author = { ...manifest.author };\n }\n // homepage 优先用 manifest.homepage,否则用 redirectTo 推导(外部 plugin)\n let homepage = manifest.homepage;\n if (!homepage && manifest.redirectTo?.kind === 'github') {\n const r = manifest.redirectTo;\n homepage = `https://github.com/${r.owner}/${r.repo}`;\n if (r.path) homepage += `/tree/${r.ref ?? 'main'}/${r.path}`;\n }\n if (homepage) out.homepage = homepage;\n\n // interface 区块(codex 详情页\"概览\"区块读取)\n const ifaceOut: Record<string, unknown> = {};\n if (manifest.author?.name) ifaceOut.developerName = manifest.author.name;\n if (homepage) ifaceOut.websiteURL = homepage;\n if (Object.keys(ifaceOut).length > 0) out.interface = ifaceOut;\n\n return out;\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":";;;;;;;;;AAgBA,SAAS,aAAa,mBAAmB;AACzC,SAAS,QAAQ,UAAU,QAAAA,aAAY;AACvC,SAAS,WAAWC,cAAa,WAAW;;;ACV5C,SAAS,cAAc,aAAa;AACpC,SAAS,kBAAkB;AAC3B,SAAS,OAAO,QAAQ,IAAI,MAAM,iBAAiB;AACnD,SAAS,WAAW,aAAa,eAAe;AAwIhD,eAAsB,qBACpB,QACA,OAAyB,CAAC,GACgB;AAC1C,QAAM,UAAU,KAAK,WAAW;AAChC,QAAM,MAAM,GAAG,cAAc,OAAO,IAAI,CAAC,GAAG,OAAO,IAAI;AAEvD,QAAM,UAAkC;AAAA,IACtC,QAAQ;AAAA,EACV;AACA,MAAI,KAAK,OAAO;AACd,YAAQ,gBAAgB,UAAU,KAAK,KAAK;AAAA,EAC9C;AAEA,QAAM,WAAW,MAAM,QAAQ,KAAK,EAAE,QAAQ,CAAC;AAE/C,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI;AAAA,MACR,yCAAyC,GAAG,KAAK,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,IACzF;AAAA,EACF;AAEA,QAAM,OAAQ,MAAM,SAAS,KAAK;AAElC,MAAI,CAAC,KAAK,WAAW,CAAC,MAAM,QAAQ,KAAK,OAAO,GAAG;AACjD,UAAM,IAAI;AAAA,MACR,qCAAqC,GAAG;AAAA,IAC1C;AAAA,EACF;AAEA,SAAO;AACT;AAOA,eAAsB,gBACpB,QACA,YACA,OAAyB,CAAC,GACW;AACrC,QAAM,UAAU,KAAK,WAAW;AAChC,QAAM,MAAM,GAAG,cAAc,OAAO,IAAI,CAAC,GAAG,OAAO,IAAI,oBAAoB,mBAAmB,UAAU,CAAC;AAEzG,QAAM,UAAkC;AAAA,IACtC,QAAQ;AAAA,EACV;AACA,MAAI,KAAK,OAAO;AACd,YAAQ,gBAAgB,UAAU,KAAK,KAAK;AAAA,EAC9C;AAEA,QAAM,WAAW,MAAM,QAAQ,KAAK,EAAE,QAAQ,CAAC;AAE/C,MAAI,SAAS,WAAW,KAAK;AAC3B,UAAM,IAAI;AAAA,MACR,WAAW,UAAU;AAAA,IACvB;AAAA,EACF;AACA,MAAI,SAAS,WAAW,KAAK;AAC3B,UAAM,IAAI;AAAA,MACR,WAAW,UAAU,sCAAsC,OAAO,IAAI;AAAA,IACxE;AAAA,EACF;AACA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI;AAAA,MACR,2BAA2B,UAAU,UAAU,GAAG,KAAK,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,IAC/F;AAAA,EACF;AAEA,SAAQ,MAAM,SAAS,KAAK;AAC9B;AAmBA,eAAsB,sBACpB,QACA,YACA,UACA,OAAyB,CAAC,GACS;AACnC,QAAM,UAAU,KAAK,WAAW;AAEhC,QAAM,UAAU,SACb,MAAM,GAAG,EACT,OAAO,CAAC,MAAM,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,MAAM,mBAAmB,CAAC,CAAC,EAChC,KAAK,GAAG;AACX,QAAM,MAAM,GAAG,cAAc,OAAO,IAAI,CAAC,GAAG,OAAO,IAAI,oBAAoB,mBAAmB,UAAU,CAAC,UAAU,OAAO;AAE1H,QAAM,UAAkC,EAAE,QAAQ,mBAAmB;AACrE,MAAI,KAAK,MAAO,SAAQ,gBAAgB,UAAU,KAAK,KAAK;AAE5D,QAAM,WAAW,MAAM,QAAQ,KAAK,EAAE,QAAQ,CAAC;AAC/C,MAAI,SAAS,WAAW,KAAK;AAC3B,UAAM,IAAI;AAAA,MACR,SAAS,QAAQ,mCAAmC,UAAU;AAAA,IAChE;AAAA,EACF;AACA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI;AAAA,MACR,yBAAyB,QAAQ,UAAU,GAAG,KAAK,SAAS,MAAM,IAAI,SAAS,UAAU;AAAA,IAC3F;AAAA,EACF;AACA,SAAQ,MAAM,SAAS,KAAK;AAC9B;AA4BA,eAAsB,sBACpB,QACA,YACA,WACA,OAAqC,CAAC,GACD;AAErC,eAAa,YAAY,aAAa;AAGtC,QAAM,WAAW,MAAM,gBAAgB,QAAQ,YAAY,IAAI;AAG/D,MAAI,SAAS,WAAW,2BAA2B;AACjD,QAAI,SAAS,YAAY;AAEvB,YAAM,4BAA4B,UAAU,SAAS;AACrD,aAAO;AAAA,IACT;AAEA,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAGA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAOA,eAAe,0BACb,QACA,YACA,UACA,WACA,MACqC;AAErC,MAAI,aAAsC,CAAC;AAC3C,MAAI,SAAS,cAAc,OAAO,KAAK,SAAS,UAAU,EAAE,SAAS,GAAG;AACtE,iBAAa,EAAE,GAAG,SAAS,WAAW;AAEtC,QAAI,KAAK,WAAW,OAAO,KAAK,KAAK,OAAO,EAAE,SAAS,GAAG;AACxD,mBAAa,qBAAqB,YAAY,KAAK,OAAO;AAAA,IAC5D;AAAA,EACF;AAEA,QAAM,aAAsC,qBAAqB,UAAU;AAAA,IACzE;AAAA,EACF,CAAC;AAGD,QAAM,SAAS,GAAG,SAAS,IAAI,WAAW,CAAC;AAE3C,MAAI;AAEF,UAAM,gBAAgB,QAAQ,QAAQ,gBAAgB;AACtD,UAAM,MAAM,eAAe,EAAE,WAAW,KAAK,CAAC;AAC9C,UAAM;AAAA,MACJ,QAAQ,eAAe,aAAa;AAAA,MACpC,GAAG,KAAK,UAAU,YAAY,MAAM,CAAC,CAAC;AAAA;AAAA,MACtC;AAAA,IACF;AAGA,QAAI,SAAS,UAAU,SAAS,OAAO,SAAS,GAAG;AAEjD,YAAM;AAAA,QACJ,QAAQ,QAAQ,aAAa;AAAA,QAC7B;AAAA,QACA;AAAA,MACF;AACA,YAAM,cAAc,GAAG,cAAc,OAAO,IAAI,CAAC,GAAG,OAAO,IAAI;AAC/D,iBAAW,SAAS,SAAS,QAAQ;AACnC,YAAI;AAGF,gBAAM,OAAO;AAAA,YACX;AAAA,YACA;AAAA,YACA;AAAA,YACA,MAAM;AAAA,YACN;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA,GAAI,KAAK,QAAQ,CAAC,MAAM,KAAK,KAAK,IAAI,CAAC;AAAA,UACzC;AACA,uBAAa,OAAO,MAAM;AAAA,YACxB,KAAK;AAAA;AAAA;AAAA,YAGL,SAAS;AAAA,YACT,OAAO;AAAA,UACT,CAAC;AAAA,QACH,SAAS,KAAK;AACZ,gBAAM,SACJ,OAAO,OAAO,QAAQ,YAAY,YAAY,MACxC,IAA2B,QAAQ,SAAS,KAAK,KACnD;AAEN,cACE,OAAO,SAAS,KAAK,KACrB,OAAO,SAAS,KAAK,KACrB,OAAO,SAAS,cAAc,KAC9B,OAAO,SAAS,WAAW,GAC3B;AACA,kBAAM,IAAI,eAAe,MAAM,MAAM,GAAG;AAAA,UAC1C;AAEA,iBAAO;AAAA,YACL,uCAAuC,MAAM,IAAI,MAC/C,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CACjD;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAKA,UAAM,eAAe,QAAQ,QAAQ,SAAS;AAC9C,UAAM,YAAY,QAAQ,QAAQ,QAAQ;AAC1C,QACE,MAAM,KAAK,YAAY,EACpB,KAAK,CAAC,MAAM,EAAE,YAAY,CAAC,EAC3B,MAAM,MAAM,KAAK,GACpB;AACA,UACE,CAAE,MAAM,KAAK,SAAS,EACnB,KAAK,CAAC,MAAM,EAAE,YAAY,CAAC,EAC3B,MAAM,MAAM,KAAK,GACpB;AACA,cAAM,OAAO,cAAc,SAAS;AAAA,MACtC;AAAA,IACF;AAGA,UAAM,GAAG,QAAQ,QAAQ,aAAa,GAAG,EAAE,OAAO,KAAK,CAAC;AACxD,UAAM,GAAG,QAAQ,QAAQ,SAAS,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACrE,UAAM,GAAG,QAAQ,QAAQ,SAAS,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACrE,UAAM,GAAG,QAAQ,QAAQ,QAAQ,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACpE,UAAM,GAAG,QAAQ,QAAQ,SAAS,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACrE,UAAM,GAAG,QAAQ,QAAQ,WAAW,GAAG,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AAGvE,UAAM,GAAG,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACpD,UAAM,MAAM,QAAQ,WAAW,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AACzD,UAAM,OAAO,QAAQ,SAAS;AAAA,EAChC,SAAS,KAAK;AAEZ,UAAM,GAAG,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AACjE,UAAM;AAAA,EACR;AAEA,SAAO;AACT;AAuBA,eAAe,yBACb,QACA,YACA,UACA,WACA,MACe;AAEf,MAAI,aAAsC,CAAC;AAC3C,MAAI,SAAS,cAAc,OAAO,KAAK,SAAS,UAAU,EAAE,SAAS,GAAG;AACtE,iBAAa,EAAE,GAAG,SAAS,WAAW;AACtC,QAAI,KAAK,WAAW,OAAO,KAAK,KAAK,OAAO,EAAE,SAAS,GAAG;AACxD,mBAAa,qBAAqB,YAAY,KAAK,OAAO;AAAA,IAC5D;AAAA,EACF;AAEA,QAAM,aAAsC,qBAAqB,UAAU;AAAA,IACzE;AAAA,EACF,CAAC;AAED,QAAM,SAAS,GAAG,SAAS,IAAI,WAAW,CAAC;AAE3C,MAAI;AACF,UAAM,MAAM,QAAQ,QAAQ,gBAAgB,GAAG,EAAE,WAAW,KAAK,CAAC;AAClE,UAAM;AAAA,MACJ,QAAQ,QAAQ,kBAAkB,aAAa;AAAA,MAC/C,GAAG,KAAK,UAAU,YAAY,MAAM,CAAC,CAAC;AAAA;AAAA,MACtC;AAAA,IACF;AAGA,QAAI,OAAO,KAAK,UAAU,EAAE,SAAS,GAAG;AACtC,YAAM;AAAA,QACJ,QAAQ,QAAQ,WAAW;AAAA,QAC3B,GAAG,KAAK,UAAU,EAAE,WAAW,GAAG,MAAM,CAAC,CAAC;AAAA;AAAA,QAC1C;AAAA,MACF;AAAA,IACF;AAGA,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAGA,UAAM,GAAG,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACpD,UAAM,MAAM,QAAQ,WAAW,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AACzD,UAAM,OAAO,QAAQ,SAAS;AAAA,EAChC,SAAS,KAAK;AACZ,UAAM,GAAG,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AACjE,UAAM;AAAA,EACR;AACF;AAQA,eAAe,2BACb,QACA,YACA,UACA,SACA,MACe;AAEf,QAAM,UAAmB,CAAC;AAC1B,aAAW,KAAK,SAAS,UAAU,CAAC,GAAG;AACrC,QAAI,EAAE,MAAM;AAGV,YAAM,gBACJ,EAAE,KAAK,SAAS,KAAK,KAAK,EAAE,KAAK,SAAS,KAAK,IAC3C,EAAE,OACF,GAAG,EAAE,KAAK,SAAS,GAAG,IAAI,EAAE,KAAK,MAAM,GAAG,EAAE,IAAI,EAAE,IAAI;AAC5D,cAAQ,KAAK,EAAE,MAAM,SAAS,MAAM,EAAE,MAAM,MAAM,cAAc,CAAC;AAAA,IACnE;AAAA,EACF;AACA,aAAW,KAAK,SAAS,UAAU,CAAC,GAAG;AACrC,YAAQ,KAAK,EAAE,MAAM,SAAS,MAAM,EAAE,MAAM,MAAM,EAAE,KAAK,CAAC;AAAA,EAC5D;AACA,aAAW,KAAK,SAAS,SAAS,CAAC,GAAG;AACpC,YAAQ,KAAK,EAAE,MAAM,QAAQ,MAAM,EAAE,MAAM,MAAM,EAAE,KAAK,CAAC;AAAA,EAC3D;AACA,aAAW,KAAK,SAAS,YAAY,CAAC,GAAG;AACvC,YAAQ,KAAK,EAAE,MAAM,WAAW,MAAM,EAAE,MAAM,MAAM,EAAE,KAAK,CAAC;AAAA,EAC9D;AACA,MAAI,QAAQ,WAAW,EAAG;AAE1B,QAAM,YAA+D,CAAC;AACtE,MAAI,KAAK,YAAY,OAAW,WAAU,UAAU,KAAK;AACzD,MAAI,KAAK,UAAU,UAAa,KAAK,UAAU,MAAM;AACnD,cAAU,QAAQ,KAAK;AAAA,EACzB;AAEA,aAAW,KAAK,SAAS;AACvB,QAAI;AACF,YAAM,OAAO,MAAM;AAAA,QACjB;AAAA,QACA;AAAA,QACA,EAAE;AAAA,QACF;AAAA,MACF;AACA,UAAI,KAAK,WAAW;AAClB,eAAO;AAAA,UACL,cAAc,EAAE,IAAI,KAAK,EAAE,IAAI,WAAW,EAAE,IAAI;AAAA,QAClD;AACA;AAAA,MACF;AAGA,UAAI,OAA+B;AACnC,UAAI,OAAO,KAAK,YAAY,UAAU;AACpC,eAAO,KAAK;AAAA,MACd,WAAW,OAAO,KAAK,cAAc,UAAU;AAC7C,cAAM,UAAU,KAAK,WAAW;AAChC,cAAM,MAAM,MAAM,QAAQ,KAAK,SAAS;AACxC,YAAI,IAAI,IAAI;AACV,gBAAM,MAAM,MAAM,IAAI,YAAY;AAClC,iBAAO,OAAO,KAAK,GAAG;AAAA,QACxB,OAAO;AACL,iBAAO;AAAA,YACL,gDAAgD,EAAE,IAAI,KAAK,EAAE,IAAI,MAAM,IAAI,MAAM;AAAA,UACnF;AACA;AAAA,QACF;AAAA,MACF,OAAO;AACL,eAAO;AAAA,UACL,cAAc,EAAE,IAAI,KAAK,EAAE,IAAI;AAAA,QACjC;AACA;AAAA,MACF;AAKA,YAAM,OAAO,QAAQ,SAAS,EAAE,IAAI;AACpC,YAAM,OAAO,YAAY,OAAO;AAChC,YAAM,cAAc,KAAK,SAAS,GAAG,IAAI,OAAO,GAAG,IAAI;AACvD,UAAI,SAAS,QAAQ,CAAC,KAAK,WAAW,WAAW,GAAG;AAClD,eAAO;AAAA,UACL,gCAAgC,EAAE,IAAI,KAAK,EAAE,IAAI,yBAAyB,EAAE,IAAI;AAAA,QAClF;AACA;AAAA,MACF;AACA,YAAM,MAAM,QAAQ,MAAM,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AACpD,YAAM,UAAU,MAAM,IAAI;AAAA,IAC5B,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,8BAA8B,EAAE,IAAI,KAAK,EAAE,IAAI,MAAM,EAAE,IAAI,MACzD,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CACjD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAiBA,eAAe,4BACb,UACA,WACe;AACf,QAAM,IAAI,SAAS;AACnB,MAAI,CAAC,KAAK,EAAE,SAAS,UAAU;AAC7B,UAAM,IAAI;AAAA,MACR,WAAW,SAAS,IAAI,sCAAsC,GAAG,QAAQ,WAAW;AAAA,IAEtF;AAAA,EACF;AACA,MAAI,CAAC,EAAE,SAAS,CAAC,EAAE,MAAM;AACvB,UAAM,IAAI;AAAA,MACR,WAAW,SAAS,IAAI;AAAA,IAC1B;AAAA,EACF;AAEA,QAAM,MAAM,sBAAsB,EAAE,KAAK,IAAI,EAAE,IAAI;AACnD,QAAM,WAAW,GAAG,SAAS,IAAI,WAAW,CAAC;AAE7C,QAAM,OAAO,CAAC,SAAS,WAAW,GAAG;AACrC,MAAI,EAAE,IAAK,MAAK,KAAK,YAAY,EAAE,GAAG;AACtC,OAAK,KAAK,MAAM,KAAK,QAAQ;AAE7B,MAAI;AACF,UAAM,YAAY,IAAI;AAGtB,QAAI;AACJ,QAAI,EAAE,QAAQ,EAAE,KAAK,SAAS,GAAG;AAE/B,YAAM,YAAY,QAAQ,UAAU,EAAE,IAAI;AAC1C,YAAM,cAAc,YAAY,QAAQ;AACxC,YAAM,kBAAkB,YAAY,SAAS,GAAG,IAC5C,cACA,GAAG,WAAW;AAClB,UAAI,cAAc,eAAe,CAAC,UAAU,WAAW,eAAe,GAAG;AACvE,cAAM,IAAI,MAAM,uCAAuC,EAAE,IAAI,GAAG;AAAA,MAClE;AACA,YAAM,KAAK,MAAM,KAAK,SAAS,EAAE,MAAM,MAAM,IAAI;AACjD,UAAI,CAAC,MAAM,CAAC,GAAG,YAAY,GAAG;AAC5B,cAAM,IAAI;AAAA,UACR,oBAAoB,EAAE,IAAI,2BAA2B,GAAG;AAAA,QAC1D;AAAA,MACF;AACA,kBAAY;AAAA,IACd,OAAO;AACL,kBAAY;AAAA,IACd;AAGA,UAAM,GAAG,WAAW,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC;AACpD,UAAM,MAAM,QAAQ,WAAW,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AACzD,UAAM,OAAO,WAAW,SAAS;AAKjC,UAAM,yBAAyB,WAAW,QAAQ;AAAA,EACpD,SAAS,KAAK;AACZ,UAAM,GAAG,UAAU,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAEnE,UAAM;AAAA,EACR,UAAE;AAEA,UAAM,GAAG,UAAU,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAAA,EACrE;AACF;AASA,eAAsB,yBACpB,WACA,UACe;AACf,QAAM,SAAS,QAAQ,WAAW,kBAAkB,aAAa;AACjE,MAAI,WAAoC,CAAC;AACzC,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,aAAkB,EAAE;AAAA,MAAK,CAAC,MACjD,EAAE,SAAS,QAAQ,MAAM;AAAA,IAC3B;AACA,UAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,QAAI,UAAU,OAAO,WAAW,YAAY,CAAC,MAAM,QAAQ,MAAM,GAAG;AAClE,iBAAW;AAAA,IACb;AAAA,EACF,QAAQ;AAEN,UAAM,MAAM,QAAQ,WAAW,gBAAgB,GAAG,EAAE,WAAW,KAAK,CAAC;AACrE,eAAW;AAAA,MACT,MAAM,SAAS;AAAA,MACf,SAAS,SAAS,WAAW;AAAA,MAC7B,aAAa,SAAS,eAAe;AAAA,IACvC;AAAA,EACF;AAGA,MAAI,kBAAkB,SAAS;AAC/B,MAAI,CAAC,mBAAmB,SAAS,YAAY,SAAS,UAAU;AAC9D,UAAM,IAAI,SAAS;AACnB,sBAAkB,sBAAsB,EAAE,KAAK,IAAI,EAAE,IAAI;AACzD,QAAI,EAAE,KAAM,oBAAmB,SAAS,EAAE,OAAO,MAAM,IAAI,EAAE,IAAI;AAAA,EACnE;AAGA,MAAI,CAAC,SAAS,UAAU,SAAS,QAAQ;AACvC,aAAS,SAAS,EAAE,GAAG,SAAS,OAAO;AAAA,EACzC;AACA,MAAI,CAAC,SAAS,YAAY,iBAAiB;AACzC,aAAS,WAAW;AAAA,EACtB;AAGA,QAAM,gBAAiB,SAAS,aAAyC,CAAC;AAC1E,QAAM,WAAW,EAAE,GAAG,cAAc;AACpC,MAAI,CAAC,SAAS,iBAAiB,SAAS,QAAQ,MAAM;AACpD,aAAS,gBAAgB,SAAS,OAAO;AAAA,EAC3C;AACA,MAAI,CAAC,SAAS,cAAc,iBAAiB;AAC3C,aAAS,aAAa;AAAA,EACxB;AACA,MAAI,OAAO,KAAK,QAAQ,EAAE,SAAS,GAAG;AACpC,aAAS,YAAY;AAAA,EACvB;AAEA,QAAM,UAAU,QAAQ,GAAG,KAAK,UAAU,UAAU,MAAM,CAAC,CAAC;AAAA,GAAM,MAAM;AAC1E;AAGA,eAAe,YAAY,MAA+B;AACxD,QAAM,IAAI,QAAc,CAAC,KAAK,QAAQ;AACpC,UAAM,QAAQ,MAAM,OAAO,MAAM;AAAA,MAC/B,OAAO,CAAC,UAAU,QAAQ,MAAM;AAAA,IAClC,CAAC;AACD,QAAI,SAAS;AACb,UAAM,QAAQ,GAAG,QAAQ,CAAC,UAA2B;AACnD,gBAAU,OAAO,UAAU,WAAW,QAAQ,MAAM,SAAS;AAAA,IAC/D,CAAC;AACD,UAAM,GAAG,SAAS,CAAC,QAAQ,IAAI,GAAG,CAAC;AACnC,UAAM,GAAG,SAAS,CAAC,SAAS;AAC1B,UAAI,SAAS,EAAG,KAAI;AAAA;AAElB;AAAA,UACE,IAAI,MAAM,OAAO,KAAK,KAAK,GAAG,CAAC,WAAW,IAAI,KAAK,OAAO,KAAK,CAAC,EAAE;AAAA,QACpE;AAAA,IACJ,CAAC;AAAA,EACH,CAAC;AACH;AAeO,SAAS,qBACd,UACA,SACyB;AACzB,QAAM,MAA+B;AAAA,IACnC,MAAM,SAAS;AAAA,IACf,SAAS,SAAS,WAAW;AAAA,IAC7B,aAAa,SAAS;AAAA,EACxB;AACA,MAAI,OAAO,KAAK,QAAQ,UAAU,EAAE,SAAS,GAAG;AAC9C,QAAI,aAAa,QAAQ;AAAA,EAC3B;AAGA,MAAI,SAAS,UAAU,OAAO,KAAK,SAAS,MAAM,EAAE,SAAS,GAAG;AAC9D,QAAI,SAAS,EAAE,GAAG,SAAS,OAAO;AAAA,EACpC;AAEA,MAAI,WAAW,SAAS;AACxB,MAAI,CAAC,YAAY,SAAS,YAAY,SAAS,UAAU;AACvD,UAAM,IAAI,SAAS;AACnB,eAAW,sBAAsB,EAAE,KAAK,IAAI,EAAE,IAAI;AAClD,QAAI,EAAE,KAAM,aAAY,SAAS,EAAE,OAAO,MAAM,IAAI,EAAE,IAAI;AAAA,EAC5D;AACA,MAAI,SAAU,KAAI,WAAW;AAG7B,QAAM,WAAoC,CAAC;AAC3C,MAAI,SAAS,QAAQ,KAAM,UAAS,gBAAgB,SAAS,OAAO;AACpE,MAAI,SAAU,UAAS,aAAa;AACpC,MAAI,OAAO,KAAK,QAAQ,EAAE,SAAS,EAAG,KAAI,YAAY;AAEtD,SAAO;AACT;AAMO,IAAM,iBAAN,cAA6B,MAAM;AAAA,EACxC,YAAY,WAAmB,QAAgB;AAC7C;AAAA,MACE,UAAU,SAAS,mCAAmC,MAAM;AAAA,IAC9D;AACA,SAAK,OAAO;AAAA,EACd;AACF;AAOA,SAAS,qBACP,YACA,SACyB;AACzB,QAAM,SAAkC,CAAC;AACzC,aAAW,CAAC,YAAY,YAAY,KAAK,OAAO,QAAQ,UAAU,GAAG;AACnE,QAAI,CAAC,gBAAgB,OAAO,iBAAiB,UAAU;AACrD,aAAO,UAAU,IAAI;AACrB;AAAA,IACF;AACA,UAAM,MAAM,EAAE,GAAI,aAAyC;AAG3D,QAAI,IAAI,OAAO,OAAO,IAAI,QAAQ,UAAU;AAC1C,YAAM,MAA8B,CAAC;AACrC,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,IAAI,GAA6B,GAAG;AACtE,YAAI,CAAC,IAAI,gBAAgB,GAAG,OAAO;AAAA,MACrC;AACA,UAAI,MAAM;AAAA,IACZ;AAGA,QAAI,IAAI,WAAW,OAAO,IAAI,YAAY,UAAU;AAClD,YAAM,UAAkC,CAAC;AACzC,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO;AAAA,QAC1B,IAAI;AAAA,MACN,GAAG;AACD,gBAAQ,CAAC,IAAI,gBAAgB,GAAG,OAAO;AAAA,MACzC;AACA,UAAI,UAAU;AAAA,IAChB;AAEA,WAAO,UAAU,IAAI;AAAA,EACvB;AACA,SAAO;AACT;AAEA,SAAS,gBACP,OACA,SACQ;AACR,SAAO,MAAM,QAAQ,kBAAkB,CAAC,OAAO,QAAgB;AAC7D,WAAO,OAAO,UAAU,QAAQ,GAAG,IAAI;AAAA,EACzC,CAAC;AACH;AAOA,SAAS,aAAa,OAAe,OAAqB;AACxD,MACE,CAAC,SACD,MAAM,SAAS,IAAI,KACnB,MAAM,WAAW,GAAG,KACpB,MAAM,SAAS,IAAI,GACnB;AACA,UAAM,IAAI;AAAA,MACR,WAAW,KAAK,KAAK,KAAK;AAAA,IAC5B;AAAA,EACF;AACF;AAKA,SAAS,cAAc,MAAsB;AAC3C,QAAM,WAAW,KAAK,MAAM,GAAG,EAAE,CAAC;AAClC,MACE,aAAa,eACb,aAAa,eACb,aAAa,WACb;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;;;ADx5BO,SAAS,kBACd,UACA,OAA2B,CAAC,GACP;AACrB,UAAQ,SAAS,OAAO,MAAM;AAAA,IAC5B,KAAK;AACH,aAAO,6BAA6B,SAAS,QAAQ,IAAI;AAAA,IAC3D,KAAK;AAAA,IACL,KAAK;AACH,aAAO,mCAAmC,QAAQ;AAAA,IACpD;AACE,aAAO,aAAa,CAAC;AAAA,EACzB;AACF;AAYO,SAAS,6BACd,QACA,OAA2B,CAAC,GACP;AACrB,QAAM,QAAQ,KAAK,SAAS,aAAa;AACzC,QAAM,YAA+D,CAAC;AACtE,MAAI,KAAK,YAAY,OAAW,WAAU,UAAU,KAAK;AACzD,MAAI,UAAU,UAAa,UAAU,KAAM,WAAU,QAAQ;AAG7D,QAAM,UAAU,aAAa,OAAO,IAAI;AACxC,QAAM,UAAU,KAAK,WAAW,WAAW;AAC3C,SAAO,OAAO,UAA0D;AACtE,QAAI,OAAO,MAAM,SAAS,YAAY,MAAM,KAAK,WAAW,EAAG,QAAO,CAAC;AACvE,QAAI;AACF,YAAM,SAAS,MAAM,gBAAgB,QAAQ,MAAM,MAAM,SAAS;AAClE,YAAM,WAAW;AAAA,QACf;AAAA,MACF;AACA,YAAM,SACJ,OAAO,cAAc,OAAO,KAAK,OAAO,UAAU,EAAE,SAAS;AAE/D,YAAM,eAAe,GAAG,OAAO,iBAAiB;AAAA,QAC9C,MAAM;AAAA,MACR,CAAC;AAOD,YAAM,aAAa,OAAO,WAAW;AACrC,YAAM,qBAAqB,cAAc,CAAC,CAAC,OAAO;AAElD,YAAM,eACJ,MAAM,QAAQ,OAAO,MAAM,KAAK,OAAO,OAAO,SAAS,IACnD,OAAO,OAAO;AAAA,QACZ,CAAC,MACC,CAAC,CAAC,KAAK,OAAO,EAAE,SAAS,YAAY,EAAE,KAAK,SAAS;AAAA,MACzD,IACA,CAAC;AACP,YAAM,qBAID,CAAC;AACN,iBAAW,KAAK,cAAc;AAE5B,cAAM,MAAM,GAAG,OAAO,iBAAiB,mBAAmB,MAAM,IAAI,CAAC;AACrE,YAAI;AAEJ,YAAI,oBAAoB;AAAA,QAExB,WACE,cACA,OAAO,EAAE,SAAS,YAClB,EAAE,KAAK,SAAS,GAChB;AAEA,gBAAM,cAAc,oBAAoB,EAAE,IAAI;AAC9C,cAAI;AACF,kBAAM,OAAO,MAAM;AAAA,cACjB;AAAA,cACA,MAAM;AAAA,cACN;AAAA,cACA;AAAA,YACF;AACA,gBAAI,OAAO,KAAK,YAAY,YAAY,KAAK,QAAQ,SAAS,GAAG;AAC/D,2BAAa,KAAK;AAAA,YACpB;AAAA,UACF,QAAQ;AAAA,UAER;AAAA,QACF,OAAO;AAEL,gBAAM,QAAQ,GAAG,OAAO,gBAAgB,mBAAmB,EAAE,IAAI,CAAC;AAClE,cAAI;AACF,kBAAM,UAAkC;AAAA,cACtC,QAAQ;AAAA,YACV;AACA,gBAAI,MAAO,SAAQ,gBAAgB,UAAU,KAAK;AAClD,kBAAM,MAAM,MAAM,QAAQ,OAAO,EAAE,QAAQ,CAAC;AAC5C,gBAAI,IAAI,IAAI;AACV,oBAAM,OAAO,MAAM,IAAI,KAAK;AAC5B,kBAAI,KAAK,SAAS,EAAG,cAAa;AAAA,YACpC;AAAA,UACF,QAAQ;AAAA,UAER;AAAA,QACF;AACA,2BAAmB,KAAK;AAAA,UACtB,MAAM,EAAE;AAAA,UACR;AAAA,UACA,GAAI,eAAe,SAAY,EAAE,WAAW,IAAI,CAAC;AAAA,QACnD,CAAC;AAAA,MACH;AAIA,aAAO;AAAA,QACL,GAAI,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC;AAAA,QAC7C,GAAI,SACA,EAAE,YAAY,OAAO,WAAsC,IAC3D,CAAC;AAAA,QACL,GAAI,mBAAmB,SAAS,IAAI,EAAE,mBAAmB,IAAI,CAAC;AAAA,QAC9D;AAAA,MACF;AAAA,IACF,QAAQ;AAEN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AACF;AAEA,SAAS,aAAa,MAAsB;AAC1C,QAAM,WAAW,KAAK,MAAM,GAAG,EAAE,CAAC;AAClC,MACE,aAAa,eACb,aAAa,eACb,aAAa,WACb;AACA,WAAO,UAAU,IAAI;AAAA,EACvB;AACA,SAAO,WAAW,IAAI;AACxB;AAkBO,SAAS,mCACd,UACqB;AACrB,SAAO,OAAO,UAA0D;AACtE,UAAM,UAAU,uBAAuB,KAAK;AAC5C,QAAI,YAAY,KAAM,QAAO,CAAC;AAE9B,UAAM,YAAYC,aAAY,SAAS,SAAS,OAAO;AAGvD,UAAM,OAAOA,aAAY,SAAS,OAAO;AACzC,UAAM,cAAc,KAAK,SAAS,GAAG,IAAI,OAAO,OAAO;AACvD,UAAM,OAAO,cAAc,QAAQ,UAAU,WAAW,WAAW;AACnE,QAAI,CAAC,KAAM,QAAO,CAAC;AAEnB,QAAI;AACJ,QAAI;AACJ,QAAI;AAMJ,QAAI;AACF,YAAM,KAAK,MAAM;AAAA,QACfA,aAAY,WAAW,kBAAkB,aAAa;AAAA,QACtD;AAAA,MACF;AACA,YAAM,SAAS,KAAK,MAAM,EAAE;AAC5B,iBAAW,mBAAmB,MAAM;AAEpC,YAAM,SAAU,OAAoC;AACpD,UACE,UACA,OAAO,WAAW,YAClB,CAAC,MAAM,QAAQ,MAAM,KACrB,OAAO,KAAK,MAAiC,EAAE,SAAS,GACxD;AACA,qBAAa;AAAA,MACf;AAAA,IACF,QAAQ;AAAA,IAER;AAGA,QAAI;AACF,YAAM,MAAM,MAAM,SAASA,aAAY,WAAW,WAAW,GAAG,MAAM;AACtE,YAAM,SAAS,KAAK,MAAM,GAAG;AAM7B,UAAI,UAA0C;AAC9C,UACE,UACA,OAAO,WAAW,YAClB,CAAC,MAAM,QAAQ,MAAM,KACrB,gBAAgB,UAChB,OAAQ,OAAoC,eAAe,UAC3D;AACA,kBAAW,OACR;AAAA,MACL,WACE,UACA,OAAO,WAAW,YAClB,CAAC,MAAM,QAAQ,MAAM,GACrB;AACA,kBAAU;AAAA,MACZ;AACA,UAAI,WAAW,OAAO,KAAK,OAAO,EAAE,SAAS,GAAG;AAC9C,qBAAa;AAAA,MACf;AAAA,IACF,QAAQ;AAAA,IAER;AAGA,UAAM,YAAYA,aAAY,WAAW,QAAQ;AACjD,QAAI,MAAM,MAAM,SAAS,GAAG;AAC1B,wBAAkB;AAAA,IACpB;AAEA,WAAO;AAAA,MACL,GAAI,aAAa,SAAY,EAAE,SAAS,IAAI,CAAC;AAAA,MAC7C,GAAI,eAAe,SAAY,EAAE,WAAW,IAAI,CAAC;AAAA,MACjD,GAAI,oBAAoB,SAAY,EAAE,gBAAgB,IAAI,CAAC;AAAA,IAC7D;AAAA,EACF;AACF;AAeA,SAAS,uBAAuB,OAA8C;AAC5E,MAAI,OAAO,MAAM,WAAW,UAAU;AACpC,WAAO,MAAM;AAAA,EACf;AACA,MACE,MAAM,UACN,OAAO,MAAM,WAAW,YACxB,CAAC,MAAM,QAAQ,MAAM,MAAM,GAC3B;AACA,UAAM,MAAM,MAAM;AAMlB,QAAI,OAAO,IAAI,QAAQ,YAAY,IAAI,IAAI,SAAS,GAAG;AACrD,aAAO;AAAA,IACT;AACA,QAAI,OAAO,IAAI,SAAS,YAAY,IAAI,KAAK,SAAS,GAAG;AACvD,aAAO,IAAI;AAAA,IACb;AAAA,EACF;AAEA,MACE,MAAM,WAAW,UACjB,OAAO,MAAM,SAAS,YACtB,MAAM,KAAK,SAAS,GACpB;AACA,WAAO,WAAW,MAAM,IAAI;AAAA,EAC9B;AACA,SAAO;AACT;AAEA,eAAe,MAAM,GAA6B;AAChD,MAAI;AACF,UAAM,IAAI,MAAMC,MAAK,CAAC;AACtB,WAAO,EAAE,YAAY;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,WAAW,GAA6B;AAC5D,MAAI;AACF,UAAM,OAAO,GAAG,YAAY,IAAI;AAChC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAcA,SAAS,mBACP,KACmC;AACnC,QAAM,IAQF,CAAC;AAEL,MAAI,OAAO,IAAI,gBAAgB,YAAY,IAAI,YAAY,SAAS,GAAG;AACrE,MAAE,cAAc,IAAI;AAAA,EACtB;AACA,MAAI,OAAO,IAAI,YAAY,YAAY,IAAI,QAAQ,SAAS,GAAG;AAC7D,MAAE,UAAU,IAAI;AAAA,EAClB;AACA,MAAI,OAAO,IAAI,aAAa,YAAY,IAAI,SAAS,SAAS,GAAG;AAC/D,MAAE,WAAW,IAAI;AAAA,EACnB;AACA,MAAI,OAAO,IAAI,YAAY,YAAY,IAAI,QAAQ,SAAS,GAAG;AAC7D,MAAE,UAAU,IAAI;AAAA,EAClB;AACA,MAAI,MAAM,QAAQ,IAAI,QAAQ,GAAG;AAC/B,UAAM,KAAK,IAAI,SAAS;AAAA,MACtB,CAAC,MAAmB,OAAO,MAAM,YAAY,EAAE,SAAS;AAAA,IAC1D;AACA,QAAI,GAAG,SAAS,EAAG,GAAE,WAAW;AAAA,EAClC;AACA,MACE,IAAI,UACJ,OAAO,IAAI,WAAW,YACtB,CAAC,MAAM,QAAQ,IAAI,MAAM,GACzB;AACA,UAAM,IAAI,IAAI;AACd,UAAM,SAA0D,CAAC;AACjE,QAAI,OAAO,EAAE,SAAS,SAAU,QAAO,OAAO,EAAE;AAChD,QAAI,OAAO,EAAE,UAAU,SAAU,QAAO,QAAQ,EAAE;AAClD,QAAI,OAAO,EAAE,QAAQ,SAAU,QAAO,MAAM,EAAE;AAC9C,QAAI,OAAO,KAAK,MAAM,EAAE,SAAS,EAAG,GAAE,SAAS;AAAA,EACjD;AACA,MACE,IAAI,aACJ,OAAO,IAAI,cAAc,YACzB,CAAC,MAAM,QAAQ,IAAI,SAAS,GAC5B;AACA,UAAM,QAAQ,oBAAoB,IAAI,SAAoC;AAC1E,QAAI,UAAU,OAAW,GAAE,YAAY;AAAA,EACzC;AAEA,SAAO,OAAO,KAAK,CAAC,EAAE,SAAS,IAAI,IAAI;AACzC;AAEA,SAAS,oBACP,KACoC;AACpC,QAAM,MAeF,CAAC;AAEL,QAAM,aAAa;AAAA,IACjB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,aAAW,OAAO,YAAY;AAC5B,UAAM,IAAI,IAAI,GAAG;AACjB,QAAI,OAAO,MAAM,YAAY,EAAE,SAAS,GAAG;AACzC,UAAI,GAAG,IAAI;AAAA,IACb;AAAA,EACF;AACA,aAAW,OAAO,CAAC,gBAAgB,iBAAiB,aAAa,GAAY;AAC3E,UAAM,IAAI,IAAI,GAAG;AACjB,QAAI,MAAM,QAAQ,CAAC,GAAG;AACpB,YAAM,MAAM,EAAE;AAAA,QACZ,CAAC,MAAmB,OAAO,MAAM,YAAY,EAAE,SAAS;AAAA,MAC1D;AACA,UAAI,IAAI,SAAS,EAAG,KAAI,GAAG,IAAI;AAAA,IACjC;AAAA,EACF;AAEA,SAAO,OAAO,KAAK,GAAG,EAAE,SAAS,IAAI,MAAM;AAC7C;AAWA,SAAS,oBAAoB,GAAmB;AAE9C,MAAI,EAAE,SAAS,KAAK,KAAK,EAAE,SAAS,KAAK,EAAG,QAAO;AAEnD,QAAM,UAAU,EAAE,SAAS,GAAG,IAAI,EAAE,MAAM,GAAG,EAAE,IAAI;AACnD,SAAO,GAAG,OAAO;AACnB;","names":["stat","pathResolve","pathResolve","stat"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/installers/claude-code/mcp.ts"],"sourcesContent":["/**\n * MCP command 规范化(task-6 产物)。\n *\n * 仅对 rush-ai 自己的插件(`rush@rush-marketplace`)做 MCP command 绝对路径规范化;\n * 其他 plugin 透传(见 plan §7.1 / spec §1.5)。\n *\n * 说明:resolver 层(`plugins/resolver.ts` 的 `normalizeRushMcpCommand`)已经做过\n * 同样的规范化。本文件提供一个 Installer 侧的**兜底**:如果 resolver 没解析到绝对路径\n * (例如 rushAiBinaryResolver 返回 undefined,resolver 选择不改),Installer 写\n * plugin.json 前再尝试一次——用 `which rush-ai`(PATH 解析)。**不 fallback 到\n * `process.argv[0]`**(那通常是 `node`,会写错误 command)。\n *\n * 为什么在两个层都做:\n * - Resolver 层:给各家 Installer 一个统一的起点(避免 Claude Code / Codex / Cursor\n * Installer 各自重复实现)\n * - Installer 层:resolver 的兜底策略有意保持宽松(失败就透传),Installer 层可以\n * 更激进(Claude Code 的 plugin.json 要求绝对路径更严)\n *\n * 注意:对 rush plugin 且 resolver + 本兜底都拿不到绝对路径时,我们仍然写入 plugin.json\n * 的原始值——不抛错、不阻塞 install。Claude Code 运行时会自己 fail(下游问题),但\n * Installer 保持 \"能装就装\" 的语义。\n */\n\nimport { execFileSync } from 'node:child_process';\nimport { readFile } from 'node:fs/promises';\nimport {\n isAbsolute,\n relative as pathRelative,\n resolve as pathResolve,\n} from 'node:path';\nimport type { McpServerConfig, PluginManifest, PluginRef } from '../types.js';\n\n/** rush 自身 plugin 的 identifier(与 resolver 层保持一致) */\nexport const RUSH_AI_PLUGIN_NAME = 'rush' as const;\nexport const RUSH_AI_MARKETPLACE_NAME = 'rush-marketplace' as const;\nexport const RUSH_MCP_SERVER_KEY = 'rush' as const;\n\n/**\n * 决定是否应对该 plugin 做 MCP command 规范化。\n *\n * 约束条件(plan §7.1):\n * - `ref.name === 'rush'`\n * - `ref.marketplace === 'rush-marketplace'`\n *\n * 第三方 marketplace 的 rush plugin(同名但不同 marketplace)**不**命中——\n * 只有我们自己发布的插件才保证 `rush-ai` binary 可解析到绝对路径。\n */\nexport function isRushOwnPlugin(ref: PluginRef): boolean {\n return (\n ref.name === RUSH_AI_PLUGIN_NAME &&\n ref.marketplace === RUSH_AI_MARKETPLACE_NAME\n );\n}\n\n/**\n * 规范化后的 MCP servers 对象——返回 Claude Code plugin.json 顶层所需的 `mcpServers`\n * 结构(`Record<string, McpServerConfig>`)。\n *\n * - `manifest.mcpServers` 是 `string`(外部文件引用)→ 返回 `undefined`,调用方需要走\n * {@link readExternalMcpServers} 读源文件,并把结果作为 normalizer 的输入再调一次\n * - 非 rush 插件 → 原样返回\n * - rush 插件:仅对 `rush` server key 做 command 规范化(其他 key 保持)\n *\n * 规范化策略(rush 插件且 command 非绝对路径时):\n * 1. 调用者注入的 `resolver`(通常是 `whichRushAiBinary`)\n * 2. 返回值是绝对路径 → 用它\n * 3. 否则保留原始 command(不抛错,让 Claude Code 运行时报)\n */\nexport function normalizeClaudeMcpServers(\n ref: PluginRef,\n manifest: PluginManifest,\n resolver: () => string | undefined = defaultRushBinaryResolver\n): Record<string, McpServerConfig> | undefined {\n const servers = manifest.mcpServers;\n if (servers === undefined || servers === null) return undefined;\n if (typeof servers === 'string') {\n // 字符串外部文件引用(典型:plugin resolver 隐式注入的 `\"./.mcp.json\"`,\n // 或作者显式声明的相对路径引用)。Installer 主流程负责读盘解析后再调\n // normalizer 一次(见 readExternalMcpServers)。这里返回 undefined,\n // installer 应该把它当作\"未直接内嵌\"。\n return undefined;\n }\n if (!isRushOwnPlugin(ref)) {\n // 第三方 plugin:透传作者写的值\n return { ...servers };\n }\n\n // rush own plugin —— 仅规范化 `rush` server key\n const rushServer = servers[RUSH_MCP_SERVER_KEY];\n if (!rushServer) {\n return { ...servers };\n }\n const currentCommand = rushServer.command;\n if (typeof currentCommand === 'string' && isAbsolute(currentCommand)) {\n // 已经是绝对路径,不动\n return { ...servers };\n }\n\n const resolved = resolver();\n if (!resolved || !isAbsolute(resolved)) {\n // 兜底失败:保留原值,不阻塞 install\n return { ...servers };\n }\n\n return {\n ...servers,\n [RUSH_MCP_SERVER_KEY]: { ...rushServer, command: resolved },\n };\n}\n\n/**\n * 当 `manifest.mcpServers` 是字符串外部文件引用时,从源 plugin 目录读取该\n * `.mcp.json` 文件并解析成 `Record<string, McpServerConfig>`。\n *\n * 触发场景:\n * - plugin 作者显式声明 `\"mcpServers\": \"./.mcp.json\"`\n * - resolver 隐式注入(plugin.json 没声明 + 目录有 `.mcp.json`)—— 见\n * `plugins/resolver.ts` 的 `injectImplicitMcpRef`\n *\n * 与 Codex installer 的 `copyAuthorProvidedMcp` 形成对称:那边读完后 copy 到\n * 版本目录 + 解析 mcpKeys;Claude Code 这边只需要解析后塞回 normalizer 流程。\n *\n * 容忍两种 .mcp.json 形态:\n * - `{ \"mcpServers\": { ... } }`(rush-ai 自己写出的,带 wrapper)\n * - `{ \"<server-name>\": { ... } }`(claude-plugins-official 的 external_plugins/*\n * 不带 wrapper)\n *\n * 安全:拒绝 `../` 逃逸 sourceDir 的引用,缺失文件 / 损坏 JSON / 路径非法 →\n * 返回 `null`,调用方按 \"无 MCP\" 继续 install(不阻塞)。\n */\nexport async function readExternalMcpServers(\n sourceDir: string,\n relativeRef: string\n): Promise<Record<string, McpServerConfig> | null> {\n if (typeof relativeRef !== 'string' || relativeRef.length === 0) return null;\n const srcPath = pathResolve(sourceDir, relativeRef);\n // 路径穿越守护:必须落在 sourceDir 下\n const rel = pathRelative(pathResolve(sourceDir), srcPath);\n if (rel === '..' || rel.startsWith('..') || rel.startsWith('/')) return null;\n\n let raw: string;\n try {\n raw = await readFile(srcPath, 'utf8');\n } catch {\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {\n return null;\n }\n\n // 形态 1:`{ mcpServers: {...} }`\n const obj = parsed as { mcpServers?: unknown } & Record<string, unknown>;\n if (\n obj.mcpServers &&\n typeof obj.mcpServers === 'object' &&\n !Array.isArray(obj.mcpServers)\n ) {\n return obj.mcpServers as Record<string, McpServerConfig>;\n }\n\n // 形态 2:`{ <server-name>: {...} }` —— external_plugins/* 形态\n // 简单识别:所有 value 都是 object(不含 mcpServers 这个 key)\n const flat = obj as Record<string, unknown>;\n if (Object.keys(flat).length === 0) return null;\n const allObjects = Object.values(flat).every(\n (v) => v !== null && typeof v === 'object' && !Array.isArray(v)\n );\n if (!allObjects) return null;\n return flat as Record<string, McpServerConfig>;\n}\n\n/**\n * 默认 rush-ai binary 解析:`which rush-ai`(PATH 解析)。\n *\n * 作为 Installer 层兜底——resolver 层已用 `process.argv[1]` 试过一次;本函数\n * 用更稳的 PATH 查找逻辑。\n *\n * **不 fallback 到 `process.argv[0]`**(那通常是 `node`,会把 rush MCP command\n * 规范化成 node 路径,运行时语义错误)——宁可返回 `undefined`(保留原 command),\n * 也不冒险写入错误绝对路径。\n *\n * 失败返回 `undefined`,调用方保留原值。\n */\nexport function defaultRushBinaryResolver(): string | undefined {\n try {\n const result = execFileSync('which', ['rush-ai'], {\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore'],\n }).trim();\n if (result.length > 0 && isAbsolute(result)) {\n return result;\n }\n } catch {\n // `which` 失败 —— rush-ai 未在 PATH 上,放弃兜底\n }\n return undefined;\n}\n"],"mappings":";;;AAuBA,SAAS,oBAAoB;AAC7B,SAAS,gBAAgB;AACzB;AAAA,EACE;AAAA,EACA,YAAY;AAAA,EACZ,WAAW;AAAA,OACN;AAIA,IAAM,sBAAsB;AAC5B,IAAM,2BAA2B;AACjC,IAAM,sBAAsB;AAY5B,SAAS,gBAAgB,KAAyB;AACvD,SACE,IAAI,SAAS,uBACb,IAAI,gBAAgB;AAExB;AAgBO,SAAS,0BACd,KACA,UACA,WAAqC,2BACQ;AAC7C,QAAM,UAAU,SAAS;AACzB,MAAI,YAAY,UAAa,YAAY,KAAM,QAAO;AACtD,MAAI,OAAO,YAAY,UAAU;AAK/B,WAAO;AAAA,EACT;AACA,MAAI,CAAC,gBAAgB,GAAG,GAAG;AAEzB,WAAO,EAAE,GAAG,QAAQ;AAAA,EACtB;AAGA,QAAM,aAAa,QAAQ,mBAAmB;AAC9C,MAAI,CAAC,YAAY;AACf,WAAO,EAAE,GAAG,QAAQ;AAAA,EACtB;AACA,QAAM,iBAAiB,WAAW;AAClC,MAAI,OAAO,mBAAmB,YAAY,WAAW,cAAc,GAAG;AAEpE,WAAO,EAAE,GAAG,QAAQ;AAAA,EACtB;AAEA,QAAM,WAAW,SAAS;AAC1B,MAAI,CAAC,YAAY,CAAC,WAAW,QAAQ,GAAG;AAEtC,WAAO,EAAE,GAAG,QAAQ;AAAA,EACtB;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,CAAC,mBAAmB,GAAG,EAAE,GAAG,YAAY,SAAS,SAAS;AAAA,EAC5D;AACF;AAsBA,eAAsB,uBACpB,WACA,aACiD;AACjD,MAAI,OAAO,gBAAgB,YAAY,YAAY,WAAW,EAAG,QAAO;AACxE,QAAM,UAAU,YAAY,WAAW,WAAW;AAElD,QAAM,MAAM,aAAa,YAAY,SAAS,GAAG,OAAO;AACxD,MAAI,QAAQ,QAAQ,IAAI,WAAW,IAAI,KAAK,IAAI,WAAW,GAAG,EAAG,QAAO;AAExE,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,SAAS,SAAS,MAAM;AAAA,EACtC,QAAQ;AACN,WAAO;AAAA,EACT;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,KAAK,MAAM,GAAG;AAAA,EACzB,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,CAAC,UAAU,OAAO,WAAW,YAAY,MAAM,QAAQ,MAAM,GAAG;AAClE,WAAO;AAAA,EACT;AAGA,QAAM,MAAM;AACZ,MACE,IAAI,cACJ,OAAO,IAAI,eAAe,YAC1B,CAAC,MAAM,QAAQ,IAAI,UAAU,GAC7B;AACA,WAAO,IAAI;AAAA,EACb;AAIA,QAAM,OAAO;AACb,MAAI,OAAO,KAAK,IAAI,EAAE,WAAW,EAAG,QAAO;AAC3C,QAAM,aAAa,OAAO,OAAO,IAAI,EAAE;AAAA,IACrC,CAAC,MAAM,MAAM,QAAQ,OAAO,MAAM,YAAY,CAAC,MAAM,QAAQ,CAAC;AAAA,EAChE;AACA,MAAI,CAAC,WAAY,QAAO;AACxB,SAAO;AACT;AAcO,SAAS,4BAAgD;AAC9D,MAAI;AACF,UAAM,SAAS,aAAa,SAAS,CAAC,SAAS,GAAG;AAAA,MAChD,UAAU;AAAA,MACV,OAAO,CAAC,UAAU,QAAQ,QAAQ;AAAA,IACpC,CAAC,EAAE,KAAK;AACR,QAAI,OAAO,SAAS,KAAK,WAAW,MAAM,GAAG;AAC3C,aAAO;AAAA,IACT;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO;AACT;","names":[]}
|
/package/dist/{_codex-content-loader-EXSX7ZGI.js.map → _codex-content-loader-WAPA56U3.js.map}
RENAMED
|
File without changes
|
|
File without changes
|