Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ format.
There is no single, referenceable, upstream JSON Schema for any supported target.
Each app's source of truth is something other than a stable schema URL:

| Target | Canonical source of truth | Referenceable schema? |
| ------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `claude` | `claude plugin validate` CLI + [plugins-reference docs](https://code.claude.com/docs/en/plugins-reference) | **No.** The `$schema` URL the manifest declares (`https://anthropic.com/claude-code/marketplace.schema.json`) returns 404. |
| `cursor` | Glean-authored schemas in `gleanwork/cursor-plugins/schemas/` | **No upstream.** The schema `$id` (`https://cursor.com/schemas/cursor-plugin/...`) 500s; no Cursor-published schema found. |
| `antigravity` | Antigravity CLI plugin docs (`plugin.json`, optional `mcp_config.json`) | **No.** Defined by product docs and observed CLI layout, not a published schema. |
| `copilot` | [`github/copilot-plugins`](https://github.com/github/copilot-plugins) — a Claude-marketplace-derived format | **Structural.** Copilot shares the Claude marketplace base but extends entries (`skills[]`, `mcpServers` as a path), which `claude plugin validate` rejects — so conformance is asserted structurally against the official format. |
| Target | Canonical source of truth | Referenceable schema? |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `claude` | `claude plugin validate` CLI + [plugins-reference docs](https://code.claude.com/docs/en/plugins-reference) | **No.** The `$schema` URL the manifest declares (`https://anthropic.com/claude-code/marketplace.schema.json`) returns 404. |
| `cursor` | Glean-authored schemas in `gleanwork/cursor-plugins/schemas/` | **No upstream.** The schema `$id` (`https://cursor.com/schemas/cursor-plugin/...`) 500s; no Cursor-published schema found. |
| `antigravity` | Antigravity CLI plugin docs (`plugin.json`, optional `mcp_config.json`) | **No.** Defined by product docs and observed CLI layout, not a published schema. |
| `copilot` | [`github/copilot-plugins`](https://github.com/github/copilot-plugins) — a Claude-marketplace-derived format | **Structural.** Copilot shares the Claude marketplace base but extends entries (`skills[]`, `mcpServers` as a path), which `claude plugin validate` rejects — so conformance is asserted structurally against the official format. |
| `codex` | [OpenAI Codex CLI plugin docs](https://developers.openai.com/codex/plugins/build) (`.codex-plugin/plugin.json` + `.agents/plugins/marketplace.json`) | **No published schema.** Defined by product docs; conformance is asserted structurally against the documented format (retrieved 2026-06-17). |

## Oracles the harness uses

Expand Down Expand Up @@ -47,6 +48,14 @@ against a temp fixture via [`bintastic`](https://github.com/scalvert/bintastic).
`tests/core.test.ts` (required `plugin.json` fields present; optional
`mcp_config.json` written when MCP servers are present). Antigravity CLI does
not expose a published schema to validate against.
- **codex** — asserted structurally in `tests/conformance.test.ts` against the
[documented Codex plugin format](https://developers.openai.com/codex/plugins/build):
a repo-scoped `.agents/plugins/marketplace.json` (`{ name, interface, plugins }`)
plus a per-plugin `.codex-plugin/plugin.json` (`{ name, version, description,
skills }`) and optional `.mcp.json`. No published JSON Schema exists; the test
pins the documented shape and confirms a per-plugin `entry` passthrough lands in
the marketplace entry. Codex shares no marketplace path with the other targets,
so it needs no separate output root.

## Refreshing vendored schemas

Expand Down
34 changes: 18 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,9 @@ The first adapters are:
- `claude`
- `antigravity`
- `copilot`
- `codex`

`cursor` emits Cursor plugin and marketplace manifests. `claude` emits Claude plugin and marketplace manifests. `antigravity` emits Antigravity CLI plugins with a `plugin.json` manifest and optional `mcp_config.json`. `copilot` emits the GitHub Copilot plugins format (per [`github/copilot-plugins`](https://github.com/github/copilot-plugins)): a `.claude-plugin/marketplace.json` mirrored to `.github/plugin/marketplace.json`, each plugin under `plugins/<name>/` with a `skills` array per marketplace entry.
`cursor` emits Cursor plugin and marketplace manifests. `claude` emits Claude plugin and marketplace manifests. `antigravity` emits Antigravity CLI plugins with a `plugin.json` manifest and optional `mcp_config.json`. `copilot` emits the GitHub Copilot plugins format (per [`github/copilot-plugins`](https://github.com/github/copilot-plugins)): a `.claude-plugin/marketplace.json` mirrored to `.github/plugin/marketplace.json`, each plugin under `plugins/<name>/` with a `skills` array per marketplace entry. `codex` emits the [OpenAI Codex CLI plugin format](https://developers.openai.com/codex/plugins/build): a repo-scoped `.agents/plugins/marketplace.json` plus a per-plugin `.codex-plugin/plugin.json` manifest and optional `.mcp.json`.

Because Copilot reuses the Claude marketplace layout, the `claude` and `copilot` targets both write `.claude-plugin/marketplace.json` and therefore need separate output roots (distinct `outDir`s or separate repos).

Expand Down Expand Up @@ -360,7 +361,7 @@ To publish a repo-root file (for example a README authored once in the source re
| `category` | string | Marketplace category. |
| `tags` | string[] | Free-form tags. |

**`targets.<name>`** — `<name>` is one of `cursor`, `claude`, `antigravity`, `copilot`.
**`targets.<name>`** — `<name>` is one of `cursor`, `claude`, `antigravity`, `copilot`, `codex`.

| Field | Type | Required | Meaning |
| ------------------ | ---------------------- | -------- | ---------------------------------------------------------------------------------------- |
Expand All @@ -375,15 +376,16 @@ To publish a repo-root file (for example a README authored once in the source re

**`targets.<name>.plugins.<name>`**

| Field | Type | Required | Meaning |
| ------------- | ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------- |
| `from` | string[] (min 1) | yes | Source plugin ids to merge into this emitted plugin. |
| `path` | string (safe relative) | no | Output path for the plugin, relative to `outDir`. Defaults to the plugin name (or `pluginRoot/<name>` for `claude`). |
| `version` | string | no | Per-plugin version override. |
| `displayName` | string | no | Per-plugin display name. |
| `description` | string | no | Per-plugin description override. |
| `manifest` | object | no | Deep-merged into the generated plugin manifest. |
| `components` | string[] | no | Exact component set, overriding the target's smart default. |
| Field | Type | Required | Meaning |
| ------------- | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `from` | string[] (min 1) | yes | Source plugin ids to merge into this emitted plugin. |
| `path` | string (safe relative) | no | Output path for the plugin, relative to `outDir`. Defaults to the plugin name (or `pluginRoot/<name>` for `claude`). |
| `version` | string | no | Per-plugin version override. |
| `displayName` | string | no | Per-plugin display name. |
| `description` | string | no | Per-plugin description override. |
| `manifest` | object | no | Deep-merged into the generated plugin manifest. |
| `entry` | object | no | Deep-merged into the generated marketplace entry (the object in the marketplace `plugins` array). Use for target-specific entry fields pluginpack can't derive — e.g. Codex `policy`/`category`. |
| `components` | string[] | no | Exact component set, overriding the target's smart default. |

## Programmatic API

Expand Down Expand Up @@ -445,7 +447,7 @@ Exit codes:
Compile configured source plugins into target-native plugin payloads.

```bash
pluginpack build [--target cursor|claude|antigravity|copilot] [--out-dir <path>] [--dry-run]
pluginpack build [--target cursor|claude|antigravity|copilot|codex] [--out-dir <path>] [--dry-run]
```

Options:
Expand All @@ -470,7 +472,7 @@ Exit codes:
Validate an existing target output directory for native manifest, path, and frontmatter requirements.

```bash
pluginpack validate --target cursor|claude|antigravity|copilot [--dir <path>]
pluginpack validate --target cursor|claude|antigravity|copilot|codex [--dir <path>]
```

Options:
Expand All @@ -492,7 +494,7 @@ Exit codes:
Build into a temporary directory and compare generated managed files with an existing target repo.

```bash
pluginpack diff --target cursor|claude|antigravity|copilot --against <path>
pluginpack diff --target cursor|claude|antigravity|copilot|codex --against <path>
```

Options:
Expand All @@ -514,7 +516,7 @@ Exit codes:
Remove stale managed files that are no longer emitted by the current config.

```bash
pluginpack prune [--target cursor|claude|antigravity|copilot] [--dry-run]
pluginpack prune [--target cursor|claude|antigravity|copilot|codex] [--dry-run]
```

Options:
Expand All @@ -538,7 +540,7 @@ Exit codes:
Remove all managed files for configured target outputs.

```bash
pluginpack clean [--target cursor|claude|antigravity|copilot] [--dry-run]
pluginpack clean [--target cursor|claude|antigravity|copilot|codex] [--dry-run]
```

Options:
Expand Down
3 changes: 3 additions & 0 deletions src/adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import path from "node:path";
import {
emitAntigravity,
emitClaude,
emitCodex,
emitCopilot,
emitCursor,
withRootFiles,
} from "./targets.js";
import {
validateAntigravity,
validateClaude,
validateCodex,
validateCopilot,
validateCursor,
} from "./validate.js";
Expand Down Expand Up @@ -47,6 +49,7 @@ export const adapters: Record<TargetName, TargetAdapter> = {
claude: { emit: emitClaude, validate: validateClaude },
antigravity: { emit: emitAntigravity, validate: validateAntigravity },
copilot: { emit: emitCopilot, validate: validateCopilot },
codex: { emit: emitCodex, validate: validateCodex },
};

export const targetNames = Object.keys(adapters) as TargetName[];
Expand Down
13 changes: 6 additions & 7 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ async function main(): Promise<void> {
function createProgram(): Command {
const program = new Command();
const pkg = readPackageJson();
const targetList = targetNames.join("|");

program.name("pluginpack").description(pkg.description).version(pkg.version);

Expand All @@ -31,9 +32,7 @@ function createProgram(): Command {
.description(
"Compile configured source plugins into target-native plugin payloads.",
)
.usage(
"[--target cursor|claude|antigravity|copilot] [--out-dir <path>] [--dry-run]",
)
.usage(`[--target ${targetList}] [--out-dir <path>] [--dry-run]`)
.addOption(
new Option(
"--target <target>",
Expand Down Expand Up @@ -77,7 +76,7 @@ function createProgram(): Command {
.description(
"Validate an existing target output directory for native manifest, path, and frontmatter requirements.",
)
.usage("--target cursor|claude|antigravity|copilot [--dir <path>]")
.usage(`--target ${targetList} [--dir <path>]`)
.requiredOption(
"--target <target>",
"Required target validator.",
Expand Down Expand Up @@ -114,7 +113,7 @@ function createProgram(): Command {
.description(
"Build into a temporary directory and compare generated managed files with an existing target repo.",
)
.usage("--target cursor|claude|antigravity|copilot --against <path>")
.usage(`--target ${targetList} --against <path>`)
.requiredOption(
"--target <target>",
"Required target to build and compare.",
Expand Down Expand Up @@ -145,7 +144,7 @@ function createProgram(): Command {
.description(
"Remove stale managed files that are no longer emitted by the current config.",
)
.usage("[--target cursor|claude|antigravity|copilot] [--dry-run]")
.usage(`[--target ${targetList}] [--dry-run]`)
.addOption(
new Option(
"--target <target>",
Expand All @@ -171,7 +170,7 @@ function createProgram(): Command {
program
.command("clean")
.description("Remove all managed files for configured target outputs.")
.usage("[--target cursor|claude|antigravity|copilot] [--dry-run]")
.usage(`[--target ${targetList}] [--dry-run]`)
.addOption(
new Option(
"--target <target>",
Expand Down
1 change: 1 addition & 0 deletions src/components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const targetDefaultComponents: Record<TargetName, readonly string[]> = {
copilot: ["skills", "agents", "hooks", "scripts", "assets"],
cursor: ["skills", "agents", "rules", "hooks", "scripts", "assets"],
antigravity: ["skills", "agents", "rules", "hooks", "scripts", "assets"],
codex: ["skills", "agents", "hooks", "scripts", "assets"],
};

export function resolveTargetComponents(
Expand Down
5 changes: 5 additions & 0 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ const emittedPluginSchema = z.object({
description: z.string().optional(),
displayName: z.string().optional(),
manifest: z.record(z.string(), z.unknown()).optional(),
// Deep-merged into this plugin's generated marketplace entry (the object in
// the marketplace `plugins` array), letting a config supply target-specific
// entry fields a target can't derive — e.g. Codex `policy`/`category`.
entry: z.record(z.string(), z.unknown()).optional(),
components: z.array(z.string()).optional(),
});

Expand Down Expand Up @@ -77,6 +81,7 @@ const configSchema = z.object({
copilot: targetSchema.optional(),
cursor: targetSchema.optional(),
antigravity: targetSchema.optional(),
codex: targetSchema.optional(),
}),
});

Expand Down
Loading