Merge pull request 'feat: split MCP servers into their own plugins, add per-target restrictions' (#3) from feat-split-mcp-plugins-and-targets into main
Reviewed-on: #3
This commit was merged in pull request #3.
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