diff --git a/scripts/dist/discover-components.cjs b/scripts/dist/discover-components.cjs index 9c09264..6b2ac09 100755 --- a/scripts/dist/discover-components.cjs +++ b/scripts/dist/discover-components.cjs @@ -7646,7 +7646,7 @@ function validateComponent(componentPath, config, options2) { errors.push(`Invalid frontmatter in ${filePath}: ${err.message}`); return { valid: false, errors }; } - const { name, description } = parsed.data; + const { name, description, author, category } = parsed.data; if (!name) { errors.push(`Missing required field 'name' in ${filePath}`); } @@ -7683,7 +7683,9 @@ function validateComponent(componentPath, config, options2) { valid: errors.length === 0, errors, name, - description + description, + author, + category }; } function validateSkill(skillPath, config) { @@ -7926,62 +7928,51 @@ function discoverPlugins(rootDir, config) { const { plugins } = groupIntoPlugins(components, rootDir, config); return plugins; } -function generatePluginJson(plugin, config) { +function extractPluginMetadata(plugin, config) { const { components } = plugin; - const { owner } = config.marketplace; let pluginName = plugin.name; let pluginDescription = `${plugin.name} plugin`; - if (components.agents && components.agents.length > 0) { - const validation = validateAgent(components.agents[0], config); - if (validation.valid && validation.description) { - pluginDescription = validation.description; - } - } else if (components.commands && components.commands.length > 0) { - const validation = validateCommand(components.commands[0], config); - if (validation.valid && validation.description) { - pluginDescription = validation.description; - } - } else if (components.skills && components.skills.length > 0) { - const validation = validateSkill(components.skills[0], config); - if (validation.valid && validation.description) { - pluginDescription = validation.description; + let pluginAuthor; + let pluginCategory; + const componentSources = [ + ...(components.agents || []).map((p) => validateAgent(p, config)), + ...(components.commands || []).map((p) => validateCommand(p, config)), + ...(components.skills || []).map((p) => validateSkill(p, config)) + ]; + for (const validation of componentSources) { + if (!validation.valid) + continue; + if (!pluginDescription || pluginDescription === `${plugin.name} plugin`) { + if (validation.description) + pluginDescription = validation.description; } + if (!pluginAuthor && validation.author) + pluginAuthor = validation.author; + if (!pluginCategory && validation.category) + pluginCategory = validation.category; } + return { name: pluginName, description: pluginDescription, author: pluginAuthor, category: pluginCategory }; +} +function generatePluginJson(plugin, config) { + const { owner } = config.marketplace; + const metadata = extractPluginMetadata(plugin, config); return { - name: pluginName, - description: pluginDescription, - author: owner + name: metadata.name, + description: metadata.description, + author: metadata.author || owner }; } function generateMarketplace(plugins, config) { const { name, owner, description } = config.marketplace; const marketplacePlugins = []; for (const plugin of plugins) { - const { components } = plugin; - let pluginName = plugin.name; - let pluginDescription = `${plugin.name} plugin`; - if (components.agents && components.agents.length > 0) { - const validation = validateAgent(components.agents[0], config); - if (validation.valid && validation.description) { - pluginDescription = validation.description; - } - } else if (components.commands && components.commands.length > 0) { - const validation = validateCommand(components.commands[0], config); - if (validation.valid && validation.description) { - pluginDescription = validation.description; - } - } else if (components.skills && components.skills.length > 0) { - const validation = validateSkill(components.skills[0], config); - if (validation.valid && validation.description) { - pluginDescription = validation.description; - } - } + const metadata = extractPluginMetadata(plugin, config); marketplacePlugins.push({ - name: pluginName, - description: pluginDescription, + name: metadata.name, + description: metadata.description, source: plugin.source, - author: owner, - category: plugin.category === "code" || plugin.category === "analysis" ? "development" : "productivity" + author: metadata.author || owner, + category: metadata.category || (plugin.category === "code" || plugin.category === "analysis" ? "development" : "productivity") }); } const marketplacePath = path.join(".claude-plugin", "marketplace.json"); @@ -8043,6 +8034,7 @@ module.exports = { getCategoryNames, groupIntoPlugins, discoverPlugins, + extractPluginMetadata, generatePluginJson, generateMarketplace, writePluginJsonFiles diff --git a/scripts/src/discover-components.js b/scripts/src/discover-components.js index eed9f21..33ed931 100644 --- a/scripts/src/discover-components.js +++ b/scripts/src/discover-components.js @@ -604,7 +604,7 @@ function validateComponent(componentPath, config, options) { return { valid: false, errors }; } - const { name, description } = parsed.data; + const { name, description, author, category } = parsed.data; // Validate required fields if (!name) { @@ -650,7 +650,9 @@ function validateComponent(componentPath, config, options) { valid: errors.length === 0, errors, name, - description + description, + author, + category }; } @@ -1213,41 +1215,52 @@ function discoverPlugins(rootDir, config) { } /** - * Generates individual plugin.json for a plugin directory. - * @param {Object} plugin - Plugin metadata + * Extracts name, description, author, and category from a plugin's components. + * Validates each component type and takes metadata from the first valid one found. + * @param {Object} plugin - Plugin metadata with components * @param {Object} config - Configuration object - * @returns {Object} Plugin manifest object + * @returns {{ name: string, description: string, author?: Object, category?: string }} */ -function generatePluginJson(plugin, config) { +function extractPluginMetadata(plugin, config) { const { components } = plugin; - const { owner } = config.marketplace; - - // Get metadata from the first valid component let pluginName = plugin.name; let pluginDescription = `${plugin.name} plugin`; + let pluginAuthor; + let pluginCategory; + + // Validate all component types, take metadata from first valid one + const componentSources = [ + ...(components.agents || []).map(p => validateAgent(p, config)), + ...(components.commands || []).map(p => validateCommand(p, config)), + ...(components.skills || []).map(p => validateSkill(p, config)) + ]; - // Try to extract description from components - if (components.agents && components.agents.length > 0) { - const validation = validateAgent(components.agents[0], config); - if (validation.valid && validation.description) { - pluginDescription = validation.description; - } - } else if (components.commands && components.commands.length > 0) { - const validation = validateCommand(components.commands[0], config); - if (validation.valid && validation.description) { - pluginDescription = validation.description; - } - } else if (components.skills && components.skills.length > 0) { - const validation = validateSkill(components.skills[0], config); - if (validation.valid && validation.description) { - pluginDescription = validation.description; + for (const validation of componentSources) { + if (!validation.valid) continue; + if (!pluginDescription || pluginDescription === `${plugin.name} plugin`) { + if (validation.description) pluginDescription = validation.description; } + if (!pluginAuthor && validation.author) pluginAuthor = validation.author; + if (!pluginCategory && validation.category) pluginCategory = validation.category; } + return { name: pluginName, description: pluginDescription, author: pluginAuthor, category: pluginCategory }; +} + +/** + * Generates individual plugin.json for a plugin directory. + * @param {Object} plugin - Plugin metadata + * @param {Object} config - Configuration object + * @returns {Object} Plugin manifest object + */ +function generatePluginJson(plugin, config) { + const { owner } = config.marketplace; + const metadata = extractPluginMetadata(plugin, config); + return { - name: pluginName, - description: pluginDescription, - author: owner + name: metadata.name, + description: metadata.description, + author: metadata.author || owner }; } @@ -1263,36 +1276,14 @@ function generateMarketplace(plugins, config) { const marketplacePlugins = []; for (const plugin of plugins) { - const { components } = plugin; - - // Get metadata from the first valid component - let pluginName = plugin.name; - let pluginDescription = `${plugin.name} plugin`; - - // Try to extract description from components - if (components.agents && components.agents.length > 0) { - const validation = validateAgent(components.agents[0], config); - if (validation.valid && validation.description) { - pluginDescription = validation.description; - } - } else if (components.commands && components.commands.length > 0) { - const validation = validateCommand(components.commands[0], config); - if (validation.valid && validation.description) { - pluginDescription = validation.description; - } - } else if (components.skills && components.skills.length > 0) { - const validation = validateSkill(components.skills[0], config); - if (validation.valid && validation.description) { - pluginDescription = validation.description; - } - } + const metadata = extractPluginMetadata(plugin, config); marketplacePlugins.push({ - name: pluginName, - description: pluginDescription, + name: metadata.name, + description: metadata.description, source: plugin.source, - author: owner, - category: plugin.category === 'code' || plugin.category === 'analysis' ? 'development' : 'productivity' + author: metadata.author || owner, + category: metadata.category || (plugin.category === 'code' || plugin.category === 'analysis' ? 'development' : 'productivity') }); } @@ -1374,6 +1365,7 @@ module.exports = { getCategoryNames, groupIntoPlugins, discoverPlugins, + extractPluginMetadata, generatePluginJson, generateMarketplace, writePluginJsonFiles diff --git a/scripts/test/discover-components.test.js b/scripts/test/discover-components.test.js index 6230b74..252189d 100644 --- a/scripts/test/discover-components.test.js +++ b/scripts/test/discover-components.test.js @@ -18,6 +18,10 @@ const { groupIntoPlugins, discoverPlugins, validateSkill, + validateComponent, + extractPluginMetadata, + generatePluginJson, + generateMarketplace, mergeHooks } = require('../src/discover-components.js'); @@ -446,6 +450,126 @@ test('discoverMarkdownComponents returns empty skipped array when all files have }); }); +// --- per-plugin author and category --- + +console.log('\nper-plugin author and category'); + +const validationConfig = { + discovery: { + skillFilename: 'SKILL.md', + commandsDir: 'commands', + agentsDir: 'agents', + excludeDirs: ['.git', 'node_modules'], + excludePatterns: [], + maxDepth: 10 + }, + validation: { + nameMaxLength: 64, + descriptionMaxLength: 1024, + reservedWords: [], + namePattern: '^[a-z0-9]+(-[a-z0-9]+)*$' + } +}; + +test('validateComponent returns author and category from frontmatter', () => { + withTempDir((tmpDir) => { + fs.writeFileSync(path.join(tmpDir, 'SKILL.md'), + '---\nname: test-skill\ndescription: A test skill\nauthor:\n name: Jane Doe\n email: jane@example.com\ncategory: design\n---\nContent\n'); + const result = validateComponent(path.join(tmpDir, 'SKILL.md'), validationConfig, { type: 'skill' }); + assert.strictEqual(result.valid, true); + assert.deepStrictEqual(result.author, { name: 'Jane Doe', email: 'jane@example.com' }); + assert.strictEqual(result.category, 'design'); + }); +}); + +test('validateComponent returns undefined author/category when not in frontmatter', () => { + withTempDir((tmpDir) => { + fs.writeFileSync(path.join(tmpDir, 'SKILL.md'), + '---\nname: test-skill\ndescription: A test skill\n---\nContent\n'); + const result = validateComponent(path.join(tmpDir, 'SKILL.md'), validationConfig, { type: 'skill' }); + assert.strictEqual(result.valid, true); + assert.strictEqual(result.author, undefined); + assert.strictEqual(result.category, undefined); + }); +}); + +test('extractPluginMetadata uses component author/category over defaults', () => { + withTempDir((tmpDir) => { + const skillDir = path.join(tmpDir, 'my-skill'); + fs.mkdirSync(skillDir); + fs.writeFileSync(path.join(skillDir, 'SKILL.md'), + '---\nname: my-skill\ndescription: A skill\nauthor:\n name: Jane Doe\n email: jane@example.com\ncategory: design\n---\nContent\n'); + + const plugin = { + name: 'my-skill', + category: 'code', + path: skillDir, + source: './code/my-skill', + components: { skills: [skillDir], commands: [], agents: [] } + }; + const metadata = extractPluginMetadata(plugin, validationConfig); + assert.deepStrictEqual(metadata.author, { name: 'Jane Doe', email: 'jane@example.com' }); + assert.strictEqual(metadata.category, 'design'); + }); +}); + +test('generateMarketplace uses frontmatter author/category when present', () => { + withTempDir((tmpDir) => { + const skillDir = path.join(tmpDir, 'my-skill'); + fs.mkdirSync(skillDir); + fs.writeFileSync(path.join(skillDir, 'SKILL.md'), + '---\nname: my-skill\ndescription: A skill\nauthor:\n name: Jane Doe\n email: jane@example.com\ncategory: design\n---\nContent\n'); + + const plugins = [{ + name: 'my-skill', + category: 'code', + path: skillDir, + source: './code/my-skill', + components: { skills: [skillDir], commands: [], agents: [] } + }]; + const config = { + ...validationConfig, + marketplace: { + name: 'test-marketplace', + description: 'Test', + owner: { name: 'Default', email: 'default@example.com' } + } + }; + const result = generateMarketplace(plugins, config); + assert.strictEqual(result.plugins.length, 1); + assert.deepStrictEqual(result.plugins[0].author, { name: 'Jane Doe', email: 'jane@example.com' }); + assert.strictEqual(result.plugins[0].category, 'design'); + }); +}); + +test('generateMarketplace falls back to owner when no frontmatter author', () => { + withTempDir((tmpDir) => { + const skillDir = path.join(tmpDir, 'my-skill'); + fs.mkdirSync(skillDir); + fs.writeFileSync(path.join(skillDir, 'SKILL.md'), + '---\nname: my-skill\ndescription: A skill\n---\nContent\n'); + + const plugins = [{ + name: 'my-skill', + category: 'code', + path: skillDir, + source: './code/my-skill', + components: { skills: [skillDir], commands: [], agents: [] } + }]; + const config = { + ...validationConfig, + marketplace: { + name: 'test-marketplace', + description: 'Test', + owner: { name: 'Default', email: 'default@example.com' } + } + }; + const result = generateMarketplace(plugins, config); + assert.deepStrictEqual(result.plugins[0].author, { name: 'Default', email: 'default@example.com' }); + assert.strictEqual(result.plugins[0].category, 'development'); + }); +}); + // --- Summary --- console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed`);