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:
Aaron Powell
2026-02-13 15:38:37 +11:00
parent de0611d0ec
commit 7a003fc75a
154 changed files with 2603 additions and 5790 deletions

View File

@@ -2,7 +2,7 @@
/**
* Generate JSON metadata files for the GitHub Pages website.
* This script extracts metadata from agents, prompts, instructions, skills, and collections
* This script extracts metadata from agents, prompts, instructions, skills, and plugins
* and writes them to website/data/ for client-side search and display.
*/
@@ -11,17 +11,16 @@ import path from "path";
import { fileURLToPath } from "url";
import {
AGENTS_DIR,
COLLECTIONS_DIR,
COOKBOOK_DIR,
HOOKS_DIR,
INSTRUCTIONS_DIR,
PLUGINS_DIR,
PROMPTS_DIR,
ROOT_FOLDER,
SKILLS_DIR
} from "./constants.mjs";
import { getGitFileDates } from "./utils/git-dates.mjs";
import {
parseCollectionYaml,
parseFrontmatter,
parseSkillMetadata,
parseHookMetadata,
@@ -483,66 +482,58 @@ function getSkillFiles(skillPath, relativePath) {
}
/**
* Generate collections metadata
* Generate plugins metadata
*/
function generateCollectionsData(gitDates) {
const collections = [];
function generatePluginsData(gitDates) {
const plugins = [];
if (!fs.existsSync(COLLECTIONS_DIR)) {
return collections;
if (!fs.existsSync(PLUGINS_DIR)) {
return plugins;
}
const files = fs
.readdirSync(COLLECTIONS_DIR)
.filter((f) => f.endsWith(".collection.yml"));
const pluginDirs = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true })
.filter(d => d.isDirectory());
// Track all unique tags
const allTags = new Set();
for (const dir of pluginDirs) {
const pluginDir = path.join(PLUGINS_DIR, dir.name);
const jsonPath = path.join(pluginDir, ".github", "plugin", "plugin.json");
for (const file of files) {
const filePath = path.join(COLLECTIONS_DIR, file);
const data = parseCollectionYaml(filePath);
const relativePath = path
.relative(ROOT_FOLDER, filePath)
.replace(/\\/g, "/");
if (!fs.existsSync(jsonPath)) continue;
if (data) {
const tags = data.tags || [];
tags.forEach((t) => allTags.add(t));
try {
const data = JSON.parse(fs.readFileSync(jsonPath, "utf-8"));
const relPath = `plugins/${dir.name}`;
const dates = gitDates[relPath] || gitDates[`${relPath}/`] || {};
// featured can be at top level or nested under display
const featured = data.featured || data.display?.featured || false;
collections.push({
id: file.replace(".collection.yml", ""),
name: data.name || file.replace(".collection.yml", ""),
plugins.push({
id: dir.name,
name: data.name || dir.name,
description: data.description || "",
tags: tags,
featured: featured,
items: (data.items || []).map((item) => ({
path: item.path,
kind: item.kind,
usage: item.usage || null,
})),
path: relativePath,
filename: file,
lastUpdated: gitDates.get(relativePath) || null,
path: relPath,
tags: data.tags || [],
featured: data.featured || false,
itemCount: data.items ? data.items.length : 0,
items: data.items || [],
lastUpdated: dates.lastModified || null,
searchText: `${data.name || dir.name} ${data.description || ""} ${(data.tags || []).join(" ")}`.toLowerCase(),
});
} catch (e) {
console.warn(`Failed to parse plugin: ${dir.name}`, e.message);
}
}
// Sort with featured first, then alphabetically
const sortedCollections = collections.sort((a, b) => {
// Collect all unique tags
const allTags = [...new Set(plugins.flatMap(p => p.tags))].sort();
const sortedPlugins = plugins.sort((a, b) => {
if (a.featured && !b.featured) return -1;
if (!a.featured && b.featured) return 1;
return a.name.localeCompare(b.name);
});
return {
items: sortedCollections,
filters: {
tags: Array.from(allTags).sort(),
},
items: sortedPlugins,
filters: { tags: allTags }
};
}
@@ -612,7 +603,7 @@ function generateSearchIndex(
instructions,
hooks,
skills,
collections
plugins
) {
const index = [];
@@ -682,18 +673,16 @@ function generateSearchIndex(
});
}
for (const collection of collections) {
for (const plugin of plugins) {
index.push({
type: "collection",
id: collection.id,
title: collection.name,
description: collection.description,
path: collection.path,
tags: collection.tags,
lastUpdated: collection.lastUpdated,
searchText: `${collection.name} ${
collection.description
} ${collection.tags.join(" ")}`.toLowerCase(),
type: "plugin",
id: plugin.id,
title: plugin.name,
description: plugin.description,
path: plugin.path,
tags: plugin.tags,
lastUpdated: plugin.lastUpdated,
searchText: plugin.searchText,
});
}
@@ -806,7 +795,7 @@ async function main() {
// Load git dates for all resource files (single efficient git command)
console.log("Loading git history for last updated dates...");
const gitDates = getGitFileDates(
["agents/", "prompts/", "instructions/", "hooks/", "skills/", "collections/"],
["agents/", "prompts/", "instructions/", "hooks/", "skills/", "plugins/"],
ROOT_FOLDER
);
console.log(`✓ Loaded dates for ${gitDates.size} files\n`);
@@ -842,10 +831,10 @@ async function main() {
`✓ Generated ${skills.length} skills (${skillsData.filters.categories.length} categories)`
);
const collectionsData = generateCollectionsData(gitDates);
const collections = collectionsData.items;
const pluginsData = generatePluginsData(gitDates);
const plugins = pluginsData.items;
console.log(
`✓ Generated ${collections.length} collections (${collectionsData.filters.tags.length} tags)`
`✓ Generated ${plugins.length} plugins (${pluginsData.filters.tags.length} tags)`
);
const toolsData = generateToolsData();
@@ -865,7 +854,7 @@ async function main() {
instructions,
hooks,
skills,
collections
plugins
);
console.log(`✓ Generated search index with ${searchIndex.length} items`);
@@ -896,8 +885,8 @@ async function main() {
);
fs.writeFileSync(
path.join(WEBSITE_DATA_DIR, "collections.json"),
JSON.stringify(collectionsData, null, 2)
path.join(WEBSITE_DATA_DIR, "plugins.json"),
JSON.stringify(pluginsData, null, 2)
);
fs.writeFileSync(
@@ -924,7 +913,7 @@ async function main() {
instructions: instructions.length,
skills: skills.length,
hooks: hooks.length,
collections: collections.length,
plugins: plugins.length,
tools: tools.length,
samples: samplesData.totalRecipes,
total: searchIndex.length,