fix: harden claude plugin manifest surfaces

This commit is contained in:
Affaan Mustafa
2026-04-08 16:27:30 -07:00
parent adfe8a8311
commit 2e5e94cb7f
6 changed files with 83 additions and 8 deletions

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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, '');

View File

@@ -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