- capability manifeste tooling supportée backend + transport runtime catalog - ctx.services publique côté SDK/runtime, gated par tooling - surface honnête: workspace, terminal, et tasks en observation/contrôle uniquement - docs/exemple/tests mis à jour - QA PASS sur feature/sdk-plugin-tooling-build-debug-surface
173 lines
7.4 KiB
JavaScript
173 lines
7.4 KiB
JavaScript
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);
|
|
}
|