agkit 0.7.0 → 0.9.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.
Files changed (4) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +231 -43
  3. package/dist/index.js +656 -170
  4. package/package.json +19 -4
package/dist/index.js CHANGED
@@ -24,7 +24,13 @@ var RESERVED_MARKETPLACE_NAMES = [
24
24
  "anthropic-plugins"
25
25
  ];
26
26
  var KEBAB_CASE_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
27
- var PLUGIN_TEMPLATES = ["skill", "command", "agent", "hook", "mcp"];
27
+ var PLUGIN_TEMPLATES = [
28
+ "skill",
29
+ "command",
30
+ "agent",
31
+ "hook",
32
+ "mcp"
33
+ ];
28
34
  var TEMPLATE_DESCRIPTIONS = {
29
35
  skill: "Plugin exposing an Agent Skill (SKILL.md): reference knowledge or a repeatable procedure Claude loads on demand.",
30
36
  command: "Plugin exposing a slash command (commands/<name>.md): a prompt shortcut invoked as /<plugin>:<name>.",
@@ -84,14 +90,69 @@ function readJson(filePath) {
84
90
  }
85
91
  function writeJson(filePath, data) {
86
92
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
87
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
93
+ fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}
94
+ `);
95
+ }
96
+
97
+ // src/lib/marketplace.ts
98
+ import fs2 from "fs";
99
+ import path2 from "path";
100
+ function marketplacePath(root) {
101
+ return path2.join(root, MARKETPLACE_DIR, MARKETPLACE_FILE);
102
+ }
103
+ function findMarketplaceRoot(startDir) {
104
+ let dir = path2.resolve(startDir);
105
+ for (; ; ) {
106
+ if (fs2.existsSync(marketplacePath(dir))) return dir;
107
+ const parent = path2.dirname(dir);
108
+ if (parent === dir) return void 0;
109
+ dir = parent;
110
+ }
111
+ }
112
+ function readMarketplace(root) {
113
+ return readJson(marketplacePath(root));
114
+ }
115
+ function writeMarketplace(root, data) {
116
+ writeJson(marketplacePath(root), data);
117
+ }
118
+ function pluginRootDir(root, mp) {
119
+ const rel = mp.metadata?.pluginRoot ?? DEFAULT_PLUGIN_ROOT;
120
+ return path2.resolve(root, rel);
121
+ }
122
+ function resolveLocalPluginDir(root, mp, entry) {
123
+ if (typeof entry.source !== "string") return void 0;
124
+ if (entry.source.startsWith("./") || entry.source.startsWith("../")) {
125
+ return path2.resolve(root, entry.source);
126
+ }
127
+ return path2.join(pluginRootDir(root, mp), entry.source);
128
+ }
129
+ function scanLocalPlugins(root, mp) {
130
+ const base = pluginRootDir(root, mp);
131
+ if (!fs2.existsSync(base)) return [];
132
+ const result = [];
133
+ for (const entry of fs2.readdirSync(base, { withFileTypes: true })) {
134
+ if (!entry.isDirectory()) continue;
135
+ const manifestPath = path2.join(
136
+ base,
137
+ entry.name,
138
+ MARKETPLACE_DIR,
139
+ PLUGIN_MANIFEST_FILE
140
+ );
141
+ if (!fs2.existsSync(manifestPath)) continue;
142
+ result.push({
143
+ dir: path2.join(base, entry.name),
144
+ dirName: entry.name,
145
+ manifest: readJson(manifestPath)
146
+ });
147
+ }
148
+ return result;
88
149
  }
89
150
 
90
151
  // src/lib/templates.ts
91
152
  import { execFileSync } from "child_process";
92
- import fs2 from "fs";
153
+ import fs3 from "fs";
93
154
  import os from "os";
94
- import path2 from "path";
155
+ import path3 from "path";
95
156
  function parseGitSpec(spec) {
96
157
  let ref;
97
158
  let body = spec;
@@ -122,8 +183,75 @@ function parseGitSpec(spec) {
122
183
  }
123
184
  return void 0;
124
185
  }
186
+ function isRemoteSpec(spec) {
187
+ if (PLUGIN_TEMPLATES.includes(spec)) return false;
188
+ return !(spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("path:") || path3.isAbsolute(spec));
189
+ }
190
+ function resolveRemoteSource(spec, opts = {}) {
191
+ if (spec.startsWith("./") || spec.startsWith("../") || spec.startsWith("path:") || path3.isAbsolute(spec)) {
192
+ throw new Error(
193
+ `--link needs a remote git source, but "${spec}" is a local path. Drop --link to scaffold from a local template.`
194
+ );
195
+ }
196
+ if (PLUGIN_TEMPLATES.includes(spec)) {
197
+ throw new Error(
198
+ `--link needs a remote git source, but "${spec}" is a built-in template. Drop --link to scaffold it locally.`
199
+ );
200
+ }
201
+ if (opts.sha && !/^[0-9a-f]{40}$/i.test(opts.sha)) {
202
+ throw new Error(
203
+ `--sha must be a full 40-character commit hash: "${opts.sha}"`
204
+ );
205
+ }
206
+ let body = spec;
207
+ let hashRef;
208
+ const hash = body.lastIndexOf("#");
209
+ if (hash > 0) {
210
+ hashRef = body.slice(hash + 1) || void 0;
211
+ body = body.slice(0, hash);
212
+ }
213
+ const git2 = parseGitSpec(spec);
214
+ const ref = opts.ref ?? git2?.ref ?? hashRef;
215
+ const pin = (base) => {
216
+ if (ref) base.ref = ref;
217
+ if (opts.sha) base.sha = opts.sha;
218
+ return base;
219
+ };
220
+ if (git2) {
221
+ if (git2.subdir) {
222
+ return {
223
+ source: pin({
224
+ source: "git-subdir",
225
+ url: git2.cloneUrl,
226
+ path: git2.subdir
227
+ }),
228
+ label: spec
229
+ };
230
+ }
231
+ const gh = git2.cloneUrl.match(
232
+ /^https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/
233
+ );
234
+ if (gh) {
235
+ return {
236
+ source: pin({ source: "github", repo: `${gh[1]}/${gh[2]}` }),
237
+ label: spec
238
+ };
239
+ }
240
+ return { source: pin({ source: "url", url: git2.cloneUrl }), label: spec };
241
+ }
242
+ const bare = body.match(/^([\w][\w.-]*)\/([\w][\w.-]*)$/);
243
+ if (bare) {
244
+ return {
245
+ source: pin({ source: "github", repo: `${bare[1]}/${bare[2]}` }),
246
+ label: spec
247
+ };
248
+ }
249
+ throw new Error(
250
+ `Unrecognized remote source "${spec}". Use owner/repo, gh:owner/repo[/dir], gl:owner/repo[/dir], or a git URL[//dir][#ref].`
251
+ );
252
+ }
125
253
  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"));
