agkit 0.8.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 (3) hide show
  1. package/README.md +86 -10
  2. package/dist/index.js +262 -12
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -30,14 +30,14 @@ Think **`ng` for Angular, but for agent plugin marketplaces**: `init` gives you
30
30
  - 🌐 **Forge‑agnostic** — distribute via GitHub, GitLab, Bitbucket, Gitea, or self‑hosted, with a plain `git push`.
31
31
  - 📁 **One source of truth** — a single canonical `.claude-plugin/marketplace.json`; generated registries derive from it and never drift.
32
32
  - 🔁 **Full lifecycle** — `init`, `add`, `build`, `sync`, `bump`, `validate`, `list`.
33
- - 🧬 **Remote & local templates** — pull plugin templates from any git repo (`gh:` / `gl:` / URL) or a local path.
33
+ - 🔗 **Reference, vendor, or scaffold** — catalog a remote plugin **by reference** (no clone, the default), **vendor** a remote template into your repo with `--vendor`, or **scaffold** from a built-in / local template. Create a standalone plugin (no marketplace) with `init --plugin`.
34
34
  - ✅ **CI‑ready** — generated GitHub Actions / GitLab CI, with `validate` + `build --check` gates.
35
35
 
36
36
  ---
37
37
 
38
38
  ## 📦 Installation
39
39
 
40
- Requires **Node.js ≥ 18**. Run it on demand with `npx` (nothing to install), or install the CLI globally:
40
+ Requires **Node.js ≥ 22**. Run it on demand with `npx` (nothing to install), or install the CLI globally:
41
41
 
42
42
  ```bash
43
43
  npx agkit --help # run without installing
@@ -61,6 +61,8 @@ git add -A && git commit -m "feat: initial marketplace" && git push -u origin ma
61
61
 
62
62
  That's the whole loop — once pushed, anyone can install your plugins straight from the repo (the per‑agent install command is in the **Create a marketplace for your agent** section below). `init` targets **Claude Code** and **GitHub Copilot** by default; add `--agents codex,cursor` to serve more.
63
63
 
64
+ Step 2 has more shapes — reference a remote plugin without cloning it, vendor a template into your repo, or scaffold a plugin on its own with `agkit init --plugin`. All of them are laid out in **[Adding plugins](#-adding-plugins)**.
65
+
64
66
  ---
65
67
 
66
68
  ## 🧩 How agents consume your marketplace
@@ -211,8 +213,8 @@ git add -A && git commit -m "feat: initial marketplace" && git push -u origin ma
211
213
 
212
214
  | Command | What it does |
213
215
  | :------ | :----------- |
214
- | `agkit init [dir]` | Scaffolds a git-first marketplace: `.claude-plugin/marketplace.json` (with `$schema`, `pluginRoot`, `targets`), `plugins/`, `AGENTS.md`, a per-agent README, `examples/team-settings.json`, CI (GitHub Actions or GitLab CI), `.gitignore`, `git init`. Pick target agents with `--agents` (see the **Target agents** section). Non-interactive with `-y`. |
215
- | `agkit add <template\|spec> <name>` | Scaffolds a plugin from a built-in template (`skill`, `command`, `agent`, `hook`, `mcp`) **or a remote/local one** — `gh:owner/repo/dir#ref`, `gl:owner/repo`, any git URL with `//subdir` and `#ref`, or a local path. Registers it in the catalog and refreshes the README table and `AGENTS.md`. |
216
+ | `agkit init [dir]` | Scaffolds a git-first marketplace: `.claude-plugin/marketplace.json` (with `$schema`, `pluginRoot`, `targets`), `plugins/`, `AGENTS.md`, a per-agent README, `examples/team-settings.json`, CI (GitHub Actions or GitLab CI), `.gitignore`, `git init`. Pick target agents with `--agents` (see the **Target agents** section). Non-interactive with `-y`. Use **`--plugin [template]`** to instead scaffold a **standalone plugin** in `[dir]` — just `.claude-plugin/plugin.json` + content, its own git repo, **no marketplace** — ready to reference from any catalog (see **Adding plugins**). |
217
+ | `agkit add <template\|spec> <name>` | Registers a plugin in the catalog and refreshes the README table and `AGENTS.md`. A **remote git source** — bare `owner/repo`, `gh:owner/repo`, `gl:owner/repo`, or any git URL (`//subdir`, `#ref` supported) — is **referenced by default** as an object `source` (`github` / `url` / `git-subdir`): nothing is cloned, the agent fetches it at install time. Pin it with `--ref <branch\|tag>` / `--sha <commit>`. A **built-in template** (`skill`, `command`, `agent`, `hook`, `mcp`) or a **local path** is scaffolded into `plugins/<name>/`. Add **`--vendor`** to a remote source to clone and scaffold from it (treat the repo as a template) instead of referencing it. |
216
218
  | `agkit build [--target] [--check]` | Generates the registries for **Codex** and **Cursor** from the catalog: their committed `marketplace.json` + per-plugin manifest mirrors. Default targets are the ones in `metadata.targets`; `--target codex,cursor` overrides. `--check` fails on drift (CI). |
