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:
@@ -91,9 +91,9 @@ try {
|
||||
class:list={[{ active: activeNav === "hooks" }]}>Hooks</a
|
||||
>
|
||||
<a
|
||||
href={`${base}collections/`}
|
||||
class:list={[{ active: activeNav === "collections" }]}
|
||||
>Collections</a
|
||||
href={`${base}plugins/`}
|
||||
class:list={[{ active: activeNav === "plugins" }]}
|
||||
>Plugins</a
|
||||
>
|
||||
<a
|
||||
href={`${base}tools/`}
|
||||
|
||||
@@ -65,13 +65,13 @@ const base = import.meta.env.BASE_URL;
|
||||
</div>
|
||||
<div class="card-count" data-count="hooks" aria-label="Hook count">-</div>
|
||||
</a>
|
||||
<a href={`${base}collections/`} class="card card-with-count" id="card-collections">
|
||||
<div class="card-icon" aria-hidden="true">📦</div>
|
||||
<a href={`${base}plugins/`} class="card card-with-count" id="card-plugins">
|
||||
<div class="card-icon" aria-hidden="true">🔌</div>
|
||||
<div class="card-content">
|
||||
<h3>Collections</h3>
|
||||
<p>Curated collections organized by themes</p>
|
||||
<h3>Plugins</h3>
|
||||
<p>Curated plugins organized by themes</p>
|
||||
</div>
|
||||
<div class="card-count" data-count="collections" aria-label="Collection count">-</div>
|
||||
<div class="card-count" data-count="plugins" aria-label="Plugin count">-</div>
|
||||
</a>
|
||||
<a href={`${base}tools/`} class="card card-with-count" id="card-tools">
|
||||
<div class="card-icon" aria-hidden="true">🔧</div>
|
||||
@@ -85,11 +85,11 @@ const base = import.meta.env.BASE_URL;
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Featured Collections -->
|
||||
<!-- Featured Plugins -->
|
||||
<section class="featured" aria-labelledby="featured-heading">
|
||||
<div class="container">
|
||||
<h2 id="featured-heading">Featured Collections</h2>
|
||||
<div id="featured-collections" class="cards-grid" aria-live="polite">
|
||||
<h2 id="featured-heading">Featured Plugins</h2>
|
||||
<div id="featured-plugins" class="cards-grid" aria-live="polite">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,20 +3,20 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import Modal from '../components/Modal.astro';
|
||||
---
|
||||
|
||||
<BaseLayout title="Collections" description="Curated collections of prompts, instructions, and agents for specific workflows" activeNav="collections">
|
||||
<BaseLayout title="Plugins" description="Curated plugins of prompts, agents, and skills for specific workflows" activeNav="plugins">
|
||||
<main id="main-content">
|
||||
<div class="page-header">
|
||||
<div class="container">
|
||||
<h1>📦 Collections</h1>
|
||||
<p>Curated collections of prompts, instructions, and agents for specific workflows</p>
|
||||
<h1>🔌 Plugins</h1>
|
||||
<p>Curated plugins of prompts, agents, and skills for specific workflows</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<div class="container">
|
||||
<div class="search-bar">
|
||||
<label for="search-input" class="sr-only">Search collections</label>
|
||||
<input type="text" id="search-input" placeholder="Search collections..." autocomplete="off">
|
||||
<label for="search-input" class="sr-only">Search plugins</label>
|
||||
<input type="text" id="search-input" placeholder="Search plugins..." autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="filters-bar" id="filters-bar">
|
||||
@@ -35,7 +35,7 @@ import Modal from '../components/Modal.astro';
|
||||
|
||||
<div class="results-count" id="results-count" aria-live="polite"></div>
|
||||
<div class="resource-list" id="resource-list" role="list">
|
||||
<div class="loading" aria-live="polite">Loading collections...</div>
|
||||
<div class="loading" aria-live="polite">Loading plugins...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -44,6 +44,6 @@ import Modal from '../components/Modal.astro';
|
||||
<Modal />
|
||||
|
||||
<script>
|
||||
import '../scripts/pages/collections';
|
||||
import '../scripts/pages/plugins';
|
||||
</script>
|
||||
</BaseLayout>
|
||||
@@ -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