mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 02:15:12 +00:00
Skills rendering in collections
This commit is contained in:
@@ -1,30 +1,28 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path, { dirname } from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { dirname } from "path";
|
|
||||||
import {
|
import {
|
||||||
parseCollectionYaml,
|
AGENTS_DIR,
|
||||||
extractMcpServers,
|
AKA_INSTALL_URLS,
|
||||||
|
COLLECTIONS_DIR,
|
||||||
|
DOCS_DIR,
|
||||||
|
INSTRUCTIONS_DIR,
|
||||||
|
PROMPTS_DIR,
|
||||||
|
repoBaseUrl,
|
||||||
|
ROOT_FOLDER,
|
||||||
|
SKILLS_DIR,
|
||||||
|
TEMPLATES,
|
||||||
|
vscodeInsidersInstallImage,
|
||||||
|
vscodeInstallImage,
|
||||||
|
} from "./constants.mjs";
|
||||||
|
import {
|
||||||
extractMcpServerConfigs,
|
extractMcpServerConfigs,
|
||||||
|
parseCollectionYaml,
|
||||||
parseFrontmatter,
|
parseFrontmatter,
|
||||||
parseSkillMetadata,
|
parseSkillMetadata,
|
||||||
} from "./yaml-parser.mjs";
|
} from "./yaml-parser.mjs";
|
||||||
import {
|
|
||||||
TEMPLATES,
|
|
||||||
AKA_INSTALL_URLS,
|
|
||||||
repoBaseUrl,
|
|
||||||
vscodeInstallImage,
|
|
||||||
vscodeInsidersInstallImage,
|
|
||||||
ROOT_FOLDER,
|
|
||||||
PROMPTS_DIR,
|
|
||||||
AGENTS_DIR,
|
|
||||||
SKILLS_DIR,
|
|
||||||
COLLECTIONS_DIR,
|
|
||||||
INSTRUCTIONS_DIR,
|
|
||||||
DOCS_DIR,
|
|
||||||
} from "./constants.mjs";
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -55,14 +53,16 @@ async function loadMcpRegistryNames() {
|
|||||||
if (MCP_REGISTRY_SET) return MCP_REGISTRY_SET;
|
if (MCP_REGISTRY_SET) return MCP_REGISTRY_SET;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('Fetching MCP registry from API...');
|
console.log("Fetching MCP registry from API...");
|
||||||
const allServers = [];
|
const allServers = [];
|
||||||
let cursor = null;
|
let cursor = null;
|
||||||
const apiUrl = 'https://api.mcp.github.com/v0.1/servers/';
|
const apiUrl = "https://api.mcp.github.com/v0.1/servers/";
|
||||||
|
|
||||||
// Fetch all pages using cursor-based pagination
|
// Fetch all pages using cursor-based pagination
|
||||||
do {
|
do {
|
||||||
const url = cursor ? `${apiUrl}?cursor=${encodeURIComponent(cursor)}` : apiUrl;
|
const url = cursor
|
||||||
|
? `${apiUrl}?cursor=${encodeURIComponent(cursor)}`
|
||||||
|
: apiUrl;
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -78,8 +78,9 @@ async function loadMcpRegistryNames() {
|
|||||||
if (serverName) {
|
if (serverName) {
|
||||||
// Try to get displayName from GitHub metadata, fall back to server name
|
// Try to get displayName from GitHub metadata, fall back to server name
|
||||||
const displayName =
|
const displayName =
|
||||||
entry?.server?._meta?.["io.modelcontextprotocol.registry/publisher-provided"]?.github?.displayName ||
|
entry?.server?._meta?.[
|
||||||
serverName;
|
"io.modelcontextprotocol.registry/publisher-provided"
|
||||||
|
]?.github?.displayName || serverName;
|
||||||
|
|
||||||
allServers.push({
|
allServers.push({
|
||||||
name: serverName,
|
name: serverName,
|
||||||
@@ -431,28 +432,31 @@ function generateMcpServerLinks(servers, registryNames) {
|
|||||||
|
|
||||||
// Match against both displayName and full name (case-insensitive)
|
// Match against both displayName and full name (case-insensitive)
|
||||||
const serverNameLower = serverName.toLowerCase();
|
const serverNameLower = serverName.toLowerCase();
|
||||||
const registryEntry = registryNames.find(
|
const registryEntry = registryNames.find((entry) => {
|
||||||
(entry) => {
|
// Exact match on displayName or fullName
|
||||||
// Exact match on displayName or fullName
|
if (
|
||||||
if (entry.displayName === serverNameLower || entry.fullName === serverNameLower) {
|
entry.displayName === serverNameLower ||
|
||||||
|
entry.fullName === serverNameLower
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the serverName matches a part of the full name after a slash
|
||||||
|
// e.g., "apify" matches "com.apify/apify-mcp-server"
|
||||||
|
const nameParts = entry.fullName.split("/");
|
||||||
|
if (nameParts.length > 1 && nameParts[1]) {
|
||||||
|
// Check if it matches the second part (after the slash)
|
||||||
|
const secondPart = nameParts[1]
|
||||||
|
.replace("-mcp-server", "")
|
||||||
|
.replace("-mcp", "");
|
||||||
|
if (secondPart === serverNameLower) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the serverName matches a part of the full name after a slash
|
|
||||||
// e.g., "apify" matches "com.apify/apify-mcp-server"
|
|
||||||
const nameParts = entry.fullName.split('/');
|
|
||||||
if (nameParts.length > 1 && nameParts[1]) {
|
|
||||||
// Check if it matches the second part (after the slash)
|
|
||||||
const secondPart = nameParts[1].replace('-mcp-server', '').replace('-mcp', '');
|
|
||||||
if (secondPart === serverNameLower) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if serverName matches the displayName ignoring case
|
|
||||||
return entry.displayName === serverNameLower;
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
// Check if serverName matches the displayName ignoring case
|
||||||
|
return entry.displayName === serverNameLower;
|
||||||
|
});
|
||||||
const serverLabel = registryEntry
|
const serverLabel = registryEntry
|
||||||
? `[${serverName}](${`https://github.com/mcp/${registryEntry.name}`})`
|
? `[${serverName}](${`https://github.com/mcp/${registryEntry.name}`})`
|
||||||
: serverName;
|
: serverName;
|
||||||
@@ -489,12 +493,10 @@ function generateSkillsSection(skillsDir) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get all skill folders (directories)
|
// Get all skill folders (directories)
|
||||||
const skillFolders = fs
|
const skillFolders = fs.readdirSync(skillsDir).filter((file) => {
|
||||||
.readdirSync(skillsDir)
|
const filePath = path.join(skillsDir, file);
|
||||||
.filter((file) => {
|
return fs.statSync(filePath).isDirectory();
|
||||||
const filePath = path.join(skillsDir, file);
|
});
|
||||||
return fs.statSync(filePath).isDirectory();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse each skill folder
|
// Parse each skill folder
|
||||||
const skillEntries = skillFolders
|
const skillEntries = skillFolders
|
||||||
@@ -768,7 +770,11 @@ function generateFeaturedCollectionsSection(collectionsDir) {
|
|||||||
* @param {string} collectionId - Collection ID
|
* @param {string} collectionId - Collection ID
|
||||||
* @param {{ name: string, displayName: string }[]} registryNames - Pre-loaded MCP registry names
|
* @param {{ name: string, displayName: string }[]} registryNames - Pre-loaded MCP registry names
|
||||||
*/
|
*/
|
||||||
function generateCollectionReadme(collection, collectionId, registryNames = []) {
|
function generateCollectionReadme(
|
||||||
|
collection,
|
||||||
|
collectionId,
|
||||||
|
registryNames = []
|
||||||
|
) {
|
||||||
if (!collection || !collection.items) {
|
if (!collection || !collection.items) {
|
||||||
return `# ${collectionId}\n\nCollection not found or invalid.`;
|
return `# ${collectionId}\n\nCollection not found or invalid.`;
|
||||||
}
|
}
|
||||||
@@ -820,20 +826,23 @@ function generateCollectionReadme(collection, collectionId, registryNames = [])
|
|||||||
? "Instruction"
|
? "Instruction"
|
||||||
: item.kind === "agent"
|
: item.kind === "agent"
|
||||||
? "Agent"
|
? "Agent"
|
||||||
|
: item.kind === "skill"
|
||||||
|
? "Skill"
|
||||||
: "Prompt";
|
: "Prompt";
|
||||||
const link = `../${item.path}`;
|
const link = `../${item.path}`;
|
||||||
|
|
||||||
// Create install badges for each item
|
// Create install badges for each item (skills don't use chat install badges)
|
||||||
const badges = makeBadges(
|
const badgeType =
|
||||||
item.path,
|
|
||||||
item.kind === "instruction"
|
item.kind === "instruction"
|
||||||
? "instructions"
|
? "instructions"
|
||||||
: item.kind === "chat-mode"
|
: item.kind === "chat-mode"
|
||||||
? "mode"
|
? "mode"
|
||||||
: item.kind === "agent"
|
: item.kind === "agent"
|
||||||
? "agent"
|
? "agent"
|
||||||
: "prompt"
|
: item.kind === "skill"
|
||||||
);
|
? null
|
||||||
|
: "prompt";
|
||||||
|
const badges = badgeType ? makeBadges(item.path, badgeType) : "";
|
||||||
|
|
||||||
const usageDescription = item.usage
|
const usageDescription = item.usage
|
||||||
? `${description} [see usage](#${title
|
? `${description} [see usage](#${title
|
||||||
@@ -891,15 +900,21 @@ function buildCollectionRow({
|
|||||||
kind,
|
kind,
|
||||||
registryNames = [],
|
registryNames = [],
|
||||||
}) {
|
}) {
|
||||||
|
const titleCell = badges
|
||||||
|
? `[${title}](${link})<br />${badges}`
|
||||||
|
: `[${title}](${link})`;
|
||||||
|
|
||||||
if (hasAgents) {
|
if (hasAgents) {
|
||||||
// Only agents currently have MCP servers; future migration may extend to chat modes.
|
// Only agents currently have MCP servers; future migration may extend to chat modes.
|
||||||
const mcpServers =
|
const mcpServers =
|
||||||
kind === "agent" ? extractMcpServerConfigs(filePath) : [];
|
kind === "agent" ? extractMcpServerConfigs(filePath) : [];
|
||||||
const mcpServerCell =
|
const mcpServerCell =
|
||||||
mcpServers.length > 0 ? generateMcpServerLinks(mcpServers, registryNames) : "";
|
mcpServers.length > 0
|
||||||
return `| [${title}](${link})<br />${badges} | ${typeDisplay} | ${usageDescription} | ${mcpServerCell} |\n`;
|
? generateMcpServerLinks(mcpServers, registryNames)
|
||||||
|
: "";
|
||||||
|
return `| ${titleCell} | ${typeDisplay} | ${usageDescription} | ${mcpServerCell} |\n`;
|
||||||
}
|
}
|
||||||
return `| [${title}](${link})<br />${badges} | ${typeDisplay} | ${usageDescription} |\n`;
|
return `| ${titleCell} | ${typeDisplay} | ${usageDescription} |\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility: write file only if content changed
|
// Utility: write file only if content changed
|
||||||
@@ -921,7 +936,13 @@ function writeFileIfChanged(filePath, content) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build per-category README content using existing generators, upgrading headings to H1
|
// Build per-category README content using existing generators, upgrading headings to H1
|
||||||
function buildCategoryReadme(sectionBuilder, dirPath, headerLine, usageLine, registryNames = []) {
|
function buildCategoryReadme(
|
||||||
|
sectionBuilder,
|
||||||
|
dirPath,
|
||||||
|
headerLine,
|
||||||
|
usageLine,
|
||||||
|
registryNames = []
|
||||||
|
) {
|
||||||
const section = sectionBuilder(dirPath, registryNames);
|
const section = sectionBuilder(dirPath, registryNames);
|
||||||
if (section && section.trim()) {
|
if (section && section.trim()) {
|
||||||
// Upgrade the first markdown heading level from ## to # for standalone README files
|
// Upgrade the first markdown heading level from ## to # for standalone README files
|
||||||
@@ -984,104 +1005,106 @@ async function main() {
|
|||||||
registryNames
|
registryNames
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate collections README
|
// Generate collections README
|
||||||
const collectionsReadme = buildCategoryReadme(
|
const collectionsReadme = buildCategoryReadme(
|
||||||
generateCollectionsSection,
|
generateCollectionsSection,
|
||||||
COLLECTIONS_DIR,
|
COLLECTIONS_DIR,
|
||||||
collectionsHeader,
|
collectionsHeader,
|
||||||
TEMPLATES.collectionsUsage,
|
TEMPLATES.collectionsUsage,
|
||||||
registryNames
|
registryNames
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ensure docs directory exists for category outputs
|
// Ensure docs directory exists for category outputs
|
||||||
if (!fs.existsSync(DOCS_DIR)) {
|
if (!fs.existsSync(DOCS_DIR)) {
|
||||||
fs.mkdirSync(DOCS_DIR, { recursive: true });
|
fs.mkdirSync(DOCS_DIR, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write category outputs into docs folder
|
// Write category outputs into docs folder
|
||||||
writeFileIfChanged(
|
writeFileIfChanged(
|
||||||
path.join(DOCS_DIR, "README.instructions.md"),
|
path.join(DOCS_DIR, "README.instructions.md"),
|
||||||
instructionsReadme
|
instructionsReadme
|
||||||
);
|
);
|
||||||
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
|
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
|
||||||
writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme);
|
writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme);
|
||||||
writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme);
|
writeFileIfChanged(path.join(DOCS_DIR, "README.skills.md"), skillsReadme);
|
||||||
writeFileIfChanged(
|
writeFileIfChanged(
|
||||||
path.join(DOCS_DIR, "README.collections.md"),
|
path.join(DOCS_DIR, "README.collections.md"),
|
||||||
collectionsReadme
|
collectionsReadme
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate individual collection README files
|
// Generate individual collection README files
|
||||||
if (fs.existsSync(COLLECTIONS_DIR)) {
|
if (fs.existsSync(COLLECTIONS_DIR)) {
|
||||||
console.log("Generating individual collection README files...");
|
console.log("Generating individual collection README files...");
|
||||||
|
|
||||||
const collectionFiles = fs
|
const collectionFiles = fs
|
||||||
.readdirSync(COLLECTIONS_DIR)
|
.readdirSync(COLLECTIONS_DIR)
|
||||||
.filter((file) => file.endsWith(".collection.yml"));
|
.filter((file) => file.endsWith(".collection.yml"));
|
||||||
|
|
||||||
for (const file of collectionFiles) {
|
for (const file of collectionFiles) {
|
||||||
const filePath = path.join(COLLECTIONS_DIR, file);
|
const filePath = path.join(COLLECTIONS_DIR, file);
|
||||||
const collection = parseCollectionYaml(filePath);
|
const collection = parseCollectionYaml(filePath);
|
||||||
|
|
||||||
if (collection) {
|
if (collection) {
|
||||||
const collectionId =
|
const collectionId =
|
||||||
collection.id || path.basename(file, ".collection.yml");
|
collection.id || path.basename(file, ".collection.yml");
|
||||||
const readmeContent = generateCollectionReadme(
|
const readmeContent = generateCollectionReadme(
|
||||||
collection,
|
collection,
|
||||||
collectionId,
|
collectionId,
|
||||||
registryNames
|
registryNames
|
||||||
);
|
);
|
||||||
const readmeFile = path.join(COLLECTIONS_DIR, `${collectionId}.md`);
|
const readmeFile = path.join(COLLECTIONS_DIR, `${collectionId}.md`);
|
||||||
writeFileIfChanged(readmeFile, readmeContent);
|
writeFileIfChanged(readmeFile, readmeContent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Generate featured collections section and update main README.md
|
// Generate featured collections section and update main README.md
|
||||||
console.log("Updating main README.md with featured collections...");
|
console.log("Updating main README.md with featured collections...");
|
||||||
const featuredSection = generateFeaturedCollectionsSection(COLLECTIONS_DIR);
|
const featuredSection = generateFeaturedCollectionsSection(COLLECTIONS_DIR);
|
||||||
|
|
||||||
if (featuredSection) {
|
if (featuredSection) {
|
||||||
const mainReadmePath = path.join(ROOT_FOLDER, "README.md");
|
const mainReadmePath = path.join(ROOT_FOLDER, "README.md");
|
||||||
|
|
||||||
if (fs.existsSync(mainReadmePath)) {
|
if (fs.existsSync(mainReadmePath)) {
|
||||||
let readmeContent = fs.readFileSync(mainReadmePath, "utf8");
|
let readmeContent = fs.readFileSync(mainReadmePath, "utf8");
|
||||||
|
|
||||||
// Define markers to identify where to insert the featured collections
|
// Define markers to identify where to insert the featured collections
|
||||||
const startMarker = "## 🌟 Featured Collections";
|
const startMarker = "## 🌟 Featured Collections";
|
||||||
const endMarker = "## MCP Server";
|
const endMarker = "## MCP Server";
|
||||||
|
|
||||||
// Check if the section already exists
|
// Check if the section already exists
|
||||||
const startIndex = readmeContent.indexOf(startMarker);
|
const startIndex = readmeContent.indexOf(startMarker);
|
||||||
|
|
||||||
if (startIndex !== -1) {
|
if (startIndex !== -1) {
|
||||||
// Section exists, replace it
|
// Section exists, replace it
|
||||||
const endIndex = readmeContent.indexOf(endMarker, startIndex);
|
const endIndex = readmeContent.indexOf(endMarker, startIndex);
|
||||||
if (endIndex !== -1) {
|
if (endIndex !== -1) {
|
||||||
// Replace the existing section
|
// Replace the existing section
|
||||||
const beforeSection = readmeContent.substring(0, startIndex);
|
const beforeSection = readmeContent.substring(0, startIndex);
|
||||||
const afterSection = readmeContent.substring(endIndex);
|
const afterSection = readmeContent.substring(endIndex);
|
||||||
readmeContent =
|
readmeContent =
|
||||||
beforeSection + featuredSection + "\n\n" + afterSection;
|
beforeSection + featuredSection + "\n\n" + afterSection;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Section doesn't exist, insert it before "## MCP Server"
|
||||||
|
const mcpIndex = readmeContent.indexOf(endMarker);
|
||||||
|
if (mcpIndex !== -1) {
|
||||||
|
const beforeMcp = readmeContent.substring(0, mcpIndex);
|
||||||
|
const afterMcp = readmeContent.substring(mcpIndex);
|
||||||
|
readmeContent = beforeMcp + featuredSection + "\n\n" + afterMcp;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeFileIfChanged(mainReadmePath, readmeContent);
|
||||||
|
console.log("Main README.md updated with featured collections");
|
||||||
} else {
|
} else {
|
||||||
// Section doesn't exist, insert it before "## MCP Server"
|
console.warn(
|
||||||
const mcpIndex = readmeContent.indexOf(endMarker);
|
"README.md not found, skipping featured collections update"
|
||||||
if (mcpIndex !== -1) {
|
);
|
||||||
const beforeMcp = readmeContent.substring(0, mcpIndex);
|
|
||||||
const afterMcp = readmeContent.substring(mcpIndex);
|
|
||||||
readmeContent = beforeMcp + featuredSection + "\n\n" + afterMcp;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
writeFileIfChanged(mainReadmePath, readmeContent);
|
|
||||||
console.log("Main README.md updated with featured collections");
|
|
||||||
} else {
|
} else {
|
||||||
console.warn("README.md not found, skipping featured collections update");
|
console.log("No featured collections found to add to README.md");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log("No featured collections found to add to README.md");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error generating category README files: ${error.message}`);
|
console.error(`Error generating category README files: ${error.message}`);
|
||||||
console.error(error.stack);
|
console.error(error.stack);
|
||||||
@@ -1095,4 +1118,3 @@ main().catch((error) => {
|
|||||||
console.error(error.stack);
|
console.error(error.stack);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user