mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-09 10:53:34 +08:00
fix: harden claude plugin manifest surfaces
This commit is contained in:
@@ -1,7 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
|
|
||||||
"name": "ecc",
|
"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": {
|
"owner": {
|
||||||
"name": "Affaan Mustafa",
|
"name": "Affaan Mustafa",
|
||||||
"email": "me@affaanmustafa.com"
|
"email": "me@affaanmustafa.com"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
description: Quick commit with natural language file targeting — describe what to commit in plain English
|
description: "Quick commit with natural language file targeting — describe what to commit in plain English"
|
||||||
argument-hint: [target description] (blank = all changes)
|
argument-hint: "[target description] (blank = all changes)"
|
||||||
---
|
---
|
||||||
|
|
||||||
# Smart Commit
|
# Smart Commit
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
description: Create a GitHub PR from current branch with unpushed commits — discovers templates, analyzes changes, pushes
|
description: "Create a GitHub PR from current branch with unpushed commits — discovers templates, analyzes changes, pushes"
|
||||||
argument-hint: [base-branch] (default: main)
|
argument-hint: "[base-branch] (default: main)"
|
||||||
---
|
---
|
||||||
|
|
||||||
# Create Pull Request
|
# Create Pull Request
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
description: Interactive PRD generator - problem-first, hypothesis-driven product spec with back-and-forth questioning
|
description: "Interactive PRD generator - problem-first, hypothesis-driven product spec with back-and-forth questioning"
|
||||||
argument-hint: [feature/product idea] (blank = start with questions)
|
argument-hint: "[feature/product idea] (blank = start with questions)"
|
||||||
---
|
---
|
||||||
|
|
||||||
# Product Requirements Document Generator
|
# Product Requirements Document Generator
|
||||||
|
|||||||
@@ -12,6 +12,53 @@ const COMMANDS_DIR = path.join(ROOT_DIR, 'commands');
|
|||||||
const AGENTS_DIR = path.join(ROOT_DIR, 'agents');
|
const AGENTS_DIR = path.join(ROOT_DIR, 'agents');
|
||||||
const SKILLS_DIR = path.join(ROOT_DIR, 'skills');
|
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() {
|
function validateCommands() {
|
||||||
if (!fs.existsSync(COMMANDS_DIR)) {
|
if (!fs.existsSync(COMMANDS_DIR)) {
|
||||||
console.log('No commands directory found, skipping validation');
|
console.log('No commands directory found, skipping validation');
|
||||||
@@ -68,6 +115,11 @@ function validateCommands() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const error of validateFrontmatter(file, content)) {
|
||||||
|
console.error(`ERROR: ${error}`);
|
||||||
|
hasErrors = true;
|
||||||
|
}
|
||||||
|
|
||||||
// Strip fenced code blocks before checking cross-references.
|
// Strip fenced code blocks before checking cross-references.
|
||||||
// Examples/templates inside ``` blocks are not real references.
|
// Examples/templates inside ``` blocks are not real references.
|
||||||
const contentNoCodeBlocks = content.replace(/```[\s\S]*?```/g, '');
|
const contentNoCodeBlocks = content.replace(/```[\s\S]*?```/g, '');
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ function assertSafeRepoRelativePath(relativePath, label) {
|
|||||||
console.log('\n=== .claude-plugin/plugin.json ===\n');
|
console.log('\n=== .claude-plugin/plugin.json ===\n');
|
||||||
|
|
||||||
const claudePluginPath = path.join(repoRoot, '.claude-plugin', 'plugin.json');
|
const claudePluginPath = path.join(repoRoot, '.claude-plugin', 'plugin.json');
|
||||||
|
const claudeMarketplacePath = path.join(repoRoot, '.claude-plugin', 'marketplace.json');
|
||||||
|
|
||||||
test('claude plugin.json exists', () => {
|
test('claude plugin.json exists', () => {
|
||||||
assert.ok(fs.existsSync(claudePluginPath), 'Expected .claude-plugin/plugin.json to exist');
|
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 ─────────────────────────────────────────────────────
|
// ── Codex plugin manifest ─────────────────────────────────────────────────────
|
||||||
// Per official docs: https://platform.openai.com/docs/codex/plugins
|
// Per official docs: https://platform.openai.com/docs/codex/plugins
|
||||||
// - .codex-plugin/plugin.json is the required manifest
|
// - .codex-plugin/plugin.json is the required manifest
|
||||||
|
|||||||
Reference in New Issue
Block a user