plugins
Version:
Install open-plugin format plugins into agent tools
1,480 lines (1,470 loc) • 59.1 kB
JavaScript
#!/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(", "