Skip to content

feat(core): externalize plugin body into .plugin/plugin.json manifest (v0.13.0) - #155

Merged
tadasant merged 2 commits into
mainfrom
feat/plugin-manifest-externalization
Jun 12, 2026
Merged

feat(core): externalize plugin body into .plugin/plugin.json manifest (v0.13.0)#155
tadasant merged 2 commits into
mainfrom
feat/plugin-manifest-externalization

Conversation

@tadasant

Copy link
Copy Markdown
Contributor

Summary

Plugins were the only AIR artifact with no body — every field (artifact references and distribution metadata like author, version, license, keywords) had to live inline on the plugins.json entry. A catalog with many plugins (or a plugin that joins dozens of roots via default_in_roots) bloated the index.

This PR lets a plugin entry set a path that externalizes its body into a <path>/.plugin/plugin.json manifest — AIR's vendor-neutral analog of the Open Plugins .plugin/plugin.json manifest. plugins.json can then stay a lightweight registry of description + path (+ default_in_roots).

// plugins.json — thin registry
{
  "code-quality": {
    "description": "Linting, formatting, and static analysis tools",
    "path": "./code-quality",
    "default_in_roots": ["web-app"]
  }
}
// code-quality/.plugin/plugin.json — the body
{
  "name": "code-quality",
  "title": "Code Quality Suite",
  "version": "1.2.0",
  "skills": ["lint-fix", "format-check"],
  "mcp_servers": ["eslint-server"],
  "hooks": ["lint-pre-commit"],
  "author": { "name": "Acme Engineering" },
  "license": "MIT"
}

How it works

  • path resolves exactly like the path on skills/references/hooks: a relative path against the index file's directory, or a provider URI (github://owner/repo[@ref]/dir). The manifest is always read from the local filesystem (the provider's local clone), so remote plugins incur no extra fetch.
  • resolveArtifacts reads the manifest and merges its fields into the entry before scope-qualification, so the manifest's skills / mcp_servers / hooks / plugins arrays canonicalize under the catalog's scope and expand identically to inline declarations.
  • Inline-wins precedence: any field on the plugins.json entry overrides the manifest (the manifest only fills gaps). This composes a single plugin's split definition — it is not cross-catalog later-wins.
  • description, path, and default_in_roots always live on the index entry (the registry needs description to list the plugin; root membership is a catalog-layer decision, not a property of the distributed plugin).
  • Unlike Open Plugins, the manifest's component fields hold AIR artifact IDs, not paths to bundled component directories — preserving cross-plugin DRY and CLI overlap reasoning. The manifest's optional name is accepted for Open Plugins compatibility but ignored for identity.

Backward compatibility

Fully opt-in. A plugin may declare everything inline (no path), externalize everything, or mix the two. Errors are loud: a missing manifest, unparseable JSON, or a non-string-array reference field fails resolution with a diagnostic that names the plugin.

Changes

  • schemas/plugin-manifest.schema.jsonnew AIR Plugin Manifest schema.
  • schemas/plugins.schema.json — adds the path field; required unchanged (["description"]).
  • packages/core/src/types.ts — adds path? to PluginEntry.
  • packages/core/src/config.tshydratePluginManifests(), invoked in loadContributions for the plugins type after path resolution.
  • packages/core/tests/plugin-manifest.test.tsnew, 9 tests (hydration, inline-wins, scope qualification, relative-path resolution, default_in_roots placement, plugin-of-plugin expansion, missing/invalid/malformed manifest errors, inline backward-compat).
  • examples/plugins/code-quality converted to the thin-index + manifest form; the other two examples stay inline to show both styles coexisting.
  • docs/plugins.md — documents the manifest model and updates the Open Plugins / Claude Plugins deviation tables (AIR now adopts the same manifest location).
  • Lockstep version bump to v0.13.0 (minor — new backward-compatible feature) + CHANGELOG entry.

Testing

  • npx vitest run1067 passed, 2 skipped, across all 57 test files.
  • tsc --noEmit clean for core, sdk, cli.
  • Verified end-to-end against examples/air.json: code-quality resolves with title/version/license sourced from the manifest body and the lint-pre-commit hook qualified to @local/lint-pre-commit.

🤖 Generated with Claude Code

… (v0.13.0)

A plugin entry can now set `path` to externalize its body into a
`<path>/.plugin/plugin.json` manifest, keeping plugins.json a lightweight
registry of description + path (+ default_in_roots). resolveArtifacts reads
the manifest and merges its fields (artifact references + distribution
metadata) into the entry before scope-qualification, with inline-wins
precedence. This is AIR's vendor-neutral analog of the Open Plugins manifest:
same file location, but component fields hold AIR artifact IDs rather than
bundled component paths.

Fully backward-compatible and opt-in. Adds schemas/plugin-manifest.schema.json,
a `path` field on the plugins schema, and `path` on the core PluginEntry type.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
….json

Warn when a plugins.json entry declares its body inline (no path) instead
of referencing a .plugin/plugin.json manifest. The warning names the plugin
and offending fields and links the removal-tracking issue (#157). Inline
overrides layered on top of a `path` stay quiet — that's the sanctioned
override path. Marks the affected fields `deprecated` in the plugins schema,
migrates the bundled examples to manifest form, and documents the deprecation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tadasant
tadasant merged commit 6963621 into main Jun 12, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant