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.
- package/README.md +86 -10
- package/dist/index.js +262 -12
- 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
|
-
-
|
|
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 ≥
|
|
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>` |
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
260
|
-
agkit add
|
|
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(
|
|
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,
|
|
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
|
-
"
|
|
1610
|
-
).option("-d, --description <text>", "one-line plugin 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) => {
|
|
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.
|
|
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": ">=
|
|
24
|
+
"node": ">=22.0.0"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsup",
|