awesome-copilot/mcp-server/update-metadata.js
2025-09-10 17:11:55 +10:00

344 lines
10 KiB
JavaScript

#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
// Constants
const APPLY_TO_KEY = "applyTo";
// Helper function to process applyTo field values
function processApplyToField(value) {
if (value.includes(",")) {
return value
.split(",")
.map((item) => item.trim())
.filter((item) => item.length > 0);
} else if (value.length > 0) {
return [value];
} else {
return [];
}
}
// Read the JSON schema to understand the structure
const schemaPath = path.join(__dirname, "frontmatter-schema.json");
const schema = JSON.parse(fs.readFileSync(schemaPath, "utf8"));
// Define the directories to process
const directories = {
chatmodes: path.join(__dirname, "src", "awesome-copilot", "chatmodes"),
instructions: path.join(__dirname, "src", "awesome-copilot", "instructions"),
prompts: path.join(__dirname, "src", "awesome-copilot", "prompts"),
};
/**
* Parses a simple YAML frontmatter string into a JavaScript object.
*
* This function handles key-value pairs, multi-line values, arrays, and special cases
* like the `applyTo` key, which is processed into an array of strings. It also removes
* comments and trims unnecessary whitespace.
*
* @param {string} yamlContent - The YAML frontmatter content as a string.
* Each line should represent a key-value pair, an array item,
* or a comment (starting with `#`).
* @returns {Object} A JavaScript object representing the parsed YAML content.
* Keys are strings, and values can be strings, arrays, or objects.
* Special handling is applied to the `applyTo` key, converting
* comma-separated strings into arrays.
*/
function parseSimpleYaml(yamlContent) {
const result = {};
const lines = yamlContent.split("\n");
let currentKey = null;
let currentValue = "";
let inArray = false;
let arrayItems = [];
// Helper to parse a bracket-style array string into array items.
function parseBracketArrayString(str) {
const items = [];
const arrayContent = str.slice(1, -1);
if (!arrayContent.trim()) return items;
// Split by comma, but be defensive and trim each item and remove trailing commas/quotes
const rawItems = arrayContent.split(",");
for (let raw of rawItems) {
let item = raw.trim();
if (!item) continue;
// Remove trailing commas left over (defensive)
if (item.endsWith(",")) item = item.slice(0, -1).trim();
// Remove surrounding quotes if present
if (
(item.startsWith('"') && item.endsWith('"')) ||
(item.startsWith("'") && item.endsWith("'"))
) {
item = item.slice(1, -1);
}
if (item.length > 0) items.push(item);
}
return items;
}
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
// Check if this is a key-value pair
const colonIndex = trimmed.indexOf(":");
if (colonIndex !== -1 && !trimmed.startsWith("-")) {
// Finish previous key if we were building one
if (currentKey) {
if (inArray) {
result[currentKey] = arrayItems;
arrayItems = [];
inArray = false;
} else {
let trimmedValue = currentValue.trim();
// If the accumulated value looks like a bracket array (possibly multiline), parse it
if (trimmedValue.startsWith("[") && trimmedValue.endsWith("]")) {
result[currentKey] = parseBracketArrayString(trimmedValue);
} else {
// Handle comma-separated strings for specific fields that should be arrays
if (currentKey === APPLY_TO_KEY) {
result[currentKey] = processApplyToField(trimmedValue);
} else {
result[currentKey] = trimmedValue;
}
}
}
}
currentKey = trimmed.substring(0, colonIndex).trim();
currentValue = trimmed.substring(colonIndex + 1).trim();
// Remove quotes if present
if (
(currentValue.startsWith('"') && currentValue.endsWith('"')) ||
(currentValue.startsWith("'") && currentValue.endsWith("'"))
) {
currentValue = currentValue.slice(1, -1);
}
// Check if this is an inline bracket-array
if (currentValue.startsWith("[") && currentValue.endsWith("]")) {
result[currentKey] = parseBracketArrayString(currentValue);
currentKey = null;
currentValue = "";
} else if (currentValue === "" || currentValue === "[]") {
// Empty value or empty array, might be multi-line
if (currentValue === "[]") {
result[currentKey] = [];
currentKey = null;
currentValue = "";
} else {
// Check if next line starts with a dash (array item)
if (i + 1 < lines.length && lines[i + 1].trim().startsWith("-")) {
inArray = true;
arrayItems = [];
}
}
}
} else if (trimmed.startsWith("-") && currentKey && inArray) {
// Array item
let item = trimmed.substring(1).trim();
// Remove trailing commas and surrounding quotes
if (item.endsWith(",")) item = item.slice(0, -1).trim();
if (
(item.startsWith('"') && item.endsWith('"')) ||
(item.startsWith("'") && item.endsWith("'"))
) {
item = item.slice(1, -1);
}
arrayItems.push(item);
} else if (currentKey && !inArray) {
// Multi-line value
currentValue += " " + trimmed;
}
}
// Finish the last key
if (currentKey) {
if (inArray) {
result[currentKey] = arrayItems;
} else {
let finalValue = currentValue.trim();
// Remove quotes if present
if (
(finalValue.startsWith('"') && finalValue.endsWith('"')) ||
(finalValue.startsWith("'") && finalValue.endsWith("'"))
) {
finalValue = finalValue.slice(1, -1);
}
// If the final value looks like a bracket array, parse it
if (finalValue.startsWith("[") && finalValue.endsWith("]")) {
result[currentKey] = parseBracketArrayString(finalValue);
} else {
// Handle comma-separated strings for specific fields that should be arrays
if (currentKey === APPLY_TO_KEY) {
result[currentKey] = processApplyToField(finalValue);
} else {
result[currentKey] = finalValue;
}
}
}
}
return result;
}
// Function to extract frontmatter from a markdown file
function extractFrontmatter(filePath) {
let content = fs.readFileSync(filePath, "utf8");
// Remove BOM if present (handles files with Byte Order Mark)
if (content.charCodeAt(0) === 0xfeff) {
content = content.slice(1);
}
// Check if the file starts with frontmatter
if (!content.startsWith("---")) {
return null;
}
const lines = content.split("\n");
let frontmatterEnd = -1;
// Find the end of frontmatter
for (let i = 1; i < lines.length; i++) {
if (lines[i].trim() === "---") {
frontmatterEnd = i;
break;
}
}
if (frontmatterEnd === -1) {
return null;
}
// Extract frontmatter content
const frontmatterContent = lines.slice(1, frontmatterEnd).join("\n");
try {
return parseSimpleYaml(frontmatterContent);
} catch (error) {
console.error(`Error parsing frontmatter in ${filePath}:`, error.message);
return null;
}
}
// Function to process files in a directory
function processDirectory(dirPath, fileExtension) {
const files = fs
.readdirSync(dirPath)
.filter((file) => file.endsWith(fileExtension))
.sort();
const results = [];
for (const file of files) {
const filePath = path.join(dirPath, file);
const frontmatter = extractFrontmatter(filePath);
if (frontmatter) {
const result = {
filename: file,
...frontmatter,
};
// Ensure description is present (required by schema)
if (!result.description) {
console.warn(
`Warning: No description found in ${file}, adding placeholder`
);
result.description = "No description provided";
}
results.push(result);
} else {
console.warn(`Warning: No frontmatter found in ${file}, skipping`);
}
}
return results;
}
// Process all directories
const metadata = {
chatmodes: processDirectory(directories.chatmodes, ".chatmode.md"),
instructions: processDirectory(directories.instructions, ".instructions.md"),
prompts: processDirectory(directories.prompts, ".prompt.md"),
};
// Write the metadata.json file
const outputPath = path.join(
__dirname,
"src",
"AwesomeCopilot.McpServer",
"metadata.json"
);
fs.writeFileSync(outputPath, JSON.stringify(metadata, null, 2));
console.log(
`Extracted frontmatter from ${metadata.chatmodes.length} chatmode files`
);
console.log(
`Extracted frontmatter from ${metadata.instructions.length} instruction files`
);
console.log(
`Extracted frontmatter from ${metadata.prompts.length} prompt files`
);
console.log(`Metadata written to ${outputPath}`);
// Validate that required fields are present
let hasErrors = false;
// Check chatmodes
metadata.chatmodes.forEach((chatmode) => {
if (!chatmode.filename || !chatmode.description) {
console.error(
`Error: Chatmode missing required fields: ${
chatmode.filename || "unknown"
}`
);
hasErrors = true;
}
});
// Check instructions
metadata.instructions.forEach((instruction) => {
if (!instruction.filename || !instruction.description) {
console.error(
`Error: Instruction missing required fields: ${
instruction.filename || "unknown"
}`
);
hasErrors = true;
}
});
// Check prompts
metadata.prompts.forEach((prompt) => {
if (!prompt.filename || !prompt.description) {
console.error(
`Error: Prompt missing required fields: ${prompt.filename || "unknown"}`
);
hasErrors = true;
}
});
if (hasErrors) {
console.error(
"Some files are missing required fields. Please check the output above."
);
process.exit(1);
} else {
console.log(
"All files have required fields. Metadata extraction completed successfully."
);
}