254
+ const hasManifest = fs3.existsSync(path3.join(dir, ".claude-plugin", "plugin.json.tpl")) || fs3.existsSync(path3.join(dir, ".claude-plugin", "plugin.json"));
127
255
  if (!hasManifest) {
128
256
  throw new Error(
129
257
  `"${label}" is not a plugin template: expected .claude-plugin/plugin.json(.tpl) in ${dir}`
@@ -133,15 +261,16 @@ function assertTemplateDir(dir, label) {
133
261
  function resolveTemplate(spec) {
134
262
  if (PLUGIN_TEMPLATES.includes(spec)) {
135
263
  return {
136
- dir: path2.join(templatesDir(), "plugins", spec),
264
+ dir: path3.join(templatesDir(), "plugins", spec),
137
265
  label: `built-in:${spec}`,
138
266
  builtin: true
139
267
  };
140
268
  }
141
- const localPath = spec.startsWith("path:") ? spec.slice(5) : spec.startsWith("./") || spec.startsWith("../") || path2.isAbsolute(spec) ? spec : void 0;
269
+ const localPath = spec.startsWith("path:") ? spec.slice(5) : spec.startsWith("./") || spec.startsWith("../") || path3.isAbsolute(spec) ? spec : void 0;
142
270
  if (localPath !== void 0) {
143
- const dir2 = path2.resolve(localPath);
144
- if (!fs2.existsSync(dir2)) throw new Error(`Template directory not found: ${dir2}`);
271
+ const dir2 = path3.resolve(localPath);
272
+ if (!fs3.existsSync(dir2))
273
+ throw new Error(`Template directory not found: ${dir2}`);
145
274
  assertTemplateDir(dir2, spec);
146
275
  return { dir: dir2, label: spec, builtin: false };
147
276
  }
@@ -151,91 +280,106 @@ function resolveTemplate(spec) {
151
280
  `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
281
  );
153
282
  }
154
- const tmp = fs2.mkdtempSync(path2.join(os.tmpdir(), "agkit-tpl-"));
283
+ const tmp = fs3.mkdtempSync(path3.join(os.tmpdir(), "agkit-tpl-"));
155
284
  const args = ["clone", "--depth", "1"];
156
285
  if (git2.ref) args.push("--branch", git2.ref);
157
286
  args.push(git2.cloneUrl, tmp);
158
287
  try {
159
288
  execFileSync("git", args, { stdio: "pipe" });
160
289
  } catch (err) {
161
- fs2.rmSync(tmp, { recursive: true, force: true });
290
+ fs3.rmSync(tmp, { recursive: true, force: true });
162
291
  const msg = err.stderr?.toString().trim();
163
- throw new Error(`git clone failed for ${git2.cloneUrl}${msg ? `:
164
- ${msg}` : ""}`);
292
+ throw new Error(
293
+ `git clone failed for ${git2.cloneUrl}${msg ? `:
294
+ ${msg}` : ""}`
295
+ );
165
296
  }
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}`);
297
+ const dir = git2.subdir ? path3.join(tmp, git2.subdir) : tmp;
298
+ if (!fs3.existsSync(dir)) {
299
+ fs3.rmSync(tmp, { recursive: true, force: true });
300
+ throw new Error(
301
+ `Subdirectory "${git2.subdir}" not found in ${git2.cloneUrl}`
302
+ );
170
303
  }
171
- fs2.rmSync(path2.join(tmp, ".git"), { recursive: true, force: true });
304
+ fs3.rmSync(path3.join(tmp, ".git"), { recursive: true, force: true });
172
305
  try {
173
306
  assertTemplateDir(dir, spec);
174
307
  } catch (err) {
175
- fs2.rmSync(tmp, { recursive: true, force: true });
308
+ fs3.rmSync(tmp, { recursive: true, force: true });
176
309
  throw err;
177
310
  }
178
311
  return { dir, label: spec, cleanup: tmp, builtin: false };
179
312
  }
180
313
 
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));
314
+ // src/commands/sync.ts
315
+ import fs5 from "fs";
316
+ import path6 from "path";
317
+ import * as p2 from "@clack/prompts";
318
+
319
+ // src/lib/docs.ts
320
+ var PLUGINS_START = "<!-- agkit:plugins:start -->";
321
+ var PLUGINS_END = "<!-- agkit:plugins:end -->";
322
+ var CHANGELOG_HEADER = "# Changelog";
323
+ function replaceBetweenMarkers(content, start, end, body) {
324
+ const s = content.indexOf(start);
325
+ const e = content.indexOf(end);
326
+ if (s === -1 || e === -1 || e < s) return void 0;
327
+ return `${content.slice(0, s + start.length)}
328
+ ${body}
329
+ ${content.slice(e)}`;
198
330
  }
199
- function writeMarketplace(root, data) {
200
- writeJson(marketplacePath(root), data);
331
+ var EMPTY_HINT = "_No plugins yet. Add one with `agkit add <template> <name>`._";
332
+ function renderPluginTable(plugins) {
333
+ if (plugins.length === 0) return EMPTY_HINT;
334
+ const withKeywords = plugins.some((p8) => (p8.keywords?.length ?? 0) > 0);
335
+ const header = withKeywords ? [
336
+ "| Plugin | Version | Description | Keywords |",
337
+ "| :----- | :------ | :---------- | :------- |"
338
+ ] : [
339
+ "| Plugin | Version | Description |",
340
+ "| :----- | :------ | :---------- |"
341
+ ];
342
+ const rows = plugins.map((e) => {
343
+ const name = e.homepage ? `[\`${e.name}\`](${e.homepage})` : `\`${e.name}\``;
344
+ const cells = [name, e.version ?? "\u2014", e.description ?? ""];
345
+ if (withKeywords) {
346
+ cells.push((e.keywords ?? []).map((k) => `\`${k}\``).join(" "));
347
+ }
348
+ return `| ${cells.join(" | ")} |`;
349
+ });
350
+ return [...header, ...rows].join("\n");
201
351
  }
202
- function pluginRootDir(root, mp) {
203
- const rel = mp.metadata?.pluginRoot ?? DEFAULT_PLUGIN_ROOT;
204
- return path3.resolve(root, rel);
352
+ function renderAgentsPluginList(plugins) {
353
+ if (plugins.length === 0) return "_No plugins yet._";
354
+ return plugins.map((e) => {
355
+ const version2 = e.version ? ` (${e.version})` : "";
356
+ const desc = e.description ? ` \u2014 ${e.description}` : "";
357
+ return `- \`${e.name}\`${version2}${desc}`;
358
+ }).join("\n");
205
359
  }
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);
360
+ function renderChangelogEntry(version2, subjects, date) {
361
+ const bullets = subjects.length > 0 ? subjects.map((s) => `- ${s}`).join("\n") : `- Release ${version2}`;
362
+ return `## ${version2} \u2014 ${date}
363
+
364
+ ${bullets}`;
212
365
  }
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
- });
366
+ function prependChangelogEntry(existing, entry) {
367
+ if (!existing?.includes(CHANGELOG_HEADER)) {
368
+ return `${CHANGELOG_HEADER}
369
+
370
+ ${entry}
371
+ `;
231
372
  }
232
- return result;
233
- }
373
+ const idx = existing.indexOf(CHANGELOG_HEADER) + CHANGELOG_HEADER.length;
374
+ const before = existing.slice(0, idx);
375
+ const after = existing.slice(idx).replace(/^\n+/, "");
376
+ return `${`${before}
234
377
 
235
- // src/commands/sync.ts
236
- import fs5 from "fs";
237
- import path6 from "path";
238
- import * as p2 from "@clack/prompts";
378
+ ${entry}
379
+
380
+ ${after}`.trimEnd()}
381
+ `;
382
+ }
239
383
 
240
384
  // src/commands/build.ts
241
385
  import fs4 from "fs";
@@ -253,7 +397,8 @@ function sourcePath(mp, dirName) {
253
397
  return `./${path4.posix.join(pluginRootRel(mp), dirName)}`;
254
398
  }
