mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-23 11:55:12 +00:00
Partners (#354)
* initial prototype of partners collection with featured collection support * Starting to add the partners * Preparing the repo for how the custom agents will work * moving some files around * Moving a bunch of stuff around to make the file easier to read * improving the front matter parsing by using a real library * Some verbage updates * some more verbage * Fixing spelling mistake * tweaking badges * Updating contributing guide to be correct * updating casing to match product * More agents * Better handling link to mcp registry * links to install mcp servers fixed up * Updating collection tags * writing the mcp registry url out properly * Adding custom agents for C# and WinForms Expert custom agents to improve your experience when working with C# and WinForms in Copilot * Adding to agents readme * Adding PagerDuty agent * Fixing description for terraform agent * Adding custom agents to the README usage * Removing the button to make the links more obvious * docs: relocate category READMEs to /docs and update generation + internal links * Updating prompts for new path * formatting --------- Co-authored-by: Chris Patterson <chrispat@github.com>
This commit is contained in:
132
eng/constants.js
Normal file
132
eng/constants.js
Normal file
@@ -0,0 +1,132 @@
|
||||
const path = require("path");
|
||||
|
||||
// Template sections for the README
|
||||
const TEMPLATES = {
|
||||
instructionsSection: `## 📋 Custom Instructions
|
||||
|
||||
Team and project-specific instructions to enhance GitHub Copilot's behavior for specific technologies and coding practices.`,
|
||||
|
||||
instructionsUsage: `### How to Use Custom Instructions
|
||||
|
||||
**To Install:**
|
||||
- Click the **VS Code** or **VS Code Insiders** install button for the instruction you want to use
|
||||
- Download the \`*.instructions.md\` file and manually add it to your project's instruction collection
|
||||
|
||||
**To Use/Apply:**
|
||||
- Copy these instructions to your \`.github/copilot-instructions.md\` file in your workspace
|
||||
- Create task-specific \`.github/.instructions.md\` files in your workspace's \`.github/instructions\` folder
|
||||
- Instructions automatically apply to Copilot behavior once installed in your workspace`,
|
||||
|
||||
promptsSection: `## 🎯 Reusable Prompts
|
||||
|
||||
Ready-to-use prompt templates for specific development scenarios and tasks, defining prompt text with a specific mode, model, and available set of tools.`,
|
||||
|
||||
promptsUsage: `### How to Use Reusable Prompts
|
||||
|
||||
**To Install:**
|
||||
- Click the **VS Code** or **VS Code Insiders** install button for the prompt you want to use
|
||||
- Download the \`*.prompt.md\` file and manually add it to your prompt collection
|
||||
|
||||
**To Run/Execute:**
|
||||
- Use \`/prompt-name\` in VS Code chat after installation
|
||||
- Run the \`Chat: Run Prompt\` command from the Command Palette
|
||||
- Hit the run button while you have a prompt file open in VS Code`,
|
||||
|
||||
chatmodesSection: `## 💭 Custom Chat Modes
|
||||
|
||||
Custom chat modes define specific behaviors and tools for GitHub Copilot Chat, enabling enhanced context-aware assistance for particular tasks or workflows.`,
|
||||
|
||||
chatmodesUsage: `### How to Use Custom Chat Modes
|
||||
|
||||
**To Install:**
|
||||
- Click the **VS Code** or **VS Code Insiders** install button for the chat mode you want to use
|
||||
- Download the \`*.chatmode.md\` file and manually install it in VS Code using the Command Palette
|
||||
|
||||
**To Activate/Use:**
|
||||
- Import the chat mode configuration into your VS Code settings
|
||||
- Access the installed chat modes through the VS Code Chat interface
|
||||
- Select the desired chat mode from the available options in VS Code Chat`,
|
||||
|
||||
collectionsSection: `## 📦 Collections
|
||||
|
||||
Curated collections of related prompts, instructions, and chat modes organized around specific themes, workflows, or use cases.`,
|
||||
|
||||
collectionsUsage: `### How to Use Collections
|
||||
|
||||
**Browse Collections:**
|
||||
- ⭐ Featured collections are highlighted and appear at the top of the list
|
||||
- Explore themed collections that group related customizations
|
||||
- Each collection includes prompts, instructions, and chat modes for specific workflows
|
||||
- Collections make it easy to adopt comprehensive toolkits for particular scenarios
|
||||
|
||||
**Install Items:**
|
||||
- Click install buttons for individual items within collections
|
||||
- Or browse to the individual files to copy content manually
|
||||
- Collections help you discover related customizations you might have missed`,
|
||||
|
||||
featuredCollectionsSection: `## 🌟 Featured Collections
|
||||
|
||||
Discover our curated collections of prompts, instructions, and chat modes organized around specific themes and workflows.`,
|
||||
|
||||
agentsSection: `## 🤖 Custom Agents
|
||||
|
||||
Custom agents for GitHub Copilot, making it easy for users and organizations to "specialize" their Copilot coding agent (CCA) through simple file-based configuration.`,
|
||||
|
||||
agentsUsage: `### How to Use Custom Agents
|
||||
|
||||
**To Install:**
|
||||
- Click the **VS Code** or **VS Code Insiders** install button for the agent you want to use
|
||||
- Download the \`*.agent.md\` file and add it to your repository
|
||||
|
||||
**MCP Server Setup:**
|
||||
- Each agent may require one or more MCP servers to function
|
||||
- Click the MCP server to view it on the GitHub MCP registry
|
||||
- Follow the guide on how to add the MCP server to your repository
|
||||
|
||||
**To Activate/Use:**
|
||||
- Access installed agents through the VS Code Chat interface, assign them in CCA, or through Copilot CLI (coming soon)
|
||||
- Agents will have access to tools from configured MCP servers
|
||||
- Follow agent-specific instructions for optimal usage`,
|
||||
};
|
||||
|
||||
const vscodeInstallImage =
|
||||
"https://img.shields.io/badge/VS_Code-Install-0098FF?style=flat-square&logo=visualstudiocode&logoColor=white";
|
||||
|
||||
const vscodeInsidersInstallImage =
|
||||
"https://img.shields.io/badge/VS_Code_Insiders-Install-24bfa5?style=flat-square&logo=visualstudiocode&logoColor=white";
|
||||
|
||||
const repoBaseUrl =
|
||||
"https://raw.githubusercontent.com/github/awesome-copilot/main";
|
||||
|
||||
const AKA_INSTALL_URLS = {
|
||||
instructions: "https://aka.ms/awesome-copilot/install/instructions",
|
||||
prompt: "https://aka.ms/awesome-copilot/install/prompt",
|
||||
mode: "https://aka.ms/awesome-copilot/install/chatmode",
|
||||
agent: "https://aka.ms/awesome-copilot/install/agent",
|
||||
};
|
||||
|
||||
const ROOT_FOLDER = path.join(__dirname, "..");
|
||||
const INSTRUCTIONS_DIR = path.join(ROOT_FOLDER, "instructions");
|
||||
const PROMPTS_DIR = path.join(ROOT_FOLDER, "prompts");
|
||||
const CHATMODES_DIR = path.join(ROOT_FOLDER, "chatmodes");
|
||||
const AGENTS_DIR = path.join(ROOT_FOLDER, "agents");
|
||||
const COLLECTIONS_DIR = path.join(ROOT_FOLDER, "collections");
|
||||
const MAX_COLLECTION_ITEMS = 50;
|
||||
|
||||
const DOCS_DIR = path.join(ROOT_FOLDER, "docs");
|
||||
|
||||
module.exports = {
|
||||
TEMPLATES,
|
||||
vscodeInstallImage,
|
||||
vscodeInsidersInstallImage,
|
||||
repoBaseUrl,
|
||||
AKA_INSTALL_URLS,
|
||||
ROOT_FOLDER,
|
||||
INSTRUCTIONS_DIR,
|
||||
PROMPTS_DIR,
|
||||
CHATMODES_DIR,
|
||||
AGENTS_DIR,
|
||||
COLLECTIONS_DIR,
|
||||
MAX_COLLECTION_ITEMS,
|
||||
DOCS_DIR,
|
||||
};
|
||||
182
eng/create-collection.js
Executable file
182
eng/create-collection.js
Executable file
@@ -0,0 +1,182 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const readline = require("readline");
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
function prompt(question) {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(question, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const out = { id: undefined, tags: undefined };
|
||||
|
||||
// simple long/short option parsing
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const a = args[i];
|
||||
if (a === "--id" || a === "-i") {
|
||||
out.id = args[i + 1];
|
||||
i++;
|
||||
} else if (a.startsWith("--id=")) {
|
||||
out.id = a.split("=")[1];
|
||||
} else if (a === "--tags" || a === "-t") {
|
||||
out.tags = args[i + 1];
|
||||
i++;
|
||||
} else if (a.startsWith("--tags=")) {
|
||||
out.tags = a.split("=")[1];
|
||||
} else if (!a.startsWith("-") && !out.id) {
|
||||
// first positional -> id
|
||||
out.id = a;
|
||||
} else if (!a.startsWith("-") && out.id && !out.tags) {
|
||||
// second positional -> tags
|
||||
out.tags = a;
|
||||
}
|
||||
}
|
||||
|
||||
// normalize tags to string (comma separated) or undefined
|
||||
if (Array.isArray(out.tags)) {
|
||||
out.tags = out.tags.join(",");
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
async function createCollectionTemplate() {
|
||||
try {
|
||||
console.log("🎯 Collection Creator");
|
||||
console.log("This tool will help you create a new collection manifest.\n");
|
||||
|
||||
// Parse CLI args and fall back to interactive prompts when missing
|
||||
const parsed = parseArgs();
|
||||
// Get collection ID
|
||||
let collectionId = parsed.id;
|
||||
if (!collectionId) {
|
||||
collectionId = await prompt("Collection ID (lowercase, hyphens only): ");
|
||||
}
|
||||
|
||||
// Validate collection ID format
|
||||
if (!collectionId) {
|
||||
console.error("❌ Collection ID is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9-]+$/.test(collectionId)) {
|
||||
console.error(
|
||||
"❌ Collection ID must contain only lowercase letters, numbers, and hyphens"
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const collectionsDir = path.join(__dirname, "collections");
|
||||
const filePath = path.join(
|
||||
collectionsDir,
|
||||
`${collectionId}.collection.yml`
|
||||
);
|
||||
|
||||
// Check if file already exists
|
||||
if (fs.existsSync(filePath)) {
|
||||
console.log(
|
||||
`⚠️ Collection ${collectionId} already exists at ${filePath}`
|
||||
);
|
||||
console.log("💡 Please edit that file instead or choose a different ID.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Ensure collections directory exists
|
||||
if (!fs.existsSync(collectionsDir)) {
|
||||
fs.mkdirSync(collectionsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Get collection name
|
||||
const defaultName = collectionId
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
|
||||
let collectionName = await prompt(
|
||||
`Collection name (default: ${defaultName}): `
|
||||
);
|
||||
if (!collectionName.trim()) {
|
||||
collectionName = defaultName;
|
||||
}
|
||||
|
||||
// Get description
|
||||
const defaultDescription = `A collection of related prompts, instructions, and chat modes for ${collectionName.toLowerCase()}.`;
|
||||
let description = await prompt(
|
||||
`Description (default: ${defaultDescription}): `
|
||||
);
|
||||
if (!description.trim()) {
|
||||
description = defaultDescription;
|
||||
}
|
||||
|
||||
// Get tags (from CLI or prompt)
|
||||
let tags = [];
|
||||
let tagInput = parsed.tags;
|
||||
if (!tagInput) {
|
||||
tagInput = await prompt(
|
||||
"Tags (comma-separated, or press Enter for defaults): "
|
||||
);
|
||||
}
|
||||
|
||||
if (tagInput && tagInput.toString().trim()) {
|
||||
tags = tagInput
|
||||
.toString()
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter((tag) => tag);
|
||||
} else {
|
||||
// Generate some default tags from the collection ID
|
||||
tags = collectionId.split("-").slice(0, 3);
|
||||
}
|
||||
|
||||
// Template content
|
||||
const template = `id: ${collectionId}
|
||||
name: ${collectionName}
|
||||
description: ${description}
|
||||
tags: [${tags.join(", ")}]
|
||||
items:
|
||||
# Add your collection items here
|
||||
# Example:
|
||||
# - path: prompts/example.prompt.md
|
||||
# kind: prompt
|
||||
# - path: instructions/example.instructions.md
|
||||
# kind: instruction
|
||||
# - path: chatmodes/example.chatmode.md
|
||||
# kind: chat-mode
|
||||
# - path: agents/example.agent.md
|
||||
# kind: agent
|
||||
# usage: |
|
||||
# This agent requires the example MCP server to be installed.
|
||||
# Configure any required environment variables (e.g., EXAMPLE_API_KEY).
|
||||
display:
|
||||
ordering: alpha # or "manual" to preserve the order above
|
||||
show_badge: false # set to true to show collection badge on items
|
||||
`;
|
||||
|
||||
fs.writeFileSync(filePath, template);
|
||||
console.log(`✅ Created collection template: ${filePath}`);
|
||||
console.log("\n📝 Next steps:");
|
||||
console.log("1. Edit the collection manifest to add your items");
|
||||
console.log("2. Update the name, description, and tags as needed");
|
||||
console.log("3. Run 'npm run validate:collections' to validate");
|
||||
console.log("4. Run 'npm start' to generate documentation");
|
||||
console.log("\n📄 Collection template contents:");
|
||||
console.log(template);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error creating collection template: ${error.message}`);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Run the interactive creation process
|
||||
createCollectionTemplate();
|
||||
1137
eng/github-mcp-registry.json
Normal file
1137
eng/github-mcp-registry.json
Normal file
File diff suppressed because it is too large
Load Diff
970
eng/update-readme.js
Executable file
970
eng/update-readme.js
Executable file
@@ -0,0 +1,970 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const {
|
||||
parseCollectionYaml,
|
||||
extractMcpServers,
|
||||
extractMcpServerConfigs,
|
||||
parseFrontmatter,
|
||||
} = require("./yaml-parser");
|
||||
const {
|
||||
TEMPLATES,
|
||||
AKA_INSTALL_URLS,
|
||||
repoBaseUrl,
|
||||
vscodeInstallImage,
|
||||
vscodeInsidersInstallImage,
|
||||
ROOT_FOLDER,
|
||||
PROMPTS_DIR,
|
||||
CHATMODES_DIR,
|
||||
AGENTS_DIR,
|
||||
COLLECTIONS_DIR,
|
||||
INSTRUCTIONS_DIR,
|
||||
DOCS_DIR,
|
||||
} = require("./constants");
|
||||
|
||||
// Cache of MCP registry server names (lower-cased) loaded from github-mcp-registry.json
|
||||
let MCP_REGISTRY_SET = null;
|
||||
/**
|
||||
* Loads and caches the set of MCP registry server display names (lowercased).
|
||||
*
|
||||
* Behavior:
|
||||
* - If a cached set already exists (MCP_REGISTRY_SET), it is returned immediately.
|
||||
* - Attempts to read a JSON registry file named "github-mcp-registry.json" from the
|
||||
* same directory as this script.
|
||||
* - Safely handles missing file or malformed JSON by returning an empty Set.
|
||||
* - Extracts server display names from: json.payload.mcpRegistryRoute.serversData.servers
|
||||
* - Normalizes names to lowercase and stores them in a Set for O(1) membership checks.
|
||||
*
|
||||
* Side Effects:
|
||||
* - Mutates the module-scoped variable MCP_REGISTRY_SET.
|
||||
* - Logs a warning to console if reading or parsing the registry fails.
|
||||
*
|
||||
* @returns {{ name: string, displayName: string }[]} A Set of lowercased server display names. May be empty if
|
||||
* the registry file is absent, unreadable, or malformed.
|
||||
*
|
||||
* @throws {none} All errors are caught internally; failures result in an empty Set.
|
||||
*/
|
||||
function loadMcpRegistryNames() {
|
||||
if (MCP_REGISTRY_SET) return MCP_REGISTRY_SET;
|
||||
try {
|
||||
const registryPath = path.join(__dirname, "github-mcp-registry.json");
|
||||
if (!fs.existsSync(registryPath)) {
|
||||
MCP_REGISTRY_SET = [];
|
||||
return MCP_REGISTRY_SET;
|
||||
}
|
||||
const raw = fs.readFileSync(registryPath, "utf8");
|
||||
const json = JSON.parse(raw);
|
||||
const servers = json?.payload?.mcpRegistryRoute?.serversData?.servers || [];
|
||||
MCP_REGISTRY_SET = servers.map((s) => ({
|
||||
name: s.name,
|
||||
displayName: s.display_name.toLowerCase(),
|
||||
}));
|
||||
} catch (e) {
|
||||
console.warn(`Failed to load MCP registry: ${e.message}`);
|
||||
MCP_REGISTRY_SET = [];
|
||||
}
|
||||
return MCP_REGISTRY_SET;
|
||||
}
|
||||
|
||||
// Add error handling utility
|
||||
/**
|
||||
* Safe file operation wrapper
|
||||
*/
|
||||
function safeFileOperation(operation, filePath, defaultValue = null) {
|
||||
try {
|
||||
return operation();
|
||||
} catch (error) {
|
||||
console.error(`Error processing file ${filePath}: ${error.message}`);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
function extractTitle(filePath) {
|
||||
return safeFileOperation(
|
||||
() => {
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
const lines = content.split("\n");
|
||||
|
||||
// Step 1: Try to get title from frontmatter using vfile-matter
|
||||
const frontmatter = parseFrontmatter(filePath);
|
||||
|
||||
if (frontmatter) {
|
||||
// Check for title field
|
||||
if (frontmatter.title && typeof frontmatter.title === "string") {
|
||||
return frontmatter.title;
|
||||
}
|
||||
|
||||
// Check for name field and convert to title case
|
||||
if (frontmatter.name && typeof frontmatter.name === "string") {
|
||||
return frontmatter.name
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: For prompt/chatmode/instructions files, look for heading after frontmatter
|
||||
if (
|
||||
filePath.includes(".prompt.md") ||
|
||||
filePath.includes(".chatmode.md") ||
|
||||
filePath.includes(".instructions.md")
|
||||
) {
|
||||
// Look for first heading after frontmatter
|
||||
let inFrontmatter = false;
|
||||
let frontmatterEnded = false;
|
||||
let inCodeBlock = false;
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim() === "---") {
|
||||
if (!inFrontmatter) {
|
||||
inFrontmatter = true;
|
||||
} else if (inFrontmatter && !frontmatterEnded) {
|
||||
frontmatterEnded = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only look for headings after frontmatter ends
|
||||
if (frontmatterEnded || !inFrontmatter) {
|
||||
// Track code blocks to ignore headings inside them
|
||||
if (
|
||||
line.trim().startsWith("```") ||
|
||||
line.trim().startsWith("````")
|
||||
) {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inCodeBlock && line.startsWith("# ")) {
|
||||
return line.substring(2).trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Format filename for prompt/chatmode/instructions files if no heading found
|
||||
const basename = path.basename(
|
||||
filePath,
|
||||
filePath.includes(".prompt.md")
|
||||
? ".prompt.md"
|
||||
: filePath.includes(".chatmode.md")
|
||||
? ".chatmode.md"
|
||||
: ".instructions.md"
|
||||
);
|
||||
return basename
|
||||
.replace(/[-_]/g, " ")
|
||||
.replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
}
|
||||
|
||||
// Step 4: For other files, look for the first heading (but not in code blocks)
|
||||
let inCodeBlock = false;
|
||||
for (const line of lines) {
|
||||
if (line.trim().startsWith("```") || line.trim().startsWith("````")) {
|
||||
inCodeBlock = !inCodeBlock;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!inCodeBlock && line.startsWith("# ")) {
|
||||
return line.substring(2).trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Fallback to filename
|
||||
const basename = path.basename(filePath, path.extname(filePath));
|
||||
return basename
|
||||
.replace(/[-_]/g, " ")
|
||||
.replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
},
|
||||
filePath,
|
||||
path
|
||||
.basename(filePath, path.extname(filePath))
|
||||
.replace(/[-_]/g, " ")
|
||||
.replace(/\b\w/g, (l) => l.toUpperCase())
|
||||
);
|
||||
}
|
||||
|
||||
function extractDescription(filePath) {
|
||||
return safeFileOperation(
|
||||
() => {
|
||||
// Use vfile-matter to parse frontmatter for all file types
|
||||
const frontmatter = parseFrontmatter(filePath);
|
||||
|
||||
if (frontmatter && frontmatter.description) {
|
||||
return frontmatter.description;
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
filePath,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function makeBadges(link, type) {
|
||||
const aka = AKA_INSTALL_URLS[type] || AKA_INSTALL_URLS.instructions;
|
||||
|
||||
const vscodeUrl = `${aka}?url=${encodeURIComponent(
|
||||
`vscode:chat-${type}/install?url=${repoBaseUrl}/${link}`
|
||||
)}`;
|
||||
const insidersUrl = `${aka}?url=${encodeURIComponent(
|
||||
`vscode-insiders:chat-${type}/install?url=${repoBaseUrl}/${link}`
|
||||
)}`;
|
||||
|
||||
return `[](${vscodeUrl})<br />[](${insidersUrl})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the instructions section with a table of all instructions
|
||||
*/
|
||||
function generateInstructionsSection(instructionsDir) {
|
||||
// Check if directory exists
|
||||
if (!fs.existsSync(instructionsDir)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Get all instruction files
|
||||
const instructionFiles = fs
|
||||
.readdirSync(instructionsDir)
|
||||
.filter((file) => file.endsWith(".instructions.md"));
|
||||
|
||||
// Map instruction files to objects with title for sorting
|
||||
const instructionEntries = instructionFiles.map((file) => {
|
||||
const filePath = path.join(instructionsDir, file);
|
||||
const title = extractTitle(filePath);
|
||||
return { file, filePath, title };
|
||||
});
|
||||
|
||||
// Sort by title alphabetically
|
||||
instructionEntries.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
console.log(`Found ${instructionEntries.length} instruction files`);
|
||||
|
||||
// Return empty string if no files found
|
||||
if (instructionEntries.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create table header
|
||||
let instructionsContent =
|
||||
"| Title | Description |\n| ----- | ----------- |\n";
|
||||
|
||||
// Generate table rows for each instruction file
|
||||
for (const entry of instructionEntries) {
|
||||
const { file, filePath, title } = entry;
|
||||
const link = encodeURI(`instructions/${file}`);
|
||||
|
||||
// Check if there's a description in the frontmatter
|
||||
const customDescription = extractDescription(filePath);
|
||||
|
||||
// Create badges for installation links
|
||||
const badges = makeBadges(link, "instructions");
|
||||
|
||||
if (customDescription && customDescription !== "null") {
|
||||
// Use the description from frontmatter
|
||||
instructionsContent += `| [${title}](${link})<br />${badges} | ${customDescription} |\n`;
|
||||
} else {
|
||||
// Fallback to the default approach - use last word of title for description, removing trailing 's' if present
|
||||
const topic = title.split(" ").pop().replace(/s$/, "");
|
||||
instructionsContent += `| [${title}](${link})<br />${badges} | ${topic} specific coding standards and best practices |\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return `${TEMPLATES.instructionsSection}\n${TEMPLATES.instructionsUsage}\n\n${instructionsContent}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the prompts section with a table of all prompts
|
||||
*/
|
||||
function generatePromptsSection(promptsDir) {
|
||||
// Check if directory exists
|
||||
if (!fs.existsSync(promptsDir)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Get all prompt files
|
||||
const promptFiles = fs
|
||||
.readdirSync(promptsDir)
|
||||
.filter((file) => file.endsWith(".prompt.md"));
|
||||
|
||||
// Map prompt files to objects with title for sorting
|
||||
const promptEntries = promptFiles.map((file) => {
|
||||
const filePath = path.join(promptsDir, file);
|
||||
const title = extractTitle(filePath);
|
||||
return { file, filePath, title };
|
||||
});
|
||||
|
||||
// Sort by title alphabetically
|
||||
promptEntries.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
console.log(`Found ${promptEntries.length} prompt files`);
|
||||
|
||||
// Return empty string if no files found
|
||||
if (promptEntries.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create table header
|
||||
let promptsContent = "| Title | Description |\n| ----- | ----------- |\n";
|
||||
|
||||
// Generate table rows for each prompt file
|
||||
for (const entry of promptEntries) {
|
||||
const { file, filePath, title } = entry;
|
||||
const link = encodeURI(`prompts/${file}`);
|
||||
|
||||
// Check if there's a description in the frontmatter
|
||||
const customDescription = extractDescription(filePath);
|
||||
|
||||
// Create badges for installation links
|
||||
const badges = makeBadges(link, "prompt");
|
||||
|
||||
if (customDescription && customDescription !== "null") {
|
||||
promptsContent += `| [${title}](${link})<br />${badges} | ${customDescription} |\n`;
|
||||
} else {
|
||||
promptsContent += `| [${title}](${link})<br />${badges} | | |\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return `${TEMPLATES.promptsSection}\n${TEMPLATES.promptsUsage}\n\n${promptsContent}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the chat modes section with a table of all chat modes
|
||||
*/
|
||||
function generateChatModesSection(chatmodesDir) {
|
||||
return generateUnifiedModeSection({
|
||||
dir: chatmodesDir,
|
||||
extension: ".chatmode.md",
|
||||
linkPrefix: "chatmodes",
|
||||
badgeType: "mode",
|
||||
includeMcpServers: false,
|
||||
sectionTemplate: TEMPLATES.chatmodesSection,
|
||||
usageTemplate: TEMPLATES.chatmodesUsage,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate MCP server links for an agent
|
||||
* @param {string[]} servers - Array of MCP server names
|
||||
* @returns {string} - Formatted MCP server links with badges
|
||||
*/
|
||||
function generateMcpServerLinks(servers) {
|
||||
if (!servers || servers.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const badges = [
|
||||
{
|
||||
type: "vscode",
|
||||
url: "https://img.shields.io/badge/Install-VS_Code-0098FF?style=flat-square",
|
||||
badgeUrl: (serverName) =>
|
||||
`https://aka.ms/awesome-copilot/install/mcp-vscode?vscode:mcp/by-name/${serverName}/mcp-server`,
|
||||
},
|
||||
{
|
||||
type: "insiders",
|
||||
url: "https://img.shields.io/badge/Install-VS_Code_Insiders-24bfa5?style=flat-square",
|
||||
badgeUrl: (serverName) =>
|
||||
`https://aka.ms/awesome-copilot/install/mcp-vscode?vscode-insiders:mcp/by-name/${serverName}/mcp-server`,
|
||||
},
|
||||
{
|
||||
type: "visualstudio",
|
||||
url: "https://img.shields.io/badge/Install-Visual_Studio-C16FDE?style=flat-square",
|
||||
badgeUrl: (serverName) =>
|
||||
`https://aka.ms/awesome-copilot/install/mcp-visualstudio?vscode:mcp/by-name/${serverName}/mcp-server`,
|
||||
},
|
||||
];
|
||||
|
||||
const registryNames = loadMcpRegistryNames();
|
||||
|
||||
return servers
|
||||
.map((entry) => {
|
||||
// Support either a string name or an object with config
|
||||
const serverObj = typeof entry === "string" ? { name: entry } : entry;
|
||||
const serverName = String(serverObj.name).trim();
|
||||
|
||||
// Build config-only JSON (no name/type for stdio; just command+args+env)
|
||||
let configPayload = {};
|
||||
if (serverObj.type && serverObj.type.toLowerCase() === "http") {
|
||||
// HTTP: url + headers
|
||||
configPayload = {
|
||||
url: serverObj.url || "",
|
||||
headers: serverObj.headers || {},
|
||||
};
|
||||
} else {
|
||||
// Local/stdio: command + args + env
|
||||
configPayload = {
|
||||
command: serverObj.command || "",
|
||||
args: Array.isArray(serverObj.args)
|
||||
? serverObj.args.map(encodeURIComponent)
|
||||
: [],
|
||||
env: serverObj.env || {},
|
||||
};
|
||||
}
|
||||
|
||||
const encodedConfig = encodeURIComponent(JSON.stringify(configPayload));
|
||||
|
||||
const installBadgeUrls = [
|
||||
`[](https://aka.ms/awesome-copilot/install/mcp-vscode?name=${serverName}&config=${encodedConfig})`,
|
||||
`[](https://aka.ms/awesome-copilot/install/mcp-vscodeinsiders?name=${serverName}&config=${encodedConfig})`,
|
||||
`[](https://aka.ms/awesome-copilot/install/mcp-visualstudio/mcp-install?${encodedConfig})`,
|
||||
].join("<br />");
|
||||
|
||||
const registryEntry = registryNames.find(
|
||||
(entry) => entry.displayName === serverName.toLowerCase()
|
||||
);
|
||||
const serverLabel = registryEntry
|
||||
? `[${serverName}](${`https://github.com/mcp/${registryEntry.name}`})`
|
||||
: serverName;
|
||||
return `${serverLabel}<br />${installBadgeUrls}`;
|
||||
})
|
||||
.join("<br />");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the agents section with a table of all agents
|
||||
*/
|
||||
function generateAgentsSection(agentsDir) {
|
||||
return generateUnifiedModeSection({
|
||||
dir: agentsDir,
|
||||
extension: ".agent.md",
|
||||
linkPrefix: "agents",
|
||||
badgeType: "agent",
|
||||
includeMcpServers: true,
|
||||
sectionTemplate: TEMPLATES.agentsSection,
|
||||
usageTemplate: TEMPLATES.agentsUsage,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified generator for chat modes & agents (future consolidation)
|
||||
* @param {Object} cfg
|
||||
* @param {string} cfg.dir - Directory path
|
||||
* @param {string} cfg.extension - File extension to match (e.g. .chatmode.md, .agent.md)
|
||||
* @param {string} cfg.linkPrefix - Link prefix folder name
|
||||
* @param {string} cfg.badgeType - Badge key (mode, agent)
|
||||
* @param {boolean} cfg.includeMcpServers - Whether to include MCP server column
|
||||
* @param {string} cfg.sectionTemplate - Section heading template
|
||||
* @param {string} cfg.usageTemplate - Usage subheading template
|
||||
*/
|
||||
function generateUnifiedModeSection(cfg) {
|
||||
const {
|
||||
dir,
|
||||
extension,
|
||||
linkPrefix,
|
||||
badgeType,
|
||||
includeMcpServers,
|
||||
sectionTemplate,
|
||||
usageTemplate,
|
||||
} = cfg;
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
console.log(`Directory missing for unified mode section: ${dir}`);
|
||||
return "";
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(dir).filter((f) => f.endsWith(extension));
|
||||
|
||||
const entries = files.map((file) => {
|
||||
const filePath = path.join(dir, file);
|
||||
return { file, filePath, title: extractTitle(filePath) };
|
||||
});
|
||||
|
||||
entries.sort((a, b) => a.title.localeCompare(b.title));
|
||||
console.log(
|
||||
`Unified mode generator: ${entries.length} files for extension ${extension}`
|
||||
);
|
||||
if (entries.length === 0) return "";
|
||||
|
||||
let header = "| Title | Description |";
|
||||
if (includeMcpServers) header += " MCP Servers |";
|
||||
let separator = "| ----- | ----------- |";
|
||||
if (includeMcpServers) separator += " ----------- |";
|
||||
|
||||
let content = `${header}\n${separator}\n`;
|
||||
|
||||
for (const { file, filePath, title } of entries) {
|
||||
const link = encodeURI(`${linkPrefix}/${file}`);
|
||||
const description = extractDescription(filePath);
|
||||
const badges = makeBadges(link, badgeType);
|
||||
let mcpServerCell = "";
|
||||
if (includeMcpServers) {
|
||||
const servers = extractMcpServerConfigs(filePath);
|
||||
mcpServerCell = generateMcpServerLinks(servers);
|
||||
}
|
||||
|
||||
if (includeMcpServers) {
|
||||
content += `| [${title}](${link})<br />${badges} | ${
|
||||
description && description !== "null" ? description : ""
|
||||
} | ${mcpServerCell} |\n`;
|
||||
} else {
|
||||
content += `| [${title}](${link})<br />${badges} | ${
|
||||
description && description !== "null" ? description : ""
|
||||
} |\n`;
|
||||
}
|
||||
}
|
||||
|
||||
return `${sectionTemplate}\n${usageTemplate}\n\n${content}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the collections section with a table of all collections
|
||||
*/
|
||||
function generateCollectionsSection(collectionsDir) {
|
||||
// Check if collections directory exists, create it if it doesn't
|
||||
if (!fs.existsSync(collectionsDir)) {
|
||||
console.log("Collections directory does not exist, creating it...");
|
||||
fs.mkdirSync(collectionsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Get all collection files
|
||||
const collectionFiles = fs
|
||||
.readdirSync(collectionsDir)
|
||||
.filter((file) => file.endsWith(".collection.yml"));
|
||||
|
||||
// Map collection files to objects with name for sorting
|
||||
const collectionEntries = collectionFiles
|
||||
.map((file) => {
|
||||
const filePath = path.join(collectionsDir, file);
|
||||
const collection = parseCollectionYaml(filePath);
|
||||
|
||||
if (!collection) {
|
||||
console.warn(`Failed to parse collection: ${file}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const collectionId =
|
||||
collection.id || path.basename(file, ".collection.yml");
|
||||
const name = collection.name || collectionId;
|
||||
const isFeatured = collection.display?.featured === true;
|
||||
return { file, filePath, collection, collectionId, name, isFeatured };
|
||||
})
|
||||
.filter((entry) => entry !== null); // Remove failed parses
|
||||
|
||||
// Separate featured and regular collections
|
||||
const featuredCollections = collectionEntries.filter(
|
||||
(entry) => entry.isFeatured
|
||||
);
|
||||
const regularCollections = collectionEntries.filter(
|
||||
(entry) => !entry.isFeatured
|
||||
);
|
||||
|
||||
// Sort each group alphabetically by name
|
||||
featuredCollections.sort((a, b) => a.name.localeCompare(b.name));
|
||||
regularCollections.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
// Combine: featured first, then regular
|
||||
const sortedEntries = [...featuredCollections, ...regularCollections];
|
||||
|
||||
console.log(
|
||||
`Found ${collectionEntries.length} collection files (${featuredCollections.length} featured)`
|
||||
);
|
||||
|
||||
// If no collections, return empty string
|
||||
if (sortedEntries.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create table header
|
||||
let collectionsContent =
|
||||
"| Name | Description | Items | Tags |\n| ---- | ----------- | ----- | ---- |\n";
|
||||
|
||||
// Generate table rows for each collection file
|
||||
for (const entry of sortedEntries) {
|
||||
const { collection, collectionId, name, isFeatured } = entry;
|
||||
const description = collection.description || "No description";
|
||||
const itemCount = collection.items ? collection.items.length : 0;
|
||||
const tags = collection.tags ? collection.tags.join(", ") : "";
|
||||
|
||||
const link = `collections/${collectionId}.md`;
|
||||
const displayName = isFeatured ? `⭐ ${name}` : name;
|
||||
|
||||
collectionsContent += `| [${displayName}](${link}) | ${description} | ${itemCount} items | ${tags} |\n`;
|
||||
}
|
||||
|
||||
return `${TEMPLATES.collectionsSection}\n${TEMPLATES.collectionsUsage}\n\n${collectionsContent}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the featured collections section for the main README
|
||||
*/
|
||||
function generateFeaturedCollectionsSection(collectionsDir) {
|
||||
// Check if collections directory exists
|
||||
if (!fs.existsSync(collectionsDir)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Get all collection files
|
||||
const collectionFiles = fs
|
||||
.readdirSync(collectionsDir)
|
||||
.filter((file) => file.endsWith(".collection.yml"));
|
||||
|
||||
// Map collection files to objects with name for sorting, filter for featured
|
||||
const featuredCollections = collectionFiles
|
||||
.map((file) => {
|
||||
const filePath = path.join(collectionsDir, file);
|
||||
return safeFileOperation(
|
||||
() => {
|
||||
const collection = parseCollectionYaml(filePath);
|
||||
if (!collection) return null;
|
||||
|
||||
// Only include collections with featured: true
|
||||
if (!collection.display?.featured) return null;
|
||||
|
||||
const collectionId =
|
||||
collection.id || path.basename(file, ".collection.yml");
|
||||
const name = collection.name || collectionId;
|
||||
const description = collection.description || "No description";
|
||||
const tags = collection.tags ? collection.tags.join(", ") : "";
|
||||
const itemCount = collection.items ? collection.items.length : 0;
|
||||
|
||||
return {
|
||||
file,
|
||||
collection,
|
||||
collectionId,
|
||||
name,
|
||||
description,
|
||||
tags,
|
||||
itemCount,
|
||||
};
|
||||
},
|
||||
filePath,
|
||||
null
|
||||
);
|
||||
})
|
||||
.filter((entry) => entry !== null); // Remove non-featured and failed parses
|
||||
|
||||
// Sort by name alphabetically
|
||||
featuredCollections.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
console.log(`Found ${featuredCollections.length} featured collection(s)`);
|
||||
|
||||
// If no featured collections, return empty string
|
||||
if (featuredCollections.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
// Create table header
|
||||
let featuredContent =
|
||||
"| Name | Description | Items | Tags |\n| ---- | ----------- | ----- | ---- |\n";
|
||||
|
||||
// Generate table rows for each featured collection
|
||||
for (const entry of featuredCollections) {
|
||||
const { collectionId, name, description, tags, itemCount } = entry;
|
||||
const readmeLink = `collections/${collectionId}.md`;
|
||||
|
||||
featuredContent += `| [${name}](${readmeLink}) | ${description} | ${itemCount} items | ${tags} |\n`;
|
||||
}
|
||||
|
||||
return `${TEMPLATES.featuredCollectionsSection}\n\n${featuredContent}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate individual collection README file
|
||||
*/
|
||||
function generateCollectionReadme(collection, collectionId) {
|
||||
if (!collection || !collection.items) {
|
||||
return `# ${collectionId}\n\nCollection not found or invalid.`;
|
||||
}
|
||||
|
||||
const name = collection.name || collectionId;
|
||||
const description = collection.description || "No description provided.";
|
||||
const tags = collection.tags ? collection.tags.join(", ") : "None";
|
||||
|
||||
let content = `# ${name}\n\n${description}\n\n`;
|
||||
|
||||
if (collection.tags && collection.tags.length > 0) {
|
||||
content += `**Tags:** ${tags}\n\n`;
|
||||
}
|
||||
|
||||
content += `## Items in this Collection\n\n`;
|
||||
|
||||
// Check if collection has any agents to determine table structure (future: chatmodes may migrate)
|
||||
const hasAgents = collection.items.some((item) => item.kind === "agent");
|
||||
|
||||
// Generate appropriate table header
|
||||
if (hasAgents) {
|
||||
content += `| Title | Type | Description | MCP Servers |\n| ----- | ---- | ----------- | ----------- |\n`;
|
||||
} else {
|
||||
content += `| Title | Type | Description |\n| ----- | ---- | ----------- |\n`;
|
||||
}
|
||||
|
||||
let collectionUsageHeader = "## Collection Usage\n\n";
|
||||
let collectionUsageContent = [];
|
||||
|
||||
// Sort items based on display.ordering setting
|
||||
const items = [...collection.items];
|
||||
if (collection.display?.ordering === "alpha") {
|
||||
items.sort((a, b) => {
|
||||
const titleA = extractTitle(path.join(ROOT_FOLDER, a.path));
|
||||
const titleB = extractTitle(path.join(ROOT_FOLDER, b.path));
|
||||
return titleA.localeCompare(titleB);
|
||||
});
|
||||
}
|
||||
|
||||
for (const item of items) {
|
||||
const filePath = path.join(ROOT_FOLDER, item.path);
|
||||
const title = extractTitle(filePath);
|
||||
const description = extractDescription(filePath) || "No description";
|
||||
|
||||
const typeDisplay =
|
||||
item.kind === "chat-mode"
|
||||
? "Chat Mode"
|
||||
: item.kind === "instruction"
|
||||
? "Instruction"
|
||||
: item.kind === "agent"
|
||||
? "Agent"
|
||||
: "Prompt";
|
||||
const link = `../${item.path}`;
|
||||
|
||||
// Create install badges for each item
|
||||
const badges = makeBadges(
|
||||
item.path,
|
||||
item.kind === "instruction"
|
||||
? "instructions"
|
||||
: item.kind === "chat-mode"
|
||||
? "mode"
|
||||
: item.kind === "agent"
|
||||
? "agent"
|
||||
: "prompt"
|
||||
);
|
||||
|
||||
const usageDescription = item.usage
|
||||
? `${description} [see usage](#${title
|
||||
.replace(/\s+/g, "-")
|
||||
.toLowerCase()})`
|
||||
: description;
|
||||
|
||||
// Generate MCP server column if collection has agents
|
||||
content += buildCollectionRow({
|
||||
hasAgents,
|
||||
title,
|
||||
link,
|
||||
badges,
|
||||
typeDisplay,
|
||||
usageDescription,
|
||||
filePath,
|
||||
kind: item.kind,
|
||||
});
|
||||
// Generate Usage section for each collection
|
||||
if (item.usage && item.usage.trim()) {
|
||||
collectionUsageContent.push(
|
||||
`### ${title}\n\n${item.usage.trim()}\n\n---\n\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Append the usage section if any items had usage defined
|
||||
if (collectionUsageContent.length > 0) {
|
||||
content += `\n${collectionUsageHeader}${collectionUsageContent.join("")}`;
|
||||
} else if (collection.display?.show_badge) {
|
||||
content += "\n---\n";
|
||||
}
|
||||
|
||||
// Optional badge note at the end if show_badge is true
|
||||
if (collection.display?.show_badge) {
|
||||
content += `*This collection includes ${items.length} curated items for **${name}**.*`;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a single markdown table row for a collection item.
|
||||
* Handles optional MCP server column when agents are present.
|
||||
*/
|
||||
function buildCollectionRow({
|
||||
hasAgents,
|
||||
title,
|
||||
link,
|
||||
badges,
|
||||
typeDisplay,
|
||||
usageDescription,
|
||||
filePath,
|
||||
kind,
|
||||
}) {
|
||||
if (hasAgents) {
|
||||
// Only agents currently have MCP servers; future migration may extend to chat modes.
|
||||
const mcpServers =
|
||||
kind === "agent" ? extractMcpServerConfigs(filePath) : [];
|
||||
const mcpServerCell =
|
||||
mcpServers.length > 0 ? generateMcpServerLinks(mcpServers) : "";
|
||||
return `| [${title}](${link})<br />${badges} | ${typeDisplay} | ${usageDescription} | ${mcpServerCell} |\n`;
|
||||
}
|
||||
return `| [${title}](${link})<br />${badges} | ${typeDisplay} | ${usageDescription} |\n`;
|
||||
}
|
||||
|
||||
// Utility: write file only if content changed
|
||||
function writeFileIfChanged(filePath, content) {
|
||||
const exists = fs.existsSync(filePath);
|
||||
if (exists) {
|
||||
const original = fs.readFileSync(filePath, "utf8");
|
||||
if (original === content) {
|
||||
console.log(
|
||||
`${path.basename(filePath)} is already up to date. No changes needed.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log(
|
||||
`${path.basename(filePath)} ${exists ? "updated" : "created"} successfully!`
|
||||
);
|
||||
}
|
||||
|
||||
// Build per-category README content using existing generators, upgrading headings to H1
|
||||
function buildCategoryReadme(sectionBuilder, dirPath, headerLine, usageLine) {
|
||||
const section = sectionBuilder(dirPath);
|
||||
if (section && section.trim()) {
|
||||
// Upgrade the first markdown heading level from ## to # for standalone README files
|
||||
return section.replace(/^##\s/m, "# ");
|
||||
}
|
||||
// Fallback content when no entries are found
|
||||
return `${headerLine}\n\n${usageLine}\n\n_No entries found yet._`;
|
||||
}
|
||||
|
||||
// Main execution
|
||||
try {
|
||||
console.log("Generating category README files...");
|
||||
|
||||
// Compose headers for standalone files by converting section headers to H1
|
||||
const instructionsHeader = TEMPLATES.instructionsSection.replace(
|
||||
/^##\s/m,
|
||||
"# "
|
||||
);
|
||||
const promptsHeader = TEMPLATES.promptsSection.replace(/^##\s/m, "# ");
|
||||
const chatmodesHeader = TEMPLATES.chatmodesSection.replace(/^##\s/m, "# ");
|
||||
const agentsHeader = TEMPLATES.agentsSection.replace(/^##\s/m, "# ");
|
||||
const collectionsHeader = TEMPLATES.collectionsSection.replace(
|
||||
/^##\s/m,
|
||||
"# "
|
||||
);
|
||||
|
||||
const instructionsReadme = buildCategoryReadme(
|
||||
generateInstructionsSection,
|
||||
INSTRUCTIONS_DIR,
|
||||
instructionsHeader,
|
||||
TEMPLATES.instructionsUsage
|
||||
);
|
||||
const promptsReadme = buildCategoryReadme(
|
||||
generatePromptsSection,
|
||||
PROMPTS_DIR,
|
||||
promptsHeader,
|
||||
TEMPLATES.promptsUsage
|
||||
);
|
||||
const chatmodesReadme = buildCategoryReadme(
|
||||
generateChatModesSection,
|
||||
CHATMODES_DIR,
|
||||
chatmodesHeader,
|
||||
TEMPLATES.chatmodesUsage
|
||||
);
|
||||
|
||||
// Generate agents README
|
||||
const agentsReadme = buildCategoryReadme(
|
||||
generateAgentsSection,
|
||||
AGENTS_DIR,
|
||||
agentsHeader,
|
||||
TEMPLATES.agentsUsage
|
||||
);
|
||||
|
||||
// Generate collections README
|
||||
const collectionsReadme = buildCategoryReadme(
|
||||
generateCollectionsSection,
|
||||
COLLECTIONS_DIR,
|
||||
collectionsHeader,
|
||||
TEMPLATES.collectionsUsage
|
||||
);
|
||||
|
||||
// Ensure docs directory exists for category outputs
|
||||
if (!fs.existsSync(DOCS_DIR)) {
|
||||
fs.mkdirSync(DOCS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Write category outputs into docs folder
|
||||
writeFileIfChanged(
|
||||
path.join(DOCS_DIR, "README.instructions.md"),
|
||||
instructionsReadme
|
||||
);
|
||||
writeFileIfChanged(path.join(DOCS_DIR, "README.prompts.md"), promptsReadme);
|
||||
writeFileIfChanged(
|
||||
path.join(DOCS_DIR, "README.chatmodes.md"),
|
||||
chatmodesReadme
|
||||
);
|
||||
writeFileIfChanged(path.join(DOCS_DIR, "README.agents.md"), agentsReadme);
|
||||
writeFileIfChanged(
|
||||
path.join(DOCS_DIR, "README.collections.md"),
|
||||
collectionsReadme
|
||||
);
|
||||
|
||||
// Generate individual collection README files
|
||||
if (fs.existsSync(COLLECTIONS_DIR)) {
|
||||
console.log("Generating individual collection README files...");
|
||||
|
||||
const collectionFiles = fs
|
||||
.readdirSync(COLLECTIONS_DIR)
|
||||
.filter((file) => file.endsWith(".collection.yml"));
|
||||
|
||||
for (const file of collectionFiles) {
|
||||
const filePath = path.join(COLLECTIONS_DIR, file);
|
||||
const collection = parseCollectionYaml(filePath);
|
||||
|
||||
if (collection) {
|
||||
const collectionId =
|
||||
collection.id || path.basename(file, ".collection.yml");
|
||||
const readmeContent = generateCollectionReadme(
|
||||
collection,
|
||||
collectionId
|
||||
);
|
||||
const readmeFile = path.join(COLLECTIONS_DIR, `${collectionId}.md`);
|
||||
writeFileIfChanged(readmeFile, readmeContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate featured collections section and update main README.md
|
||||
console.log("Updating main README.md with featured collections...");
|
||||
const featuredSection = generateFeaturedCollectionsSection(COLLECTIONS_DIR);
|
||||
|
||||
if (featuredSection) {
|
||||
const mainReadmePath = path.join(ROOT_FOLDER, "README.md");
|
||||
|
||||
if (fs.existsSync(mainReadmePath)) {
|
||||
let readmeContent = fs.readFileSync(mainReadmePath, "utf8");
|
||||
|
||||
// Define markers to identify where to insert the featured collections
|
||||
const startMarker = "## 🌟 Featured Collections";
|
||||
const endMarker = "## MCP Server";
|
||||
|
||||
// Check if the section already exists
|
||||
const startIndex = readmeContent.indexOf(startMarker);
|
||||
|
||||
if (startIndex !== -1) {
|
||||
// Section exists, replace it
|
||||
const endIndex = readmeContent.indexOf(endMarker, startIndex);
|
||||
if (endIndex !== -1) {
|
||||
// Replace the existing section
|
||||
const beforeSection = readmeContent.substring(0, startIndex);
|
||||
const afterSection = readmeContent.substring(endIndex);
|
||||
readmeContent =
|
||||
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 {
|
||||
console.warn("README.md not found, skipping featured collections update");
|
||||
}
|
||||
} else {
|
||||
console.log("No featured collections found to add to README.md");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error generating category README files: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
372
eng/validate-collections.js
Executable file
372
eng/validate-collections.js
Executable file
@@ -0,0 +1,372 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { parseCollectionYaml, parseFrontmatter } = require("./yaml-parser");
|
||||
const {
|
||||
ROOT_FOLDER,
|
||||
COLLECTIONS_DIR,
|
||||
MAX_COLLECTION_ITEMS,
|
||||
} = require("./constants");
|
||||
|
||||
// Validation functions
|
||||
function validateCollectionId(id) {
|
||||
if (!id || typeof id !== "string") {
|
||||
return "ID is required and must be a string";
|
||||
}
|
||||
if (!/^[a-z0-9-]+$/.test(id)) {
|
||||
return "ID must contain only lowercase letters, numbers, and hyphens";
|
||||
}
|
||||
if (id.length < 1 || id.length > 50) {
|
||||
return "ID must be between 1 and 50 characters";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateCollectionName(name) {
|
||||
if (!name || typeof name !== "string") {
|
||||
return "Name is required and must be a string";
|
||||
}
|
||||
if (name.length < 1 || name.length > 100) {
|
||||
return "Name must be between 1 and 100 characters";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateCollectionDescription(description) {
|
||||
if (!description || typeof description !== "string") {
|
||||
return "Description is required and must be a string";
|
||||
}
|
||||
if (description.length < 1 || description.length > 500) {
|
||||
return "Description must be between 1 and 500 characters";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateCollectionTags(tags) {
|
||||
if (tags && !Array.isArray(tags)) {
|
||||
return "Tags must be an array";
|
||||
}
|
||||
if (tags && tags.length > 10) {
|
||||
return "Maximum 10 tags allowed";
|
||||
}
|
||||
if (tags) {
|
||||
for (const tag of tags) {
|
||||
if (typeof tag !== "string") {
|
||||
return "All tags must be strings";
|
||||
}
|
||||
if (!/^[a-z0-9-]+$/.test(tag)) {
|
||||
return `Tag "${tag}" must contain only lowercase letters, numbers, and hyphens`;
|
||||
}
|
||||
if (tag.length < 1 || tag.length > 30) {
|
||||
return `Tag "${tag}" must be between 1 and 30 characters`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateAgentFile(filePath) {
|
||||
try {
|
||||
const agent = parseFrontmatter(filePath);
|
||||
|
||||
if (!agent) {
|
||||
return `Item ${filePath} agent file could not be parsed`;
|
||||
}
|
||||
|
||||
// Validate name field
|
||||
if (!agent.name || typeof agent.name !== "string") {
|
||||
return `Item ${filePath} agent must have a 'name' field`;
|
||||
}
|
||||
if (agent.name.length < 1 || agent.name.length > 50) {
|
||||
return `Item ${filePath} agent name must be between 1 and 50 characters`;
|
||||
}
|
||||
|
||||
// Validate description field
|
||||
if (!agent.description || typeof agent.description !== "string") {
|
||||
return `Item ${filePath} agent must have a 'description' field`;
|
||||
}
|
||||
if (agent.description.length < 1 || agent.description.length > 500) {
|
||||
return `Item ${filePath} agent description must be between 1 and 500 characters`;
|
||||
}
|
||||
|
||||
// Validate tools field (optional)
|
||||
if (agent.tools !== undefined && !Array.isArray(agent.tools)) {
|
||||
return `Item ${filePath} agent 'tools' must be an array`;
|
||||
}
|
||||
|
||||
// Validate mcp-servers field (optional)
|
||||
if (agent["mcp-servers"]) {
|
||||
if (
|
||||
typeof agent["mcp-servers"] !== "object" ||
|
||||
Array.isArray(agent["mcp-servers"])
|
||||
) {
|
||||
return `Item ${filePath} agent 'mcp-servers' must be an object`;
|
||||
}
|
||||
|
||||
// Validate each MCP server configuration
|
||||
for (const [serverName, serverConfig] of Object.entries(
|
||||
agent["mcp-servers"]
|
||||
)) {
|
||||
if (!serverConfig || typeof serverConfig !== "object") {
|
||||
return `Item ${filePath} agent MCP server '${serverName}' must be an object`;
|
||||
}
|
||||
|
||||
if (!serverConfig.type || typeof serverConfig.type !== "string") {
|
||||
return `Item ${filePath} agent MCP server '${serverName}' must have a 'type' field`;
|
||||
}
|
||||
|
||||
// For local type servers, command is required
|
||||
if (serverConfig.type === "local" && !serverConfig.command) {
|
||||
return `Item ${filePath} agent MCP server '${serverName}' with type 'local' must have a 'command' field`;
|
||||
}
|
||||
|
||||
// Validate args if present
|
||||
if (
|
||||
serverConfig.args !== undefined &&
|
||||
!Array.isArray(serverConfig.args)
|
||||
) {
|
||||
return `Item ${filePath} agent MCP server '${serverName}' 'args' must be an array`;
|
||||
}
|
||||
|
||||
// Validate tools if present
|
||||
if (
|
||||
serverConfig.tools !== undefined &&
|
||||
!Array.isArray(serverConfig.tools)
|
||||
) {
|
||||
return `Item ${filePath} agent MCP server '${serverName}' 'tools' must be an array`;
|
||||
}
|
||||
|
||||
// Validate env if present
|
||||
if (serverConfig.env !== undefined) {
|
||||
if (
|
||||
typeof serverConfig.env !== "object" ||
|
||||
Array.isArray(serverConfig.env)
|
||||
) {
|
||||
return `Item ${filePath} agent MCP server '${serverName}' 'env' must be an object`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null; // All validations passed
|
||||
} catch (error) {
|
||||
return `Item ${filePath} agent file validation failed: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function validateCollectionItems(items) {
|
||||
if (!items || !Array.isArray(items)) {
|
||||
return "Items is required and must be an array";
|
||||
}
|
||||
if (items.length < 1) {
|
||||
return "At least one item is required";
|
||||
}
|
||||
if (items.length > MAX_COLLECTION_ITEMS) {
|
||||
return `Maximum ${MAX_COLLECTION_ITEMS} items allowed`;
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (!item || typeof item !== "object") {
|
||||
return `Item ${i + 1} must be an object`;
|
||||
}
|
||||
if (!item.path || typeof item.path !== "string") {
|
||||
return `Item ${i + 1} must have a path string`;
|
||||
}
|
||||
if (!item.kind || typeof item.kind !== "string") {
|
||||
return `Item ${i + 1} must have a kind string`;
|
||||
}
|
||||
if (!["prompt", "instruction", "chat-mode", "agent"].includes(item.kind)) {
|
||||
return `Item ${
|
||||
i + 1
|
||||
} kind must be one of: prompt, instruction, chat-mode, agent`;
|
||||
}
|
||||
|
||||
// Validate file path exists
|
||||
const filePath = path.join(ROOT_FOLDER, item.path);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return `Item ${i + 1} file does not exist: ${item.path}`;
|
||||
}
|
||||
|
||||
// Validate path pattern matches kind
|
||||
if (item.kind === "prompt" && !item.path.endsWith(".prompt.md")) {
|
||||
return `Item ${
|
||||
i + 1
|
||||
} kind is "prompt" but path doesn't end with .prompt.md`;
|
||||
}
|
||||
if (
|
||||
item.kind === "instruction" &&
|
||||
!item.path.endsWith(".instructions.md")
|
||||
) {
|
||||
return `Item ${
|
||||
i + 1
|
||||
} kind is "instruction" but path doesn't end with .instructions.md`;
|
||||
}
|
||||
if (item.kind === "chat-mode" && !item.path.endsWith(".chatmode.md")) {
|
||||
return `Item ${
|
||||
i + 1
|
||||
} kind is "chat-mode" but path doesn't end with .chatmode.md`;
|
||||
}
|
||||
if (item.kind === "agent" && !item.path.endsWith(".agent.md")) {
|
||||
return `Item ${
|
||||
i + 1
|
||||
} kind is "agent" but path doesn't end with .agent.md`;
|
||||
}
|
||||
|
||||
// Validate agent-specific frontmatter
|
||||
if (item.kind === "agent") {
|
||||
const agentValidation = validateAgentFile(filePath, i + 1);
|
||||
if (agentValidation) {
|
||||
return agentValidation;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateCollectionDisplay(display) {
|
||||
if (display && typeof display !== "object") {
|
||||
return "Display must be an object";
|
||||
}
|
||||
if (display) {
|
||||
// Normalize ordering and show_badge in case the YAML parser left inline comments
|
||||
const normalize = (val) => {
|
||||
if (typeof val !== "string") return val;
|
||||
// Strip any inline comment starting with '#'
|
||||
const hashIndex = val.indexOf("#");
|
||||
if (hashIndex !== -1) {
|
||||
val = val.substring(0, hashIndex).trim();
|
||||
}
|
||||
// Also strip surrounding quotes if present
|
||||
if (
|
||||
(val.startsWith('"') && val.endsWith('"')) ||
|
||||
(val.startsWith("'") && val.endsWith("'"))
|
||||
) {
|
||||
val = val.substring(1, val.length - 1);
|
||||
}
|
||||
return val.trim();
|
||||
};
|
||||
|
||||
if (display.ordering) {
|
||||
const normalizedOrdering = normalize(display.ordering);
|
||||
if (!["manual", "alpha"].includes(normalizedOrdering)) {
|
||||
return "Display ordering must be 'manual' or 'alpha'";
|
||||
}
|
||||
}
|
||||
|
||||
if (display.show_badge !== undefined) {
|
||||
const raw = display.show_badge;
|
||||
const normalizedBadge = normalize(raw);
|
||||
// Accept boolean or string boolean values
|
||||
if (typeof normalizedBadge === "string") {
|
||||
if (!["true", "false"].includes(normalizedBadge.toLowerCase())) {
|
||||
return "Display show_badge must be boolean";
|
||||
}
|
||||
} else if (typeof normalizedBadge !== "boolean") {
|
||||
return "Display show_badge must be boolean";
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateCollectionManifest(collection, filePath) {
|
||||
const errors = [];
|
||||
|
||||
const idError = validateCollectionId(collection.id);
|
||||
if (idError) errors.push(`ID: ${idError}`);
|
||||
|
||||
const nameError = validateCollectionName(collection.name);
|
||||
if (nameError) errors.push(`Name: ${nameError}`);
|
||||
|
||||
const descError = validateCollectionDescription(collection.description);
|
||||
if (descError) errors.push(`Description: ${descError}`);
|
||||
|
||||
const tagsError = validateCollectionTags(collection.tags);
|
||||
if (tagsError) errors.push(`Tags: ${tagsError}`);
|
||||
|
||||
const itemsError = validateCollectionItems(collection.items);
|
||||
if (itemsError) errors.push(`Items: ${itemsError}`);
|
||||
|
||||
const displayError = validateCollectionDisplay(collection.display);
|
||||
if (displayError) errors.push(`Display: ${displayError}`);
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Main validation function
|
||||
function validateCollections() {
|
||||
if (!fs.existsSync(COLLECTIONS_DIR)) {
|
||||
console.log("No collections directory found - validation skipped");
|
||||
return true;
|
||||
}
|
||||
|
||||
const collectionFiles = fs
|
||||
.readdirSync(COLLECTIONS_DIR)
|
||||
.filter((file) => file.endsWith(".collection.yml"));
|
||||
|
||||
if (collectionFiles.length === 0) {
|
||||
console.log("No collection files found - validation skipped");
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`Validating ${collectionFiles.length} collection files...`);
|
||||
|
||||
let hasErrors = false;
|
||||
const usedIds = new Set();
|
||||
|
||||
for (const file of collectionFiles) {
|
||||
const filePath = path.join(COLLECTIONS_DIR, file);
|
||||
console.log(`\nValidating ${file}...`);
|
||||
|
||||
const collection = parseCollectionYaml(filePath);
|
||||
if (!collection) {
|
||||
console.error(`❌ Failed to parse ${file}`);
|
||||
hasErrors = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Validate the collection structure
|
||||
const errors = validateCollectionManifest(collection, filePath);
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(`❌ Validation errors in ${file}:`);
|
||||
errors.forEach((error) => console.error(` - ${error}`));
|
||||
hasErrors = true;
|
||||
} else {
|
||||
console.log(`✅ ${file} is valid`);
|
||||
}
|
||||
|
||||
// Check for duplicate IDs
|
||||
if (collection.id) {
|
||||
if (usedIds.has(collection.id)) {
|
||||
console.error(
|
||||
`❌ Duplicate collection ID "${collection.id}" found in ${file}`
|
||||
);
|
||||
hasErrors = true;
|
||||
} else {
|
||||
usedIds.add(collection.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasErrors) {
|
||||
console.log(`\n✅ All ${collectionFiles.length} collections are valid`);
|
||||
}
|
||||
|
||||
return !hasErrors;
|
||||
}
|
||||
|
||||
// Run validation
|
||||
try {
|
||||
const isValid = validateCollections();
|
||||
if (!isValid) {
|
||||
console.error("\n❌ Collection validation failed");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("\n🎉 Collection validation passed");
|
||||
} catch (error) {
|
||||
console.error(`Error during validation: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
147
eng/yaml-parser.js
Normal file
147
eng/yaml-parser.js
Normal file
@@ -0,0 +1,147 @@
|
||||
// YAML parser for collection files and frontmatter parsing using vfile-matter
|
||||
const fs = require("fs");
|
||||
const yaml = require("js-yaml");
|
||||
const { VFile } = require("vfile");
|
||||
const { matter } = require("vfile-matter");
|
||||
|
||||
function safeFileOperation(operation, filePath, defaultValue = null) {
|
||||
try {
|
||||
return operation();
|
||||
} catch (error) {
|
||||
console.error(`Error processing file ${filePath}: ${error.message}`);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a collection YAML file (.collection.yml)
|
||||
* Collections are pure YAML files without frontmatter delimiters
|
||||
* @param {string} filePath - Path to the collection file
|
||||
* @returns {object|null} Parsed collection object or null on error
|
||||
*/
|
||||
function parseCollectionYaml(filePath) {
|
||||
return safeFileOperation(
|
||||
() => {
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
// Collections are pure YAML files, parse directly with js-yaml
|
||||
return yaml.load(content, { schema: yaml.JSON_SCHEMA });
|
||||
},
|
||||
filePath,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse frontmatter from a markdown file using vfile-matter
|
||||
* Works with any markdown file that has YAML frontmatter (agents, prompts, chatmodes, instructions)
|
||||
* @param {string} filePath - Path to the markdown file
|
||||
* @returns {object|null} Parsed frontmatter object or null on error
|
||||
*/
|
||||
function parseFrontmatter(filePath) {
|
||||
return safeFileOperation(
|
||||
() => {
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
const file = new VFile({ path: filePath, value: content });
|
||||
|
||||
// Parse the frontmatter using vfile-matter
|
||||
matter(file);
|
||||
|
||||
// The frontmatter is now available in file.data.matter
|
||||
const frontmatter = file.data.matter;
|
||||
|
||||
// Normalize string fields that can accumulate trailing newlines/spaces
|
||||
if (frontmatter) {
|
||||
if (typeof frontmatter.name === "string") {
|
||||
frontmatter.name = frontmatter.name.replace(/[\r\n]+$/g, "").trim();
|
||||
}
|
||||
if (typeof frontmatter.title === "string") {
|
||||
frontmatter.title = frontmatter.title.replace(/[\r\n]+$/g, "").trim();
|
||||
}
|
||||
if (typeof frontmatter.description === "string") {
|
||||
// Remove only trailing whitespace/newlines; preserve internal formatting
|
||||
frontmatter.description = frontmatter.description.replace(
|
||||
/[\s\r\n]+$/g,
|
||||
""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return frontmatter;
|
||||
},
|
||||
filePath,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract agent metadata including MCP server information
|
||||
* @param {string} filePath - Path to the agent file
|
||||
* @returns {object|null} Agent metadata object with name, description, tools, and mcp-servers
|
||||
*/
|
||||
function extractAgentMetadata(filePath) {
|
||||
const frontmatter = parseFrontmatter(filePath);
|
||||
|
||||
if (!frontmatter) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: typeof frontmatter.name === "string" ? frontmatter.name : null,
|
||||
description:
|
||||
typeof frontmatter.description === "string"
|
||||
? frontmatter.description
|
||||
: null,
|
||||
tools: frontmatter.tools || [],
|
||||
mcpServers: frontmatter["mcp-servers"] || {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract MCP server names from an agent file
|
||||
* @param {string} filePath - Path to the agent file
|
||||
* @returns {string[]} Array of MCP server names
|
||||
*/
|
||||
function extractMcpServers(filePath) {
|
||||
const metadata = extractAgentMetadata(filePath);
|
||||
|
||||
if (!metadata || !metadata.mcpServers) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.keys(metadata.mcpServers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract full MCP server configs from an agent file
|
||||
* @param {string} filePath - Path to the agent file
|
||||
* @returns {Array<{name:string,type?:string,command?:string,args?:string[],url?:string,headers?:object}>}
|
||||
*/
|
||||
function extractMcpServerConfigs(filePath) {
|
||||
const metadata = extractAgentMetadata(filePath);
|
||||
if (!metadata || !metadata.mcpServers) return [];
|
||||
return Object.entries(metadata.mcpServers).map(([name, cfg]) => {
|
||||
// Ensure we don't mutate original cfg
|
||||
const copy = { ...cfg };
|
||||
return {
|
||||
name,
|
||||
type: typeof copy.type === "string" ? copy.type : undefined,
|
||||
command: typeof copy.command === "string" ? copy.command : undefined,
|
||||
args: Array.isArray(copy.args) ? copy.args : undefined,
|
||||
url: typeof copy.url === "string" ? copy.url : undefined,
|
||||
headers:
|
||||
typeof copy.headers === "object" && copy.headers !== null
|
||||
? copy.headers
|
||||
: undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
parseCollectionYaml,
|
||||
parseFrontmatter,
|
||||
extractAgentMetadata,
|
||||
extractMcpServers,
|
||||
extractMcpServerConfigs,
|
||||
safeFileOperation,
|
||||
};
|
||||
Reference in New Issue
Block a user