chore: publish from main

This commit is contained in:
github-actions[bot]
2026-08-07 20:03:01 +00:00
parent 6f8afa8c34
commit 507b3408cb
34 changed files with 4795 additions and 18 deletions
+81
View File
@@ -0,0 +1,81 @@
import fs from "fs";
import path from "path";
const AWESOME_COPILOT_NAMESPACE = "com.github.awesome-copilot";
function extensionIdFromReference(reference) {
if (typeof reference !== "string" || !reference.startsWith("./extensions/")) {
return null;
}
return reference.replace(/^\.\/extensions\//, "").replace(/\/$/, "");
}
export function buildExtensionPluginOwners(pluginEntries) {
const owners = new Map();
const sortedEntries = [...pluginEntries].sort((a, b) =>
a.directoryName.localeCompare(b.directoryName)
);
for (const { directoryName, manifest } of sortedEntries) {
const pluginName =
typeof manifest?.name === "string" && manifest.name.trim()
? manifest.name.trim()
: directoryName;
const extensionIds = new Set([directoryName]);
const references =
manifest?.extensions?.[AWESOME_COPILOT_NAMESPACE]?.extensions;
if (Array.isArray(references)) {
for (const reference of references) {
const extensionId = extensionIdFromReference(reference);
if (extensionId) {
extensionIds.add(extensionId);
}
}
}
for (const extensionId of extensionIds) {
const pluginNames = owners.get(extensionId) ?? [];
if (!pluginNames.includes(pluginName)) {
pluginNames.push(pluginName);
}
owners.set(extensionId, pluginNames);
}
}
return owners;
}
export function readExtensionPluginOwners(pluginsDir) {
if (!fs.existsSync(pluginsDir)) {
return new Map();
}
const pluginEntries = fs
.readdirSync(pluginsDir, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => {
const manifestPath = path.join(pluginsDir, entry.name, "plugin.json");
if (!fs.existsSync(manifestPath)) {
return null;
}
return {
directoryName: entry.name,
manifest: JSON.parse(fs.readFileSync(manifestPath, "utf-8")),
};
})
.filter(Boolean);
return buildExtensionPluginOwners(pluginEntries);
}
export function resolveExtensionPluginName(extensionId, owners) {
const pluginNames = owners.get(extensionId) ?? [];
return (
pluginNames.find((pluginName) => pluginName === extensionId) ??
pluginNames[0] ??
extensionId
);
}
+61
View File
@@ -0,0 +1,61 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import {
buildExtensionPluginOwners,
resolveExtensionPluginName,
} from "./extension-plugin-ownership.mjs";
const namespace = "com.github.awesome-copilot";
test("resolves an extension bundled only by its parent plugin", () => {
const owners = buildExtensionPluginOwners([
{
directoryName: "ember",
manifest: {
name: "ember",
extensions: {
[namespace]: {
extensions: ["./extensions/daily-focus-board"],
},
},
},
},
]);
assert.equal(
resolveExtensionPluginName("daily-focus-board", owners),
"ember"
);
});
test("prefers a same-named standalone plugin over another owner", () => {
const owners = buildExtensionPluginOwners([
{
directoryName: "parent-plugin",
manifest: {
name: "parent-plugin",
extensions: {
[namespace]: {
extensions: ["./extensions/daily-focus-board/"],
},
},
},
},
{
directoryName: "daily-focus-board",
manifest: { name: "daily-focus-board" },
},
]);
assert.equal(
resolveExtensionPluginName("daily-focus-board", owners),
"daily-focus-board"
);
});
test("preserves the extension name when no plugin owns it", () => {
assert.equal(
resolveExtensionPluginName("daily-focus-board", new Map()),
"daily-focus-board"
);
});
+8 -2
View File
@@ -26,6 +26,10 @@ import {
parseYamlFile,
} from "./yaml-parser.mjs";
import { readExternalPlugins } from "./external-plugin-validation.mjs";
import {
readExtensionPluginOwners,
resolveExtensionPluginName,
} from "./extension-plugin-ownership.mjs";
const __filename = fileURLToPath(import.meta.url);
@@ -1177,6 +1181,7 @@ function generateCanvasManifest(gitDates, commitSha) {
return { items: [], filters: { keywords: [] } };
}
const extensionPluginOwners = readExtensionPluginOwners(PLUGINS_DIR);
const extensionDirs = fs
.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
.filter((entry) => {
@@ -1212,6 +1217,7 @@ function generateCanvasManifest(gitDates, commitSha) {
normalizeText(packageJson.description, "Canvas extension")
);
const extensionName = normalizeText(pluginJson.name, normalizeText(packageJson.name, dir.name));
const pluginName = resolveExtensionPluginName(dir.name, extensionPluginOwners);
const extensionVersion = normalizeText(pluginJson.version, normalizeText(packageJson.version, "1.0.0"));
const readmeFile = fs.existsSync(path.join(extensionDir, "README.md"))
? `${relPath}/README.md`
@@ -1230,7 +1236,7 @@ function generateCanvasManifest(gitDates, commitSha) {
/\\/g,
"/"
)}`;
const installCommand = `copilot plugin install ${extensionName}@awesome-copilot`;
const installCommand = `copilot plugin install ${pluginName}@awesome-copilot`;
for (const canvas of canvasEntries) {
const canvasId = normalizeText(canvas.id, dir.name);
@@ -1241,7 +1247,7 @@ function generateCanvasManifest(gitDates, commitSha) {
canvasId,
extensionId: dir.name,
extensionName,
pluginName: extensionName,
pluginName,
name: canvasName,
version: extensionVersion,
readmeFile,
+29 -13
View File
@@ -2,6 +2,7 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { ROOT_FOLDER } from "./constants.mjs";
import { readExternalPlugins } from "./external-plugin-validation.mjs";
import { validateLicenseField } from "./lib/license.mjs";
@@ -334,7 +335,11 @@ function validateExtensionScreenshotPath(extensionDir, pathValue, fieldName, err
}
// Main validation function
function validatePlugins() {
export function isReusableExtensionRegistered(extensionName, pluginDirectoryNames, referencedExtensionNames) {
return pluginDirectoryNames.has(extensionName) || referencedExtensionNames.has(extensionName);
}
export function validatePlugins() {
const pluginDirs = fs.existsSync(PLUGINS_DIR)
? fs.readdirSync(PLUGINS_DIR, { withFileTypes: true })
.filter((d) => d.isDirectory())
@@ -350,6 +355,8 @@ function validatePlugins() {
let hasErrors = false;
const seenNames = new Set();
const localPluginNames = [];
const pluginDirectoryNames = new Set(pluginDirs);
const referencedExtensionNames = new Set();
for (const dir of pluginDirs) {
console.log(`Validating ${dir}...`);
@@ -377,12 +384,20 @@ function validatePlugins() {
localPluginNames.push(plugin.name);
}
}
const extensionReferences = plugin?.extensions?.[AWESOME_COPILOT_NAMESPACE]?.extensions;
if (Array.isArray(extensionReferences)) {
for (const reference of extensionReferences) {
if (typeof reference === "string" && reference.startsWith("./extensions/")) {
referencedExtensionNames.add(reference.replace(/^\.\/extensions\//, "").replace(/\/$/, ""));
}
}
}
}
for (const dir of getExtensionFolderNames()) {
const pluginJsonPath = path.join(PLUGINS_DIR, dir, "plugin.json");
if (!fs.existsSync(pluginJsonPath)) {
console.error(`❌ extension ${dir}: missing plugin manifest at plugins/${dir}/plugin.json`);
if (!isReusableExtensionRegistered(dir, pluginDirectoryNames, referencedExtensionNames)) {
console.error(`❌ extension ${dir}: must be referenced by a plugin or have a standalone manifest at plugins/${dir}/plugin.json`);
hasErrors = true;
}
}
@@ -410,15 +425,16 @@ function validatePlugins() {
return !hasErrors;
}
// Run validation
try {
const isValid = validatePlugins();
if (!isValid) {
console.error("\n❌ Plugin validation failed");
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
try {
const isValid = validatePlugins();
if (!isValid) {
console.error("\n❌ Plugin validation failed");
process.exit(1);
}
console.log("\n🎉 Plugin validation passed");
} catch (error) {
console.error(`Error during validation: ${error.message}`);
process.exit(1);
}
console.log("\n🎉 Plugin validation passed");
} catch (error) {
console.error(`Error during validation: ${error.message}`);
process.exit(1);
}
+36
View File
@@ -0,0 +1,36 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { isReusableExtensionRegistered } from "./validate-plugins.mjs";
test("accepts a reusable extension bundled only by a parent plugin", () => {
assert.equal(
isReusableExtensionRegistered(
"daily-focus-board",
new Set(["ember"]),
new Set(["daily-focus-board"])
),
true
);
});
test("accepts a same-named standalone extension plugin", () => {
assert.equal(
isReusableExtensionRegistered(
"daily-focus-board",
new Set(["daily-focus-board"]),
new Set()
),
true
);
});
test("rejects an orphaned reusable extension", () => {
assert.equal(
isReusableExtensionRegistered(
"daily-focus-board",
new Set(["ember"]),
new Set()
),
false
);
});