mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 02:15:12 +00:00
Deprecate Collections in favour of Plugins
Replace Collections with Plugins as first-class citizens in the repo. With the Copilot CLI v0.409 release making plugins an on-by-default marketplace, collections are redundant overhead. ## What changed ### Plugin Infrastructure - Created eng/validate-plugins.mjs (replaces validate-collections.mjs) - Created eng/create-plugin.mjs (replaces create-collection.mjs) - Enhanced all 42 plugin.json files with tags, featured, display, and items metadata from their corresponding collection.yml files ### Build & Website - Updated eng/update-readme.mjs to generate plugin docs - Updated eng/generate-website-data.mjs to emit plugins.json with full items array for modal rendering - Renamed website collections page to plugins (/plugins/) - Fixed plugin modal to use <div> instead of <pre> for proper styling - Updated README.md featured section from Collections to Plugins ### Documentation & CI - Updated CONTRIBUTING.md, AGENTS.md, copilot-instructions.md, PR template - Updated CI workflows to validate plugins instead of collections - Replaced docs/README.collections.md with docs/README.plugins.md ### Cleanup - Removed eng/validate-collections.mjs, eng/create-collection.mjs, eng/collection-to-plugin.mjs - Removed entire collections/ directory (41 .collection.yml + .md files) - Removed parseCollectionYaml from yaml-parser.mjs - Removed COLLECTIONS_DIR from constants.mjs Closes #711
This commit is contained in:
247
eng/validate-plugins.mjs
Executable file
247
eng/validate-plugins.mjs
Executable file
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { ROOT_FOLDER } from "./constants.mjs";
|
||||
|
||||
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
|
||||
|
||||
const VALID_ITEM_KINDS = ["prompt", "agent", "instruction", "skill", "hook"];
|
||||
|
||||
// Validation functions
|
||||
function validateName(name, folderName) {
|
||||
const errors = [];
|
||||
if (!name || typeof name !== "string") {
|
||||
errors.push("name is required and must be a string");
|
||||
return errors;
|
||||
}
|
||||
if (name.length < 1 || name.length > 50) {
|
||||
errors.push("name must be between 1 and 50 characters");
|
||||
}
|
||||
if (!/^[a-z0-9-]+$/.test(name)) {
|
||||
errors.push("name must contain only lowercase letters, numbers, and hyphens");
|
||||
}
|
||||
if (name !== folderName) {
|
||||
errors.push(`name "${name}" must match folder name "${folderName}"`);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateDescription(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 validateVersion(version) {
|
||||
if (!version || typeof version !== "string") {
|
||||
return "version is required and must be a string";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateTags(tags) {
|
||||
if (tags === undefined) return null;
|
||||
if (!Array.isArray(tags)) {
|
||||
return "tags must be an array";
|
||||
}
|
||||
if (tags.length > 10) {
|
||||
return "maximum 10 tags allowed";
|
||||
}
|
||||
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 validateFeatured(featured) {
|
||||
if (featured === undefined) return null;
|
||||
if (typeof featured !== "boolean") {
|
||||
return "featured must be a boolean";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateDisplay(display) {
|
||||
if (display === undefined) return null;
|
||||
if (typeof display !== "object" || Array.isArray(display) || display === null) {
|
||||
return "display must be an object";
|
||||
}
|
||||
if (display.ordering !== undefined) {
|
||||
if (!["manual", "alpha"].includes(display.ordering)) {
|
||||
return "display.ordering must be 'manual' or 'alpha'";
|
||||
}
|
||||
}
|
||||
if (display.show_badge !== undefined) {
|
||||
if (typeof display.show_badge !== "boolean") {
|
||||
return "display.show_badge must be a boolean";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function validateItems(items) {
|
||||
if (items === undefined) return [];
|
||||
const errors = [];
|
||||
if (!Array.isArray(items)) {
|
||||
errors.push("items must be an array");
|
||||
return errors;
|
||||
}
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (!item || typeof item !== "object") {
|
||||
errors.push(`items[${i}] must be an object`);
|
||||
continue;
|
||||
}
|
||||
if (!item.path || typeof item.path !== "string") {
|
||||
errors.push(`items[${i}] must have a path string`);
|
||||
}
|
||||
if (!item.kind || typeof item.kind !== "string") {
|
||||
errors.push(`items[${i}] must have a kind string`);
|
||||
} else if (!VALID_ITEM_KINDS.includes(item.kind)) {
|
||||
errors.push(
|
||||
`items[${i}] kind must be one of: ${VALID_ITEM_KINDS.join(", ")}`
|
||||
);
|
||||
}
|
||||
// Validate referenced path exists relative to repo root
|
||||
if (item.path && typeof item.path === "string") {
|
||||
const filePath = path.join(ROOT_FOLDER, item.path);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
errors.push(`items[${i}] file does not exist: ${item.path}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validatePlugin(folderName) {
|
||||
const pluginDir = path.join(PLUGINS_DIR, folderName);
|
||||
const errors = [];
|
||||
|
||||
// Rule 1: Must have .github/plugin/plugin.json
|
||||
const pluginJsonPath = path.join(pluginDir, ".github", "plugin", "plugin.json");
|
||||
if (!fs.existsSync(pluginJsonPath)) {
|
||||
errors.push("missing required file: .github/plugin/plugin.json");
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Rule 2: Must have README.md
|
||||
const readmePath = path.join(pluginDir, "README.md");
|
||||
if (!fs.existsSync(readmePath)) {
|
||||
errors.push("missing required file: README.md");
|
||||
}
|
||||
|
||||
// Parse plugin.json
|
||||
let plugin;
|
||||
try {
|
||||
const raw = fs.readFileSync(pluginJsonPath, "utf-8");
|
||||
plugin = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
errors.push(`failed to parse plugin.json: ${err.message}`);
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Rule 3 & 4: name, description, version
|
||||
const nameErrors = validateName(plugin.name, folderName);
|
||||
errors.push(...nameErrors);
|
||||
|
||||
const descError = validateDescription(plugin.description);
|
||||
if (descError) errors.push(descError);
|
||||
|
||||
const versionError = validateVersion(plugin.version);
|
||||
if (versionError) errors.push(versionError);
|
||||
|
||||
// Rule 5: tags
|
||||
const tagsError = validateTags(plugin.tags);
|
||||
if (tagsError) errors.push(tagsError);
|
||||
|
||||
// Rule 8: featured
|
||||
const featuredError = validateFeatured(plugin.featured);
|
||||
if (featuredError) errors.push(featuredError);
|
||||
|
||||
// Rule 9: display
|
||||
const displayError = validateDisplay(plugin.display);
|
||||
if (displayError) errors.push(displayError);
|
||||
|
||||
// Rule 6 & 7: items
|
||||
const itemErrors = validateItems(plugin.items);
|
||||
errors.push(...itemErrors);
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Main validation function
|
||||
function validatePlugins() {
|
||||
if (!fs.existsSync(PLUGINS_DIR)) {
|
||||
console.log("No plugins directory found - validation skipped");
|
||||
return true;
|
||||
}
|
||||
|
||||
const pluginDirs = fs
|
||||
.readdirSync(PLUGINS_DIR, { withFileTypes: true })
|
||||
.filter((d) => d.isDirectory())
|
||||
.map((d) => d.name);
|
||||
|
||||
if (pluginDirs.length === 0) {
|
||||
console.log("No plugin directories found - validation skipped");
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log(`Validating ${pluginDirs.length} plugins...\n`);
|
||||
|
||||
let hasErrors = false;
|
||||
const seenNames = new Set();
|
||||
|
||||
for (const dir of pluginDirs) {
|
||||
console.log(`Validating ${dir}...`);
|
||||
|
||||
const errors = validatePlugin(dir);
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(`❌ ${dir}:`);
|
||||
errors.forEach((e) => console.error(` - ${e}`));
|
||||
hasErrors = true;
|
||||
} else {
|
||||
console.log(`✅ ${dir} is valid`);
|
||||
}
|
||||
|
||||
// Rule 10: duplicate names
|
||||
if (seenNames.has(dir)) {
|
||||
console.error(`❌ Duplicate plugin name "${dir}"`);
|
||||
hasErrors = true;
|
||||
} else {
|
||||
seenNames.add(dir);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasErrors) {
|
||||
console.log(`\n✅ All ${pluginDirs.length} plugins are valid`);
|
||||
}
|
||||
|
||||
return !hasErrors;
|
||||
}
|
||||
|
||||
// Run validation
|
||||
try {
|
||||
const isValid = validatePlugins();
|
||||
if (!isValid) {
|
||||
console.error("\n❌ Plugin validation failed");
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("\n🎉 Plugin validation passed");
|
||||
} catch (error) {
|
||||
console.error(`Error during validation: ${error.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user