add-plugin 0.0.1 → 0.0.2
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/README.md +15 -0
- package/dist/index.js +690 -0
- package/package.json +14 -9
- package/index.js +0 -1
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# add-plugin
|
|
2
|
+
|
|
3
|
+
To install dependencies:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun install
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
To run:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun run index.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This project was created using `bun init` in bun v1.3.8. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,690 @@
|
|
|
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
|
+
async function discover(repoPath) {
|
|
15
|
+
const marketplacePaths = [
|
|
16
|
+
join(repoPath, "marketplace.json"),
|
|
17
|
+
join(repoPath, ".plugin", "marketplace.json"),
|
|
18
|
+
join(repoPath, ".claude-plugin", "marketplace.json"),
|
|
19
|
+
join(repoPath, ".cursor-plugin", "marketplace.json")
|
|
20
|
+
];
|
|
21
|
+
for (const mp of marketplacePaths) {
|
|
22
|
+
if (await fileExists(mp)) {
|
|
23
|
+
const data = await readJson(mp);
|
|
24
|
+
if (data && typeof data === "object" && "plugins" in data && Array.isArray(data.plugins)) {
|
|
25
|
+
return discoverFromMarketplace(repoPath, data);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (await isPluginDir(repoPath)) {
|
|
30
|
+
const plugin = await inspectPlugin(repoPath);
|
|
31
|
+
return plugin ? [plugin] : [];
|
|
32
|
+
}
|
|
33
|
+
const plugins = [];
|
|
34
|
+
await scanForPlugins(repoPath, plugins, 2);
|
|
35
|
+
return plugins;
|
|
36
|
+
}
|
|
37
|
+
async function scanForPlugins(dirPath, results, depth) {
|
|
38
|
+
if (depth <= 0) return;
|
|
39
|
+
const entries = await readDirSafe(dirPath);
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
42
|
+
const childPath = join(dirPath, entry.name);
|
|
43
|
+
if (await isPluginDir(childPath)) {
|
|
44
|
+
const plugin = await inspectPlugin(childPath);
|
|
45
|
+
if (plugin) results.push(plugin);
|
|
46
|
+
} else {
|
|
47
|
+
await scanForPlugins(childPath, results, depth - 1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function discoverFromMarketplace(repoPath, marketplace) {
|
|
52
|
+
const plugins = [];
|
|
53
|
+
const root = marketplace.metadata?.pluginRoot ?? ".";
|
|
54
|
+
for (const entry of marketplace.plugins) {
|
|
55
|
+
const sourcePath = join(repoPath, root, entry.source.replace(/^\.\//, ""));
|
|
56
|
+
if (!await dirExists(sourcePath)) {
|
|
57
|
+
console.warn(` Warning: plugin source not found: ${entry.source}`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
let skills;
|
|
61
|
+
if (entry.skills && Array.isArray(entry.skills)) {
|
|
62
|
+
skills = [];
|
|
63
|
+
for (const skillPath of entry.skills) {
|
|
64
|
+
const resolvedPath = join(repoPath, root, skillPath.replace(/^\.\//, ""));
|
|
65
|
+
const skillMd = join(resolvedPath, "SKILL.md");
|
|
66
|
+
if (await fileExists(skillMd)) {
|
|
67
|
+
const content = await readFile(skillMd, "utf-8");
|
|
68
|
+
const fm = parseFrontmatter(content);
|
|
69
|
+
skills.push({
|
|
70
|
+
name: fm.name ?? dirName(resolvedPath),
|
|
71
|
+
description: fm.description ?? ""
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
skills = await discoverSkills(sourcePath);
|
|
77
|
+
}
|
|
78
|
+
let manifest = null;
|
|
79
|
+
for (const manifestDir of [".plugin", ".claude-plugin", ".cursor-plugin"]) {
|
|
80
|
+
const manifestPath = join(sourcePath, manifestDir, "plugin.json");
|
|
81
|
+
if (await fileExists(manifestPath)) {
|
|
82
|
+
manifest = await readJson(manifestPath);
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const [commands, agents, rules, hasHooks, hasMcp, hasLsp] = await Promise.all([
|
|
87
|
+
discoverCommands(sourcePath),
|
|
88
|
+
discoverAgents(sourcePath),
|
|
89
|
+
discoverRules(sourcePath),
|
|
90
|
+
fileExists(join(sourcePath, "hooks", "hooks.json")),
|
|
91
|
+
fileExists(join(sourcePath, ".mcp.json")),
|
|
92
|
+
fileExists(join(sourcePath, ".lsp.json"))
|
|
93
|
+
]);
|
|
94
|
+
const name = entry.name || manifest?.name || dirName(sourcePath);
|
|
95
|
+
plugins.push({
|
|
96
|
+
name,
|
|
97
|
+
version: entry.version || manifest?.version || void 0,
|
|
98
|
+
description: entry.description || manifest?.description || void 0,
|
|
99
|
+
path: sourcePath,
|
|
100
|
+
marketplace: marketplace.name,
|
|
101
|
+
skills,
|
|
102
|
+
commands,
|
|
103
|
+
agents,
|
|
104
|
+
rules,
|
|
105
|
+
hasHooks,
|
|
106
|
+
hasMcp,
|
|
107
|
+
hasLsp,
|
|
108
|
+
manifest,
|
|
109
|
+
explicitSkillPaths: entry.skills,
|
|
110
|
+
marketplaceEntry: entry
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return plugins;
|
|
114
|
+
}
|
|
115
|
+
async function isPluginDir(dirPath) {
|
|
116
|
+
const checks = [
|
|
117
|
+
join(dirPath, ".plugin", "plugin.json"),
|
|
118
|
+
join(dirPath, ".claude-plugin", "plugin.json"),
|
|
119
|
+
join(dirPath, ".cursor-plugin", "plugin.json"),
|
|
120
|
+
join(dirPath, "skills"),
|
|
121
|
+
join(dirPath, "commands"),
|
|
122
|
+
join(dirPath, "agents"),
|
|
123
|
+
join(dirPath, "SKILL.md")
|
|
124
|
+
];
|
|
125
|
+
for (const check of checks) {
|
|
126
|
+
if (await pathExists(check)) return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
async function inspectPlugin(pluginPath) {
|
|
131
|
+
let manifest = null;
|
|
132
|
+
for (const manifestDir of [".plugin", ".claude-plugin", ".cursor-plugin"]) {
|
|
133
|
+
const manifestPath = join(pluginPath, manifestDir, "plugin.json");
|
|
134
|
+
if (await fileExists(manifestPath)) {
|
|
135
|
+
manifest = await readJson(manifestPath);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const name = manifest?.name ?? dirName(pluginPath);
|
|
140
|
+
const [skills, commands, agents, rules, hasHooks, hasMcp, hasLsp] = await Promise.all([
|
|
141
|
+
discoverSkills(pluginPath),
|
|
142
|
+
discoverCommands(pluginPath),
|
|
143
|
+
discoverAgents(pluginPath),
|
|
144
|
+
discoverRules(pluginPath),
|
|
145
|
+
fileExists(join(pluginPath, "hooks", "hooks.json")),
|
|
146
|
+
fileExists(join(pluginPath, ".mcp.json")),
|
|
147
|
+
fileExists(join(pluginPath, ".lsp.json"))
|
|
148
|
+
]);
|
|
149
|
+
return {
|
|
150
|
+
name,
|
|
151
|
+
version: manifest?.version,
|
|
152
|
+
description: manifest?.description,
|
|
153
|
+
path: pluginPath,
|
|
154
|
+
marketplace: void 0,
|
|
155
|
+
skills,
|
|
156
|
+
commands,
|
|
157
|
+
agents,
|
|
158
|
+
rules,
|
|
159
|
+
hasHooks,
|
|
160
|
+
hasMcp,
|
|
161
|
+
hasLsp,
|
|
162
|
+
manifest,
|
|
163
|
+
explicitSkillPaths: void 0,
|
|
164
|
+
marketplaceEntry: void 0
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
async function discoverSkills(pluginPath) {
|
|
168
|
+
const skillsDir = join(pluginPath, "skills");
|
|
169
|
+
const entries = await readDirSafe(skillsDir);
|
|
170
|
+
const skills = [];
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (!entry.isDirectory()) continue;
|
|
173
|
+
const skillMd = join(skillsDir, entry.name, "SKILL.md");
|
|
174
|
+
if (await fileExists(skillMd)) {
|
|
175
|
+
const content = await readFile(skillMd, "utf-8");
|
|
176
|
+
const fm = parseFrontmatter(content);
|
|
177
|
+
skills.push({
|
|
178
|
+
name: fm.name ?? entry.name,
|
|
179
|
+
description: fm.description ?? ""
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (skills.length === 0) {
|
|
184
|
+
const rootSkill = join(pluginPath, "SKILL.md");
|
|
185
|
+
if (await fileExists(rootSkill)) {
|
|
186
|
+
const content = await readFile(rootSkill, "utf-8");
|
|
187
|
+
const fm = parseFrontmatter(content);
|
|
188
|
+
skills.push({
|
|
189
|
+
name: fm.name ?? dirName(pluginPath),
|
|
190
|
+
description: fm.description ?? ""
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return skills;
|
|
195
|
+
}
|
|
196
|
+
async function discoverCommands(pluginPath) {
|
|
197
|
+
const commandsDir = join(pluginPath, "commands");
|
|
198
|
+
const entries = await readDirSafe(commandsDir);
|
|
199
|
+
const commands = [];
|
|
200
|
+
for (const entry of entries) {
|
|
201
|
+
if (!entry.isFile() || !entry.name.match(/\.(md|mdc|markdown)$/)) continue;
|
|
202
|
+
const filePath = join(commandsDir, entry.name);
|
|
203
|
+
const content = await readFile(filePath, "utf-8");
|
|
204
|
+
const fm = parseFrontmatter(content);
|
|
205
|
+
commands.push({
|
|
206
|
+
name: entry.name.replace(/\.(md|mdc|markdown)$/, ""),
|
|
207
|
+
description: fm.description ?? ""
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
return commands;
|
|
211
|
+
}
|
|
212
|
+
async function discoverAgents(pluginPath) {
|
|
213
|
+
const agentsDir = join(pluginPath, "agents");
|
|
214
|
+
const entries = await readDirSafe(agentsDir);
|
|
215
|
+
const agents = [];
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
if (!entry.isFile() || !entry.name.match(/\.(md|mdc|markdown)$/)) continue;
|
|
218
|
+
const filePath = join(agentsDir, entry.name);
|
|
219
|
+
const content = await readFile(filePath, "utf-8");
|
|
220
|
+
const fm = parseFrontmatter(content);
|
|
221
|
+
if (fm.name && fm.description) {
|
|
222
|
+
agents.push({
|
|
223
|
+
name: fm.name,
|
|
224
|
+
description: fm.description
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return agents;
|
|
229
|
+
}
|
|
230
|
+
async function discoverRules(pluginPath) {
|
|
231
|
+
const rulesDir = join(pluginPath, "rules");
|
|
232
|
+
const entries = await readDirSafe(rulesDir);
|
|
233
|
+
const rules = [];
|
|
234
|
+
for (const entry of entries) {
|
|
235
|
+
if (!entry.isFile() || !entry.name.match(/\.(mdc|md|markdown)$/)) continue;
|
|
236
|
+
const filePath = join(rulesDir, entry.name);
|
|
237
|
+
const content = await readFile(filePath, "utf-8");
|
|
238
|
+
const fm = parseFrontmatter(content);
|
|
239
|
+
rules.push({
|
|
240
|
+
name: entry.name.replace(/\.(mdc|md|markdown)$/, ""),
|
|
241
|
+
description: fm.description ?? ""
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return rules;
|
|
245
|
+
}
|
|
246
|
+
function parseFrontmatter(content) {
|
|
247
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
248
|
+
if (!match?.[1]) return {};
|
|
249
|
+
const result = {};
|
|
250
|
+
for (const line of match[1].split("\n")) {
|
|
251
|
+
const kv = line.match(/^(\w[\w-]*):\s*(.+)$/);
|
|
252
|
+
if (kv) {
|
|
253
|
+
let val = kv[2].trim();
|
|
254
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
255
|
+
val = val.slice(1, -1);
|
|
256
|
+
}
|
|
257
|
+
if (val === "true") {
|
|
258
|
+
result[kv[1]] = true;
|
|
259
|
+
} else if (val === "false") {
|
|
260
|
+
result[kv[1]] = false;
|
|
261
|
+
} else {
|
|
262
|
+
result[kv[1]] = val;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
function dirName(p) {
|
|
269
|
+
const parts = p.replace(/\/$/, "").split("/");
|
|
270
|
+
return parts[parts.length - 1] ?? "unknown";
|
|
271
|
+
}
|
|
272
|
+
async function fileExists(path) {
|
|
273
|
+
try {
|
|
274
|
+
const s = await stat(path);
|
|
275
|
+
return s.isFile();
|
|
276
|
+
} catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function dirExists(dirPath) {
|
|
281
|
+
try {
|
|
282
|
+
const s = await stat(dirPath);
|
|
283
|
+
return s.isDirectory();
|
|
284
|
+
} catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function pathExists(p) {
|
|
289
|
+
return existsSync(p);
|
|
290
|
+
}
|
|
291
|
+
async function readJson(path) {
|
|
292
|
+
try {
|
|
293
|
+
const content = await readFile(path, "utf-8");
|
|
294
|
+
return JSON.parse(content);
|
|
295
|
+
} catch {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async function readDirSafe(dirPath) {
|
|
300
|
+
try {
|
|
301
|
+
return await readdir(dirPath, { withFileTypes: true });
|
|
302
|
+
} catch {
|
|
303
|
+
return [];
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// lib/targets.ts
|
|
308
|
+
import { join as join2 } from "path";
|
|
309
|
+
import { homedir } from "os";
|
|
310
|
+
import { execSync } from "child_process";
|
|
311
|
+
var HOME = homedir();
|
|
312
|
+
var TARGET_DEFS = [
|
|
313
|
+
{
|
|
314
|
+
id: "claude-code",
|
|
315
|
+
name: "Claude Code",
|
|
316
|
+
description: "Anthropic's CLI coding agent",
|
|
317
|
+
configPath: join2(HOME, ".claude")
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: "cursor",
|
|
321
|
+
name: "Cursor",
|
|
322
|
+
description: "AI-powered code editor",
|
|
323
|
+
configPath: join2(HOME, ".cursor")
|
|
324
|
+
}
|
|
325
|
+
// Future targets can be added here:
|
|
326
|
+
// {
|
|
327
|
+
// id: "opencode",
|
|
328
|
+
// name: "OpenCode",
|
|
329
|
+
// description: "Open-source coding agent",
|
|
330
|
+
// configPath: join(HOME, ".config", "opencode"),
|
|
331
|
+
// },
|
|
332
|
+
];
|
|
333
|
+
async function getTargets() {
|
|
334
|
+
const targets = [];
|
|
335
|
+
for (const def of TARGET_DEFS) {
|
|
336
|
+
const detected = detectTarget(def);
|
|
337
|
+
targets.push({ ...def, detected });
|
|
338
|
+
}
|
|
339
|
+
return targets;
|
|
340
|
+
}
|
|
341
|
+
function detectTarget(def) {
|
|
342
|
+
switch (def.id) {
|
|
343
|
+
case "claude-code":
|
|
344
|
+
return detectBinary("claude");
|
|
345
|
+
case "cursor":
|
|
346
|
+
return detectBinary("cursor");
|
|
347
|
+
default:
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function detectBinary(name) {
|
|
352
|
+
try {
|
|
353
|
+
execSync(`which ${name}`, { stdio: "pipe" });
|
|
354
|
+
return true;
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// lib/install.ts
|
|
361
|
+
import { join as join3, relative } from "path";
|
|
362
|
+
import { mkdir, cp, readFile as readFile2, writeFile } from "fs/promises";
|
|
363
|
+
import { existsSync as existsSync2 } from "fs";
|
|
364
|
+
import { execSync as execSync2 } from "child_process";
|
|
365
|
+
async function installPlugins(plugins, target, scope, repoPath, source) {
|
|
366
|
+
switch (target.id) {
|
|
367
|
+
case "claude-code":
|
|
368
|
+
await installToVendor(plugins, scope, repoPath, source, {
|
|
369
|
+
vendorDir: ".claude-plugin",
|
|
370
|
+
cliCommand: "claude",
|
|
371
|
+
envVar: "CLAUDE_PLUGIN_ROOT",
|
|
372
|
+
displayName: "Claude Code"
|
|
373
|
+
});
|
|
374
|
+
break;
|
|
375
|
+
case "cursor":
|
|
376
|
+
await installToVendor(plugins, scope, repoPath, source, {
|
|
377
|
+
vendorDir: ".cursor-plugin",
|
|
378
|
+
cliCommand: "cursor",
|
|
379
|
+
envVar: "CURSOR_PLUGIN_ROOT",
|
|
380
|
+
displayName: "Cursor"
|
|
381
|
+
});
|
|
382
|
+
break;
|
|
383
|
+
default:
|
|
384
|
+
throw new Error(`Unsupported target: ${target.id}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async function installToVendor(plugins, scope, repoPath, source, vendor) {
|
|
388
|
+
const marketplaceName = plugins[0]?.marketplace ?? deriveMarketplaceName(source);
|
|
389
|
+
console.log(`
|
|
390
|
+
Preparing plugins for ${vendor.displayName}...`);
|
|
391
|
+
await prepareForVendor(plugins, repoPath, marketplaceName, vendor);
|
|
392
|
+
console.log(`Adding marketplace from local path: ${repoPath}`);
|
|
393
|
+
try {
|
|
394
|
+
const output = execSync2(`${vendor.cliCommand} plugin marketplace add ${repoPath}`, {
|
|
395
|
+
encoding: "utf-8",
|
|
396
|
+
stdio: "pipe"
|
|
397
|
+
});
|
|
398
|
+
console.log(" Marketplace added.");
|
|
399
|
+
} catch (err) {
|
|
400
|
+
const stderr = err.stderr?.toString().trim() ?? "";
|
|
401
|
+
const stdout = err.stdout?.toString().trim() ?? "";
|
|
402
|
+
if (stderr.includes("already") || stdout.includes("already")) {
|
|
403
|
+
console.log(" Marketplace already registered.");
|
|
404
|
+
} else {
|
|
405
|
+
console.error(`Failed to add marketplace: ${stderr || stdout}`);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
for (const plugin of plugins) {
|
|
410
|
+
const pluginRef = `${plugin.name}@${marketplaceName}`;
|
|
411
|
+
console.log(`
|
|
412
|
+
Installing ${pluginRef}...`);
|
|
413
|
+
try {
|
|
414
|
+
execSync2(`${vendor.cliCommand} plugin install ${pluginRef} --scope ${scope}`, {
|
|
415
|
+
encoding: "utf-8",
|
|
416
|
+
stdio: "pipe"
|
|
417
|
+
});
|
|
418
|
+
console.log(` Installed.`);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
const stderr = err.stderr?.toString().trim() ?? "";
|
|
421
|
+
const stdout = err.stdout?.toString().trim() ?? "";
|
|
422
|
+
if (stderr.includes("already") || stdout.includes("already")) {
|
|
423
|
+
console.log(` Already installed.`);
|
|
424
|
+
} else {
|
|
425
|
+
console.error(` Failed: ${stderr || stdout}`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function prepareForVendor(plugins, repoPath, marketplaceName, vendor) {
|
|
431
|
+
const vendorPluginDir = join3(repoPath, vendor.vendorDir);
|
|
432
|
+
await mkdir(vendorPluginDir, { recursive: true });
|
|
433
|
+
const marketplaceJson = {
|
|
434
|
+
name: marketplaceName,
|
|
435
|
+
owner: { name: "add-plugin" },
|
|
436
|
+
plugins: plugins.map((p) => {
|
|
437
|
+
const rel = relative(repoPath, p.path);
|
|
438
|
+
const sourcePath = rel === "" ? "./" : `./${rel}`;
|
|
439
|
+
const entry = {
|
|
440
|
+
name: p.name,
|
|
441
|
+
source: sourcePath,
|
|
442
|
+
description: p.description ?? ""
|
|
443
|
+
};
|
|
444
|
+
if (p.version) entry.version = p.version;
|
|
445
|
+
if (p.manifest?.author) entry.author = p.manifest.author;
|
|
446
|
+
if (p.manifest?.license) entry.license = p.manifest.license;
|
|
447
|
+
if (p.manifest?.keywords) entry.keywords = p.manifest.keywords;
|
|
448
|
+
return entry;
|
|
449
|
+
})
|
|
450
|
+
};
|
|
451
|
+
await writeFile(
|
|
452
|
+
join3(vendorPluginDir, "marketplace.json"),
|
|
453
|
+
JSON.stringify(marketplaceJson, null, 2)
|
|
454
|
+
);
|
|
455
|
+
console.log(` Generated ${vendor.vendorDir}/marketplace.json`);
|
|
456
|
+
for (const plugin of plugins) {
|
|
457
|
+
await preparePluginDir(plugin, vendor);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
async function preparePluginDir(plugin, vendor) {
|
|
461
|
+
const pluginPath = plugin.path;
|
|
462
|
+
const openPluginDir = join3(pluginPath, ".plugin");
|
|
463
|
+
const vendorPluginDir = join3(pluginPath, vendor.vendorDir);
|
|
464
|
+
const hasOpenPlugin = existsSync2(join3(openPluginDir, "plugin.json"));
|
|
465
|
+
const hasVendorPlugin = existsSync2(join3(vendorPluginDir, "plugin.json"));
|
|
466
|
+
if (hasOpenPlugin && !hasVendorPlugin) {
|
|
467
|
+
await cp(openPluginDir, vendorPluginDir, { recursive: true });
|
|
468
|
+
console.log(` ${plugin.name}: translated .plugin/ -> ${vendor.vendorDir}/`);
|
|
469
|
+
}
|
|
470
|
+
if (!hasOpenPlugin && !hasVendorPlugin) {
|
|
471
|
+
await mkdir(vendorPluginDir, { recursive: true });
|
|
472
|
+
await writeFile(
|
|
473
|
+
join3(vendorPluginDir, "plugin.json"),
|
|
474
|
+
JSON.stringify(
|
|
475
|
+
{
|
|
476
|
+
name: plugin.name,
|
|
477
|
+
description: plugin.description ?? "",
|
|
478
|
+
version: plugin.version ?? "0.0.0"
|
|
479
|
+
},
|
|
480
|
+
null,
|
|
481
|
+
2
|
|
482
|
+
)
|
|
483
|
+
);
|
|
484
|
+
console.log(` ${plugin.name}: generated ${vendor.vendorDir}/plugin.json`);
|
|
485
|
+
}
|
|
486
|
+
await translateEnvVars(pluginPath, plugin.name, vendor);
|
|
487
|
+
}
|
|
488
|
+
async function translateEnvVars(pluginPath, pluginName, vendor) {
|
|
489
|
+
const configFiles = [
|
|
490
|
+
join3(pluginPath, "hooks", "hooks.json"),
|
|
491
|
+
join3(pluginPath, ".mcp.json"),
|
|
492
|
+
join3(pluginPath, ".lsp.json")
|
|
493
|
+
];
|
|
494
|
+
for (const filePath of configFiles) {
|
|
495
|
+
if (!existsSync2(filePath)) continue;
|
|
496
|
+
let content = await readFile2(filePath, "utf-8");
|
|
497
|
+
if (content.includes("${PLUGIN_ROOT}")) {
|
|
498
|
+
content = content.replaceAll("${PLUGIN_ROOT}", `\${${vendor.envVar}}`);
|
|
499
|
+
await writeFile(filePath, content);
|
|
500
|
+
console.log(
|
|
501
|
+
` ${pluginName}: translated \${PLUGIN_ROOT} -> \${${vendor.envVar}} in ${filePath.split("/").pop()}`
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function deriveMarketplaceName(source) {
|
|
507
|
+
if (source.match(/^[\w-]+\/[\w.-]+$/)) {
|
|
508
|
+
return source.replace("/", "-");
|
|
509
|
+
}
|
|
510
|
+
try {
|
|
511
|
+
const url = new URL(source);
|
|
512
|
+
const parts2 = url.pathname.replace(/\.git$/, "").split("/").filter(Boolean);
|
|
513
|
+
if (parts2.length >= 2) {
|
|
514
|
+
return `${parts2[parts2.length - 2]}-${parts2[parts2.length - 1]}`;
|
|
515
|
+
}
|
|
516
|
+
} catch {
|
|
517
|
+
}
|
|
518
|
+
const parts = source.replace(/\/$/, "").split("/");
|
|
519
|
+
return parts[parts.length - 1] ?? "add-plugin";
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// index.ts
|
|
523
|
+
var { values, positionals } = parseArgs({
|
|
524
|
+
args: process.argv.slice(2),
|
|
525
|
+
options: {
|
|
526
|
+
help: { type: "boolean", short: "h" },
|
|
527
|
+
target: { type: "string", short: "t" },
|
|
528
|
+
scope: { type: "string", short: "s", default: "user" },
|
|
529
|
+
yes: { type: "boolean", short: "y" }
|
|
530
|
+
},
|
|
531
|
+
allowPositionals: true,
|
|
532
|
+
strict: true
|
|
533
|
+
});
|
|
534
|
+
var [command, ...rest] = positionals;
|
|
535
|
+
if (values.help || !command) {
|
|
536
|
+
printUsage();
|
|
537
|
+
process.exit(0);
|
|
538
|
+
}
|
|
539
|
+
switch (command) {
|
|
540
|
+
case "discover":
|
|
541
|
+
await cmdDiscover(rest[0]);
|
|
542
|
+
break;
|
|
543
|
+
case "targets":
|
|
544
|
+
await cmdTargets();
|
|
545
|
+
break;
|
|
546
|
+
case "install":
|
|
547
|
+
await cmdInstall(rest[0], values);
|
|
548
|
+
break;
|
|
549
|
+
default:
|
|
550
|
+
await cmdInstall(command, values);
|
|
551
|
+
}
|
|
552
|
+
function printUsage() {
|
|
553
|
+
console.log(`
|
|
554
|
+
add-plugin - Install open-plugin format plugins into agent tools
|
|
555
|
+
|
|
556
|
+
Usage:
|
|
557
|
+
add-plugin discover <repo-path-or-url> Discover plugins in a repo
|
|
558
|
+
add-plugin targets List available install targets
|
|
559
|
+
add-plugin install <repo-path-or-url> Install plugins from a repo
|
|
560
|
+
add-plugin <repo-path-or-url> Shorthand for install
|
|
561
|
+
|
|
562
|
+
Options:
|
|
563
|
+
-t, --target <target> Target tool (e.g. claude-code). Default: auto-detect
|
|
564
|
+
-s, --scope <scope> Install scope: user, project, local. Default: user
|
|
565
|
+
-y, --yes Skip confirmation prompts
|
|
566
|
+
-h, --help Show this help
|
|
567
|
+
`);
|
|
568
|
+
}
|
|
569
|
+
async function cmdDiscover(source) {
|
|
570
|
+
if (!source) {
|
|
571
|
+
console.error("Error: provide a repo path or URL");
|
|
572
|
+
process.exit(1);
|
|
573
|
+
}
|
|
574
|
+
const repoPath = resolveSource(source);
|
|
575
|
+
const plugins = await discover(repoPath);
|
|
576
|
+
if (plugins.length === 0) {
|
|
577
|
+
console.log("No plugins found.");
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
console.log(`Found ${plugins.length} plugin(s) in ${source}:
|
|
581
|
+
`);
|
|
582
|
+
for (const p of plugins) {
|
|
583
|
+
printPlugin(p);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async function cmdTargets() {
|
|
587
|
+
const targets = await getTargets();
|
|
588
|
+
if (targets.length === 0) {
|
|
589
|
+
console.log("No supported targets detected.");
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
console.log("Available install targets:\n");
|
|
593
|
+
for (const t of targets) {
|
|
594
|
+
console.log(` ${t.name}`);
|
|
595
|
+
console.log(` ${t.description}`);
|
|
596
|
+
console.log(` Config: ${t.configPath}`);
|
|
597
|
+
console.log(` Status: ${t.detected ? "detected" : "not found"}`);
|
|
598
|
+
console.log();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
async function cmdInstall(source, opts) {
|
|
602
|
+
if (!source) {
|
|
603
|
+
console.error("Error: provide a repo path or URL");
|
|
604
|
+
process.exit(1);
|
|
605
|
+
}
|
|
606
|
+
const repoPath = resolveSource(source);
|
|
607
|
+
const plugins = await discover(repoPath);
|
|
608
|
+
if (plugins.length === 0) {
|
|
609
|
+
console.log("No plugins found.");
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const targets = await getTargets();
|
|
613
|
+
const detectedTargets = targets.filter((t) => t.detected);
|
|
614
|
+
let target;
|
|
615
|
+
if (opts.target) {
|
|
616
|
+
const found = targets.find((t) => t.id === opts.target);
|
|
617
|
+
if (!found) {
|
|
618
|
+
console.error(`Unknown target: ${opts.target}`);
|
|
619
|
+
console.error(`Available: ${targets.map((t) => t.id).join(", ")}`);
|
|
620
|
+
process.exit(1);
|
|
621
|
+
}
|
|
622
|
+
target = found;
|
|
623
|
+
} else if (detectedTargets.length === 1) {
|
|
624
|
+
target = detectedTargets[0];
|
|
625
|
+
} else if (detectedTargets.length === 0) {
|
|
626
|
+
console.error("No supported targets detected. Use --target to specify one.");
|
|
627
|
+
process.exit(1);
|
|
628
|
+
} else {
|
|
629
|
+
console.log(`Multiple targets detected: ${detectedTargets.map((t) => t.id).join(", ")}`);
|
|
630
|
+
console.log(`Using: ${detectedTargets[0].id} (use --target to override)
|
|
631
|
+
`);
|
|
632
|
+
target = detectedTargets[0];
|
|
633
|
+
}
|
|
634
|
+
console.log(`Found ${plugins.length} plugin(s):
|
|
635
|
+
`);
|
|
636
|
+
for (const p of plugins) {
|
|
637
|
+
printPlugin(p);
|
|
638
|
+
}
|
|
639
|
+
console.log(`Target: ${target.name}`);
|
|
640
|
+
console.log(`Scope: ${opts.scope ?? "user"}
|
|
641
|
+
`);
|
|
642
|
+
if (!opts.yes) {
|
|
643
|
+
const response = await readLine("Install? [y/N] ");
|
|
644
|
+
if (response.trim().toLowerCase() !== "y") {
|
|
645
|
+
console.log("Aborted.");
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
const scope = opts.scope ?? "user";
|
|
650
|
+
await installPlugins(plugins, target, scope, repoPath, source);
|
|
651
|
+
console.log("\nDone. Restart your agent tool to load the plugins.");
|
|
652
|
+
}
|
|
653
|
+
function printPlugin(p) {
|
|
654
|
+
console.log(` ${p.name} (v${p.version ?? "0.0.0"})`);
|
|
655
|
+
if (p.description) console.log(` ${p.description}`);
|
|
656
|
+
const parts = [];
|
|
657
|
+
if (p.skills.length) parts.push(`${p.skills.length} skill(s)`);
|
|
658
|
+
if (p.commands.length) parts.push(`${p.commands.length} command(s)`);
|
|
659
|
+
if (p.agents.length) parts.push(`${p.agents.length} agent(s)`);
|
|
660
|
+
if (p.rules.length) parts.push(`${p.rules.length} rule(s)`);
|
|
661
|
+
if (p.hasHooks) parts.push("hooks");
|
|
662
|
+
if (p.hasMcp) parts.push("MCP servers");
|
|
663
|
+
if (p.hasLsp) parts.push("LSP servers");
|
|
664
|
+
if (parts.length) console.log(` Components: ${parts.join(", ")}`);
|
|
665
|
+
console.log();
|
|
666
|
+
}
|
|
667
|
+
function resolveSource(source) {
|
|
668
|
+
if (source.startsWith("https://") || source.startsWith("git@") || source.match(/^[\w-]+\/[\w.-]+$/)) {
|
|
669
|
+
const url = source.match(/^[\w-]+\/[\w.-]+$/) ? `https://github.com/${source}` : source;
|
|
670
|
+
const cacheDir = join4(process.env.HOME ?? "~", ".cache", "add-plugin");
|
|
671
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
672
|
+
const tmpDir = join4(cacheDir, encodeURIComponent(url));
|
|
673
|
+
if (existsSync3(join4(tmpDir, ".git", "HEAD"))) {
|
|
674
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
675
|
+
}
|
|
676
|
+
console.log(`Cloning ${url}...`);
|
|
677
|
+
execSync3(`git clone --depth 1 -q ${url} ${tmpDir}`, { stdio: "inherit" });
|
|
678
|
+
return tmpDir;
|
|
679
|
+
}
|
|
680
|
+
return resolve(source);
|
|
681
|
+
}
|
|
682
|
+
function readLine(prompt) {
|
|
683
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
684
|
+
return new Promise((resolve2) => {
|
|
685
|
+
rl.question(prompt, (answer) => {
|
|
686
|
+
rl.close();
|
|
687
|
+
resolve2(answer);
|
|
688
|
+
});
|
|
689
|
+
});
|
|
690
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "add-plugin",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Install open-plugin format plugins into agent tools",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"add-plugin": "dist/index.js"
|
|
6
8
|
},
|
|
7
|
-
"
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
8
12
|
"scripts": {
|
|
9
|
-
"
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"start": "node dist/index.js"
|
|
10
15
|
},
|
|
11
|
-
"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"tsup": "^8",
|
|
18
|
+
"typescript": "^5"
|
|
19
|
+
}
|
|
15
20
|
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
console.log("Hello World");
|