pluginpack compiles one source of portable agent plugins (skills, agents,
commands, rules, hooks, MCP servers, assets, metadata) into the native plugin
layouts each AI app expects. It is a build tool: it copies files, writes the
manifests each target needs, and validates the result. It is not a package
manager or publisher.
npm run dev -- <args>— run the CLI from source (tsx src/cli.ts).npm test— vitest. Apretesthook builds first, because the conformance tests run the real built binary (dist/cli.js) viabintastic.npm run check— the full gate (test:all):format:check→lint→typecheck→test→build→docs. Run this before considering work done.npm run build— bundle with tsup.- After changing CLI commands/options, regenerate the README CLI reference with
node dist/cli.js docs(the gate'sdocs --checkfails if it is stale).
Node >= 24, ESM, moduleResolution: nodenext, strict: true.
Data flows: config → discover source → collect/render files → emit per target
→ artifact (in-memory file map) → write / prune / validate / diff. The
Artifact (a Map<path, contents> plus managedPaths) is the seam — dry-run,
diff, prune, and validate all derive from it.
src/cli.ts— commander CLI:init,build,validate,diff,prune,clean,docs.src/schema.ts— zod schemas are the source of truth for config types; the public types are derived withz.infer. Edit schemas here, nottypes.ts.src/types.ts— non-config types; re-exports the config types fromschema.ts.src/components.ts—componentDirs+staticFiles(shared by render/config).src/config.ts—loadConfig(jiti loadspluginpack.config.ts), source plugin discovery (only dirs with a manifest or a component dir count, so generated output is never misread as source), and the root-skills plugin.src/render.ts—collectPluginFiles(component dirs + static files, withtargets/<name>/override resolution) andresolveMcpServers.src/targets.ts—emitTarget+ per-target emitters.cursor/claude/geminishare theemitPluginsengine via callbacks;emitCopilotis bespoke (no per-plugin manifest, dual marketplace). Manifest builders live here too.src/build.ts—build(): emit all targets →assertNoCrossTargetCollisions→ write/prune/manifest. Holds the delete guard.src/managed.ts— the managed-file manifest (.pluginpack/<target>.json),prune/clean, the delete guard, and path-safety checks.src/diff.ts—diffTarget: build to a temp dir and compare against an existing target repo (the CI staleness gate).src/validate.ts— per-target output validation.
cursor, claude, gemini, copilot. Adding a target currently touches ~5
places: the TargetName union (types.ts), the targets array + parseTarget
(cli.ts), allTargets (build.ts), the emitters map + a new emitFoo
(targets.ts), and a branch + validateFoo (validate.ts). If you are adding a
target, consider introducing a single target registry first to localize this.
CONFORMANCE.md is the reference. There is no referenceable upstream JSON
Schema for any target, so the harness uses the strongest available oracle per
target: Cursor against vendored published schemas (tests/fixtures/cursor/,
provenance in SOURCE.md); Claude via claude plugin validate --strict (when
the CLI is present); Copilot and Gemini structurally against their real formats
(github/copilot-plugins, google-gemini/gemini-cli). Don't fetch schemas at
runtime — vendor a pinned copy with recorded provenance.
- Recommended shape: top-level
skills/(the portable surface) + generated native outputs underplugins/<target>/in the same repo. - claude + copilot collide: both write
.claude-plugin/marketplace.json, so they need distinctoutDirs.build()errors on overlapping output paths. - MCP: a source plugin declares servers via a
.mcp.jsonfile (standard{ mcpServers: {...} }) or anmcpServerskey inplugin.pluginpack.json(file wins). claude ships the file (auto-discovered); cursor/copilot reference it; gemini inlines the map intogemini-extension.json.
- Strict TypeScript, no
any(the one exception isreadJsoninvalidate.ts). - Prettier + eslint enforced by the gate.
- Conventional commits. Keep the README CLI reference regenerated.