#!/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." ); }