255
399
  function json(value) {
256
- return JSON.stringify(value, null, 2) + "\n";
400
+ return `${JSON.stringify(value, null, 2)}
401
+ `;
257
402
  }
258
403
  function manifestCopy(local) {
259
404
  return json(local.manifest);
@@ -275,11 +420,19 @@ function generateCursor(ctx) {
275
420
  }))
276
421
  };
277
422
  const files = [
278
- { relPath: path4.posix.join(".cursor-plugin", "marketplace.json"), content: json(registry) }
423
+ {
424
+ relPath: path4.posix.join(".cursor-plugin", "marketplace.json"),
425
+ content: json(registry)
426
+ }
279
427
  ];
280
428
  for (const p8 of plugins) {
281
429
  files.push({
282
- relPath: path4.posix.join(pluginRootRel(mp), p8.dirName, ".cursor-plugin", "plugin.json"),
430
+ relPath: path4.posix.join(
431
+ pluginRootRel(mp),
432
+ p8.dirName,
433
+ ".cursor-plugin",
434
+ "plugin.json"
435
+ ),
283
436
  content: manifestCopy(p8)
284
437
  });
285
438
  }
@@ -304,7 +457,12 @@ function generateCodex(ctx) {
304
457
  ];
305
458
  for (const p8 of plugins) {
306
459
  files.push({
307
- relPath: path4.posix.join(pluginRootRel(mp), p8.dirName, ".codex-plugin", "plugin.json"),
460
+ relPath: path4.posix.join(
461
+ pluginRootRel(mp),
462
+ p8.dirName,
463
+ ".codex-plugin",
464
+ "plugin.json"
465
+ ),
308
466
  content: manifestCopy(p8)
309
467
  });
310
468
  }
