From 3cc42ffb7849534690c7a7a2593e88841021bd72 Mon Sep 17 00:00:00 2001 From: Siddharth Date: Sat, 18 Apr 2026 15:37:24 +0530 Subject: [PATCH] fix: community-index file scoping, nested entry detection, and auto-child deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix detectEntryPoints to recursively match agents and commands in subdirectories (agents/**/*.md, commands/**/*.md) instead of flat only - Filter infrastructure files (.gitkeep, README.md, TEMPLATE.md, CLAUDE.md) from entry point detection - Add resolveFileOwnership() with three categories: A) Flat shared dirs (multiple entries same dirHint) → entry file only B) Dedicated dirs (skills with own subdir) → own files minus child subtrees C) Root-level → root dir files only - Add Pass C auto-child dependencies for parent skills containing child entry points (required: false, source: auto-child) - Use refs/heads/main in rawBaseUrl and plug-deps.json fetch URLs --- scripts/build-community-index.js | 122 ++++++++++++++++++++++++++----- 1 file changed, 102 insertions(+), 20 deletions(-) diff --git a/scripts/build-community-index.js b/scripts/build-community-index.js index 165faeb..3036fec 100644 --- a/scripts/build-community-index.js +++ b/scripts/build-community-index.js @@ -114,6 +114,8 @@ function buildDirMap(treeItems) { // Detect entry points // --------------------------------------------------------------------------- +const INFRA_FILES = new Set(['.gitkeep', '.gitignore', 'CLAUDE.md', 'README.md', 'TEMPLATE.md']); + function detectEntryPoints(treeItems) { const entries = []; for (const item of treeItems) { @@ -128,23 +130,98 @@ function detectEntryPoints(treeItems) { continue; } - // agents/*.md — direct child only - const agentMatch = p.match(/^agents\/([^/]+\.md)$/); + // agents/**/*.md — recursive, skip infra files + const agentMatch = p.match(/^agents\/(.+\.md)$/); if (agentMatch) { - entries.push({ path: p, type: 'agent', dirHint: 'agents', name: null, filename: agentMatch[1] }); + if (INFRA_FILES.has(path.posix.basename(p))) continue; + const dir = path.posix.dirname(p); + entries.push({ path: p, type: 'agent', dirHint: dir, name: null, filename: path.posix.basename(p) }); continue; } - // commands/*.md — direct child only - const commandMatch = p.match(/^commands\/([^/]+\.md)$/); + // commands/**/*.md — recursive, skip infra files + const commandMatch = p.match(/^commands\/(.+\.md)$/); if (commandMatch) { - entries.push({ path: p, type: 'command', dirHint: 'commands', name: null, filename: commandMatch[1] }); + if (INFRA_FILES.has(path.posix.basename(p))) continue; + const dir = path.posix.dirname(p); + entries.push({ path: p, type: 'command', dirHint: dir, name: null, filename: path.posix.basename(p) }); continue; } } return entries; } +// --------------------------------------------------------------------------- +// Resolve file ownership per entry point +// --------------------------------------------------------------------------- + +function resolveFileOwnership(entryPoints, dirMap) { + const dirHintCounts = {}; + for (const ep of entryPoints) { + const d = ep.dirHint === '.' ? '' : ep.dirHint; + dirHintCounts[d] = (dirHintCounts[d] || 0) + 1; + } + + const entryPointDirSet = new Set( + entryPoints.map(ep => ep.dirHint === '.' ? '' : ep.dirHint) + ); + + const result = new Map(); + + for (const ep of entryPoints) { + const directory = ep.dirHint === '.' ? '' : ep.dirHint; + + // Category A: flat shared directory — entry file only + if (dirHintCounts[directory] > 1) { + result.set(ep.path, { files: [ep.path], childEntryPoints: [] }); + continue; + } + + // Category C: root-level + if (directory === '') { + const rootFiles = dirMap['.'] || []; + result.set(ep.path, { files: rootFiles.length > 0 ? rootFiles : [ep.path], childEntryPoints: [] }); + continue; + } + + // Category B: dedicated directory — exclude child entry point subtrees + const prefix = directory + '/'; + const childEpDirs = []; + for (const d of entryPointDirSet) { + if (d !== directory && d.startsWith(prefix)) { + childEpDirs.push(d); + } + } + + const filesInDir = []; + for (const [dir, files] of Object.entries(dirMap)) { + if (dir !== directory && !dir.startsWith(prefix)) continue; + const ownedByChild = childEpDirs.some(childDir => + dir === childDir || dir.startsWith(childDir + '/') + ); + if (!ownedByChild) { + filesInDir.push(...files); + } + } + + const childNames = entryPoints + .filter(child => { + if (child === ep) return false; + const childDir = child.dirHint === '.' ? '' : child.dirHint; + return childDir.startsWith(prefix); + }) + .map(child => child.name || (child.filename ? child.filename.replace(/\.md$/, '') : null)) + .filter(Boolean); + + result.set(ep.path, { + files: filesInDir.length > 0 ? filesInDir : [ep.path], + childEntryPoints: childNames, + }); + } + + return result; +} + // --------------------------------------------------------------------------- // Process a single vault // --------------------------------------------------------------------------- @@ -153,7 +230,7 @@ async function processVault(vault, plugDeps, depPattern) { const urlParts = new URL(vault.url); const [, owner, repoName] = urlParts.pathname.split('/'); const vaultName = vault.name; - const rawBase = `https://raw.githubusercontent.com/${owner}/${repoName}/main/`; + const rawBase = `https://raw.githubusercontent.com/${owner}/${repoName}/refs/heads/main/`; const apiTreeUrl = `https://api.github.com/repos/${owner}/${repoName}/git/trees/main?recursive=1`; process.stderr.write(`[${vaultName}] Fetching git tree...\n`); @@ -166,6 +243,7 @@ async function processVault(vault, plugDeps, depPattern) { const treeItems = treeData.tree; const dirMap = buildDirMap(treeItems); const entryPoints = detectEntryPoints(treeItems); + const ownershipMap = resolveFileOwnership(entryPoints, dirMap); process.stderr.write(`[${vaultName}] Found ${entryPoints.length} entry points.\n`); @@ -194,18 +272,8 @@ async function processVault(vault, plugDeps, depPattern) { } const directory = ep.dirHint === '.' ? '' : ep.dirHint; - let filesInDir = []; - if (directory === '') { - filesInDir = Object.values(dirMap).flat(); - } else { - const prefix = directory + '/'; - for (const [dir, files] of Object.entries(dirMap)) { - if (dir === directory || dir.startsWith(prefix)) { - filesInDir.push(...files); - } - } - } - if (filesInDir.length === 0) filesInDir = [ep.path]; + const ownership = ownershipMap.get(ep.path) || { files: [ep.path], childEntryPoints: [] }; + const filesInDir = ownership.files; // Dependencies — Pass A (curated) const curatedRaw = plugDeps[entryName] || []; @@ -227,6 +295,20 @@ async function processVault(vault, plugDeps, depPattern) { } } + // Dependencies — Pass C (auto-child entry points) + for (const childName of ownership.childEntryPoints) { + if (!curatedNames.has(childName)) { + dependencies.push({ + name: childName, + type: 'skill', + vault: vaultName, + required: false, + source: 'auto-child', + }); + curatedNames.add(childName); + } + } + results.push({ name: entryName, type: ep.type, @@ -267,7 +349,7 @@ async function main() { // Fetch plug-deps.json for this vault (graceful 404) const urlParts = new URL(vault.url); const [, owner, repoName] = urlParts.pathname.split('/'); - const depsUrl = `https://raw.githubusercontent.com/${owner}/${repoName}/main/plug-deps.json`; + const depsUrl = `https://raw.githubusercontent.com/${owner}/${repoName}/refs/heads/main/plug-deps.json`; process.stderr.write(`[${vault.name}] Fetching plug-deps.json...\n`); const plugDeps = (await fetchJSON(depsUrl)) || {};