mirror of
https://github.com/github/awesome-copilot.git
synced 2026-02-20 18:35:14 +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:
@@ -21,27 +21,27 @@ let currentFileContent: string | null = null;
|
||||
let currentFileType: string | null = null;
|
||||
let triggerElement: HTMLElement | null = null;
|
||||
|
||||
// Collection data cache
|
||||
interface CollectionItem {
|
||||
// Plugin data cache
|
||||
interface PluginItem {
|
||||
path: string;
|
||||
kind: string;
|
||||
usage?: string | null;
|
||||
}
|
||||
|
||||
interface Collection {
|
||||
interface Plugin {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
path: string;
|
||||
items: CollectionItem[];
|
||||
items: PluginItem[];
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
interface CollectionsData {
|
||||
items: Collection[];
|
||||
interface PluginsData {
|
||||
items: Plugin[];
|
||||
}
|
||||
|
||||
let collectionsCache: CollectionsData | null = null;
|
||||
let pluginsCache: PluginsData | null = null;
|
||||
|
||||
/**
|
||||
* Get all focusable elements within a container
|
||||
@@ -299,7 +299,7 @@ export async function openFileModal(
|
||||
): Promise<void> {
|
||||
const modal = document.getElementById("file-modal");
|
||||
const title = document.getElementById("modal-title");
|
||||
const modalContent = document.getElementById("modal-content");
|
||||
let modalContent = document.getElementById("modal-content");
|
||||
const contentEl = modalContent?.querySelector("code");
|
||||
const installDropdown = document.getElementById("install-dropdown");
|
||||
const installBtnMain = document.getElementById(
|
||||
@@ -337,9 +337,9 @@ export async function openFileModal(
|
||||
closeBtn?.focus();
|
||||
}, 0);
|
||||
|
||||
// Handle collections differently - show as item list
|
||||
if (type === "collection") {
|
||||
await openCollectionModal(
|
||||
// Handle plugins differently - show as item list
|
||||
if (type === "plugin") {
|
||||
await openPluginModal(
|
||||
filePath,
|
||||
title,
|
||||
modalContent,
|
||||
@@ -359,9 +359,16 @@ export async function openFileModal(
|
||||
if (copyBtn) copyBtn.style.display = "inline-flex";
|
||||
if (downloadBtn) downloadBtn.style.display = "inline-flex";
|
||||
|
||||
// Restore pre/code structure if it was replaced by collection view
|
||||
if (!modalContent.querySelector("pre")) {
|
||||
modalContent.innerHTML = '<pre id="modal-content"><code></code></pre>';
|
||||
// Restore pre/code structure if it was replaced by plugin view
|
||||
if (modalContent.tagName !== 'PRE') {
|
||||
const modalBody = modalContent.parentElement;
|
||||
if (modalBody) {
|
||||
const pre = document.createElement("pre");
|
||||
pre.id = "modal-content";
|
||||
pre.innerHTML = "<code></code>";
|
||||
modalBody.replaceChild(pre, modalContent);
|
||||
modalContent = pre;
|
||||
}
|
||||
}
|
||||
const codeEl = modalContent.querySelector("code");
|
||||
|
||||
@@ -392,9 +399,9 @@ export async function openFileModal(
|
||||
}
|
||||
|
||||
/**
|
||||
* Open collection modal with item list
|
||||
* Open plugin modal with item list
|
||||
*/
|
||||
async function openCollectionModal(
|
||||
async function openPluginModal(
|
||||
filePath: string,
|
||||
title: HTMLElement,
|
||||
modalContent: HTMLElement,
|
||||
@@ -402,48 +409,56 @@ async function openCollectionModal(
|
||||
copyBtn: HTMLElement | null,
|
||||
downloadBtn: HTMLElement | null
|
||||
): Promise<void> {
|
||||
// Hide install dropdown and copy/download for collections
|
||||
// Hide install dropdown and copy/download for plugins
|
||||
if (installDropdown) installDropdown.style.display = "none";
|
||||
if (copyBtn) copyBtn.style.display = "none";
|
||||
if (downloadBtn) downloadBtn.style.display = "none";
|
||||
|
||||
// Show loading
|
||||
modalContent.innerHTML =
|
||||
'<div class="collection-loading">Loading collection...</div>';
|
||||
|
||||
// Load collections data if not cached
|
||||
if (!collectionsCache) {
|
||||
collectionsCache = await fetchData<CollectionsData>("collections.json");
|
||||
// Replace <pre> with a <div> so plugin content isn't styled as preformatted text
|
||||
const modalBody = modalContent.parentElement;
|
||||
if (modalBody) {
|
||||
const div = document.createElement("div");
|
||||
div.id = "modal-content";
|
||||
div.innerHTML = '<div class="collection-loading">Loading plugin...</div>';
|
||||
modalBody.replaceChild(div, modalContent);
|
||||
modalContent = div;
|
||||
} else {
|
||||
modalContent.innerHTML = '<div class="collection-loading">Loading plugin...</div>';
|
||||
}
|
||||
|
||||
if (!collectionsCache) {
|
||||
// Load plugins data if not cached
|
||||
if (!pluginsCache) {
|
||||
pluginsCache = await fetchData<PluginsData>("plugins.json");
|
||||
}
|
||||
|
||||
if (!pluginsCache) {
|
||||
modalContent.innerHTML =
|
||||
'<div class="collection-error">Failed to load collection data.</div>';
|
||||
'<div class="collection-error">Failed to load plugin data.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the collection
|
||||
const collection = collectionsCache.items.find((c) => c.path === filePath);
|
||||
if (!collection) {
|
||||
// Find the plugin
|
||||
const plugin = pluginsCache.items.find((c) => c.path === filePath);
|
||||
if (!plugin) {
|
||||
modalContent.innerHTML =
|
||||
'<div class="collection-error">Collection not found.</div>';
|
||||
'<div class="collection-error">Plugin not found.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Update title
|
||||
title.textContent = collection.name;
|
||||
title.textContent = plugin.name;
|
||||
|
||||
// Render collection view
|
||||
// Render plugin view
|
||||
modalContent.innerHTML = `
|
||||
<div class="collection-view">
|
||||
<div class="collection-description">${escapeHtml(
|
||||
collection.description || ""
|
||||
plugin.description || ""
|
||||
)}</div>
|
||||
${
|
||||
collection.tags && collection.tags.length > 0
|
||||
plugin.tags && plugin.tags.length > 0
|
||||
? `
|
||||
<div class="collection-tags">
|
||||
${collection.tags
|
||||
${plugin.tags
|
||||
.map((t) => `<span class="resource-tag">${escapeHtml(t)}</span>`)
|
||||
.join("")}
|
||||
</div>
|
||||
@@ -451,10 +466,10 @@ async function openCollectionModal(
|
||||
: ""
|
||||
}
|
||||
<div class="collection-items-header">
|
||||
<strong>${collection.items.length} items in this collection</strong>
|
||||
<strong>${plugin.items.length} items in this plugin</strong>
|
||||
</div>
|
||||
<div class="collection-items-list">
|
||||
${collection.items
|
||||
${plugin.items
|
||||
.map(
|
||||
(item) => `
|
||||
<div class="collection-item" data-path="${escapeHtml(
|
||||
@@ -484,7 +499,7 @@ async function openCollectionModal(
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add click handlers to collection items
|
||||
// Add click handlers to plugin items
|
||||
modalContent.querySelectorAll(".collection-item").forEach((el) => {
|
||||
el.addEventListener("click", () => {
|
||||
const path = (el as HTMLElement).dataset.path;
|
||||
|
||||
@@ -12,12 +12,12 @@ interface Manifest {
|
||||
instructions: number;
|
||||
skills: number;
|
||||
hooks: number;
|
||||
collections: number;
|
||||
plugins: number;
|
||||
tools: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface Collection {
|
||||
interface Plugin {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
@@ -27,8 +27,8 @@ interface Collection {
|
||||
itemCount: number;
|
||||
}
|
||||
|
||||
interface CollectionsData {
|
||||
items: Collection[];
|
||||
interface PluginsData {
|
||||
items: Plugin[];
|
||||
}
|
||||
|
||||
export async function initHomepage(): Promise<void> {
|
||||
@@ -36,7 +36,7 @@ export async function initHomepage(): Promise<void> {
|
||||
const manifest = await fetchData<Manifest>('manifest.json');
|
||||
if (manifest && manifest.counts) {
|
||||
// Populate counts in cards
|
||||
const countKeys = ['agents', 'prompts', 'instructions', 'skills', 'hooks', 'collections', 'tools'] as const;
|
||||
const countKeys = ['agents', 'prompts', 'instructions', 'skills', 'hooks', 'plugins', 'tools'] as const;
|
||||
countKeys.forEach(key => {
|
||||
const countEl = document.querySelector(`.card-count[data-count="${key}"]`);
|
||||
if (countEl && manifest.counts[key] !== undefined) {
|
||||
@@ -97,11 +97,11 @@ export async function initHomepage(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Load featured collections
|
||||
const collectionsData = await fetchData<CollectionsData>('collections.json');
|
||||
if (collectionsData && collectionsData.items) {
|
||||
const featured = collectionsData.items.filter(c => c.featured).slice(0, 6);
|
||||
const featuredEl = document.getElementById('featured-collections');
|
||||
// Load featured plugins
|
||||
const pluginsData = await fetchData<PluginsData>('plugins.json');
|
||||
if (pluginsData && pluginsData.items) {
|
||||
const featured = pluginsData.items.filter(c => c.featured).slice(0, 6);
|
||||
const featuredEl = document.getElementById('featured-plugins');
|
||||
if (featuredEl) {
|
||||
if (featured.length > 0) {
|
||||
featuredEl.innerHTML = featured.map(c => `
|
||||
@@ -119,11 +119,11 @@ export async function initHomepage(): Promise<void> {
|
||||
featuredEl.querySelectorAll('.card').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const path = (el as HTMLElement).dataset.path;
|
||||
if (path) openFileModal(path, 'collection');
|
||||
if (path) openFileModal(path, 'plugin');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
featuredEl.innerHTML = '<p style="text-align: center; color: var(--color-text-muted);">No featured collections yet</p>';
|
||||
featuredEl.innerHTML = '<p style="text-align: center; color: var(--color-text-muted);">No featured plugins yet</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Collections page functionality
|
||||
* Plugins page functionality
|
||||
*/
|
||||
import { createChoices, getChoicesValues, type Choices } from '../choices';
|
||||
import { FuzzySearch, SearchItem } from '../search';
|
||||
import { fetchData, debounce, escapeHtml, getGitHubUrl } from '../utils';
|
||||
import { setupModal, openFileModal } from '../modal';
|
||||
|
||||
interface Collection extends SearchItem {
|
||||
interface Plugin extends SearchItem {
|
||||
id: string;
|
||||
name: string;
|
||||
path: string;
|
||||
@@ -15,16 +15,16 @@ interface Collection extends SearchItem {
|
||||
itemCount: number;
|
||||
}
|
||||
|
||||
interface CollectionsData {
|
||||
items: Collection[];
|
||||
interface PluginsData {
|
||||
items: Plugin[];
|
||||
filters: {
|
||||
tags: string[];
|
||||
};
|
||||
}
|
||||
|
||||
const resourceType = 'collection';
|
||||
let allItems: Collection[] = [];
|
||||
let search = new FuzzySearch<Collection>();
|
||||
const resourceType = 'plugin';
|
||||
let allItems: Plugin[] = [];
|
||||
let search = new FuzzySearch<Plugin>();
|
||||
let tagSelect: Choices;
|
||||
let currentFilters = {
|
||||
tags: [] as string[],
|
||||
@@ -49,19 +49,19 @@ function applyFiltersAndRender(): void {
|
||||
const activeFilters: string[] = [];
|
||||
if (currentFilters.tags.length > 0) activeFilters.push(`${currentFilters.tags.length} tag${currentFilters.tags.length > 1 ? 's' : ''}`);
|
||||
if (currentFilters.featured) activeFilters.push('featured');
|
||||
let countText = `${results.length} of ${allItems.length} collections`;
|
||||
let countText = `${results.length} of ${allItems.length} plugins`;
|
||||
if (activeFilters.length > 0) {
|
||||
countText += ` (filtered by ${activeFilters.join(', ')})`;
|
||||
}
|
||||
if (countEl) countEl.textContent = countText;
|
||||
}
|
||||
|
||||
function renderItems(items: Collection[], query = ''): void {
|
||||
function renderItems(items: Plugin[], query = ''): void {
|
||||
const list = document.getElementById('resource-list');
|
||||
if (!list) return;
|
||||
|
||||
if (items.length === 0) {
|
||||
list.innerHTML = '<div class="empty-state"><h3>No collections found</h3><p>Try a different search term or adjust filters</p></div>';
|
||||
list.innerHTML = '<div class="empty-state"><h3>No plugins found</h3><p>Try a different search term or adjust filters</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,13 +91,13 @@ function renderItems(items: Collection[], query = ''): void {
|
||||
});
|
||||
}
|
||||
|
||||
export async function initCollectionsPage(): Promise<void> {
|
||||
export async function initPluginsPage(): Promise<void> {
|
||||
const list = document.getElementById('resource-list');
|
||||
const searchInput = document.getElementById('search-input') as HTMLInputElement;
|
||||
const featuredCheckbox = document.getElementById('filter-featured') as HTMLInputElement;
|
||||
const clearFiltersBtn = document.getElementById('clear-filters');
|
||||
|
||||
const data = await fetchData<CollectionsData>('collections.json');
|
||||
const data = await fetchData<PluginsData>('plugins.json');
|
||||
if (!data || !data.items) {
|
||||
if (list) list.innerHTML = '<div class="empty-state"><h3>Failed to load data</h3></div>';
|
||||
return;
|
||||
@@ -105,7 +105,7 @@ export async function initCollectionsPage(): Promise<void> {
|
||||
|
||||
allItems = data.items;
|
||||
|
||||
// Map collection items to search items
|
||||
// Map plugin items to search items
|
||||
const searchItems = allItems.map(item => ({
|
||||
...item,
|
||||
title: item.name,
|
||||
@@ -140,4 +140,4 @@ export async function initCollectionsPage(): Promise<void> {
|
||||
}
|
||||
|
||||
// Auto-initialize when DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', initCollectionsPage);
|
||||
document.addEventListener('DOMContentLoaded', initPluginsPage);
|
||||
@@ -233,7 +233,7 @@ export function getResourceType(filePath: string): string {
|
||||
return "skill";
|
||||
if (/(^|\/)hooks\//.test(filePath) && filePath.endsWith("README.md"))
|
||||
return "hook";
|
||||
if (filePath.endsWith(".collection.yml")) return "collection";
|
||||
if (filePath.endsWith(".collection.yml")) return "plugin";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ export function formatResourceType(type: string): string {
|
||||
instruction: "📋 Instruction",
|
||||
skill: "⚡ Skill",
|
||||
hook: "🪝 Hook",
|
||||
collection: "📦 Collection",
|
||||
plugin: "🔌 Plugin",
|
||||
};
|
||||
return labels[type] || type;
|
||||
}
|
||||
@@ -262,7 +262,7 @@ export function getResourceIcon(type: string): string {
|
||||
instruction: "📋",
|
||||
skill: "⚡",
|
||||
hook: "🪝",
|
||||
collection: "📦",
|
||||
plugin: "🔌",
|
||||
};
|
||||
return icons[type] || "📄";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user