From 4476bf37cb5df6fbb17d1599a7981778ceb7a24d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 9 Sep 2025 01:37:15 +0000 Subject: [PATCH] Complete Collections feature implementation with validation, tooling, and documentation Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com> --- .github/workflows/validate-readme.yml | 4 + .vscode/tasks.json | 25 ++ CONTRIBUTING.md | 42 +++ collections/TEMPLATE.md | 81 ++++++ create-collection.js | 76 ++++++ instructions/collections.instructions.md | 54 ++++ validate-collections.js | 332 +++++++++++++++++++++++ 7 files changed, 614 insertions(+) create mode 100644 collections/TEMPLATE.md create mode 100644 create-collection.js create mode 100644 instructions/collections.instructions.md create mode 100644 validate-collections.js diff --git a/.github/workflows/validate-readme.yml b/.github/workflows/validate-readme.yml index 94e8db0..baaf656 100644 --- a/.github/workflows/validate-readme.yml +++ b/.github/workflows/validate-readme.yml @@ -7,6 +7,7 @@ on: - "instructions/**" - "prompts/**" - "chatmodes/**" + - "collections/**" - "*.js" jobs: @@ -26,6 +27,9 @@ jobs: with: node-version: "20" + - name: Validate collections + run: node validate-collections.js + - name: Update README.md run: node update-readme.js diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 95c8bec..3c19b49 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -11,6 +11,31 @@ "isDefault": true }, "detail": "Generates the README.md file using update-readme.js script." + }, + { + "label": "validate-collections", + "type": "shell", + "command": "node validate-collections.js", + "problemMatcher": [], + "group": "build", + "detail": "Validates all collection manifest files." + }, + { + "label": "create-collection", + "type": "shell", + "command": "${workspaceFolder}/create-collection.js", + "args": ["${input:collectionId}"], + "problemMatcher": [], + "group": "build", + "detail": "Creates a new collection manifest template." + } + ], + "inputs": [ + { + "id": "collectionId", + "description": "Collection ID (lowercase, hyphen-separated)", + "default": "my-collection", + "type": "promptString" } ] } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 87a07e4..2d7a693 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -100,6 +100,48 @@ You are an expert [domain/role] with deep knowledge in [specific areas]. - [Best practices to follow] ``` +### Adding Collections + +Collections group related prompts, instructions, and chat modes around specific themes or workflows, making it easier for users to discover and adopt comprehensive toolkits. + +1. **Create your collection manifest**: Add a new `.collection.yml` file in the `collections/` directory +2. **Follow the naming convention**: Use descriptive, lowercase filenames with hyphens (e.g., `python-web-development.collection.yml`) +3. **Reference existing items**: Collections should only reference files that already exist in the repository +4. **Test your collection**: Verify all referenced files exist and work well together + +#### Creating a collection: +```bash +# Using the creation script +node create-collection.js my-collection-id + +# Or using VS Code Task: Ctrl+Shift+P > "Tasks: Run Task" > "create-collection" +``` + +#### Example collection format: +```yaml +id: my-collection-id +name: My Collection Name +description: A brief description of what this collection provides and who should use it. +tags: [tag1, tag2, tag3] # Optional discovery tags +items: + - path: prompts/my-prompt.prompt.md + kind: prompt + - path: instructions/my-instructions.instructions.md + kind: instruction + - path: chatmodes/my-chatmode.chatmode.md + kind: chat-mode +display: + ordering: alpha # or "manual" to preserve order above + show_badge: false # set to true to show collection badge +``` + +#### Collection Guidelines: +- **Focus on workflows**: Group items that work together for specific use cases +- **Reasonable size**: Typically 3-10 items work well +- **Test combinations**: Ensure the items complement each other effectively +- **Clear purpose**: The collection should solve a specific problem or workflow +- **Validate before submitting**: Run `node validate-collections.js` to ensure your manifest is valid + ## Submitting Your Contribution 1. **Fork this repository** diff --git a/collections/TEMPLATE.md b/collections/TEMPLATE.md new file mode 100644 index 0000000..693e041 --- /dev/null +++ b/collections/TEMPLATE.md @@ -0,0 +1,81 @@ +# Collections Template + +Use this template to create a new collection of related prompts, instructions, and chat modes. + +## Basic Template + +```yaml +id: my-collection-id +name: My Collection Name +description: A brief description of what this collection provides and who should use it. +tags: [tag1, tag2, tag3] # Optional discovery tags +items: + - path: prompts/my-prompt.prompt.md + kind: prompt + - path: instructions/my-instructions.instructions.md + kind: instruction + - path: chatmodes/my-chatmode.chatmode.md + kind: chat-mode +display: + ordering: alpha # or "manual" to preserve order above + show_badge: false # set to true to show collection badge +``` + +## Field Descriptions + +- **id**: Unique identifier using lowercase letters, numbers, and hyphens only +- **name**: Display name for the collection +- **description**: Brief explanation of the collection's purpose (1-500 characters) +- **tags**: Optional array of discovery tags (max 10, each 1-30 characters) +- **items**: Array of items in the collection (1-50 items) + - **path**: Relative path from repository root to the file + - **kind**: Must be `prompt`, `instruction`, or `chat-mode` +- **display**: Optional display settings + - **ordering**: `alpha` (alphabetical) or `manual` (preserve order) + - **show_badge**: Show collection badge on items (true/false) + +## Creating a New Collection + +### Using VS Code Tasks +1. Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on Mac) +2. Type "Tasks: Run Task" +3. Select "create-collection" +4. Enter your collection ID when prompted + +### Using Command Line +```bash +node create-collection.js my-collection-id +``` + +### Manual Creation +1. Create `collections/my-collection-id.collection.yml` +2. Use the template above as starting point +3. Add your items and customize settings +4. Run `node validate-collections.js` to validate +5. Run `node update-readme.js` to generate documentation + +## Validation + +Collections are automatically validated to ensure: +- Required fields are present and valid +- File paths exist and match the item kind +- IDs are unique across collections +- Tags and display settings follow the schema + +Run validation manually: +```bash +node validate-collections.js +``` + +## File Organization + +Collections don't require reorganizing existing files. Items can be located anywhere in the repository as long as the paths are correct in the manifest. + +## Best Practices + +1. **Meaningful Collections**: Group items that work well together for a specific workflow or use case +2. **Clear Naming**: Use descriptive names and IDs that reflect the collection's purpose +3. **Good Descriptions**: Explain who should use the collection and what benefit it provides +4. **Relevant Tags**: Add discovery tags that help users find related collections +5. **Reasonable Size**: Keep collections focused - typically 3-10 items work well +6. **Test Items**: Ensure all referenced files exist and are functional before adding to a collection \ No newline at end of file diff --git a/create-collection.js b/create-collection.js new file mode 100644 index 0000000..0f1eb67 --- /dev/null +++ b/create-collection.js @@ -0,0 +1,76 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +function createCollectionTemplate(collectionId) { + if (!collectionId) { + console.error("Collection ID is required"); + console.log("Usage: node create-collection.js "); + process.exit(1); + } + + // Validate collection ID format + 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.error(`Collection ${collectionId} already exists at ${filePath}`); + process.exit(1); + } + + // Ensure collections directory exists + if (!fs.existsSync(collectionsDir)) { + fs.mkdirSync(collectionsDir, { recursive: true }); + } + + // Create a friendly name from the ID + const friendlyName = collectionId + .split("-") + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(" "); + + // 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 +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 +display: + ordering: alpha # or "manual" to preserve the order above + 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("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(template); + } catch (error) { + console.error(`Error creating collection template: ${error.message}`); + process.exit(1); + } +} + +// Get collection ID from command line arguments +const collectionId = process.argv[2]; +createCollectionTemplate(collectionId); \ No newline at end of file diff --git a/instructions/collections.instructions.md b/instructions/collections.instructions.md new file mode 100644 index 0000000..e29c2f1 --- /dev/null +++ b/instructions/collections.instructions.md @@ -0,0 +1,54 @@ +--- +description: 'Guidelines for creating and managing awesome-copilot collections' +applyTo: 'collections/*.collection.yml' +--- + +# Collections Development + +## Collection Instructions + +When working with collections in the awesome-copilot repository: + +- Always validate collections using `node validate-collections.js` before committing +- Follow the established YAML schema for collection manifests +- Reference only existing files in the repository +- Use descriptive collection IDs with lowercase letters, numbers, and hyphens +- Keep collections focused on specific workflows or themes +- Test that all referenced items work well together + +## Collection Structure + +- **Required fields**: id, name, description, items +- **Optional fields**: tags, display +- **Item requirements**: path must exist, kind must match file extension +- **Display options**: ordering (alpha/manual), show_badge (true/false) + +## Validation Rules + +- Collection IDs must be unique across all collections +- File paths must exist and match the item kind +- Tags must use lowercase letters, numbers, and hyphens only +- Collections must contain 1-50 items +- Descriptions must be 1-500 characters + +## Best Practices + +- Group 3-10 related items for optimal usability +- Use clear, descriptive names and descriptions +- Add relevant tags for discoverability +- Test the complete workflow the collection enables +- Ensure items complement each other effectively + +## File Organization + +- Collections don't require file reorganization +- Items can be located anywhere in the repository +- Use relative paths from repository root +- Maintain existing directory structure (prompts/, instructions/, chatmodes/) + +## Generation Process + +- Collections automatically generate README files via `update-readme.js` +- Individual collection pages are created in collections/ directory +- Main collections overview is generated as README.collections.md +- VS Code install badges are automatically created for each item \ No newline at end of file diff --git a/validate-collections.js b/validate-collections.js new file mode 100644 index 0000000..28ab240 --- /dev/null +++ b/validate-collections.js @@ -0,0 +1,332 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const path = require("path"); + +// Simple YAML parser (same as in update-readme.js) +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 > leadingSpaces) { + // Property of array item object + currentObject[key] = value; + } + } + } + + return result; + }, + filePath, + null + ); +} + +// 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 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 > 50) { + return "Maximum 50 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"].includes(item.kind)) { + return `Item ${i + 1} kind must be one of: prompt, instruction, chat-mode`; + } + + // Validate file path exists + const filePath = path.join(__dirname, 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`; + } + } + return null; +} + +function validateCollectionDisplay(display) { + if (display && typeof display !== "object") { + return "Display must be an object"; + } + if (display) { + if (display.ordering && !["manual", "alpha"].includes(display.ordering)) { + return "Display ordering must be 'manual' or 'alpha'"; + } + if (display.show_badge && typeof display.show_badge !== "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() { + const collectionsDir = path.join(__dirname, "collections"); + + if (!fs.existsSync(collectionsDir)) { + console.log("No collections directory found - validation skipped"); + return true; + } + + const collectionFiles = fs + .readdirSync(collectionsDir) + .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(collectionsDir, 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); +} \ No newline at end of file