217
219
  | `agkit bump [plugin] [level]` | Bumps a plugin version from conventional commits scoped to its directory since the last `<plugin>@x.y.z` tag (`feat`→minor, breaking→major, else patch), or an explicit `major\|minor\|patch`. Prepends a dated entry to `plugins/<name>/CHANGELOG.md` from those commits. `--tag` commits and tags; `--dry-run` previews. Catalog, README, `AGENTS.md`, and any built registries stay in sync. |
218
220
  | `agkit sync` | Reconciles the catalog with what's on disk. Source of truth: each plugin's `.claude-plugin/plugin.json`. Adds missing entries, fixes drift, flags orphans, regenerates the README table and the `AGENTS.md` plugin list, and refreshes any already-built Codex/Cursor registry. |
@@ -251,17 +253,74 @@ Everything derives from `.claude-plugin/marketplace.json` and the per-plugin man
251
253
 
252
254
  ---
253
255
 
254
- ## 🧬 Team templates (remote or local)
256
+ ## 📥 Adding plugins
257
+
258
+ A plugin can live **inside** your marketplace (its files committed next to the catalog) or **outside** it (kept in its own repo and fetched by the agent at install time). `agkit add` handles both; `agkit init --plugin` scaffolds a plugin as a standalone repo. The table maps each intent to a command and the `source` it writes to `marketplace.json`:
259
+
260
+ | You want to… | Command | Resulting `source` | Files copied in? |
261
+ | :----------- | :------ | :----------------- | :--------------- |
262
+ | Scaffold a new plugin from a **built-in** template | `agkit add skill my-skill` | `"./plugins/my-skill"` | ✅ scaffolded |
263
+ | Scaffold from a **local** template directory | `agkit add ./templates/hook-guard no-secrets` | `"./plugins/no-secrets"` | ✅ scaffolded |
264
+ | Create a plugin **without a marketplace** | `agkit init --plugin skill my-skill` | *(its own repo)* | ✅ scaffolded |
265
+ | Reference a repo that **is a plugin** (GitHub) | `agkit add acme/deploy deploy` | `{ "source": "github", "repo": … }` | ❌ referenced |
266
+ | Reference a repo that **is a plugin** (any forge) | `agkit add https://gitlab.com/team/plugin.git gl` | `{ "source": "url", "url": … }` | ❌ referenced |
267
+ | Reference a plugin **inside a marketplace/monorepo** | `agkit add "https://gitlab.com/team/marketplace.git//my-plugin" my-plugin` | `{ "source": "git-subdir", "url": …, "path": "my-plugin" }` | ❌ referenced |
268
+ | **Vendor** a remote template into your marketplace | `agkit add gh:my-org/templates/kata-fr gilded-rose --vendor` | `"./plugins/gilded-rose"` | ✅ cloned + scaffolded |
269
+
270
+ Rule of thumb: **a remote git source is referenced by default** (no clone); pass **`--vendor`** only when you want its files copied into your repo.
271
+
272
+ ### Local plugins (scaffolded)
255
273
 
