feat: split MCP servers into their own plugins, add per-target restrictions
Split the GitHub/Gitea MCP servers out of core-essentials into their own
github-mcp/gitea-mcp plugins, so a user can install either, both, or
neither independently instead of always getting both bundled together.
Add an optional "targets" field, honored at both levels:
- plugin.json: restricts the whole plugin (including any hooks/mcp/
rules/commands/agents it bundles) to specific targets.
- a skill's own SKILL.md frontmatter: restricts just that one skill,
independent of its plugin's sibling skills (narrowed to, never wider
than, the plugin's own targets).
Omitting it (the default everywhere) means every target in manifest.yaml,
so this is fully backward compatible. A plugin left with no content at
all for a given target is now skipped entirely for that target rather
than emitting an empty output directory / marketplace entry.
Applies this to the actual motivating case: delegate-agy only makes sense
run from Claude Code (it shells out to `agy`), so it's now marked
targets: [claude-code] and no longer ships into dist/agy at all — it was
previously being installed into agy for no reason.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014YVWWAnvR5TDQzmfY3RXG9
This commit is contained in:
@@ -22,6 +22,14 @@
|
||||
"name": "git-publish-workflow",
|
||||
"source": "./dist/claude-code/git-publish-workflow"
|
||||
},
|
||||
{
|
||||
"name": "gitea-mcp",
|
||||
"source": "./dist/claude-code/gitea-mcp"
|
||||
},
|
||||
{
|
||||
"name": "github-mcp",
|
||||
"source": "./dist/claude-code/github-mcp"
|
||||
},
|
||||
{
|
||||
"name": "readme-sync-audit",
|
||||
"source": "./dist/claude-code/readme-sync-audit"
|
||||
|
||||
@@ -19,11 +19,14 @@ from one source tree and published as a native marketplace for both tools.
|
||||
- [technical-devlog-scribe](#technical-devlog-scribe)
|
||||
- [ship-it](#ship-it)
|
||||
- [core-essentials](#core-essentials)
|
||||
- [github-mcp](#github-mcp)
|
||||
- [gitea-mcp](#gitea-mcp)
|
||||
- [Installation](#installation)
|
||||
- [Claude Code](#claude-code)
|
||||
- [Antigravity CLI (agy)](#antigravity-cli-agy)
|
||||
- [Repository Structure](#repository-structure)
|
||||
- [Anatomy of a Plugin](#anatomy-of-a-plugin)
|
||||
- [Restricting a Plugin (or a Skill) to Specific Targets](#restricting-a-plugin-or-a-skill-to-specific-targets)
|
||||
- [How Generation Works](#how-generation-works)
|
||||
- [Private Overlay Builds](#private-overlay-builds)
|
||||
- [License](#license)
|
||||
@@ -130,7 +133,28 @@ Two sequential phases — Phase 2 is blocked until Phase 1 succeeds:
|
||||
Currently bundles `delegate-agy`, which hands a subtask off to the
|
||||
Antigravity CLI (`agy`) in headless mode — useful for a second opinion,
|
||||
external grounded research, or a large multi-file audit (>500 lines) that
|
||||
would otherwise bloat the current context.
|
||||
would otherwise bloat the current context. Only makes sense run *from*
|
||||
Claude Code, so its `SKILL.md` declares `targets: [claude-code]` — this
|
||||
plugin has no agy output at all.
|
||||
|
||||
---
|
||||
|
||||
### `github-mcp`
|
||||
|
||||
**Purpose:** Connect Claude Code/agy to GitHub via the official
|
||||
`@modelcontextprotocol/server-github`, authenticated with a
|
||||
`GITHUB_PERSONAL_ACCESS_TOKEN` you set locally — never stored in the repo.
|
||||
|
||||
---
|
||||
|
||||
### `gitea-mcp`
|
||||
|
||||
**Purpose:** Connect Claude Code/agy to a Gitea instance via the official
|
||||
`gitea-mcp` server, authenticated with `GITEA_HOST`/`GITEA_ACCESS_TOKEN`
|
||||
you set locally.
|
||||
|
||||
Split from `github-mcp` into its own plugin so you can install either,
|
||||
both, or neither independently.
|
||||
|
||||
---
|
||||
|
||||
@@ -218,6 +242,26 @@ verbatim where the two tools' schemas diverge:
|
||||
target; `commands/`/`agents/` have no agy equivalent and are skipped for
|
||||
that target.
|
||||
|
||||
### Restricting a Plugin (or a Skill) to Specific Targets
|
||||
|
||||
Some content only makes sense for one tool — `delegate-agy` (Claude Code
|
||||
shelling out to `agy`) has no reason to exist *inside* agy, for instance.
|
||||
Declare a `targets` list wherever it's needed:
|
||||
|
||||
- In `plugin.json`, `"targets": ["claude-code"]` restricts the **whole
|
||||
plugin** — including any hooks/mcp/rules/commands/agents it bundles —
|
||||
to just the listed targets.
|
||||
- In a skill's own `SKILL.md` frontmatter, `targets: [claude-code]`
|
||||
restricts **just that skill**, independent of its sibling skills in the
|
||||
same plugin. A skill's targets are narrowed to, never wider than, its
|
||||
plugin's own targets.
|
||||
|
||||
Omitting `targets` (the default everywhere) means "every target in
|
||||
`manifest.yaml`" — today's behavior for every existing plugin. A plugin
|
||||
left with no content at all for a given target (every skill excluded, and
|
||||
nothing of its own) is skipped entirely for that target — no empty output
|
||||
directory, no marketplace entry.
|
||||
|
||||
### How Generation Works
|
||||
|
||||
`scripts/generate_plugins.py` reads `manifest.yaml` (marketplace metadata,
|
||||
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"name": "core-essentials"
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: delegate-agy
|
||||
description: Delegates a subtask to the Antigravity CLI (agy) when the user wants a second opinion, external grounded research, or needs a large multi-file audit (>500 lines) processed without bloating the current context.
|
||||
version: 1.0.0
|
||||
user-invocable: true
|
||||
author: Rootiest
|
||||
---
|
||||
|
||||
# Antigravity Subagent Delegation (`delegate-agy`)
|
||||
|
||||
When the user asks for a second opinion, external grounded research, or when a task requires processing large multi-file audits (>500 lines) that would bloat context, delegate the subtask to `agy`.
|
||||
|
||||
## Execution Syntax
|
||||
Run `agy` in headless, non-interactive mode using the bash tool:
|
||||
|
||||
```bash
|
||||
agy --dangerously-skip-permissions -p "<detailed_task_prompt>"
|
||||
```
|
||||
-10
@@ -12,16 +12,6 @@
|
||||
"GITEA_HOST": "${GITEA_HOST}",
|
||||
"GITEA_ACCESS_TOKEN": "${GITEA_ACCESS_TOKEN}"
|
||||
}
|
||||
},
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-github"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "gitea-mcp"
|
||||
}
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-github"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"name": "github-mcp"
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
name: delegate-agy
|
||||
description: Delegates a subtask to the Antigravity CLI (agy) when the user wants a second opinion, external grounded research, or needs a large multi-file audit (>500 lines) processed without bloating the current context.
|
||||
version: 1.0.0
|
||||
user-invocable: true
|
||||
author: Rootiest
|
||||
---
|
||||
|
||||
# Antigravity Subagent Delegation (`delegate-agy`)
|
||||
|
||||
When the user asks for a second opinion, external grounded research, or when a task requires processing large multi-file audits (>500 lines) that would bloat context, delegate the subtask to `agy`.
|
||||
|
||||
## Execution Syntax
|
||||
Run `agy` in headless, non-interactive mode using the bash tool:
|
||||
|
||||
```bash
|
||||
agy --dangerously-skip-permissions -p "<detailed_task_prompt>"
|
||||
```
|
||||
@@ -4,6 +4,7 @@ description: Delegates a subtask to the Antigravity CLI (agy) when the user want
|
||||
version: 1.0.0
|
||||
user-invocable: true
|
||||
author: Rootiest
|
||||
targets: [claude-code]
|
||||
---
|
||||
|
||||
# Antigravity Subagent Delegation (`delegate-agy`)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "gitea-mcp",
|
||||
"description": "Connects Claude Code/agy to a Gitea instance via the official gitea-mcp server, authenticated with GITEA_HOST/GITEA_ACCESS_TOKEN you set locally.",
|
||||
"version": "1.0.0",
|
||||
"author": "Rootiest"
|
||||
}
|
||||
-11
@@ -13,17 +13,6 @@
|
||||
"GITEA_HOST": "${GITEA_HOST}",
|
||||
"GITEA_ACCESS_TOKEN": "${GITEA_ACCESS_TOKEN}"
|
||||
}
|
||||
},
|
||||
"github": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-github"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "github-mcp",
|
||||
"description": "Connects Claude Code/agy to GitHub via the official MCP server, authenticated with a GITHUB_PERSONAL_ACCESS_TOKEN you set locally.",
|
||||
"version": "1.0.0",
|
||||
"author": "Rootiest"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": [
|
||||
"-y",
|
||||
"@modelcontextprotocol/server-github"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ description: Delegates a subtask to the Antigravity CLI (agy) when the user want
|
||||
version: 1.0.0
|
||||
user-invocable: true
|
||||
author: Rootiest
|
||||
targets: [claude-code]
|
||||
---
|
||||
|
||||
# Antigravity Subagent Delegation (`delegate-agy`)
|
||||
|
||||
@@ -4,6 +4,7 @@ description: Delegates a subtask to the Antigravity CLI (agy) when the user want
|
||||
version: 1.0.0
|
||||
user-invocable: true
|
||||
author: Rootiest
|
||||
targets: [claude-code]
|
||||
---
|
||||
|
||||
# Antigravity Subagent Delegation (`delegate-agy`)
|
||||
|
||||
@@ -8,14 +8,6 @@
|
||||
"GITEA_HOST": "${GITEA_HOST}",
|
||||
"GITEA_ACCESS_TOKEN": "${GITEA_ACCESS_TOKEN}"
|
||||
}
|
||||
},
|
||||
"github": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "gitea-mcp",
|
||||
"description": "Connects Claude Code/agy to a Gitea instance via the official gitea-mcp server, authenticated with GITEA_HOST/GITEA_ACCESS_TOKEN you set locally.",
|
||||
"version": "1.0.0",
|
||||
"author": "Rootiest"
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"github": {
|
||||
"type": "stdio",
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-github"],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_PERSONAL_ACCESS_TOKEN}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "github-mcp",
|
||||
"description": "Connects Claude Code/agy to GitHub via the official MCP server, authenticated with a GITHUB_PERSONAL_ACCESS_TOKEN you set locally.",
|
||||
"version": "1.0.0",
|
||||
"author": "Rootiest"
|
||||
}
|
||||
+96
-34
@@ -3,14 +3,25 @@
|
||||
|
||||
A "plugin" is a directory under `plugins/<name>/` that may bundle any mix of:
|
||||
|
||||
- plugin.json (required marker + metadata: name, description, version, author)
|
||||
- skills/<name>/SKILL.md (0+ skills)
|
||||
- plugin.json (required marker + metadata: name, description, version, author,
|
||||
optional targets: ["claude-code", "agy", ...] — restricts the
|
||||
WHOLE plugin, including its hooks/mcp/rules/commands/agents, to
|
||||
only the listed targets; default is every target in manifest.yaml)
|
||||
- skills/<name>/SKILL.md (0+ skills; a skill's own frontmatter may also declare
|
||||
`targets:` to further restrict just that one skill, independent
|
||||
of its sibling skills — narrowed to, never wider than, the
|
||||
plugin's own `targets`)
|
||||
- hooks.json (canonical, Claude-shaped: {"<EventName>": [<matcher-group>, ...]})
|
||||
- mcp.json ({"mcpServers": {...}}, shared shape across targets)
|
||||
- rules/AGENTS.md (agy-only; ignored by the Claude Code target)
|
||||
- commands/*.md (Claude Code-only slash commands)
|
||||
- agents/*.md (Claude Code-only subagents)
|
||||
|
||||
A plugin that ends up with no content at all for a given target (every
|
||||
skill excluded, and no hooks/mcp/rules/commands/agents of its own) is
|
||||
skipped entirely for that target — it never gets an empty output directory
|
||||
or marketplace entry.
|
||||
|
||||
One or more source roots (each containing its own `plugins/` directory) are
|
||||
layered together — a later source overlays/overrides an earlier one on a
|
||||
per-plugin, per-file basis. This is how a private repo (PII/tokens/local-only
|
||||
@@ -120,7 +131,19 @@ def merge_layers(layers: "dict[str, list[Path]]", workdir: Path) -> Path:
|
||||
return merged_root
|
||||
|
||||
|
||||
def discover_plugins(merged_root: Path) -> list[dict]:
|
||||
def validate_targets(value: "list | None", valid_targets: list[str], context: str) -> "list[str] | None":
|
||||
"""Validate an optional 'targets' restriction list. None means 'all targets'."""
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, list) or not value:
|
||||
raise ValidationError(f"{context}: 'targets' must be a non-empty list")
|
||||
for t in value:
|
||||
if t not in valid_targets:
|
||||
raise ValidationError(f"{context}: unknown target '{t}' (valid: {', '.join(valid_targets)})")
|
||||
return value
|
||||
|
||||
|
||||
def discover_plugins(merged_root: Path, valid_targets: list[str]) -> list[dict]:
|
||||
plugins = []
|
||||
for plugin_dir in sorted(merged_root.iterdir()):
|
||||
if not plugin_dir.is_dir():
|
||||
@@ -136,6 +159,7 @@ def discover_plugins(merged_root: Path) -> list[dict]:
|
||||
for field in REQUIRED_PLUGIN_FIELDS:
|
||||
if not meta.get(field):
|
||||
raise ValidationError(f"{manifest_path}: missing required field '{field}'")
|
||||
plugin_targets = validate_targets(meta.get("targets"), valid_targets, str(manifest_path))
|
||||
|
||||
skills = []
|
||||
skills_dir = plugin_dir / "skills"
|
||||
@@ -151,7 +175,8 @@ def discover_plugins(merged_root: Path) -> list[dict]:
|
||||
raise ValidationError(
|
||||
f"{skill_md}: frontmatter name '{fm['name']}' does not match directory name '{skill_dir.name}'"
|
||||
)
|
||||
skills.append({"dir": skill_dir, "frontmatter": fm})
|
||||
skill_targets = validate_targets(fm.get("targets"), valid_targets, str(skill_md))
|
||||
skills.append({"dir": skill_dir, "frontmatter": fm, "targets": skill_targets})
|
||||
|
||||
hooks_path = plugin_dir / "hooks.json"
|
||||
hooks = load_json(hooks_path) if hooks_path.exists() else None
|
||||
@@ -163,6 +188,7 @@ def discover_plugins(merged_root: Path) -> list[dict]:
|
||||
{
|
||||
"dir": plugin_dir,
|
||||
"meta": meta,
|
||||
"targets": plugin_targets,
|
||||
"skills": skills,
|
||||
"hooks": hooks,
|
||||
"mcp": mcp,
|
||||
@@ -181,6 +207,22 @@ def discover_plugins(merged_root: Path) -> list[dict]:
|
||||
return plugins
|
||||
|
||||
|
||||
def plugin_supports(plugin: dict, target: str) -> bool:
|
||||
allowed = plugin["targets"]
|
||||
return allowed is None or target in allowed
|
||||
|
||||
|
||||
def skill_supports(skill: dict, plugin: dict, target: str) -> bool:
|
||||
if not plugin_supports(plugin, target):
|
||||
return False
|
||||
allowed = skill["targets"]
|
||||
return allowed is None or target in allowed
|
||||
|
||||
|
||||
def active_skills(plugin: dict, target: str) -> list[dict]:
|
||||
return [s for s in plugin["skills"] if skill_supports(s, plugin, target)]
|
||||
|
||||
|
||||
# ── Private Repo Cloning ─────────────────────────────────────────────────────
|
||||
|
||||
_ASKPASS_SCRIPT = """#!/bin/sh
|
||||
@@ -256,7 +298,15 @@ def translate_mcp_for_agy(mcp: dict) -> dict:
|
||||
# ── Claude Code Target ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def write_claude_plugin(plugin: dict, dest: Path) -> None:
|
||||
def write_claude_plugin(plugin: dict, dest: Path) -> bool:
|
||||
"""Returns False (writing nothing) if the plugin has no claude-code content."""
|
||||
skills = active_skills(plugin, "claude-code")
|
||||
has_content = bool(
|
||||
skills or plugin["hooks"] or plugin["mcp"] or plugin["commands_dir"] or plugin["agents_dir"]
|
||||
)
|
||||
if not has_content:
|
||||
return False
|
||||
|
||||
meta = plugin["meta"]
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -268,9 +318,9 @@ def write_claude_plugin(plugin: dict, dest: Path) -> None:
|
||||
manifest[field] = meta[field]
|
||||
(claude_plugin_dir / "plugin.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
if plugin["skills"]:
|
||||
if skills:
|
||||
skills_out = dest / "skills"
|
||||
for s in plugin["skills"]:
|
||||
for s in skills:
|
||||
shutil.copytree(s["dir"], skills_out / s["frontmatter"]["name"])
|
||||
|
||||
for optional_dir in ("commands_dir", "agents_dir"):
|
||||
@@ -288,6 +338,8 @@ def write_claude_plugin(plugin: dict, dest: Path) -> None:
|
||||
if plugin["mcp"] is not None:
|
||||
(dest / ".mcp.json").write_text(json.dumps(plugin["mcp"], indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def gen_claude_code(manifest: dict, plugins: list[dict], out_dir: Path) -> None:
|
||||
# Generated output lives under dist/claude-code/ — never inside plugins/,
|
||||
@@ -300,25 +352,26 @@ def gen_claude_code(manifest: dict, plugins: list[dict], out_dir: Path) -> None:
|
||||
marketplace_entries = []
|
||||
for p in plugins:
|
||||
name = p["meta"]["name"]
|
||||
write_claude_plugin(p, plugins_out / name)
|
||||
marketplace_entries.append({"name": name, "source": f"./dist/claude-code/{name}"})
|
||||
if write_claude_plugin(p, plugins_out / name):
|
||||
marketplace_entries.append({"name": name, "source": f"./dist/claude-code/{name}"})
|
||||
|
||||
bundle_id = manifest["bundle"]["id"]
|
||||
bundle_dest = plugins_out / bundle_id
|
||||
(bundle_dest / ".claude-plugin").mkdir(parents=True)
|
||||
(bundle_dest / ".claude-plugin" / "plugin.json").write_text(
|
||||
json.dumps(
|
||||
{"name": bundle_id, "description": f"{manifest['marketplace']['description']} (all skills)"},
|
||||
indent=2,
|
||||
bundle_skills = [(p, s) for p in plugins for s in active_skills(p, "claude-code")]
|
||||
if bundle_skills:
|
||||
bundle_dest = plugins_out / bundle_id
|
||||
(bundle_dest / ".claude-plugin").mkdir(parents=True)
|
||||
(bundle_dest / ".claude-plugin" / "plugin.json").write_text(
|
||||
json.dumps(
|
||||
{"name": bundle_id, "description": f"{manifest['marketplace']['description']} (all skills)"},
|
||||
indent=2,
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
+ "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
bundle_skills = bundle_dest / "skills"
|
||||
for p in plugins:
|
||||
for s in p["skills"]:
|
||||
shutil.copytree(s["dir"], bundle_skills / s["frontmatter"]["name"])
|
||||
marketplace_entries.append({"name": bundle_id, "source": f"./dist/claude-code/{bundle_id}"})
|
||||
bundle_skills_dir = bundle_dest / "skills"
|
||||
for _, s in bundle_skills:
|
||||
shutil.copytree(s["dir"], bundle_skills_dir / s["frontmatter"]["name"])
|
||||
marketplace_entries.append({"name": bundle_id, "source": f"./dist/claude-code/{bundle_id}"})
|
||||
|
||||
marketplace = {
|
||||
"name": manifest["marketplace"]["name"],
|
||||
@@ -334,15 +387,21 @@ def gen_claude_code(manifest: dict, plugins: list[dict], out_dir: Path) -> None:
|
||||
# ── Antigravity CLI (agy) Target ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def write_agy_plugin(plugin: dict, dest: Path) -> None:
|
||||
def write_agy_plugin(plugin: dict, dest: Path) -> bool:
|
||||
"""Returns False (writing nothing) if the plugin has no agy content."""
|
||||
skills = active_skills(plugin, "agy")
|
||||
has_content = bool(skills or plugin["rules_dir"] or plugin["hooks"] or plugin["mcp"])
|
||||
if not has_content:
|
||||
return False
|
||||
|
||||
meta = plugin["meta"]
|
||||
name = meta["name"]
|
||||
dest.mkdir(parents=True, exist_ok=True)
|
||||
(dest / "plugin.json").write_text(json.dumps({"name": name}, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
if plugin["skills"]:
|
||||
if skills:
|
||||
skills_out = dest / "skills"
|
||||
for s in plugin["skills"]:
|
||||
for s in skills:
|
||||
shutil.copytree(s["dir"], skills_out / s["frontmatter"]["name"])
|
||||
|
||||
if plugin["rules_dir"] is not None:
|
||||
@@ -358,6 +417,8 @@ def write_agy_plugin(plugin: dict, dest: Path) -> None:
|
||||
json.dumps(translate_mcp_for_agy(plugin["mcp"]), indent=2) + "\n", encoding="utf-8"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def gen_agy(manifest: dict, plugins: list[dict], out_dir: Path) -> None:
|
||||
agy_dir = out_dir / "dist" / "agy"
|
||||
@@ -369,13 +430,14 @@ def gen_agy(manifest: dict, plugins: list[dict], out_dir: Path) -> None:
|
||||
write_agy_plugin(p, agy_dir / p["meta"]["name"])
|
||||
|
||||
bundle_id = manifest["bundle"]["id"]
|
||||
bundle_dest = agy_dir / bundle_id
|
||||
bundle_dest.mkdir(parents=True)
|
||||
(bundle_dest / "plugin.json").write_text(json.dumps({"name": bundle_id}, indent=2) + "\n", encoding="utf-8")
|
||||
bundle_skills = bundle_dest / "skills"
|
||||
for p in plugins:
|
||||
for s in p["skills"]:
|
||||
shutil.copytree(s["dir"], bundle_skills / s["frontmatter"]["name"])
|
||||
bundle_skills = [(p, s) for p in plugins for s in active_skills(p, "agy")]
|
||||
if bundle_skills:
|
||||
bundle_dest = agy_dir / bundle_id
|
||||
bundle_dest.mkdir(parents=True)
|
||||
(bundle_dest / "plugin.json").write_text(json.dumps({"name": bundle_id}, indent=2) + "\n", encoding="utf-8")
|
||||
bundle_skills_dir = bundle_dest / "skills"
|
||||
for _, s in bundle_skills:
|
||||
shutil.copytree(s["dir"], bundle_skills_dir / s["frontmatter"]["name"])
|
||||
|
||||
dist_readme = out_dir / "dist" / "README.md"
|
||||
dist_readme.write_text(
|
||||
@@ -474,7 +536,7 @@ def main() -> int:
|
||||
|
||||
layers = collect_layers(source_roots)
|
||||
merged_root = merge_layers(layers, workdir)
|
||||
plugins = discover_plugins(merged_root)
|
||||
plugins = discover_plugins(merged_root, manifest["targets"])
|
||||
|
||||
if args.check:
|
||||
print(f"OK: {len(plugins)} plugin(s), {len(manifest['targets'])} target(s) validated")
|
||||
|
||||
Reference in New Issue
Block a user