Files
awesome-copilot/eng/yaml-parser.mjs
Aaron Powell 7a003fc75a Deprecate Collections in favour of Plugins
Replace Collections with Plugins as first-class citizens in the repo.
With the Copilot CLI v0.409 release making plugins an on-by-default
marketplace, collections are redundant overhead.

## What changed

### Plugin Infrastructure
- Created eng/validate-plugins.mjs (replaces validate-collections.mjs)
- Created eng/create-plugin.mjs (replaces create-collection.mjs)
- Enhanced all 42 plugin.json files with tags, featured, display, and
  items metadata from their corresponding collection.yml files

### Build & Website
- Updated eng/update-readme.mjs to generate plugin docs
- Updated eng/generate-website-data.mjs to emit plugins.json with full
  items array for modal rendering
- Renamed website collections page to plugins (/plugins/)
- Fixed plugin modal to use <div> instead of <pre> for proper styling
- Updated README.md featured section from Collections to Plugins

### Documentation & CI
- Updated CONTRIBUTING.md, AGENTS.md, copilot-instructions.md, PR template
- Updated CI workflows to validate plugins instead of collections
- Replaced docs/README.collections.md with docs/README.plugins.md

### Cleanup
- Removed eng/validate-collections.mjs, eng/create-collection.mjs,
  eng/collection-to-plugin.mjs
- Removed entire collections/ directory (41 .collection.yml + .md files)
- Removed parseCollectionYaml from yaml-parser.mjs
- Removed COLLECTIONS_DIR from constants.mjs

Closes #711
2026-02-13 15:38:37 +11:00

282 lines
8.4 KiB
JavaScript

// YAML parser for frontmatter parsing using vfile-matter
import fs from "fs";
import yaml from "js-yaml";
import path from "path";
import { VFile } from "vfile";
import { matter } from "vfile-matter";
function safeFileOperation(operation, filePath, defaultValue = null) {
try {
return operation();
} catch (error) {
console.error(`Error processing file ${filePath}: ${error.message}`);
return defaultValue;
}
}
/**
* Parse frontmatter from a markdown file using vfile-matter
* Works with any markdown file that has YAML frontmatter (agents, prompts, 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,
};
});
}
/**
* Parse SKILL.md frontmatter and list bundled assets in a skill folder
* @param {string} skillPath - Path to skill folder
* @returns {object|null} Skill metadata with name, description, and assets array
*/
function parseSkillMetadata(skillPath) {
return safeFileOperation(
() => {
const skillFile = path.join(skillPath, "SKILL.md");
if (!fs.existsSync(skillFile)) {
return null;
}
const frontmatter = parseFrontmatter(skillFile);
// Validate required fields
if (!frontmatter?.name || !frontmatter?.description) {
console.warn(
`Invalid skill at ${skillPath}: missing name or description in frontmatter`
);
return null;
}
// List bundled assets (all files except SKILL.md), recursing through subdirectories
const getAllFiles = (dirPath, arrayOfFiles = []) => {
const files = fs.readdirSync(dirPath);
files.forEach((file) => {
const filePath = path.join(dirPath, file);
if (fs.statSync(filePath).isDirectory()) {
arrayOfFiles = getAllFiles(filePath, arrayOfFiles);
} else {
const relativePath = path.relative(skillPath, filePath);
if (relativePath !== "SKILL.md") {
// Normalize path separators to forward slashes for cross-platform consistency
arrayOfFiles.push(relativePath.replace(/\\/g, "/"));
}
}
});
return arrayOfFiles;
};
const assets = getAllFiles(skillPath).sort();
return {
name: frontmatter.name,
description: frontmatter.description,
assets,
path: skillPath,
};
},
skillPath,
null
);
}
/**
* Parse hook metadata from a hook folder (similar to skills)
* @param {string} hookPath - Path to the hook folder
* @returns {object|null} Hook metadata or null on error
*/
function parseHookMetadata(hookPath) {
return safeFileOperation(
() => {
const readmeFile = path.join(hookPath, "README.md");
if (!fs.existsSync(readmeFile)) {
return null;
}
const frontmatter = parseFrontmatter(readmeFile);
// Validate required fields
if (!frontmatter?.name || !frontmatter?.description) {
console.warn(
`Invalid hook at ${hookPath}: missing name or description in frontmatter`
);
return null;
}
// Extract hook events from hooks.json if it exists
let hookEvents = [];
const hooksJsonPath = path.join(hookPath, "hooks.json");
if (fs.existsSync(hooksJsonPath)) {
try {
const hooksJsonContent = fs.readFileSync(hooksJsonPath, "utf8");
const hooksConfig = JSON.parse(hooksJsonContent);
// Extract all hook event names from the hooks object
if (hooksConfig.hooks && typeof hooksConfig.hooks === "object") {
hookEvents = Object.keys(hooksConfig.hooks);
}
} catch (error) {
console.warn(
`Failed to parse hooks.json at ${hookPath}: ${error.message}`
);
}
}
// List bundled assets (all files except README.md), recursing through subdirectories
const getAllFiles = (dirPath, arrayOfFiles = []) => {
const files = fs.readdirSync(dirPath);
files.forEach((file) => {
const filePath = path.join(dirPath, file);
if (fs.statSync(filePath).isDirectory()) {
arrayOfFiles = getAllFiles(filePath, arrayOfFiles);
} else {
const relativePath = path.relative(hookPath, filePath);
if (relativePath !== "README.md") {
// Normalize path separators to forward slashes for cross-platform consistency
arrayOfFiles.push(relativePath.replace(/\\/g, "/"));
}
}
});
return arrayOfFiles;
};
const assets = getAllFiles(hookPath).sort();
return {
name: frontmatter.name,
description: frontmatter.description,
hooks: hookEvents,
tags: frontmatter.tags || [],
assets,
path: hookPath,
};
},
hookPath,
null
);
}
/**
* Parse a generic YAML file (used for tools.yml and other config files)
* @param {string} filePath - Path to the YAML file
* @returns {object|null} Parsed YAML object or null on error
*/
function parseYamlFile(filePath) {
return safeFileOperation(
() => {
const content = fs.readFileSync(filePath, "utf8");
return yaml.load(content, { schema: yaml.JSON_SCHEMA });
},
filePath,
null
);
}
export {
extractAgentMetadata,
extractMcpServerConfigs,
extractMcpServers,
parseFrontmatter,
parseSkillMetadata,
parseHookMetadata,
parseYamlFile,
safeFileOperation,
};