From 2e5e94cb7fbd85ebfaae010126e469bc0abf3495 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Wed, 8 Apr 2026 16:27:30 -0700 Subject: [PATCH] fix: harden claude plugin manifest surfaces --- .claude-plugin/marketplace.json | 2 -- commands/prp-commit.md | 4 +-- commands/prp-pr.md | 4 +-- commands/prp-prd.md | 4 +-- scripts/ci/validate-commands.js | 52 +++++++++++++++++++++++++++++++++ tests/plugin-manifest.test.js | 25 ++++++++++++++++ 6 files changed, 83 insertions(+), 8 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bdae85c8..8250c266 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,7 +1,5 @@ { - "$schema": "https://anthropic.com/claude-code/marketplace.schema.json", "name": "ecc", - "description": "Battle-tested Claude Code configurations from an Anthropic hackathon winner — agents, skills, hooks, rules, and legacy command shims evolved over 10+ months of intensive daily use", "owner": { "name": "Affaan Mustafa", "email": "me@affaanmustafa.com" diff --git a/commands/prp-commit.md b/commands/prp-commit.md index 5a9c0763..85935b8c 100644 --- a/commands/prp-commit.md +++ b/commands/prp-commit.md @@ -1,6 +1,6 @@ --- -description: Quick commit with natural language file targeting — describe what to commit in plain English -argument-hint: [target description] (blank = all changes) +description: "Quick commit with natural language file targeting — describe what to commit in plain English" +argument-hint: "[target description] (blank = all changes)" --- # Smart Commit diff --git a/commands/prp-pr.md b/commands/prp-pr.md index 6551e2e2..9469cb88 100644 --- a/commands/prp-pr.md +++ b/commands/prp-pr.md @@ -1,6 +1,6 @@ --- -description: Create a GitHub PR from current branch with unpushed commits — discovers templates, analyzes changes, pushes -argument-hint: [base-branch] (default: main) +description: "Create a GitHub PR from current branch with unpushed commits — discovers templates, analyzes changes, pushes" +argument-hint: "[base-branch] (default: main)" --- # Create Pull Request diff --git a/commands/prp-prd.md b/commands/prp-prd.md index 969fdc3a..5292c38c 100644 --- a/commands/prp-prd.md +++ b/commands/prp-prd.md @@ -1,6 +1,6 @@ --- -description: Interactive PRD generator - problem-first, hypothesis-driven product spec with back-and-forth questioning -argument-hint: [feature/product idea] (blank = start with questions) +description: "Interactive PRD generator - problem-first, hypothesis-driven product spec with back-and-forth questioning" +argument-hint: "[feature/product idea] (blank = start with questions)" --- # Product Requirements Document Generator diff --git a/scripts/ci/validate-commands.js b/scripts/ci/validate-commands.js index 1ca5ae49..ffe09e90 100644 --- a/scripts/ci/validate-commands.js +++ b/scripts/ci/validate-commands.js @@ -12,6 +12,53 @@ const COMMANDS_DIR = path.join(ROOT_DIR, 'commands'); const AGENTS_DIR = path.join(ROOT_DIR, 'agents'); const SKILLS_DIR = path.join(ROOT_DIR, 'skills'); +function validateFrontmatter(file, content) { + if (!content.startsWith('---\n')) { + return []; + } + + const endIndex = content.indexOf('\n---\n', 4); + if (endIndex === -1) { + return [`${file} - frontmatter block is missing a closing --- delimiter`]; + } + + const block = content.slice(4, endIndex); + const errors = []; + + for (const rawLine of block.split('\n')) { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) { + continue; + } + + const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/); + if (!match) { + errors.push(`${file} - invalid frontmatter line: ${rawLine}`); + continue; + } + + const value = match[2].trim(); + const isQuoted = ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ); + + if (!isQuoted && value.startsWith('[') && !value.endsWith(']')) { + errors.push( + `${file} - frontmatter value for "${match[1]}" starts with "[" but is not a closed YAML sequence; wrap it in quotes`, + ); + } + + if (!isQuoted && value.startsWith('{') && !value.endsWith('}')) { + errors.push( + `${file} - frontmatter value for "${match[1]}" starts with "{" but is not a closed YAML mapping; wrap it in quotes`, + ); + } + } + + return errors; +} + function validateCommands() { if (!fs.existsSync(COMMANDS_DIR)) { console.log('No commands directory found, skipping validation'); @@ -68,6 +115,11 @@ function validateCommands() { continue; } + for (const error of validateFrontmatter(file, content)) { + console.error(`ERROR: ${error}`); + hasErrors = true; + } + // Strip fenced code blocks before checking cross-references. // Examples/templates inside ``` blocks are not real references. const contentNoCodeBlocks = content.replace(/```[\s\S]*?```/g, ''); diff --git a/tests/plugin-manifest.test.js b/tests/plugin-manifest.test.js index 20ad64bb..0fa99946 100644 --- a/tests/plugin-manifest.test.js +++ b/tests/plugin-manifest.test.js @@ -68,6 +68,7 @@ function assertSafeRepoRelativePath(relativePath, label) { console.log('\n=== .claude-plugin/plugin.json ===\n'); const claudePluginPath = path.join(repoRoot, '.claude-plugin', 'plugin.json'); +const claudeMarketplacePath = path.join(repoRoot, '.claude-plugin', 'marketplace.json'); test('claude plugin.json exists', () => { assert.ok(fs.existsSync(claudePluginPath), 'Expected .claude-plugin/plugin.json to exist'); @@ -131,6 +132,30 @@ test('claude plugin.json does NOT have explicit hooks declaration', () => { ); }); +console.log('\n=== .claude-plugin/marketplace.json ===\n'); + +test('claude marketplace.json exists', () => { + assert.ok(fs.existsSync(claudeMarketplacePath), 'Expected .claude-plugin/marketplace.json to exist'); +}); + +const claudeMarketplace = loadJsonObject(claudeMarketplacePath, '.claude-plugin/marketplace.json'); + +test('claude marketplace.json keeps only Claude-supported top-level keys', () => { + const unsupportedTopLevelKeys = ['$schema', 'description']; + for (const key of unsupportedTopLevelKeys) { + assert.ok( + !(key in claudeMarketplace), + `.claude-plugin/marketplace.json must not declare unsupported top-level key "${key}"`, + ); + } +}); + +test('claude marketplace.json has plugins array with a short ecc plugin entry', () => { + assert.ok(Array.isArray(claudeMarketplace.plugins) && claudeMarketplace.plugins.length > 0, 'Expected plugins array'); + assert.strictEqual(claudeMarketplace.name, 'ecc'); + assert.strictEqual(claudeMarketplace.plugins[0].name, 'ecc'); +}); + // ── Codex plugin manifest ───────────────────────────────────────────────────── // Per official docs: https://platform.openai.com/docs/codex/plugins // - .codex-plugin/plugin.json is the required manifest