UNPKG

plugins

Version:

Install open-plugin format plugins into agent tools

1,422 lines (1,416 loc) 82.8 kB
#!/usr/bin/env node // index.ts import { parseArgs } from "util"; import { resolve, join as join4 } from "path"; import { execSync as execSync3 } from "child_process"; import { existsSync as existsSync3, rmSync, mkdirSync } from "fs"; import { homedir as homedir3 } from "os"; import { createInterface } from "readline"; // lib/discover.ts import { join } from "path"; import { readFile, readdir, stat } from "fs/promises"; import { existsSync } from "fs"; async function discover(repoPath) { const marketplacePaths = [ join(repoPath, "marketplace.json"), join(repoPath, ".plugin", "marketplace.json"), join(repoPath, ".claude-plugin", "marketplace.json"), join(repoPath, ".cursor-plugin", "marketplace.json"), join(repoPath, ".codex-plugin", "marketplace.json") ]; for (const mp of marketplacePaths) { if (await fileExists(mp)) { const data = await readJson(mp); if (data && typeof data === "object" && "plugins" in data && Array.isArray(data.plugins)) { return discoverFromMarketplace(repoPath, data); } } } if (await isPluginDir(repoPath)) { const plugin = await inspectPlugin(repoPath); return { plugins: plugin ? [plugin] : [], remotePlugins: [], missingPaths: [] }; } const plugins = []; await scanForPlugins(repoPath, plugins, 2); return { plugins, remotePlugins: [], missingPaths: [] }; } async function scanForPlugins(dirPath, results, depth) { if (depth <= 0) return; const entries = await readDirSafe(dirPath); for (const entry of entries) { if (!entry.isDirectory() || entry.name.startsWith(".")) continue; const childPath = join(dirPath, entry.name); if (await isPluginDir(childPath)) { const plugin = await inspectPlugin(childPath); if (plugin) results.push(plugin); } else { await scanForPlugins(childPath, results, depth - 1); } } } async function discoverFromMarketplace(repoPath, marketplace) { const plugins = []; const remotePlugins = []; const missingPaths = []; const root = marketplace.metadata?.pluginRoot ?? "."; for (const entry of marketplace.plugins) { if (typeof entry.source !== "string") { remotePlugins.push({ name: entry.name, description: entry.description || void 0, source: entry.source }); continue; } const sourcePath = join(repoPath, root, entry.source.replace(/^\.\//, "")); if (!await dirExists(sourcePath)) { missingPaths.push(entry.source); continue; } let skills; if (entry.skills && Array.isArray(entry.skills)) { skills = []; for (const skillPath of entry.skills) { const resolvedPath = join(repoPath, root, skillPath.replace(/^\.\//, "")); const skillMd = join(resolvedPath, "SKILL.md"); if (await fileExists(skillMd)) { const content = await readFile(skillMd, "utf-8"); const fm = parseFrontmatter(content); skills.push({ name: fm.name ?? dirName(resolvedPath), description: fm.description ?? "" }); } } } else { skills = await discoverSkills(sourcePath); } let manifest = null; for (const manifestDir of [".plugin", ".claude-plugin", ".cursor-plugin", ".codex-plugin"]) { const manifestPath = join(sourcePath, manifestDir, "plugin.json"); if (await fileExists(manifestPath)) { manifest = await readJson(manifestPath); break; } } const [commands, agents, rules, hasHooks, hasMcp, hasLsp] = await Promise.all([ discoverCommands(sourcePath), discoverAgents(sourcePath), discoverRules(sourcePath), fileExists(join(sourcePath, "hooks", "hooks.json")), fileExists(join(sourcePath, ".mcp.json")), fileExists(join(sourcePath, ".lsp.json")) ]); const name = entry.name || manifest?.name || dirName(sourcePath); plugins.push({ name, version: entry.version || manifest?.version || void 0, description: entry.description || manifest?.description || void 0, path: sourcePath, marketplace: marketplace.name, skills, commands, agents, rules, hasHooks, hasMcp, hasLsp, manifest, explicitSkillPaths: entry.skills, marketplaceEntry: entry }); } return { plugins, remotePlugins, missingPaths }; } async function isPluginDir(dirPath) { const checks = [ join(dirPath, ".plugin", "plugin.json"), join(dirPath, ".claude-plugin", "plugin.json"), join(dirPath, ".cursor-plugin", "plugin.json"), join(dirPath, ".codex-plugin", "plugin.json"), join(dirPath, "skills"), join(dirPath, "commands"), join(dirPath, "agents"), join(dirPath, "SKILL.md") ]; for (const check of checks) { if (await pathExists(check)) return true; } return false; } async function inspectPlugin(pluginPath) { let manifest = null; for (const manifestDir of [".plugin", ".claude-plugin", ".cursor-plugin", ".codex-plugin"]) { const manifestPath = join(pluginPath, manifestDir, "plugin.json"); if (await fileExists(manifestPath)) { manifest = await readJson(manifestPath); break; } } const name = manifest?.name ?? dirName(pluginPath); const [skills, commands, agents, rules, hasHooks, hasMcp, hasLsp] = await Promise.all([ discoverSkills(pluginPath), discoverCommands(pluginPath), discoverAgents(pluginPath), discoverRules(pluginPath), fileExists(join(pluginPath, "hooks", "hooks.json")), fileExists(join(pluginPath, ".mcp.json")), fileExists(join(pluginPath, ".lsp.json")) ]); return { name, version: manifest?.version, description: manifest?.description, path: pluginPath, marketplace: void 0, skills, commands, agents, rules, hasHooks, hasMcp, hasLsp, manifest, explicitSkillPaths: void 0, marketplaceEntry: void 0 }; } async function discoverSkills(pluginPath) { const skillsDir = join(pluginPath, "skills"); const entries = await readDirSafe(skillsDir); const skills = []; for (const entry of entries) { if (!entry.isDirectory()) continue; const skillMd = join(skillsDir, entry.name, "SKILL.md"); if (await fileExists(skillMd)) { const content = await readFile(skillMd, "utf-8"); const fm = parseFrontmatter(content); skills.push({ name: fm.name ?? entry.name, description: fm.description ?? "" }); } } if (skills.length === 0) { const rootSkill = join(pluginPath, "SKILL.md"); if (await fileExists(rootSkill)) { const content = await readFile(rootSkill, "utf-8"); const fm = parseFrontmatter(content); skills.push({ name: fm.name ?? dirName(pluginPath), description: fm.description ?? "" }); } } return skills; } async function discoverCommands(pluginPath) { const commandsDir = join(pluginPath, "commands"); const entries = await readDirSafe(commandsDir); const commands = []; for (const entry of entries) { if (!entry.isFile() || !entry.name.match(/\.(md|mdc|markdown)$/)) continue; const filePath = join(commandsDir, entry.name); const content = await readFile(filePath, "utf-8"); const fm = parseFrontmatter(content); commands.push({ name: entry.name.replace(/\.(md|mdc|markdown)$/, ""), description: fm.description ?? "" }); } return commands; } async function discoverAgents(pluginPath) { const agentsDir = join(pluginPath, "agents"); const entries = await readDirSafe(agentsDir); const agents = []; for (const entry of entries) { if (!entry.isFile() || !entry.name.match(/\.(md|mdc|markdown)$/)) continue; const filePath = join(agentsDir, entry.name); const content = await readFile(filePath, "utf-8"); const fm = parseFrontmatter(content); if (fm.name && fm.description) { agents.push({ name: fm.name, description: fm.description }); } } return agents; } async function discoverRules(pluginPath) { const rulesDir = join(pluginPath, "rules"); const entries = await readDirSafe(rulesDir); const rules = []; for (const entry of entries) { if (!entry.isFile() || !entry.name.match(/\.(mdc|md|markdown)$/)) continue; const filePath = join(rulesDir, entry.name); const content = await readFile(filePath, "utf-8"); const fm = parseFrontmatter(content); rules.push({ name: entry.name.replace(/\.(mdc|md|markdown)$/, ""), description: fm.description ?? "" }); } return rules; } function parseFrontmatter(content) { const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/); if (!match?.[1]) return {}; const result = {}; for (const line of match[1].split("\n")) { const kv = line.match(/^(\w[\w-]*):\s*(.+)$/); if (kv) { let val = kv[2].trim(); if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) { val = val.slice(1, -1); } if (val === "true") { result[kv[1]] = true; } else if (val === "false") { result[kv[1]] = false; } else { result[kv[1]] = val; } } } return result; } function dirName(p) { const parts = p.replace(/\/$/, "").split("/"); return parts[parts.length - 1] ?? "unknown"; } async function fileExists(path) { try { const s = await stat(path); return s.isFile(); } catch { return false; } } async function dirExists(dirPath) { try { const s = await stat(dirPath); return s.isDirectory(); } catch { return false; } } async function pathExists(p) { return existsSync(p); } async function readJson(path) { try { const content = await readFile(path, "utf-8"); return JSON.parse(content); } catch { return null; } } async function readDirSafe(dirPath) { try { return await readdir(dirPath, { withFileTypes: true }); } catch { return []; } } // lib/targets.ts import { dirname, join as join2 } from "path"; import { homedir } from "os"; import { execFileSync, execSync } from "child_process"; var HOME = homedir(); function getVsCodeSettingsPath() { const product = detectBinary("code") || !detectBinary("code-insiders") ? "Code" : "Code - Insiders"; if (process.platform === "darwin") { return join2(HOME, "Library", "Application Support", product, "User", "settings.json"); } if (process.platform === "win32") { return join2( process.env.APPDATA ?? join2(HOME, "AppData", "Roaming"), product, "User", "settings.json" ); } return join2(HOME, ".config", product, "User", "settings.json"); } var TARGET_DEFS = [ { id: "claude-code", name: "Claude Code", description: "Anthropic's CLI coding agent", configPath: join2(HOME, ".claude") }, { id: "cursor", name: "Cursor", description: "AI-powered code editor", configPath: join2(HOME, ".cursor") }, { id: "codex", name: "Codex", description: "OpenAI's coding agent", configPath: join2(HOME, ".codex") }, { id: "grok", name: "Grok Build", description: "xAI's CLI coding agent", configPath: join2(HOME, ".grok") }, { id: "kimi", name: "Kimi Code", description: "Moonshot AI's CLI coding agent", configPath: process.env.KIMI_CODE_HOME ?? join2(HOME, ".kimi-code") }, { id: "github-copilot", name: "GitHub Copilot CLI", description: "GitHub Copilot\u2019s standalone coding agent", configPath: join2(HOME, ".copilot") }, { id: "vscode", name: "Visual Studio Code", description: "VS Code agent plugins (Preview)", configPath: dirname(getVsCodeSettingsPath()) } ]; async function getTargets() { const targets = []; for (const def of TARGET_DEFS) { const detected = detectTarget(def); targets.push({ ...def, detected }); } return targets; } function detectTarget(def) { switch (def.id) { case "claude-code": return detectBinary("claude"); case "cursor": return detectBinary("cursor"); case "codex": return detectBinary("codex"); case "grok": return detectBinary("grok"); case "kimi": return getKimiBinary() !== null; case "github-copilot": return detectBinary("copilot"); case "vscode": return detectBinary("code") || detectBinary("code-insiders"); default: return false; } } function getKimiBinary() { try { const path = execSync("which kimi", { encoding: "utf-8", stdio: "pipe" }).trim(); if (path) return path; } catch { } const kimiHome = process.env.KIMI_CODE_HOME ?? join2(HOME, ".kimi-code"); const extension = process.platform === "win32" ? ".exe" : ""; const candidates = [ join2(kimiHome, "bin", `kimi${extension}`), join2(HOME, ".local", "bin", `kimi${extension}`), join2(HOME, ".kimi", "bin", `kimi${extension}`) ]; for (const candidate of candidates) { try { execFileSync(candidate, ["--version"], { stdio: "pipe", timeout: 1e4 }); return candidate; } catch { } } return null; } function detectBinary(name) { try { execSync(`which ${name}`, { stdio: "pipe" }); return true; } catch { return false; } } // lib/install.ts import { dirname as dirname2, join as join3, relative } from "path"; import { mkdir, cp, readFile as readFile2, readdir as readdir2, rename, writeFile, rm } from "fs/promises"; import { existsSync as existsSync2 } from "fs"; import { execFileSync as execFileSync2, execSync as execSync2 } from "child_process"; import { homedir as homedir2 } from "os"; import { createHash } from "crypto"; // lib/ui.ts var isColorSupported = process.env.FORCE_COLOR !== "0" && !process.env.NO_COLOR && (process.env.FORCE_COLOR !== void 0 || process.stdout.isTTY); function ansi(code) { return isColorSupported ? `\x1B[${code}m` : ""; } var reset = ansi("0"); var bold = ansi("1"); var dim = ansi("2"); var italic = ansi("3"); var underline = ansi("4"); var red = ansi("31"); var green = ansi("32"); var yellow = ansi("33"); var blue = ansi("34"); var magenta = ansi("35"); var cyan = ansi("36"); var gray = ansi("90"); var bgGreen = ansi("42"); var bgRed = ansi("41"); var bgYellow = ansi("43"); var bgCyan = ansi("46"); var black = ansi("30"); var c = { bold: (s) => `${bold}${s}${reset}`, dim: (s) => `${dim}${s}${reset}`, italic: (s) => `${italic}${s}${reset}`, underline: (s) => `${underline}${s}${reset}`, red: (s) => `${red}${s}${reset}`, green: (s) => `${green}${s}${reset}`, yellow: (s) => `${yellow}${s}${reset}`, blue: (s) => `${blue}${s}${reset}`, magenta: (s) => `${magenta}${s}${reset}`, cyan: (s) => `${cyan}${s}${reset}`, gray: (s) => `${gray}${s}${reset}`, bgGreen: (s) => `${bgGreen}${black}${s}${reset}`, bgRed: (s) => `${bgRed}${black}${s}${reset}`, bgYellow: (s) => `${bgYellow}${black}${s}${reset}`, bgCyan: (s) => `${bgCyan}${black}${s}${reset}` }; var S = { // Box drawing bar: "\u2502", barEnd: "\u2514", barStart: "\u250C", barH: "\u2500", corner: "\u256E", // Bullets diamond: "\u25C7", diamondFilled: "\u25C6", bullet: "\u25CF", circle: "\u25CB", check: "\u2714", cross: "\u2716", arrow: "\u2192", warning: "\u25B2", info: "\u2139", step: "\u25C7", stepActive: "\u25C6", stepComplete: "\u25CF", stepError: "\u25A0" }; function barLine(content = "") { console.log(`${c.gray(S.bar)} ${content}`); } function barEmpty() { console.log(`${c.gray(S.bar)}`); } var _debug = false; function setDebug(enabled) { _debug = enabled; } function barDebug(content = "") { if (_debug) barLine(content); } function step(content) { console.log(`${c.gray(S.step)} ${content}`); } function stepDone(content) { console.log(`${c.green(S.stepComplete)} ${content}`); } function stepError(content) { console.log(`${c.red(S.stepError)} ${content}`); } function header(label) { console.log(); console.log(`${c.gray(S.barStart)} ${c.bgCyan(` ${label} `)}`); } function footer(message) { if (message) { console.log(`${c.gray(S.barEnd)} ${message}`); } else { console.log(`${c.gray(S.barEnd)}`); } } function error(title, details) { console.log(`${c.red(S.stepError)} ${c.red(c.bold(title))}`); if (details) { for (const line of details) { barLine(c.dim(line)); } } } function warn(message) { barLine(`${c.yellow(S.warning)} ${c.yellow(message)}`); } async function multiSelect(title, options, maxVisible = 8) { if (!process.stdin.isTTY) { return options.map((o) => o.value); } const { createInterface: createInterface2, emitKeypressEvents } = await import("readline"); const { Writable } = await import("stream"); const silentOutput = new Writable({ write(_chunk, _encoding, callback) { callback(); } }); return new Promise((resolve2) => { const rl = createInterface2({ input: process.stdin, output: silentOutput, terminal: false }); if (process.stdin.isTTY) { process.stdin.setRawMode(true); } emitKeypressEvents(process.stdin, rl); let query = ""; let cursor = 0; const selected = new Set(options.map((o) => o.value)); let lastRenderHeight = 0; const filter = (item, q) => { if (!q) return true; const lq = q.toLowerCase(); return item.label.toLowerCase().includes(lq) || (item.hint?.toLowerCase().includes(lq) ?? false); }; const getFiltered = () => options.filter((item) => filter(item, query)); const clearRender = () => { if (lastRenderHeight > 0) { process.stdout.write(`\x1B[${lastRenderHeight}A`); for (let i = 0; i < lastRenderHeight; i++) { process.stdout.write("\x1B[2K\x1B[1B"); } process.stdout.write(`\x1B[${lastRenderHeight}A`); } }; const render = (state = "active") => { clearRender(); const lines = []; const filtered = getFiltered(); const icon = state === "active" ? c.cyan(S.stepActive) : state === "cancel" ? c.red(S.stepError) : c.green(S.stepComplete); lines.push(`${icon} ${state === "active" ? title : c.dim(title)}`); if (state === "active") { const blockCursor = isColorSupported ? `\x1B[7m \x1B[0m` : "_"; lines.push(`${c.gray(S.bar)} ${c.dim("Search:")} ${query}${blockCursor}`); lines.push( `${c.gray(S.bar)} ${c.dim("\u2191\u2193 move, space toggle, a all, n none, enter confirm")}` ); lines.push(`${c.gray(S.bar)}`); const visibleStart = Math.max( 0, Math.min(cursor - Math.floor(maxVisible / 2), filtered.length - maxVisible) ); const visibleEnd = Math.min(filtered.length, visibleStart + maxVisible); const visibleItems = filtered.slice(visibleStart, visibleEnd); if (filtered.length === 0) { lines.push(`${c.gray(S.bar)} ${c.dim("No matches found")}`); } else { for (let i = 0; i < visibleItems.length; i++) { const item = visibleItems[i]; const actualIndex = visibleStart + i; const isSelected = selected.has(item.value); const isCursor = actualIndex === cursor; const radio = isSelected ? c.green(S.stepComplete) : c.dim(S.circle); const label = isCursor ? c.underline(item.label) : item.label; const hint = item.hint ? c.dim(` (${item.hint})`) : ""; const pointer = isCursor ? c.cyan("\u276F") : " "; lines.push(`${c.gray(S.bar)} ${pointer} ${radio} ${label}${hint}`); } const hiddenBefore = visibleStart; const hiddenAfter = filtered.length - visibleEnd; if (hiddenBefore > 0 || hiddenAfter > 0) { const parts = []; if (hiddenBefore > 0) parts.push(`\u2191 ${hiddenBefore} more`); if (hiddenAfter > 0) parts.push(`\u2193 ${hiddenAfter} more`); lines.push(`${c.gray(S.bar)} ${c.dim(parts.join(" "))}`); } } lines.push(`${c.gray(S.bar)}`); const selectedLabels = options.filter((o) => selected.has(o.value)).map((o) => o.label); if (selectedLabels.length === 0) { lines.push(`${c.gray(S.bar)} ${c.dim("Selected: (none)")}`); } else { const summary = selectedLabels.length <= 3 ? selectedLabels.join(", ") : `${selectedLabels.slice(0, 3).join(", ")} +${selectedLabels.length - 3} more`; lines.push(`${c.gray(S.bar)} ${c.green("Selected:")} ${summary}`); } lines.push(c.gray(S.barEnd)); } else if (state === "submit") { const selectedLabels = options.filter((o) => selected.has(o.value)).map((o) => o.label); lines.push(`${c.gray(S.bar)} ${c.dim(selectedLabels.join(", "))}`); } else if (state === "cancel") { lines.push(`${c.gray(S.bar)} ${c.dim("Cancelled")}`); } process.stdout.write(lines.join("\n") + "\n"); lastRenderHeight = lines.length; }; const cleanup = () => { process.stdin.removeListener("keypress", onKeypress); if (process.stdin.isTTY) { process.stdin.setRawMode(false); } rl.close(); }; const onKeypress = (_str, key) => { if (!key) return; const filtered = getFiltered(); if (key.name === "return") { render("submit"); cleanup(); resolve2([...selected]); return; } if (key.name === "escape" || key.ctrl && key.name === "c") { render("cancel"); cleanup(); resolve2(null); return; } if (key.name === "up") { cursor = Math.max(0, cursor - 1); render(); return; } if (key.name === "down") { cursor = Math.min(filtered.length - 1, cursor + 1); render(); return; } if (key.name === "space") { const item = filtered[cursor]; if (item) { if (selected.has(item.value)) selected.delete(item.value); else selected.add(item.value); } render(); return; } if (key.name === "backspace") { query = query.slice(0, -1); cursor = 0; render(); return; } if (key.sequence && !key.ctrl && !key.meta && key.sequence.length === 1) { if (key.sequence === "a" && query === "") { for (const o of options) selected.add(o.value); render(); return; } if (key.sequence === "n" && query === "") { selected.clear(); render(); return; } query += key.sequence; cursor = 0; render(); return; } }; process.stdin.on("keypress", onKeypress); render(); }); } var BANNER_LINES = [ "\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557", "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D", "\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557", "\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551", "\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551", "\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D" ]; var GRADIENT = [ [60, 60, 60], [90, 90, 90], [125, 125, 125], [160, 160, 160], [200, 200, 200], [240, 240, 240] ]; function rgb(r, g, b) { return isColorSupported ? `\x1B[38;2;${r};${g};${b}m` : ""; } function banner() { console.log(); for (let i = 0; i < BANNER_LINES.length; i++) { const [r, g, b] = GRADIENT[i]; console.log(`${rgb(r, g, b)}${BANNER_LINES[i]}${reset}`); } } // lib/install.ts var cachePopulated = false; async function installPlugins(plugins, target, scope, repoPath, source) { try { await installPluginsUnsafe(plugins, target, scope, repoPath, source); return { succeeded: true }; } catch (err) { return { succeeded: false, message: err instanceof Error ? err.message : String(err) }; } } async function installPluginsUnsafe(plugins, target, scope, repoPath, source) { if (scope !== "user" && ["grok", "kimi", "github-copilot", "vscode"].includes(target.id)) { warn(`${target.name} installs plugins per-user; ignoring the requested ${scope} scope.`); } switch (target.id) { case "claude-code": { const officialRef = getOfficialPluginRef(source); if (officialRef) { const ok = await installViaClaudeCli(officialRef, scope); if (ok) { cachePopulated = true; break; } barDebug(c.dim("Falling back to direct file-based install")); } const workspace = await stageInstallWorkspace(plugins, repoPath, target.id); await installToClaudeCode(workspace.plugins, scope, workspace.repoPath, source); break; } case "cursor": { if (cachePopulated) return; const workspace = await stageInstallWorkspace(plugins, repoPath, target.id); await installToCursor(workspace.plugins, scope, workspace.repoPath, source); break; } case "codex": { const officialRef = getOfficialCodexPluginRef(source); if (officialRef) { installViaCodexCli(officialRef); break; } const workspace = await stageInstallWorkspace(plugins, repoPath, target.id); await installToCodex(workspace.plugins, scope, workspace.repoPath, source); break; } case "grok": { const nativeSource = getGrokNativeSource(source, plugins, repoPath); if (nativeSource) { await installToGrok(plugins, nativeSource); break; } const workspace = await stageInstallWorkspace(plugins, repoPath, target.id); await installToGrok(workspace.plugins); break; } case "kimi": { const workspace = await stageInstallWorkspace(plugins, repoPath, target.id); await installToKimi(workspace.plugins); break; } case "github-copilot": { await installToGitHubCopilot(plugins, repoPath, source); break; } case "vscode": { const workspace = await stageInstallWorkspace(plugins, repoPath, target.id); await installToVsCode(workspace.plugins); break; } default: throw new Error(`Unsupported target: ${target.id}`); } } async function stageInstallWorkspace(plugins, repoPath, targetId, stagingBaseDir = join3(homedir2(), ".cache", "plugins", ".install-staging")) { const stageKey = createHash("sha1").update(repoPath).digest("hex"); const stageRoot = join3(stagingBaseDir, stageKey, targetId); const stagedRepoPath = join3(stageRoot, "repo"); await mkdir(stageRoot, { recursive: true }); await rm(stagedRepoPath, { recursive: true, force: true }); await cp(repoPath, stagedRepoPath, { recursive: true }); const stagedPlugins = plugins.map((plugin) => { const relPath = relative(repoPath, plugin.path); return { ...plugin, path: relPath === "" ? stagedRepoPath : join3(stagedRepoPath, relPath) }; }); return { repoPath: stagedRepoPath, plugins: stagedPlugins }; } var OFFICIAL_MARKETPLACE_SOURCE = "anthropics/claude-plugins-official"; function getGitHubSourceRepo(source) { let repo = null; const shorthand = source.match(/^([\w.-]+\/[\w.-]+?)(?:\.git)?$/); if (shorthand) repo = shorthand[1].toLowerCase(); if (!repo) { const https = source.match(/^https?:\/\/github\.com\/([\w.-]+\/[\w.-]+?)(?:\.git)?$/); if (https) repo = https[1].toLowerCase(); } if (!repo) { const ssh = source.match(/^git@github\.com:([\w.-]+\/[\w.-]+?)(?:\.git)?$/); if (ssh) repo = ssh[1].toLowerCase(); } return repo; } function getOfficialPluginRef(source) { const repo = getGitHubSourceRepo(source); if (repo === "vercel/vercel-plugin") { return "vercel@claude-plugins-official"; } return null; } function getOfficialCodexPluginRef(source) { return getGitHubSourceRepo(source) === "vercel/vercel-plugin" ? "vercel@openai-curated" : null; } async function installViaClaudeCli(pluginRef, scope) { const claude = findClaudeOrNull(); if (!claude) return false; try { step("Registering official Claude marketplace"); execSync2(`${claude} plugin marketplace add ${OFFICIAL_MARKETPLACE_SOURCE}`, { stdio: "pipe", timeout: 12e4 }); stepDone("Official marketplace registered"); step(`Installing ${c.cyan(pluginRef)} via Claude CLI`); execSync2(`${claude} plugin install "${pluginRef}" --scope ${scope}`, { stdio: "pipe", timeout: 12e4 }); stepDone(`Installed ${c.cyan(pluginRef)} via Claude CLI`); return true; } catch (err) { const msg = err instanceof Error ? err.message : String(err); barDebug(c.dim(`Claude CLI install failed: ${msg}`)); return false; } } function installViaCodexCli(pluginRef) { step(`Installing ${c.cyan(pluginRef)} via Codex marketplace`); runNativeCommand("Codex", "codex", ["plugin", "add", pluginRef]); stepDone(`Installed ${c.cyan(pluginRef)} via Codex marketplace`); } async function installToClaudeCode(plugins, scope, repoPath, source) { await installToPluginCache(plugins, scope, repoPath, source); } async function installToCursor(plugins, scope, repoPath, source) { if (cachePopulated) return; if (process.platform === "win32") { await installToCursorExtensions(plugins, scope, repoPath, source); return; } await installToPluginCache(plugins, scope, repoPath, source); } async function installToPluginCache(plugins, scope, repoPath, source) { const marketplaceName = plugins[0]?.marketplace ?? deriveMarketplaceName(source); const home = homedir2(); const pluginsDir = join3(home, ".claude", "plugins"); const cacheDir = join3(pluginsDir, "cache"); step("Preparing plugins for Cursor..."); barEmpty(); await prepareForClaudeCode(plugins, repoPath, marketplaceName); step("Registering marketplace"); await mkdir(pluginsDir, { recursive: true }); const knownPath = join3(pluginsDir, "known_marketplaces.json"); let knownMarketplaces = {}; if (existsSync2(knownPath)) { try { knownMarketplaces = JSON.parse(await readFile2(knownPath, "utf-8")); } catch { } } const githubRepo = extractGitHubRepo(source); const marketplacesDir = join3(pluginsDir, "marketplaces"); const marketplaceInstallLocation = join3(marketplacesDir, marketplaceName); await mkdir(marketplacesDir, { recursive: true }); if (existsSync2(marketplaceInstallLocation)) { await rm(marketplaceInstallLocation, { recursive: true }); } await cp(repoPath, marketplaceInstallLocation, { recursive: true }); barDebug(c.dim(`Marketplace copied to ${marketplaceInstallLocation}`)); if (knownMarketplaces[marketplaceName]) { stepDone(`Marketplace ${c.dim("'" + marketplaceName + "'")} already registered`); } else { let marketplaceSource; if (githubRepo) { marketplaceSource = { source: "github", repo: githubRepo }; } else if (isRemoteSource(source)) { const gitUrl = normalizeGitUrl(source); marketplaceSource = { source: "git", url: gitUrl.endsWith(".git") ? gitUrl : gitUrl + ".git" }; } else { marketplaceSource = { source: "directory", path: repoPath }; } knownMarketplaces[marketplaceName] = { source: marketplaceSource, installLocation: marketplaceInstallLocation, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }; await writeFile(knownPath, JSON.stringify(knownMarketplaces, null, 2)); stepDone("Marketplace registered"); } barEmpty(); const installedPath = join3(pluginsDir, "installed_plugins.json"); let installedData = { version: 2, plugins: {} }; if (existsSync2(installedPath)) { try { const parsed = JSON.parse(await readFile2(installedPath, "utf-8")); installedData.version = parsed.version ?? 2; installedData.plugins = parsed.plugins ?? {}; } catch { } } let gitSha; try { gitSha = execSync2("git rev-parse HEAD", { cwd: repoPath, encoding: "utf-8", stdio: "pipe" }).trim(); } catch { } for (const plugin of plugins) { const pluginRef = `${plugin.name}@${marketplaceName}`; const version = plugin.version ?? "0.0.0"; const versionKey = gitSha ? gitSha.slice(0, 12) : version; step(`Installing ${c.bold(pluginRef)}...`); const cacheDest = join3(cacheDir, marketplaceName, plugin.name, versionKey); await mkdir(cacheDest, { recursive: true }); await cp(plugin.path, cacheDest, { recursive: true }); barDebug(c.dim(`Cached to ${cacheDest}`)); const pluginKey = `${plugin.name}@${marketplaceName}`; const now = (/* @__PURE__ */ new Date()).toISOString(); const entry = { scope, installPath: cacheDest, version, installedAt: now, lastUpdated: now }; if (gitSha) entry.gitCommitSha = gitSha; installedData.plugins[pluginKey] = [entry]; stepDone(`Installed ${c.cyan(pluginRef)}`); } await writeFile(installedPath, JSON.stringify(installedData, null, 2)); barDebug(c.dim("Updated installed_plugins.json")); const settingsPath = join3(home, ".claude", "settings.json"); let settings = {}; let settingsCorrupted = false; if (existsSync2(settingsPath)) { try { settings = JSON.parse(await readFile2(settingsPath, "utf-8")); } catch { settingsCorrupted = true; } } if (settingsCorrupted) { warn( "Could not parse ~/.claude/settings.json \u2014 skipping enabledPlugins update to avoid overwriting existing settings." ); barLine(c.dim("You may need to manually enable the plugins in Claude Code settings.")); } else { const enabled = settings.enabledPlugins ?? {}; for (const plugin of plugins) { const pluginKey = `${plugin.name}@${marketplaceName}`; enabled[pluginKey] = true; } settings.enabledPlugins = enabled; await writeFile(settingsPath, JSON.stringify(settings, null, 2)); barDebug(c.dim("Updated settings.json enabledPlugins")); } cachePopulated = true; } async function installToCursorExtensions(plugins, scope, repoPath, source) { const marketplaceName = plugins[0]?.marketplace ?? deriveMarketplaceName(source); const home = homedir2(); const extensionsDir = join3(home, ".cursor", "extensions"); step("Preparing plugins for Cursor..."); barEmpty(); await prepareForClaudeCode(plugins, repoPath, marketplaceName); await mkdir(extensionsDir, { recursive: true }); const extensionsJsonPath = join3(extensionsDir, "extensions.json"); let extensions = []; if (existsSync2(extensionsJsonPath)) { try { const parsed = JSON.parse(await readFile2(extensionsJsonPath, "utf-8")); if (Array.isArray(parsed)) extensions = parsed; } catch { } } let gitSha; try { gitSha = execSync2("git rev-parse HEAD", { cwd: repoPath, encoding: "utf-8", stdio: "pipe" }).trim(); } catch { } for (const plugin of plugins) { const pluginRef = `${plugin.name}@${marketplaceName}`; const version = plugin.version ?? "0.0.0"; const versionKey = gitSha ? gitSha.slice(0, 12) : version; const folderName = `${marketplaceName}.${plugin.name}-${versionKey}`; const destDir = join3(extensionsDir, folderName); step(`Installing ${c.bold(pluginRef)}...`); await mkdir(destDir, { recursive: true }); await cp(plugin.path, destDir, { recursive: true }); barDebug(c.dim(`Copied to ${destDir}`)); const identifier = `${marketplaceName}.${plugin.name}`; extensions = extensions.filter((e) => e?.identifier?.id !== identifier); const uriPath = "/" + destDir.replace(/\\/g, "/"); extensions.push({ identifier: { id: identifier }, version, location: { $mid: 1, path: uriPath, scheme: "file" }, relativeLocation: folderName, metadata: { installedTimestamp: Date.now(), ...gitSha ? { gitCommitSha: gitSha } : {} } }); stepDone(`Installed ${c.cyan(pluginRef)}`); } await writeFile(extensionsJsonPath, JSON.stringify(extensions, null, 2)); barDebug(c.dim("Updated extensions.json")); cachePopulated = true; } async function installToCodex(plugins, scope, repoPath, source) { const marketplaceName = plugins[0]?.marketplace ?? deriveMarketplaceName(source); const home = homedir2(); const cacheDir = join3(home, ".codex", "plugins", "cache"); const configPath = join3(home, ".codex", "config.toml"); const marketplaceDir = join3(home, ".agents", "plugins"); const marketplacePath = join3(marketplaceDir, "marketplace.json"); const marketplaceRoot = home; step("Preparing plugins for Codex..."); barEmpty(); for (const plugin of plugins) { await preparePluginDirForVendor(plugin, ".codex-plugin", "CODEX_PLUGIN_ROOT"); await enrichForCodex(plugin); } let gitSha; try { gitSha = execSync2("git rev-parse HEAD", { cwd: repoPath, encoding: "utf-8", stdio: "pipe" }).trim(); } catch { } const versionKey = gitSha ?? "local"; const pluginPaths = {}; for (const plugin of plugins) { const pluginRef = `${plugin.name}@${marketplaceName}`; step(`Installing ${c.bold(pluginRef)}...`); const cacheDest = join3(cacheDir, marketplaceName, plugin.name, versionKey); await mkdir(cacheDest, { recursive: true }); await cp(plugin.path, cacheDest, { recursive: true }); pluginPaths[plugin.name] = cacheDest; barDebug(c.dim(`Cached to ${cacheDest}`)); stepDone(`Installed ${c.cyan(pluginRef)}`); } step("Updating marketplace..."); await mkdir(marketplaceDir, { recursive: true }); let marketplace = { name: "plugins-cli", interface: { displayName: "Plugins CLI" }, plugins: [] }; if (existsSync2(marketplacePath)) { try { const existing = JSON.parse(await readFile2(marketplacePath, "utf-8")); if (existing && typeof existing === "object" && Array.isArray(existing.plugins)) { marketplace = existing; } } catch { } } for (const plugin of plugins) { const cacheDest = pluginPaths[plugin.name]; const relPath = relative(marketplaceRoot, cacheDest); marketplace.plugins = marketplace.plugins.filter( (e) => e.name !== plugin.name ); marketplace.plugins.push({ name: plugin.name, source: { source: "local", path: `./${relPath}` }, policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" }, category: "Coding" }); } await writeFile(marketplacePath, JSON.stringify(marketplace, null, 2)); stepDone("Marketplace updated"); step("Updating config.toml..."); await mkdir(join3(home, ".codex"), { recursive: true }); let configContent = ""; if (existsSync2(configPath)) { configContent = await readFile2(configPath, "utf-8"); } let configChanged = false; for (const plugin of plugins) { const pluginKey = `${plugin.name}@plugins-cli`; const tomlSection = `[plugins."${pluginKey}"]`; if (configContent.includes(tomlSection)) { barDebug(c.dim(`${pluginKey} already in config.toml`)); continue; } const entry = ` ${tomlSection} enabled = true `; configContent += entry; configChanged = true; barDebug(c.dim(`Added ${pluginKey} to config.toml`)); } if (configChanged) { await writeFile(configPath, configContent); } stepDone("Config updated"); } async function enrichForCodex(plugin) { const codexManifestPath = join3(plugin.path, ".codex-plugin", "plugin.json"); if (!existsSync2(codexManifestPath)) return; let manifest; try { manifest = JSON.parse(await readFile2(codexManifestPath, "utf-8")); } catch { return; } if (manifest.interface) return; let changed = false; if (!manifest.skills && existsSync2(join3(plugin.path, "skills"))) { manifest.skills = "./skills/"; changed = true; } if (!manifest.mcpServers && existsSync2(join3(plugin.path, ".mcp.json"))) { manifest.mcpServers = "./.mcp.json"; changed = true; } if (!manifest.apps && existsSync2(join3(plugin.path, ".app.json"))) { manifest.apps = "./.app.json"; changed = true; } const name = manifest.name ?? plugin.name; const description = manifest.description ?? plugin.description ?? ""; const author = manifest.author; const iface = { displayName: name.charAt(0).toUpperCase() + name.slice(1), shortDescription: description, developerName: author?.name ?? "Unknown", category: "Coding", capabilities: ["Interactive", "Write"] }; if (manifest.homepage) iface.websiteURL = manifest.homepage; else if (manifest.repository) iface.websiteURL = manifest.repository; const assetCandidates = [ "assets/app-icon.png", "assets/icon.png", "assets/logo.png", "assets/logo.svg" ]; for (const candidate of assetCandidates) { if (existsSync2(join3(plugin.path, candidate))) { iface.logo = `./${candidate}`; iface.composerIcon = `./${candidate}`; break; } } manifest.interface = iface; changed = true; if (changed) { await writeFile(codexManifestPath, JSON.stringify(manifest, null, 2)); barDebug(c.dim(`${plugin.name}: enriched .codex-plugin/plugin.json for Codex`)); } } async function installToGrok(plugins, source) { step("Preparing plugins for Grok Build..."); barEmpty(); if (source) { for (const plugin of plugins) { step(`Installing ${c.bold(plugin.name)} from source...`); installGrokPlugin(plugin.name, source); } return; } for (const plugin of plugins) { await preparePluginDirForVendor(plugin, ".claude-plugin", "CLAUDE_PLUGIN_ROOT"); } for (const plugin of plugins) { step(`Installing ${c.bold(plugin.name)}...`); installGrokPlugin(plugin.name, plugin.path); } } function installGrokPlugin(pluginName, source) { try { runNativeCommand("Grok Build", "grok", ["plugin", "install", source, "--trust"]); stepDone(`Installed ${c.cyan(pluginName)}`); } catch (err) { if (!isAlreadyInstalledError(err)) throw err; stepDone(`${c.cyan(pluginName)} is already installed`); } } function isAlreadyInstalledError(err) { const message = err instanceof Error ? err.message : String(err); return /\balready installed\b/i.test(message); } function getGrokNativeSource(source, plugins, repoPath) { if (plugins.length !== 1 || plugins[0]?.path !== repoPath) return null; if (!existsSync2(join3(repoPath, ".claude-plugin", "plugin.json"))) return null; const isGitSource = /^https?:\/\//.test(source) || /^git@/.test(source) || /^[\w.-]+\/[\w.-]+(?:\.git)?$/.test(source); return isGitSource ? source : null; } async function installToKimi(plugins) { step("Preparing plugins for Kimi Code..."); barEmpty(); for (const plugin of plugins) { await preparePluginDirForVendor(plugin, ".kimi-plugin", "KIMI_PLUGIN_ROOT"); await enrichForKimi(plugin); } await installToKimiNativeStore(plugins); } async function installToKimiNativeStore(plugins) { const kimiHome = process.env.KIMI_CODE_HOME ?? join3(homedir2(), ".kimi-code"); const pluginsDir = join3(kimiHome, "plugins"); const installedPath = join3(pluginsDir, "installed.json"); const installed = await readKimiInstalledFile(installedPath); for (const plugin of plugins) { const managedRoot = await copyToKimiManagedRoot(kimiHome, plugin.name, plugin.path); const existing = installed.plugins.find((entry) => entry.id === plugin.name); const now = (/* @__PURE__ */ new Date()).toISOString(); const record = { id: plugin.name, root: managedRoot, source: "local-path", enabled: existing?.enabled ?? true, installedAt: existing?.installedAt ?? now, updatedAt: now, originalSource: plugin.path, ...existing?.capabilities ? { capabilities: existing.capabilities } : {}, ...existing?.github ? { github: existing.github } : {} }; installed.plugins = installed.plugins.filter((entry) => entry.id !== plugin.name); installed.plugins.push(record); stepDone(`Installed ${c.cyan(plugin.name)}`); if (plugin.agents.length > 0 || plugin.hasLsp) { warn(`${plugin.name}: Kimi plugins do not currently support agents or LSP servers.`); } } await mkdir(pluginsDir, { recursive: true }); const temporaryInstalledPath = `${installedPath}.tmp`; await writeFile(temporaryInstalledPath, JSON.stringify(installed, null, 2)); await rename(temporaryInstalledPath, installedPath); await removeKimiCompatibilityLayer(plugins, kimiHome); barDebug(c.dim(`Updated ${installedPath}`)); } async function copyToKimiManagedRoot(kimiHome, pluginId, sourceRoot) { const managedRoot = join3(kimiHome, "plugins", "managed", pluginId); const managedDir = dirname2(managedRoot); await mkdir(managedDir, { recursive: true }); const stagingRoot = join3(managedDir, `${pluginId}-${Date.now()}-${process.pid}`); await rm(stagingRoot, { recursive: true, force: true }); await cp(sourceRoot, stagingRoot, { recursive: true }); await rm(managedRoot, { recursive: true, force: true }); await rename(stagingRoot, managedRoot); return managedRoot; } async function readKimiInstalledFile(path) { if (!existsSync2(path)) return { version: 1, plugins: [] }; const parsed = await readJsonObject(path); if (!parsed || !Array.isArray(parsed.plugins)) { throw new Error(`Could not parse ${path}; Kimi plugin records were left unchanged.`); } return { version: 1, plugins: parsed.plugins }; } async function removeKimiCompatibilityLayer(plugins, kimiHome) { const prefixes = plugins.map((plugin) => `${plugin.name}-`); const skillsDir = join3(kimiHome, "skills"); try { const entries = await readdir2(skillsDir, { withFileTypes: true }); for (const entry of entries) { if (prefixes.some((prefix) => entry.name.startsWith(prefix))) { await rm(join3(skillsDir, entry.name), { recursive: true, force: true }); } } } catch { } const mcpPath = join3(kimiHome, "mcp.json"); const mcpConfig = await readJsonObject(mcpPath); if (!mcpConfig?.mcpServers || typeof mcpConfig.mcpServers !== "object") return; const servers = mcpConfig.mcpServers; const retained = Object.fromEntries( Object.entries(servers).filter(([name]) => !prefixes.some((prefix) => name.startsWith(prefix))) ); if (Object.keys(retained).length === Object.keys(servers).length) return; mcpConfig.mcpServers = retained; await writeFile(mcpPath, JSON.stringify(mcpConfig, null, 2)); } async function enrichForKimi(plugin) { const manifestPath = join3(plugin.path, ".kimi-plugin", "plugin.json"); const manifest = await readJsonObject(manifestPath); if (!manifest) return; let changed = false; if (!manifest.skills && existsSync2(join3(plugin.path, "skills"))) { manifest.skills = "./skills/"; changed = true; } if (!manifest.commands && existsSync2(join3(plugin.path, "commands"))) { manifest.commands = "./commands/"; changed = true; } const mcpConfig = await readJsonObject(join3(plugin.path, ".mcp.json")); if (mcpConfig?.mcpServers && typeof mcpConfig.mcpServers === "object") { if (manifest.mcpServers !== mcpConfig.mcpServers) { manifest.mcpServers = mcpConfig.mcpServers; changed = true; } } const kimiHooks = await readKimiHooks(plugin.path, manifest.hooks); if (kimiHooks.length > 0) { manifest.hooks = kimiHooks; changed = true; } if (changed) { await writeFile(manifestPath, JSON.stringify(manifest, null, 2)); barDebug(c.dim(`${plugin.name}: enriched .kimi-plugin/plugin.json for Kimi Code`)); } } async function readKimiHooks(pluginPath, declaredHooks) { let source = declaredHooks; if (typeof declaredHooks === "string") { source = await readJsonObject(join3(pluginPath, declaredHooks.replace(/^\.\//, ""))); } if (!source) { source = await readJsonObject(join3(pluginPath, "hooks", "hooks.json")); } if (!source || typeof source !== "object" || Array.isArray(source)) return []; const hookMap = source.hooks; if (!hookMap || typeof hookMap !== "object" || Array.isArray(hookMap)) return []; const result = []; for (const [event, groups] of Object.entries(hookMap)) { if (!Array.isArray(groups)) continue; for (const group of groups) { if (!group || typeof group !== "object" || Array.isArray(group)) continue; const groupRecord = group; const hooks = Array.isArray(groupRecord.hooks) ? groupRecord.hooks : [groupRecord]; for (const hook of hooks) { if (!hook || typeof hook !== "object" || Array.isArray(hook)) continue; const hookRecord = hook; if (hookRecord.type !== "command" || typeof hookRecord.c