256
- Version your team's plugin templates in any git repository and consume them from every marketplace:
274
+ Built-in templates (`skill`, `command`, `agent`, `hook`, `mcp`) and local paths are scaffolded into `plugins/<name>/` and registered with a relative `"./plugins/<name>"` source:
257
275
 
258
276
  ```bash
259
- agkit add gh:my-org/plugin-templates/kata-fr gilded-rose
260
- agkit add "https://gitlab.company.io/craft/templates//skill-ddd#v2" tactical-ddd
261
- agkit add ./shared-templates/hook-guard no-secrets
277
+ agkit add skill tdd-coach # built-in template
278
+ agkit add ./shared-templates/hook-guard guard # a local template directory
262
279
  ```
263
280
 
264
- A template is any directory containing `.claude-plugin/plugin.json` (adopted, `name` rewritten) or `plugin.json.tpl`. Files ending in `.tpl` are rendered with `{{pluginName}}`, `{{pluginTitle}}`, `{{description}}`, `{{authorName}}` — in contents **and** in file/directory names. Executable bits are preserved.
281
+ A **template** is any directory containing `.claude-plugin/plugin.json` (adopted, `name` rewritten) or `plugin.json.tpl`. Files ending in `.tpl` are rendered with `{{pluginName}}`, `{{pluginTitle}}`, `{{description}}`, `{{authorName}}` — in contents **and** in file/directory names. Executable bits are preserved.
282
+
283
+ To create a plugin on its own, **outside any marketplace**, use `init --plugin`. It writes just `.claude-plugin/plugin.json` + the template content and makes the folder its own git repo (unless it's already inside one, e.g. dropped into a marketplace's `plugins/`):
284
+
285
+ ```bash
286
+ agkit init --plugin skill ./my-skill # standalone plugin repo, no marketplace
287
+ # push it somewhere, then reference it from any marketplace:
288
+ agkit add my-org/my-skill my-skill # (run inside a marketplace project)
289
+ ```
290
+
291
+ ### Remote plugins (referenced — the default)
292
+
293
+ When the spec is a **remote git repo**, agkit writes an object `source` the agent resolves at install time — **nothing is cloned**, no files are added to your marketplace. The spec grammar (bare `owner/repo`, `gh:`/`gl:` shorthands, git URLs, `//subdir`, `#ref`) maps to the schema's three git forms:
294
+
295
+ ```bash
296
+ # The repo IS the plugin (its .claude-plugin/plugin.json is at the root):
297
+ agkit add acme/deploy-plugin deploy # → { "source": "github", "repo": "acme/deploy-plugin" }
298
+ agkit add https://gitlab.com/team/plugin.git gl-tools # → { "source": "url", "url": "https://gitlab.com/team/plugin.git" }
299
+
300
+ # The plugin is a SUBDIRECTORY of a bigger repo — a marketplace or a monorepo.
301
+ # Point at the plugin's folder (it must contain .claude-plugin/plugin.json):
302
+ agkit add "https://gitlab.com/team/marketplace.git//my-plugin" my-plugin
303
+ # → { "source": "git-subdir", "url": "https://gitlab.com/team/marketplace.git", "path": "my-plugin" }
304
+ ```
305
+
306
+ Pin any referenced source to a branch/tag with `--ref` (or `#ref` in the spec) and to an exact commit with `--sha <40-hex>`:
307
+
308
+ ```bash
309
+ agkit add acme/deploy-plugin deploy --ref v2.0.0 --sha a1b2c3… # or agkit add acme/deploy-plugin#v2.0.0 deploy
310
+ ```
311
+
312
+ > **Referencing vs. consuming.** `git-subdir` points **directly at a plugin folder** — it does *not* read that repo's own `marketplace.json`. Use it to **re-catalog one plugin** from someone else's marketplace into yours. If you only want to *use* their plugins, add their marketplace directly instead: `/plugin marketplace add team/marketplace` then `/plugin install my-plugin@their-marketplace`.
313
+
314
+ ### Vendoring a remote template (`--vendor`)
315
+
316
+ Add **`--vendor`** to a remote spec to treat the repo as a **template**: agkit clones it (shallow), renders its `.tpl` files, and copies the result into `plugins/<name>/` — so the plugin now lives in, and releases with, your marketplace (relative `"./plugins/<name>"` source). Use it to seed a plugin from a shared team template:
317
+
318
+ ```bash
319
+ agkit add gh:my-org/plugin-templates/kata-fr gilded-rose --vendor
320
+ agkit add "https://gitlab.company.io/craft/templates//skill-ddd#v2" tactical-ddd --vendor
321
+ ```
322
+
323
+ Pin the vendored clone with `#ref` in the spec (as above). The clone's `.git` is never copied into the plugin.
265
324
 
