fix(plugins): namespace Copilot materialized content (#2643)

Place Copilot-specific content in com.github.copilot and remove unsupported command handling.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 04c14c3f-d248-4a7f-93ab-93fd8b2b119e
This commit is contained in:
Aaron Powell
2026-08-13 11:24:06 +10:00
committed by GitHub
parent 0a6e37e4e2
commit 55b952d2f9
7 changed files with 78 additions and 55 deletions
+56 -28
View File
@@ -7,25 +7,35 @@ import { ROOT_FOLDER } from "./constants.mjs";
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions");
const COPILOT_CONTENT_DIR = "com.github.copilot";
const AWESOME_COPILOT_NAMESPACE = "com.github.awesome-copilot";
const MATERIALIZED_SPECS = {
agents: {
path: "agents",
paths: [path.join(COPILOT_CONTENT_DIR, "agents"), "agents"],
restore(dirPath) {
return collectFiles(dirPath).map((relativePath) => `./agents/${relativePath}`);
},
},
commands: {
path: "commands",
hooks: {
paths: [path.join(COPILOT_CONTENT_DIR, "hooks"), "hooks"],
restore(dirPath) {
return collectFiles(dirPath).map((relativePath) => `./commands/${relativePath}`);
return collectDirectoriesContainingFile(dirPath, "hooks.json")
.map((relativePath) => `./hooks/${relativePath}/`);
},
},
skills: {
path: "skills",
paths: ["skills"],
restore(dirPath) {
return collectSkillDirectories(dirPath).map((relativePath) => `./skills/${relativePath}/`);
},
},
extensions: {
paths: [path.join(COPILOT_CONTENT_DIR, "extensions"), "extensions"],
restore(dirPath) {
return collectDirectoriesContainingFile(dirPath, "extension.mjs")
.map((relativePath) => `./extensions/${relativePath}`);
},
},
};
function copyDirRecursive(src, dest) {
@@ -78,22 +88,19 @@ export function restoreManifestFromMaterializedFiles(pluginPath) {
let changed = false;
for (const [field, spec] of Object.entries(MATERIALIZED_SPECS)) {
if (Array.isArray(plugin[field])) {
const sortedEntries = sortPluginEntries(plugin[field]);
if (!arraysEqual(plugin[field], sortedEntries)) {
plugin[field] = sortedEntries;
changed = true;
}
}
const materializedPath = path.join(pluginPath, spec.path);
if (!fs.existsSync(materializedPath) || !fs.statSync(materializedPath).isDirectory()) {
const materializedPath = spec.paths
.map((subdir) => path.join(pluginPath, subdir))
.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isDirectory());
if (!materializedPath) {
continue;
}
const restored = spec.restore(materializedPath);
if (!arraysEqual(plugin[field], restored)) {
plugin[field] = restored;
const composition = plugin.extensions?.[AWESOME_COPILOT_NAMESPACE];
if (!arraysEqual(composition?.[field], restored)) {
plugin.extensions ??= {};
plugin.extensions[AWESOME_COPILOT_NAMESPACE] ??= {};
plugin.extensions[AWESOME_COPILOT_NAMESPACE][field] = restored;
changed = true;
}
}
@@ -112,16 +119,23 @@ function cleanPlugin(pluginPath) {
}
let removed = 0;
for (const { path: subdir } of Object.values(MATERIALIZED_SPECS)) {
const target = path.join(pluginPath, subdir);
if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {
const count = countFiles(target);
fs.rmSync(target, { recursive: true, force: true });
removed += count;
console.log(` Removed ${path.basename(pluginPath)}/${subdir}/ (${count} files)`);
for (const { paths } of Object.values(MATERIALIZED_SPECS)) {
for (const subdir of paths) {
const target = path.join(pluginPath, subdir);
if (fs.existsSync(target) && fs.statSync(target).isDirectory()) {
const count = countFiles(target);
fs.rmSync(target, { recursive: true, force: true });
removed += count;
console.log(` Removed ${path.basename(pluginPath)}/${subdir}/ (${count} files)`);
}
}
}
const copilotContentPath = path.join(pluginPath, COPILOT_CONTENT_DIR);
if (fs.existsSync(copilotContentPath) && fs.readdirSync(copilotContentPath).length === 0) {
fs.rmdirSync(copilotContentPath);
}
return { removed, manifestUpdated };
}
@@ -230,6 +244,24 @@ function collectSkillDirectories(dir, rootDir = dir) {
return skillDirs.sort();
}
function collectDirectoriesContainingFile(dir, fileName, rootDir = dir) {
const directories = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const entryPath = path.join(dir, entry.name);
if (fs.existsSync(path.join(entryPath, fileName))) {
directories.push(toPosixPath(path.relative(rootDir, entryPath)));
continue;
}
directories.push(...collectDirectoriesContainingFile(entryPath, fileName, rootDir));
}
return directories.sort();
}
function arraysEqual(left, right) {
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
return false;
@@ -238,10 +270,6 @@ function arraysEqual(left, right) {
return left.every((value, index) => value === right[index]);
}
function sortPluginEntries(entries) {
return [...entries].sort((left, right) => left.localeCompare(right));
}
function toPosixPath(filePath) {
return filePath.split(path.sep).join("/");
}
+1 -2
View File
@@ -618,10 +618,9 @@ function generatePluginsData(gitDates, resourceIndex = {}) {
}
}
// Build items list from spec fields (agents, commands, skills, mcpServers)
// Build items list from supported composition fields.
const items = [
...agentItems,
...(composition.commands || []).map((p) => ({ kind: "prompt", path: p })),
...(composition.skills || []).map((p) => ({ kind: "skill", path: p })),
...extensionItems,
...mcpItems,
+11 -11
View File
@@ -9,6 +9,7 @@ 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";
const COPILOT_CONTENT_DIR = COPILOT_NAMESPACE;
/**
* Recursively copy a directory.
@@ -49,9 +50,6 @@ function resolveSource(relPath) {
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;
}
@@ -114,7 +112,7 @@ export function materializePlugins() {
const composition = metadata.extensions?.[AWESOME_COPILOT_NAMESPACE] ?? {};
// Process repository composition fields.
for (const field of ["agents", "commands", "hooks", "skills"]) {
for (const field of ["agents", "hooks", "skills"]) {
const entries = composition[field];
if (!Array.isArray(entries)) continue;
for (const relPath of entries) {
@@ -129,7 +127,10 @@ export function materializePlugins() {
warnings++;
continue;
}
const dest = path.join(pluginPath, relPath.replace(/^\.\//, "").replace(/\/$/, ""));
const relativeDestination = relPath.replace(/^\.\//, "").replace(/\/$/, "");
const dest = field === "skills"
? path.join(pluginPath, relativeDestination)
: path.join(pluginPath, COPILOT_CONTENT_DIR, relativeDestination);
fs.mkdirSync(path.dirname(dest), { recursive: true });
if (fs.statSync(src).isDirectory()) copyDirRecursive(src, dest);
else fs.copyFileSync(src, dest);
@@ -153,17 +154,16 @@ export function materializePlugins() {
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);
const dest = path.join(pluginPath, COPILOT_CONTENT_DIR, "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.
// Source manifests keep repository composition fields for build tooling.
// The served manifest retains only Agent Plugins v1.0.0 fields; standard
// skills are discovered from skills/, while Copilot-specific content is
// discovered from com.github.copilot/.
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";
-2
View File
@@ -800,7 +800,6 @@ function generatePluginsSection(pluginsDir) {
: 0;
const itemCount =
(composition.agents || []).length +
(composition.commands || []).length +
(composition.skills || []).length +
extensionReferences +
implicitExtension;
@@ -869,7 +868,6 @@ function generateFeaturedPluginsSection(pluginsDir) {
: 0;
const itemCount =
(composition.agents || []).length +
(composition.commands || []).length +
(composition.skills || []).length +
extensionReferences +
implicitExtension;
+3 -4
View File
@@ -120,7 +120,6 @@ function validateSpecPaths(plugin) {
const errors = [];
const specs = {
agents: { prefix: "./agents/", suffix: ".md", repoDir: "agents", repoSuffix: ".agent.md" },
commands: { prefix: "./commands/", suffix: ".md", repoDir: "commands", repoSuffix: ".md" },
hooks: { prefix: "./hooks/", suffix: "/", repoDir: "hooks", repoFile: "README.md" },
skills: { prefix: "./skills/", suffix: "/", repoDir: "skills", repoFile: "SKILL.md" },
};
@@ -215,7 +214,7 @@ function validateExtensionReferences(plugin, pluginDir) {
function validateCompositionNamespace(plugin) {
const errors = [];
const compositionFields = ["agents", "commands", "hooks", "mcpServers", "skills"];
const compositionFields = ["agents", "hooks", "mcpServers", "skills"];
const extensions = plugin.extensions;
const composition = extensions?.[AWESOME_COPILOT_NAMESPACE];
@@ -291,7 +290,7 @@ function validatePlugin(folderName) {
// Rule 5b: license (shared with external plugins). Non-SPDX is a warning, not an error.
const warnings = [];
for (const field of ["agents", "commands", "hooks", "mcpServers", "skills"]) {
for (const field of ["agents", "hooks", "mcpServers", "skills"]) {
if (plugin[field] !== undefined) {
errors.push(`${field} must be moved to extensions["${AWESOME_COPILOT_NAMESPACE}"].${field}`);
}
@@ -301,7 +300,7 @@ function validatePlugin(folderName) {
errors.push(...licenseResult.errors);
warnings.push(...licenseResult.warnings);
// Rule 6: agents, commands, skills paths
// Rule 6: agents, hooks, and skills paths
const specErrors = validateSpecPaths(plugin);
errors.push(...specErrors);