@@ -348,7 +506,9 @@ function computeFiles(root, mp, ids) {
348
506
  async function buildCommand(startDir, opts = {}) {
349
507
  const root = findMarketplaceRoot(startDir);
350
508
  if (!root) {
351
- p.log.error("No .claude-plugin/marketplace.json found. Run `agkit init` first.");
509
+ p.log.error(
510
+ "No .claude-plugin/marketplace.json found. Run `agkit init` first."
511
+ );
352
512
  process.exitCode = 1;
353
513
  return;
354
514
  }
@@ -420,12 +580,27 @@ function refreshBuiltTargets(root, mp) {
420
580
  }
421
581
 
422
582
  // src/commands/sync.ts
423
- var README_START = "<!-- agkit:plugins:start -->";
424
- var README_END = "<!-- agkit:plugins:end -->";
583
+ function refreshMarkedDoc(root, relPath, body, label, changes) {
584
+ const abs = path6.join(root, relPath);
585
+ if (!fs5.existsSync(abs)) return;
586
+ const content = fs5.readFileSync(abs, "utf8");
587
+ const updated = replaceBetweenMarkers(
588
+ content,
589
+ PLUGINS_START,
590
+ PLUGINS_END,
591
+ body
592
+ );
593
+ if (updated !== void 0 && updated !== content) {
594
+ fs5.writeFileSync(abs, updated);
595
+ changes.push(`~ refreshed ${label}`);
596
+ }
597
+ }
425
598
  async function syncCommand(startDir, opts = {}) {
426
599
  const root = findMarketplaceRoot(startDir);
427
600
  if (!root) {
428
- p2.log.error("No .claude-plugin/marketplace.json found. Run `agkit init` first.");
601
+ p2.log.error(
602
+ "No .claude-plugin/marketplace.json found. Run `agkit init` first."
603
+ );
429
604
  process.exitCode = 1;
430
605
  return;
431
606
  }
@@ -439,12 +614,18 @@ async function syncCommand(startDir, opts = {}) {
439
614
  if (!entry) {
440
615
  entry = {
441
616
  name: entryName,
442
- source: mp.metadata?.pluginRoot ? local.dirName : `./${path6.relative(root, local.dir).split(path6.sep).join("/")}`
617
+ source: `./${path6.relative(root, local.dir).split(path6.sep).join("/")}`
443
618
  };
444
619
  mp.plugins.push(entry);
445
620
  changes.push(`+ added "${entryName}" to the catalog`);
446
621
  }
447
- for (const field of ["version", "description", "author", "keywords", "homepage"]) {
622
+ for (const field of [
623
+ "version",
624
+ "description",
625
+ "author",
626
+ "keywords",
627
+ "homepage"
628
+ ]) {
448
629
  const value = manifest[field];
449
630
  if (value !== void 0 && JSON.stringify(entry[field]) !== JSON.stringify(value)) {
450
631
  entry[field] = value;
@@ -463,27 +644,22 @@ async function syncCommand(startDir, opts = {}) {
463
644
  mp.plugins.sort((a, b) => a.name.localeCompare(b.name));
464
645
  writeMarketplace(root, mp);
465
646
  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
- }
647
+ for (const id of refreshed)
648
+ changes.push(`~ refreshed ${id} tier-2 artifacts`);
649
+ refreshMarkedDoc(
650
+ root,
651
+ "README.md",
652
+ renderPluginTable(mp.plugins),
653
+ "plugin list in README.md",
654
+ changes
655
+ );
656
+ refreshMarkedDoc(
657
+ root,
658
+ "AGENTS.md",
659
+ renderAgentsPluginList(mp.plugins),
660
+ "plugin list in AGENTS.md",
661
+ changes
662
+ );
487
663
  if (!opts.quiet) {
488
664
  if (changes.length === 0) {
489
665
  p2.log.success("Catalog already in sync.");
@@ -521,6 +697,7 @@ async function addPlugin(startDir, templateArg, nameArg, opts = {}) {
521
697
  if (p3.isCancel(answer)) return cancel2();
522
698
  templateSpec = answer;
523
699
  }
700
+ const reference = isRemoteSpec(templateSpec) && opts.vendor !== true;
524
701
  let name = nameArg;
525
702
  if (!name) {
526
703
  const answer = await p3.text({
@@ -542,12 +719,6 @@ async function addPlugin(startDir, templateArg, nameArg, opts = {}) {
542
719
  process.exitCode = 1;
543
720
  return;
544
721
  }
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
722
  let description = opts.description;
552
723
  if (description === void 0 && opts.interactive !== false) {
553
724
  const answer = await p3.text({
@@ -559,6 +730,39 @@ async function addPlugin(startDir, templateArg, nameArg, opts = {}) {
559
730
  description = answer;
560
731
  }
561
732
  description = description || `TODO: describe what ${name} does.`;
733
+ if (reference) {
734
+ let remote;
735
+ try {
736
+ remote = resolveRemoteSource(templateSpec, {
737
+ ref: opts.ref,
738
+ sha: opts.sha
739
+ });
740
+ } catch (err) {
741
+ p3.log.error(err.message);
742
+ process.exitCode = 1;
743
+ return;
744
+ }
745
+ mp.plugins.push({ name, source: remote.source, description });
746
+ writeMarketplace(root, mp);
747
+ await syncCommand(root, { quiet: true });
748
+ p3.log.success(
749
+ `Registered ${pc2.cyan(name)} as a remote plugin (${remote.label}) in marketplace.json \u2014 nothing was cloned.`
750
+ );
751
+ p3.log.info(
752
+ `Claude Code fetches it from the source at install time:
753
+ claude
754
+ /plugin marketplace add ${root}
755
+ /plugin install ${name}@${mp.name}
756
+ (pass --vendor to clone it into ${mp.metadata?.pluginRoot ?? "./plugins"} instead)`
757
+ );
758
+ return;
759
+ }
760
+ const destDir = path7.join(pluginRootDir(root, mp), name);
761
+ if (fs6.existsSync(destDir)) {
762
+ p3.log.error(`Directory already exists: ${destDir}`);
763
+ process.exitCode = 1;
764
+ return;
765
+ }
562
766
  const vars = {
563
767
  pluginName: name,
564
768
  pluginTitle: titleCase(name),
@@ -576,7 +780,8 @@ async function addPlugin(startDir, templateArg, nameArg, opts = {}) {
576
780
  try {
577
781
  copyTemplateDir(resolved.dir, destDir, vars);
578
782
  } finally {
579
- if (resolved.cleanup) fs6.rmSync(resolved.cleanup, { recursive: true, force: true });
783
+ if (resolved.cleanup)
784
+ fs6.rmSync(resolved.cleanup, { recursive: true, force: true });
580
785
  }
581
786
  const manifestPath = path7.join(destDir, ".claude-plugin", "plugin.json");
582
787
  if (!resolved.builtin && fs6.existsSync(manifestPath)) {
@@ -590,7 +795,7 @@ async function addPlugin(startDir, templateArg, nameArg, opts = {}) {
590
795
  }
591
796
  }
592
797
  const template = resolved.label;
593
- const source = mp.metadata?.pluginRoot ? name : `./${path7.relative(root, destDir).split(path7.sep).join("/")}`;
798
+ const source = `./${path7.relative(root, destDir).split(path7.sep).join("/")}`;
594
799
  mp.plugins.push({ name, source, description, version: "0.1.0" });
595
800
  writeMarketplace(root, mp);
596
801
  await syncCommand(root, { quiet: true });
@@ -611,6 +816,7 @@ function cancel2() {
611
816
 
612
817
  // src/commands/bump.ts
613
818
  import { execFileSync as execFileSync2 } from "child_process";
819
+ import fs7 from "fs";
614
820
  import path8 from "path";
615
821
  import * as p4 from "@clack/prompts";
616
822
  import pc3 from "picocolors";
@@ -656,7 +862,8 @@ function analyzeCommits(root, pluginDir, since) {
656
862
  return { level: void 0, commits: 0, reasons: ["not a git repository"] };
657
863
  }
658
864
  const commits = raw.split("").map((c) => c.trim()).filter(Boolean);
659
- if (commits.length === 0) return { level: void 0, commits: 0, reasons: [] };
865
+ if (commits.length === 0)
866
+ return { level: void 0, commits: 0, reasons: [] };
660
867
  let level = "patch";
661
868
  const reasons = [];
662
869
  for (const c of commits) {
@@ -678,7 +885,9 @@ function analyzeCommits(root, pluginDir, since) {
678
885
  async function bumpCommand(startDir, pluginName, levelArg, opts = {}) {
679
886
  const root = findMarketplaceRoot(startDir);
680
887
  if (!root) {
681
- p4.log.error("No .claude-plugin/marketplace.json found. Run `agkit init` first.");
888
+ p4.log.error(
889
+ "No .claude-plugin/marketplace.json found. Run `agkit init` first."
890
+ );
682
891
  process.exitCode = 1;
683
892
  return;
684
893
  }
@@ -707,20 +916,24 @@ async function bumpCommand(startDir, pluginName, levelArg, opts = {}) {
707
916
  }
708
917
  const local = locals.find((l) => (l.manifest.name || l.dirName) === name);
709
918
  if (!local) {
710
- p4.log.error(`No local plugin named "${name}". Known: ${locals.map((l) => l.manifest.name || l.dirName).join(", ")}`);
919
+ p4.log.error(
920
+ `No local plugin named "${name}". Known: ${locals.map((l) => l.manifest.name || l.dirName).join(", ")}`
921
+ );
711
922
  process.exitCode = 1;
712
923
  return;
713
924
  }
714
925
  if (levelArg && !["major", "minor", "patch", "auto"].includes(levelArg)) {
715
- p4.log.error(`Invalid level "${levelArg}". Use major, minor, patch, or auto.`);
926
+ p4.log.error(
927
+ `Invalid level "${levelArg}". Use major, minor, patch, or auto.`
928
+ );
716
929
  process.exitCode = 1;
717
930
  return;
718
931
  }
719
932
  const current = local.manifest.version ?? "0.1.0";
933
+ const sinceTag = lastReleaseTag(root, name);
934
+ const analysis = analyzeCommits(root, local.dir, sinceTag);
720
935
  let level;
721
936
  if (!levelArg || levelArg === "auto") {
722
- const sinceTag = lastReleaseTag(root, name);
723
- const analysis = analyzeCommits(root, local.dir, sinceTag);
724
937
  if (!analysis.level) {
725
938
  p4.log.info(
726
939
  `No commits touching ${path8.relative(root, local.dir)}${sinceTag ? ` since ${sinceTag}` : ""} \u2014 nothing to bump.`
@@ -738,14 +951,32 @@ async function bumpCommand(startDir, pluginName, levelArg, opts = {}) {
738
951
  }
739
952
  const next = incrementSemver(current, level);
740
953
  const tagName = `${name}@${next}`;
954
+ const subjects = analysis.commits > 0 ? analysis.reasons.map((r) => r.replace(/^(?:major|minor|patch)\s+/, "")) : [];
741
955
  if (opts.dryRun) {
742
- p4.log.info(`[dry-run] ${name}: ${current} -> ${pc3.green(next)} (${level})${opts.tag ? ` + tag ${tagName}` : ""}`);
956
+ p4.log.info(
957
+ `[dry-run] ${name}: ${current} -> ${pc3.green(next)} (${level})${opts.tag ? ` + tag ${tagName}` : ""} + CHANGELOG.md entry`
958
+ );
743
959
  return;
744
960
  }
745
- const manifestPath = path8.join(local.dir, MARKETPLACE_DIR, PLUGIN_MANIFEST_FILE);
961
+ const manifestPath = path8.join(
962
+ local.dir,
963
+ MARKETPLACE_DIR,
964
+ PLUGIN_MANIFEST_FILE
965
+ );
746
966
  const manifest = readJson(manifestPath);
747
967
  manifest.version = next;
748
968
  writeJson(manifestPath, manifest);
969
+ const changelogPath = path8.join(local.dir, "CHANGELOG.md");
970
+ const existingChangelog = fs7.existsSync(changelogPath) ? fs7.readFileSync(changelogPath, "utf8") : void 0;
971
+ const entry = renderChangelogEntry(
972
+ next,
973
+ subjects,
974
+ (/* @__PURE__ */ new Date()).toISOString().slice(0, 10)
975
+ );
976
+ fs7.writeFileSync(
977
+ changelogPath,
978
+ prependChangelogEntry(existingChangelog, entry)
979
+ );
749
980
  await syncCommand(root, { quiet: true });
750
981
  p4.log.success(`${name}: ${current} -> ${pc3.green(next)} (${level})`);
751
982
  if (opts.tag) {
@@ -753,17 +984,23 @@ async function bumpCommand(startDir, pluginName, levelArg, opts = {}) {
753
984
  git(root, ["add", "-A"]);
754
985
  git(root, ["commit", "-m", `chore(release): ${tagName}`]);
755
986
  git(root, ["tag", tagName]);
756
- p4.log.success(`Committed and tagged ${pc3.cyan(tagName)} \u2014 push with: git push --follow-tags`);
987
+ p4.log.success(
988
+ `Committed and tagged ${pc3.cyan(tagName)} \u2014 push with: git push --follow-tags`
989
+ );
757
990
  } catch (err) {
758
- p4.log.warn(`Version bumped but commit/tag failed: ${err.message}`);
991
+ p4.log.warn(
992
+ `Version bumped but commit/tag failed: ${err.message}`
993
+ );
759
994
  }
760
995
  } else {
761
- p4.log.info("Commit the change, then push. Use --tag to commit and tag in one step.");
996
+ p4.log.info(
997
+ "Commit the change, then push. Use --tag to commit and tag in one step."
998
+ );
762
999
  }
763
1000
  }
764
1001
 
765
1002
  // src/commands/init.ts
766
- import fs8 from "fs";
1003
+ import fs9 from "fs";
767
1004
  import path10 from "path";
768
1005
  import * as p5 from "@clack/prompts";
769
1006
  import pc4 from "picocolors";
@@ -774,7 +1011,10 @@ var claudeCode = {
774
1011
  label: "Claude Code",
775
1012
  tier: 1,
776
1013
  install: (arg, name) => ({
777
- lines: [`/plugin marketplace add ${arg}`, `/plugin install <plugin-name>@${name}`]
1014
+ lines: [
1015
+ `/plugin marketplace add ${arg}`,
1016
+ `/plugin install <plugin-name>@${name}`
1017
+ ]
778
1018
  }),
779
1019
  teamSettings: { path: ".claude/settings.json" }
780
1020
  };
@@ -805,7 +1045,9 @@ var cursor = {
805
1045
  label: "Cursor",
806
1046
  tier: 2,
807
1047
  install: () => ({
808
- lines: ["# add the marketplace in Cursor, then: /plugin install <plugin-name>"],
1048
+ lines: [
1049
+ "# add the marketplace in Cursor, then: /plugin install <plugin-name>"
1050
+ ],
809
1051
  note: "Requires a committed Cursor registry (.cursor-plugin/). Generate it with `agkit build --target cursor` (planned)."
810
1052
  })
811
1053
  };
@@ -911,6 +1153,12 @@ The catalog lives in \`.claude-plugin/marketplace.json\`; each plugin lives unde
911
1153
 
912
1154
  Target agents: ${labels}.
913
1155
 
1156
+ ## Plugins
1157
+
1158
+ ${PLUGINS_START}
1159
+ ${renderAgentsPluginList([])}
1160
+ ${PLUGINS_END}
1161
+
914
1162
  ## For agents working in this repo
915
1163
 
916
1164
  - Plugins are self-contained under \`plugins/<name>/\` with a \`.claude-plugin/plugin.json\` manifest and \`skills/\`, \`commands/\`, \`agents/\`, \`hooks/\` as needed.
@@ -921,7 +1169,7 @@ Target agents: ${labels}.
921
1169
 
922
1170
  // src/lib/git.ts
923
1171
  import { execFileSync as execFileSync3 } from "child_process";
924
- import fs7 from "fs";
1172
+ import fs8 from "fs";
925
1173
  import path9 from "path";
926
1174
  function isGitAvailable() {
927
1175
  try {
@@ -932,7 +1180,16 @@ function isGitAvailable() {
932
1180
  }
933
1181
  }
934
1182
  function isGitRepo(dir) {
935
- return fs7.existsSync(path9.join(dir, ".git"));
1183
+ return fs8.existsSync(path9.join(dir, ".git"));
1184
+ }
1185
+ function isInsideGitRepo(dir) {
1186
+ let d = path9.resolve(dir);
1187
+ for (; ; ) {
1188
+ if (fs8.existsSync(path9.join(d, ".git"))) return true;
1189
+ const parent = path9.dirname(d);
1190
+ if (parent === d) return false;
1191
+ d = parent;
1192
+ }
936
1193
  }
937
1194
  function gitInit(dir) {
938
1195
  execFileSync3("git", ["init", "--initial-branch=main"], {
@@ -954,7 +1211,9 @@ function parseRemoteUrl(raw) {
954
1211
  let host;
955
1212
  let repoPath;
956
1213
  const scp = raw.match(/^(?:[\w.-]+)@([\w.-]+):(.+?)(?:\.git)?\/?$/);
957
- const url = raw.match(/^(?:https?|ssh|git):\/\/(?:[\w.-]+@)?([\w.-]+(?::\d+)?)\/(.+?)(?:\.git)?\/?$/);
1214
+ const url = raw.match(
1215
+ /^(?:https?|ssh|git):\/\/(?:[\w.-]+@)?([\w.-]+(?::\d+)?)\/(.+?)(?:\.git)?\/?$/
1216
+ );
958
1217
  if (scp) {
959
1218
  host = scp[1];
960
1219
  repoPath = scp[2];
@@ -976,6 +1235,9 @@ function parseRemoteUrl(raw) {
976
1235
  }
977
1236
 
978
1237
  // src/commands/init.ts
1238
+ function titleCase2(kebab) {
1239
+ return kebab.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1240
+ }
979
1241
  function validateName(name) {
980
1242
  if (!KEBAB_CASE_RE.test(name)) {
981
1243
  return "Name must be kebab-case (lowercase letters, digits, hyphens)";
@@ -986,22 +1248,151 @@ function validateName(name) {
986
1248
  return void 0;
987
1249
  }
988
1250
  function marketplaceTpl(file) {
989
- return fs8.readFileSync(path10.join(templatesDir(), "marketplace", file), "utf8");
1251
+ return fs9.readFileSync(
1252
+ path10.join(templatesDir(), "marketplace", file),
1253
+ "utf8"
1254
+ );
990
1255
  }
991
1256
  function addArgument(remote) {
992
1257
  if (!remote) return "<git-url-or-owner/repo>";
993
1258
  return remote.isGitHub && remote.slug ? remote.slug : remote.httpsUrl;
994
1259
  }
995
1260
  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");
1261
+ const source = remote?.isGitHub && remote.slug ? { source: "github", repo: remote.slug } : {
1262
+ source: "url",
1263
+ url: remote?.httpsUrl ?? "https://<git-host>/<owner>/<repo>.git"
1264
+ };
1265
+ return JSON.stringify(source, null, 2).split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n");
1266
+ }
1267
+ async function initPlugin(targetDir, templateSpec, opts) {
1268
+ p5.intro(pc4.bgCyan(pc4.black(" agkit init --plugin ")));
1269
+ const defaultName = path10.basename(targetDir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1270
+ let spec = typeof templateSpec === "string" ? templateSpec : void 0;
1271
+ if (!spec) {
1272
+ if (opts.yes) {
1273
+ spec = "skill";
1274
+ } else {
1275
+ const answer = await p5.select({
1276
+ message: "Plugin template",
1277
+ options: PLUGIN_TEMPLATES.map((t) => ({
1278
+ value: t,
1279
+ label: t,
1280
+ hint: TEMPLATE_DESCRIPTIONS[t]
1281
+ }))
1282
+ });
1283
+ if (p5.isCancel(answer)) return cancel5();
1284
+ spec = answer;
1285
+ }
1286
+ }
1287
+ let name = opts.name;
1288
+ if (!name && !opts.yes) {
1289
+ const answer = await p5.text({
1290
+ message: "Plugin name (kebab-case)",
1291
+ initialValue: defaultName,
1292
+ validate: (v) => KEBAB_CASE_RE.test(v) ? void 0 : "Must be kebab-case (lowercase, digits, hyphens)"
1293
+ });
1294
+ if (p5.isCancel(answer)) return cancel5();
1295
+ name = answer;
1296
+ }
1297
+ name = name || defaultName;
1298
+ if (!KEBAB_CASE_RE.test(name)) {
1299
+ p5.log.error(`Plugin name "${name}" must be kebab-case.`);
1300
+ process.exitCode = 1;
1301
+ return;
1302
+ }
1303
+ let description = opts.description;
1304
+ if (description === void 0 && !opts.yes) {
1305
+ const answer = await p5.text({
1306
+ message: "One-line description",
1307
+ defaultValue: "",
1308
+ placeholder: `What does ${name} do?`
1309
+ });
1310
+ if (p5.isCancel(answer)) return cancel5();
1311
+ description = answer;
1312
+ }
1313
+ description = description || `TODO: describe what ${name} does.`;
1314
+ const manifestPath = path10.join(targetDir, ".claude-plugin", "plugin.json");
1315
+ if (fs9.existsSync(manifestPath)) {
1316
+ p5.log.error(`A plugin already exists here: ${manifestPath}`);
1317
+ process.exitCode = 1;
1318
+ return;
1319
+ }
1320
+ const s = p5.spinner();
1321
+ s.start("Scaffolding plugin");
1322
+ fs9.mkdirSync(targetDir, { recursive: true });
1323
+ const vars = {
1324
+ pluginName: name,
1325
+ pluginTitle: titleCase2(name),
1326
+ description,
1327
+ authorName: opts.owner || "Unknown"
1328
+ };
1329
+ let resolved;
1330
+ try {
1331
+ resolved = resolveTemplate(spec);
1332
+ } catch (err) {
1333
+ s.stop("Scaffolding failed");
1334
+ p5.log.error(err.message);
1335
+ process.exitCode = 1;
1336
+ return;
1337
+ }
1338
+ try {
1339
+ copyTemplateDir(resolved.dir, targetDir, vars);
1340
+ } finally {
1341
+ if (resolved.cleanup)
1342
+ fs9.rmSync(resolved.cleanup, { recursive: true, force: true });
1343
+ }
1344
+ if (!resolved.builtin && fs9.existsSync(manifestPath)) {
1345
+ try {
1346
+ const manifest = readJson(manifestPath);
1347
+ manifest.name = name;
1348
+ if (opts.description) manifest.description = description;
1349
+ writeJson(manifestPath, manifest);
1350
+ } catch {
1351
+ p5.log.warn("Template plugin.json could not be parsed; left as-is.");
1352
+ }
1353
+ }
1354
+ const standalone = !isInsideGitRepo(targetDir);
1355
+ if (standalone) {
1356
+ const gitignore = path10.join(targetDir, ".gitignore");
1357
+ if (!fs9.existsSync(gitignore)) {
1358
+ fs9.writeFileSync(
1359
+ gitignore,
1360
+ ["node_modules/", ".DS_Store", "*.log", ""].join("\n")
1361
+ );
1362
+ }
1363
+ if (isGitAvailable()) {
1364
+ try {
1365
+ gitInit(targetDir);
1366
+ } catch {
1367
+ p5.log.warn("git init failed; initialize the repository manually.");
1368
+ }
1369
+ }
1370
+ }
1371
+ s.stop("Plugin scaffolded");
1372
+ const rel = path10.relative(process.cwd(), targetDir) || ".";
1373
+ p5.note(
1374
+ [
1375
+ `Standalone plugin "${name}" (${resolved.label}) \u2014 no marketplace was created.`,
1376
+ "",
1377
+ "Distribute it either way:",
1378
+ " \u2022 Push this folder to its own git repo, then reference it from a",
1379
+ ` marketplace: agkit add <owner/repo> ${name}`,
1380
+ " \u2022 Or drop it into an existing marketplace's plugins/ and run:",
1381
+ " agkit sync"
1382
+ ].join("\n"),
1383
+ `Next steps (cd ${rel})`
1384
+ );
1385
+ p5.outro("Done \u2714");
998
1386
  }
999
1387
  async function initCommand(dirArg, opts) {
1000
- p5.intro(pc4.bgCyan(pc4.black(" agkit init ")));
1001
1388
  const targetDir = path10.resolve(dirArg ?? ".");
1389
+ if (opts.plugin) {
1390
+ return initPlugin(targetDir, opts.plugin, opts);
1391
+ }
1392
+ p5.intro(pc4.bgCyan(pc4.black(" agkit init ")));
1002
1393
  const defaultName = path10.basename(targetDir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1003
1394
  const gitAvailable = isGitAvailable();
1004
- const existingRemote = gitAvailable && fs8.existsSync(targetDir) ? getOriginUrl(targetDir) : void 0;
1395
+ const existingRemote = gitAvailable && fs9.existsSync(targetDir) ? getOriginUrl(targetDir) : void 0;
1005
1396
  let name = opts.name;
1006
1397
  let owner = opts.owner;
1007
1398
  let email = opts.email;
@@ -1091,7 +1482,9 @@ async function initCommand(dirArg, opts) {
1091
1482
  }
1092
1483
  owner = owner || "Unknown Owner";
1093
1484
  ci = ci ?? "github";
1094
- const { ids: agentIds, unknown } = parseAgentList(agentsRaw ?? DEFAULT_AGENTS.join(","));
1485
+ const { ids: agentIds, unknown } = parseAgentList(
1486
+ agentsRaw ?? DEFAULT_AGENTS.join(",")
1487
+ );
1095
1488
  if (unknown.length > 0) {
1096
1489
  p5.log.warn(
1097
1490
  `Unknown agent(s) ignored: ${unknown.join(", ")}. Known: ${AGENT_ADAPTERS.map((a) => a.id).join(", ")}.`
@@ -1100,11 +1493,13 @@ async function initCommand(dirArg, opts) {
1100
1493
  const targets = agentIds.length > 0 ? agentIds : [...DEFAULT_AGENTS];
1101
1494
  const remote = repoUrl ? parseRemoteUrl(repoUrl) : void 0;
1102
1495
  if (repoUrl && !remote) {
1103
- p5.log.warn(`Could not parse remote URL "${repoUrl}"; install instructions will use placeholders.`);
1496
+ p5.log.warn(
1497
+ `Could not parse remote URL "${repoUrl}"; install instructions will use placeholders.`
1498
+ );
1104
1499
  }
1105
1500
  const s = p5.spinner();
1106
1501
  s.start("Scaffolding marketplace");
1107
- fs8.mkdirSync(targetDir, { recursive: true });
1502
+ fs9.mkdirSync(targetDir, { recursive: true });
1108
1503
  const marketplace = {
1109
1504
  $schema: MARKETPLACE_SCHEMA_URL,
1110
1505
  name,
@@ -1117,9 +1512,12 @@ async function initCommand(dirArg, opts) {
1117
1512
  },
1118
1513
  plugins: []
1119
1514
  };
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"), "");
1515
+ writeJson(
1516
+ path10.join(targetDir, ".claude-plugin", "marketplace.json"),
1517
+ marketplace
1518
+ );
1519
+ fs9.mkdirSync(path10.join(targetDir, "plugins"), { recursive: true });
1520
+ fs9.writeFileSync(path10.join(targetDir, "plugins", ".gitkeep"), "");
1123
1521
  const desc = description || "A curated collection of agent plugins.";
1124
1522
  const mktArg = addArgument(remote);
1125
1523
  const teamBlock = teamSource(remote);
@@ -1131,34 +1529,34 @@ async function initCommand(dirArg, opts) {
1131
1529
  installSections: renderInstallSections(targets, mktArg, name),
1132
1530
  teamSections: renderTeamSections(targets, teamBlock, name)
1133
1531
  };
1134
- fs8.writeFileSync(
1532
+ fs9.writeFileSync(
1135
1533
  path10.join(targetDir, "README.md"),
1136
1534
  renderTemplate(marketplaceTpl("README.md.tpl"), vars)
1137
1535
  );
1138
- fs8.writeFileSync(
1536
+ fs9.writeFileSync(
1139
1537
  path10.join(targetDir, "AGENTS.md"),
1140
1538
  renderAgentsMd(name, desc, targets)
1141
1539
  );
1142
1540
  const exampleDir = path10.join(targetDir, "examples");
1143
- fs8.mkdirSync(exampleDir, { recursive: true });
1144
- fs8.writeFileSync(
1541
+ fs9.mkdirSync(exampleDir, { recursive: true });
1542
+ fs9.writeFileSync(
1145
1543
  path10.join(exampleDir, "team-settings.json"),
1146
1544
  renderTemplate(marketplaceTpl("team-settings.json.tpl"), vars)
1147
1545
  );
1148
1546
  if (ci === "github") {
1149
1547
  const wfDir = path10.join(targetDir, ".github", "workflows");
1150
- fs8.mkdirSync(wfDir, { recursive: true });
1151
- fs8.writeFileSync(
1548
+ fs9.mkdirSync(wfDir, { recursive: true });
1549
+ fs9.writeFileSync(
1152
1550
  path10.join(wfDir, "validate.yml"),
1153
1551
  renderTemplate(marketplaceTpl("github-validate.yml.tpl"), vars)
1154
1552
  );
1155
1553
  } else if (ci === "gitlab") {
1156
- fs8.writeFileSync(
1554
+ fs9.writeFileSync(
1157
1555
  path10.join(targetDir, ".gitlab-ci.yml"),
1158
1556
  renderTemplate(marketplaceTpl("gitlab-ci.yml.tpl"), vars)
1159
1557
  );
1160
1558
  }
1161
- fs8.writeFileSync(
1559
+ fs9.writeFileSync(
1162
1560
  path10.join(targetDir, ".gitignore"),
1163
1561
  ["node_modules/", ".DS_Store", "*.log", ""].join("\n")
1164
1562
  );
@@ -1229,14 +1627,16 @@ async function listCommand() {
1229
1627
 
1230
1628
  // src/commands/validate.ts
1231
1629
  import { execFileSync as execFileSync4 } from "child_process";
1232
- import fs9 from "fs";
1630
+ import fs10 from "fs";
1233
1631
  import path11 from "path";
1234
1632
  import * as p7 from "@clack/prompts";
1235
1633
  import pc6 from "picocolors";
1236
1634
  async function validateCommand(startDir, opts = {}) {
1237
1635
  const root = findMarketplaceRoot(startDir);
1238
1636
  if (!root) {
1239
- p7.log.error("No .claude-plugin/marketplace.json found. Run `agkit init` first.");
1637
+ p7.log.error(
1638
+ "No .claude-plugin/marketplace.json found. Run `agkit init` first."
1639
+ );
1240
1640
  process.exitCode = 1;
1241
1641
  return;
1242
1642
  }
@@ -1252,7 +1652,10 @@ async function validateCommand(startDir, opts = {}) {
1252
1652
  }
1253
1653
  if (mp) {
1254
1654
  if (!mp.name) {
1255
- findings.push({ level: "error", message: "Missing required field: name" });
1655
+ findings.push({
1656
+ level: "error",
1657
+ message: "Missing required field: name"
1658
+ });
1256
1659
  } else {
1257
1660
  if (!KEBAB_CASE_RE.test(mp.name)) {
1258
1661
  findings.push({
@@ -1268,7 +1671,10 @@ async function validateCommand(startDir, opts = {}) {
1268
1671
  }
1269
1672
  }
1270
1673
  if (!mp.owner?.name) {
1271
- findings.push({ level: "error", message: "Missing required field: owner.name" });
1674
+ findings.push({
1675
+ level: "error",
1676
+ message: "Missing required field: owner.name"
1677
+ });
1272
1678
  }
1273
1679
  if (!Array.isArray(mp.plugins)) {
1274
1680
  findings.push({ level: "error", message: "plugins must be an array" });
@@ -1277,7 +1683,10 @@ async function validateCommand(startDir, opts = {}) {
1277
1683
  for (const entry of mp.plugins ?? []) {
1278
1684
  const label = entry.name ?? "<unnamed>";
1279
1685
  if (!entry.name) {
1280
- findings.push({ level: "error", message: "A plugin entry is missing its name" });
1686
+ findings.push({
1687
+ level: "error",
1688
+ message: "A plugin entry is missing its name"
1689
+ });
1281
1690
  continue;
1282
1691
  }
1283
1692
  if (!KEBAB_CASE_RE.test(entry.name)) {
@@ -1287,30 +1696,42 @@ async function validateCommand(startDir, opts = {}) {
1287
1696
  });
1288
1697
  }
1289
1698
  if (seen.has(entry.name)) {
1290
- findings.push({ level: "error", message: `Duplicate plugin name "${label}"` });
1699
+ findings.push({
1700
+ level: "error",
1701
+ message: `Duplicate plugin name "${label}"`
1702
+ });
1291
1703
  }
1292
1704
  seen.add(entry.name);
1293
1705
  if (entry.source === void 0) {
1294
- findings.push({ level: "error", message: `Plugin "${label}" is missing source` });
1706
+ findings.push({
1707
+ level: "error",
1708
+ message: `Plugin "${label}" is missing source`
1709
+ });
1295
1710
  continue;
1296
1711
  }
1297
1712
  const localDir = resolveLocalPluginDir(root, mp, entry);
1298
1713
  if (localDir) {
1299
- if (!fs9.existsSync(localDir)) {
1714
+ if (!fs10.existsSync(localDir)) {
1300
1715
  findings.push({
1301
1716
  level: "error",
1302
1717
  message: `Plugin "${label}" source resolves to missing directory ${path11.relative(root, localDir)}`
1303
1718
  });
1304
1719
  } else {
1305
- const manifestPath = path11.join(localDir, MARKETPLACE_DIR, PLUGIN_MANIFEST_FILE);
1306
- if (!fs9.existsSync(manifestPath)) {
1720
+ const manifestPath = path11.join(
1721
+ localDir,
1722
+ MARKETPLACE_DIR,
1723
+ PLUGIN_MANIFEST_FILE
1724
+ );
1725
+ if (!fs10.existsSync(manifestPath)) {
1307
1726
  findings.push({
1308
1727
  level: "error",
1309
1728
  message: `Plugin "${label}" has no ${MARKETPLACE_DIR}/${PLUGIN_MANIFEST_FILE}`
1310
1729
  });
1311
1730
  } else {
1312
1731
  try {
1313
- const manifest = JSON.parse(fs9.readFileSync(manifestPath, "utf8"));
1732
+ const manifest = JSON.parse(
1733
+ fs10.readFileSync(manifestPath, "utf8")
1734
+ );
1314
1735
  if (manifest.name && manifest.name !== entry.name) {
1315
1736
  findings.push({
1316
1737
  level: "warn",
@@ -1333,7 +1754,9 @@ async function validateCommand(startDir, opts = {}) {
1333
1754
  }
1334
1755
  }
1335
1756
  }
1336
- const hasRelative = (mp.plugins ?? []).some((e) => typeof e.source === "string");
1757
+ const hasRelative = (mp.plugins ?? []).some(
1758
+ (e) => typeof e.source === "string"
1759
+ );
1337
1760
  if (hasRelative) {
1338
1761
  p7.log.info(
1339
1762
  "Note: relative plugin sources resolve only when the marketplace is added via Git (any forge), not via a direct URL to marketplace.json."
@@ -1341,7 +1764,9 @@ async function validateCommand(startDir, opts = {}) {
1341
1764
  }
1342
1765
  }
1343
1766
  if (mp?.metadata?.targets) {
1344
- const needBuild = mp.metadata.targets.filter((id) => (getAdapter(id)?.tier ?? 1) > 1);
1767
+ const needBuild = mp.metadata.targets.filter(
1768
+ (id) => (getAdapter(id)?.tier ?? 1) > 1
1769
+ );
1345
1770
  if (needBuild.length > 0) {
1346
1771
  p7.log.info(
1347
1772
  `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.`
@@ -1350,7 +1775,11 @@ async function validateCommand(startDir, opts = {}) {
1350
1775
  }
1351
1776
  let officialRan = false;
1352
1777
  try {
1353
- execFileSync4("claude", ["plugin", "validate", root, ...opts.strict ? ["--strict"] : []], { stdio: "pipe" });
1778
+ execFileSync4(
1779
+ "claude",
1780
+ ["plugin", "validate", root, ...opts.strict ? ["--strict"] : []],
1781
+ { stdio: "pipe" }
1782
+ );
1354
1783
  officialRan = true;
1355
1784
  p7.log.success("claude plugin validate: passed");
1356
1785
  } catch (err) {
@@ -1364,7 +1793,8 @@ async function validateCommand(startDir, opts = {}) {
1364
1793
  const out = [e.stdout?.toString(), e.stderr?.toString()].filter(Boolean).join("\n").trim();
1365
1794
  findings.push({
1366
1795
  level: "error",
1367
- message: `claude plugin validate failed${out ? ":\n" + out : ""}`
1796
+ message: `claude plugin validate failed${out ? `:
1797
+ ${out}` : ""}`
1368
1798
  });
1369
1799
  }
1370
1800
  }
@@ -1373,7 +1803,11 @@ async function validateCommand(startDir, opts = {}) {
1373
1803
  for (const f of warns) p7.log.warn(f.message);
1374
1804
  for (const f of errors) p7.log.error(f.message);
1375
1805
  if (errors.length > 0) {
1376
- p7.log.error(pc6.red(`Validation failed: ${errors.length} error(s), ${warns.length} warning(s).`));
1806
+ p7.log.error(
1807
+ pc6.red(
1808
+ `Validation failed: ${errors.length} error(s), ${warns.length} warning(s).`
1809
+ )
1810
+ );
1377
1811
  process.exitCode = 1;
1378
1812
  } else {
1379
1813
  p7.log.success(
@@ -1392,26 +1826,78 @@ var program = new Command();
1392
1826
  program.name("agkit").description(
1393
1827
  "Scaffold and manage plugin marketplaces for Claude Code and GitHub Copilot, distributed via any Git host."
1394
1828
  ).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) => {
1829
+ program.command("init").argument("[dir]", "target directory (default: current directory)").description(
1830
+ "Scaffold a new plugin marketplace (git-first), or a standalone plugin with --plugin"
1831
+ ).option(
1832
+ "-n, --name <name>",
1833
+ "marketplace (or plugin, with --plugin) name (kebab-case)"
1834
+ ).option("-o, --owner <owner>", "owner / plugin author name").option("-e, --email <email>", "owner email").option(
1835
+ "-d, --description <text>",
1836
+ "marketplace (or plugin, with --plugin) description"
1837
+ ).option("-r, --repo <url>", "git remote URL (any forge)").option("--ci <ci>", "CI workflow: github | gitlab | none").option(
1838
+ "--agents <list>",
1839
+ "comma-separated target agents (e.g. claude-code,copilot,codex)"
1840
+ ).option(
1841
+ "--plugin [template]",
1842
+ "scaffold a standalone plugin (skill|command|agent|hook|mcp, or a template spec) in [dir] instead of a marketplace"
1843
+ ).option("-y, --yes", "non-interactive, accept defaults").action(async (dir, opts) => {
1396
1844
  await initCommand(dir, opts);
1397
1845
  });
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) => {
1846
+ program.command("add").argument(
1847
+ "[template]",
1848
+ "built-in template name, local path, or remote git source (owner/repo, gh:/gl: shorthand, git URL). Remote sources are referenced, not cloned (see --vendor)."
1849
+ ).argument("[name]", "plugin name (kebab-case)").description(
1850
+ "Register a plugin in the catalog: reference a remote git repo, or scaffold from a built-in/local template"
1851
+ ).option("-d, --description <text>", "one-line plugin description").option(
1852
+ "--vendor",
1853
+ "clone the remote repo and scaffold from its files into the marketplace, instead of referencing it"
1854
+ ).option("--ref <ref>", "branch or tag to pin a referenced remote source to").option(
1855
+ "--sha <sha>",
1856
+ "commit hash (40 hex) to pin a referenced remote source to"
1857
+ ).action(async (template, name, opts) => {
1399
1858
  await addPlugin(process.cwd(), template, name, {
1400
1859
  description: opts.description,
1401
- interactive: opts.description === void 0
1860
+ interactive: opts.description === void 0,
1861
+ vendor: opts.vendor,
1862
+ ref: opts.ref,
1863
+ sha: opts.sha
1402
1864
  });
1403
1865
  });
1404
1866
  program.command("sync").description("Reconcile marketplace.json and README with the plugins on disk").action(async () => {
1405
1867
  await syncCommand(process.cwd());
1406
1868
  });
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) => {
1869
+ program.command("validate").description(
1870
+ "Validate the catalog (local checks + `claude plugin validate` if available)"
1871
+ ).option("--strict", "treat warnings as errors in the official validator").action(async (opts) => {
1408
1872
  await validateCommand(process.cwd(), { strict: opts.strict });
1409
1873
  });
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 });
1874
+ program.command("bump").argument("[plugin]", "plugin name (prompted if omitted)").argument(
1875
+ "[level]",
1876
+ "major | minor | patch | auto (default: auto, from conventional commits)"
1877
+ ).description(
1878
+ "Bump a plugin version (conventional-commit aware) and sync the catalog"
1879
+ ).option(
1880
+ "-t, --tag",
1881
+ "commit the bump and create a release tag <plugin>@<version>"
1882
+ ).option("--dry-run", "show what would change without writing").action(async (plugin, level, opts) => {
1883
+ await bumpCommand(process.cwd(), plugin, level, {
1884
+ tag: opts.tag,
1885
+ dryRun: opts.dryRun
1886
+ });
1412
1887
  });
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 });
1888
+ program.command("build").description(
1889
+ "Generate tier-2 agent artifacts (codex, cursor) from the catalog"
1890
+ ).option(
1891
+ "--target <list>",
1892
+ "comma-separated tier-2 targets (default: those in metadata.targets)"
1893
+ ).option(
1894
+ "--check",
1895
+ "verify generated artifacts are up to date (CI); non-zero exit on drift"
1896
+ ).action(async (opts) => {
1897
+ await buildCommand(process.cwd(), {
1898
+ targets: opts.target,
1899
+ check: opts.check
1900
+ });
1415
1901
  });
1416
1902
  program.command("list").description("List available plugin templates").action(async () => {
1417
1903
  await listCommand();