Migrate extension plugin materialization to extensions container (#2334)

* 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

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Aaron Powell
2026-07-17 18:04:03 +10:00
committed by GitHub
parent 38ab1360a4
commit 40665c23b7
32 changed files with 583 additions and 49 deletions
+92 -2
View File
@@ -2,9 +2,11 @@
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");
/**
* Recursively copy a directory.
@@ -22,6 +24,17 @@ function copyDirRecursive(src, dest) {
}
}
function copyEntryRecursive(srcPath, destPath) {
const stats = fs.statSync(srcPath);
if (stats.isDirectory()) {
copyDirRecursive(srcPath, destPath);
return;
}
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.copyFileSync(srcPath, destPath);
}
/**
* Resolve a plugin-relative path to the repo-root source file.
*
@@ -45,6 +58,48 @@ function resolveSource(relPath) {
return null;
}
export function materializeExtensionPlugin(extensionPath) {
const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json");
if (!fs.existsSync(pluginJsonPath)) {
return { copiedEntries: 0, manifestUpdated: false, skipped: true };
}
let metadata;
try {
metadata = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
} catch (err) {
throw new Error(`Failed to parse ${pluginJsonPath}: ${err.message}`);
}
const extensionContainerPath = path.join(extensionPath, "extensions");
fs.rmSync(extensionContainerPath, { recursive: true, force: true });
fs.mkdirSync(extensionContainerPath, { recursive: true });
let copiedEntries = 0;
for (const entry of fs.readdirSync(extensionPath, { withFileTypes: true })) {
if (entry.name === ".github" || entry.name === "extensions") {
continue;
}
copyEntryRecursive(
path.join(extensionPath, entry.name),
path.join(extensionContainerPath, entry.name)
);
copiedEntries++;
}
let manifestUpdated = false;
if (metadata.extensions !== "extensions") {
metadata.extensions = "extensions";
manifestUpdated = true;
}
if (manifestUpdated) {
fs.writeFileSync(pluginJsonPath, JSON.stringify(metadata, null, 2) + "\n", "utf8");
}
return { copiedEntries, manifestUpdated, skipped: false };
}
function materializePlugins() {
console.log("Materializing plugin files...\n");
@@ -61,6 +116,8 @@ function materializePlugins() {
let totalAgents = 0;
let totalSkills = 0;
let totalExtensions = 0;
let totalExtensionPlugins = 0;
let totalExtensionPluginEntries = 0;
let warnings = 0;
let errors = 0;
@@ -185,7 +242,36 @@ function materializePlugins() {
}
}
console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} extensions.`);
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 (!fs.existsSync(path.join(extensionPath, "extension.mjs"))) {
continue;
}
try {
const result = materializeExtensionPlugin(extensionPath);
if (result.skipped) {
continue;
}
totalExtensionPlugins++;
totalExtensionPluginEntries += result.copiedEntries;
console.log(`${dirName}: materialized extension bundle into ./extensions (${result.copiedEntries} entries)`);
} catch (err) {
console.error(`Error: Failed to materialize extension plugin ${dirName}: ${err.message}`);
errors++;
}
}
}
console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} plugin extension refs.`);
console.log(`Materialized ${totalExtensionPlugins} extension plugins (${totalExtensionPluginEntries} top-level entries).`);
if (warnings > 0) {
console.log(`${warnings} warning(s).`);
}
@@ -195,4 +281,8 @@ function materializePlugins() {
}
}
materializePlugins();
export { materializePlugins };
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
materializePlugins();
}