mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-07 01:28:46 +00:00
a7fdcd5006
* feat: migrate plugins and extensions to Agent Plugins v1.0.0 spec - Add \ to all 69 curated plugin manifests - Migrate all 18 extension manifests: add \, move logo into xtensions.com.github.copilot.logo namespace, remove top-level logo and string xtensions: '.' - Update eng/validate-plugins.mjs: require \, validate namespace-keyed extensions object for canvas extensions, widen name pattern to allow dots (spec §5.5, max 64 chars) - Update eng/materialize-plugins.mjs: emit spec-clean served manifests (only spec fields: \, name, version, description, author, homepage, repository, license, keywords, extensions) - Update eng/generate-website-data.mjs: read logo from namespace with fallback to top-level logo for compatibility - Update eng/create-plugin.mjs: scaffold emits \ - Add .github/workflows/validate-plugins.yml: blocking CI for PRs touching plugins/** or extensions/** - Add spec compliance check to external plugin quality gates: non-blocking warnings with ✅/⚠️/🛑 emoji legend - Update AGENTS.md: document new extension manifest shape, add \ to plugin checklist Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76 * refactor: consolidate canvas extension plugins - Move all extension plugin manifests from extensions/<name> to plugins/<name> - Keep extensions/<name> as reusable source only - Remove standalone extension discovery from marketplace and website plugin catalogs - Auto-bundle same-name extension sources during materialization - Add build-only extensions.json references for sharing extensions across plugins - Remove x-awesome-copilot extension metadata support - Update validation and contributor documentation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76 * feat: add canvas extension scaffolding skill - Add repo-local skill for creating canvas extension sources - Generate spec-compliant plugin manifests under plugins/ - Support registering reusable extensions with multiple plugins - Remove guidance for extension-local plugin manifests and custom fields Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76 * fix: align extension namespaces with current guidance - Use each extension ID as its manifest namespace key - Update validation and website generation to resolve extension-specific namespaces - Upsert plugin validation PR comments using the existing repository pattern Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76 * fix: use Copilot extension namespace - Adopt com.github.copilot for all canvas extension manifests - Require the namespace during validation and website generation - Update extension scaffolding guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76 * docs: regenerate plugin catalog after merge Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76 * refactor(plugins): move manifests to plugin roots Use root plugin.json manifests and namespaced extension directories throughout local tooling, validation, generation, and contributor documentation. Restore materialize-plugins.mjs line breaks so the source remains readable in GitHub. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76 * feat(plugins): migrate manifests to namespaced composition Move repository composition metadata under com.github.awesome-copilot, materialize reusable extensions into the plugin extensions directory, and improve contributor and PR validation guidance. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76 * fix(validation): address plugin review findings Restore executable build scripts, validate namespaced manifests and hook directories, improve README item counts, and manage validation comments across reruns. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76 * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Copilot-Session: 8f3a88cb-e01e-4760-8125-460490dc1a76
209 lines
7.3 KiB
JavaScript
209 lines
7.3 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
import { ROOT_FOLDER } from "./constants.mjs";
|
|
|
|
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
|
|
const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions");
|
|
const COPILOT_NAMESPACE = "com.github.copilot";
|
|
const AWESOME_COPILOT_NAMESPACE = "com.github.awesome-copilot";
|
|
|
|
/**
|
|
* Recursively copy a directory.
|
|
*/
|
|
function copyDirRecursive(src, dest) {
|
|
fs.mkdirSync(dest, { recursive: true });
|
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
const srcPath = path.join(src, entry.name);
|
|
const destPath = path.join(dest, entry.name);
|
|
if (entry.isDirectory()) {
|
|
copyDirRecursive(srcPath, destPath);
|
|
} else {
|
|
fs.copyFileSync(srcPath, destPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve a plugin-relative path to the repo-root source file.
|
|
*
|
|
* ./agents/foo.md → ROOT/agents/foo.agent.md
|
|
* ./skills/baz/ → ROOT/skills/baz/
|
|
*/
|
|
function resolveSource(relPath) {
|
|
const basename = path.basename(relPath, ".md");
|
|
if (relPath.startsWith("./agents/")) {
|
|
return path.join(ROOT_FOLDER, "agents", `${basename}.agent.md`);
|
|
}
|
|
if (relPath.startsWith("./skills/")) {
|
|
// Strip trailing slash and get the skill folder name
|
|
const skillName = relPath.replace(/^\.\/skills\//, "").replace(/\/$/, "");
|
|
return path.join(ROOT_FOLDER, "skills", skillName);
|
|
}
|
|
if (relPath.startsWith("./extensions/")) {
|
|
const extensionName = relPath.replace(/^\.\/extensions\//, "").replace(/\/$/, "");
|
|
return path.join(ROOT_FOLDER, "extensions", extensionName);
|
|
}
|
|
if (relPath.startsWith("./hooks/")) {
|
|
return path.join(ROOT_FOLDER, "hooks", relPath.replace(/^\.\/hooks\//, ""));
|
|
}
|
|
if (relPath.startsWith("./commands/")) {
|
|
return path.join(ROOT_FOLDER, "commands", relPath.replace(/^\.\/commands\//, ""));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function readExtensionReferences(metadata, pluginName) {
|
|
const extensionData = metadata.extensions?.[AWESOME_COPILOT_NAMESPACE];
|
|
const directories = extensionData?.extensions ?? [];
|
|
if (!Array.isArray(directories) ||
|
|
directories.some((entry) => typeof entry !== "string" || !entry.startsWith("./extensions/"))) {
|
|
throw new Error(`extensions["${AWESOME_COPILOT_NAMESPACE}"].extensions must contain plugin-relative paths`);
|
|
}
|
|
|
|
const names = new Set(directories.map((entry) =>
|
|
entry.replace(/^\.\/extensions\//, "").replace(/\/$/, "")
|
|
));
|
|
if (fs.existsSync(path.join(EXTENSIONS_DIR, pluginName, "extension.mjs"))) {
|
|
names.add(pluginName);
|
|
}
|
|
|
|
return [...names].sort();
|
|
}
|
|
|
|
export function materializePlugins() {
|
|
console.log("Materializing plugin files...\n");
|
|
|
|
if (!fs.existsSync(PLUGINS_DIR)) {
|
|
console.error(`Error: Plugins directory not found at ${PLUGINS_DIR}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
const pluginDirs = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true })
|
|
.filter(entry => entry.isDirectory())
|
|
.map(entry => entry.name)
|
|
.sort();
|
|
|
|
let totalAgents = 0;
|
|
let totalSkills = 0;
|
|
let totalExtensions = 0;
|
|
let warnings = 0;
|
|
let errors = 0;
|
|
|
|
for (const dirName of pluginDirs) {
|
|
const pluginPath = path.join(PLUGINS_DIR, dirName);
|
|
const pluginJsonPath = path.join(pluginPath, "plugin.json");
|
|
|
|
if (!fs.existsSync(pluginJsonPath)) {
|
|
continue;
|
|
}
|
|
|
|
let metadata;
|
|
try {
|
|
metadata = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
|
|
} catch (err) {
|
|
console.error(`Error: Failed to parse ${pluginJsonPath}: ${err.message}`);
|
|
errors++;
|
|
continue;
|
|
}
|
|
|
|
const pluginName = metadata.name || dirName;
|
|
|
|
const composition = metadata.extensions?.[AWESOME_COPILOT_NAMESPACE] ?? {};
|
|
|
|
// Process repository composition fields.
|
|
for (const field of ["agents", "commands", "hooks", "skills"]) {
|
|
const entries = composition[field];
|
|
if (!Array.isArray(entries)) continue;
|
|
for (const relPath of entries) {
|
|
const src = resolveSource(relPath);
|
|
if (!src) {
|
|
console.warn(` ⚠ ${pluginName}: Unknown ${field} path format: ${relPath}`);
|
|
warnings++;
|
|
continue;
|
|
}
|
|
if (!fs.existsSync(src)) {
|
|
console.warn(` ⚠ ${pluginName}: ${field} source not found: ${src}`);
|
|
warnings++;
|
|
continue;
|
|
}
|
|
const dest = path.join(pluginPath, relPath.replace(/^\.\//, "").replace(/\/$/, ""));
|
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
if (fs.statSync(src).isDirectory()) copyDirRecursive(src, dest);
|
|
else fs.copyFileSync(src, dest);
|
|
if (field === "agents") totalAgents++;
|
|
if (field === "skills") totalSkills++;
|
|
}
|
|
}
|
|
|
|
// Process reusable extensions declared in the repository namespace.
|
|
const extensionRefs = readExtensionReferences(metadata, pluginName);
|
|
for (const extensionName of extensionRefs) {
|
|
const relPath = `./extensions/${extensionName}`;
|
|
const src = resolveSource(relPath);
|
|
if (!src) {
|
|
console.warn(` ⚠ ${pluginName}: Unknown extension path format: ${relPath}`);
|
|
warnings++;
|
|
continue;
|
|
}
|
|
if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) {
|
|
console.warn(` ⚠ ${pluginName}: Extension source directory not found: ${src}`);
|
|
warnings++;
|
|
continue;
|
|
}
|
|
// Extensions are conventional plugin content and belong under the
|
|
// plugin's top-level extensions directory, not the client namespace.
|
|
const dest = path.join(pluginPath, "extensions", extensionName);
|
|
copyDirRecursive(src, dest);
|
|
totalExtensions++;
|
|
}
|
|
|
|
// Emit a spec-compliant served manifest for the marketplace branch.
|
|
// Source manifests keep composition fields (agents and skills)
|
|
// for build tooling. The served manifest retains only Agent Plugins v1.0.0 fields
|
|
// so the runtime uses conventional directory discovery for all content.
|
|
const SPEC_FIELDS = new Set(["$schema", "name", "version", "description", "author",
|
|
"homepage", "repository", "license", "keywords", "extensions"]);
|
|
const AGENT_PLUGINS_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
|
|
|
|
const served = { "$schema": AGENT_PLUGINS_SCHEMA };
|
|
for (const [key, val] of Object.entries(metadata)) {
|
|
if (SPEC_FIELDS.has(key) && key !== "$schema") {
|
|
if (key === "extensions") {
|
|
const copilot = val?.[COPILOT_NAMESPACE];
|
|
if (copilot) {
|
|
served.extensions = { [COPILOT_NAMESPACE]: { ...copilot } };
|
|
}
|
|
} else {
|
|
served[key] = val;
|
|
}
|
|
}
|
|
}
|
|
|
|
fs.writeFileSync(pluginJsonPath, JSON.stringify(served, null, 2) + "\n", "utf8");
|
|
|
|
const counts = [];
|
|
if (composition.agents?.length) counts.push(`${composition.agents.length} agents`);
|
|
if (composition.skills?.length) counts.push(`${composition.skills.length} skills`);
|
|
if (extensionRefs.length) counts.push(`${extensionRefs.length} extensions`);
|
|
if (counts.length) {
|
|
console.log(`✓ ${pluginName}: ${counts.join(", ")}`);
|
|
}
|
|
}
|
|
|
|
console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} extensions.`);
|
|
if (warnings > 0) {
|
|
console.log(`${warnings} warning(s).`);
|
|
}
|
|
if (errors > 0) {
|
|
console.error(`${errors} error(s).`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
materializePlugins();
|
|
}
|