mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-18 11:50:00 +00:00
26fe2d126b
* Migrate extension plugin materialization layout Materialize extension plugins into a dedicated extensions/ container, validate the new manifest convention, and bump extension plugin versions. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Keep extension manifests source-compatible Restore source extension manifests to "extensions": "." while preserving materialization-time rewrite to "extensions" in distribution output. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Validate canvas extension layout for external submissions Add intake and quality-gate checks for canvas-tagged external plugins so they must include extensions/extension.mjs and optional manifest extensions is validated when present. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Fix plugin clean extension pass and typo guard text Declare EXTENSIONS_DIR in clean-materialized-plugins and run extension cleanup once after plugin cleanup. Also normalize misspelled-key detection strings to satisfy spelling checks without changing validation behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Proper codespell fix * Separate canvas structure quality gate status Track canvas structure as its own gate status and output, include it in aggregate summaries, and enforce Git object types so extensions/ is a tree and extensions/extension.mjs is a blob. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Move extension bundles during materialization Change extension-plugin materialization to move root bundle entries into extensions/ instead of copying them, and assert originals are removed in test coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Nest materialized extension bundles by plugin name Materialize extension plugins into extensions/<plugin-name>/... (moved entries) so resulting paths are duplicated by design, and bump extension plugin versions to the next patch release. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Fix extension materialization publish and cleanup Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1 * Preserve root extension logo asset Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1
305 lines
9.0 KiB
JavaScript
305 lines
9.0 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 MATERIALIZED_SPECS = {
|
|
agents: {
|
|
path: "agents",
|
|
restore(dirPath) {
|
|
return collectFiles(dirPath).map((relativePath) => `./agents/${relativePath}`);
|
|
},
|
|
},
|
|
commands: {
|
|
path: "commands",
|
|
restore(dirPath) {
|
|
return collectFiles(dirPath).map((relativePath) => `./commands/${relativePath}`);
|
|
},
|
|
},
|
|
skills: {
|
|
path: "skills",
|
|
restore(dirPath) {
|
|
return collectSkillDirectories(dirPath).map((relativePath) => `./skills/${relativePath}/`);
|
|
},
|
|
},
|
|
};
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
function moveEntry(srcPath, destPath) {
|
|
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
try {
|
|
fs.renameSync(srcPath, destPath);
|
|
return;
|
|
} catch (error) {
|
|
if (!["EXDEV", "EEXIST", "ENOTEMPTY", "EPERM"].includes(error?.code)) {
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const stats = fs.statSync(srcPath);
|
|
if (stats.isDirectory()) {
|
|
copyDirRecursive(srcPath, destPath);
|
|
fs.rmSync(srcPath, { recursive: true, force: true });
|
|
return;
|
|
}
|
|
|
|
fs.copyFileSync(srcPath, destPath);
|
|
fs.rmSync(srcPath, { force: true });
|
|
}
|
|
|
|
export function restoreManifestFromMaterializedFiles(pluginPath) {
|
|
const pluginJsonPath = path.join(pluginPath, ".github/plugin", "plugin.json");
|
|
if (!fs.existsSync(pluginJsonPath)) {
|
|
return false;
|
|
}
|
|
|
|
let plugin;
|
|
try {
|
|
plugin = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
|
|
} catch (error) {
|
|
throw new Error(`Failed to parse ${pluginJsonPath}: ${error.message}`);
|
|
}
|
|
|
|
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()) {
|
|
continue;
|
|
}
|
|
|
|
const restored = spec.restore(materializedPath);
|
|
if (!arraysEqual(plugin[field], restored)) {
|
|
plugin[field] = restored;
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
if (changed) {
|
|
fs.writeFileSync(pluginJsonPath, JSON.stringify(plugin, null, 2) + "\n", "utf8");
|
|
}
|
|
|
|
return changed;
|
|
}
|
|
|
|
function cleanPlugin(pluginPath) {
|
|
const manifestUpdated = restoreManifestFromMaterializedFiles(pluginPath);
|
|
if (manifestUpdated) {
|
|
console.log(` Updated ${path.basename(pluginPath)}/.github/plugin/plugin.json`);
|
|
}
|
|
|
|
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)`);
|
|
}
|
|
}
|
|
|
|
return { removed, manifestUpdated };
|
|
}
|
|
|
|
export function cleanMaterializedExtensionPlugin(extensionPath) {
|
|
const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json");
|
|
let manifestUpdated = false;
|
|
if (fs.existsSync(pluginJsonPath)) {
|
|
const plugin = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
|
|
const extensionBundlePrefix = `extensions/${path.basename(extensionPath)}/`;
|
|
if (plugin.extensions === "extensions") {
|
|
plugin.extensions = ".";
|
|
manifestUpdated = true;
|
|
}
|
|
if (typeof plugin.logo === "string" && plugin.logo.startsWith(extensionBundlePrefix)) {
|
|
plugin.logo = plugin.logo.slice(extensionBundlePrefix.length);
|
|
manifestUpdated = true;
|
|
}
|
|
if (manifestUpdated) {
|
|
fs.writeFileSync(pluginJsonPath, JSON.stringify(plugin, null, 2) + "\n", "utf8");
|
|
console.log(` Updated ${path.basename(extensionPath)}/.github/plugin/plugin.json`);
|
|
}
|
|
}
|
|
|
|
const target = path.join(extensionPath, "extensions");
|
|
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
|
|
return { removed: 0, manifestUpdated };
|
|
}
|
|
|
|
const bundleRoot = path.join(target, path.basename(extensionPath));
|
|
const count = countFiles(target);
|
|
if (fs.existsSync(bundleRoot) && fs.statSync(bundleRoot).isDirectory()) {
|
|
for (const entry of fs.readdirSync(bundleRoot, { withFileTypes: true })) {
|
|
moveEntry(path.join(bundleRoot, entry.name), path.join(extensionPath, entry.name));
|
|
}
|
|
console.log(` Restored ${path.basename(extensionPath)}/ from materialized extensions bundle`);
|
|
}
|
|
|
|
fs.rmSync(target, { recursive: true, force: true });
|
|
console.log(` Removed ${path.basename(extensionPath)}/extensions/ (${count} files)`);
|
|
return { removed: count, manifestUpdated };
|
|
}
|
|
|
|
function isExtensionPluginDirectory(extensionPath) {
|
|
if (fs.existsSync(path.join(extensionPath, "extension.mjs"))) {
|
|
return true;
|
|
}
|
|
|
|
const bundleEntry = path.join(extensionPath, "extensions", path.basename(extensionPath), "extension.mjs");
|
|
if (fs.existsSync(bundleEntry)) {
|
|
return true;
|
|
}
|
|
|
|
const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json");
|
|
if (!fs.existsSync(pluginJsonPath)) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
const plugin = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
|
|
return plugin.extensions === "extensions";
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function countFiles(dir) {
|
|
let count = 0;
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
if (entry.isDirectory()) {
|
|
count += countFiles(path.join(dir, entry.name));
|
|
} else {
|
|
count++;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
|
|
function collectFiles(dir, rootDir = dir) {
|
|
const files = [];
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const entryPath = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) {
|
|
files.push(...collectFiles(entryPath, rootDir));
|
|
} else {
|
|
files.push(toPosixPath(path.relative(rootDir, entryPath)));
|
|
}
|
|
}
|
|
return files.sort();
|
|
}
|
|
|
|
function collectSkillDirectories(dir, rootDir = dir) {
|
|
const skillDirs = [];
|
|
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, "SKILL.md"))) {
|
|
skillDirs.push(toPosixPath(path.relative(rootDir, entryPath)));
|
|
continue;
|
|
}
|
|
|
|
skillDirs.push(...collectSkillDirectories(entryPath, rootDir));
|
|
}
|
|
return skillDirs.sort();
|
|
}
|
|
|
|
function arraysEqual(left, right) {
|
|
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
|
|
return false;
|
|
}
|
|
|
|
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("/");
|
|
}
|
|
|
|
function main() {
|
|
console.log("Cleaning materialized files from plugins...\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 total = 0;
|
|
let manifestsUpdated = 0;
|
|
for (const dirName of pluginDirs) {
|
|
const { removed, manifestUpdated } = cleanPlugin(path.join(PLUGINS_DIR, dirName));
|
|
total += removed;
|
|
if (manifestUpdated) {
|
|
manifestsUpdated++;
|
|
}
|
|
}
|
|
|
|
if (fs.existsSync(EXTENSIONS_DIR)) {
|
|
const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name)
|
|
.sort();
|
|
|
|
for (const dirName of extensionDirs) {
|
|
const extensionPath = path.join(EXTENSIONS_DIR, dirName);
|
|
if (!isExtensionPluginDirectory(extensionPath)) {
|
|
continue;
|
|
}
|
|
const { removed, manifestUpdated } = cleanMaterializedExtensionPlugin(extensionPath);
|
|
total += removed;
|
|
if (manifestUpdated) {
|
|
manifestsUpdated++;
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log();
|
|
if (total === 0 && manifestsUpdated === 0) {
|
|
console.log("✅ No materialized files found. Plugins are already clean.");
|
|
} else {
|
|
console.log(`✅ Removed ${total} materialized file(s) from plugins.`);
|
|
if (manifestsUpdated > 0) {
|
|
console.log(`✅ Updated ${manifestsUpdated} plugin manifest(s) to restore and normalize spec entries.`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
main();
|
|
}
|