feat: add marketplace-architect skill with Bash scripts and evals

Adds a new skill for creating, managing, and adopting plugins across
Claude Code and GitHub Copilot CLI marketplaces. Includes three Bash
scripts (inventory, gen_manifests, validate), three reference docs
(cross-compat, claude-code, copilot-cli), a test harness with 18
passing tests, and an eval.yaml. Also adds the `marketplace` category
to CATEGORIES.md and commits the plugin marketplace architecture
research doc that informed the skill design.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 13:09:16 +00:00
parent 25a6a454b9
commit 36ca3744aa
12 changed files with 1683 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
skill_name: marketplace-architect
trigger_tests:
- id: explicit-trigger-refactor
name: "Explicit trigger — refactor repo into marketplace"
query: "turn this repo into a plugin marketplace"
should_trigger: true
- id: implicit-trigger-team-sharing
name: "Implicit trigger — team distribution without saying marketplace"
query: "how do I share my skills with my team so they can install them"
should_trigger: true
- id: negative-trigger-new-skill
name: "Negative trigger — new skill authoring belongs to write-skill"
query: "write me a new skill for code review"
should_trigger: false
output_tests:
- id: deterministic-cross-compat-loaded
name: "Deterministic — cross-compat reference loaded before tool-specific recommendations"
type: deterministic
prompt: "audit this repo and recommend how to convert it into a plugin marketplace for both Claude Code and Copilot CLI"
expected_output: >
The skill reads references/cross-compat.md early in its response and demonstrates
awareness of the Claude Code / Copilot CLI format differences — specifically that
manifest paths differ (.claude-plugin/ vs .github/plugin/), agent files differ
(.md vs .agent.md), and hooks placement differs — before recommending any layout.
assertions:
- "Output references or acknowledges the divergence between Claude Code and Copilot CLI manifest paths before proposing a directory layout"
- "Output does not recommend a single shared plugin.json location without noting the two-location requirement (.claude-plugin/ and plugin root)"
- "Output does not write or propose writing any files before presenting a plan"
- id: deterministic-gate-a-blocks-writes
name: "Deterministic — Gate A plan presented and approval requested before any writes"
type: deterministic
prompt: "help me migrate my skills and agents into a plugin marketplace layout"
expected_output: >
The skill produces a migration plan (checklist of old path → new path, one row per
file) and explicitly asks the user for approval before proceeding to generate any
manifest files. No plugin.json or marketplace.json is written or shown as written output.
assertions:
- "Output contains a migration plan or checklist with at least one file-move entry in the form 'old path → new path'"
- "Output explicitly asks the user to approve or confirm the plan before proceeding"
- "Output does not contain a complete plugin.json or marketplace.json file unless the user has already said 'yes' or equivalent in the prompt"
- id: llm-rubric-adopt-plugin
name: "LLM-rubric — adopt external plugin covers all required checks without premature writes"
type: llm-rubric
prompt: "I want to add this plugin to my marketplace: https://github.com/example/my-tool-plugin"
expected_output: >
The skill fetches and inspects the plugin source, classifies what assets it contains
(skills, agents, hooks, MCP servers), checks whether any existing plugin in the
marketplace has a naming conflict, evaluates cross-tool compatibility against the
divergence table, and summarises what would be added to marketplace.json — all without
writing any files and without proceeding past Gate A without explicit user approval.
assertions:
- "Response identifies what asset types the external plugin contains (skills, agents, hooks, and/or MCP servers)"
- "Response checks or asks about naming conflicts with existing plugins in the marketplace"
- "Response notes at least one Claude Code vs Copilot CLI compatibility consideration for the adopted plugin"
- "Response summarises the proposed change to marketplace.json without writing the file"
- "Response asks for user approval (Gate A) before any file is created or modified"

View File

@@ -0,0 +1,21 @@
```yaml
version: "1.0"
updated: 2026-06-20
when: >
Invoked when the user wants to create, manage, or maintain a plugin marketplace for
Claude Code and/or GitHub Copilot CLI. Covers four operations: (a) audit and refactor
a repository of skills/agents/hooks into a plugin marketplace layout, (b) adopt external
plugins/skills/agents from outside sources, (c) update and maintain an existing
marketplace.json and plugin manifests, (d) validate existing plugin manifests for naming,
structure, and cross-tool compatibility. Also triggered implicitly when the user asks
about distributing skills to a team, organizing loose skills into installable units, or
setting up cross-tool distribution — even without saying "marketplace".
references:
- https://code.claude.com/docs/en/plugins
- https://code.claude.com/docs/en/plugin-marketplaces
- https://code.claude.com/docs/en/plugins-reference
- https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-creating
- https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/plugins-marketplace
```

View File

