const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9.-]*[a-z0-9]$/; const SEMVER_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/; export function validatePluginManifest(input) { const errors = []; if (!isRecord(input)) { return { success: false, errors: ["manifest must be an object"] }; } if (input.ideaPluginManifestVersion !== 1) { errors.push("ideaPluginManifestVersion must be 1"); } requireString(input, "id", errors); requireString(input, "displayName", errors); requireString(input, "version", errors); requireString(input, "main", errors); if (input.trustLevel !== "full") { errors.push("trustLevel must be full"); } optionalString(input, "description", errors); optionalString(input, "publisher", errors); if (typeof input.id === "string" && !PLUGIN_ID_PATTERN.test(input.id)) { errors.push("id must contain lowercase letters, digits, dots or dashes, and start/end with an alphanumeric character"); } if (typeof input.version === "string" && !SEMVER_PATTERN.test(input.version)) { errors.push("version must use semver syntax, for example 0.1.0"); } validateEngines(input.engines, errors); validateCapabilities(input.capabilities, errors); validateContributes(input.contributes, errors); if (errors.length > 0) { return { success: false, errors }; } return { success: true, data: input, errors: [] }; } export function isPluginManifest(input) { return validatePluginManifest(input).success; } export function assertPluginManifest(input) { const result = validatePluginManifest(input); if (!result.success) { throw new Error(`Invalid IdeA plugin manifest: ${result.errors.join("; ")}`); } } function validateEngines(value, errors) { if (value === undefined) { return; } if (!isRecord(value)) { errors.push("engines must be an object when provided"); return; } optionalString(value, "idea", errors, "engines.idea"); } function validateCapabilities(value, errors) { if (value === undefined) { return; } if (!Array.isArray(value)) { errors.push("capabilities must be an array when provided"); return; } value.forEach((capability, index) => { if (capability !== "ui" && capability !== "mcp" && capability !== "tooling") { errors.push(`capabilities[${index}] must be "ui", "mcp" or "tooling"`); } }); } function validateContributes(value, errors) { if (value === undefined) { return; } if (!isRecord(value)) { errors.push("contributes must be an object when provided"); return; } validateArray(value, "menus", errors, (menu, index) => { requireString(menu, "id", errors, `contributes.menus[${index}].id`); requireString(menu, "label", errors, `contributes.menus[${index}].label`); if (menu.topLevel !== true) { errors.push(`contributes.menus[${index}].topLevel must be true`); } optionalNumber(menu, "order", errors, `contributes.menus[${index}].order`); optionalString(menu, "icon", errors, `contributes.menus[${index}].icon`); }); validateArray(value, "menuItems", errors, (item, index) => { requireString(item, "id", errors, `contributes.menuItems[${index}].id`); requireString(item, "targetMenuId", errors, `contributes.menuItems[${index}].targetMenuId`); requireString(item, "label", errors, `contributes.menuItems[${index}].label`); requireString(item, "command", errors, `contributes.menuItems[${index}].command`); optionalNumber(item, "order", errors, `contributes.menuItems[${index}].order`); optionalString(item, "icon", errors, `contributes.menuItems[${index}].icon`); optionalString(item, "when", errors, `contributes.menuItems[${index}].when`); }); validateArray(value, "layouts", errors, (layout, index) => { requireString(layout, "type", errors, `contributes.layouts[${index}].type`); requireString(layout, "label", errors, `contributes.layouts[${index}].label`); requireString(layout, "component", errors, `contributes.layouts[${index}].component`); optionalNumber(layout, "order", errors, `contributes.layouts[${index}].order`); optionalString(layout, "icon", errors, `contributes.layouts[${index}].icon`); optionalString(layout, "when", errors, `contributes.layouts[${index}].when`); }); validateArray(value, "mcpServers", errors, (server, index) => { requireString(server, "id", errors, `contributes.mcpServers[${index}].id`); requireString(server, "displayName", errors, `contributes.mcpServers[${index}].displayName`); requireString(server, "command", errors, `contributes.mcpServers[${index}].command`); if (server.transport !== "stdio") { errors.push(`contributes.mcpServers[${index}].transport must be "stdio"`); } optionalStringArray(server, "args", errors, `contributes.mcpServers[${index}].args`); optionalStringRecord(server, "env", errors, `contributes.mcpServers[${index}].env`); optionalString(server, "cwd", errors, `contributes.mcpServers[${index}].cwd`); optionalBoolean(server, "autoStart", errors, `contributes.mcpServers[${index}].autoStart`); optionalBoolean(server, "allowAbsoluteCommand", errors, `contributes.mcpServers[${index}].allowAbsoluteCommand`); }); } function validateArray(record, key, errors, validateItem) { const value = record[key]; if (value === undefined) { return; } if (!Array.isArray(value)) { errors.push(`contributes.${key} must be an array when provided`); return; } value.forEach((item, index) => { if (!isRecord(item)) { errors.push(`contributes.${key}[${index}] must be an object`); return; } validateItem(item, index); }); } function requireString(record, key, errors, label = key) { if (typeof record[key] !== "string" || record[key].trim().length === 0) { errors.push(`${label} must be a non-empty string`); } } function optionalString(record, key, errors, label = key) { if (record[key] !== undefined && typeof record[key] !== "string") { errors.push(`${label} must be a string when provided`); } } function optionalNumber(record, key, errors, label = key) { if (record[key] !== undefined && typeof record[key] !== "number") { errors.push(`${label} must be a number when provided`); } } function optionalBoolean(record, key, errors, label = key) { if (record[key] !== undefined && typeof record[key] !== "boolean") { errors.push(`${label} must be a boolean when provided`); } } function optionalStringArray(record, key, errors, label = key) { const value = record[key]; if (value === undefined) { return; } if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) { errors.push(`${label} must be an array of strings when provided`); } } function optionalStringRecord(record, key, errors, label = key) { const value = record[key]; if (value === undefined) { return; } if (!isRecord(value) || Object.values(value).some((item) => typeof item !== "string")) { errors.push(`${label} must be an object of strings when provided`); } } function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); }