266
325
  ---
267
326
 
@@ -277,6 +336,23 @@ A template is any directory containing `.claude-plugin/plugin.json` (adopted, `n
277
336
 
278
337
  ---
279
338
 
339
+ ## 🧪 Development
340
+
341
+ Requires **Node.js ≥ 22**. Clone, install, and run the same gate CI does:
342
+
343
+ ```bash
344
+ npm install
345
+ npm run typecheck # tsc --noEmit
346
+ npm run lint # biome check (lint:fix to autofix)
347
+ npm test # vitest run
348
+ npm run build # tsup → dist/index.js
349
+ node dist/index.js --help # run the built CLI
350
+ ```
351
+
352
+ Tests live next to the code as `*.test.ts` and cover both the pure `lib/` helpers and the command layer (`add`, `init --plugin`, `sync`, `validate`) via temp-directory fixtures — the Claude CLI is stubbed in `validate` tests so the suite stays hermetic. CI runs the gate on Node 22.x and 24.x.
353
+
354
+ ---
355
+
280
356
  ## 📄 License
281
357
 
282
358
  [MIT](LICENSE) © fair3n
package/dist/index.js CHANGED
@@ -183,6 +183,73 @@ function parseGitSpec(spec) {
183
183
  }
184
184
  return void 0;
185
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
+ }
186
253
  function assertTemplateDir(dir, label) {
187
254
  const hasManifest = fs3.existsSync(path3.join(dir, ".claude-plugin", "plugin.json.tpl")) || fs3.existsSync(path3.join(dir, ".claude-plugin", "plugin.json"));
188
255
  if (!hasManifest) {
@@ -630,6 +697,7 @@ async function addPlugin(startDir, templateArg, nameArg, opts = {}) {
630
697
  if (p3.isCancel(answer)) return cancel2();
631
698
  templateSpec = answer;
632
699
  }
700
+ const reference = isRemoteSpec(templateSpec) && opts.vendor !== true;
633
701
  let name = nameArg;
634
702
  if (!name) {
635
703
  const answer = await p3.text({
@@ -651,12 +719,6 @@ async function addPlugin(startDir, templateArg, nameArg, opts = {}) {
651
719
  process.exitCode = 1;
652
720
  return;
653
721
  }
654
- const destDir = path7.join(pluginRootDir(root, mp), name);
655
- if (fs6.existsSync(destDir)) {
656
- p3.log.error(`Directory already exists: ${destDir}`);
657
- process.exitCode = 1;
658
- return;
659
- }
660
722
  let description = opts.description;
661
723
  if (description === void 0 && opts.interactive !== false) {
662
724
  const answer = await p3.text({
@@ -668,6 +730,39 @@ async function addPlugin(startDir, templateArg, nameArg, opts = {}) {
668
730
  description = answer;
669
731
  }
670
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
+ }
671
766
  const vars = {
672
767
  pluginName: name,
673
768
  pluginTitle: titleCase(name),
@@ -1087,6 +1182,15 @@ function isGitAvailable() {
1087
1182
  function isGitRepo(dir) {
1088
1183
  return fs8.existsSync(path9.join(dir, ".git"));
1089
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
+ }
1193
+ }
1090
1194
  function gitInit(dir) {
1091
1195
  execFileSync3("git", ["init", "--initial-branch=main"], {
1092
1196
  cwd: dir,
@@ -1131,6 +1235,9 @@ function parseRemoteUrl(raw) {
1131
1235
  }
1132
1236
 
1133
1237
  // src/commands/init.ts
1238
+ function titleCase2(kebab) {
1239
+ return kebab.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1240
+ }
1134
1241
  function validateName(name) {
1135
1242
  if (!KEBAB_CASE_RE.test(name)) {
1136
1243
  return "Name must be kebab-case (lowercase letters, digits, hyphens)";
@@ -1157,9 +1264,132 @@ function teamSource(remote) {
1157
1264
  };
1158
1265
  return JSON.stringify(source, null, 2).split("\n").map((line, i) => i === 0 ? line : ` ${line}`).join("\n");
1159
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");
1386
+ }
1160
1387
  async function initCommand(dirArg, opts) {
1161
- p5.intro(pc4.bgCyan(pc4.black(" agkit init ")));
1162
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 ")));
1163
1393
  const defaultName = path10.basename(targetDir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1164
1394
  const gitAvailable = isGitAvailable();
1165
1395
  const existingRemote = gitAvailable && fs9.existsSync(targetDir) ? getOriginUrl(targetDir) : void 0;
@@ -1596,21 +1826,41 @@ var program = new Command();
1596
1826
  program.name("agkit").description(
1597
1827
  "Scaffold and manage plugin marketplaces for Claude Code and GitHub Copilot, distributed via any Git host."
1598
1828
  ).version(version);
1599
- 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(
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(
1600
1838
  "--agents <list>",
1601
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"
1602
1843
  ).option("-y, --yes", "non-interactive, accept defaults").action(async (dir, opts) => {
1603
1844
  await initCommand(dir, opts);
1604
1845
  });
1605
1846
  program.command("add").argument(
1606
1847
  "[template]",
1607
- "template name, local path, gh:/gl: shorthand, or git URL (see `agkit list`)"
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)."
1608
1849
  ).argument("[name]", "plugin name (kebab-case)").description(
1609
- "Scaffold a plugin from a template and register it in the catalog"
1610
- ).option("-d, --description <text>", "one-line plugin description").action(async (template, name, opts) => {
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) => {
1611
1858
  await addPlugin(process.cwd(), template, name, {
1612
1859
  description: opts.description,
1613
- interactive: opts.description === void 0
1860
+ interactive: opts.description === void 0,
1861
+ vendor: opts.vendor,
1862
+ ref: opts.ref,
1863
+ sha: opts.sha
1614
1864
  });
1615
1865
  });
1616
1866
  program.command("sync").description("Reconcile marketplace.json and README with the plugins on disk").action(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agkit",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Scaffold and manage plugin marketplaces for Claude Code and GitHub Copilot, distributed via any Git host (GitHub, GitLab, Bitbucket, Gitea, self-hosted). Init, add plugins from built-in or remote templates, sync, validate, bump.",
5
5
  "license": "MIT",
6
6
  "author": "fair3n <fairen.dev@gmail.com>",
@@ -21,7 +21,7 @@
21
21
  "LICENSE"
22
22
  ],
23
23
  "engines": {
24
- "node": ">=18.0.0"
24
+ "node": ">=22.0.0"
25
25
  },
26
26
  "scripts": {
27
27
  "build": "tsup",