@@ -0,0 +1,101 @@
---
name: marketplace-architect
description: >
Manages and maintains a plugin marketplace for Claude Code and GitHub Copilot CLI.
Use this whenever the user wants to: create or update a marketplace (marketplace.json,
plugin.json manifests), adopt plugins/skills/agents/hooks from external sources, evaluate
cross-tool compatibility between Claude Code and Copilot CLI, plan plugin groupings and
boundaries, refactor a repository into marketplace format, validate plugin naming, detect
duplicate capabilities, or generate per-plugin install docs — even if they don't use the
word "marketplace". Do NOT use when the user wants to author a new skill from scratch
(use write-skill), debug an existing skill (use diagnose), or run a direct plugin CLI
command (copilot plugin install, claude plugin list).
metadata:
category: marketplace
---
<requirements>
## Required inputs
- **Target operation** — what the user wants to do; inferred from request. If ambiguous, ask: audit/refactor, adopt an external plugin, update/maintain an existing marketplace, or validate manifests.
- **Repository path** — path to the repo to act on; defaults to current working directory if not stated.
- **Marketplace name** — kebab-case identifier (e.g. `my-ai-marketplace`); required only when generating a new `marketplace.json`. Infer from repo name if obvious, ask if not.
- **Plugin source** — URL, GitHub slug, or local path; required only when adopting an external plugin.
## Constraints
- Load `references/cross-compat.md` before any tool-specific decision — Claude Code and Copilot CLI diverge in ways that cause silent breakage at install time.
- Never write files until the user has approved the plan at Gate A and the specific file contents at Gate B — two separate explicit approvals required.
- If credential-shaped content is detected in any manifest field, halt and redirect to environment variable references (e.g. `$MY_TOKEN`) — do not generate the manifest.
- Produce cross-tool deltas and per-plugin READMEs only when explicitly requested — do not generate them automatically.
- Scripts in `scripts/` are loaded on demand by the step that needs them — never preloaded.
- Flag any `../` cross-references in the audited repo before recommending plugin boundaries — plugins cannot reference files outside their own directory after install-time caching.
- Plugin names must be kebab-case; validate against the reserved name list in `references/claude-code.md` before generating any manifest.
- Do not set `version` in both `plugin.json` and the marketplace entry — `plugin.json` wins silently and causes update failures.
</requirements>
<steps>
## Process
1. **Identify the operation.** Determine intent from the user's request — one of: (a) audit/refactor a repo into marketplace format, (b) adopt an external plugin/skill/agent, (c) maintain or update an existing marketplace, (d) validate existing manifests. Ask if the operation cannot be inferred.
2. **Load the compatibility reference.** Read `references/cross-compat.md` before any tool-specific decision. Claude Code and Copilot CLI diverge in manifest paths, agent file naming, and hooks layout — every recommendation depends on this table.
3. **Execute the operation phase.**
**(a) Audit/refactor:** Run `scripts/inventory.sh` against the repo to classify every asset (skill / command / agent / hook / prompt / MCP). Flag any `../` cross-references — these break under install-time caching. Recommend plugin groupings by user outcome (~10–20 plugins); warn if proposed count exceeds 20 or falls below 3. Diff skill descriptions for duplicate capabilities before finalising boundaries. Produce a concrete migration checklist: old path → new path, one row per file.
**(b) Adopt external plugin:** Fetch and inspect the plugin source. Classify included assets. Check for naming conflicts with existing plugins in the marketplace. Evaluate cross-tool compatibility using `references/cross-compat.md`. Summarise what will be added to `marketplace.json`.
**(c) Maintain/update:** Read current `marketplace.json` and all `plugin.json` files. Identify stale versions, reserved name violations, kebab-case violations, and `version` duplication between plugin.json and marketplace entry. Report findings as a prioritised fix list.
**(d) Validate:** Run `scripts/validate.sh` (wraps `claude plugin validate` plus custom JSON and naming checks). Report each violation with a recommended fix. Do not proceed to file writes until all errors are resolved.
4. **Gate A — plan review.** Present the full plan or fix list to the user. Wait for explicit approval before proceeding. Do not interpret silence or "looks good" as approval — require a direct "yes" or equivalent.
5. **Generate outputs.** After Gate A approval: for audit/refactor and adopt operations, run `scripts/gen_manifests.sh` to produce `plugin.json` (at both `.claude-plugin/plugin.json` and plugin root until the Copilot fallback is verified) and `marketplace.json` (at `.claude-plugin/marketplace.json`; optionally mirror to `.github/plugin/marketplace.json`). Read `references/claude-code.md` for Claude-specific path rules and `references/copilot-cli.md` for Copilot-specific requirements.
6. **Gate B — file write approval.** Show the user every file that will be written with its full contents. Wait for explicit approval per file or as a batch. Write nothing until approved.
7. **Validate post-write.** After writes complete, run `scripts/validate.sh` again. Report any remaining issues. Suggest local install test commands: `claude --plugin-dir ./plugins/<name>` and `copilot plugin install ./plugins/<name>`.
8. **Optional deliverables.** Only when the user explicitly asks: emit cross-tool delta notes (what each plugin needs for Copilot vs Claude Code) and per-plugin README with install commands for both tools.
## Output format
Files generated depend on operation:
- **Audit/refactor and adopt:** `plugin.json` (two locations per plugin until verified), `marketplace.json` (`.claude-plugin/`, optionally `.github/plugin/`), migration checklist as a markdown table
- **Maintain/update:** updated `marketplace.json` and affected `plugin.json` files
- **Validate:** report only — no file writes unless explicitly requested after review
- **Optional:** per-plugin `README.md` with both `claude` and `copilot` install commands
</steps>
<checks>
## Failure handling
- `scripts/inventory.sh` not found or fails — perform manual asset classification using Read and Bash find; note the fallback in output.
- `scripts/gen_manifests.sh` not found or fails — generate manifest JSON inline; flag that the output was not script-produced.
- `scripts/validate.sh` not found or `claude plugin validate` unavailable — run manual JSON schema and naming checks using `references/claude-code.md`; flag that automated validation was skipped.
- Plugin source unreachable (bad URL, private repo, missing path) — stop the adopt operation, report the error, ask the user to verify the source before retrying.
- Reserved name detected in proposed plugin or marketplace name — halt, report the name and the reserved list from `references/claude-code.md`, ask for a replacement before proceeding.
- Credential-shaped content detected in any manifest field — halt, do not generate the manifest, redirect to environment variable references.
## Self-check
- [ ] `references/cross-compat.md` loaded before any tool-specific recommendation was made
- [ ] Operation identified before any scanning or file reading began
- [ ] Gate A presented and explicit approval received before any manifest was generated
- [ ] Gate B presented with full file contents and explicit approval received before any file was written
- [ ] No credential-shaped content in any generated manifest field
- [ ] All plugin names validated as kebab-case and checked against reserved name list
- [ ] `version` field not set in both `plugin.json` and marketplace entry for the same plugin
- [ ] Scripts loaded on demand by step — not preloaded at skill invocation
- [ ] Cross-tool deltas and READMEs produced only if explicitly requested
- [ ] Post-write validation run and findings reported
</checks>

View File

