agkit 0.7.0

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,1422 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync } from "fs";
5
+ import { Command } from "commander";
6
+
7
+ // src/commands/add.ts
8
+ import fs6 from "fs";
9
+ import path7 from "path";
10
+ import * as p3 from "@clack/prompts";
11
+ import pc2 from "picocolors";
12
+
13
+ // src/lib/constants.ts
14
+ var MARKETPLACE_SCHEMA_URL = "https://json.schemastore.org/claude-code-marketplace.json";
15
+ var MARKETPLACE_DIR = ".claude-plugin";
16
+ var MARKETPLACE_FILE = "marketplace.json";
17
+ var PLUGIN_MANIFEST_FILE = "plugin.json";
18
+ var DEFAULT_PLUGIN_ROOT = "./plugins";
19
+ var RESERVED_MARKETPLACE_NAMES = [
20
+ "claude-code-marketplace",
21
+ "claude-code-plugins",
22
+ "claude-plugins-official",
23
+ "anthropic-marketplace",
24
+ "anthropic-plugins"
25
+ ];
26
+ var KEBAB_CASE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
27
+ var PLUGIN_TEMPLATES = ["skill", "command", "agent", "hook", "mcp"];
28
+ var TEMPLATE_DESCRIPTIONS = {
29
+ skill: "Plugin exposing an Agent Skill (SKILL.md): reference knowledge or a repeatable procedure Claude loads on demand.",
30
+ command: "Plugin exposing a slash command (commands/<name>.md): a prompt shortcut invoked as /<plugin>:<name>.",
31
+ agent: "Plugin exposing a subagent (agents/<name>.md): a named specialist with its own role and instructions.",
32
+ hook: "Plugin exposing an event handler (hooks/hooks.json + script): deterministic automation on Claude Code lifecycle events.",
33
+ mcp: "Plugin bundling an MCP server (.mcp.json + zero-dependency Node stdio server): external tools for Claude."
34
+ };
35
+
36
+ // src/lib/fsutils.ts
37
+ import fs from "fs";
38
+ import path from "path";
39
+ import { fileURLToPath } from "url";
40
+ function packageRoot() {
41
+ const here = path.dirname(fileURLToPath(import.meta.url));
42
+ let dir = here;
43
+ for (let i = 0; i < 5; i++) {
44
+ if (fs.existsSync(path.join(dir, "package.json"))) return dir;
45
+ dir = path.dirname(dir);
46
+ }
47
+ throw new Error("Unable to locate package root");
48
+ }
49
+ function templatesDir() {
50
+ return path.join(packageRoot(), "templates");
51
+ }
52
+ function renderTemplate(content, vars) {
53
+ return content.replace(
54
+ /\{\{([a-zA-Z0-9_]+)\}\}/g,
55
+ (match, key) => key in vars ? vars[key] : match
56
+ );
57
+ }
58
+ function copyTemplateDir(srcDir, destDir, vars) {
59
+ const written = [];
60
+ fs.mkdirSync(destDir, { recursive: true });
61
+ for (const entry of fs.readdirSync(srcDir, { withFileTypes: true })) {
62
+ const renderedName = renderTemplate(entry.name, vars);
63
+ const srcPath = path.join(srcDir, entry.name);
64
+ if (entry.isDirectory()) {
65
+ written.push(
66
+ ...copyTemplateDir(srcPath, path.join(destDir, renderedName), vars)
67
+ );
68
+ } else if (renderedName.endsWith(".tpl")) {
69
+ const destPath = path.join(destDir, renderedName.slice(0, -4));
70
+ const content = renderTemplate(fs.readFileSync(srcPath, "utf8"), vars);
71
+ fs.writeFileSync(destPath, content, { mode: fs.statSync(srcPath).mode });
72
+ written.push(destPath);
73
+ } else {
74
+ const destPath = path.join(destDir, renderedName);
75
+ fs.copyFileSync(srcPath, destPath);
76
+ fs.chmodSync(destPath, fs.statSync(srcPath).mode);
77
+ written.push(destPath);
78
+ }
79
+ }
80
+ return written;
81
+ }
82
+ function readJson(filePath) {
83
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
84
+ }
85
+ function writeJson(filePath, data) {
86
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
87
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
88
+ }
89
+
90
+ // src/lib/templates.ts
91
+ import { execFileSync } from "child_process";
92
+ import fs2 from "fs";
93
+ import os from "os";
94
+ import path2 from "path";
95
+ function parseGitSpec(spec) {
96
+ let ref;
97
+ let body = spec;
98
+ const hash = body.lastIndexOf("#");
99
+ if (hash > 0) {
100
+ ref = body.slice(hash + 1) || void 0;
101
+ body = body.slice(0, hash);
102
+ }
103
+ const shorthand = body.match(/^(gh|gl):([^/]+)\/([^/]+)(?:\/(.+))?$/);
104
+ if (shorthand) {
105
+ const host = shorthand[1] === "gh" ? "github.com" : "gitlab.com";
106
+ return {
107
+ cloneUrl: `https://${host}/${shorthand[2]}/${shorthand[3]}.git`,
108
+ subdir: shorthand[4],
109
+ ref
110
+ };
111
+ }
112
+ const looksLikeUrl = /^(https?|ssh|git|file):\/\//.test(body) || /^[\w.-]+@[\w.-]+:/.test(body);
113
+ if (looksLikeUrl) {
114
+ let subdir;
115
+ const protoEnd = body.indexOf("://");
116
+ const sep = body.indexOf("//", protoEnd === -1 ? 0 : protoEnd + 3);
117
+ if (sep !== -1) {
118
+ subdir = body.slice(sep + 2);
119
+ body = body.slice(0, sep);
120
+ }
121
+ return { cloneUrl: body, subdir, ref };
122
+ }
123
+ return void 0;
124
+ }
125
+ function assertTemplateDir(dir, label) {
126
+ const hasManifest = fs2.existsSync(path2.join(dir, ".claude-plugin", "plugin.json.tpl")) || fs2.existsSync(path2.join(dir, ".claude-plugin", "plugin.json"));
127
+ if (!hasManifest) {
128
+ throw new Error(
129
+ `"${label}" is not a plugin template: expected .claude-plugin/plugin.json(.tpl) in ${dir}`
130
+ );
131
+ }
132
+ }
133
+ function resolveTemplate(spec) {
134
+ if (PLUGIN_TEMPLATES.includes(spec)) {
135
+ return {
136
+ dir: path2.join(templatesDir(), "plugins", spec),
137
+ label: `built-in:${spec}`,
138
+ builtin: true
139
+ };
140
+ }
141
+ const localPath = spec.startsWith("path:") ? spec.slice(5) : spec.startsWith("./") || spec.startsWith("../") || path2.isAbsolute(spec) ? spec : void 0;
142
+ if (localPath !== void 0) {
143
+ const dir2 = path2.resolve(localPath);
144
+ if (!fs2.existsSync(dir2)) throw new Error(`Template directory not found: ${dir2}`);
145
+ assertTemplateDir(dir2, spec);
146
+ return { dir: dir2, label: spec, builtin: false };
147
+ }
148
+ const git2 = parseGitSpec(spec);
149
+ if (!git2) {
150
+ throw new Error(
151
+ `Unrecognized template "${spec}". Use a built-in name (${PLUGIN_TEMPLATES.join(", ")}), a local path, gh:owner/repo[/dir][#ref], gl:owner/repo[/dir][#ref], or a git URL.`
152
+ );
153
+ }
154
+ const tmp = fs2.mkdtempSync(path2.join(os.tmpdir(), "agkit-tpl-"));
155
+ const args = ["clone", "--depth", "1"];
156
+ if (git2.ref) args.push("--branch", git2.ref);
157
+ args.push(git2.cloneUrl, tmp);
158
+ try {
159
+ execFileSync("git", args, { stdio: "pipe" });
160
+ } catch (err) {
161
+ fs2.rmSync(tmp, { recursive: true, force: true });
162
+ const msg = err.stderr?.toString().trim();
163
+ throw new Error(`git clone failed for ${git2.cloneUrl}${msg ? `:
164
+ ${msg}` : ""}`);
165
+ }
166
+ const dir = git2.subdir ? path2.join(tmp, git2.subdir) : tmp;
167
+ if (!fs2.existsSync(dir)) {
168
+ fs2.rmSync(tmp, { recursive: true, force: true });
169
+ throw new Error(`Subdirectory "${git2.subdir}" not found in ${git2.cloneUrl}`);
170
+ }
171
+ fs2.rmSync(path2.join(tmp, ".git"), { recursive: true, force: true });
172
+ try {
173
+ assertTemplateDir(dir, spec);
174
+ } catch (err) {
175
+ fs2.rmSync(tmp, { recursive: true, force: true });
176
+ throw err;
177
+ }
178
+ return { dir, label: spec, cleanup: tmp, builtin: false };
179
+ }
180
+
181
+ // src/lib/marketplace.ts
182
+ import fs3 from "fs";
183
+ import path3 from "path";
184
+ function marketplacePath(root) {
185
+ return path3.join(root, MARKETPLACE_DIR, MARKETPLACE_FILE);
186
+ }
187
+ function findMarketplaceRoot(startDir) {
188
+ let dir = path3.resolve(startDir);
189
+ for (; ; ) {
190
+ if (fs3.existsSync(marketplacePath(dir))) return dir;
191
+ const parent = path3.dirname(dir);
192
+ if (parent === dir) return void 0;
193
+ dir = parent;
194
+ }
195
+ }
196
+ function readMarketplace(root) {
197
+ return readJson(marketplacePath(root));
198
+ }
199
+ function writeMarketplace(root, data) {
200
+ writeJson(marketplacePath(root), data);
201
+ }
202
+ function pluginRootDir(root, mp) {
203
+ const rel = mp.metadata?.pluginRoot ?? DEFAULT_PLUGIN_ROOT;
204
+ return path3.resolve(root, rel);
205
+ }
206
+ function resolveLocalPluginDir(root, mp, entry) {
207
+ if (typeof entry.source !== "string") return void 0;
208
+ if (entry.source.startsWith("./") || entry.source.startsWith("../")) {
209
+ return path3.resolve(root, entry.source);
210
+ }
211
+ return path3.join(pluginRootDir(root, mp), entry.source);
212
+ }
213
+ function scanLocalPlugins(root, mp) {
214
+ const base = pluginRootDir(root, mp);
215
+ if (!fs3.existsSync(base)) return [];
216
+ const result = [];
217
+ for (const entry of fs3.readdirSync(base, { withFileTypes: true })) {
218
+ if (!entry.isDirectory()) continue;
219
+ const manifestPath = path3.join(
220
+ base,
221
+ entry.name,
222
+ MARKETPLACE_DIR,
223
+ PLUGIN_MANIFEST_FILE
224
+ );
225
+ if (!fs3.existsSync(manifestPath)) continue;
226
+ result.push({
227
+ dir: path3.join(base, entry.name),
228
+ dirName: entry.name,
229
+ manifest: readJson(manifestPath)
230
+ });
231
+ }
232
+ return result;
233
+ }
234
+
235
+ // src/commands/sync.ts
236
+ import fs5 from "fs";
237
+ import path6 from "path";
238
+ import * as p2 from "@clack/prompts";
239
+
240
+ // src/commands/build.ts
241
+ import fs4 from "fs";
242
+ import path5 from "path";
243
+ import * as p from "@clack/prompts";
244
+ import pc from "picocolors";
245
+
246
+ // src/lib/build-targets.ts
247
+ import path4 from "path";
248
+ function pluginRootRel(mp) {
249
+ const raw = mp.metadata?.pluginRoot ?? DEFAULT_PLUGIN_ROOT;
250
+ return raw.replace(/^\.\//, "").replace(/\/+$/, "");
251
+ }
252
+ function sourcePath(mp, dirName) {
253
+ return `./${path4.posix.join(pluginRootRel(mp), dirName)}`;
254
+ }
255
+ function json(value) {
256
+ return JSON.stringify(value, null, 2) + "\n";
257
+ }
258
+ function manifestCopy(local) {
259
+ return json(local.manifest);
260
+ }
261
+ function generateCursor(ctx) {
262
+ const { mp, plugins } = ctx;
263
+ const registry = {
264
+ name: mp.name,
265
+ owner: mp.owner,
266
+ ...mp.metadata ? {
267
+ metadata: {
268
+ ...mp.metadata.description ? { description: mp.metadata.description } : {},
269
+ ...mp.metadata.version ? { version: mp.metadata.version } : {}
270
+ }
271
+ } : {},
272
+ plugins: plugins.map((p8) => ({
273
+ name: p8.manifest.name || p8.dirName,
274
+ source: sourcePath(mp, p8.dirName)
275
+ }))
276
+ };
277
+ const files = [
278
+ { relPath: path4.posix.join(".cursor-plugin", "marketplace.json"), content: json(registry) }
279
+ ];
280
+ for (const p8 of plugins) {
281
+ files.push({
282
+ relPath: path4.posix.join(pluginRootRel(mp), p8.dirName, ".cursor-plugin", "plugin.json"),
283
+ content: manifestCopy(p8)
284
+ });
285
+ }
286
+ return files;
287
+ }
288
+ function generateCodex(ctx) {
289
+ const { mp, plugins } = ctx;
290
+ const registry = {
291
+ name: mp.name,
292
+ plugins: plugins.map((p8) => ({
293
+ name: p8.manifest.name || p8.dirName,
294
+ source: { source: "local", path: sourcePath(mp, p8.dirName) },
295
+ policy: { installation: "AVAILABLE", authentication: "ON_INSTALL" },
296
+ category: "Productivity"
297
+ }))
298
+ };
299
+ const files = [
300
+ {
301
+ relPath: path4.posix.join(".agents", "plugins", "marketplace.json"),
302
+ content: json(registry)
303
+ }
304
+ ];
305
+ for (const p8 of plugins) {
306
+ files.push({
307
+ relPath: path4.posix.join(pluginRootRel(mp), p8.dirName, ".codex-plugin", "plugin.json"),
308
+ content: manifestCopy(p8)
309
+ });
310
+ }
311
+ return files;
312
+ }
313
+ var TIER2_GENERATORS = {
314
+ cursor: generateCursor,
315
+ codex: generateCodex
316
+ };
317
+ var TIER2_ROOT_REGISTRY = {
318
+ cursor: path4.posix.join(".cursor-plugin", "marketplace.json"),
319
+ codex: path4.posix.join(".agents", "plugins", "marketplace.json")
320
+ };
321
+ function isTier2Buildable(id) {
322
+ return id in TIER2_GENERATORS;
323
+ }
324
+
325
+ // src/commands/build.ts
326
+ function resolveTargets(mp, requested) {
327
+ if (requested) {
328
+ const ids2 = [];
329
+ const unknown = [];
330
+ for (const t of requested.split(",").map((s) => s.trim()).filter(Boolean)) {
331
+ if (isTier2Buildable(t)) ids2.push(t);
332
+ else unknown.push(t);
333
+ }
334
+ return { ids: ids2, unknown };
335
+ }
336
+ const ids = (mp.metadata?.targets ?? []).filter(isTier2Buildable);
337
+ return { ids, unknown: [] };
338
+ }
339
+ function computeFiles(root, mp, ids) {
340
+ const ctx = { mp, plugins: scanLocalPlugins(root, mp) };
341
+ const files = [];
342
+ for (const id of ids) {
343
+ const gen = TIER2_GENERATORS[id];
344
+ if (gen) files.push(...gen(ctx));
345
+ }
346
+ return files;
347
+ }
348
+ async function buildCommand(startDir, opts = {}) {
349
+ const root = findMarketplaceRoot(startDir);
350
+ if (!root) {
351
+ p.log.error("No .claude-plugin/marketplace.json found. Run `agkit init` first.");
352
+ process.exitCode = 1;
353
+ return;
354
+ }
355
+ const mp = readMarketplace(root);
356
+ const { ids, unknown } = resolveTargets(mp, opts.targets);
357
+ if (unknown.length > 0) {
358
+ p.log.warn(
359
+ `Not tier-2 buildable (ignored): ${unknown.join(", ")}. Buildable: ${Object.keys(TIER2_GENERATORS).join(", ")}.`
360
+ );
361
+ }
362
+ if (ids.length === 0) {
363
+ if (!opts.quiet) {
364
+ p.log.info(
365
+ "No tier-2 targets to build. Add one with `agkit init --agents \u2026` (codex, cursor) or pass `--target codex,cursor`."
366
+ );
367
+ }
368
+ return;
369
+ }
370
+ const files = computeFiles(root, mp, ids);
371
+ if (opts.check) {
372
+ const drift = [];
373
+ for (const f of files) {
374
+ const abs = path5.join(root, f.relPath);
375
+ let actual;
376
+ try {
377
+ actual = fs4.readFileSync(abs, "utf8");
378
+ } catch {
379
+ actual = void 0;
380
+ }
381
+ if (actual === void 0) drift.push(`missing: ${f.relPath}`);
382
+ else if (actual !== f.content) drift.push(`stale: ${f.relPath}`);
383
+ }
384
+ if (drift.length > 0) {
385
+ p.log.error(
386
+ `Tier-2 artifacts out of date (${ids.join(", ")}):
387
+ ${drift.join("\n ")}
388
+ Run \`agkit build\` to regenerate.`
389
+ );
390
+ process.exitCode = 1;
391
+ } else if (!opts.quiet) {
392
+ p.log.success(`Tier-2 artifacts up to date (${ids.join(", ")}).`);
393
+ }
394
+ return;
395
+ }
396
+ for (const f of files) {
397
+ const abs = path5.join(root, f.relPath);
398
+ fs4.mkdirSync(path5.dirname(abs), { recursive: true });
399
+ fs4.writeFileSync(abs, f.content);
400
+ }
401
+ if (!opts.quiet) {
402
+ const registries = ids.map((id) => TIER2_ROOT_REGISTRY[id]).filter(Boolean).join(", ");
403
+ p.log.success(
404
+ `Built ${files.length} file(s) for ${pc.cyan(ids.join(", "))}.
405
+ Registries: ${registries}
406
+ Commit the generated files so consumers can install from these agents.`
407
+ );
408
+ }
409
+ }
410
+ function refreshBuiltTargets(root, mp) {
411
+ const built = (mp.metadata?.targets ?? []).filter(isTier2Buildable).filter((id) => fs4.existsSync(path5.join(root, TIER2_ROOT_REGISTRY[id])));
412
+ if (built.length === 0) return [];
413
+ const files = computeFiles(root, mp, built);
414
+ for (const f of files) {
415
+ const abs = path5.join(root, f.relPath);
416
+ fs4.mkdirSync(path5.dirname(abs), { recursive: true });
417
+ fs4.writeFileSync(abs, f.content);
418
+ }
419
+ return built;
420
+ }
421
+
422
+ // src/commands/sync.ts
423
+ var README_START = "<!-- agkit:plugins:start -->";
424
+ var README_END = "<!-- agkit:plugins:end -->";
425
+ async function syncCommand(startDir, opts = {}) {
426
+ const root = findMarketplaceRoot(startDir);
427
+ if (!root) {
428
+ p2.log.error("No .claude-plugin/marketplace.json found. Run `agkit init` first.");
429
+ process.exitCode = 1;
430
+ return;
431
+ }
432
+ const mp = readMarketplace(root);
433
+ const locals = scanLocalPlugins(root, mp);
434
+ const changes = [];
435
+ for (const local of locals) {
436
+ const manifest = local.manifest;
437
+ const entryName = manifest.name || local.dirName;
438
+ let entry = mp.plugins.find((e) => e.name === entryName);
439
+ if (!entry) {
440
+ entry = {
441
+ name: entryName,
442
+ source: mp.metadata?.pluginRoot ? local.dirName : `./${path6.relative(root, local.dir).split(path6.sep).join("/")}`
443
+ };
444
+ mp.plugins.push(entry);
445
+ changes.push(`+ added "${entryName}" to the catalog`);
446
+ }
447
+ for (const field of ["version", "description", "author", "keywords", "homepage"]) {
448
+ const value = manifest[field];
449
+ if (value !== void 0 && JSON.stringify(entry[field]) !== JSON.stringify(value)) {
450
+ entry[field] = value;
451
+ changes.push(`~ updated ${field} of "${entryName}"`);
452
+ }
453
+ }
454
+ }
455
+ for (const entry of mp.plugins) {
456
+ const dir = resolveLocalPluginDir(root, mp, entry);
457
+ if (dir && !fs5.existsSync(dir)) {
458
+ changes.push(
459
+ `! "${entry.name}" points to missing directory ${path6.relative(root, dir)} \u2014 remove the entry or restore the plugin`
460
+ );
461
+ }
462
+ }
463
+ mp.plugins.sort((a, b) => a.name.localeCompare(b.name));
464
+ writeMarketplace(root, mp);
465
+ const refreshed = refreshBuiltTargets(root, mp);
466
+ for (const id of refreshed) changes.push(`~ refreshed ${id} tier-2 artifacts`);
467
+ const readmePath = path6.join(root, "README.md");
468
+ if (fs5.existsSync(readmePath)) {
469
+ const readme = fs5.readFileSync(readmePath, "utf8");
470
+ const start = readme.indexOf(README_START);
471
+ const end = readme.indexOf(README_END);
472
+ if (start !== -1 && end !== -1 && end > start) {
473
+ const table = mp.plugins.length === 0 ? "_No plugins yet. Add one with `agkit add <template> <name>`._" : [
474
+ "| Plugin | Version | Description |",
475
+ "| :----- | :------ | :---------- |",
476
+ ...mp.plugins.map(
477
+ (e) => `| \`${e.name}\` | ${e.version ?? "\u2014"} | ${e.description ?? ""} |`
478
+ )
479
+ ].join("\n");
480
+ const updated = readme.slice(0, start + README_START.length) + "\n" + table + "\n" + readme.slice(end);
481
+ if (updated !== readme) {
482
+ fs5.writeFileSync(readmePath, updated);
483
+ changes.push("~ refreshed plugin list in README.md");
484
+ }
485
+ }
486
+ }
487
+ if (!opts.quiet) {
488
+ if (changes.length === 0) {
489
+ p2.log.success("Catalog already in sync.");
490
+ } else {
491
+ p2.log.success(`Sync complete:
492
+ ${changes.join("\n ")}`);
493
+ if (changes.some((c) => c.startsWith("!"))) process.exitCode = 1;
494
+ }
495
+ }
496
+ }
497
+
498
+ // src/commands/add.ts
499
+ function titleCase(kebab) {
500
+ return kebab.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
501
+ }
502
+ async function addPlugin(startDir, templateArg, nameArg, opts = {}) {
503
+ const root = findMarketplaceRoot(startDir);
504
+ if (!root) {
505
+ p3.log.error(
506
+ "No .claude-plugin/marketplace.json found here or in any parent directory. Run `agkit init` first."
507
+ );
508
+ process.exitCode = 1;
509
+ return;
510
+ }
511
+ let templateSpec = templateArg;
512
+ if (!templateSpec) {
513
+ const answer = await p3.select({
514
+ message: "Plugin template",
515
+ options: PLUGIN_TEMPLATES.map((t) => ({
516
+ value: t,
517
+ label: t,
518
+ hint: TEMPLATE_DESCRIPTIONS[t]
519
+ }))
520
+ });
521
+ if (p3.isCancel(answer)) return cancel2();
522
+ templateSpec = answer;
523
+ }
524
+ let name = nameArg;
525
+ if (!name) {
526
+ const answer = await p3.text({
527
+ message: "Plugin name (kebab-case)",
528
+ placeholder: "my-plugin",
529
+ validate: (v) => KEBAB_CASE_RE.test(v) ? void 0 : "Must be kebab-case (lowercase, digits, hyphens)"
530
+ });
531
+ if (p3.isCancel(answer)) return cancel2();
532
+ name = answer;
533
+ }
534
+ if (!KEBAB_CASE_RE.test(name)) {
535
+ p3.log.error(`Plugin name "${name}" must be kebab-case.`);
536
+ process.exitCode = 1;
537
+ return;
538
+ }
539
+ const mp = readMarketplace(root);
540
+ if (mp.plugins.some((e) => e.name === name)) {
541
+ p3.log.error(`A plugin named "${name}" already exists in the catalog.`);
542
+ process.exitCode = 1;
543
+ return;
544
+ }
545
+ const destDir = path7.join(pluginRootDir(root, mp), name);
546
+ if (fs6.existsSync(destDir)) {
547
+ p3.log.error(`Directory already exists: ${destDir}`);
548
+ process.exitCode = 1;
549
+ return;
550
+ }
551
+ let description = opts.description;
552
+ if (description === void 0 && opts.interactive !== false) {
553
+ const answer = await p3.text({
554
+ message: "One-line description",
555
+ defaultValue: "",
556
+ placeholder: `What does ${name} do?`
557
+ });
558
+ if (p3.isCancel(answer)) return cancel2();
559
+ description = answer;
560
+ }
561
+ description = description || `TODO: describe what ${name} does.`;
562
+ const vars = {
563
+ pluginName: name,
564
+ pluginTitle: titleCase(name),
565
+ description,
566
+ authorName: mp.owner?.name ?? "Unknown"
567
+ };
568
+ let resolved;
569
+ try {
570
+ resolved = resolveTemplate(templateSpec);
571
+ } catch (err) {
572
+ p3.log.error(err.message);
573
+ process.exitCode = 1;
574
+ return;
575
+ }
576
+ try {
577
+ copyTemplateDir(resolved.dir, destDir, vars);
578
+ } finally {
579
+ if (resolved.cleanup) fs6.rmSync(resolved.cleanup, { recursive: true, force: true });
580
+ }
581
+ const manifestPath = path7.join(destDir, ".claude-plugin", "plugin.json");
582
+ if (!resolved.builtin && fs6.existsSync(manifestPath)) {
583
+ try {
584
+ const manifest = readJson(manifestPath);
585
+ manifest.name = name;
586
+ if (opts.description) manifest.description = description;
587
+ writeJson(manifestPath, manifest);
588
+ } catch {
589
+ p3.log.warn("Template plugin.json could not be parsed; left as-is.");
590
+ }
591
+ }
592
+ const template = resolved.label;
593
+ const source = mp.metadata?.pluginRoot ? name : `./${path7.relative(root, destDir).split(path7.sep).join("/")}`;
594
+ mp.plugins.push({ name, source, description, version: "0.1.0" });
595
+ writeMarketplace(root, mp);
596
+ await syncCommand(root, { quiet: true });
597
+ p3.log.success(
598
+ `Created ${pc2.cyan(path7.relative(startDir, destDir) || destDir)} (${template} template) and registered it in marketplace.json`
599
+ );
600
+ p3.log.info(
601
+ `Edit the generated files, then test locally:
602
+ claude
603
+ /plugin marketplace add ${root}
604
+ /plugin install ${name}@${mp.name}`
605
+ );
606
+ }
607
+ function cancel2() {
608
+ p3.cancel("Cancelled.");
609
+ process.exitCode = 1;
610
+ }
611
+
612
+ // src/commands/bump.ts
613
+ import { execFileSync as execFileSync2 } from "child_process";
614
+ import path8 from "path";
615
+ import * as p4 from "@clack/prompts";
616
+ import pc3 from "picocolors";
617
+ function git(root, args) {
618
+ return execFileSync2("git", args, {
619
+ cwd: root,
620
+ stdio: ["ignore", "pipe", "pipe"]
621
+ }).toString().trim();
622
+ }
623
+ function incrementSemver(version2, level) {
624
+ const m = version2.match(/^(\d+)\.(\d+)\.(\d+)/);
625
+ if (!m) throw new Error(`"${version2}" is not a semver version`);
626
+ const [major, minor, patch] = [Number(m[1]), Number(m[2]), Number(m[3])];
627
+ if (level === "major") return `${major + 1}.0.0`;
628
+ if (level === "minor") return `${major}.${minor + 1}.0`;
629
+ return `${major}.${minor}.${patch + 1}`;
630
+ }
631
+ function lastReleaseTag(root, plugin) {
632
+ try {
633
+ const tags = git(root, [
634
+ "tag",
635
+ "--list",
636
+ `${plugin}@*`,
637
+ "--sort=-v:refname"
638
+ ]);
639
+ return tags.split("\n").filter(Boolean)[0];
640
+ } catch {
641
+ return void 0;
642
+ }
643
+ }
644
+ function analyzeCommits(root, pluginDir, since) {
645
+ const range = since ? [`${since}..HEAD`] : [];
646
+ let raw = "";
647
+ try {
648
+ raw = git(root, [
649
+ "log",
650
+ ...range,
651
+ "--format=%H%x1f%s%x1f%b%x1e",
652
+ "--",
653
+ path8.relative(root, pluginDir) || "."
654
+ ]);
655
+ } catch {
656
+ return { level: void 0, commits: 0, reasons: ["not a git repository"] };
657
+ }
658
+ const commits = raw.split("").map((c) => c.trim()).filter(Boolean);
659
+ if (commits.length === 0) return { level: void 0, commits: 0, reasons: [] };
660
+ let level = "patch";
661
+ const reasons = [];
662
+ for (const c of commits) {
663
+ const [, subject = "", body = ""] = c.split("");
664
+ const breaking = /^[a-z]+(\([^)]*\))?!:/.test(subject) || /BREAKING[ -]CHANGE/.test(body);
665
+ const feat = /^feat(\([^)]*\))?!?:/.test(subject);
666
+ if (breaking) {
667
+ level = "major";
668
+ reasons.push(`major ${subject}`);
669
+ } else if (feat) {
670
+ if (level !== "major") level = "minor";
671
+ reasons.push(`minor ${subject}`);
672
+ } else {
673
+ reasons.push(`patch ${subject}`);
674
+ }
675
+ }
676
+ return { level, commits: commits.length, reasons };
677
+ }
678
+ async function bumpCommand(startDir, pluginName, levelArg, opts = {}) {
679
+ const root = findMarketplaceRoot(startDir);
680
+ if (!root) {
681
+ p4.log.error("No .claude-plugin/marketplace.json found. Run `agkit init` first.");
682
+ process.exitCode = 1;
683
+ return;
684
+ }
685
+ const mp = readMarketplace(root);
686
+ const locals = scanLocalPlugins(root, mp);
687
+ if (locals.length === 0) {
688
+ p4.log.error("No local plugins found under the plugin root.");
689
+ process.exitCode = 1;
690
+ return;
691
+ }
692
+ let name = pluginName;
693
+ if (!name) {
694
+ const answer = await p4.select({
695
+ message: "Plugin to bump",
696
+ options: locals.map((l) => ({
697
+ value: l.manifest.name || l.dirName,
698
+ label: `${l.manifest.name || l.dirName} (${l.manifest.version ?? "unversioned"})`
699
+ }))
700
+ });
701
+ if (p4.isCancel(answer)) {
702
+ p4.cancel("Cancelled.");
703
+ process.exitCode = 1;
704
+ return;
705
+ }
706
+ name = answer;
707
+ }
708
+ const local = locals.find((l) => (l.manifest.name || l.dirName) === name);
709
+ if (!local) {
710
+ p4.log.error(`No local plugin named "${name}". Known: ${locals.map((l) => l.manifest.name || l.dirName).join(", ")}`);
711
+ process.exitCode = 1;
712
+ return;
713
+ }
714
+ if (levelArg && !["major", "minor", "patch", "auto"].includes(levelArg)) {
715
+ p4.log.error(`Invalid level "${levelArg}". Use major, minor, patch, or auto.`);
716
+ process.exitCode = 1;
717
+ return;
718
+ }
719
+ const current = local.manifest.version ?? "0.1.0";
720
+ let level;
721
+ if (!levelArg || levelArg === "auto") {
722
+ const sinceTag = lastReleaseTag(root, name);
723
+ const analysis = analyzeCommits(root, local.dir, sinceTag);
724
+ if (!analysis.level) {
725
+ p4.log.info(
726
+ `No commits touching ${path8.relative(root, local.dir)}${sinceTag ? ` since ${sinceTag}` : ""} \u2014 nothing to bump.`
727
+ );
728
+ return;
729
+ }
730
+ level = analysis.level;
731
+ p4.log.info(
732
+ `${analysis.commits} commit(s)${sinceTag ? ` since ${pc3.cyan(sinceTag)}` : " (no previous release tag)"}:
733
+ ` + analysis.reasons.slice(0, 12).join("\n ") + (analysis.reasons.length > 12 ? `
734
+ \u2026 ${analysis.reasons.length - 12} more` : "")
735
+ );
736
+ } else {
737
+ level = levelArg;
738
+ }
739
+ const next = incrementSemver(current, level);
740
+ const tagName = `${name}@${next}`;
741
+ if (opts.dryRun) {
742
+ p4.log.info(`[dry-run] ${name}: ${current} -> ${pc3.green(next)} (${level})${opts.tag ? ` + tag ${tagName}` : ""}`);
743
+ return;
744
+ }
745
+ const manifestPath = path8.join(local.dir, MARKETPLACE_DIR, PLUGIN_MANIFEST_FILE);
746
+ const manifest = readJson(manifestPath);
747
+ manifest.version = next;
748
+ writeJson(manifestPath, manifest);
749
+ await syncCommand(root, { quiet: true });
750
+ p4.log.success(`${name}: ${current} -> ${pc3.green(next)} (${level})`);
751
+ if (opts.tag) {
752
+ try {
753
+ git(root, ["add", "-A"]);
754
+ git(root, ["commit", "-m", `chore(release): ${tagName}`]);
755
+ git(root, ["tag", tagName]);
756
+ p4.log.success(`Committed and tagged ${pc3.cyan(tagName)} \u2014 push with: git push --follow-tags`);
757
+ } catch (err) {
758
+ p4.log.warn(`Version bumped but commit/tag failed: ${err.message}`);
759
+ }
760
+ } else {
761
+ p4.log.info("Commit the change, then push. Use --tag to commit and tag in one step.");
762
+ }
763
+ }
764
+
765
+ // src/commands/init.ts
766
+ import fs8 from "fs";
767
+ import path10 from "path";
768
+ import * as p5 from "@clack/prompts";
769
+ import pc4 from "picocolors";
770
+
771
+ // src/lib/agents.ts
772
+ var claudeCode = {
773
+ id: "claude-code",
774
+ label: "Claude Code",
775
+ tier: 1,
776
+ install: (arg, name) => ({
777
+ lines: [`/plugin marketplace add ${arg}`, `/plugin install <plugin-name>@${name}`]
778
+ }),
779
+ teamSettings: { path: ".claude/settings.json" }
780
+ };
781
+ var copilot = {
782
+ id: "copilot",
783
+ label: "GitHub Copilot CLI",
784
+ tier: 1,
785
+ install: (arg, name) => ({
786
+ lines: [
787
+ `copilot plugin marketplace add ${arg}`,
788
+ `copilot plugin install <plugin-name>@${name}`
789
+ ],
790
+ note: "Copilot CLI and Copilot in VS Code read marketplace.json under .claude-plugin/ natively."
791
+ }),
792
+ teamSettings: { path: ".github/copilot/settings.json" }
793
+ };
794
+ var codex = {
795
+ id: "codex",
796
+ label: "OpenAI Codex CLI",
797
+ tier: 2,
798
+ install: (arg) => ({
799
+ lines: [`codex marketplace add ${arg}`],
800
+ note: "Requires a Codex registry (.agents/plugins/marketplace.json). Generate it with `agkit build --target codex` (planned); the Claude catalog alone is not read by Codex."
801
+ })
802
+ };
803
+ var cursor = {
804
+ id: "cursor",
805
+ label: "Cursor",
806
+ tier: 2,
807
+ install: () => ({
808
+ lines: ["# add the marketplace in Cursor, then: /plugin install <plugin-name>"],
809
+ note: "Requires a committed Cursor registry (.cursor-plugin/). Generate it with `agkit build --target cursor` (planned)."
810
+ })
811
+ };
812
+ var gemini = {
813
+ id: "gemini",
814
+ label: "Gemini CLI",
815
+ tier: 3,
816
+ install: () => ({
817
+ lines: ["gemini extensions install ."],
818
+ note: "Needs a transformed tree (gemini-extension.json + GEMINI.md) via `agkit build --target gemini` (planned). Note: Gemini CLI is being retired (2026-06-18) in favour of Antigravity CLI (`agy`)."
819
+ })
820
+ };
821
+ var opencode = {
822
+ id: "opencode",
823
+ label: "OpenCode",
824
+ tier: 3,
825
+ install: () => ({
826
+ lines: ["# clone the repo, then generate the OpenCode tree (.opencode/)"],
827
+ note: "Needs a transformed tree via `agkit build --target opencode` (planned). OpenCode reads AGENTS.md natively for context."
828
+ })
829
+ };
830
+ var kiro = {
831
+ id: "kiro",
832
+ label: "Kiro",
833
+ tier: 3,
834
+ install: () => ({
835
+ lines: ["# install the generated root artifact (POWER.md, .kiro/)"],
836
+ note: "Needs a transformed tree via `agkit build --target kiro` (planned)."
837
+ })
838
+ };
839
+ var AGENT_ADAPTERS = [
840
+ claudeCode,
841
+ copilot,
842
+ codex,
843
+ cursor,
844
+ gemini,
845
+ opencode,
846
+ kiro
847
+ ];
848
+ var DEFAULT_AGENTS = ["claude-code", "copilot"];
849
+ function getAdapter(id) {
850
+ return AGENT_ADAPTERS.find((a) => a.id === id);
851
+ }
852
+ function parseAgentList(raw) {
853
+ const ids = [];
854
+ const unknown = [];
855
+ for (const part of raw.split(",").map((s) => s.trim()).filter(Boolean)) {
856
+ if (getAdapter(part)) {
857
+ if (!ids.includes(part)) ids.push(part);
858
+ } else {
859
+ unknown.push(part);
860
+ }
861
+ }
862
+ return { ids, unknown };
863
+ }
864
+ function renderInstallSections(agentIds, marketplaceArg, marketplaceName) {
865
+ const blocks = [];
866
+ for (const id of agentIds) {
867
+ const a = getAdapter(id);
868
+ if (!a) continue;
869
+ const hint = a.install(marketplaceArg, marketplaceName);
870
+ const tierTag = a.tier === 1 ? "" : ` _(tier ${a.tier} \u2014 requires generation)_`;
871
+ blocks.push(
872
+ `**${a.label}**${tierTag}
873
+
874
+ \`\`\`
875
+ ${hint.lines.join("\n")}
876
+ \`\`\`` + (hint.note ? `
877
+
878
+ > ${hint.note}` : "")
879
+ );
880
+ }
881
+ return blocks.join("\n\n");
882
+ }
883
+ function renderTeamSections(agentIds, teamSourceBlock, marketplaceName) {
884
+ const supported = agentIds.map(getAdapter).filter((a) => Boolean(a?.teamSettings));
885
+ if (supported.length === 0) return "";
886
+ const paths = supported.map((a) => `- **${a.label}** \u2014 \`${a.teamSettings.path}\``).join("\n");
887
+ return `Register the marketplace so teammates get it automatically when they trust the repository.
888
+
889
+ ${paths}
890
+
891
+ Each accepts the same \`extraKnownMarketplaces\` block:
892
+
893
+ \`\`\`json
894
+ {
895
+ "extraKnownMarketplaces": {
896
+ "${marketplaceName}": {
897
+ "source": ${teamSourceBlock}
898
+ }
899
+ }
900
+ }
901
+ \`\`\``;
902
+ }
903
+ function renderAgentsMd(marketplaceName, description, agentIds) {
904
+ const labels = agentIds.map((id) => getAdapter(id)?.label ?? id).join(", ");
905
+ return `# ${marketplaceName}
906
+
907
+ ${description}
908
+
909
+ This repository is a plugin marketplace managed with [agkit](https://www.npmjs.com/package/agkit).
910
+ The catalog lives in \`.claude-plugin/marketplace.json\`; each plugin lives under \`plugins/<name>/\`.
911
+
912
+ Target agents: ${labels}.
913
+
914
+ ## For agents working in this repo
915
+
916
+ - Plugins are self-contained under \`plugins/<name>/\` with a \`.claude-plugin/plugin.json\` manifest and \`skills/\`, \`commands/\`, \`agents/\`, \`hooks/\` as needed.
917
+ - After changing a plugin, run \`agkit sync\` to reconcile the catalog and \`agkit validate\` before committing.
918
+ - Version a plugin with \`agkit bump <plugin>\` (conventional-commit aware).
919
+ `;
920
+ }
921
+
922
+ // src/lib/git.ts
923
+ import { execFileSync as execFileSync3 } from "child_process";
924
+ import fs7 from "fs";
925
+ import path9 from "path";
926
+ function isGitAvailable() {
927
+ try {
928
+ execFileSync3("git", ["--version"], { stdio: "ignore" });
929
+ return true;
930
+ } catch {
931
+ return false;
932
+ }
933
+ }
934
+ function isGitRepo(dir) {
935
+ return fs7.existsSync(path9.join(dir, ".git"));
936
+ }
937
+ function gitInit(dir) {
938
+ execFileSync3("git", ["init", "--initial-branch=main"], {
939
+ cwd: dir,
940
+ stdio: "ignore"
941
+ });
942
+ }
943
+ function getOriginUrl(dir) {
944
+ try {
945
+ return execFileSync3("git", ["remote", "get-url", "origin"], {
946
+ cwd: dir,
947
+ stdio: ["ignore", "pipe", "ignore"]
948
+ }).toString().trim();
949
+ } catch {
950
+ return void 0;
951
+ }
952
+ }
953
+ function parseRemoteUrl(raw) {
954
+ let host;
955
+ let repoPath;
956
+ const scp = raw.match(/^(?:[\w.-]+)@([\w.-]+):(.+?)(?:\.git)?\/?$/);
957
+ const url = raw.match(/^(?:https?|ssh|git):\/\/(?:[\w.-]+@)?([\w.-]+(?::\d+)?)\/(.+?)(?:\.git)?\/?$/);
958
+ if (scp) {
959
+ host = scp[1];
960
+ repoPath = scp[2];
961
+ } else if (url) {
962
+ host = url[1];
963
+ repoPath = url[2];
964
+ }
965
+ if (!host || !repoPath) return void 0;
966
+ const bareHost = host.replace(/:\d+$/, "");
967
+ const segments = repoPath.split("/").filter(Boolean);
968
+ const slug = segments.length === 2 ? `${segments[0]}/${segments[1]}` : void 0;
969
+ return {
970
+ httpsUrl: `https://${host}/${repoPath}.git`,
971
+ host: bareHost,
972
+ slug,
973
+ isGitHub: bareHost === "github.com",
974
+ isGitLab: bareHost === "gitlab.com" || bareHost.startsWith("gitlab.")
975
+ };
976
+ }
977
+
978
+ // src/commands/init.ts
979
+ function validateName(name) {
980
+ if (!KEBAB_CASE_RE.test(name)) {
981
+ return "Name must be kebab-case (lowercase letters, digits, hyphens)";
982
+ }
983
+ if (RESERVED_MARKETPLACE_NAMES.includes(name)) {
984
+ return `"${name}" is reserved by Claude Code and cannot be used`;
985
+ }
986
+ return void 0;
987
+ }
988
+ function marketplaceTpl(file) {
989
+ return fs8.readFileSync(path10.join(templatesDir(), "marketplace", file), "utf8");
990
+ }
991
+ function addArgument(remote) {
992
+ if (!remote) return "<git-url-or-owner/repo>";
993
+ return remote.isGitHub && remote.slug ? remote.slug : remote.httpsUrl;
994
+ }
995
+ function teamSource(remote) {
996
+ const source = remote?.isGitHub && remote.slug ? { source: "github", repo: remote.slug } : { source: "url", url: remote?.httpsUrl ?? "https://<git-host>/<owner>/<repo>.git" };
997
+ return JSON.stringify(source, null, 2).split("\n").map((line, i) => i === 0 ? line : " " + line).join("\n");
998
+ }
999
+ async function initCommand(dirArg, opts) {
1000
+ p5.intro(pc4.bgCyan(pc4.black(" agkit init ")));
1001
+ const targetDir = path10.resolve(dirArg ?? ".");
1002
+ const defaultName = path10.basename(targetDir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1003
+ const gitAvailable = isGitAvailable();
1004
+ const existingRemote = gitAvailable && fs8.existsSync(targetDir) ? getOriginUrl(targetDir) : void 0;
1005
+ let name = opts.name;
1006
+ let owner = opts.owner;
1007
+ let email = opts.email;
1008
+ let description = opts.description;
1009
+ let repoUrl = opts.repo ?? existingRemote;
1010
+ let ci = opts.ci;
1011
+ let agentsRaw = opts.agents;
1012
+ if (!opts.yes) {
1013
+ if (!name) {
1014
+ const answer = await p5.text({
1015
+ message: "Marketplace name (kebab-case, shown in /plugin install <plugin>@<name>)",
1016
+ initialValue: defaultName,
1017
+ validate: (v) => validateName(v)
1018
+ });
1019
+ if (p5.isCancel(answer)) return cancel5();
1020
+ name = answer;
1021
+ }
1022
+ if (!owner) {
1023
+ const answer = await p5.text({
1024
+ message: "Owner name (person or team)",
1025
+ placeholder: "Your Team",
1026
+ validate: (v) => v.trim() ? void 0 : "Owner name is required"
1027
+ });
1028
+ if (p5.isCancel(answer)) return cancel5();
1029
+ owner = answer;
1030
+ }
1031
+ if (email === void 0) {
1032
+ const answer = await p5.text({
1033
+ message: "Owner email (optional)",
1034
+ defaultValue: "",
1035
+ placeholder: "team@example.com"
1036
+ });
1037
+ if (p5.isCancel(answer)) return cancel5();
1038
+ email = answer;
1039
+ }
1040
+ if (description === void 0) {
1041
+ const answer = await p5.text({
1042
+ message: "Short description",
1043
+ defaultValue: "",
1044
+ placeholder: "A curated collection of Claude Code plugins"
1045
+ });
1046
+ if (p5.isCancel(answer)) return cancel5();
1047
+ description = answer;
1048
+ }
1049
+ if (repoUrl === void 0) {
1050
+ const answer = await p5.text({
1051
+ message: "Git remote URL where this marketplace will be pushed (optional, any forge)",
1052
+ defaultValue: "",
1053
+ placeholder: "git@gitlab.example.com:team/my-marketplace.git"
1054
+ });
1055
+ if (p5.isCancel(answer)) return cancel5();
1056
+ repoUrl = answer || void 0;
1057
+ }
1058
+ if (!ci) {
1059
+ const answer = await p5.select({
1060
+ message: "CI validation workflow",
1061
+ options: [
1062
+ { value: "github", label: "GitHub Actions" },
1063
+ { value: "gitlab", label: "GitLab CI" },
1064
+ { value: "none", label: "None" }
1065
+ ],
1066
+ initialValue: parseRemoteUrl(repoUrl ?? "")?.isGitLab ? "gitlab" : "github"
1067
+ });
1068
+ if (p5.isCancel(answer)) return cancel5();
1069
+ ci = answer;
1070
+ }
1071
+ if (agentsRaw === void 0) {
1072
+ const answer = await p5.multiselect({
1073
+ message: "Target coding agents (space to toggle)",
1074
+ options: AGENT_ADAPTERS.map((a) => ({
1075
+ value: a.id,
1076
+ label: `${a.label}${a.tier === 1 ? "" : ` (tier ${a.tier}, needs generation)`}`
1077
+ })),
1078
+ initialValues: DEFAULT_AGENTS,
1079
+ required: true
1080
+ });
1081
+ if (p5.isCancel(answer)) return cancel5();
1082
+ agentsRaw = answer.join(",");
1083
+ }
1084
+ }
1085
+ name = name || defaultName;
1086
+ const nameError = validateName(name);
1087
+ if (nameError) {
1088
+ p5.log.error(nameError);
1089
+ process.exitCode = 1;
1090
+ return;
1091
+ }
1092
+ owner = owner || "Unknown Owner";
1093
+ ci = ci ?? "github";
1094
+ const { ids: agentIds, unknown } = parseAgentList(agentsRaw ?? DEFAULT_AGENTS.join(","));
1095
+ if (unknown.length > 0) {
1096
+ p5.log.warn(
1097
+ `Unknown agent(s) ignored: ${unknown.join(", ")}. Known: ${AGENT_ADAPTERS.map((a) => a.id).join(", ")}.`
1098
+ );
1099
+ }
1100
+ const targets = agentIds.length > 0 ? agentIds : [...DEFAULT_AGENTS];
1101
+ const remote = repoUrl ? parseRemoteUrl(repoUrl) : void 0;
1102
+ if (repoUrl && !remote) {
1103
+ p5.log.warn(`Could not parse remote URL "${repoUrl}"; install instructions will use placeholders.`);
1104
+ }
1105
+ const s = p5.spinner();
1106
+ s.start("Scaffolding marketplace");
1107
+ fs8.mkdirSync(targetDir, { recursive: true });
1108
+ const marketplace = {
1109
+ $schema: MARKETPLACE_SCHEMA_URL,
1110
+ name,
1111
+ owner: { name: owner, ...email ? { email } : {} },
1112
+ metadata: {
1113
+ ...description ? { description } : {},
1114
+ version: "1.0.0",
1115
+ pluginRoot: DEFAULT_PLUGIN_ROOT,
1116
+ targets
1117
+ },
1118
+ plugins: []
1119
+ };
1120
+ writeJson(path10.join(targetDir, ".claude-plugin", "marketplace.json"), marketplace);
1121
+ fs8.mkdirSync(path10.join(targetDir, "plugins"), { recursive: true });
1122
+ fs8.writeFileSync(path10.join(targetDir, "plugins", ".gitkeep"), "");
1123
+ const desc = description || "A curated collection of agent plugins.";
1124
+ const mktArg = addArgument(remote);
1125
+ const teamBlock = teamSource(remote);
1126
+ const vars = {
1127
+ marketplaceName: name,
1128
+ description: desc,
1129
+ addArgument: mktArg,
1130
+ teamSource: teamBlock,
1131
+ installSections: renderInstallSections(targets, mktArg, name),
1132
+ teamSections: renderTeamSections(targets, teamBlock, name)
1133
+ };
1134
+ fs8.writeFileSync(
1135
+ path10.join(targetDir, "README.md"),
1136
+ renderTemplate(marketplaceTpl("README.md.tpl"), vars)
1137
+ );
1138
+ fs8.writeFileSync(
1139
+ path10.join(targetDir, "AGENTS.md"),
1140
+ renderAgentsMd(name, desc, targets)
1141
+ );
1142
+ const exampleDir = path10.join(targetDir, "examples");
1143
+ fs8.mkdirSync(exampleDir, { recursive: true });
1144
+ fs8.writeFileSync(
1145
+ path10.join(exampleDir, "team-settings.json"),
1146
+ renderTemplate(marketplaceTpl("team-settings.json.tpl"), vars)
1147
+ );
1148
+ if (ci === "github") {
1149
+ const wfDir = path10.join(targetDir, ".github", "workflows");
1150
+ fs8.mkdirSync(wfDir, { recursive: true });
1151
+ fs8.writeFileSync(
1152
+ path10.join(wfDir, "validate.yml"),
1153
+ renderTemplate(marketplaceTpl("github-validate.yml.tpl"), vars)
1154
+ );
1155
+ } else if (ci === "gitlab") {
1156
+ fs8.writeFileSync(
1157
+ path10.join(targetDir, ".gitlab-ci.yml"),
1158
+ renderTemplate(marketplaceTpl("gitlab-ci.yml.tpl"), vars)
1159
+ );
1160
+ }
1161
+ fs8.writeFileSync(
1162
+ path10.join(targetDir, ".gitignore"),
1163
+ ["node_modules/", ".DS_Store", "*.log", ""].join("\n")
1164
+ );
1165
+ if (gitAvailable && !isGitRepo(targetDir)) {
1166
+ try {
1167
+ gitInit(targetDir);
1168
+ } catch {
1169
+ p5.log.warn("git init failed; initialize the repository manually.");
1170
+ }
1171
+ }
1172
+ s.stop("Marketplace scaffolded");
1173
+ if (!opts.yes) {
1174
+ const wantStarter = await p5.confirm({
1175
+ message: "Add a starter plugin now?",
1176
+ initialValue: true
1177
+ });
1178
+ if (!p5.isCancel(wantStarter) && wantStarter) {
1179
+ await addPlugin(targetDir, void 0, void 0, { interactive: true });
1180
+ }
1181
+ }
1182
+ const rel = path10.relative(process.cwd(), targetDir) || ".";
1183
+ p5.note(
1184
+ [
1185
+ `cd ${rel}`,
1186
+ remote ? `git remote add origin ${remote.httpsUrl} # if not already set` : "git remote add origin <your-git-url>",
1187
+ "agkit add skill my-first-skill",
1188
+ "agkit validate",
1189
+ "git add -A && git commit -m 'feat: initial marketplace'",
1190
+ "git push -u origin main",
1191
+ "",
1192
+ `Target agents: ${targets.map((id) => getAdapter(id)?.label ?? id).join(", ")}`,
1193
+ `Native install (no build): ${targets.filter((id) => getAdapter(id)?.tier === 1).map((id) => getAdapter(id)?.label).join(", ") || "none"}`
1194
+ ].join("\n"),
1195
+ "Next steps"
1196
+ );
1197
+ const needBuild = targets.filter((id) => (getAdapter(id)?.tier ?? 1) > 1);
1198
+ if (needBuild.length > 0) {
1199
+ p5.log.warn(
1200
+ `Recorded tier 2/3 target(s) in metadata.targets: ${needBuild.map((id) => getAdapter(id)?.label).join(", ")}.
1201
+ These are NOT installable from the Claude catalog alone \u2014 they need generated per-agent artifacts. agkit records the intent and documents it; artifact generation (\`agkit build --target \u2026\`) is not implemented yet. See README install notes.`
1202
+ );
1203
+ }
1204
+ p5.outro("Done \u2714");
1205
+ }
1206
+ function cancel5() {
1207
+ p5.cancel("Cancelled.");
1208
+ process.exitCode = 1;
1209
+ }
1210
+
1211
+ // src/commands/list.ts
1212
+ import * as p6 from "@clack/prompts";
1213
+ import pc5 from "picocolors";
1214
+ async function listCommand() {
1215
+ p6.intro(pc5.bgCyan(pc5.black(" Available plugin templates ")));
1216
+ for (const t of PLUGIN_TEMPLATES) {
1217
+ p6.log.message(`${pc5.cyan(pc5.bold(t))}
1218
+ ${TEMPLATE_DESCRIPTIONS[t]}`);
1219
+ }
1220
+ p6.log.message(
1221
+ `Remote and local templates are also supported:
1222
+ agkit add gh:owner/repo/dir my-plugin
1223
+ agkit add gl:owner/repo#v2 my-plugin
1224
+ agkit add https://git.company.io/team/templates.git//skill-fr my-plugin
1225
+ agkit add ./local-template my-plugin`
1226
+ );
1227
+ p6.outro("Usage: agkit add <template|spec> <plugin-name>");
1228
+ }
1229
+
1230
+ // src/commands/validate.ts
1231
+ import { execFileSync as execFileSync4 } from "child_process";
1232
+ import fs9 from "fs";
1233
+ import path11 from "path";
1234
+ import * as p7 from "@clack/prompts";
1235
+ import pc6 from "picocolors";
1236
+ async function validateCommand(startDir, opts = {}) {
1237
+ const root = findMarketplaceRoot(startDir);
1238
+ if (!root) {
1239
+ p7.log.error("No .claude-plugin/marketplace.json found. Run `agkit init` first.");
1240
+ process.exitCode = 1;
1241
+ return;
1242
+ }
1243
+ const findings = [];
1244
+ let mp;
1245
+ try {
1246
+ mp = readMarketplace(root);
1247
+ } catch (err) {
1248
+ findings.push({
1249
+ level: "error",
1250
+ message: `marketplace.json is not valid JSON: ${err.message}`
1251
+ });
1252
+ }
1253
+ if (mp) {
1254
+ if (!mp.name) {
1255
+ findings.push({ level: "error", message: "Missing required field: name" });
1256
+ } else {
1257
+ if (!KEBAB_CASE_RE.test(mp.name)) {
1258
+ findings.push({
1259
+ level: "error",
1260
+ message: `Marketplace name "${mp.name}" must be kebab-case`
1261
+ });
1262
+ }
1263
+ if (RESERVED_MARKETPLACE_NAMES.includes(mp.name)) {
1264
+ findings.push({
1265
+ level: "error",
1266
+ message: `Marketplace name "${mp.name}" is reserved by Claude Code`
1267
+ });
1268
+ }
1269
+ }
1270
+ if (!mp.owner?.name) {
1271
+ findings.push({ level: "error", message: "Missing required field: owner.name" });
1272
+ }
1273
+ if (!Array.isArray(mp.plugins)) {
1274
+ findings.push({ level: "error", message: "plugins must be an array" });
1275
+ }
1276
+ const seen = /* @__PURE__ */ new Set();
1277
+ for (const entry of mp.plugins ?? []) {
1278
+ const label = entry.name ?? "<unnamed>";
1279
+ if (!entry.name) {
1280
+ findings.push({ level: "error", message: "A plugin entry is missing its name" });
1281
+ continue;
1282
+ }
1283
+ if (!KEBAB_CASE_RE.test(entry.name)) {
1284
+ findings.push({
1285
+ level: "error",
1286
+ message: `Plugin name "${label}" must be kebab-case`
1287
+ });
1288
+ }
1289
+ if (seen.has(entry.name)) {
1290
+ findings.push({ level: "error", message: `Duplicate plugin name "${label}"` });
1291
+ }
1292
+ seen.add(entry.name);
1293
+ if (entry.source === void 0) {
1294
+ findings.push({ level: "error", message: `Plugin "${label}" is missing source` });
1295
+ continue;
1296
+ }
1297
+ const localDir = resolveLocalPluginDir(root, mp, entry);
1298
+ if (localDir) {
1299
+ if (!fs9.existsSync(localDir)) {
1300
+ findings.push({
1301
+ level: "error",
1302
+ message: `Plugin "${label}" source resolves to missing directory ${path11.relative(root, localDir)}`
1303
+ });
1304
+ } else {
1305
+ const manifestPath = path11.join(localDir, MARKETPLACE_DIR, PLUGIN_MANIFEST_FILE);
1306
+ if (!fs9.existsSync(manifestPath)) {
1307
+ findings.push({
1308
+ level: "error",
1309
+ message: `Plugin "${label}" has no ${MARKETPLACE_DIR}/${PLUGIN_MANIFEST_FILE}`
1310
+ });
1311
+ } else {
1312
+ try {
1313
+ const manifest = JSON.parse(fs9.readFileSync(manifestPath, "utf8"));
1314
+ if (manifest.name && manifest.name !== entry.name) {
1315
+ findings.push({
1316
+ level: "warn",
1317
+ message: `Catalog entry "${label}" but plugin.json declares "${manifest.name}" \u2014 run \`agkit sync\``
1318
+ });
1319
+ }
1320
+ if (manifest.version && entry.version && manifest.version !== entry.version) {
1321
+ findings.push({
1322
+ level: "warn",
1323
+ message: `Version drift for "${label}": catalog ${entry.version} vs plugin.json ${manifest.version} \u2014 run \`agkit sync\``
1324
+ });
1325
+ }
1326
+ } catch {
1327
+ findings.push({
1328
+ level: "error",
1329
+ message: `Plugin "${label}": plugin.json is not valid JSON`
1330
+ });
1331
+ }
1332
+ }
1333
+ }
1334
+ }
1335
+ }
1336
+ const hasRelative = (mp.plugins ?? []).some((e) => typeof e.source === "string");
1337
+ if (hasRelative) {
1338
+ p7.log.info(
1339
+ "Note: relative plugin sources resolve only when the marketplace is added via Git (any forge), not via a direct URL to marketplace.json."
1340
+ );
1341
+ }
1342
+ }
1343
+ if (mp?.metadata?.targets) {
1344
+ const needBuild = mp.metadata.targets.filter((id) => (getAdapter(id)?.tier ?? 1) > 1);
1345
+ if (needBuild.length > 0) {
1346
+ p7.log.info(
1347
+ `metadata.targets includes tier 2/3 agent(s): ${needBuild.join(", ")}. The Claude catalog validates here; run \`agkit build\` to (re)generate their registries and \`agkit build --check\` in CI.`
1348
+ );
1349
+ }
1350
+ }
1351
+ let officialRan = false;
1352
+ try {
1353
+ execFileSync4("claude", ["plugin", "validate", root, ...opts.strict ? ["--strict"] : []], { stdio: "pipe" });
1354
+ officialRan = true;
1355
+ p7.log.success("claude plugin validate: passed");
1356
+ } catch (err) {
1357
+ const e = err;
1358
+ if (e.code === "ENOENT") {
1359
+ p7.log.info(
1360
+ "Claude Code CLI not found \u2014 skipped `claude plugin validate` (install it for the official schema check)."
1361
+ );
1362
+ } else {
1363
+ officialRan = true;
1364
+ const out = [e.stdout?.toString(), e.stderr?.toString()].filter(Boolean).join("\n").trim();
1365
+ findings.push({
1366
+ level: "error",
1367
+ message: `claude plugin validate failed${out ? ":\n" + out : ""}`
1368
+ });
1369
+ }
1370
+ }
1371
+ const errors = findings.filter((f) => f.level === "error");
1372
+ const warns = findings.filter((f) => f.level === "warn");
1373
+ for (const f of warns) p7.log.warn(f.message);
1374
+ for (const f of errors) p7.log.error(f.message);
1375
+ if (errors.length > 0) {
1376
+ p7.log.error(pc6.red(`Validation failed: ${errors.length} error(s), ${warns.length} warning(s).`));
1377
+ process.exitCode = 1;
1378
+ } else {
1379
+ p7.log.success(
1380
+ pc6.green(
1381
+ `${path11.relative(process.cwd(), marketplacePath(root)) || marketplacePath(root)} is valid` + (warns.length ? ` (${warns.length} warning(s))` : "") + (officialRan ? "" : " [local checks only]")
1382
+ )
1383
+ );
1384
+ }
1385
+ }
1386
+
1387
+ // src/index.ts
1388
+ var { version } = JSON.parse(
1389
+ readFileSync(new URL("../package.json", import.meta.url), "utf8")
1390
+ );
1391
+ var program = new Command();
1392
+ program.name("agkit").description(
1393
+ "Scaffold and manage plugin marketplaces for Claude Code and GitHub Copilot, distributed via any Git host."
1394
+ ).version(version);
1395
+ program.command("init").argument("[dir]", "target directory (default: current directory)").description("Scaffold a new plugin marketplace (git-first)").option("-n, --name <name>", "marketplace name (kebab-case)").option("-o, --owner <owner>", "owner name").option("-e, --email <email>", "owner email").option("-d, --description <text>", "marketplace description").option("-r, --repo <url>", "git remote URL (any forge)").option("--ci <ci>", "CI workflow: github | gitlab | none").option("--agents <list>", "comma-separated target agents (e.g. claude-code,copilot,codex)").option("-y, --yes", "non-interactive, accept defaults").action(async (dir, opts) => {
1396
+ await initCommand(dir, opts);
1397
+ });
1398
+ program.command("add").argument("[template]", "template name, local path, gh:/gl: shorthand, or git URL (see `agkit list`)").argument("[name]", "plugin name (kebab-case)").description("Scaffold a plugin from a template and register it in the catalog").option("-d, --description <text>", "one-line plugin description").action(async (template, name, opts) => {
1399
+ await addPlugin(process.cwd(), template, name, {
1400
+ description: opts.description,
1401
+ interactive: opts.description === void 0
1402
+ });
1403
+ });
1404
+ program.command("sync").description("Reconcile marketplace.json and README with the plugins on disk").action(async () => {
1405
+ await syncCommand(process.cwd());
1406
+ });
1407
+ program.command("validate").description("Validate the catalog (local checks + `claude plugin validate` if available)").option("--strict", "treat warnings as errors in the official validator").action(async (opts) => {
1408
+ await validateCommand(process.cwd(), { strict: opts.strict });
1409
+ });
1410
+ program.command("bump").argument("[plugin]", "plugin name (prompted if omitted)").argument("[level]", "major | minor | patch | auto (default: auto, from conventional commits)").description("Bump a plugin version (conventional-commit aware) and sync the catalog").option("-t, --tag", "commit the bump and create a release tag <plugin>@<version>").option("--dry-run", "show what would change without writing").action(async (plugin, level, opts) => {
1411
+ await bumpCommand(process.cwd(), plugin, level, { tag: opts.tag, dryRun: opts.dryRun });
1412
+ });
1413
+ program.command("build").description("Generate tier-2 agent artifacts (codex, cursor) from the catalog").option("--target <list>", "comma-separated tier-2 targets (default: those in metadata.targets)").option("--check", "verify generated artifacts are up to date (CI); non-zero exit on drift").action(async (opts) => {
1414
+ await buildCommand(process.cwd(), { targets: opts.target, check: opts.check });
1415
+ });
1416
+ program.command("list").description("List available plugin templates").action(async () => {
1417
+ await listCommand();
1418
+ });
1419
+ program.parseAsync().catch((err) => {
1420
+ console.error(err);
1421
+ process.exit(1);
1422
+ });