plugins 0.4.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,946 @@
1
+ #!/usr/bin/env node
2
+
3
+ // index.ts
4
+ import { parseArgs } from "util";
5
+ import { resolve, join as join4 } from "path";
6
+ import { execSync as execSync3 } from "child_process";
7
+ import { existsSync as existsSync3, rmSync, mkdirSync } from "fs";
8
+ import { createInterface } from "readline";
9
+
10
+ // lib/discover.ts
11
+ import { join } from "path";
12
+ import { readFile, readdir, stat } from "fs/promises";
13
+ import { existsSync } from "fs";
14
+
15
+ // lib/ui.ts
16
+ var isColorSupported = process.env.FORCE_COLOR !== "0" && !process.env.NO_COLOR && (process.env.FORCE_COLOR !== void 0 || process.stdout.isTTY);
17
+ function ansi(code) {
18
+ return isColorSupported ? `\x1B[${code}m` : "";
19
+ }
20
+ var reset = ansi("0");
21
+ var bold = ansi("1");
22
+ var dim = ansi("2");
23
+ var italic = ansi("3");
24
+ var underline = ansi("4");
25
+ var red = ansi("31");
26
+ var green = ansi("32");
27
+ var yellow = ansi("33");
28
+ var blue = ansi("34");
29
+ var magenta = ansi("35");
30
+ var cyan = ansi("36");
31
+ var gray = ansi("90");
32
+ var bgGreen = ansi("42");
33
+ var bgRed = ansi("41");
34
+ var bgYellow = ansi("43");
35
+ var bgCyan = ansi("46");
36
+ var black = ansi("30");
37
+ var c = {
38
+ bold: (s) => `${bold}${s}${reset}`,
39
+ dim: (s) => `${dim}${s}${reset}`,
40
+ italic: (s) => `${italic}${s}${reset}`,
41
+ underline: (s) => `${underline}${s}${reset}`,
42
+ red: (s) => `${red}${s}${reset}`,
43
+ green: (s) => `${green}${s}${reset}`,
44
+ yellow: (s) => `${yellow}${s}${reset}`,
45
+ blue: (s) => `${blue}${s}${reset}`,
46
+ magenta: (s) => `${magenta}${s}${reset}`,
47
+ cyan: (s) => `${cyan}${s}${reset}`,
48
+ gray: (s) => `${gray}${s}${reset}`,
49
+ bgGreen: (s) => `${bgGreen}${black}${s}${reset}`,
50
+ bgRed: (s) => `${bgRed}${black}${s}${reset}`,
51
+ bgYellow: (s) => `${bgYellow}${black}${s}${reset}`,
52
+ bgCyan: (s) => `${bgCyan}${black}${s}${reset}`
53
+ };
54
+ var S = {
55
+ // Box drawing
56
+ bar: "\u2502",
57
+ barEnd: "\u2514",
58
+ barStart: "\u250C",
59
+ barH: "\u2500",
60
+ corner: "\u256E",
61
+ // Bullets
62
+ diamond: "\u25C7",
63
+ diamondFilled: "\u25C6",
64
+ bullet: "\u25CF",
65
+ circle: "\u25CB",
66
+ check: "\u2714",
67
+ cross: "\u2716",
68
+ arrow: "\u2192",
69
+ warning: "\u25B2",
70
+ info: "\u2139",
71
+ step: "\u25C7",
72
+ stepActive: "\u25C6",
73
+ stepComplete: "\u25CF",
74
+ stepError: "\u25A0"
75
+ };
76
+ function barLine(content = "") {
77
+ console.log(`${c.gray(S.bar)} ${content}`);
78
+ }
79
+ function barEmpty() {
80
+ console.log(`${c.gray(S.bar)}`);
81
+ }
82
+ function step(content) {
83
+ console.log(`${c.gray(S.step)} ${content}`);
84
+ }
85
+ function stepDone(content) {
86
+ console.log(`${c.green(S.stepComplete)} ${content}`);
87
+ }
88
+ function stepError(content) {
89
+ console.log(`${c.red(S.stepError)} ${content}`);
90
+ }
91
+ function header(label) {
92
+ console.log();
93
+ console.log(`${c.gray(S.barStart)} ${c.bgCyan(` ${label} `)}`);
94
+ }
95
+ function footer(message) {
96
+ if (message) {
97
+ console.log(`${c.gray(S.barEnd)} ${message}`);
98
+ } else {
99
+ console.log(`${c.gray(S.barEnd)}`);
100
+ }
101
+ }
102
+ function error(title, details) {
103
+ console.log(`${c.red(S.stepError)} ${c.red(c.bold(title))}`);
104
+ if (details) {
105
+ for (const line of details) {
106
+ barLine(c.dim(line));
107
+ }
108
+ }
109
+ }
110
+ var BANNER_LINES = [
111
+ "\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",
112
+ "\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",
113
+ "\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",
114
+ "\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",
115
+ "\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",
116
+ "\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"
117
+ ];
118
+ var GRADIENT = [
119
+ [60, 60, 60],
120
+ [90, 90, 90],
121
+ [125, 125, 125],
122
+ [160, 160, 160],
123
+ [200, 200, 200],
124
+ [240, 240, 240]
125
+ ];
126
+ function rgb(r, g, b) {
127
+ return isColorSupported ? `\x1B[38;2;${r};${g};${b}m` : "";
128
+ }
129
+ function banner() {
130
+ console.log();
131
+ for (let i = 0; i < BANNER_LINES.length; i++) {
132
+ const [r, g, b] = GRADIENT[i];
133
+ console.log(`${rgb(r, g, b)}${BANNER_LINES[i]}${reset}`);
134
+ }
135
+ }
136
+
137
+ // lib/discover.ts
138
+ async function discover(repoPath) {
139
+ const marketplacePaths = [
140
+ join(repoPath, "marketplace.json"),
141
+ join(repoPath, ".plugin", "marketplace.json"),
142
+ join(repoPath, ".claude-plugin", "marketplace.json"),
143
+ join(repoPath, ".cursor-plugin", "marketplace.json")
144
+ ];
145
+ for (const mp of marketplacePaths) {
146
+ if (await fileExists(mp)) {
147
+ const data = await readJson(mp);
148
+ if (data && typeof data === "object" && "plugins" in data && Array.isArray(data.plugins)) {
149
+ return discoverFromMarketplace(repoPath, data);
150
+ }
151
+ }
152
+ }
153
+ if (await isPluginDir(repoPath)) {
154
+ const plugin = await inspectPlugin(repoPath);
155
+ return plugin ? [plugin] : [];
156
+ }
157
+ const plugins = [];
158
+ await scanForPlugins(repoPath, plugins, 2);
159
+ return plugins;
160
+ }
161
+ async function scanForPlugins(dirPath, results, depth) {
162
+ if (depth <= 0) return;
163
+ const entries = await readDirSafe(dirPath);
164
+ for (const entry of entries) {
165
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
166
+ const childPath = join(dirPath, entry.name);
167
+ if (await isPluginDir(childPath)) {
168
+ const plugin = await inspectPlugin(childPath);
169
+ if (plugin) results.push(plugin);
170
+ } else {
171
+ await scanForPlugins(childPath, results, depth - 1);
172
+ }
173
+ }
174
+ }
175
+ async function discoverFromMarketplace(repoPath, marketplace) {
176
+ const plugins = [];
177
+ const root = marketplace.metadata?.pluginRoot ?? ".";
178
+ for (const entry of marketplace.plugins) {
179
+ const sourcePath = join(repoPath, root, entry.source.replace(/^\.\//, ""));
180
+ if (!await dirExists(sourcePath)) {
181
+ barLine(`${c.yellow(S.warning)} ${c.yellow(`Plugin source not found: ${entry.source}`)}`);
182
+ continue;
183
+ }
184
+ let skills;
185
+ if (entry.skills && Array.isArray(entry.skills)) {
186
+ skills = [];
187
+ for (const skillPath of entry.skills) {
188
+ const resolvedPath = join(repoPath, root, skillPath.replace(/^\.\//, ""));
189
+ const skillMd = join(resolvedPath, "SKILL.md");
190
+ if (await fileExists(skillMd)) {
191
+ const content = await readFile(skillMd, "utf-8");
192
+ const fm = parseFrontmatter(content);
193
+ skills.push({
194
+ name: fm.name ?? dirName(resolvedPath),
195
+ description: fm.description ?? ""
196
+ });
197
+ }
198
+ }
199
+ } else {
200
+ skills = await discoverSkills(sourcePath);
201
+ }
202
+ let manifest = null;
203
+ for (const manifestDir of [".plugin", ".claude-plugin", ".cursor-plugin"]) {
204
+ const manifestPath = join(sourcePath, manifestDir, "plugin.json");
205
+ if (await fileExists(manifestPath)) {
206
+ manifest = await readJson(manifestPath);
207
+ break;
208
+ }
209
+ }
210
+ const [commands, agents, rules, hasHooks, hasMcp, hasLsp] = await Promise.all([
211
+ discoverCommands(sourcePath),
212
+ discoverAgents(sourcePath),
213
+ discoverRules(sourcePath),
214
+ fileExists(join(sourcePath, "hooks", "hooks.json")),
215
+ fileExists(join(sourcePath, ".mcp.json")),
216
+ fileExists(join(sourcePath, ".lsp.json"))
217
+ ]);
218
+ const name = entry.name || manifest?.name || dirName(sourcePath);
219
+ plugins.push({
220
+ name,
221
+ version: entry.version || manifest?.version || void 0,
222
+ description: entry.description || manifest?.description || void 0,
223
+ path: sourcePath,
224
+ marketplace: marketplace.name,
225
+ skills,
226
+ commands,
227
+ agents,
228
+ rules,
229
+ hasHooks,
230
+ hasMcp,
231
+ hasLsp,
232
+ manifest,
233
+ explicitSkillPaths: entry.skills,
234
+ marketplaceEntry: entry
235
+ });
236
+ }
237
+ return plugins;
238
+ }
239
+ async function isPluginDir(dirPath) {
240
+ const checks = [
241
+ join(dirPath, ".plugin", "plugin.json"),
242
+ join(dirPath, ".claude-plugin", "plugin.json"),
243
+ join(dirPath, ".cursor-plugin", "plugin.json"),
244
+ join(dirPath, "skills"),
245
+ join(dirPath, "commands"),
246
+ join(dirPath, "agents"),
247
+ join(dirPath, "SKILL.md")
248
+ ];
249
+ for (const check of checks) {
250
+ if (await pathExists(check)) return true;
251
+ }
252
+ return false;
253
+ }
254
+ async function inspectPlugin(pluginPath) {
255
+ let manifest = null;
256
+ for (const manifestDir of [".plugin", ".claude-plugin", ".cursor-plugin"]) {
257
+ const manifestPath = join(pluginPath, manifestDir, "plugin.json");
258
+ if (await fileExists(manifestPath)) {
259
+ manifest = await readJson(manifestPath);
260
+ break;
261
+ }
262
+ }
263
+ const name = manifest?.name ?? dirName(pluginPath);
264
+ const [skills, commands, agents, rules, hasHooks, hasMcp, hasLsp] = await Promise.all([
265
+ discoverSkills(pluginPath),
266
+ discoverCommands(pluginPath),
267
+ discoverAgents(pluginPath),
268
+ discoverRules(pluginPath),
269
+ fileExists(join(pluginPath, "hooks", "hooks.json")),
270
+ fileExists(join(pluginPath, ".mcp.json")),
271
+ fileExists(join(pluginPath, ".lsp.json"))
272
+ ]);
273
+ return {
274
+ name,
275
+ version: manifest?.version,
276
+ description: manifest?.description,
277
+ path: pluginPath,
278
+ marketplace: void 0,
279
+ skills,
280
+ commands,
281
+ agents,
282
+ rules,
283
+ hasHooks,
284
+ hasMcp,
285
+ hasLsp,
286
+ manifest,
287
+ explicitSkillPaths: void 0,
288
+ marketplaceEntry: void 0
289
+ };
290
+ }
291
+ async function discoverSkills(pluginPath) {
292
+ const skillsDir = join(pluginPath, "skills");
293
+ const entries = await readDirSafe(skillsDir);
294
+ const skills = [];
295
+ for (const entry of entries) {
296
+ if (!entry.isDirectory()) continue;
297
+ const skillMd = join(skillsDir, entry.name, "SKILL.md");
298
+ if (await fileExists(skillMd)) {
299
+ const content = await readFile(skillMd, "utf-8");
300
+ const fm = parseFrontmatter(content);
301
+ skills.push({
302
+ name: fm.name ?? entry.name,
303
+ description: fm.description ?? ""
304
+ });
305
+ }
306
+ }
307
+ if (skills.length === 0) {
308
+ const rootSkill = join(pluginPath, "SKILL.md");
309
+ if (await fileExists(rootSkill)) {
310
+ const content = await readFile(rootSkill, "utf-8");
311
+ const fm = parseFrontmatter(content);
312
+ skills.push({
313
+ name: fm.name ?? dirName(pluginPath),
314
+ description: fm.description ?? ""
315
+ });
316
+ }
317
+ }
318
+ return skills;
319
+ }
320
+ async function discoverCommands(pluginPath) {
321
+ const commandsDir = join(pluginPath, "commands");
322
+ const entries = await readDirSafe(commandsDir);
323
+ const commands = [];
324
+ for (const entry of entries) {
325
+ if (!entry.isFile() || !entry.name.match(/\.(md|mdc|markdown)$/)) continue;
326
+ const filePath = join(commandsDir, entry.name);
327
+ const content = await readFile(filePath, "utf-8");
328
+ const fm = parseFrontmatter(content);
329
+ commands.push({
330
+ name: entry.name.replace(/\.(md|mdc|markdown)$/, ""),
331
+ description: fm.description ?? ""
332
+ });
333
+ }
334
+ return commands;
335
+ }
336
+ async function discoverAgents(pluginPath) {
337
+ const agentsDir = join(pluginPath, "agents");
338
+ const entries = await readDirSafe(agentsDir);
339
+ const agents = [];
340
+ for (const entry of entries) {
341
+ if (!entry.isFile() || !entry.name.match(/\.(md|mdc|markdown)$/)) continue;
342
+ const filePath = join(agentsDir, entry.name);
343
+ const content = await readFile(filePath, "utf-8");
344
+ const fm = parseFrontmatter(content);
345
+ if (fm.name && fm.description) {
346
+ agents.push({
347
+ name: fm.name,
348
+ description: fm.description
349
+ });
350
+ }
351
+ }
352
+ return agents;
353
+ }
354
+ async function discoverRules(pluginPath) {
355
+ const rulesDir = join(pluginPath, "rules");
356
+ const entries = await readDirSafe(rulesDir);
357
+ const rules = [];
358
+ for (const entry of entries) {
359
+ if (!entry.isFile() || !entry.name.match(/\.(mdc|md|markdown)$/)) continue;
360
+ const filePath = join(rulesDir, entry.name);
361
+ const content = await readFile(filePath, "utf-8");
362
+ const fm = parseFrontmatter(content);
363
+ rules.push({
364
+ name: entry.name.replace(/\.(mdc|md|markdown)$/, ""),
365
+ description: fm.description ?? ""
366
+ });
367
+ }
368
+ return rules;
369
+ }
370
+ function parseFrontmatter(content) {
371
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
372
+ if (!match?.[1]) return {};
373
+ const result = {};
374
+ for (const line of match[1].split("\n")) {
375
+ const kv = line.match(/^(\w[\w-]*):\s*(.+)$/);
376
+ if (kv) {
377
+ let val = kv[2].trim();
378
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
379
+ val = val.slice(1, -1);
380
+ }
381
+ if (val === "true") {
382
+ result[kv[1]] = true;
383
+ } else if (val === "false") {
384
+ result[kv[1]] = false;
385
+ } else {
386
+ result[kv[1]] = val;
387
+ }
388
+ }
389
+ }
390
+ return result;
391
+ }
392
+ function dirName(p) {
393
+ const parts = p.replace(/\/$/, "").split("/");
394
+ return parts[parts.length - 1] ?? "unknown";
395
+ }
396
+ async function fileExists(path) {
397
+ try {
398
+ const s = await stat(path);
399
+ return s.isFile();
400
+ } catch {
401
+ return false;
402
+ }
403
+ }
404
+ async function dirExists(dirPath) {
405
+ try {
406
+ const s = await stat(dirPath);
407
+ return s.isDirectory();
408
+ } catch {
409
+ return false;
410
+ }
411
+ }
412
+ async function pathExists(p) {
413
+ return existsSync(p);
414
+ }
415
+ async function readJson(path) {
416
+ try {
417
+ const content = await readFile(path, "utf-8");
418
+ return JSON.parse(content);
419
+ } catch {
420
+ return null;
421
+ }
422
+ }
423
+ async function readDirSafe(dirPath) {
424
+ try {
425
+ return await readdir(dirPath, { withFileTypes: true });
426
+ } catch {
427
+ return [];
428
+ }
429
+ }
430
+
431
+ // lib/targets.ts
432
+ import { join as join2 } from "path";
433
+ import { homedir } from "os";
434
+ import { execSync } from "child_process";
435
+ var HOME = homedir();
436
+ var TARGET_DEFS = [
437
+ {
438
+ id: "claude-code",
439
+ name: "Claude Code",
440
+ description: "Anthropic's CLI coding agent",
441
+ configPath: join2(HOME, ".claude")
442
+ },
443
+ {
444
+ id: "cursor",
445
+ name: "Cursor",
446
+ description: "AI-powered code editor",
447
+ configPath: join2(HOME, ".cursor")
448
+ }
449
+ // Future targets can be added here:
450
+ // {
451
+ // id: "opencode",
452
+ // name: "OpenCode",
453
+ // description: "Open-source coding agent",
454
+ // configPath: join(HOME, ".config", "opencode"),
455
+ // },
456
+ ];
457
+ async function getTargets() {
458
+ const targets = [];
459
+ for (const def of TARGET_DEFS) {
460
+ const detected = detectTarget(def);
461
+ targets.push({ ...def, detected });
462
+ }
463
+ return targets;
464
+ }
465
+ function detectTarget(def) {
466
+ switch (def.id) {
467
+ case "claude-code":
468
+ return detectBinary("claude");
469
+ case "cursor":
470
+ return detectBinary("cursor") && detectBinary("claude");
471
+ default:
472
+ return false;
473
+ }
474
+ }
475
+ function detectBinary(name) {
476
+ try {
477
+ execSync(`which ${name}`, { stdio: "pipe" });
478
+ return true;
479
+ } catch {
480
+ return false;
481
+ }
482
+ }
483
+
484
+ // lib/install.ts
485
+ import { join as join3, relative } from "path";
486
+ import { mkdir, cp, readFile as readFile2, writeFile } from "fs/promises";
487
+ import { existsSync as existsSync2 } from "fs";
488
+ import { execSync as execSync2 } from "child_process";
489
+ import { homedir as homedir2 } from "os";
490
+ function installerKey(targetId) {
491
+ switch (targetId) {
492
+ case "claude-code":
493
+ case "cursor":
494
+ return "claude-code";
495
+ default:
496
+ return targetId;
497
+ }
498
+ }
499
+ var completedInstallers = /* @__PURE__ */ new Set();
500
+ async function installPlugins(plugins, target, scope, repoPath, source) {
501
+ const key = installerKey(target.id);
502
+ if (completedInstallers.has(key)) {
503
+ return;
504
+ }
505
+ switch (key) {
506
+ case "claude-code":
507
+ await installToClaudeCode(plugins, scope, repoPath, source);
508
+ break;
509
+ default:
510
+ throw new Error(`Unsupported target: ${target.id}`);
511
+ }
512
+ completedInstallers.add(key);
513
+ }
514
+ async function installToClaudeCode(plugins, scope, repoPath, source) {
515
+ const marketplaceName = plugins[0]?.marketplace ?? deriveMarketplaceName(source);
516
+ step("Preparing plugins for Claude Code...");
517
+ barEmpty();
518
+ await prepareForClaudeCode(plugins, repoPath, marketplaceName);
519
+ const claudePath = findClaude();
520
+ step("Adding marketplace");
521
+ barLine(c.dim(`Binary: ${claudePath}`));
522
+ try {
523
+ const version = execSync2(`${claudePath} --version`, { encoding: "utf-8", stdio: "pipe" }).trim();
524
+ barLine(c.dim(`Version: ${version}`));
525
+ } catch {
526
+ barLine(c.dim(`Warning: could not get claude version`));
527
+ }
528
+ try {
529
+ const result = execSync2(`${claudePath} plugin marketplace add ${repoPath}`, {
530
+ encoding: "utf-8",
531
+ stdio: "pipe"
532
+ });
533
+ if (result.trim()) barLine(c.dim(result.trim()));
534
+ stepDone("Marketplace added");
535
+ } catch (err) {
536
+ const stderr = err.stderr?.toString().trim() ?? "";
537
+ const stdout = err.stdout?.toString().trim() ?? "";
538
+ if (stderr.includes("already") || stdout.includes("already")) {
539
+ stepDone(`Marketplace ${c.dim("'" + marketplaceName + "'")} already on disk`);
540
+ } else {
541
+ stepError("Failed to add marketplace.");
542
+ barLine(c.dim(`Command: ${claudePath} plugin marketplace add ${repoPath}`));
543
+ if (stdout) barLine(c.dim(`stdout: ${stdout}`));
544
+ if (stderr) barLine(c.dim(`stderr: ${stderr}`));
545
+ barLine(c.dim(`exit code: ${err.status}`));
546
+ process.exit(1);
547
+ }
548
+ }
549
+ barEmpty();
550
+ for (const plugin of plugins) {
551
+ const pluginRef = `${plugin.name}@${marketplaceName}`;
552
+ step(`Installing ${c.bold(pluginRef)}...`);
553
+ try {
554
+ execSync2(`${claudePath} plugin install ${pluginRef} --scope ${scope}`, {
555
+ encoding: "utf-8",
556
+ stdio: "pipe"
557
+ });
558
+ stepDone(`Installed ${c.cyan(pluginRef)}`);
559
+ } catch (err) {
560
+ const stderr = err.stderr?.toString().trim() ?? "";
561
+ const stdout = err.stdout?.toString().trim() ?? "";
562
+ if (stderr.includes("already") || stdout.includes("already")) {
563
+ stepDone(`${c.cyan(pluginRef)} ${c.dim("already installed")}`);
564
+ } else {
565
+ stepError(`Failed to install ${pluginRef}`);
566
+ barLine(c.dim(`Command: ${claudePath} plugin install ${pluginRef} --scope ${scope}`));
567
+ if (stdout) barLine(c.dim(`stdout: ${stdout}`));
568
+ if (stderr) barLine(c.dim(`stderr: ${stderr}`));
569
+ barLine(c.dim(`exit code: ${err.status}`));
570
+ }
571
+ }
572
+ }
573
+ }
574
+ async function prepareForClaudeCode(plugins, repoPath, marketplaceName) {
575
+ const claudePluginDir = join3(repoPath, ".claude-plugin");
576
+ await mkdir(claudePluginDir, { recursive: true });
577
+ const marketplaceJson = {
578
+ name: marketplaceName,
579
+ owner: { name: "plugins" },
580
+ plugins: plugins.map((p) => {
581
+ const rel = relative(repoPath, p.path);
582
+ const sourcePath = rel === "" ? "./" : `./${rel}`;
583
+ const entry = {
584
+ name: p.name,
585
+ source: sourcePath,
586
+ description: p.description ?? ""
587
+ };
588
+ if (p.version) entry.version = p.version;
589
+ if (p.manifest?.author) entry.author = p.manifest.author;
590
+ if (p.manifest?.license) entry.license = p.manifest.license;
591
+ if (p.manifest?.keywords) entry.keywords = p.manifest.keywords;
592
+ return entry;
593
+ })
594
+ };
595
+ await writeFile(
596
+ join3(claudePluginDir, "marketplace.json"),
597
+ JSON.stringify(marketplaceJson, null, 2)
598
+ );
599
+ barLine(c.dim("Generated .claude-plugin/marketplace.json"));
600
+ for (const plugin of plugins) {
601
+ await preparePluginDirForVendor(plugin, ".claude-plugin", "CLAUDE_PLUGIN_ROOT");
602
+ }
603
+ }
604
+ function findClaude() {
605
+ try {
606
+ const path = execSync2("which claude", { encoding: "utf-8", stdio: "pipe" }).trim();
607
+ if (path) return path;
608
+ } catch {
609
+ }
610
+ const home = homedir2();
611
+ const candidates = [
612
+ join3(home, ".local", "bin", "claude"),
613
+ join3(home, ".bun", "bin", "claude"),
614
+ "/usr/local/bin/claude"
615
+ ];
616
+ for (const candidate of candidates) {
617
+ if (existsSync2(candidate)) return candidate;
618
+ }
619
+ return "claude";
620
+ }
621
+ async function preparePluginDirForVendor(plugin, vendorDir, envVar) {
622
+ const pluginPath = plugin.path;
623
+ const openPluginDir = join3(pluginPath, ".plugin");
624
+ const vendorPluginDir = join3(pluginPath, vendorDir);
625
+ const hasOpenPlugin = existsSync2(join3(openPluginDir, "plugin.json"));
626
+ const hasVendorPlugin = existsSync2(join3(vendorPluginDir, "plugin.json"));
627
+ if (hasOpenPlugin && !hasVendorPlugin) {
628
+ await cp(openPluginDir, vendorPluginDir, { recursive: true });
629
+ barLine(c.dim(`${plugin.name}: translated .plugin/ \u2192 ${vendorDir}/`));
630
+ }
631
+ if (!hasOpenPlugin && !hasVendorPlugin) {
632
+ await mkdir(vendorPluginDir, { recursive: true });
633
+ await writeFile(
634
+ join3(vendorPluginDir, "plugin.json"),
635
+ JSON.stringify(
636
+ {
637
+ name: plugin.name,
638
+ description: plugin.description ?? "",
639
+ version: plugin.version ?? "0.0.0"
640
+ },
641
+ null,
642
+ 2
643
+ )
644
+ );
645
+ barLine(c.dim(`${plugin.name}: generated ${vendorDir}/plugin.json`));
646
+ }
647
+ await translateEnvVars(pluginPath, plugin.name, envVar);
648
+ }
649
+ var KNOWN_PLUGIN_ROOT_VARS = [
650
+ "PLUGIN_ROOT",
651
+ "CLAUDE_PLUGIN_ROOT",
652
+ "CURSOR_PLUGIN_ROOT"
653
+ ];
654
+ async function translateEnvVars(pluginPath, pluginName, envVar) {
655
+ const configFiles = [
656
+ join3(pluginPath, "hooks", "hooks.json"),
657
+ join3(pluginPath, ".mcp.json"),
658
+ join3(pluginPath, ".lsp.json")
659
+ ];
660
+ const target = `\${${envVar}}`;
661
+ const patterns = KNOWN_PLUGIN_ROOT_VARS.filter((v) => v !== envVar).map((v) => `\${${v}}`);
662
+ for (const filePath of configFiles) {
663
+ if (!existsSync2(filePath)) continue;
664
+ let content = await readFile2(filePath, "utf-8");
665
+ let changed = false;
666
+ for (const pattern of patterns) {
667
+ if (content.includes(pattern)) {
668
+ content = content.replaceAll(pattern, target);
669
+ changed = true;
670
+ }
671
+ }
672
+ if (changed) {
673
+ await writeFile(filePath, content);
674
+ barLine(
675
+ c.dim(`${pluginName}: translated plugin root \u2192 \${${envVar}} in ${filePath.split("/").pop()}`)
676
+ );
677
+ }
678
+ }
679
+ }
680
+ function deriveMarketplaceName(source) {
681
+ if (source.match(/^[\w-]+\/[\w.-]+$/)) {
682
+ return source.replace("/", "-");
683
+ }
684
+ const sshMatch = source.match(/^git@[^:]+:(.+?)(?:\.git)?$/);
685
+ if (sshMatch) {
686
+ const parts2 = sshMatch[1].split("/").filter(Boolean);
687
+ if (parts2.length >= 2) {
688
+ return `${parts2[parts2.length - 2]}-${parts2[parts2.length - 1]}`;
689
+ }
690
+ }
691
+ try {
692
+ const url = new URL(source);
693
+ const parts2 = url.pathname.replace(/\.git$/, "").split("/").filter(Boolean);
694
+ if (parts2.length >= 2) {
695
+ return `${parts2[parts2.length - 2]}-${parts2[parts2.length - 1]}`;
696
+ }
697
+ } catch {
698
+ }
699
+ const parts = source.replace(/\/$/, "").split("/");
700
+ return parts[parts.length - 1] ?? "plugins";
701
+ }
702
+
703
+ // index.ts
704
+ var { values, positionals } = parseArgs({
705
+ args: process.argv.slice(2),
706
+ options: {
707
+ help: { type: "boolean", short: "h" },
708
+ target: { type: "string", short: "t" },
709
+ scope: { type: "string", short: "s", default: "user" },
710
+ yes: { type: "boolean", short: "y" }
711
+ },
712
+ allowPositionals: true,
713
+ strict: true
714
+ });
715
+ var [command, ...rest] = positionals;
716
+ if (values.help || !command) {
717
+ printUsage();
718
+ process.exit(0);
719
+ }
720
+ switch (command) {
721
+ case "add":
722
+ await cmdInstall(rest[0], values);
723
+ break;
724
+ case "discover":
725
+ await cmdDiscover(rest[0]);
726
+ break;
727
+ case "targets":
728
+ await cmdTargets();
729
+ break;
730
+ default:
731
+ await cmdInstall(command, values);
732
+ }
733
+ function printUsage() {
734
+ console.log(`
735
+ ${c.bold("plugins")} \u2014 Install open-plugin format plugins into agent tools
736
+
737
+ ${c.dim("Usage:")}
738
+ ${c.cyan("plugins add")} <repo-path-or-url> Install plugins from a repo
739
+ ${c.cyan("plugins discover")} <repo-path-or-url> Discover plugins in a repo
740
+ ${c.cyan("plugins targets")} List available install targets
741
+ ${c.cyan("plugins")} <repo-path-or-url> Shorthand for add
742
+
743
+ ${c.dim("Options:")}
744
+ ${c.yellow("-t, --target")} <target> Target tool (e.g. claude-code). Default: auto-detect
745
+ ${c.yellow("-s, --scope")} <scope> Install scope: user, project, local. Default: user
746
+ ${c.yellow("-y, --yes")} Skip confirmation prompts
747
+ ${c.yellow("-h, --help")} Show this help
748
+ `);
749
+ }
750
+ async function cmdDiscover(source) {
751
+ if (!source) {
752
+ error("Provide a repo path or URL");
753
+ process.exit(1);
754
+ }
755
+ banner();
756
+ header("plugins");
757
+ const repoPath = resolveSource(source);
758
+ const plugins = await discover(repoPath);
759
+ if (plugins.length === 0) {
760
+ barEmpty();
761
+ step("No plugins found.");
762
+ footer();
763
+ return;
764
+ }
765
+ barEmpty();
766
+ step(`Found ${c.bold(String(plugins.length))} plugin(s) in ${c.dim(source)}`);
767
+ barEmpty();
768
+ for (const p of plugins) {
769
+ printPlugin(p);
770
+ }
771
+ footer();
772
+ }
773
+ async function cmdTargets() {
774
+ const targets = await getTargets();
775
+ banner();
776
+ header("plugins");
777
+ if (targets.length === 0) {
778
+ barEmpty();
779
+ step("No supported targets detected.");
780
+ footer();
781
+ return;
782
+ }
783
+ barEmpty();
784
+ step("Available install targets");
785
+ barEmpty();
786
+ for (const t of targets) {
787
+ barLine(` ${c.bold(t.name)}`);
788
+ barLine(` ${c.dim(t.description)}`);
789
+ barLine(` Config: ${c.dim(t.configPath)}`);
790
+ barLine(` Status: ${t.detected ? c.green("detected") : c.dim("not found")}`);
791
+ barEmpty();
792
+ }
793
+ footer();
794
+ }
795
+ async function cmdInstall(source, opts) {
796
+ if (!source) {
797
+ error("Provide a repo path or URL");
798
+ process.exit(1);
799
+ }
800
+ banner();
801
+ header("plugins");
802
+ const repoPath = resolveSource(source);
803
+ const plugins = await discover(repoPath);
804
+ if (plugins.length === 0) {
805
+ barEmpty();
806
+ step("No plugins found.");
807
+ footer();
808
+ return;
809
+ }
810
+ const targets = await getTargets();
811
+ const detectedTargets = targets.filter((t) => t.detected);
812
+ let installTargets;
813
+ if (opts.target) {
814
+ const found = targets.find((t) => t.id === opts.target);
815
+ if (!found) {
816
+ barEmpty();
817
+ stepError(`Unknown target: ${c.bold(opts.target)}`);
818
+ barLine(c.dim(`Available: ${targets.map((t) => t.id).join(", ")}`));
819
+ footer();
820
+ process.exit(1);
821
+ }
822
+ installTargets = [found];
823
+ } else if (detectedTargets.length === 0) {
824
+ barEmpty();
825
+ stepError("No supported targets detected.");
826
+ barLine(c.dim("Use --target to specify one."));
827
+ footer();
828
+ process.exit(1);
829
+ } else {
830
+ installTargets = detectedTargets;
831
+ }
832
+ barEmpty();
833
+ step(`Found ${c.bold(String(plugins.length))} plugin(s)`);
834
+ barEmpty();
835
+ for (const p of plugins) {
836
+ printPlugin(p);
837
+ }
838
+ barLine(`${c.dim("Targets:")} ${installTargets.map((t) => c.cyan(t.name)).join(c.dim(", "))}`);
839
+ barLine(`${c.dim("Scope:")} ${c.cyan(opts.scope ?? "user")}`);
840
+ barEmpty();
841
+ if (!opts.yes) {
842
+ const response = await readLine(`${c.cyan(S.stepActive)} Install? ${c.dim("[Y/n]")} `);
843
+ if (response.trim().toLowerCase() === "n") {
844
+ step("Aborted.");
845
+ footer();
846
+ return;
847
+ }
848
+ }
849
+ const scope = opts.scope ?? "user";
850
+ for (const target of installTargets) {
851
+ await installPlugins(plugins, target, scope, repoPath, source);
852
+ }
853
+ barEmpty();
854
+ stepDone(c.green("Done.") + " Restart your agent tools to load the plugins.");
855
+ footer();
856
+ }
857
+ function printPlugin(p) {
858
+ barLine(`${c.bold(p.name)} ${p.version ? c.dim(`(v${p.version})`) : ""}`);
859
+ if (p.description) barLine(`${c.dim(p.description)}`);
860
+ const parts = [];
861
+ if (p.skills.length) parts.push(`${p.skills.length} skill(s)`);
862
+ if (p.commands.length) parts.push(`${p.commands.length} command(s)`);
863
+ if (p.agents.length) parts.push(`${p.agents.length} agent(s)`);
864
+ if (p.rules.length) parts.push(`${p.rules.length} rule(s)`);
865
+ if (p.hasHooks) parts.push("hooks");
866
+ if (p.hasMcp) parts.push("MCP servers");
867
+ if (p.hasLsp) parts.push("LSP servers");
868
+ if (parts.length) barLine(`${c.dim("Components:")} ${parts.join(c.dim(", "))}`);
869
+ barEmpty();
870
+ }
871
+ function sshToHttps(sshUrl) {
872
+ const m = sshUrl.match(/^git@([^:]+):(.+)$/);
873
+ if (!m) return null;
874
+ return `https://${m[1]}/${m[2]}`;
875
+ }
876
+ function resolveSource(source) {
877
+ if (source.startsWith("https://") || source.startsWith("git@") || source.match(/^[\w-]+\/[\w.-]+$/)) {
878
+ const url = source.match(/^[\w-]+\/[\w.-]+$/) ? `https://github.com/${source}` : source;
879
+ const cacheDir = join4(process.env.HOME ?? "~", ".cache", "plugins");
880
+ mkdirSync(cacheDir, { recursive: true });
881
+ const slug = url.replace(/^https?:\/\//, "").replace(/^git@/, "").replace(/\.git$/, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
882
+ const tmpDir = join4(cacheDir, slug);
883
+ if (existsSync3(join4(tmpDir, ".git", "HEAD"))) {
884
+ rmSync(tmpDir, { recursive: true, force: true });
885
+ }
886
+ step(`Source: ${c.dim(url)}`);
887
+ barEmpty();
888
+ try {
889
+ execSync3(`git clone --depth 1 -q ${url} ${tmpDir}`, { stdio: "pipe" });
890
+ } catch (err) {
891
+ const stderr = err.stderr?.toString() ?? "";
892
+ if (url.startsWith("git@") && stderr.includes("Permission denied")) {
893
+ const httpsUrl = sshToHttps(url);
894
+ if (httpsUrl) {
895
+ barLine(c.yellow("SSH authentication failed. Retrying over HTTPS..."));
896
+ step(`Source: ${c.dim(httpsUrl)}`);
897
+ barEmpty();
898
+ try {
899
+ execSync3(`git clone --depth 1 -q ${httpsUrl} ${tmpDir}`, { stdio: "inherit" });
900
+ stepDone("Repository cloned");
901
+ barEmpty();
902
+ return tmpDir;
903
+ } catch {
904
+ }
905
+ }
906
+ }
907
+ if (existsSync3(tmpDir)) {
908
+ rmSync(tmpDir, { recursive: true, force: true });
909
+ }
910
+ if (stderr.includes("Permission denied") || stderr.includes("Could not read from remote repository")) {
911
+ barEmpty();
912
+ stepError(`Could not access ${c.bold(url)}`);
913
+ barEmpty();
914
+ barLine(c.dim("Make sure you have access to this repository. For private repos, try:"));
915
+ barLine(` ${c.dim("HTTPS:")} plugins add https://github.com/owner/repo`);
916
+ barLine(` ${c.dim(" (uses git credential helper / browser auth)")}`);
917
+ barLine(` ${c.dim("SSH:")} plugins add git@github.com:owner/repo.git`);
918
+ barLine(` ${c.dim(" (requires SSH keys)")}`);
919
+ } else if (stderr.includes("not found") || stderr.includes("does not exist") || err.status === 128) {
920
+ barEmpty();
921
+ stepError(`Repository not found: ${c.bold(url)}`);
922
+ barLine(c.dim("Check that the URL is correct and the repository exists."));
923
+ } else {
924
+ barEmpty();
925
+ stepError("git clone failed.");
926
+ if (stderr.trim()) barLine(c.dim(stderr.trim()));
927
+ }
928
+ footer();
929
+ process.exit(1);
930
+ }
931
+ stepDone("Repository cloned");
932
+ barEmpty();
933
+ return tmpDir;
934
+ }
935
+ return resolve(source);
936
+ }
937
+ function readLine(prompt) {
938
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
939
+ return new Promise((resolve2) => {
940
+ rl.question(prompt, (answer) => {
941
+ rl.close();
942
+ if (!process.stdin.isTTY) process.stdout.write("\n");
943
+ resolve2(answer);
944
+ });
945
+ });
946
+ }