Complete Collections feature implementation with validation, tooling, and documentation

Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-09-09 01:37:15 +00:00
parent 406b613cf8
commit 4476bf37cb
7 changed files with 614 additions and 0 deletions

View File

@ -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

25
.vscode/tasks.json vendored
View File

@ -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"
}
]
}

View File

@ -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**

81
collections/TEMPLATE.md Normal file
View File

@ -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

76
create-collection.js Normal file
View File

@ -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 <collection-id>");
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);

View File

@ -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

332
validate-collections.js Normal file
View File

@ -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);
}