Преглед изворни кода

fix(plugin): Make plugin/marketplace manifests valid + CI validation gate (#4)

Fixes #4 - /plugin marketplace add failed to load. Three layered manifest
defects, surfaced by the authoritative `claude plugin validate`:

- marketplace.json: moved to .claude-plugin/, added required `owner` object,
  fixed plugin `source` to relative "./" (canonical, avoids re-clone).
- plugin.json: `author` -> object; removed unrecognized `components`/`categories`
  keys (Claude Code auto-discovers components from their directories).
- Dropped stale hard-coded counts from both descriptions.

Guard so it can't recur: both validators now shell out to `claude plugin
validate` (a hand-rolled jq check gave false confidence), and a new GitHub
Actions workflow runs it on every push/PR. Verified end-to-end with a local
marketplace add + install (81 skills / 23 agents / 3 commands / 10 hooks /
13 output-styles auto-discovered).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
0xDarkMatter пре 2 недеља
родитељ
комит
eae1b2cd76
8 измењених фајлова са 225 додато и 170 уклоњено
  1. 15 0
      .claude-plugin/marketplace.json
  2. 5 156
      .claude-plugin/plugin.json
  3. 30 0
      .github/workflows/validate.yml
  4. 1 0
      README.md
  5. 0 14
      marketplace.json
  6. 8 0
      tests/justfile
  7. 80 0
      tests/validate.ps1
  8. 86 0
      tests/validate.sh

+ 15 - 0
.claude-plugin/marketplace.json

@@ -0,0 +1,15 @@
+{
+  "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
+  "name": "claude-mods",
+  "description": "Custom commands, skills, agents, and rules for Claude Code",
+  "owner": {
+    "name": "0xDarkMatter"
+  },
+  "plugins": [
+    {
+      "name": "claude-mods",
+      "description": "Expert agents, skills, commands, rules, hooks, and output styles for Claude Code - session continuity and modern CLI tooling",
+      "source": "./"
+    }
+  ]
+}

+ 5 - 156
.claude-plugin/plugin.json

@@ -1,8 +1,11 @@
 {
+  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
   "name": "claude-mods",
   "version": "2.10.0",
-  "description": "Custom commands, skills, and agents for Claude Code - session continuity, 23 expert agents, 80 skills, 2 commands, 8 rules, 9 hooks, 13 output styles, modern CLI tools",
-  "author": "0xDarkMatter",
+  "description": "Custom commands, skills, agents, rules, hooks, and output styles for Claude Code - session continuity and modern CLI tooling for real-world development workflows",
+  "author": {
+    "name": "0xDarkMatter"
+  },
   "repository": "https://github.com/0xDarkMatter/claude-mods",
   "license": "MIT",
   "keywords": [
@@ -13,159 +16,5 @@
     "session-management",
     "cli-tools",
     "output-styles"
-  ],
-  "components": {
-    "commands": [
-      "commands/sync.md",
-      "commands/save.md"
-    ],
-    "agents": [
-      "agents/astro-expert.md",
-      "agents/asus-router-expert.md",
-      "agents/aws-fargate-ecs-expert.md",
-      "agents/bash-expert.md",
-      "agents/claude-architect.md",
-      "agents/cloudflare-expert.md",
-      "agents/craftcms-expert.md",
-      "agents/cypress-expert.md",
-      "agents/firecrawl-expert.md",
-      "agents/go-expert.md",
-      "agents/javascript-expert.md",
-      "agents/laravel-expert.md",
-      "agents/payloadcms-expert.md",
-      "agents/postgres-expert.md",
-      "agents/project-organizer.md",
-      "agents/python-expert.md",
-      "agents/react-expert.md",
-      "agents/rust-expert.md",
-      "agents/sql-expert.md",
-      "agents/typescript-expert.md",
-      "agents/vue-expert.md",
-      "agents/wrangler-expert.md",
-      "agents/git-agent.md"
-    ],
-    "skills": [
-      "skills/api-design-ops",
-      "skills/auto-skill",
-      "skills/pigeon",
-      "skills/astro-ops",
-      "skills/atomise",
-      "skills/auth-ops",
-      "skills/ci-cd-ops",
-      "skills/claude-code-debug",
-      "skills/claude-code-headless",
-      "skills/claude-code-hooks",
-      "skills/cli-ops",
-      "skills/code-stats",
-      "skills/color-ops",
-      "skills/container-orchestration",
-      "skills/data-processing",
-      "skills/doc-scanner",
-      "skills/debug-ops",
-      "skills/docker-ops",
-      "skills/explain",
-      "skills/file-search",
-      "skills/find-replace",
-      "skills/fleet-ops",
-      "skills/git-ops",
-      "skills/github-ops",
-      "skills/go-ops",
-      "skills/introspect",
-      "skills/iterate",
-      "skills/javascript-ops",
-      "skills/laravel-ops",
-      "skills/leveldb-ops",
-      "skills/log-ops",
-      "skills/markitdown",
-      "skills/mcp-ops",
-      "skills/migrate-ops",
-      "skills/mac-ops",
-      "skills/monitoring-ops",
-      "skills/net-ops",
-      "skills/nginx-ops",
-      "skills/perf-ops",
-      "skills/portless-ops",
-      "skills/postgres-ops",
-      "skills/prompt-injection-defense",
-      "skills/process-compose-ops",
-      "skills/project-planner",
-      "skills/push-gate",
-      "skills/python-async-ops",
-      "skills/python-cli-ops",
-      "skills/python-database-ops",
-      "skills/python-env",
-      "skills/python-fastapi-ops",
-      "skills/python-observability-ops",
-      "skills/python-pytest-ops",
-      "skills/python-typing-ops",
-      "skills/react-ops",
-      "skills/refactor-ops",
-      "skills/rest-ops",
-      "skills/review",
-      "skills/rust-ops",
-      "skills/scaffold",
-      "skills/screenshot",
-      "skills/security-ops",
-      "skills/supply-chain-defense",
-      "skills/setperms",
-      "skills/skill-creator",
-      "skills/spawn",
-      "skills/sql-ops",
-      "skills/sqlite-ops",
-      "skills/structural-search",
-      "skills/summon",
-      "skills/tailwind-ops",
-      "skills/task-runner",
-      "skills/techdebt",
-      "skills/testing-ops",
-      "skills/testgen",
-      "skills/tool-discovery",
-      "skills/typescript-ops",
-      "skills/unfold-admin",
-      "skills/vue-ops",
-      "skills/genart-ops",
-      "skills/windows-ops"
-    ],
-    "rules": [
-      "rules/cli-tools.md",
-      "rules/commit-style.md",
-      "rules/naming-conventions.md",
-      "rules/prompt-injection.md",
-      "rules/skill-agent-updates.md",
-      "rules/supply-chain.md",
-      "rules/thinking.md",
-      "rules/worktree-boundaries.md"
-    ],
-    "hooks": [
-      "hooks/pre-commit-lint.sh",
-      "hooks/post-edit-format.sh",
-      "hooks/dangerous-cmd-warn.sh",
-      "hooks/pre-install-scan.sh",
-      "hooks/manifest-dep-scan.sh",
-      "hooks/check-mail.sh",
-      "hooks/enforce-uv.sh",
-      "hooks/session-start-unicode-scan.sh",
-      "hooks/pre-commit-unicode-scan.sh"
-    ],
-    "output-styles": [
-      "output-styles/vesper.md",
-      "output-styles/spartan.md",
-      "output-styles/mentor.md",
-      "output-styles/executive.md",
-      "output-styles/pair.md",
-      "output-styles/atlas.md",
-      "output-styles/coach.md",
-      "output-styles/harbour.md",
-      "output-styles/meridian.md",
-      "output-styles/noir.md",
-      "output-styles/roast.md",
-      "output-styles/sage.md",
-      "output-styles/scout.md"
-    ]
-  },
-  "categories": [
-    "productivity",
-    "development",
-    "workflow"
   ]
 }

+ 30 - 0
.github/workflows/validate.yml

@@ -0,0 +1,30 @@
+name: Validate extensions
+
+on:
+  push:
+    branches: [main]
+  pull_request:
+
+jobs:
+  validate:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+
+      - name: Install jq
+        run: sudo apt-get update && sudo apt-get install -y jq
+
+      - name: Install Claude Code (for authoritative plugin validation)
+        run: npm install -g @anthropic-ai/claude-code
+
+      - name: Validate plugin + marketplace manifests
+        run: |
+          claude plugin validate .
+          tmp="$(mktemp -d)"
+          mkdir -p "$tmp/.claude-plugin"
+          cp .claude-plugin/plugin.json "$tmp/.claude-plugin/plugin.json"
+          claude plugin validate "$tmp"
+
+      - name: Validate extensions (frontmatter, naming, structure)
+        run: bash tests/validate.sh

+ 1 - 0
README.md

@@ -438,6 +438,7 @@ powershell validate.ps1
 - Required fields (name, description)
 - Naming conventions (kebab-case)
 - File structure (agents/*.md, skills/*/SKILL.md)
+- Plugin manifests (`.claude-plugin/plugin.json` + `marketplace.json`) via the authoritative `claude plugin validate`, plus a guard against a stray root `marketplace.json`
 
 ### Available Tasks
 

+ 0 - 14
marketplace.json

@@ -1,14 +0,0 @@
-{
-  "name": "claude-mods",
-  "description": "Custom commands, skills, agents, and rules for Claude Code",
-  "plugins": [
-    {
-      "name": "claude-mods",
-      "description": "22 expert agents, 38 skills, 3 commands, 5 rules, and output styles for Claude Code",
-      "source": {
-        "type": "git",
-        "url": "https://github.com/0xDarkMatter/claude-mods.git"
-      }
-    }
-  ]
-}

+ 8 - 0
tests/justfile

@@ -49,3 +49,11 @@ list-rules:
 validate-settings:
     @echo "Validating settings template..."
     @jq empty templates/settings.local.json && echo "Valid JSON" || echo "Invalid JSON"
+
+# Validate plugin + marketplace manifests (.claude-plugin/)
+# Uses the authoritative `claude plugin validate` when available.
+validate-plugin:
+    @echo "Validating plugin manifests..."
+    @test ! -f marketplace.json || (echo "ERROR: stray marketplace.json at repo root - move to .claude-plugin/" && exit 1)
+    @claude plugin validate .
+    @tmp="$(mktemp -d)"; mkdir -p "$tmp/.claude-plugin"; cp .claude-plugin/plugin.json "$tmp/.claude-plugin/plugin.json"; claude plugin validate "$tmp"; rm -rf "$tmp"

+ 80 - 0
tests/validate.ps1

@@ -236,6 +236,9 @@ function Test-Skills {
 
     $subdirs = Get-ChildItem -Path $skillsDir -Directory
     foreach ($subdir in $subdirs) {
+        # Skip shared helper dirs (e.g. _lib) - not skills, no SKILL.md expected.
+        if ($subdir.Name -like "_*") { continue }
+
         $skillFile = Join-Path $subdir.FullName "SKILL.md"
 
         if (-not (Test-Path $skillFile)) {
@@ -369,6 +372,82 @@ function Test-Settings {
     }
 }
 
+function Test-Plugin {
+    Write-Host ""
+    Write-Host "=== Validating Plugin Manifests ===" -ForegroundColor Cyan
+
+    # The authoritative validator is `claude plugin validate` (it tracks the
+    # live schema - it caught a bad plugin `source` shape and an `author` type
+    # error that a hand-rolled check sailed past). Prefer it when present; fall
+    # back to lightweight structural checks otherwise. The stray-root-file guard
+    # runs regardless - the official tool can't see a misplaced copy.
+    $pluginDir = Join-Path $ProjectDir ".claude-plugin"
+    $pluginFile = Join-Path $pluginDir "plugin.json"
+    $mktFile = Join-Path $pluginDir "marketplace.json"
+
+    # --- location guard ---
+    if (Test-Path (Join-Path $ProjectDir "marketplace.json")) {
+        Write-Fail "marketplace.json found at repo root - must live at .claude-plugin/marketplace.json"
+    }
+    if (-not (Test-Path $pluginFile)) { Write-Fail ".claude-plugin/plugin.json - Missing" }
+    if (-not (Test-Path $mktFile)) { Write-Fail ".claude-plugin/marketplace.json - Missing (required for /plugin marketplace add)" }
+
+    $claude = Get-Command claude -ErrorAction SilentlyContinue
+
+    # --- authoritative path ---
+    if ($claude) {
+        & claude plugin validate $ProjectDir 2>&1 | Out-Null
+        if ($LASTEXITCODE -eq 0) {
+            Write-Pass "marketplace.json - claude plugin validate passed"
+        } else {
+            Write-Fail "marketplace.json - claude plugin validate failed (run: claude plugin validate .)"
+        }
+
+        if (Test-Path $pluginFile) {
+            $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName())
+            New-Item -ItemType Directory -Path (Join-Path $tmp ".claude-plugin") -Force | Out-Null
+            Copy-Item $pluginFile (Join-Path $tmp ".claude-plugin\plugin.json")
+            & claude plugin validate $tmp 2>&1 | Out-Null
+            if ($LASTEXITCODE -eq 0) {
+                Write-Pass "plugin.json - claude plugin validate passed"
+            } else {
+                Write-Fail "plugin.json - claude plugin validate failed (unrecognized keys or wrong field types)"
+            }
+            Remove-Item -Recurse -Force $tmp
+        }
+        return
+    }
+
+    # --- fallback path: lightweight structural checks ---
+    Write-Warn "claude CLI not found - using lightweight manifest checks only (install Claude Code for authoritative validation)"
+
+    if (Test-Path $pluginFile) {
+        try {
+            $plugin = Get-Content -Path $pluginFile -Raw | ConvertFrom-Json
+            if ($plugin.name -is [string] -and $plugin.name) {
+                Write-Pass "$pluginFile - structurally OK (name present)"
+            } else {
+                Write-Fail "$pluginFile - Missing required field: name"
+            }
+        } catch {
+            Write-Fail "$pluginFile - Invalid JSON"
+        }
+    }
+
+    if (Test-Path $mktFile) {
+        try {
+            $mkt = Get-Content -Path $mktFile -Raw | ConvertFrom-Json
+            $ok = $true
+            if (-not ($mkt.name -is [string] -and $mkt.name)) { Write-Fail "$mktFile - Missing required field: name (string)"; $ok = $false }
+            if ($null -eq $mkt.owner -or -not ($mkt.owner.name -is [string] -and $mkt.owner.name)) { Write-Fail "$mktFile - owner.name missing (owner must be an object with a name)"; $ok = $false }
+            if ($mkt.plugins -isnot [array]) { Write-Fail "$mktFile - Missing required field: plugins (array)"; $ok = $false }
+            if ($ok) { Write-Pass "$mktFile - structurally OK (run claude plugin validate for full schema)" }
+        } catch {
+            Write-Fail "$mktFile - Invalid JSON"
+        }
+    }
+}
+
 # Main
 Write-Host "claude-mods Validation"
 Write-Host "======================"
@@ -379,6 +458,7 @@ Test-Commands
 Test-Skills
 Test-Rules
 Test-Settings
+Test-Plugin
 
 Write-Host ""
 Write-Host "======================"

+ 86 - 0
tests/validate.sh

@@ -235,6 +235,9 @@ validate_skills() {
     fi
 
     while IFS= read -r -d '' skill_subdir; do
+        # Skip shared helper dirs (e.g. _lib) - not skills, no SKILL.md expected.
+        [[ "$(basename "$skill_subdir")" == _* ]] && continue
+
         local skill_file="$skill_subdir/SKILL.md"
         if [[ ! -f "$skill_file" ]]; then
             log_fail "$skill_subdir - Missing SKILL.md"
@@ -371,6 +374,88 @@ validate_settings() {
     fi
 }
 
+# Validate plugin + marketplace manifests (.claude-plugin/)
+#
+# The authoritative validator is `claude plugin validate` (it tracks the live
+# schema - it caught a bad plugin `source` shape and an `author` type error
+# that a hand-rolled jq check sailed past). We prefer it when the CLI is
+# present and fall back to lightweight structural jq checks otherwise. The
+# stray-root-file guard runs regardless, because the official tool validates
+# whatever path it is given and cannot see a misplaced copy.
+validate_plugin() {
+    echo ""
+    echo "=== Validating Plugin Manifests ==="
+
+    local plugin_dir="$PROJECT_DIR/.claude-plugin"
+    local plugin_file="$plugin_dir/plugin.json"
+    local mkt_file="$plugin_dir/marketplace.json"
+
+    # --- location guard (official tool can't see this) ---
+    # The spec mandates .claude-plugin/marketplace.json. A copy at the repo
+    # root is the regression that caused /plugin marketplace add to fail (#4).
+    if [[ -f "$PROJECT_DIR/marketplace.json" ]]; then
+        log_fail "marketplace.json found at repo root - must live at .claude-plugin/marketplace.json"
+    fi
+
+    [[ -f "$plugin_file" ]] || log_fail ".claude-plugin/plugin.json - Missing"
+    [[ -f "$mkt_file" ]] || log_fail ".claude-plugin/marketplace.json - Missing (required for /plugin marketplace add)"
+
+    # --- authoritative path: claude plugin validate ---
+    if command -v claude >/dev/null 2>&1; then
+        # Marketplace manifest (repo root resolves to the marketplace).
+        if claude plugin validate "$PROJECT_DIR" >/dev/null 2>&1; then
+            log_pass "marketplace.json - claude plugin validate passed"
+        else
+            log_fail "marketplace.json - claude plugin validate failed (run: claude plugin validate .)"
+        fi
+
+        # Plugin manifest: validate in isolation so it is not shadowed by the
+        # marketplace manifest in the same .claude-plugin/ directory.
+        if [[ -f "$plugin_file" ]]; then
+            local tmp
+            tmp=$(mktemp -d)
+            mkdir -p "$tmp/.claude-plugin"
+            cp "$plugin_file" "$tmp/.claude-plugin/plugin.json"
+            if claude plugin validate "$tmp" >/dev/null 2>&1; then
+                log_pass "plugin.json - claude plugin validate passed"
+            else
+                log_fail "plugin.json - claude plugin validate failed (unrecognized keys or wrong field types)"
+            fi
+            rm -rf "$tmp"
+        fi
+        return
+    fi
+
+    # --- fallback path: lightweight structural checks (jq) ---
+    log_warn "claude CLI not found - using lightweight manifest checks only (install Claude Code for authoritative validation)"
+
+    if [[ -f "$plugin_file" ]]; then
+        if ! jq empty "$plugin_file" 2>/dev/null; then
+            log_fail "$plugin_file - Invalid JSON"
+        elif jq -e '.name | strings' "$plugin_file" >/dev/null 2>&1; then
+            log_pass "$plugin_file - structurally OK (name present)"
+        else
+            log_fail "$plugin_file - Missing required field: name"
+        fi
+    fi
+
+    if [[ -f "$mkt_file" ]]; then
+        if ! jq empty "$mkt_file" 2>/dev/null; then
+            log_fail "$mkt_file - Invalid JSON"
+        else
+            jq -e '.name | strings' "$mkt_file" >/dev/null 2>&1 \
+                || log_fail "$mkt_file - Missing required field: name (string)"
+            jq -e '.owner.name | strings' "$mkt_file" >/dev/null 2>&1 \
+                || log_fail "$mkt_file - owner.name missing (owner must be an object with a name)"
+            jq -e '.plugins | arrays' "$mkt_file" >/dev/null 2>&1 \
+                || log_fail "$mkt_file - Missing required field: plugins (array)"
+            if jq -e '.name and (.owner.name | strings) and (.plugins | arrays)' "$mkt_file" >/dev/null 2>&1; then
+                log_pass "$mkt_file - structurally OK (run claude plugin validate for full schema)"
+            fi
+        fi
+    fi
+}
+
 # Main
 main() {
     echo "claude-mods Validation"
@@ -382,6 +467,7 @@ main() {
     validate_skills
     validate_rules
     validate_settings
+    validate_plugin
 
     echo ""
     echo "======================"