Refactor: Extract YAML parser to shared module and improve user experience

Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-09-15 00:47:16 +00:00
parent 22e4bb21f0
commit 0202b019b2
5 changed files with 212 additions and 250 deletions

View File

@ -83,7 +83,7 @@ We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.
## 🌟 Getting Started
1. **Browse the Collections**: Check out our comprehensive lists of [collections](README.collections.md), [prompts](README.prompts.md), [instructions](README.instructions.md), and [chat modes](README.chatmodes.md).
1. **Browse the Collections**: Check out our comprehensive lists of [prompts](README.prompts.md), [instructions](README.instructions.md), [chat modes](README.chatmodes.md), and [collections](README.collections.md).
2. **Add to your editor**: Click the "Install" button to install to VS Code, or copy the file contents for other editors.
3. **Start Using**: Copy prompts to use with `/` commands, let instructions enhance your coding experience, or activate chat modes for specialized assistance.

View File

@ -2,17 +2,40 @@
const fs = require("fs");
const path = require("path");
const readline = require("readline");
function createCollectionTemplate(collectionId) {
if (!collectionId) {
console.error("Collection ID is required");
console.log("Usage: node create-collection.js <collection-id>");
process.exit(1);
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
function prompt(question) {
return new Promise((resolve) => {
rl.question(question, resolve);
});
}
async function createCollectionTemplate() {
try {
console.log("🎯 Collection Creator");
console.log("This tool will help you create a new collection manifest.\n");
// Get collection ID
let collectionId;
if (process.argv[2]) {
collectionId = process.argv[2];
} else {
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");
console.error("Collection ID must contain only lowercase letters, numbers, and hyphens");
process.exit(1);
}
@ -21,7 +44,8 @@ function createCollectionTemplate(collectionId) {
// Check if file already exists
if (fs.existsSync(filePath)) {
console.error(`Collection ${collectionId} already exists at ${filePath}`);
console.log(`⚠️ Collection ${collectionId} already exists at ${filePath}`);
console.log("💡 Please edit that file instead or choose a different ID.");
process.exit(1);
}
@ -30,17 +54,39 @@ function createCollectionTemplate(collectionId) {
fs.mkdirSync(collectionsDir, { recursive: true });
}
// Create a friendly name from the ID
const friendlyName = collectionId
// 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
let tags = [];
const tagInput = await prompt("Tags (comma-separated, or press Enter for defaults): ");
if (tagInput.trim()) {
tags = tagInput.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: ${friendlyName}
description: A collection of related prompts, instructions, and chat modes for ${friendlyName.toLowerCase()}.
tags: [${collectionId.split("-").slice(0, 3).join(", ")}] # Add relevant tags
name: ${collectionName}
description: ${description}
tags: [${tags.join(", ")}]
items:
# Add your collection items here
# Example:
@ -55,22 +101,23 @@ display:
show_badge: false # set to true to show collection badge on items
`;
try {
fs.writeFileSync(filePath, template);
console.log(`✅ Created collection template: ${filePath}`);
console.log("\nNext steps:");
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 'node validate-collections.js' to validate");
console.log("4. Run 'node update-readme.js' to generate documentation");
console.log("\nCollection template contents:");
console.log("\n📄 Collection template contents:");
console.log(template);
} catch (error) {
console.error(`Error creating collection template: ${error.message}`);
console.error(`Error creating collection template: ${error.message}`);
process.exit(1);
} finally {
rl.close();
}
}
// Get collection ID from command line arguments
const collectionId = process.argv[2];
createCollectionTemplate(collectionId);
// Run the interactive creation process
createCollectionTemplate();

View File

@ -2,6 +2,7 @@
const fs = require("fs");
const path = require("path");
const { parseCollectionYaml } = require("./yaml-parser");
// Template sections for the README
const TEMPLATES = {
@ -68,6 +69,9 @@ Curated collections of related prompts, instructions, and chat modes organized a
};
// Add error handling utility
/**
* Safe file operation wrapper
*/
function safeFileOperation(operation, filePath, defaultValue = null) {
try {
return operation();
@ -77,110 +81,6 @@ function safeFileOperation(operation, filePath, defaultValue = null) {
}
}
/**
* Simple YAML parser for collection manifests
* Handles our specific YAML structure without external dependencies
*/
function parseCollectionYaml(filePath) {
return safeFileOperation(
() => {
const content = fs.readFileSync(filePath, "utf8");
const lines = content.split("\n");
const result = {};
let currentKey = null;
let currentArray = null;
let currentObject = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const leadingSpaces = line.length - line.trimLeft().length;
// Handle array items starting with -
if (trimmed.startsWith("- ")) {
if (currentKey === "items") {
if (!currentArray) {
currentArray = [];
result[currentKey] = currentArray;
}
// Parse item object
const item = {};
currentArray.push(item);
currentObject = item;
// Handle inline properties on same line as -
const restOfLine = trimmed.substring(2).trim();
if (restOfLine) {
const colonIndex = restOfLine.indexOf(":");
if (colonIndex > -1) {
const key = restOfLine.substring(0, colonIndex).trim();
const value = restOfLine.substring(colonIndex + 1).trim();
item[key] = value;
}
}
} else if (currentKey === "tags") {
if (!currentArray) {
currentArray = [];
result[currentKey] = currentArray;
}
const value = trimmed.substring(2).trim();
currentArray.push(value);
}
}
// Handle key-value pairs
else if (trimmed.includes(":")) {
const colonIndex = trimmed.indexOf(":");
const key = trimmed.substring(0, colonIndex).trim();
let value = trimmed.substring(colonIndex + 1).trim();
if (leadingSpaces === 0) {
// Top-level property
currentKey = key;
currentArray = null;
currentObject = null;
if (value) {
// Handle array format [item1, item2, item3]
if (value.startsWith("[") && value.endsWith("]")) {
const arrayContent = value.slice(1, -1);
if (arrayContent.trim()) {
result[key] = arrayContent.split(",").map(item => item.trim());
} else {
result[key] = [];
}
currentKey = null; // Reset since we handled the array
} else {
result[key] = value;
}
} else if (key === "items" || key === "tags") {
// Will be populated by array items
result[key] = [];
currentArray = result[key];
} else if (key === "display") {
result[key] = {};
currentObject = result[key];
}
} else if (currentObject && leadingSpaces > 0) {
// Property of current object (e.g., display properties)
currentObject[key] = value === "true" ? true : value === "false" ? false : value;
} else if (currentArray && currentObject && leadingSpaces > 2) {
// Property of array item object
currentObject[key] = value;
}
}
}
return result;
},
filePath,
null
);
}
function extractTitle(filePath) {
return safeFileOperation(
() => {
@ -533,10 +433,10 @@ function generateChatModesSection(chatmodesDir) {
* Generate the collections section with a table of all collections
*/
function generateCollectionsSection(collectionsDir) {
// Check if collections directory exists
// Check if collections directory exists, create it if it doesn't
if (!fs.existsSync(collectionsDir)) {
console.log("Collections directory does not exist");
return "";
console.log("Collections directory does not exist, creating it...");
fs.mkdirSync(collectionsDir, { recursive: true });
}
// Get all collection files

View File

@ -2,8 +2,10 @@
const fs = require("fs");
const path = require("path");
const { parseCollectionYaml } = require("./yaml-parser");
// Simple YAML parser (same as in update-readme.js)
// Maximum number of items allowed in a collection
const MAX_COLLECTION_ITEMS = 50;
function safeFileOperation(operation, filePath, defaultValue = null) {
try {
return operation();
@ -13,106 +15,6 @@ function safeFileOperation(operation, filePath, defaultValue = null) {
}
}
function parseCollectionYaml(filePath) {
return safeFileOperation(
() => {
const content = fs.readFileSync(filePath, "utf8");
const lines = content.split("\n");
const result = {};
let currentKey = null;
let currentArray = null;
let currentObject = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const leadingSpaces = line.length - line.trimLeft().length;
// Handle array items starting with -
if (trimmed.startsWith("- ")) {
if (currentKey === "items") {
if (!currentArray) {
currentArray = [];
result[currentKey] = currentArray;
}
// Parse item object
const item = {};
currentArray.push(item);
currentObject = item;
// Handle inline properties on same line as -
const restOfLine = trimmed.substring(2).trim();
if (restOfLine) {
const colonIndex = restOfLine.indexOf(":");
if (colonIndex > -1) {
const key = restOfLine.substring(0, colonIndex).trim();
const value = restOfLine.substring(colonIndex + 1).trim();
item[key] = value;
}
}
} else if (currentKey === "tags") {
if (!currentArray) {
currentArray = [];
result[currentKey] = currentArray;
}
const value = trimmed.substring(2).trim();
currentArray.push(value);
}
}
// Handle key-value pairs
else if (trimmed.includes(":")) {
const colonIndex = trimmed.indexOf(":");
const key = trimmed.substring(0, colonIndex).trim();
let value = trimmed.substring(colonIndex + 1).trim();
if (leadingSpaces === 0) {
// Top-level property
currentKey = key;
currentArray = null;
currentObject = null;
if (value) {
// Handle array format [item1, item2, item3]
if (value.startsWith("[") && value.endsWith("]")) {
const arrayContent = value.slice(1, -1);
if (arrayContent.trim()) {
result[key] = arrayContent.split(",").map(item => item.trim());
} else {
result[key] = [];
}
currentKey = null; // Reset since we handled the array
} else {
result[key] = value;
}
} else if (key === "items" || key === "tags") {
// Will be populated by array items
result[key] = [];
currentArray = result[key];
} else if (key === "display") {
result[key] = {};
currentObject = result[key];
}
} else if (currentObject && leadingSpaces > 0) {
// Property of current object (e.g., display properties)
currentObject[key] = value === "true" ? true : value === "false" ? false : value;
} else if (currentArray && currentObject && leadingSpaces > 2) {
// Property of array item object
currentObject[key] = value;
}
}
}
return result;
},
filePath,
null
);
}
// Validation functions
function validateCollectionId(id) {
if (!id || typeof id !== "string") {
@ -177,8 +79,8 @@ function validateCollectionItems(items) {
if (items.length < 1) {
return "At least one item is required";
}
if (items.length > 50) {
return "Maximum 50 items allowed";
if (items.length > MAX_COLLECTION_ITEMS) {
return `Maximum ${MAX_COLLECTION_ITEMS} items allowed`;
}
for (let i = 0; i < items.length; i++) {

113
yaml-parser.js Normal file
View File

@ -0,0 +1,113 @@
// Simple YAML parser for collection files
const fs = require("fs");
function safeFileOperation(operation, filePath, defaultValue = null) {
try {
return operation();
} catch (error) {
console.error(`Error processing file ${filePath}: ${error.message}`);
return defaultValue;
}
}
function parseCollectionYaml(filePath) {
return safeFileOperation(
() => {
const content = fs.readFileSync(filePath, "utf8");
const lines = content.split("\n");
const result = {};
let currentKey = null;
let currentArray = null;
let currentObject = null;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const leadingSpaces = line.length - line.trimLeft().length;
// Handle array items starting with -
if (trimmed.startsWith("- ")) {
if (currentKey === "items") {
if (!currentArray) {
currentArray = [];
result[currentKey] = currentArray;
}
// Parse item object
const item = {};
currentArray.push(item);
currentObject = item;
// Handle inline properties on same line as -
const restOfLine = trimmed.substring(2).trim();
if (restOfLine) {
const colonIndex = restOfLine.indexOf(":");
if (colonIndex > -1) {
const key = restOfLine.substring(0, colonIndex).trim();
const value = restOfLine.substring(colonIndex + 1).trim();
item[key] = value;
}
}
} else if (currentKey === "tags") {
if (!currentArray) {
currentArray = [];
result[currentKey] = currentArray;
}
const value = trimmed.substring(2).trim();
currentArray.push(value);
}
}
// Handle key-value pairs
else if (trimmed.includes(":")) {
const colonIndex = trimmed.indexOf(":");
const key = trimmed.substring(0, colonIndex).trim();
let value = trimmed.substring(colonIndex + 1).trim();
if (leadingSpaces === 0) {
// Top-level property
currentKey = key;
currentArray = null;
currentObject = null;
if (value) {
// Handle array format [item1, item2, item3]
if (value.startsWith("[") && value.endsWith("]")) {
const arrayContent = value.slice(1, -1);
if (arrayContent.trim()) {
result[key] = arrayContent.split(",").map(item => item.trim());
} else {
result[key] = [];
}
currentKey = null; // Reset since we handled the array
} else {
result[key] = value;
}
} else if (key === "items" || key === "tags") {
// Will be populated by array items
result[key] = [];
currentArray = result[key];
} else if (key === "display") {
result[key] = {};
currentObject = result[key];
}
} else if (currentObject && leadingSpaces > 0) {
// Property of current object (e.g., display properties)
currentObject[key] = value === "true" ? true : value === "false" ? false : value;
} else if (currentArray && currentObject && leadingSpaces > 2) {
// Property of array item object
currentObject[key] = value;
}
}
}
return result;
},
filePath,
null
);
}
module.exports = { parseCollectionYaml, safeFileOperation };