UNPKG

plugins

Version:

Install open-plugin format plugins into agent tools

1,480 lines (1,470 loc) 59.1 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 { join as join2 } from "path"; import { homedir } from "os"; import { execSync } from "child_process"; var HOME = homedir(); 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") } // Future targets can be added here: // { // id: "opencode", // name: "OpenCode", // description: "Open-source coding agent", // configPath: join(HOME, ".config", "opencode"), // }, ]; 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"); default: return false; } } function detectBinary(name) { try { execSync(`which ${name}`, { stdio: "pipe" }); return true; } catch { return false; } } // lib/install.ts import { join as join3, relative } from "path"; import { mkdir, cp, readFile as readFile2, writeFile, rm } from "fs/promises"; import { existsSync as existsSync2 } from "fs"; import { 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) { 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 workspace = await stageInstallWorkspace(plugins, repoPath, target.id); await installToCodex(workspace.plugins, scope, workspace.repoPath, source); 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 getOfficialPluginRef(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(); } if (repo === "vercel/vercel-plugin") { return "vercel@claude-plugins-official"; } return 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; } } 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 prepareForClaudeCode(plugins, repoPath, marketplaceName) { const claudePluginDir = join3(repoPath, ".claude-plugin"); await mkdir(claudePluginDir, { recursive: true }); const marketplaceJson = { name: marketplaceName, owner: { name: "plugins" }, plugins: plugins.map((p) => { const rel = relative(repoPath, p.path); const sourcePath = rel === "" ? "./" : `./${rel}`; const entry = { name: p.name, source: sourcePath, description: p.description ?? "" }; if (p.version) entry.version = p.version; if (p.manifest?.author) entry.author = p.manifest.author; if (p.manifest?.license) entry.license = p.manifest.license; if (p.manifest?.keywords) entry.keywords = p.manifest.keywords; return entry; }) }; await writeFile( join3(claudePluginDir, "marketplace.json"), JSON.stringify(marketplaceJson, null, 2) ); barDebug(c.dim("Generated .claude-plugin/marketplace.json")); for (const plugin of plugins) { await preparePluginDirForVendor(plugin, ".claude-plugin", "CLAUDE_PLUGIN_ROOT"); } } function findClaudeOrNull() { try { const path = execSync2("which claude", { encoding: "utf-8", stdio: "pipe" }).trim(); if (path) return path; } catch { } const home = homedir2(); const candidates = [ join3(home, ".local", "bin", "claude"), join3(home, ".bun", "bin", "claude"), "/usr/local/bin/claude" ]; for (const candidate of candidates) { if (existsSync2(candidate)) return candidate; } return null; } async function preparePluginDirForVendor(plugin, vendorDir, envVar) { const pluginPath = plugin.path; const openPluginDir = join3(pluginPath, ".plugin"); const vendorPluginDir = join3(pluginPath, vendorDir); const hasOpenPlugin = existsSync2(join3(openPluginDir, "plugin.json")); const hasVendorPlugin = existsSync2(join3(vendorPluginDir, "plugin.json")); if (hasOpenPlugin && !hasVendorPlugin) { await cp(openPluginDir, vendorPluginDir, { recursive: true }); barDebug(c.dim(`${plugin.name}: translated .plugin/ \u2192 ${vendorDir}/`)); } if (!hasOpenPlugin && !hasVendorPlugin) { await mkdir(vendorPluginDir, { recursive: true }); await writeFile( join3(vendorPluginDir, "plugin.json"), JSON.stringify( { name: plugin.name, description: plugin.description ?? "", version: plugin.version ?? "0.0.0" }, null, 2 ) ); barDebug(c.dim(`${plugin.name}: generated ${vendorDir}/plugin.json`)); } await translateEnvVars(pluginPath, plugin.name, envVar); } var KNOWN_PLUGIN_ROOT_VARS = [ "PLUGIN_ROOT", "CLAUDE_PLUGIN_ROOT", "CURSOR_PLUGIN_ROOT", "CODEX_PLUGIN_ROOT" ]; async function translateEnvVars(pluginPath, pluginName, envVar) { const configFiles = [ join3(pluginPath, "hooks", "hooks.json"), join3(pluginPath, ".mcp.json"), join3(pluginPath, ".lsp.json") ]; const target = `\${${envVar}}`; const patterns = KNOWN_PLUGIN_ROOT_VARS.filter((v) => v !== envVar).map((v) => `\${${v}}`); for (const filePath of configFiles) { if (!existsSync2(filePath)) continue; let content = await readFile2(filePath, "utf-8"); let changed = false; for (const pattern of patterns) { if (content.includes(pattern)) { content = content.replaceAll(pattern, target); changed = true; } } if (changed) { await writeFile(filePath, content); barDebug( c.dim( `${pluginName}: translated plugin root \u2192 \${${envVar}} in ${filePath.split("/").pop()}` ) ); } } } function deriveMarketplaceName(source) { if (source.match(/^[\w-]+\/[\w.-]+$/)) { return source.replace("/", "-"); } const sshMatch = source.match(/^git@[^:]+:(.+?)(?:\.git)?$/); if (sshMatch) { const parts2 = sshMatch[1].split("/").filter(Boolean); if (parts2.length >= 2) { return `${parts2[parts2.length - 2]}-${parts2[parts2.length - 1]}`; } } try { const url = new URL(source); const parts2 = url.pathname.replace(/\.git$/, "").split("/").filter(Boolean); if (parts2.length >= 2) { return `${parts2[parts2.length - 2]}-${parts2[parts2.length - 1]}`; } } catch { } const parts = source.replace(/\/$/, "").split("/"); return parts[parts.length - 1] ?? "plugins"; } function extractGitHubRepo(source) { const shorthand = source.match(/^([\w-]+\/[\w.-]+)$/); if (shorthand) return shorthand[1]; const httpsMatch = source.match(/^https?:\/\/github\.com\/([\w.-]+\/[\w.-]+?)(?:\.git)?$/); if (httpsMatch) return httpsMatch[1]; const sshMatch = source.match(/^git@github\.com:([\w.-]+\/[\w.-]+?)(?:\.git)?$/); if (sshMatch) return sshMatch[1]; return null; } function isRemoteSource(source) { if (source.match(/^[\w-]+\/[\w.-]+$/)) return true; if (source.startsWith("git@")) return true; if (source.startsWith("https://") || source.startsWith("http://")) return true; return false; } function normalizeGitUrl(source) { if (source.match(/^[\w-]+\/[\w.-]+$/)) { return `https://github.com/${source}`; } const sshMatch = source.match(/^git@([^:]+):(.+?)(?:\.git)?$/); if (sshMatch) { return `https://${sshMatch[1]}/${sshMatch[2]}`; } return source; } async function isMarketplaceNew(marketplaceName) { const knownPath = join3(homedir2(), ".claude", "plugins", "known_marketplaces.json"); if (!existsSync2(knownPath)) return true; try { const data = JSON.parse(await readFile2(knownPath, "utf-8")); return !data[marketplaceName]; } catch { return true; } } async function setAutoUpdate(marketplaceName, enabled) { const knownPath = join3(homedir2(), ".claude", "plugins", "known_marketplaces.json"); if (!existsSync2(knownPath)) return; let data = {}; try { data = JSON.parse(await readFile2(knownPath, "utf-8")); } catch { return; } if (!data[marketplaceName]) return; data[marketplaceName].autoUpdate = enabled; await writeFile(knownPath, JSON.stringify(data, null, 2)); } // lib/telemetry.ts var TELEMETRY_URL = "https://plugins-telemetry.labs.vercel.dev/t"; var cliVersion = null; function isCI() { return !!(process.env.CI || process.env.GITHUB_ACTIONS || process.env.GITLAB_CI || process.env.CIRCLECI || process.env.TRAVIS || process.env.BUILDKITE || process.env.JENKINS_URL || process.env.TEAMCITY_VERSION); } function isEnabled() { return !process.env.DISABLE_TELEMETRY && !process.env.DO_NOT_TRACK; } function setVersion(version) { cliVersion = version; } function track(data) { if (!isEnabled()) return; try { const params = new URLSearchParams(); if (cliVersion) { params.set("v", cliVersion); } if (isCI()) { params.set("ci", "1"); } for (const [key, value] of Object.entries(data)) { if (value !== void 0 && value !== null) { params.set(key, String(value)); } } fetch(`${TELEMETRY_URL}?${params.toString()}`).catch(() => { }); } catch { } } // index.ts setVersion("1.2.8"); var { values, positionals } = parseArgs({ args: process.argv.slice(2), options: { help: { type: "boolean", short: "h" }, target: { type: "string", short: "t" }, scope: { type: "string", short: "s", default: "user" }, yes: { type: "boolean", short: "y" }, remote: { type: "boolean" }, debug: { type: "boolean" } }, allowPositionals: true, strict: true }); var [command, ...rest] = positionals; if (values.debug) setDebug(true); if (values.help || !command) { printUsage(); process.exit(0); } switch (command) { case "add": await cmdInstall(rest[0], values); break; case "discover": await cmdDiscover(rest[0]); break; case "targets": await cmdTargets(); break; default: await cmdInstall(command, values); } function printUsage() { console.log(` ${c.bold("plugins")} \u2014 Install open-plugin format plugins into agent tools ${c.dim("Usage:")} ${c.cyan("plugins add")} <repo-path-or-url> Install plugins from a repo ${c.cyan("plugins discover")} <repo-path-or-url> Discover plugins in a repo ${c.cyan("plugins targets")} List available install targets ${c.cyan("plugins")} <repo-path-or-url> Shorthand for add ${c.dim("Options:")} ${c.yellow("-t, --target")} <target> Target tool (e.g. claude-code). Default: auto-detect ${c.yellow("-s, --scope")} <scope> Install scope: user, project, local. Default: user ${c.yellow("-y, --yes")} Skip confirmation prompts ${c.yellow("--remote")} Include remote-source plugins in output ${c.yellow("--debug")} Show verbose installation output ${c.yellow("-h, --help")} Show this help `); } async function cmdDiscover(source) { if (!source) { error("Provide a repo path or URL"); process.exit(1); } banner(); header("plugins"); const repoPath = resolveSource(source); const { plugins, remotePlugins, missingPaths } = await discover(repoPath); if (plugins.length === 0 && remotePlugins.length === 0) { barEmpty(); step("No plugins found."); footer(); return; } if (plugins.length > 0) { barEmpty(); step(`Found ${c.bold(String(plugins.length))} local plugin(s)`); barEmpty(); printPluginTable(plugins); } if (remotePlugins.length > 0) { if (values.remote) { barEmpty(); step( `${c.bold(String(remotePlugins.length))} remote plugin(s) ${c.dim("(hosted in external repos)")}` ); barEmpty(); printRemotePluginTable(remotePlugins); } else { barEmpty(); barLine(c.dim(`${remotePlugins.length} remote plugin(s) not shown. Run:`)); barLine(` ${c.cyan(`npx plugins discover ${source} --remote`)}`); } printMissingPaths(missingPaths); } footer(); } async function cmdTargets() { const targets = await getTargets(); banner(); header("plugins"); if (targets.length === 0) { barEmpty(); step("No supported targets detected."); footer(); return; } barEmpty(); step("Available install targets"); barEmpty(); for (const t of targets) { barLine(` ${c.bold(t.name)}`); barLine(` ${c.dim(t.description)}`); barLine(` Config: ${c.dim(t.configPath)}`); barLine(` Status: ${t.detected ? c.green("detected") : c.dim("not found")}`); barEmpty(); } footer(); } async function cmdInstall(source, opts) { if (!source) { error("Provide a repo path or URL"); process.exit(1); } banner(); header("plugins"); const repoPath = resolveSource(source); const { plugins, remotePlugins, missingPaths } = await discover(repoPath); if (plugins.length === 0) { barEmpty(); step("No plugins found."); if (remotePlugins.length > 0) { barLine(c.dim(`${remotePlugins.length} remote plugin(s) not shown. Run:`)); barLine(` ${c.cyan(`npx plugins discover ${source} --remote`)}`); printMissingPaths(missingPaths); } footer(); return; } const targets = await getTargets(); const detectedTargets = targets.filter((t) => t.detected); let installTargets; if (opts.target) { const found = targets.find((t) => t.id === opts.target); if (!found) { barEmpty(); stepError(`Unknown target: ${c.bold(opts.target)}`); barLine(c.dim(`Available: ${targets.map((t) => t.id).join(", "