@@ -0,0 +1,169 @@
# Claude Code Plugin Reference
Verified against code.claude.com/docs as of June 2026.
---
## Directory structure
```text
plugin-root/
├── .claude-plugin/
│ └── plugin.json # ONLY plugin.json goes here; all other dirs at plugin root
├── skills/ # skill directories: <name>/SKILL.md
├── commands/ # legacy flat .md files; promote to skills/ for new plugins
├── agents/ # agent definitions: <name>.md
├── hooks/
│ └── hooks.json
├── .mcp.json
├── .lsp.json
├── monitors/
│ └── monitors.json
├── bin/ # executables added to PATH while plugin is enabled
└── settings.json # default settings applied when plugin is enabled
```
A plugin that ships exactly one skill may place `SKILL.md` directly at the plugin root.
Use `skills/` for plugins that may grow beyond one skill.
---
## plugin.json schema
```json
{
"name": "my-plugin", // kebab-case, no spaces — also the skill namespace prefix
"displayName": "My Plugin", // human-readable; shown in UI (v2.1.143+)
"description": "What it does",
"version": "1.0.0", // OPTIONAL — omit to use git SHA per commit
"author": { "name": "Name", "url": "https://..." },
"homepage": "https://...",
"repository": "https://github.com/...",
"license": "MIT",
"keywords": [],
"defaultEnabled": true, // set false to install disabled (v2.1.154+)
"dependencies": [
{ "name": "other-plugin", "version": "~2.1.0" }
]
}
```
Only `name` is required. Add fields only when needed.
---
## marketplace.json schema
```json
{
"name": "my-ai-marketplace",
"owner": { "name": "Your Name", "email": "you@example.com" },
"description": "Description",
"version": "1.0.0",
"plugins": [
{
"name": "startup-cto",
"source": "./plugins/startup-cto",
"description": "...",
"strict": true
}
]
}
```
`description` and `version` are also accepted under a `metadata` key for backward compatibility.
---
## Plugin source types
| Type | Format | Notes |
|---|---|---|
| Relative path | `"./plugins/my-plugin"` | Must start with `./`. Resolved from marketplace root. Only works with git-hosted marketplaces, not URL-based. |
| GitHub | `{ "source": "github", "repo": "owner/repo", "ref": "main", "sha": "abc123" }` | sha pins exact commit; ref is branch/tag |
| URL / git | `{ "source": "url", "url": "https://...", "ref": "main" }` | also accepts `owner/repo` shorthand and SSH URLs |
| git-subdir | `{ "source": "git-subdir", "url": "...", "path": "packages/my-plugin" }` | sparse clone of a monorepo path |
| npm | `{ "source": "npm", "package": "@scope/pkg", "version": "^2.0.0", "registry": "https://..." }` | installed via npm install |
When both `ref` and `sha` are set, `sha` is the effective pin.
---
## Strict mode
Controls whether `plugin.json` is the authority for component definitions.
- **`strict: true`** (default) — plugin has its own `plugin.json`; marketplace entry can add extra skills/hooks on top.
- **`strict: false`** — marketplace entry is the entire definition; plugin needs no `plugin.json`. The entry declares `skills`, `agents`, `hooks`, `mcpServers` path arrays.
Do not use `strict: false` plus a component-declaring `plugin.json` — this is a conflict and fails to load.
---
## Reserved marketplace names
These names are blocked for third-party use:
`claude-code-marketplace`, `claude-code-plugins`, `claude-plugins-official`,
`claude-plugins-community`, `claude-community`, `anthropic-marketplace`,
`anthropic-plugins`, `agent-skills`, `anthropic-agent-skills`,
`knowledge-work-plugins`, `life-sciences`, `claude-for-legal`,
`claude-for-financial-services`, `financial-services-plugins`
Names that impersonate official marketplaces are also blocked (e.g. `official-claude-plugins`,
`anthropic-tools-v2`).
Plugin names must be kebab-case (lowercase, digits, hyphens). Claude.ai marketplace sync
rejects anything else even if the local CLI tolerates it.
---
## Version management
- If `version` is set in `plugin.json`, users receive updates only when you bump it.
- If `version` is omitted, git commit SHA is used — every commit is a new version.
- If `version` is set in both `plugin.json` and the marketplace entry, `plugin.json` wins silently.
- **Recommendation:** omit `version` unless you need explicit release gates.
---
## Environment variables
- **`${CLAUDE_PLUGIN_ROOT}`** — absolute path to the plugin's installation directory. Use in hook commands and MCP/LSP configs for all in-plugin file references. This path changes on update.
- **`${CLAUDE_PLUGIN_DATA}`** — persistent directory for plugin state that survives updates. Use for `node_modules`, generated code, caches.
---
## Validation and CLI commands
```bash
# Validate plugin structure and manifest
claude plugin validate ./my-plugin
claude plugin validate ./my-plugin --strict # treat warnings as errors
# Install/manage
claude plugin install <name>@<marketplace>
claude plugin update <name>@<marketplace>
claude plugin uninstall <name>
claude plugin list
claude plugin enable <name>
claude plugin disable <name>
# Marketplace
claude plugin marketplace add owner/repo
claude plugin marketplace update
# Development
claude --plugin-dir ./my-plugin # load without installing
claude --plugin-dir ./my-plugin.zip # load from zip (v2.1.128+)
claude plugin init my-tool # scaffold a skills-dir plugin
```
---
## Key gotchas
1. **Plugins are copied to cache on install.** Cannot reference `../shared-utils` — those files are not copied. Duplicate shared files into each plugin or use symlinks.
2. **`commands/` ≠ `skills/`.** Flat `foo.md` is a legacy command; `foo/SKILL.md` is a skill. Promote flat commands to skill directories during migration.
3. **Only `plugin.json` in `.claude-plugin/`.** Skills, agents, hooks, and other directories must be at the plugin root, not inside `.claude-plugin/`.
4. **Plugin names are skill namespace prefixes.** `name: my-plugin` means skills invoke as `/my-plugin:skill-name`.
5. **`defaultEnabled: false` requires v2.1.154+.** Earlier versions ignore it and enable on install.

View File

