#!/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(); }