mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-25 18:25:01 +00:00
chore: publish from main
This commit is contained in:
@@ -6,7 +6,7 @@ import { fileURLToPath } from "url";
|
||||
import { ROOT_FOLDER } from "./constants.mjs";
|
||||
import { readExternalPlugins } from "./external-plugin-validation.mjs";
|
||||
import { validateLicenseField } from "./lib/license.mjs";
|
||||
import { AGENT_PLUGIN_SCHEMA_URL, validateAgentPluginManifest } from "./agent-plugin-schema.mjs";
|
||||
import { AGENT_PLUGIN_SCHEMA_URL, validateAgentPluginManifest, validateAgentPluginMcpConfig } from "./agent-plugin-schema.mjs";
|
||||
|
||||
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
|
||||
const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions");
|
||||
@@ -212,9 +212,77 @@ function validateExtensionReferences(plugin, pluginDir) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
function validateCompositionNamespace(plugin) {
|
||||
export function validateMcpConfig(pluginDir) {
|
||||
const errors = [];
|
||||
const compositionFields = ["agents", "hooks", "mcpServers", "skills"];
|
||||
const legacyPath = path.join(pluginDir, ".mcp.json");
|
||||
if (fs.existsSync(legacyPath)) {
|
||||
errors.push("MCP configuration must live at mcp.json in the plugin root, not .mcp.json");
|
||||
}
|
||||
|
||||
const mcpJsonPath = path.join(pluginDir, "mcp.json");
|
||||
let mcpStat;
|
||||
try {
|
||||
mcpStat = fs.lstatSync(mcpJsonPath);
|
||||
} catch (error) {
|
||||
if (error.code === "ENOENT") {
|
||||
try {
|
||||
fs.readlinkSync(mcpJsonPath);
|
||||
errors.push("mcp.json is a dangling symbolic link");
|
||||
} catch (readlinkError) {
|
||||
if (readlinkError.code !== "EINVAL" && readlinkError.code !== "ENOENT") {
|
||||
errors.push(`mcp.json could not be inspected: ${readlinkError.message}`);
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
errors.push(`mcp.json could not be inspected: ${error.message}`);
|
||||
return errors;
|
||||
}
|
||||
|
||||
let pluginRoot;
|
||||
let resolvedMcpJsonPath;
|
||||
try {
|
||||
pluginRoot = fs.realpathSync.native(pluginDir);
|
||||
resolvedMcpJsonPath = fs.realpathSync.native(mcpJsonPath);
|
||||
} catch (error) {
|
||||
if (mcpStat.isSymbolicLink() && error.code === "ENOENT") {
|
||||
errors.push("mcp.json is a dangling symbolic link");
|
||||
} else {
|
||||
errors.push(`mcp.json could not be resolved: ${error.message}`);
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
const relativeMcpPath = path.relative(pluginRoot, resolvedMcpJsonPath);
|
||||
if (relativeMcpPath === ".." ||
|
||||
relativeMcpPath.startsWith(`..${path.sep}`) ||
|
||||
path.isAbsolute(relativeMcpPath)) {
|
||||
errors.push("mcp.json must resolve to a file inside the plugin root");
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (!fs.statSync(resolvedMcpJsonPath).isFile()) {
|
||||
errors.push("mcp.json must be a regular file");
|
||||
return errors;
|
||||
}
|
||||
|
||||
const parsed = parseJsonFile(resolvedMcpJsonPath);
|
||||
if (parsed.parseError) {
|
||||
errors.push(`failed to parse mcp.json: ${parsed.parseError}`);
|
||||
return errors;
|
||||
}
|
||||
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
||||
errors.push("mcp.json must contain a top-level object");
|
||||
return errors;
|
||||
}
|
||||
errors.push(...validateAgentPluginMcpConfig(parsed, pluginDir).map((message) => `mcp.json ${message}`));
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateCompositionNamespace(plugin) {
|
||||
const errors = [];
|
||||
const compositionFields = ["agents", "hooks", "skills"];
|
||||
const extensions = plugin.extensions;
|
||||
const composition = extensions?.[AWESOME_COPILOT_NAMESPACE];
|
||||
|
||||
@@ -230,6 +298,17 @@ function validateCompositionNamespace(plugin) {
|
||||
return errors;
|
||||
}
|
||||
|
||||
if (extensions && typeof extensions === "object" && !Array.isArray(extensions)) {
|
||||
for (const [namespace, value] of Object.entries(extensions)) {
|
||||
if (value && typeof value === "object" && !Array.isArray(value) && value.mcpServers !== undefined) {
|
||||
errors.push(`extensions["${namespace}"].mcpServers is not supported; declare MCP servers in mcp.json at the plugin root`);
|
||||
}
|
||||
}
|
||||
if (extensions.mcpServers !== undefined) {
|
||||
errors.push("extensions.mcpServers is not supported; declare MCP servers in mcp.json at the plugin root");
|
||||
}
|
||||
}
|
||||
|
||||
for (const field of compositionFields) {
|
||||
if (extensions?.[field] !== undefined) {
|
||||
errors.push(`extensions.${field} must be moved to extensions["${AWESOME_COPILOT_NAMESPACE}"].${field}`);
|
||||
@@ -290,12 +369,16 @@ 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", "hooks", "mcpServers", "skills"]) {
|
||||
if (plugin.mcpServers !== undefined) {
|
||||
errors.push("mcpServers must be declared in mcp.json at the plugin root, not in plugin.json");
|
||||
}
|
||||
for (const field of ["agents", "hooks", "skills"]) {
|
||||
if (plugin[field] !== undefined) {
|
||||
errors.push(`${field} must be moved to extensions["${AWESOME_COPILOT_NAMESPACE}"].${field}`);
|
||||
}
|
||||
}
|
||||
errors.push(...validateCompositionNamespace(plugin));
|
||||
errors.push(...validateMcpConfig(pluginDir));
|
||||
const licenseResult = validateLicenseField(plugin.license, { required: false });
|
||||
errors.push(...licenseResult.errors);
|
||||
warnings.push(...licenseResult.warnings);
|
||||
|
||||
Reference in New Issue
Block a user