@@ -0,0 +1,143 @@
# GitHub Copilot CLI Plugin Reference
Verified against docs.github.com as of June 2026.
---
## Directory structure
```text
plugin-root/
├── plugin.json # at plugin root (NOT in .claude-plugin/)
├── skills/ # skill directories: <name>/SKILL.md (same as Claude Code)
├── agents/ # agent files: <name>.agent.md (differs from Claude Code)
├── hooks.json # at plugin root (differs from Claude Code: hooks/hooks.json)
└── .mcp.json # at plugin root (same as Claude Code)
```
---
## plugin.json schema (Copilot)
```json
{
"name": "my-plugin",
"description": "What it does",
"version": "1.0.0",
"author": { "name": "Name", "email": "you@example.com" },
"license": "MIT",
"keywords": [],
"agents": "agents/",
"skills": ["skills/"],
"hooks": "hooks.json",
"mcpServers": ".mcp.json"
}
```
Key difference from Claude Code: Copilot expects component path declarations inside
`plugin.json` (`"skills": "skills/"`, `"agents": "agents/"`, etc.). Claude Code instead
defaults to standard dirs and takes path overrides only via the marketplace entry.
This means the same `plugin.json` may need these fields for Copilot but not for Claude.
---
## marketplace.json schema (Copilot)
```json
{
"name": "my-ai-marketplace",
"owner": { "name": "Your Name", "email": "you@example.com" },
"metadata": { "description": "Agents, skills and workflows", "version": "1.0.0" },
"plugins": [
{
"name": "startup-cto",
"source": "./plugins/startup-cto",
"description": "...",
"version": "1.0.0"
}
]
}
```
Copilot's primary marketplace manifest path is `.github/plugin/marketplace.json`.
It also reads `.claude-plugin/marketplace.json` as a fallback.
Relative `source` paths: `./x` and `x` are both valid (Claude requires `./`).
---
## Agent file format
Copilot agents use `.agent.md` extension with frontmatter:
```markdown
---
name: my-agent
description: What this agent does
tools:
- read_file
- run_command
---
Agent instructions here.
```
Claude Code agents use `.md` extension without the `.agent.md` suffix.
If shipping agents for both tools, create both files:
- `agents/my-agent.md` — Claude Code
- `agents/my-agent.agent.md` — Copilot CLI
---
## CLI commands
```bash
# Install plugin locally (development)
copilot plugin install ./my-plugin
# List installed plugins
copilot plugin list
# In interactive mode
/plugin list
/skills list
/agent
# Reload after changes
/reload-plugins
# Uninstall (uses bare plugin name, not @marketplace form)
copilot plugin uninstall <name>
# Marketplace
copilot plugin marketplace add owner/repo
```
> ⚠️ **UNVERIFIED: Copilot marketplace install command.**
> The `update`/`uninstall` commands take a bare `<name>`. Whether install from a marketplace
> uses `<name>@<marketplace>` (Claude Code's form) or a bare `<name>` is not confirmed in docs.
> Run `copilot plugin install --help` before documenting the install command anywhere.
---
## Validation
Copilot has no documented `plugin validate` command. For Copilot-side validation, run manual checks:
- Valid JSON in `plugin.json` and `marketplace.json`
- Required fields: `name`, `description`
- Unique plugin names across marketplace
- Kebab-case plugin names
- All `source` paths resolve to existing directories
- `.agent.md` files have valid YAML frontmatter with `name`, `description`, `tools`
---
## Key differences from Claude Code (summary)
| What | Claude Code | Copilot CLI |
|---|---|---|
| Plugin manifest location | `.claude-plugin/plugin.json` | `plugin.json` at plugin root |
| Agent files | `agents/<name>.md` | `agents/<name>.agent.md` |
| Hooks file | `hooks/hooks.json` | `hooks.json` at plugin root |
| Component paths | Declared in marketplace entry | Declared in `plugin.json` |
| Validate command | `claude plugin validate` | None — manual checks only |
| Relative source `./` | Required | Optional (`x` also valid) |

View File

@@ -0,0 +1,79 @@
# Cross-Tool Compatibility Reference
Claude Code and GitHub Copilot CLI share the plugin concept but diverge in specific, breaking
ways. **Skills are the portable core. Manifests and agents are where they split.**
Make Claude Code the source of truth — it is the stricter, more fully specified format.
Treat "loads in Copilot CLI" as a tested checklist item per plugin, not an assumption.
---
## Divergence table
| Concern | Claude Code | GitHub Copilot CLI | Portable choice |
|---|---|---|---|
| Marketplace manifest path | `.claude-plugin/marketplace.json` (required) | `.github/plugin/marketplace.json` (primary); also reads `.claude-plugin/` | Put it in `.claude-plugin/` — both read it. Optionally mirror to `.github/plugin/`. |
| Plugin manifest path | `.claude-plugin/plugin.json` (required; only `plugin.json` goes in this dir) | `plugin.json` at **plugin root** | Ship in both locations until verified — see ⚠️ below |
| Skills | `skills/<name>/SKILL.md` | `skills/<name>/SKILL.md` | ✅ Identical |
| Agents | `agents/<name>.md` | `agents/<name>.agent.md` (frontmatter incl. `tools:`) | Diverges — keep portable logic in skills; ship per-tool agent files only when needed |
| Hooks | `hooks/hooks.json` | `hooks.json` at plugin root | Diverges; declare paths in manifest to be safe |
| MCP servers | `.mcp.json` at plugin root | `.mcp.json` at plugin root | ✅ Same |
| Relative `source` | must start with `./` | `./x` and `x` both valid | Always use `./` — valid for both |
| Validate command | `claude plugin validate .` (or `/plugin validate .`) | none documented | Run Claude validator + manual JSON checks for Copilot |
| Install marketplace | `claude plugin marketplace add owner/repo` | `copilot plugin marketplace add owner/repo` | Same shape |
| Install plugin | `claude plugin install <name>@<marketplace-name>` | install by plugin name; `@marketplace` suffix unconfirmed | ⚠️ Verify Copilot install string before documenting |
| Local install (dev) | `claude --plugin-dir ./plugin` | `copilot plugin install ./plugin` | Tool-specific |
| Component paths in plugin.json | Claude defaults to standard dirs; path overrides via marketplace entry only | `"skills": "skills/"`, `"agents": "agents/"`, etc. in plugin.json | Generate per-tool manifests rather than one shared file |
> ⚠️ **UNVERIFIED — test before committing to a layout.**
> Copilot docs confirm it reads the **marketplace** manifest from `.claude-plugin/`. They do NOT
> confirm the same fallback for a plugin's `plugin.json`. Copilot docs show `plugin.json` at plugin
> root; Claude requires it in `.claude-plugin/`. Until verified: ship `plugin.json` in BOTH
> `plugin-name/plugin.json` and `plugin-name/.claude-plugin/plugin.json` (identical content), then
> drop whichever proves redundant.
---
## `@<marketplace-name>` resolution
`claude plugin install startup-cto@my-ai-marketplace` requires the marketplace manifest's
top-level `name` field to be exactly `my-ai-marketplace`. It is **not** the GitHub repo name.
Keep them aligned to avoid confusion, but they are separate fields.
---
## Canonical cross-compatible repo layout
```text
repo-root/
├── .claude-plugin/
│ └── marketplace.json # both tools read here
├── .github/plugin/
│ └── marketplace.json # OPTIONAL: Copilot canonical path (mirror)
├── plugins/
│ └── startup-cto/
│ ├── plugin.json # Copilot root manifest ┐ ship both until
│ ├── .claude-plugin/ # │ the note above is
│ │ └── plugin.json # Claude manifest ┘ verified
│ ├── skills/
│ │ ├── fundraising/SKILL.md
│ │ └── hiring/SKILL.md
│ ├── agents/
│ │ ├── startup-cto.md # Claude
│ │ └── startup-cto.agent.md # Copilot (only if shipping native agents)
│ ├── hooks/hooks.json # Claude
│ ├── hooks.json # Copilot (if hooks used)
│ └── README.md
└── README.md
```
---
## Open questions to resolve before generating layouts
1. **Does Copilot CLI load a plugin whose `plugin.json` lives only in `.claude-plugin/`?**
Install a test plugin both ways. The answer decides whether to ship one manifest or two.
2. **What is Copilot's exact install-from-marketplace command?**
Run `copilot plugin install --help`. The `update`/`uninstall` commands take a bare plugin
name — the `@marketplace` form may not apply.

View File

@@ -0,0 +1,194 @@
#!/usr/bin/env bash
# Generate plugin.json (in both locations) and marketplace.json.
# Dry-run by default; pass --write to apply.
#
# Usage:
# gen_manifests.sh <repo-root> --marketplace-name <name> [options]
#
# Options:
# --marketplace-name <name> kebab-case marketplace identifier (required)
# --author "Name <email>" author string (default: "Unknown <unknown@example.com>")
# --plugins-dir <dir> subdir containing plugin folders (default: plugins)
# --mirror-github also write to .github/plugin/marketplace.json
# --write apply changes (default is dry run)
set -euo pipefail
RESERVED_NAMES="claude-code-marketplace claude-code-plugins claude-plugins-official
claude-plugins-community claude-community anthropic-marketplace anthropic-plugins
agent-skills anthropic-agent-skills knowledge-work-plugins life-sciences
claude-for-legal claude-for-financial-services financial-services-plugins"
RESERVED_PATTERNS="official-claude anthropic-tools claude-official"
is_kebab_case() {
[[ "$1" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]
}
is_reserved() {
local name="$1"
for n in $RESERVED_NAMES; do
[[ "$name" == "$n" ]] && return 0
done
for p in $RESERVED_PATTERNS; do
[[ "$name" == "$p"* ]] && return 0
done
return 1
}
validate_name() {
local name="$1" context="$2"
local ok=true
if ! is_kebab_case "$name"; then
echo "ERROR: $context: name '$name' is not kebab-case (lowercase, digits, hyphens only)." >&2
ok=false
fi
if is_reserved "$name"; then
echo "ERROR: $context: name '$name' is reserved for official Anthropic use." >&2
ok=false
fi
[[ "$ok" == "true" ]]
}
write_json() {
local path="$1" content="$2" dry_run="$3"
if [[ "$dry_run" == "true" ]]; then
echo ""
echo "--- $path (dry run) ---"
echo "$content"
else
mkdir -p "$(dirname "$path")"
echo "$content" > "$path"
echo " Written: $path"
fi
}
# ── parse args ───────────────────────────────────────────────────────────────
ROOT=""
MARKETPLACE_NAME=""
AUTHOR="Unknown <unknown@example.com>"
PLUGINS_DIR="plugins"
MIRROR_GITHUB=false
WRITE=false
while [[ $# -gt 0 ]]; do
case "$1" in
--marketplace-name) MARKETPLACE_NAME="$2"; shift 2 ;;
--author) AUTHOR="$2"; shift 2 ;;
--plugins-dir) PLUGINS_DIR="$2"; shift 2 ;;
--mirror-github) MIRROR_GITHUB=true; shift ;;
--write) WRITE=true; shift ;;
-*) echo "Unknown option: $1" >&2; exit 1 ;;
*) ROOT="$1"; shift ;;
esac
done
if [[ -z "$ROOT" || -z "$MARKETPLACE_NAME" ]]; then
echo "Usage: gen_manifests.sh <repo-root> --marketplace-name <name> [--write]" >&2
exit 1
fi
ROOT="$(cd "$ROOT" && pwd)"
DRY_RUN=$( [[ "$WRITE" == "true" ]] && echo "false" || echo "true" )
[[ "$DRY_RUN" == "true" ]] && echo "DRY RUN — pass --write to apply changes"
# Parse author
AUTHOR_NAME="${AUTHOR%% <*}"
AUTHOR_EMAIL=""
if [[ "$AUTHOR" =~ \<(.+)\> ]]; then
AUTHOR_EMAIL="${BASH_REMATCH[1]}"
fi
# Validate marketplace name
validate_name "$MARKETPLACE_NAME" "marketplace" || exit 1
# Discover plugins
PLUGINS_PATH="$ROOT/$PLUGINS_DIR"
if [[ ! -d "$PLUGINS_PATH" ]]; then
echo "No plugins directory found at $PLUGINS_PATH" >&2
exit 1
fi
mapfile -t PLUGIN_DIRS < <(find "$PLUGINS_PATH" -mindepth 1 -maxdepth 1 -type d ! -name '.*' | sort)
if [[ ${#PLUGIN_DIRS[@]} -eq 0 ]]; then
echo "No plugin directories found in $PLUGINS_PATH" >&2
exit 1
fi
echo "Found ${#PLUGIN_DIRS[@]} plugin(s)"
# Build plugins array for marketplace.json
PLUGINS_JSON="[]"
for pd in "${PLUGIN_DIRS[@]}"; do
pname="$(basename "$pd")"
validate_name "$pname" "plugin '$pname'" || exit 1
# Read existing plugin.json if present
existing_claude="$pd/.claude-plugin/plugin.json"
existing_root="$pd/plugin.json"
existing_desc="Plugin: $pname"
existing_name="$pname"
for existing in "$existing_claude" "$existing_root"; do
if [[ -f "$existing" ]] && jq -e . "$existing" >/dev/null 2>&1; then
d=$(jq -r '.description // empty' "$existing")
n=$(jq -r '.name // empty' "$existing")
[[ -n "$d" ]] && existing_desc="$d"
[[ -n "$n" ]] && existing_name="$n"
break
fi
done
# Warn on version
for existing in "$existing_claude" "$existing_root"; do
if [[ -f "$existing" ]] && jq -e '.version' "$existing" >/dev/null 2>&1; then
echo " WARNING: plugin '$pname' sets version in plugin.json. Do not also set it in the marketplace entry — plugin.json wins silently."
break
fi
done
# Build plugin.json
plugin_json=$(jq -n \
--arg name "$existing_name" \
--arg desc "$existing_desc" \
--arg aname "$AUTHOR_NAME" \
--arg aemail "$AUTHOR_EMAIL" \
'{name: $name, description: $desc, author: {name: $aname, email: $aemail}}')
write_json "$pd/plugin.json" "$plugin_json" "$DRY_RUN"
write_json "$pd/.claude-plugin/plugin.json" "$plugin_json" "$DRY_RUN"
source="./$PLUGINS_DIR/$pname"
PLUGINS_JSON=$(echo "$PLUGINS_JSON" | jq \
--arg name "$existing_name" \
--arg src "$source" \
--arg desc "$existing_desc" \
'. + [{name: $name, source: $src, description: $desc}]')
done
# Build marketplace.json
marketplace_json=$(jq -n \
--arg name "$MARKETPLACE_NAME" \
--arg aname "$AUTHOR_NAME" \
--arg aemail "$AUTHOR_EMAIL" \
--arg desc "$MARKETPLACE_NAME plugin marketplace" \
--argjson plugins "$PLUGINS_JSON" \
'{name: $name, owner: {name: $aname, email: $aemail}, description: $desc, plugins: $plugins}')
write_json "$ROOT/.claude-plugin/marketplace.json" "$marketplace_json" "$DRY_RUN"
if [[ "$MIRROR_GITHUB" == "true" ]]; then
write_json "$ROOT/.github/plugin/marketplace.json" "$marketplace_json" "$DRY_RUN"
fi
if [[ "$DRY_RUN" == "true" ]]; then
echo ""
echo "--- End dry run. Pass --write to apply. ---"
else
echo ""
echo "Done. Run scripts/validate.sh to verify."
fi

View File

@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# Scan a repository and classify every asset as skill/command/agent/hook/prompt/MCP.
# Outputs a markdown table of findings plus a list of cross-reference warnings.
#
# Usage: inventory.sh <repo-path>
set -euo pipefail
if [[ $# -lt 1 ]]; then
echo "Usage: inventory.sh <repo-path>" >&2
exit 1
fi
ROOT="$(cd "$1" && pwd)"
if [[ ! -d "$ROOT" ]]; then
echo "Error: $ROOT is not a directory" >&2
exit 1
fi
# ── classify assets ──────────────────────────────────────────────────────────
declare -a ROWS=()
declare -a CROSS_REFS=()
while IFS= read -r -d '' path; do
rel="${path#"$ROOT/"}"
name="$(basename "$path")"
dir="$(dirname "$rel")"
parent="$(basename "$dir")"
# Skip hidden dirs except .claude-plugin and .github
skip=false
IFS='/' read -ra parts <<< "$dir"
for part in "${parts[@]}"; do
if [[ "$part" == .* && "$part" != ".claude-plugin" && "$part" != ".github" ]]; then
skip=true; break
fi
done
$skip && continue
asset_type=""
case "$name" in
SKILL.md) asset_type="skill" ;;
hooks.json) asset_type="hook" ;;
.mcp.json) asset_type="mcp" ;;
.lsp.json) asset_type="lsp" ;;
plugin.json) asset_type="manifest-plugin" ;;
marketplace.json) asset_type="manifest-marketplace" ;;
*.agent.md) asset_type="agent-copilot" ;;
*.md)
if [[ "$parent" == "agents" ]]; then
asset_type="agent-claude"
elif [[ "$parent" == "commands" ]]; then
asset_type="command"
elif [[ "$rel" != *"/skills/"* && "$rel" != *"/commands/"* && "$rel" != *"/agents/"* ]]; then
asset_type="prompt"
fi
;;
esac
[[ -n "$asset_type" ]] && ROWS+=("$asset_type|$rel")
# Check for cross-references in text files
case "$name" in *.md|*.json|*.sh)
if grep -q '\.\.\/' "$path" 2>/dev/null; then
while IFS= read -r line; do
lineno="${line%%:*}"
content="${line#*:}"
CROSS_REFS+=("$rel:$lineno: $content")
done < <(grep -n '\.\.\/' "$path" 2>/dev/null | head -20)
fi
;;
esac
done < <(find "$ROOT" -type f -print0 | sort -z)
# ── report ───────────────────────────────────────────────────────────────────
echo "# Asset Inventory: $ROOT"
echo ""
echo "## Assets"
echo ""
echo "| Type | Path |"
echo "|---|---|"
for row in "${ROWS[@]+"${ROWS[@]}"}"; do
type="${row%%|*}"
path="${row#*|}"
echo "| \`$type\` | \`$path\` |"
done | sort
total="${#ROWS[@]}"
echo ""
echo "**Total: $total assets**"
echo ""
# Summary by type
echo "## Summary by type"
echo ""
for row in "${ROWS[@]+"${ROWS[@]}"}"; do
echo "${row%%|*}"
done | sort | uniq -c | while read -r count type; do
echo "- \`$type\`: $count"
done
# Cross-reference warnings
echo ""
if [[ ${#CROSS_REFS[@]} -gt 0 ]]; then
echo "## ⚠️ Cross-reference warnings (${#CROSS_REFS[@]} found)"
echo ""
echo "These \`../\` references will break after install-time caching:"
echo ""
for ref in "${CROSS_REFS[@]}"; do
echo "- \`$ref\`"
done
else
echo "## Cross-references"
echo ""
echo "No \`../\` cross-references found. Safe to proceed with plugin boundaries."
fi

View File

@@ -0,0 +1,264 @@
#!/usr/bin/env bash
# Validate plugin marketplace manifests for Claude Code and GitHub Copilot CLI.
# Wraps `claude plugin validate` (Claude-side) and runs manual checks (Copilot-side).
#
# Usage:
# validate.sh <repo-root>
# validate.sh <repo-root> --plugin plugins/my-plugin
set -euo pipefail
RESERVED_NAMES="claude-code-marketplace claude-code-plugins claude-plugins-official
claude-plugins-community claude-community anthropic-marketplace anthropic-plugins
agent-skills anthropic-agent-skills knowledge-work-plugins life-sciences
claude-for-legal claude-for-financial-services financial-services-plugins"
RESERVED_PATTERNS="official-claude anthropic-tools claude-official"
ERRORS=0
WARNINGS=0
error() { echo "ERROR: $1"; ((ERRORS++)) || true; }
warn() { echo "WARN: $1"; ((WARNINGS++)) || true; }
is_kebab_case() { [[ "$1" =~ ^[a-z0-9]+(-[a-z0-9]+)*$ ]]; }
is_reserved() {
local name="$1"
for n in $RESERVED_NAMES; do [[ "$name" == "$n" ]] && return 0; done
for p in $RESERVED_PATTERNS; do [[ "$name" == "$p"* ]] && return 0; done
return 1
}
validate_name() {
local name="$1" context="$2"
[[ -z "$name" ]] && { error "$context: name is missing or empty"; return 0; }
is_kebab_case "$name" || error "$context: name '$name' is not kebab-case"
if is_reserved "$name"; then
error "$context: name '$name' is reserved for official Anthropic use"
fi
return 0
}
valid_json() {
local path="$1"
if ! jq -e . "$path" >/dev/null 2>&1; then
error "Invalid JSON in $path"
return 1
fi
return 0
}
validate_marketplace_json() {
local path="$1"
[[ -f "$path" ]] || return 0
valid_json "$path" || return 0
local name
name=$(jq -r '.name // empty' "$path")
[[ -z "$name" ]] && error "$path: 'name' field is required" || validate_name "$name" "$path"
local plugins_type
plugins_type=$(jq -r 'if .plugins | type == "array" then "ok" else "bad" end' "$path")
if [[ "$plugins_type" != "ok" ]]; then
error "$path: 'plugins' must be an array"
return 0
fi
# Check each plugin entry
local seen_names=()
while IFS= read -r pname; do
# Duplicate check
for seen in "${seen_names[@]+"${seen_names[@]}"}"; do
if [[ "$seen" == "$pname" ]]; then error "$path: duplicate plugin name '$pname'"; fi
done
seen_names+=("$pname")
validate_name "$pname" "$path plugin '$pname'"
# Source path check
local src
src=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .source // empty' "$path")
if [[ -n "$src" && "$src" != ./* && "$src" != "github" && "$src" != "npm" && "$src" != "url" && "$src" != "git-subdir" ]]; then
warn "$path plugin '$pname': relative source '$src' should start with './' for Claude Code compatibility"
fi
# Version duplication warning
local has_ver
has_ver=$(jq -r --arg n "$pname" '.plugins[] | select(.name==$n) | .version // empty' "$path")
if [[ -n "$has_ver" ]]; then
warn "$path plugin '$pname': version set in marketplace entry. If also set in plugin.json, plugin.json wins silently."
fi
done < <(jq -r '.plugins[].name // empty' "$path")
return 0
}
validate_plugin_json() {
local path="$1" marketplace_json="${2:-}"
[[ -f "$path" ]] || return 0
valid_json "$path" || return 0
local name
name=$(jq -r '.name // empty' "$path")
if [[ -z "$name" ]]; then
warn "$path: 'name' field missing (plugin dir name will be used)"
else
validate_name "$name" "$path"
# Version duplication check
if [[ -n "$marketplace_json" && -f "$marketplace_json" ]]; then
local pver mver
pver=$(jq -r '.version // empty' "$path")
mver=$(jq -r --arg n "$name" '.plugins[]? | select(.name==$n) | .version // empty' "$marketplace_json")
if [[ -n "$pver" && -n "$mver" ]]; then
error "$path: version '$pver' set in both plugin.json and marketplace entry — plugin.json wins silently. Remove one."
fi
fi
fi
return 0
}
validate_skill_md() {
local path="$1"
local content
content=$(cat "$path")
if [[ "$content" != ---* ]]; then
warn "$path: SKILL.md has no YAML frontmatter"
return
fi
if ! echo "$content" | awk 'NR>1 && /^---/' | grep -q '^---'; then
error "$path: SKILL.md frontmatter not closed"
return
fi
if ! echo "$content" | awk '/^---/{n++; if(n==2) exit} n==1' | grep -q 'description:'; then
warn "$path: SKILL.md frontmatter missing 'description' field"
fi
}
validate_plugin_dir() {
local pd="$1" marketplace_json="${2:-}"
local claude_manifest="$pd/.claude-plugin/plugin.json"
local root_manifest="$pd/plugin.json"
if [[ ! -f "$claude_manifest" && ! -f "$root_manifest" ]]; then
warn "$pd: no plugin.json found (will auto-discover components)"
else
validate_plugin_json "$claude_manifest" "$marketplace_json"
validate_plugin_json "$root_manifest" "$marketplace_json"
# Sync check
if [[ -f "$claude_manifest" && -f "$root_manifest" ]]; then
if ! diff <(jq -S . "$claude_manifest") <(jq -S . "$root_manifest") >/dev/null 2>&1; then
warn "$pd: .claude-plugin/plugin.json and plugin.json differ — keep them in sync"
fi
fi
fi
# Components must not be inside .claude-plugin/
for bad_dir in skills agents hooks commands; do
if [[ -d "$pd/.claude-plugin/$bad_dir" ]]; then
error "$pd/.claude-plugin/$bad_dir: only plugin.json belongs in .claude-plugin/; move $bad_dir/ to plugin root"
fi
done
# Validate SKILL.md files
while IFS= read -r -d '' skill_md; do
validate_skill_md "$skill_md"
done < <(find "$pd" -name "SKILL.md" -print0 2>/dev/null)
# Cross-reference check
while IFS= read -r -d '' f; do
if grep -q '\.\.\/' "$f" 2>/dev/null; then
local rel="${f#"$pd/"}"
error "$rel: contains '../' reference — plugins cannot access files outside their directory after caching"
fi
done < <(find "$pd" \( -name "*.md" -o -name "*.json" \) -print0 2>/dev/null)
}
run_claude_validate() {
local path="$1"
if command -v claude >/dev/null 2>&1; then
if ! claude plugin validate "$path" 2>&1; then
error "claude plugin validate failed for $path"
fi
else
warn "'claude' CLI not found — skipping claude plugin validate"
fi
}
# ── parse args ────────────────────────────────────────────────────────────────
ROOT=""
PLUGIN_ONLY=""
while [[ $# -gt 0 ]]; do
case "$1" in
--plugin) PLUGIN_ONLY="$2"; shift 2 ;;
-*) echo "Unknown option: $1" >&2; exit 1 ;;
*) ROOT="$1"; shift ;;
esac
done
if [[ -z "$ROOT" ]]; then
echo "Usage: validate.sh <repo-root> [--plugin <path>]" >&2
exit 1
fi
ROOT="$(cd "$ROOT" && pwd)"
# ── validate marketplace.json ─────────────────────────────────────────────────
MARKETPLACE_JSON=""
for mp in "$ROOT/.claude-plugin/marketplace.json" "$ROOT/.github/plugin/marketplace.json"; do
if [[ -f "$mp" ]]; then
MARKETPLACE_JSON="$mp"
validate_marketplace_json "$mp"
break
fi
done
if [[ -z "$MARKETPLACE_JSON" ]]; then
warn "No marketplace.json found. Expected at .claude-plugin/marketplace.json"
fi
# ── validate plugins ──────────────────────────────────────────────────────────
if [[ -n "$PLUGIN_ONLY" ]]; then
validate_plugin_dir "$(cd "$PLUGIN_ONLY" && pwd)" "$MARKETPLACE_JSON"
run_claude_validate "$(cd "$PLUGIN_ONLY" && pwd)"
else
plugins_path="$ROOT/plugins"
if [[ -d "$plugins_path" ]]; then
while IFS= read -r -d '' pd; do
validate_plugin_dir "$pd" "$MARKETPLACE_JSON"
run_claude_validate "$pd"
done < <(find "$plugins_path" -mindepth 1 -maxdepth 1 -type d ! -name '.*' -print0 | sort -z)
else
warn "No plugins/ directory found at $ROOT"
fi
fi
# ── check source paths resolve ────────────────────────────────────────────────
if [[ -n "$MARKETPLACE_JSON" ]]; then
while IFS= read -r src; do
[[ "$src" != ./* ]] && continue
src_path="$ROOT/${src#./}"
if [[ ! -d "$src_path" ]]; then error "Marketplace source path '$src' does not exist at $src_path"; fi
done < <(jq -r '.plugins[]?.source | strings' "$MARKETPLACE_JSON" 2>/dev/null)
fi
# ── report ────────────────────────────────────────────────────────────────────
echo ""
if [[ $ERRORS -eq 0 && $WARNINGS -eq 0 ]]; then
echo "✓ All checks passed."
exit 0
elif [[ $ERRORS -eq 0 ]]; then
echo "Passed with $WARNINGS warning(s)."
exit 0
else
echo "Failed. Fix $ERRORS error(s) before proceeding."
exit 1
fi

View File

@@ -0,0 +1,194 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPTS_DIR="$(cd "$(dirname "$0")/../scripts" && pwd)"
PASS=0; FAIL=0
# Use += to avoid ((var++)) returning 0 exit code when var was 0 under set -e
# ── helpers ─────────────────────────────────────────────────────────────────
tmpdir() { mktemp -d; }
assert_contains() {
local label="$1" expected="$2" actual="$3"
if echo "$actual" | grep -qF "$expected"; then
echo " PASS: $label"
PASS=$((PASS+1))
else
echo " FAIL: $label"
echo " expected to contain: $expected"
echo " got: $(echo "$actual" | head -5)"
FAIL=$((FAIL+1))
fi
}
assert_not_contains() {
local label="$1" unexpected="$2" actual="$3"
if echo "$actual" | grep -qF "$unexpected"; then
echo " FAIL: $label"
echo " expected NOT to contain: $unexpected"
FAIL=$((FAIL+1))
else
echo " PASS: $label"
PASS=$((PASS+1))
fi
}
assert_exit() {
local label="$1" expected="$2" actual="$3"
if [[ "$actual" -eq "$expected" ]]; then
echo " PASS: $label"
PASS=$((PASS+1))
else
echo " FAIL: $label"
echo " expected exit $expected, got $actual"
FAIL=$((FAIL+1))
fi
}
assert_file_exists() {
local label="$1" path="$2"
if [[ -f "$path" ]]; then
echo " PASS: $label"
PASS=$((PASS+1))
else
echo " FAIL: $label"
echo " file not found: $path"
FAIL=$((FAIL+1))
fi
}
assert_file_absent() {
local label="$1" path="$2"
if [[ ! -e "$path" ]]; then
echo " PASS: $label"
PASS=$((PASS+1))
else
echo " FAIL: $label"
echo " file should not exist: $path"
FAIL=$((FAIL+1))
fi
}
# ── inventory.sh tests ───────────────────────────────────────────────────────
echo "=== inventory.sh ==="
# 1. SKILL.md classified as skill
t=$(tmpdir)
mkdir -p "$t/skills/my-skill"
echo "---" > "$t/skills/my-skill/SKILL.md"
out=$("$SCRIPTS_DIR/inventory.sh" "$t")
assert_contains "SKILL.md classified as skill" "skill" "$out"
rm -rf "$t"
# 2. agents/foo.agent.md classified as agent-copilot
t=$(tmpdir)
mkdir -p "$t/agents"
touch "$t/agents/my-agent.agent.md"
out=$("$SCRIPTS_DIR/inventory.sh" "$t")
assert_contains "agents/*.agent.md classified as agent-copilot" "agent-copilot" "$out"
rm -rf "$t"
# 3. ../ in file content produces cross-reference warning
t=$(tmpdir)
mkdir -p "$t/skills/my-skill"
printf -- '---\ndescription: test\n---\nSee ../shared/file.md\n' > "$t/skills/my-skill/SKILL.md"
out=$("$SCRIPTS_DIR/inventory.sh" "$t")
assert_contains "../ cross-reference warning emitted" "Cross-reference" "$out"
assert_contains "../ path shown in warning" "../shared/file.md" "$out"
rm -rf "$t"
# ── gen_manifests.sh tests ───────────────────────────────────────────────────
echo ""
echo "=== gen_manifests.sh ==="
# 4. Without --write, no files are created
t=$(tmpdir)
mkdir -p "$t/plugins/my-plugin/skills/hello"
echo "---" > "$t/plugins/my-plugin/skills/hello/SKILL.md"
"$SCRIPTS_DIR/gen_manifests.sh" "$t" --marketplace-name my-marketplace >/dev/null
assert_file_absent "dry run: .claude-plugin/marketplace.json not written" "$t/.claude-plugin/marketplace.json"
assert_file_absent "dry run: plugin.json not written" "$t/plugins/my-plugin/plugin.json"
rm -rf "$t"
# 5. With --write, creates plugin.json in both locations
t=$(tmpdir)
mkdir -p "$t/plugins/my-plugin/skills/hello"
echo "---" > "$t/plugins/my-plugin/skills/hello/SKILL.md"
"$SCRIPTS_DIR/gen_manifests.sh" "$t" --marketplace-name my-marketplace --write >/dev/null
assert_file_exists "--write: plugin root plugin.json created" "$t/plugins/my-plugin/plugin.json"
assert_file_exists "--write: .claude-plugin/plugin.json created" "$t/plugins/my-plugin/.claude-plugin/plugin.json"
rm -rf "$t"
# 6. Reserved name exits 1
t=$(tmpdir)
mkdir -p "$t/plugins/my-plugin"
set +e
out=$("$SCRIPTS_DIR/gen_manifests.sh" "$t" --marketplace-name claude-plugins-official 2>&1)
code=$?
set -e
assert_exit "reserved name: exit 1" 1 "$code"
assert_contains "reserved name: error message" "reserved" "$out"
rm -rf "$t"
# ── validate.sh tests ────────────────────────────────────────────────────────
echo ""
echo "=== validate.sh ==="
# 7. Invalid JSON exits 1
t=$(tmpdir)
mkdir -p "$t/.claude-plugin"
echo "not json" > "$t/.claude-plugin/marketplace.json"
set +e
out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1)
code=$?
set -e
assert_exit "invalid JSON: exit 1" 1 "$code"
assert_contains "invalid JSON: error message" "ERROR" "$out"
rm -rf "$t"
# 8. Reserved marketplace name exits 1
t=$(tmpdir)
mkdir -p "$t/.claude-plugin"
printf '{"name":"claude-plugins-official","plugins":[]}\n' > "$t/.claude-plugin/marketplace.json"
set +e
out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1)
code=$?
set -e
assert_exit "reserved marketplace name: exit 1" 1 "$code"
assert_contains "reserved marketplace name: error message" "reserved" "$out"
rm -rf "$t"
# 9. ../ in plugin file exits 1
t=$(tmpdir)
mkdir -p "$t/.claude-plugin" "$t/plugins/my-plugin/skills/hello"
printf '{"name":"my-marketplace","plugins":[{"name":"my-plugin","source":"./plugins/my-plugin"}]}\n' \
> "$t/.claude-plugin/marketplace.json"
printf -- '---\ndescription: test\n---\nSee ../shared.md\n' \
> "$t/plugins/my-plugin/skills/hello/SKILL.md"
set +e
out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1)
code=$?
set -e
assert_exit "..// in plugin: exit 1" 1 "$code"
assert_contains "..// in plugin: error message" "../" "$out"
rm -rf "$t"
# 10. No marketplace.json warns but exits 0
t=$(tmpdir)
set +e
out=$("$SCRIPTS_DIR/validate.sh" "$t" 2>&1)
code=$?
set -e
assert_exit "no marketplace.json: exit 0" 0 "$code"
assert_contains "no marketplace.json: warning emitted" "WARN" "$out"
rm -rf "$t"
# ── summary ──────────────────────────────────────────────────────────────────
echo ""
echo "Results: $PASS passed, $FAIL failed"
[[ $FAIL -eq 0 ]]

View File

@@ -12,4 +12,5 @@
| `iac` | write-ansible-role, write-terraform-module, write-k8s-manifest, write-docker-compose, proxmox-vm-spec, iac-security-review, write-molecule-test |
| `cross-cutting` | zoom-out, caveman, session-handoff, governance-check, git-guardrails, git-commit-message |
| `factory` | write-skill, write-adr, write-workflow, write-eval, validate-skill, upgrade-skill, write-issue-spec |
| `marketplace` | marketplace-architect — plugin and skill distribution tooling for Claude Code / GitHub Copilot CLI |
| `roles` | architect, developer, reviewer, security, qa, ops — Chunk 5 |