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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/installers/codex/mirror.ts","../src/installers/codex/paths.ts","../src/installers/codex/toml.ts"],"sourcesContent":["/**\n * Codex marketplace mirror —— `~/.codex/plugins/marketplaces/<mkt>/` 写入器。\n *\n * Spec: `specs/rush-v2/web/plugins/codex-marketplace-mirror.spec.md`\n *\n * 这个模块负责把\"一个 marketplace 应该展示哪些 plugin\"落到 Codex 桌面端能读的\n * `~/.codex/plugins/marketplaces/<mkt>/.agents/plugins/marketplace.json` 上,让\n * Codex Plugins 页能展示 marketplace 完整目录(即便 plugin 还没安装)。\n *\n * 两类调用方:\n *\n * 1. **Install 路径**(已有):`CodexInstaller.install` 在装完 plugin 后调\n * `writeCodexMarketplacePluginMirror`,把单个 plugin 的完整文件镜像到\n * `<mktDir>/plugins/<name>/`,并在 catalog 里 upsert 一条 entry。\n *\n * 2. **Sync 路径**(PR2/PR3 接入):`syncCodexMarketplaceMirror` 把整个 marketplace\n * 全量列表写成 stub —— 只写元数据,不下载内容。`policy.installation = \"NOT_AVAILABLE\"`,\n * Codex UI 渲染为\"列表可见但禁用连接\",避免用户点错伪安装。\n * 真正安装通过 `npx rush-ai@latest plugin install <name>@<marketplace>`。\n *\n * 设计原则:\n *\n * - **Stub 不能覆盖 full**:sync 时若发现 `<mktDir>/plugins/<name>/` 已经是 install\n * 写出的 full 形态(`mcpServers` 字段 / `skills/` 子目录存在),保留 full 不动。\n * 保护已装 plugin 的运行时状态。\n *\n * - **catalog 是真值**:sync 计算 added/refreshed/preservedFull/removed 四象限,\n * 按 remote `manifest.plugins` 全量重写 catalog(保留 full,仅删 stub)。\n *\n * - **错误隔离**:单个 plugin stub 写失败不阻断其他;catalog JSON 损坏 → 重建。\n * I/O 致命错(写整个 catalog 失败)才上抛。\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { type Dirent, constants as fsConstants } from 'node:fs';\nimport {\n access,\n cp,\n mkdir,\n mkdtemp,\n readdir,\n readFile,\n rename,\n rm,\n stat,\n writeFile,\n} from 'node:fs/promises';\nimport { basename, dirname, resolve } from 'node:path';\nimport type {\n MarketplacePluginEntry,\n ResolvedMarketplace,\n} from '../../marketplaces/types.js';\nimport { output } from '../../output/logger.js';\nimport type { PluginManifest, ResolvedPlugin } from '../types.js';\nimport {\n codexConfigBackupPath,\n codexConfigTomlPath,\n codexHomeDir,\n codexMarketplaceDir,\n codexMarketplaceManifestPath,\n codexMarketplacePluginDir,\n codexPluginManifestPath,\n codexPluginMcpPath,\n codexPluginSkillsDir,\n} from './paths.js';\nimport {\n backupCodexConfig,\n readCodexConfig,\n setMarketplaceSection,\n writeCodexConfig,\n} from './toml.js';\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/** Codex marketplace catalog (`marketplace.json`) 顶层 shape。 */\nexport interface CodexMarketplaceCatalog {\n name: string;\n interface?: { displayName?: string };\n plugins: CodexCatalogPluginEntry[];\n}\n\n/** catalog `plugins[]` 单条 entry 的 shape。 */\nexport interface CodexCatalogPluginEntry {\n name: string;\n source?: unknown;\n policy?: unknown;\n category?: unknown;\n description?: string;\n version?: string;\n [extra: string]: unknown;\n}\n\n/**\n * 一个 plugin 的可选内容载荷 —— sync 把它落到 codex 镜像目录里。\n *\n * 由调用方(CLI 层)按 source kind 注入对应的 loader 实现:\n * - `rush://` source → 调 `fetchRushPlugin(name)` 拿 mcpServers\n * - `directory:` / `github:` source → 从 cache 中 plugin 子目录读 `.claude-plugin/plugin.json`\n * + `.mcp.json` + `skills/`\n *\n * 字段语义:\n * - `manifest`:plugin manifest(仅用于读 description / version 等元数据,\n * sync 写出的 `.codex-plugin/plugin.json` 不直接透传它,避免泄漏作者私有字段)。\n * - `mcpServers`:MCP servers 配置对象。`undefined` / `null` → sync 不写 `.mcp.json`、\n * `.codex-plugin/plugin.json` 也不引用 mcpServers(保持纯 stub 行为)。\n * - `skillsSourceDir`:要 copy 到镜像 `skills/` 子目录的源路径;不存在或不可读 → 跳过。\n */\nexport interface PluginContent {\n readonly manifest?: PluginContentManifest | null;\n readonly mcpServers?: Record<string, unknown> | null;\n readonly skillsSourceDir?: string | null;\n /**\n * 占位 skills 列表 —— 当 skill 真实内容拿不到(如 rush:// 后端只返回 name+version\n * 没有 SKILL.md 内容)时,sync 为每个条目写一个占位 SKILL.md,让 Codex 详情页\n * \"技能 N\"区块能显示 skill 名字 + 简介。\n *\n * 与 `skillsSourceDir` 互斥:若两者都提供,优先 `skillsSourceDir`(真实内容)。\n */\n readonly skillsPlaceholders?: ReadonlyArray<{\n name: string;\n description?: string;\n /**\n * 该 skill 在 web 站点的查看链接(如 `https://rush.zhenguanyu.com/next/skills/...`)。\n * 写到占位 SKILL.md 的 markdown body 里,让用户能直接跳到 web 看完整内容。\n */\n url?: string;\n /**\n * 真实 SKILL.md 完整内容(含 frontmatter + body)。提供时**优先使用**,\n * 直接写到 mirror 的 `skills/<dir>/SKILL.md`,不走占位流程。\n *\n * sync 时由 rush:// loader 调 `<host>/next/skills/<name>.md` 拉到内容后填充。\n * 占位流程仅作 fallback(远端没暴露真内容时)。\n */\n rawSkillMd?: string;\n }> | null;\n /**\n * plugin 在 web 站点的查看链接 —— 写到占位 SKILL.md body 里,让用户能跳到 web 站\n * 浏览完整 plugin 详情。仅 rush:// 类 source 提供(github / directory 类没有 web 站点)。\n */\n readonly pluginWebUrl?: string | null;\n /**\n * 安装命令字符串 —— 用于 SKILL.md body 引导用户安装。默认由 sync 自己拼成\n * `rush-ai plugin install <name>@<mkt> --target codex`,但允许 loader 覆盖。\n */\n readonly installCommand?: string | null;\n}\n\n/** Codex plugin.json `interface` 字段子集 —— 详情页展示用。 */\nexport interface PluginContentInterface {\n readonly displayName?: string;\n readonly shortDescription?: string;\n readonly longDescription?: string;\n readonly developerName?: string;\n readonly category?: string;\n readonly capabilities?: ReadonlyArray<string>;\n readonly websiteURL?: string;\n readonly privacyPolicyURL?: string;\n readonly termsOfServiceURL?: string;\n readonly defaultPrompt?: ReadonlyArray<string>;\n readonly brandColor?: string;\n readonly composerIcon?: string;\n readonly logo?: string;\n readonly screenshots?: ReadonlyArray<string>;\n}\n\nexport interface PluginContentManifest {\n readonly description?: string;\n readonly version?: string;\n readonly author?: { name?: string; email?: string; url?: string };\n readonly homepage?: string;\n readonly license?: string;\n readonly keywords?: ReadonlyArray<string>;\n readonly interface?: PluginContentInterface;\n}\n\n/**\n * 给定 marketplace.json 的一条 plugin entry,返回 sync 应写入的内容载荷。\n *\n * **必须不抛错**(错误自行处理并返回空载荷),否则调用方会把错误冒泡到 CLI 层,\n * 阻断其他 plugin 的 sync。如果某个 plugin 真的取不到内容,返回 `{}` 即可,\n * sync 会写一个最简 stub。\n *\n * 测试默认值:`async () => ({})`(纯 stub 行为,等同 v1)。\n */\nexport type PluginContentLoader = (\n entry: MarketplacePluginEntry\n) => Promise<PluginContent>;\n\nexport interface SyncMirrorOptions {\n /** 时间戳注入器(仅测试用),默认 `() => new Date()` */\n readonly now?: () => Date;\n /** 见 `PluginContentLoader`。默认实现:始终返回空载荷(写最简 stub)。 */\n readonly contentLoader?: PluginContentLoader;\n}\n\nexport interface SyncMirrorResult {\n /** Codex 没装(`~/.codex/` 不存在)→ true,其余字段为空 */\n skipped: boolean;\n /** 本次新增到 catalog 的 plugin 名 */\n added: string[];\n /** 本次重写 stub 的 plugin 名(catalog/磁盘已有 stub,被覆盖刷新) */\n refreshed: string[];\n /** 本次保留为 full 的 plugin 名(已装,sync 不改写其内容;entry 仍出现在 catalog) */\n preservedFull: string[];\n /** 本次从 catalog 移除的 plugin 名(remote 列表中已无) */\n removed: string[];\n /** sync 期间被跳过的 plugin(如 name 非法);diagnostic 用 */\n warnings: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Constants\n// ---------------------------------------------------------------------------\n\n/**\n * Plugin name 白名单 —— 对齐 `installer.ts:assertSafePathComponents`:\n * - 不允许路径分隔符 `/` `\\`\n * - 不允许 `..` 起始或子串(防 `../evil`)\n * - 不允许 `.` `..` 整名\n *\n * 允许字母数字、下划线、连字符、点(中段)、`@`。\n */\nfunction isSafePluginName(name: string): boolean {\n if (typeof name !== 'string' || name.length === 0) return false;\n if (name === '.' || name === '..') return false;\n if (name.startsWith('.')) return false;\n if (name.includes('/') || name.includes('\\\\')) return false;\n if (name.includes('..')) return false;\n return /^[A-Za-z0-9_@][A-Za-z0-9._@-]*$/.test(name);\n}\n\nconst DEFAULT_CATEGORY = 'Engineering';\nconst DEFAULT_CAPABILITIES: ReadonlyArray<string> = ['Read', 'Write'];\n\n// ---------------------------------------------------------------------------\n// Sync 入口(PR1 引入)\n// ---------------------------------------------------------------------------\n\n/**\n * 把 `resolved.manifest.plugins` 全量同步到 Codex marketplace 镜像目录,让\n * Codex 桌面端能浏览 marketplace 完整目录(含未装 plugin)。\n *\n * 实现细节:\n *\n * 1. `~/.codex/` 不存在 → `{ skipped: true, ... }`,不触磁盘。\n * 2. 读旧 catalog(`<mktDir>/.agents/plugins/marketplace.json`);损坏视为空。\n * 3. 对 remote 每个 plugin:\n * - 若磁盘上 `<mktDir>/plugins/<name>/.codex-plugin/plugin.json` 已是 full\n * 形态(`mcpServers` 字段 / `skills/` 目录存在)→ 保留,不动文件,\n * 仅在 catalog 里保留同名 entry(按现有 entry 字段透传)。\n * - 否则写 stub plugin.json + 在 catalog 里 upsert AVAILABLE entry。\n * 4. catalog 中存在但 remote 无的 entry:\n * - stub → 删 `<mktDir>/plugins/<name>/` + 从 catalog 移除。\n * - full → 保留 catalog entry + 目录,stderr 警告\"orphan installed plugin\"。\n * 5. 原子重写 catalog(atomic rename)。\n *\n * 不做事务回滚:sync 是衍生镜像,调用方对失败的容忍度更高(继续装别的 plugin)。\n * 单个 plugin stub I/O 失败 → 该 plugin 不进 catalog,记 warnings。\n */\nexport async function syncCodexMarketplaceMirror(\n home: string,\n resolved: ResolvedMarketplace,\n opts: SyncMirrorOptions = {}\n): Promise<SyncMirrorResult> {\n const result: SyncMirrorResult = {\n skipped: false,\n added: [],\n refreshed: [],\n preservedFull: [],\n removed: [],\n warnings: [],\n };\n\n if (!(await isDir(codexHomeDir(home)))) {\n result.skipped = true;\n return result;\n }\n\n const marketplaceDir = codexMarketplaceDir(home, resolved.name);\n const manifestPath = codexMarketplaceManifestPath(marketplaceDir);\n const contentLoader: PluginContentLoader =\n opts.contentLoader ?? (async () => ({}));\n\n // 旧 catalog(损坏/缺失 → 空 catalog)\n const fallback: CodexMarketplaceCatalog = {\n name: resolved.name,\n interface: { displayName: resolved.name },\n plugins: [],\n };\n const existing = await readCodexMarketplaceCatalog(manifestPath, fallback);\n const existingByName = new Map(existing.plugins.map((p) => [p.name, p]));\n\n // 过滤掉 name 不安全的 entry(防 path traversal / shell 注入)\n const remotePlugins: MarketplacePluginEntry[] = [];\n const remoteNames = new Set<string>();\n for (const entry of resolved.manifest.plugins) {\n if (typeof entry?.name !== 'string' || !isSafePluginName(entry.name)) {\n const display =\n typeof entry?.name === 'string' ? entry.name : '<unnamed>';\n result.warnings.push(`skipped plugin with unsafe name: ${display}`);\n output.warn(\n `[codex] marketplace '${resolved.name}': skipped plugin with unsafe name: ${display}`\n );\n continue;\n }\n if (remoteNames.has(entry.name)) {\n result.warnings.push(`duplicate plugin name in manifest: ${entry.name}`);\n continue;\n }\n remotePlugins.push(entry);\n remoteNames.add(entry.name);\n }\n\n // 计算 next catalog\n const nextEntries: CodexCatalogPluginEntry[] = [];\n for (const entry of remotePlugins) {\n const pluginDir = codexMarketplacePluginDir(marketplaceDir, entry.name);\n const fullState = await detectFullPluginDir(pluginDir);\n if (fullState === 'full') {\n // 已装 full —— 保留磁盘 + 复用 catalog 已有 entry(若有),缺则按 stub 形状补\n // 重要:full 路径下 policy.installation 必须是 AVAILABLE(即便 prior 是\n // 老 stub 写的 NOT_AVAILABLE),否则用户在 UI 上看不到\"已安装\"按钮。\n const prior = existingByName.get(entry.name);\n const base = prior ?? buildStubCatalogEntry(entry);\n const fullEntry: CodexCatalogPluginEntry = {\n ...base,\n policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },\n };\n nextEntries.push(fullEntry);\n result.preservedFull.push(entry.name);\n continue;\n }\n\n // 拉内容(loader 只返回空 → 退化为最简 stub)\n let content: PluginContent = {};\n try {\n content = await contentLoader(entry);\n } catch (err) {\n result.warnings.push(\n `contentLoader failed for '${entry.name}': ${(err as Error).message}`\n );\n output.warn(\n `[codex] marketplace '${resolved.name}': contentLoader failed for '${entry.name}': ${(err as Error).message}`\n );\n content = {};\n }\n\n try {\n await writePluginMaterials(pluginDir, entry, content, resolved.name);\n } catch (err) {\n result.warnings.push(\n `failed to write stub for '${entry.name}': ${(err as Error).message}`\n );\n output.warn(\n `[codex] marketplace '${resolved.name}': failed to write stub for '${entry.name}': ${(err as Error).message}`\n );\n continue;\n }\n nextEntries.push(buildStubCatalogEntry(entry, content));\n if (existingByName.has(entry.name)) {\n result.refreshed.push(entry.name);\n } else {\n result.added.push(entry.name);\n }\n }\n\n // 删除 remote 已无的 entry\n for (const prior of existing.plugins) {\n if (remoteNames.has(prior.name)) continue;\n // 防御:本地 catalog 可能被人篡改 / 历史损坏,prior.name 可能是 `../../../cache`\n // 这种字符串。不再走 unsafe name 路径删盘——只把 entry 从 catalog 里去掉,\n // 任何同名磁盘目录交给后续手工清理。\n if (!isSafePluginName(prior.name)) {\n result.warnings.push(\n `dropped catalog entry with unsafe name: ${prior.name}`\n );\n output.warn(\n `[codex] marketplace '${resolved.name}': dropped catalog entry with unsafe name: ${prior.name}`\n );\n result.removed.push(prior.name);\n continue;\n }\n const pluginDir = codexMarketplacePluginDir(marketplaceDir, prior.name);\n if ((await detectFullPluginDir(pluginDir)) === 'full') {\n // orphan installed —— 保留 entry,发警告\n nextEntries.push(prior);\n output.warn(\n `[codex] marketplace '${resolved.name}': installed plugin '${prior.name}' no longer in remote catalog; keeping entry. Run 'rush-ai plugin uninstall ${prior.name}@${resolved.name}' if intended.`\n );\n continue;\n }\n // stub orphan → 删\n await rm(pluginDir, { recursive: true, force: true }).catch(() => {});\n result.removed.push(prior.name);\n }\n\n // 字典序排序后写\n nextEntries.sort((a, b) => a.name.localeCompare(b.name));\n\n const nextCatalog: CodexMarketplaceCatalog = {\n name: existing.name || resolved.name,\n interface: existing.interface ?? fallback.interface,\n plugins: nextEntries,\n };\n\n await writeFileAtomic(\n manifestPath,\n `${JSON.stringify(nextCatalog, null, 2)}\\n`\n );\n\n // Codex UI 仅扫描 `~/.codex/config.toml` 中已注册的 `[marketplaces.<name>]`,\n // 不扫盘。所以光写 marketplace mirror 还不够,必须把 marketplace 注册到\n // config.toml(source_type = local,source 指向 mirror 目录),UI 才能看到。\n //\n // 这一步与 `CodexInstaller.install` 写 `[marketplaces.<name>]` 同构,但语义更靠前:\n // marketplace add / sync 时就让 Codex 看见,不用等用户跑 plugin install。\n //\n // 失败处理:写 config.toml 失败不应阻塞 sync 主流程(catalog 已经写好磁盘\n // 是正确的),错误降级到 warning + 计入 result.warnings。\n try {\n await registerMarketplaceInCodexConfig({\n home,\n marketplaceName: resolved.name,\n marketplaceDir,\n now: opts.now ?? (() => new Date()),\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n result.warnings.push(\n `failed to register [marketplaces.${resolved.name}] in config.toml: ${msg}`\n );\n output.warn(\n `[codex] marketplace '${resolved.name}': failed to register in config.toml: ${msg}`\n );\n }\n\n return result;\n}\n\n/**\n * 把 `[marketplaces.<name>]` section 注册到 `~/.codex/config.toml`,让 Codex\n * 桌面端 UI 能识别这个 marketplace。\n *\n * 写法对齐现有 `CodexInstaller.install` 流程:\n * - source_type = \"local\"\n * - source = `<home>/.codex/plugins/marketplaces/<name>`(mirror 目录的绝对路径)\n * - last_updated = ISO 8601 当前时间\n *\n * 备份 + 原子写:和 install 走同一套 readCodexConfig / writeCodexConfig(带 mtime\n * 冲突检测),保护并发场景。\n */\nasync function registerMarketplaceInCodexConfig(args: {\n home: string;\n marketplaceName: string;\n marketplaceDir: string;\n now: () => Date;\n}): Promise<void> {\n const cfgPath = codexConfigTomlPath(args.home);\n // 备份(文件不存在时 backup 返回 null —— 此情况下不需要备份)\n await backupCodexConfig(\n cfgPath,\n codexConfigBackupPath(args.home, args.now())\n );\n const { data, mtimeMs } = await readCodexConfig(cfgPath);\n setMarketplaceSection(data, args.marketplaceName, {\n last_updated: args.now().toISOString(),\n source_type: 'local',\n source: args.marketplaceDir,\n });\n await writeCodexConfig(cfgPath, data, mtimeMs);\n}\n\n// ---------------------------------------------------------------------------\n// Install 路径专用:单 plugin full mirror(PR1 抽自 installer.ts)\n// ---------------------------------------------------------------------------\n\nexport interface WriteFullMirrorInput {\n marketplaceDir: string;\n pluginDir: string;\n plugin: ResolvedPlugin;\n versionDir: string;\n}\n\n/**\n * 把 `versionDir` 下的完整 plugin 内容(plugin.json / .mcp.json / skills/ 等)\n * 镜像到 `pluginDir`,并把 catalog entry 升级为 full(覆盖任何 stub)。\n *\n * 行为对齐 PR1 之前 `writeCodexMarketplaceMirror`:原子目录 swap + 失败回滚到旧\n * 目录。catalog 写入复用 `upsertCodexCatalogPlugin`。\n */\nexport async function writeCodexMarketplacePluginMirror(\n input: WriteFullMirrorInput\n): Promise<void> {\n const pluginParent = dirname(input.pluginDir);\n const pluginBase = basename(input.pluginDir);\n await mkdir(pluginParent, { recursive: true });\n const tmpPluginDir = await mkdtemp(\n resolve(pluginParent, `.${pluginBase}.tmp-`)\n );\n const backupPluginDir = await mkdtemp(\n resolve(pluginParent, `.${pluginBase}.bak-`)\n );\n let hadExistingPluginDir = false;\n let swappedPluginDir = false;\n try {\n await rm(tmpPluginDir, { recursive: true, force: true });\n await rm(backupPluginDir, { recursive: true, force: true }).catch(() => {});\n await cp(input.versionDir, tmpPluginDir, {\n recursive: true,\n dereference: false,\n preserveTimestamps: true,\n verbatimSymlinks: true,\n });\n hadExistingPluginDir = await pathExists(input.pluginDir);\n if (hadExistingPluginDir) {\n await rename(input.pluginDir, backupPluginDir);\n }\n await rename(tmpPluginDir, input.pluginDir);\n swappedPluginDir = true;\n await upsertCodexCatalogPluginFull(input.marketplaceDir, input.plugin);\n if (hadExistingPluginDir) {\n await rm(backupPluginDir, { recursive: true, force: true });\n }\n } catch (err) {\n await rm(tmpPluginDir, { recursive: true, force: true }).catch(() => {});\n let restoredBackup = false;\n if (hadExistingPluginDir && (await pathExists(backupPluginDir))) {\n let displacedPluginDir: string | null = null;\n try {\n if (await pathExists(input.pluginDir)) {\n displacedPluginDir = await mkdtemp(\n resolve(pluginParent, `.${pluginBase}.failed-`)\n );\n await rm(displacedPluginDir, { recursive: true, force: true });\n await rename(input.pluginDir, displacedPluginDir);\n }\n await rename(backupPluginDir, input.pluginDir);\n restoredBackup = true;\n if (displacedPluginDir) {\n await rm(displacedPluginDir, { recursive: true, force: true }).catch(\n () => {}\n );\n }\n } catch {\n if (\n displacedPluginDir &&\n !(await pathExists(input.pluginDir)) &&\n (await pathExists(displacedPluginDir))\n ) {\n await rename(displacedPluginDir, input.pluginDir).catch(() => {});\n }\n }\n } else if (swappedPluginDir) {\n await rm(input.pluginDir, { recursive: true, force: true }).catch(\n () => {}\n );\n }\n if (restoredBackup || !hadExistingPluginDir) {\n await rm(backupPluginDir, { recursive: true, force: true }).catch(\n () => {}\n );\n }\n throw err;\n }\n}\n\n/**\n * 在 catalog 中 upsert 一条 full entry(对应已装 plugin)。供 install 路径使用。\n *\n * 单条 upsert 语义;不参与 sync 的 added/refreshed/removed 统计。\n */\nexport async function upsertCodexCatalogPluginFull(\n marketplaceDir: string,\n plugin: ResolvedPlugin\n): Promise<void> {\n const manifestPath = codexMarketplaceManifestPath(marketplaceDir);\n const fallback: CodexMarketplaceCatalog = {\n name: plugin.ref.marketplace,\n interface: { displayName: plugin.ref.marketplace },\n plugins: [],\n };\n const catalog = await readCodexMarketplaceCatalog(manifestPath, fallback);\n const nextEntry: CodexCatalogPluginEntry = {\n name: plugin.ref.name,\n source: { source: 'local', path: `./plugins/${plugin.ref.name}` },\n policy: { installation: 'AVAILABLE', authentication: 'ON_INSTALL' },\n category: buildInterfaceCategory(plugin.manifest),\n };\n if (plugin.manifest.description !== undefined) {\n nextEntry.description = plugin.manifest.description;\n }\n if (typeof plugin.manifest.version === 'string') {\n nextEntry.version = plugin.manifest.version;\n }\n const next = [\n ...catalog.plugins.filter((entry) => entry.name !== plugin.ref.name),\n nextEntry,\n ].sort((a, b) => a.name.localeCompare(b.name));\n await writeFileAtomic(\n manifestPath,\n `${JSON.stringify({ ...catalog, plugins: next }, null, 2)}\\n`\n );\n}\n\n/**\n * 卸载场景下把 catalog 里的 full entry 降级回 stub。\n *\n * 调用时机:`CodexInstaller.uninstall` 完成 config.toml 清理之后。功能:\n * 1. 删 `<mktDir>/plugins/<plugin>/` plugin 目录(解除 detectFullPluginDir 的 full 判定)\n * 2. catalog entry 改成 stub 形态:policy.installation = NOT_AVAILABLE,保留\n * name / category / description / version 这些展示字段,让用户卸载后插件\n * 依然能在 Codex UI 列表里看到(重装入口)\n *\n * 不会做:\n * - 删 marketplace section / 镜像目录 —— 那是 marketplace remove 的责任\n * - 重新发起 sync —— 这里只针对单个 plugin 做最小修复,不联网\n *\n * 失败容忍:catalog 不存在 / IO 错 → 静默忽略(uninstall 本身已经成功)\n */\nexport async function downgradeCodexCatalogEntryToStub(\n marketplaceDir: string,\n pluginName: string\n): Promise<void> {\n const manifestPath = codexMarketplaceManifestPath(marketplaceDir);\n // 1. 删 plugin 目录(之后再读 catalog,顺序无所谓)\n const pluginDir = codexMarketplacePluginDir(marketplaceDir, pluginName);\n await rm(pluginDir, { recursive: true, force: true }).catch(() => {});\n\n // 2. catalog entry 改 stub\n if (!(await pathExists(manifestPath))) return;\n try {\n const fallback: CodexMarketplaceCatalog = {\n name: '',\n interface: {},\n plugins: [],\n };\n const catalog = await readCodexMarketplaceCatalog(manifestPath, fallback);\n const next = catalog.plugins.map((entry) => {\n if (entry.name !== pluginName) return entry;\n return {\n ...entry,\n policy: {\n installation: 'NOT_AVAILABLE',\n authentication: 'ON_INSTALL',\n },\n } satisfies CodexCatalogPluginEntry;\n });\n await writeFileAtomic(\n manifestPath,\n `${JSON.stringify({ ...catalog, plugins: next }, null, 2)}\\n`\n );\n } catch {\n // ignore\n }\n}\n\n// ---------------------------------------------------------------------------\n// Catalog read(exported for reuse by installer.ts)\n// ---------------------------------------------------------------------------\n\nexport async function readCodexMarketplaceCatalog(\n manifestPath: string,\n fallback: CodexMarketplaceCatalog\n): Promise<CodexMarketplaceCatalog> {\n if (!(await pathExists(manifestPath))) return fallback;\n try {\n const parsed = JSON.parse(await readFile(manifestPath, 'utf8')) as {\n name?: unknown;\n interface?: unknown;\n plugins?: unknown;\n };\n return {\n name: typeof parsed.name === 'string' ? parsed.name : fallback.name,\n interface:\n parsed.interface &&\n typeof parsed.interface === 'object' &&\n !Array.isArray(parsed.interface)\n ? (parsed.interface as CodexMarketplaceCatalog['interface'])\n : fallback.interface,\n plugins: Array.isArray(parsed.plugins)\n ? parsed.plugins.filter(isCatalogPluginEntry)\n : fallback.plugins,\n };\n } catch {\n return fallback;\n }\n}\n\nfunction isCatalogPluginEntry(\n value: unknown\n): value is CodexCatalogPluginEntry {\n return (\n !!value &&\n typeof value === 'object' &&\n !Array.isArray(value) &&\n typeof (value as { name?: unknown }).name === 'string'\n );\n}\n\n// ---------------------------------------------------------------------------\n// Stub builders\n// ---------------------------------------------------------------------------\n\nfunction buildStubCatalogEntry(\n entry: MarketplacePluginEntry,\n content: PluginContent = {}\n): CodexCatalogPluginEntry {\n const description =\n pickNonEmptyString(entry.description) ??\n pickNonEmptyString(content.manifest?.description);\n const version =\n pickNonEmptyString(entry.version) ??\n pickNonEmptyString(content.manifest?.version);\n // catalog 的 category 同 plugin.json 计算逻辑:plugin manifest > marketplace entry > 默认。\n const m = content.manifest ?? undefined;\n const ifaceIn = m?.interface ?? {};\n const entryCategory = pickNonEmptyString(\n (entry as { category?: unknown }).category\n );\n const category =\n pickNonEmptyString(ifaceIn.category) ??\n normalizeCategory(entryCategory) ??\n DEFAULT_CATEGORY;\n const out: CodexCatalogPluginEntry = {\n name: entry.name,\n source: { source: 'local', path: `./plugins/${entry.name}` },\n // stub 入口 → 标记为 NOT_AVAILABLE,让 Codex UI 禁用\"连接\"按钮,\n // 因为点击连接只会写一份没有 MCP key 的伪安装。真正安装走\n // `npx rush-ai@latest plugin install <name>@<marketplace>`。\n // 已装 full 路径在 upsertCodexCatalogPluginFull 中覆写为 AVAILABLE。\n policy: { installation: 'NOT_AVAILABLE', authentication: 'ON_INSTALL' },\n category,\n };\n if (description !== undefined) out.description = description;\n if (version !== undefined) out.version = version;\n return out;\n}\n\nfunction buildPluginJson(\n entry: MarketplacePluginEntry,\n content: PluginContent,\n marketplaceName: string\n): Record<string, unknown> {\n // 元数据来源优先级:plugin manifest(作者写的 plugin.json / 后端 API 详情)→\n // marketplace.json 顶层 entry(claude-plugins-official 这类 marketplace 把元数据\n // 写在 marketplace.json 而非 plugin 目录)。\n const m = content.manifest ?? undefined;\n const author =\n m?.author ??\n (entry.author && typeof entry.author === 'object'\n ? entry.author\n : undefined);\n const homepage =\n pickNonEmptyString(m?.homepage) ?? pickNonEmptyString(entry.homepage);\n const license =\n pickNonEmptyString(m?.license) ?? pickNonEmptyString(entry.license);\n const keywords =\n Array.isArray(m?.keywords) && m.keywords.length > 0\n ? m.keywords\n : Array.isArray(entry.keywords) && entry.keywords.length > 0\n ? entry.keywords\n : undefined;\n // category 不在 MarketplacePluginEntry 强类型里(spec 没定义),但 claude-plugins-official\n // 的 entry 实际有 `category` 字段(如 \"design\" / \"development\")—— 通过 [extra]: unknown 取。\n const entryCategory = pickNonEmptyString(\n (entry as { category?: unknown }).category\n );\n\n // 源代码链接 —— marketplace.json entry 的 `source.url` / `source.path` 指向真正\n // plugin 实现位置(如 GitHub repo)。和 `homepage` 字段(产品官网)互补:\n // - homepage → 产品介绍页(如 cloud.google.com/alloydb)\n // - source.url → 实现仓库(如 github.com/.../alloydb.git)\n // Codex `interface` 仅 1 个 `websiteURL` 字段,所以把 source.url 渲染到\n // longDescription 里作为 markdown 链接,让用户至少能看到。\n const sourceUrl = extractSourceUrl(entry.source);\n\n const description =\n pickNonEmptyString(m?.description) ??\n pickNonEmptyString(entry.description) ??\n entry.name;\n const version =\n pickNonEmptyString(m?.version) ??\n pickNonEmptyString(entry.version) ??\n '0.0.0';\n\n // 详情页\"信息\"区块要展示开发者 / 链接 / 类别 / 示例 prompt 等。\n // 透传作者 manifest 的 interface 字段;缺失时回退到 marketplace.json entry 的字段。\n const ifaceIn = m?.interface ?? {};\n const developerName =\n pickNonEmptyString(ifaceIn.developerName) ??\n pickNonEmptyString(author?.name);\n // 链接区\"网站\"字段:优先级 ifaceIn.websiteURL(作者显式声明)> source.url\n // (GitHub 源码,对开发者最有用)> homepage(产品官网)。\n // claude-plugins-official 的 plugin 多数填了 source.url 指向 git 仓库 + homepage\n // 是产品页 —— 用户在 marketplace 浏览阶段更想看 plugin 实现,所以 source.url\n // 优先级高于 homepage。\n const websiteURL =\n pickNonEmptyString(ifaceIn.websiteURL) ?? sourceUrl ?? homepage;\n const category =\n pickNonEmptyString(ifaceIn.category) ??\n normalizeCategory(entryCategory) ??\n DEFAULT_CATEGORY;\n const capabilities =\n Array.isArray(ifaceIn.capabilities) && ifaceIn.capabilities.length > 0\n ? [...ifaceIn.capabilities]\n : [...DEFAULT_CAPABILITIES];\n\n // 安装引导 —— 详情页用户能看到完整命令。\n //\n // 用 `npx rush-ai@latest`(而非 `rush-ai`)是因为:\n // - 用户机器上**可能没装** rush-ai;npx 自动拉最新版即可执行,无前置依赖\n // - `@latest` 锁定到最新发布版,避免缓存的旧版本不带本次必要功能\n //\n // 命令格式:`npx rush-ai@latest plugin install <name>@<mkt> --target codex`\n //\n // 三个位置都写:\n // 1. shortDescription(副标题 + 卡片视图)—— **短版**警告前缀 + 原描述。\n // Codex UI 单行展示有截断,所以警告只用 1 个 emoji + 7 字提示,给作者描述\n // 留出尽可能多的位置;过长仍会被截但至少 emoji 永远保住,用户能识别。\n // 2. defaultPrompt(详情页\"示例\"区块第一条)—— 用户最容易复制的地方。\n // 3. longDescription(详情页\"描述\"区块)—— 完整警告 banner + 命令 + 原描述。\n const installCmd = `npx rush-ai@latest plugin install ${entry.name}@${marketplaceName} --target codex`;\n\n // 副标题前缀:短到极致(emoji + 7 字汉字 + 分隔符),给作者描述留 30+ 字。\n // emoji ⚠ 在 Codex UI 渲染为视觉焦点,用户扫一眼就能识别\"特殊状态\"。\n const shortHintPrefix = '⚠ 需在终端安装 | ';\n const authorShort =\n pickNonEmptyString(ifaceIn.shortDescription) ?? description;\n\n // 详情页\"描述\"区块组装 ——\n // 1. **作者原描述**最先呈现(用户最想看的内容)\n // 2. 补充链接(仅显示与\"链接\"区那个 websiteURL **不重复**的链接 ——\n // 比如 websiteURL 取了 sourceUrl 时,这里补 homepage;反之亦然)\n // 3. 一段视觉分隔\n // 4. ⚠ 警告 + 安装命令(rush-ai 引导)\n //\n // 实测 Codex UI **不** 渲染 markdown(截图里 `## 链接` `[label](url)` 都是字面量)\n // → 用纯文本格式排版。链接 URL 直接列出来,用户能选中复制。\n const baseLongDescription =\n pickNonEmptyString(ifaceIn.longDescription) ??\n pickNonEmptyString(ifaceIn.shortDescription) ??\n description;\n\n // 补充链接:websiteURL 已经在\"链接\"区显示,这里只列 websiteURL 没占用的另一个。\n const extraLinks: string[] = [];\n if (homepage !== undefined && homepage !== websiteURL) {\n extraLinks.push(`官网:${homepage}`);\n }\n if (\n sourceUrl !== undefined &&\n sourceUrl !== websiteURL &&\n sourceUrl !== homepage\n ) {\n extraLinks.push(`源代码:${sourceUrl}`);\n }\n const extraLinksSection =\n extraLinks.length > 0 ? `\\n\\n${extraLinks.join('\\n')}` : '';\n\n const installSection =\n `———\\n⚠ Codex 桌面端的\"连接\"按钮无法完整安装本插件\\n` +\n `请在终端执行以下命令完成安装:\\n${installCmd}`;\n\n const longDescription = `${baseLongDescription}${extraLinksSection}\\n\\n${installSection}`;\n\n // 作者声明的 defaultPrompt 优先;为空时塞引导命令本身做兜底(让示例区块不为空)。\n const defaultPrompt: string[] = Array.isArray(ifaceIn.defaultPrompt)\n ? [...ifaceIn.defaultPrompt]\n : [];\n if (defaultPrompt.length === 0) {\n defaultPrompt.push(installCmd);\n }\n\n const ifaceOut: Record<string, unknown> = {\n displayName: pickNonEmptyString(ifaceIn.displayName) ?? entry.name,\n shortDescription: `${shortHintPrefix}${authorShort}`,\n longDescription,\n category,\n capabilities,\n defaultPrompt,\n };\n if (developerName !== undefined) ifaceOut.developerName = developerName;\n if (websiteURL !== undefined) ifaceOut.websiteURL = websiteURL;\n if (pickNonEmptyString(ifaceIn.privacyPolicyURL) !== undefined)\n ifaceOut.privacyPolicyURL = ifaceIn.privacyPolicyURL;\n if (pickNonEmptyString(ifaceIn.termsOfServiceURL) !== undefined)\n ifaceOut.termsOfServiceURL = ifaceIn.termsOfServiceURL;\n if (pickNonEmptyString(ifaceIn.brandColor) !== undefined)\n ifaceOut.brandColor = ifaceIn.brandColor;\n if (pickNonEmptyString(ifaceIn.composerIcon) !== undefined)\n ifaceOut.composerIcon = ifaceIn.composerIcon;\n if (pickNonEmptyString(ifaceIn.logo) !== undefined)\n ifaceOut.logo = ifaceIn.logo;\n if (Array.isArray(ifaceIn.screenshots) && ifaceIn.screenshots.length > 0)\n ifaceOut.screenshots = [...ifaceIn.screenshots];\n\n const out: Record<string, unknown> = {\n // marker:让 detectFullPluginDir 可靠区分 sync vs install 写出的目录。\n // Codex 不知道这个字段,会被忽略;marker 必须是合法 JSON 字段名才能被 plugin.json 接受。\n [STUB_MARKER_KEY]: STUB_MARKER_VALUE,\n name: entry.name,\n version,\n description,\n skills: './skills/',\n interface: ifaceOut,\n };\n // 顶层透传作者元数据 —— Codex 详情页\"开发者 / 信息\"区块会读 author / homepage / license。\n if (author && typeof author === 'object') {\n out.author = { ...author };\n }\n if (homepage !== undefined) {\n out.homepage = homepage;\n }\n // repository 字段 —— Codex `documents` 等 OpenAI bundled plugin 的 plugin.json\n // 顶层有这个字段。我们也写出来(用 sourceUrl 即 GitHub repo URL)以保持一致性,\n // 即便 Codex UI 当前不显示 repository 字段,未来版本可能会展示。\n if (sourceUrl !== undefined) {\n out.repository = sourceUrl;\n }\n if (license !== undefined) {\n out.license = license;\n }\n if (keywords !== undefined) {\n out.keywords = [...keywords];\n }\n if (hasMcpServers(content)) {\n out.mcpServers = './.mcp.json';\n }\n return out;\n}\n\n/**\n * 从 marketplace.json plugin entry 的 `source` 字段提取真正的源码 URL。\n *\n * 处理三种 shape:\n * - 字符串:\"./plugins/foo\" 形式 → 不是 URL,返回 undefined\n * - 对象含 `url`:`{source: \"git-subdir\", url: \"https://github.com/...\", ...}`\n * → 返回 URL(去掉 `.git` 后缀以便用户在浏览器直接打开)\n * - 其他:undefined\n *\n * 仅对 `https://` / `http://` 开头的 URL 生效,防止把 `local` / 路径误当链接。\n */\nfunction extractSourceUrl(\n source: MarketplacePluginEntry['source']\n): string | undefined {\n if (!source || typeof source === 'string') return undefined;\n const obj = source as { url?: unknown };\n const url = obj.url;\n if (typeof url !== 'string' || url.length === 0) return undefined;\n if (!/^https?:\\/\\//i.test(url)) return undefined;\n // 去 .git 后缀 → 浏览器友好(github.com/x/y.git → github.com/x/y)\n return url.replace(/\\.git$/, '');\n}\n\n/**\n * 把 marketplace.json entry 的 `category`(小写、可能是英文 slug)规范化到 Codex\n * 详情页期望的\"首字母大写\"格式。Codex 显示的 category 是字面量,不识别就直接显示\n * 原始字符串。\n *\n * 已知 mapping(来自 claude-plugins-official 实际值):\n * - \"design\" → \"Design\"\n * - \"development\" → \"Engineering\"(claude 用 development,Codex 用 Engineering)\n * - \"security\" → \"Security\"\n * - \"data\" → \"Engineering\"\n *\n * 未匹配的值 → 首字母大写后透传。\n */\nfunction normalizeCategory(raw: string | undefined): string | undefined {\n if (raw === undefined) return undefined;\n const lower = raw.toLowerCase().trim();\n const map: Record<string, string> = {\n development: 'Engineering',\n data: 'Engineering',\n devops: 'Engineering',\n design: 'Design',\n productivity: 'Productivity',\n security: 'Security',\n research: 'Research',\n automation: 'Productivity',\n integration: 'Engineering',\n };\n if (lower in map) return map[lower];\n // 首字母大写\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n}\n\n/**\n * 写出一个 plugin 镜像目录里的全部 materials:\n * - 总是写 `.codex-plugin/plugin.json`\n * - 若 content 含 mcpServers → 写 `.mcp.json`(占位符 `${KEY}` 不替换)\n * - 若 content 提供 `skillsSourceDir` 且目录存在 → cp 到镜像 `skills/`\n *\n * 写之前 best-effort 清理旧 stub `.mcp.json` / `skills/`,保证刷新时不留旧内容。\n */\nasync function writePluginMaterials(\n pluginDir: string,\n entry: MarketplacePluginEntry,\n content: PluginContent,\n marketplaceName: string\n): Promise<void> {\n const manifestPath = codexPluginManifestPath(pluginDir);\n await mkdir(dirname(manifestPath), { recursive: true });\n\n // 先清旧 .mcp.json / skills/(刷新场景),保证状态可由当前 content 完整决定\n await rm(codexPluginMcpPath(pluginDir), { force: true }).catch(() => {});\n await rm(codexPluginSkillsDir(pluginDir), {\n recursive: true,\n force: true,\n }).catch(() => {});\n\n const pluginJson = buildPluginJson(entry, content, marketplaceName);\n await writeFileAtomic(\n manifestPath,\n `${JSON.stringify(pluginJson, null, 2)}\\n`\n );\n\n if (hasMcpServers(content)) {\n const mcpPath = codexPluginMcpPath(pluginDir);\n await writeFileAtomic(\n mcpPath,\n `${JSON.stringify({ mcpServers: content.mcpServers }, null, 2)}\\n`\n );\n }\n\n if (content.skillsSourceDir && (await isDir(content.skillsSourceDir))) {\n const skillsDir = codexPluginSkillsDir(pluginDir);\n await mkdir(skillsDir, { recursive: true });\n await cp(content.skillsSourceDir, skillsDir, {\n recursive: true,\n dereference: false,\n preserveTimestamps: true,\n verbatimSymlinks: true,\n });\n } else if (\n Array.isArray(content.skillsPlaceholders) &&\n content.skillsPlaceholders.length > 0\n ) {\n // 真实 skills 拿不到时,为每个 skill 写占位 SKILL.md,让 Codex 详情页\"技能\"区\n // 显示名字。\n //\n // Codex 要求 `skills/<single-segment>/SKILL.md` —— 不能有嵌套子目录。所以\n // skill name 中的 `/`、`@` 等会被 slug 化作目录名(例:`@kanyun/sonic-query`\n // → `kanyun-sonic-query`);SKILL.md frontmatter 的 `name` 字段保留原始全名\n // 让 UI 显示完整的 `@scope/name`。\n const installCmd =\n pickNonEmptyString(content.installCommand) ??\n `npx rush-ai@latest plugin install ${entry.name}@${marketplaceName} --target codex`;\n const pluginWebUrl = pickNonEmptyString(content.pluginWebUrl);\n\n const skillsDir = codexPluginSkillsDir(pluginDir);\n const usedDirs = new Set<string>();\n for (const skill of content.skillsPlaceholders) {\n if (!isSafeSkillName(skill.name)) continue;\n const dirName = uniqueDirName(slugifySkillName(skill.name), usedDirs);\n usedDirs.add(dirName);\n const skillDir = resolve(skillsDir, dirName);\n await mkdir(skillDir, { recursive: true });\n\n const rawSkillMd = pickNonEmptyString(skill.rawSkillMd);\n if (rawSkillMd !== undefined) {\n // 真实 SKILL.md 内容(rush:// 后端 .md 端点拉到的)—— 直接写入磁盘。\n // 但**必须重写 frontmatter `name` 字段**为目录名 slug,否则 Codex 校验\n // `name` 跟目录名不一致会丢弃整个 SKILL(实测过 yaml 解析问题,name 含\n // `@` `/` 也会失败)。其他 frontmatter 字段(description / version /\n // tags / trigger)原样保留。\n const rewritten = rewriteSkillMdName(rawSkillMd, dirName);\n await writeFileAtomic(resolve(skillDir, 'SKILL.md'), rewritten);\n continue;\n }\n\n // 没拿到真内容 → 写占位 SKILL.md 引导用户安装。\n // SKILL.md frontmatter 严格按 OpenAI bundled plugin 的格式:name / description\n // 都用双引号包裹。description 含 `@` `/` 等 yaml 特殊字符时,不加引号会让\n // yaml 解析报错 → Codex 视为损坏 SKILL,整个 plugin 的 skills 区块都不显示。\n const desc =\n pickNonEmptyString(skill.description) ?? `Skill ${skill.name}`;\n const fullDesc =\n skill.name === dirName ? desc : `${skill.name} — ${desc}`;\n const skillUrl = pickNonEmptyString(skill.url);\n const body = buildSkillPlaceholderBody({\n name: skill.name,\n description: fullDesc,\n skillUrl,\n pluginWebUrl,\n installCmd,\n });\n const md =\n `---\\n` +\n `name: ${jsonQuote(dirName)}\\n` +\n `description: ${jsonQuote(fullDesc)}\\n` +\n `---\\n\\n${body}\\n`;\n await writeFileAtomic(resolve(skillDir, 'SKILL.md'), md);\n }\n }\n}\n\n/**\n * 把 SKILL.md frontmatter 的 `name` 字段重写为指定 slug。\n *\n * 输入是从 rush 后端 `.md` 端点拿到的真 SKILL.md,frontmatter 的 `name` 可能是\n * `@kanyun/foo` 这种含特殊字符的全名 —— Codex yaml 解析会失败、校验会失败 ——\n * 必须替换成目录名 slug(如 `kanyun-foo`),同时给值加双引号防 yaml 特殊字符。\n *\n * 其他字段全部原样保留(description / version / trigger / tags / 内容 body)。\n *\n * 实现:从开头的 `---` 之间找 `name:` 行替换;如果没 frontmatter 就在文件最前\n * 加一段最简 frontmatter。\n */\nfunction rewriteSkillMdName(raw: string, dirName: string): string {\n const text = raw.replace(/\\r\\n/g, '\\n');\n // 检测 frontmatter\n if (!text.startsWith('---\\n')) {\n // 没 frontmatter → 在最前补\n return (\n `---\\nname: ${jsonQuote(dirName)}\\ndescription: ${jsonQuote(dirName)}\\n---\\n\\n` +\n text\n );\n }\n const lines = text.split('\\n');\n // 找闭合的 `---`\n let closeIdx = -1;\n for (let i = 1; i < lines.length; i++) {\n if (lines[i] === '---') {\n closeIdx = i;\n break;\n }\n }\n if (closeIdx === -1) {\n // frontmatter 没闭合 —— 当无 frontmatter 处理\n return (\n `---\\nname: ${jsonQuote(dirName)}\\ndescription: ${jsonQuote(dirName)}\\n---\\n\\n` +\n text\n );\n }\n // frontmatter 范围 [1, closeIdx)\n let foundName = false;\n for (let i = 1; i < closeIdx; i++) {\n const line = lines[i] ?? '';\n if (/^name\\s*:/.test(line)) {\n lines[i] = `name: ${jsonQuote(dirName)}`;\n foundName = true;\n break;\n }\n }\n if (!foundName) {\n // 在 frontmatter 第一行插入 name\n lines.splice(1, 0, `name: ${jsonQuote(dirName)}`);\n }\n return lines.join('\\n');\n}\n\n/**\n * 占位 SKILL.md 的 markdown body —— 让用户在 Codex 详情页一眼看到:\n * 1. 这是占位(未真装)\n * 2. 装好的命令\n * 3. 跳到 web 站点看完整 skill 内容的链接\n *\n * 链接缺失时退化为只展示文字。\n */\nfunction buildSkillPlaceholderBody(input: {\n name: string;\n description: string;\n skillUrl?: string | undefined;\n pluginWebUrl?: string | undefined;\n installCmd: string;\n}): string {\n const lines: string[] = [];\n lines.push(`# ${input.name}`);\n lines.push('');\n lines.push(input.description);\n lines.push('');\n lines.push(\n '> 这是 rush-ai marketplace 同步出的占位 skill —— skill 完整内容(trigger / 操作步骤 / 提示词)尚未下载。'\n );\n lines.push('>');\n lines.push('> 在终端执行以下命令以完整安装:');\n lines.push('>');\n lines.push('> ```bash');\n lines.push(`> ${input.installCmd}`);\n lines.push('> ```');\n if (input.skillUrl !== undefined) {\n lines.push('>');\n lines.push(`> 也可在 web 站点查看:[${input.name}](${input.skillUrl})`);\n }\n if (input.pluginWebUrl !== undefined) {\n lines.push('>');\n lines.push(`> 浏览所属 plugin:${input.pluginWebUrl}`);\n }\n return lines.join('\\n');\n}\n\n/**\n * YAML/JSON 双引号字符串 —— 用 JSON.stringify 把任何字符串转成 yaml-safe 的\n * `\"...\"` 形式(双引号 + 反斜杠转义)。yaml 1.2 双引号串和 JSON 字符串语法兼容,\n * 所以这是最可靠的 escape 方式。\n */\nfunction jsonQuote(s: string): string {\n return JSON.stringify(s);\n}\n\n/**\n * 把 `@scope/name` 转成单段目录名 —— 去掉前导 `@`,把 `/` / `.` 等转成 `-`。\n * Codex 要求 skill 目录名是 path 单段。\n */\nfunction slugifySkillName(name: string): string {\n return (\n name\n .replace(/^@/, '')\n .replace(/[\\\\/.]+/g, '-')\n .replace(/-+/g, '-')\n .replace(/^-+|-+$/g, '') || 'skill'\n );\n}\n\nfunction uniqueDirName(base: string, used: Set<string>): string {\n if (!used.has(base)) return base;\n let i = 2;\n while (used.has(`${base}-${i}`)) i += 1;\n return `${base}-${i}`;\n}\n\n/**\n * Skill name 白名单 —— 与 plugin name 同构,但允许 `@scope/name` 形态\n * (例:`@kanyun/rush-mcp-doris-query`)。\n */\nfunction isSafeSkillName(name: string): boolean {\n if (typeof name !== 'string' || name.length === 0) return false;\n if (name === '.' || name === '..') return false;\n if (name.startsWith('.')) return false;\n if (name.includes('\\\\')) return false;\n if (name.includes('..')) return false;\n // `/` 限制为最多一段(@scope/name),其他位置不允许\n const slashCount = (name.match(/\\//g) ?? []).length;\n if (slashCount > 1) return false;\n if (slashCount === 1 && !name.startsWith('@')) return false;\n return /^@?[A-Za-z0-9_][A-Za-z0-9._@/-]*$/.test(name);\n}\n\nfunction hasMcpServers(content: PluginContent): boolean {\n return (\n !!content.mcpServers &&\n typeof content.mcpServers === 'object' &&\n Object.keys(content.mcpServers).length > 0\n );\n}\n\nfunction pickNonEmptyString(value: unknown): string | undefined {\n if (typeof value === 'string' && value.length > 0) return value;\n return undefined;\n}\n\n// ---------------------------------------------------------------------------\n// \"full vs stub\" 判定\n// ---------------------------------------------------------------------------\n\n/**\n * 判断 `<mktDir>/plugins/<name>/` 是当前哪一种形态。\n *\n * - `full`:`rush-ai plugin install` 真实安装写出的目录,sync 必须保留不动。\n * - `stub`:sync 自己写出的(或不完整目录),sync 可以重建(覆盖刷新)。\n *\n * 主信号 —— marker:sync 写出的 plugin.json 顶层带 `\"x-rush-mirror\": \"stub\"`,\n * install 路径写出的 plugin.json 没有这个字段。靠 marker 区分远比\"启发式扫描\"可靠 ——\n * 例如 octopus 这种 plugin 默认 env 就是真值(无 `${KEY}` 占位符),无 marker 时无法\n * 与 install 写出的\"已替换 secret\"区分。\n *\n * 兼容旧 sync 写出的目录(无 marker):fallback 看真 install 才会写的信号 ——\n * - `<pluginDir>/skills/` 非空(install 路径会拷贝真 skills 内容)\n * - `<pluginDir>/.mcp.json` 不含 `${KEY}` 占位符(install 已注入 secret)\n *\n * 仅当上述信号都缺失,视为 stub 让 sync 接管刷新。\n */\ntype FullState = 'full' | 'stub';\n\nconst STUB_MARKER_KEY = 'x-rush-mirror';\nconst STUB_MARKER_VALUE = 'stub';\n\nasync function detectFullPluginDir(pluginDir: string): Promise<FullState> {\n if (!(await isDir(pluginDir))) return 'stub';\n\n const manifestPath = codexPluginManifestPath(pluginDir);\n if (await pathExists(manifestPath)) {\n try {\n const raw = await readFile(manifestPath, 'utf8');\n const parsed = JSON.parse(raw) as Record<string, unknown>;\n if (parsed[STUB_MARKER_KEY] === STUB_MARKER_VALUE) {\n return 'stub';\n }\n } catch {\n // JSON 损坏 → 走兼容启发式\n }\n }\n\n // 兼容路径:无 marker 时按 install 才会写的真信号判定\n if (await dirHasEntries(codexPluginSkillsDir(pluginDir))) {\n return 'full';\n }\n\n const mcpPath = codexPluginMcpPath(pluginDir);\n if (await pathExists(mcpPath)) {\n try {\n const raw = await readFile(mcpPath, 'utf8');\n if (!/\\$\\{[^}]+\\}/.test(raw)) {\n return 'full';\n }\n } catch {\n // 读不到当 stub 处理\n }\n }\n\n return 'stub';\n}\n\nasync function dirHasEntries(dir: string): Promise<boolean> {\n if (!(await isDir(dir))) return false;\n let entries: Dirent[];\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return false;\n }\n return entries.length > 0;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction buildInterfaceCategory(manifest: PluginManifest): string {\n return manifest.interface?.category ?? DEFAULT_CATEGORY;\n}\n\nasync function writeFileAtomic(\n filePath: string,\n content: string\n): Promise<void> {\n await mkdir(dirname(filePath), { recursive: true });\n const tmp = `${filePath}.${randomUUID()}.tmp`;\n try {\n await writeFile(tmp, content, { encoding: 'utf8', flag: 'w' });\n await rename(tmp, filePath);\n } catch (err) {\n await rm(tmp, { force: true }).catch(() => {});\n throw err;\n }\n}\n\nasync 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\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 * Codex Installer 路径 helper。\n *\n * Source of truth: `specs/plugin-schemas.md` §2.1 目录布局。\n *\n * 磁盘布局(全部以 `home` 注入目录为根,生产默认 `os.homedir()`):\n * ```\n * <home>/.codex/\n * ├── config.toml ← TOML 合并文件\n * └── plugins/\n * ├── marketplaces/\n * │ └── <marketplace>/\n * │ ├── .agents/plugins/marketplace.json ← 插件页 marketplace manifest\n * │ └── plugins/<plugin>/... ← 最新 plugin 镜像\n * └── cache/\n * └── <marketplace>/<plugin>/<version>/... ← 运行时 cache\n * ```\n *\n * **测试铁律**:所有单测必须通过 `home` 注入 `os.tmpdir()` 下的临时目录,\n * **禁止**写用户真实 `~/.codex/`。\n */\n\nimport { resolve as pathResolve } from 'node:path';\nimport type { PluginRef } from '../types.js';\n\n/**\n * `<home>/.codex/` 目录。`detect()` 用此判断 Codex 是否安装。\n */\nexport function codexHomeDir(home: string): string {\n return pathResolve(home, '.codex');\n}\n\n/**\n * `<home>/.codex/config.toml` —— 合并 TOML 文件。\n */\nexport function codexConfigTomlPath(home: string): string {\n return pathResolve(codexHomeDir(home), 'config.toml');\n}\n\n/**\n * `<home>/.codex/plugins/cache/` —— plugin cache 根目录。\n */\nexport function codexPluginsCacheDir(home: string): string {\n return pathResolve(codexHomeDir(home), 'plugins', 'cache');\n}\n\n/**\n * `<home>/.codex/plugins/marketplaces/` —— Codex 插件页读取的 marketplace 根。\n */\nexport function codexPluginsMarketplacesDir(home: string): string {\n return pathResolve(codexHomeDir(home), 'plugins', 'marketplaces');\n}\n\n/**\n * 单个 Codex marketplace mirror 目录:\n * `<home>/.codex/plugins/marketplaces/<marketplace>/`。\n */\nexport function codexMarketplaceDir(home: string, marketplace: string): string {\n return pathResolve(codexPluginsMarketplacesDir(home), marketplace);\n}\n\n/**\n * Codex marketplace manifest:\n * `<marketplaceDir>/.agents/plugins/marketplace.json`。\n */\nexport function codexMarketplaceManifestPath(marketplaceDir: string): string {\n return pathResolve(marketplaceDir, '.agents', 'plugins', 'marketplace.json');\n}\n\n/**\n * Marketplace mirror 中单个 plugin 的目录:\n * `<marketplaceDir>/plugins/<plugin>/`。\n */\nexport function codexMarketplacePluginDir(\n marketplaceDir: string,\n pluginName: string\n): string {\n return pathResolve(marketplaceDir, 'plugins', pluginName);\n}\n\n/**\n * 单个 plugin 版本的 cache 目录:\n * `<home>/.codex/plugins/cache/<marketplace>/<plugin>/<version>/`。\n *\n * 对齐 spec §2.1:三段 marketplace/plugin/version 目录,和 Claude Code\n * cache layout 完全同构。\n */\nexport function codexPluginVersionDir(\n home: string,\n ref: PluginRef,\n version: string\n): string {\n return pathResolve(\n codexPluginsCacheDir(home),\n ref.marketplace,\n ref.name,\n version\n );\n}\n\n/**\n * plugin cache 版本目录下的 `.codex-plugin/plugin.json`(Codex 原生约定)。\n */\nexport function codexPluginManifestPath(versionDir: string): string {\n return pathResolve(versionDir, '.codex-plugin', 'plugin.json');\n}\n\n/**\n * plugin cache 版本目录下的外部 `.mcp.json`(Codex 特有——spec §2.4)。\n */\nexport function codexPluginMcpPath(versionDir: string): string {\n return pathResolve(versionDir, '.mcp.json');\n}\n\n/**\n * plugin cache 版本目录下的 `skills/` 子目录。\n */\nexport function codexPluginSkillsDir(versionDir: string): string {\n return pathResolve(versionDir, 'skills');\n}\n\n/**\n * 生成 config.toml 备份路径——`config.toml.bak.<YYYYMMDD-HHMMSS>`。\n *\n * 时间戳使用 UTC 以便多机协作时备份路径可比较(spec §2.2 不要求 local time)。\n */\nexport function codexConfigBackupPath(home: string, now: Date): string {\n const ts = formatBackupTimestamp(now);\n return `${codexConfigTomlPath(home)}.bak.${ts}`;\n}\n\n/**\n * 格式化备份时间戳 `YYYYMMDD-HHMMSS`(UTC)。\n *\n * 导出便于测试。\n */\nexport function formatBackupTimestamp(date: Date): string {\n const pad = (n: number) => String(n).padStart(2, '0');\n const yyyy = date.getUTCFullYear();\n const mm = pad(date.getUTCMonth() + 1);\n const dd = pad(date.getUTCDate());\n const hh = pad(date.getUTCHours());\n const mi = pad(date.getUTCMinutes());\n const ss = pad(date.getUTCSeconds());\n return `${yyyy}${mm}${dd}-${hh}${mi}${ss}`;\n}\n\n/**\n * config.toml 里 marketplace section 的 header key(spec §2.2)。\n * 例如 `\"marketplaces.rush-marketplace\"`。\n */\nexport function marketplaceSectionKey(marketplaceName: string): string {\n return `marketplaces.${marketplaceName}`;\n}\n\n/**\n * config.toml 里 plugin section 的 header key(spec §2.2)。\n * 例如 `plugins.\"rush@rush-marketplace\"`(带引号——@iarna/toml 自动处理)。\n *\n * 返回\"逻辑 key\"——插入 TOML 对象时直接作为嵌套 key 使用,stringify 时\n * @iarna/toml 会自动按 TOML 规范加引号。\n */\nexport function pluginSectionKey(ref: PluginRef): string {\n return `${ref.name}@${ref.marketplace}`;\n}\n","/**\n * `~/.codex/config.toml` 读-改-写(task-8 产物)。\n *\n * Source of truth: `specs/plugin-schemas.md` §2.2。\n *\n * 关键规则(spec §2.2):\n * - 用 `@iarna/toml` parse / stringify(注释不保留——决策已定)\n * - 写前**必须**备份 `config.toml.bak.<YYYYMMDD-HHMMSS>`\n * - 写前读 mtime,写完后再 stat 确认(并发保护)\n * - 保留用户 sections:`[model_providers.*]` / `[projects.*]` / 顶层字段\n * `model` / `approval_policy` / `sandbox_mode` 等——全部通过 \"read-merge-write\" 保留\n * - 所有写入走 write `.tmp` → `rename` 原子替换\n * - `[plugins.\"<name>@<mkt>\"]` key 含 `@`,@iarna/toml 自动处理引号(已实测)\n *\n * 错误处理:\n * - 文件不存在 → 视为空 config(返回 `{}`)\n * - TOML 解析失败 → 抛 `CodexConfigTomlCorruptError`(CLI 层提示备份路径)\n * - mtime 冲突 → 抛 `CodexConfigTomlConflictError`(让用户重试)\n *\n * 命名约定:\n * - `CodexConfigToml` = 数据 shape(`JsonMap` 别名)\n * - `readCodexConfig` / `writeCodexConfig` = 顶层读写 API\n * - `backupCodexConfig` = 备份当前文件到 `<path>.bak.<ts>`\n * - `setMarketplaceSection` / `removeMarketplaceSection` / `setPluginEntry` / `removePluginEntry`\n * = 结构化的 section 操作函数(纯函数,接受 config → 返回新 config)\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { constants as fsConstants } from 'node:fs';\nimport {\n access,\n copyFile,\n mkdir,\n readFile,\n rename,\n rm,\n stat,\n writeFile,\n} from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport tomlModule from '@iarna/toml';\n\nconst { parse: tomlParse, stringify: tomlStringify } = tomlModule;\n\n// ---------------------------------------------------------------------------\n// 类型 & 错误\n// ---------------------------------------------------------------------------\n\n/**\n * config.toml 解析后的顶层对象。\n *\n * 不强约束 shape——@iarna/toml 返回的 `JsonMap` 即为本类型,保留用户任意自定义\n * sections / 顶层字段(spec §2.2 明确要求保留 `[model_providers.*]` 等)。\n */\n// @iarna/toml 的 JsonMap 不直接导出,这里用与 stringify 入参相同的 shape:\nexport type CodexConfigToml = Record<string, unknown>;\n\n/** 基类 —— CLI 层统一 `instanceof CodexConfigTomlError` catch。 */\nexport class CodexConfigTomlError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'CodexConfigTomlError';\n }\n}\n\n/** TOML 解析失败 —— 指向备份路径供用户处理。 */\nexport class CodexConfigTomlCorruptError extends CodexConfigTomlError {\n constructor(\n public readonly filePath: string,\n public readonly cause: unknown\n ) {\n super(\n `Codex config.toml 解析失败(${filePath})。请备份后手工修复。原因:${String(\n (cause as Error | undefined)?.message ?? cause\n )}`\n );\n this.name = 'CodexConfigTomlCorruptError';\n }\n}\n\n/** 并发写冲突(mtime 在读-改-写期间被其他进程修改)。 */\nexport class CodexConfigTomlConflictError extends CodexConfigTomlError {\n constructor(public readonly filePath: string) {\n super(\n `Codex config.toml 并发写冲突:读取和写入之间 ${filePath} 被其他进程修改。请重试。`\n );\n this.name = 'CodexConfigTomlConflictError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// 读 / 写 / 备份\n// ---------------------------------------------------------------------------\n\n/**\n * 从磁盘读 config.toml + 记录 mtime(用于并发保护)。\n *\n * - 文件不存在 → 返回空配置 `{}`,`mtimeMs = null`\n * - 解析失败 → `CodexConfigTomlCorruptError`\n *\n * 返回的 `data` 是可 mutate 的副本(@iarna/toml 每次 parse 都新建对象)。\n */\nexport async function readCodexConfig(\n filePath: string\n): Promise<{ data: CodexConfigToml; mtimeMs: number | null }> {\n if (!(await pathExists(filePath))) {\n return { data: {}, mtimeMs: null };\n }\n\n const stats = await stat(filePath);\n const raw = await readFile(filePath, 'utf8');\n\n try {\n const parsed = tomlParse(raw);\n return { data: parsed as CodexConfigToml, mtimeMs: stats.mtimeMs };\n } catch (err) {\n throw new CodexConfigTomlCorruptError(filePath, err);\n }\n}\n\n/**\n * 备份当前 config.toml 到 `<path>.bak.<ts>`。\n *\n * - 源文件不存在 → no-op,返回 null(无需备份)\n * - 使用 `copyFile`(非 rename,保留原文件位置给后续 `writeCodexConfig` 覆盖)\n * - 若目标 `.bak.<ts>` 已存在(同秒多次 backup),追加 `.<uuid>` 避免覆盖\n *\n * @returns 备份文件绝对路径;源文件不存在时返回 null\n */\nexport async function backupCodexConfig(\n filePath: string,\n backupPath: string\n): Promise<string | null> {\n if (!(await pathExists(filePath))) {\n return null;\n }\n // 目标已存在 → 附加 uuid,不覆盖已有备份\n let finalBackup = backupPath;\n if (await pathExists(finalBackup)) {\n finalBackup = `${backupPath}.${randomUUID().slice(0, 8)}`;\n }\n await copyFile(filePath, finalBackup);\n return finalBackup;\n}\n\n/**\n * 原子写 config.toml(+ mtime 冲突检测)。\n *\n * 流程:\n * 1. 写前 `stat` 当前磁盘 mtime,与 `expectedMtimeMs` 比较\n * - `null` 意味着我们 load 时文件不存在;此时允许仅当现在依然不存在\n * - 值不等 → `CodexConfigTomlConflictError`(不 retry——spec §2.2 要求\n * \"写前读 mtime,写完后再 stat 确认\";retry 策略留给 Installer 层感知)\n * 2. stringify → write `.tmp` → rename 原子替换\n * 3. 写完后 stat 获取新 mtime 返回(方便 Installer 后续串联操作)\n *\n * **注意**:本函数不处理备份——备份由调用方显式调用 `backupCodexConfig`。\n * 解耦原因:installer.ts 需要在失败回滚时把备份 restore 回来,备份路径必须\n * 由上层管控、透传到 rollback。\n */\nexport async function writeCodexConfig(\n filePath: string,\n data: CodexConfigToml,\n expectedMtimeMs: number | null\n): Promise<{ mtimeMs: number }> {\n await assertNoMtimeDrift(filePath, expectedMtimeMs);\n\n const serialized = tomlStringify(data as Parameters<typeof tomlStringify>[0]);\n await atomicWrite(filePath, serialized);\n\n const afterStats = await stat(filePath);\n return { mtimeMs: afterStats.mtimeMs };\n}\n\n/**\n * 从备份文件恢复 config.toml(用于失败回滚)。\n *\n * - 备份路径为空字符串 / null / 不存在 → 说明 install 前没有原文件,直接删除当前\n * config.toml(如果存在)即可,让磁盘彻底回到\"install 前状态\"\n * - 备份存在 → copy 回去(原子替换)并删掉备份\n */\nexport async function restoreCodexConfigFromBackup(\n filePath: string,\n backupPath: string | null\n): Promise<void> {\n if (!backupPath || !(await pathExists(backupPath))) {\n // 没有备份 → install 前本无 config.toml,回滚 = 清掉当前文件(如果我们写过)\n if (await pathExists(filePath)) {\n await rm(filePath, { force: true });\n }\n return;\n }\n // 有备份 → 原子覆盖当前文件 + 删备份\n const raw = await readFile(backupPath, 'utf8');\n await atomicWrite(filePath, raw);\n await rm(backupPath, { force: true }).catch(() => {});\n}\n\n// ---------------------------------------------------------------------------\n// Section 操作(纯函数,不触磁盘)\n// ---------------------------------------------------------------------------\n\n/**\n * 在 config 上写入 / 更新 `[marketplaces.<name>]` section。\n *\n * - 若 `config.marketplaces` 缺失,自动创建 object\n * - 保留同 `marketplaces` 下其他 marketplace(只更新 `name` 这一个 key)\n *\n * **mutates** 传入对象(调用方已是内存副本,无外部副作用风险)。\n */\nexport function setMarketplaceSection(\n config: CodexConfigToml,\n name: string,\n entry: { last_updated: string; source_type: string; source: string }\n): void {\n const mkts = ensureObjectSection(config, 'marketplaces');\n mkts[name] = { ...entry };\n}\n\n/**\n * 移除 `[marketplaces.<name>]` section。若本就没有 → no-op。\n *\n * 若移除后 `marketplaces` 变空对象,**同步删除父级 `marketplaces` key**——\n * 否则 @iarna/toml stringify 出 `marketplaces = { }` 空 inline table,视觉\n * 和语义上都是污染(regression fix,回归测试 bug #5)。\n */\nexport function removeMarketplaceSection(\n config: CodexConfigToml,\n name: string\n): void {\n const mkts = config.marketplaces as Record<string, unknown> | undefined;\n if (!mkts || typeof mkts !== 'object') return;\n delete mkts[name];\n if (Object.keys(mkts).length === 0) {\n delete config.marketplaces;\n }\n}\n\n/**\n * 在 config 上写入 / 更新 `[plugins.\"<name>@<mkt>\"]` section。\n *\n * - key 带 `@`,stringify 时 @iarna/toml 自动加引号(已实测)\n * - 保留同 `plugins` 下其他 plugin 条目\n * - rush-ai 始终写 `enabled = true`(spec §2.2 约定)\n */\nexport function setPluginEntry(\n config: CodexConfigToml,\n key: string,\n entry: { enabled: boolean }\n): void {\n const plugins = ensureObjectSection(config, 'plugins');\n plugins[key] = { ...entry };\n}\n\n/**\n * 移除 `[plugins.\"<name>@<mkt>\"]` section。若本就没有 → no-op。\n *\n * 若移除后 `plugins` 变空对象,同步删除父级 `plugins` key——避免留下\n * `plugins = { }` 空 inline table 污染 config.toml(regression fix,回归测试 bug #5)。\n */\nexport function removePluginEntry(config: CodexConfigToml, key: string): void {\n const plugins = config.plugins as Record<string, unknown> | undefined;\n if (!plugins || typeof plugins !== 'object') return;\n delete plugins[key];\n if (Object.keys(plugins).length === 0) {\n delete config.plugins;\n }\n}\n\n/**\n * `[mcp_servers.<name>]` section 的 marker key —— rush-ai 写出的 entry 才有\n * 这个字段。uninstall 时仅删除带匹配 marker 的 entry,避免误删用户手写的 server。\n *\n * Codex 接受顶层未知字段并忽略(实测过),所以 marker 不影响 MCP runtime 行为。\n */\nexport const MCP_OWNER_MARKER_KEY = '_rush_ai_owner' as const;\n\n/**\n * 在 config 上写入 / 更新 `[mcp_servers.<name>]` section。\n *\n * `entry` 字段支持 stdio + http 两种 transport:\n * - stdio: `{ command, args?, env? }`(+ 可选 `cwd`)\n * - http: `{ url, http_headers?, bearer_token_env_var? }`\n *\n * 自动加 marker `_rush_ai_owner = \"<plugin>@<mkt>\"` —— uninstall 用它识别 rush-ai 自己写的。\n *\n * **mutates** 传入对象。\n */\nexport function setMcpServerSection(\n config: CodexConfigToml,\n serverName: string,\n ownerKey: string,\n entry: Record<string, unknown>\n): void {\n const servers = ensureObjectSection(config, 'mcp_servers');\n servers[serverName] = { ...entry, [MCP_OWNER_MARKER_KEY]: ownerKey };\n}\n\n/**\n * 移除 `[mcp_servers.<name>]` section —— **仅当**它由 rush-ai 自己写出(marker\n * 匹配 `expectedOwner`),避免误删用户手动添加的同名 server。\n *\n * 返回值:\n * - `'removed'`:成功删除\n * - `'absent'`:section 本就不存在\n * - `'foreign'`:section 存在但 marker 不匹配(用户手写或其他工具创建)→ 不动\n */\nexport function removeMcpServerSection(\n config: CodexConfigToml,\n serverName: string,\n expectedOwner: string\n): 'removed' | 'absent' | 'foreign' {\n const servers = config.mcp_servers as Record<string, unknown> | undefined;\n if (!servers || typeof servers !== 'object') return 'absent';\n const entry = servers[serverName] as Record<string, unknown> | undefined;\n if (!entry || typeof entry !== 'object') return 'absent';\n const owner = entry[MCP_OWNER_MARKER_KEY];\n if (owner !== expectedOwner) return 'foreign';\n delete servers[serverName];\n if (Object.keys(servers).length === 0) {\n delete config.mcp_servers;\n }\n return 'removed';\n}\n\n/**\n * 查询 `[mcp_servers.<name>]` 当前归属(marker 值)。\n *\n * 返回值:\n * - `null`:section 不存在\n * - `string`:marker 值(rush-ai 写的为 `<plugin>@<mkt>`;用户手写的可能没 marker → undefined → 转为 `''` 标识\"无主\")\n * - `undefined`:section 存在但无 marker(用户手写)\n */\nexport function getMcpServerOwner(\n config: CodexConfigToml,\n serverName: string\n): string | undefined | null {\n const servers = config.mcp_servers as Record<string, unknown> | undefined;\n if (!servers || typeof servers !== 'object') return null;\n const entry = servers[serverName] as Record<string, unknown> | undefined;\n if (!entry || typeof entry !== 'object') return null;\n const owner = entry[MCP_OWNER_MARKER_KEY];\n return typeof owner === 'string' ? owner : undefined;\n}\n\n/**\n * 判断 `[marketplaces.<name>]` 下是否至少还有一个 plugin 使用。\n *\n * Codex Installer uninstall 时用:若某 marketplace 已经没有任何 plugin 安装\n * (说明 rush-ai 不再需要这个 marketplace 注册表项)→ 才允许删该 marketplace section。\n *\n * 实现:扫所有 `[plugins.\"<name>@<mkt>\"]` key,找出后缀 `@<marketplace>` 匹配的条目。\n */\nexport function marketplaceHasOtherPlugins(\n config: CodexConfigToml,\n marketplaceName: string,\n excludedPluginKey: string\n): boolean {\n const plugins = config.plugins as Record<string, unknown> | undefined;\n if (!plugins || typeof plugins !== 'object') return false;\n const suffix = `@${marketplaceName}`;\n for (const key of Object.keys(plugins)) {\n if (key === excludedPluginKey) continue;\n if (key.endsWith(suffix)) return true;\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// Internals\n// ---------------------------------------------------------------------------\n\nasync 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\nasync function atomicWrite(filePath: string, content: string): Promise<void> {\n await mkdir(dirname(filePath), { recursive: true });\n const tmp = `${filePath}.${randomUUID()}.tmp`;\n try {\n await writeFile(tmp, content, { encoding: 'utf8', flag: 'w' });\n await rename(tmp, filePath);\n } catch (err) {\n await rm(tmp, { force: true }).catch(() => {});\n throw err;\n }\n}\n\nasync function assertNoMtimeDrift(\n filePath: string,\n expectedMtimeMs: number | null\n): Promise<void> {\n if (expectedMtimeMs === null) {\n // 我们读取时文件不存在;写入前再看一次,仍不存在才允许\"首次写\"路径\n if (await pathExists(filePath)) {\n throw new CodexConfigTomlConflictError(filePath);\n }\n return;\n }\n const current = await stat(filePath).catch(() => null);\n if (current === null) {\n // 读取时存在,现在消失 → 冲突\n throw new CodexConfigTomlConflictError(filePath);\n }\n if (current.mtimeMs !== expectedMtimeMs) {\n throw new CodexConfigTomlConflictError(filePath);\n }\n}\n\n/**\n * 确保 `config[key]` 是 object(不存在时初始化空 object),返回可 mutate 的引用。\n *\n * 若已存在但不是 object(例如用户手写成了数组或字符串)→ 抛 `CodexConfigTomlError`,\n * 提示不会破坏用户内容(对齐 spec §5.3 \"TOML 解析失败 → 报错\")。\n */\nfunction ensureObjectSection(\n config: CodexConfigToml,\n key: string\n): Record<string, unknown> {\n const existing = config[key];\n if (existing === undefined) {\n const fresh: Record<string, unknown> = {};\n config[key] = fresh;\n return fresh;\n }\n if (\n typeof existing !== 'object' ||\n existing === null ||\n Array.isArray(existing)\n ) {\n throw new CodexConfigTomlError(\n `config.toml 顶层 '${key}' 已存在但不是 object(type=${Array.isArray(existing) ? 'array' : typeof existing})。请手工备份并整理该字段后重试。`\n );\n }\n return existing as Record<string, unknown>;\n}\n"],"mappings":"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","names":["randomUUID","fsConstants","access","mkdir","readFile","rename","rm","stat","writeFile","dirname","rm","dirname","mkdir","pathExists","rename","readFile","randomUUID","writeFile","access","fsConstants","stat"]}
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/output/logger.ts
4
+ import chalk from "chalk";
5
+ var output = {
6
+ /** Content output → stdout. Use for data the user may pipe/redirect. */
7
+ log(message) {
8
+ console.log(message);
9
+ },
10
+ /** Status → stderr */
11
+ success(message) {
12
+ console.error(chalk.green("Success!"), message);
13
+ },
14
+ /** Error → stderr */
15
+ error(message) {
16
+ console.error(chalk.red("Error:"), message);
17
+ },
18
+ /** Warning → stderr */
19
+ warn(message) {
20
+ console.error(chalk.yellow("Warning:"), message);
21
+ },
22
+ /** Info → stderr */
23
+ info(message) {
24
+ console.error(chalk.cyan("Info:"), message);
25
+ },
26
+ /** Dim status → stderr */
27
+ dim(message) {
28
+ console.error(chalk.dim(message));
29
+ },
30
+ table(data) {
31
+ console.table(data);
32
+ },
33
+ /** Newline → stderr (status separator) */
34
+ newline() {
35
+ console.error();
36
+ },
37
+ link(text, url) {
38
+ return chalk.cyan.underline(url);
39
+ },
40
+ bold(text) {
41
+ return chalk.bold(text);
42
+ }
43
+ };
44
+
45
+ export {
46
+ output
47
+ };
48
+ //# sourceMappingURL=chunk-T5S6NCHZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/output/logger.ts"],"sourcesContent":["import chalk from 'chalk';\n\n/**\n * Output utilities.\n *\n * Design principle: stdout is for **content** (data the user wants to pipe/redirect).\n * Status messages (success, warn, info, dim) go to **stderr** so they don't pollute pipes.\n *\n * rush-ai task create --agent rush --prompt \"generate HTML\" --json > task.json\n * # stdout → task.json (task data)\n * # stderr → \"Success! Task xxx created.\" (status info, visible in terminal)\n */\nexport const output = {\n /** Content output → stdout. Use for data the user may pipe/redirect. */\n log(message: string): void {\n console.log(message);\n },\n\n /** Status → stderr */\n success(message: string): void {\n console.error(chalk.green('Success!'), message);\n },\n\n /** Error → stderr */\n error(message: string): void {\n console.error(chalk.red('Error:'), message);\n },\n\n /** Warning → stderr */\n warn(message: string): void {\n console.error(chalk.yellow('Warning:'), message);\n },\n\n /** Info → stderr */\n info(message: string): void {\n console.error(chalk.cyan('Info:'), message);\n },\n\n /** Dim status → stderr */\n dim(message: string): void {\n console.error(chalk.dim(message));\n },\n\n table(data: Record<string, string>[]): void {\n console.table(data);\n },\n\n /** Newline → stderr (status separator) */\n newline(): void {\n console.error();\n },\n\n link(text: string, url: string): string {\n return chalk.cyan.underline(url);\n },\n\n bold(text: string): string {\n return chalk.bold(text);\n },\n};\n"],"mappings":";;;AAAA,OAAO,WAAW;AAYX,IAAM,SAAS;AAAA;AAAA,EAEpB,IAAI,SAAuB;AACzB,YAAQ,IAAI,OAAO;AAAA,EACrB;AAAA;AAAA,EAGA,QAAQ,SAAuB;AAC7B,YAAQ,MAAM,MAAM,MAAM,UAAU,GAAG,OAAO;AAAA,EAChD;AAAA;AAAA,EAGA,MAAM,SAAuB;AAC3B,YAAQ,MAAM,MAAM,IAAI,QAAQ,GAAG,OAAO;AAAA,EAC5C;AAAA;AAAA,EAGA,KAAK,SAAuB;AAC1B,YAAQ,MAAM,MAAM,OAAO,UAAU,GAAG,OAAO;AAAA,EACjD;AAAA;AAAA,EAGA,KAAK,SAAuB;AAC1B,YAAQ,MAAM,MAAM,KAAK,OAAO,GAAG,OAAO;AAAA,EAC5C;AAAA;AAAA,EAGA,IAAI,SAAuB;AACzB,YAAQ,MAAM,MAAM,IAAI,OAAO,CAAC;AAAA,EAClC;AAAA,EAEA,MAAM,MAAsC;AAC1C,YAAQ,MAAM,IAAI;AAAA,EACpB;AAAA;AAAA,EAGA,UAAgB;AACd,YAAQ,MAAM;AAAA,EAChB;AAAA,EAEA,KAAK,MAAc,KAAqB;AACtC,WAAO,MAAM,KAAK,UAAU,GAAG;AAAA,EACjC;AAAA,EAEA,KAAK,MAAsB;AACzB,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AACF;","names":[]}
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/installers/claude-code/mcp.ts
4
+ import { execFileSync } from "child_process";
5
+ import { readFile } from "fs/promises";
6
+ import {
7
+ isAbsolute,
8
+ relative as pathRelative,
9
+ resolve as pathResolve
10
+ } from "path";
11
+ var RUSH_AI_PLUGIN_NAME = "rush";
12
+ var RUSH_AI_MARKETPLACE_NAME = "rush-marketplace";
13
+ var RUSH_MCP_SERVER_KEY = "rush";
14
+ function isRushOwnPlugin(ref) {
15
+ return ref.name === RUSH_AI_PLUGIN_NAME && ref.marketplace === RUSH_AI_MARKETPLACE_NAME;
16
+ }
17
+ function normalizeClaudeMcpServers(ref, manifest, resolver = defaultRushBinaryResolver) {
18
+ const servers = manifest.mcpServers;
19
+ if (servers === void 0 || servers === null) return void 0;
20
+ if (typeof servers === "string") {
21
+ return void 0;
22
+ }
23
+ if (!isRushOwnPlugin(ref)) {
24
+ return { ...servers };
25
+ }
26
+ const rushServer = servers[RUSH_MCP_SERVER_KEY];
27
+ if (!rushServer) {
28
+ return { ...servers };
29
+ }
30
+ const currentCommand = rushServer.command;
31
+ if (typeof currentCommand === "string" && isAbsolute(currentCommand)) {
32
+ return { ...servers };
33
+ }
34
+ const resolved = resolver();
35
+ if (!resolved || !isAbsolute(resolved)) {
36
+ return { ...servers };
37
+ }
38
+ return {
39
+ ...servers,
40
+ [RUSH_MCP_SERVER_KEY]: { ...rushServer, command: resolved }
41
+ };
42
+ }
43
+ async function readExternalMcpServers(sourceDir, relativeRef) {
44
+ if (typeof relativeRef !== "string" || relativeRef.length === 0) return null;
45
+ const srcPath = pathResolve(sourceDir, relativeRef);
46
+ const rel = pathRelative(pathResolve(sourceDir), srcPath);
47
+ if (rel === ".." || rel.startsWith("..") || rel.startsWith("/")) return null;
48
+ let raw;
49
+ try {
50
+ raw = await readFile(srcPath, "utf8");
51
+ } catch {
52
+ return null;
53
+ }
54
+ let parsed;
55
+ try {
56
+ parsed = JSON.parse(raw);
57
+ } catch {
58
+ return null;
59
+ }
60
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
61
+ return null;
62
+ }
63
+ const obj = parsed;
64
+ if (obj.mcpServers && typeof obj.mcpServers === "object" && !Array.isArray(obj.mcpServers)) {
65
+ return obj.mcpServers;
66
+ }
67
+ const flat = obj;
68
+ if (Object.keys(flat).length === 0) return null;
69
+ const allObjects = Object.values(flat).every(
70
+ (v) => v !== null && typeof v === "object" && !Array.isArray(v)
71
+ );
72
+ if (!allObjects) return null;
73
+ return flat;
74
+ }
75
+ function defaultRushBinaryResolver() {
76
+ try {
77
+ const result = execFileSync("which", ["rush-ai"], {
78
+ encoding: "utf8",
79
+ stdio: ["ignore", "pipe", "ignore"]
80
+ }).trim();
81
+ if (result.length > 0 && isAbsolute(result)) {
82
+ return result;
83
+ }
84
+ } catch {
85
+ }
86
+ return void 0;
87
+ }
88
+
89
+ export {
90
+ RUSH_AI_PLUGIN_NAME,
91
+ RUSH_AI_MARKETPLACE_NAME,
92
+ RUSH_MCP_SERVER_KEY,
93
+ isRushOwnPlugin,
94
+ normalizeClaudeMcpServers,
95
+ readExternalMcpServers,
96
+ defaultRushBinaryResolver
97
+ };
98
+ //# sourceMappingURL=chunk-X45FKY3L.js.map
@@ -0,0 +1 @@
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":[]}