openclaw/packages/plugin-package-contract/src/index.ts

97 lines
3.2 KiB
TypeScript

export type JsonObject = Record<string, unknown>;
export type ExternalPluginCompatibility = {
pluginApiRange?: string;
builtWithOpenClawVersion?: string;
pluginSdkVersion?: string;
minGatewayVersion?: string;
};
export type ExternalPluginValidationIssue = {
fieldPath: string;
message: string;
};
export type ExternalCodePluginValidationResult = {
compatibility?: ExternalPluginCompatibility;
issues: ExternalPluginValidationIssue[];
};
export const EXTERNAL_CODE_PLUGIN_REQUIRED_FIELD_PATHS = [
"openclaw.compat.pluginApi",
"openclaw.build.openclawVersion",
] as const;
function isRecord(value: unknown): value is JsonObject {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function getTrimmedString(value: unknown): string | undefined {
return typeof value === "string" && value.trim() ? value.trim() : undefined;
}
function readOpenClawBlock(packageJson: unknown) {
const root = isRecord(packageJson) ? packageJson : undefined;
const openclaw = isRecord(root?.openclaw) ? root.openclaw : undefined;
const compat = isRecord(openclaw?.compat) ? openclaw.compat : undefined;
const build = isRecord(openclaw?.build) ? openclaw.build : undefined;
const install = isRecord(openclaw?.install) ? openclaw.install : undefined;
return { root, openclaw, compat, build, install };
}
export function normalizeExternalPluginCompatibility(
packageJson: unknown,
): ExternalPluginCompatibility | undefined {
const { root, compat, build, install } = readOpenClawBlock(packageJson);
const version = getTrimmedString(root?.version);
const minHostVersion = getTrimmedString(install?.minHostVersion);
const compatibility: ExternalPluginCompatibility = {};
const pluginApi = getTrimmedString(compat?.pluginApi);
if (pluginApi) {
compatibility.pluginApiRange = pluginApi;
}
const minGatewayVersion = getTrimmedString(compat?.minGatewayVersion) ?? minHostVersion;
if (minGatewayVersion) {
compatibility.minGatewayVersion = minGatewayVersion;
}
const builtWithOpenClawVersion = getTrimmedString(build?.openclawVersion) ?? version;
if (builtWithOpenClawVersion) {
compatibility.builtWithOpenClawVersion = builtWithOpenClawVersion;
}
const pluginSdkVersion = getTrimmedString(build?.pluginSdkVersion);
if (pluginSdkVersion) {
compatibility.pluginSdkVersion = pluginSdkVersion;
}
return Object.keys(compatibility).length > 0 ? compatibility : undefined;
}
export function listMissingExternalCodePluginFieldPaths(packageJson: unknown): string[] {
const { compat, build } = readOpenClawBlock(packageJson);
const missing: string[] = [];
if (!getTrimmedString(compat?.pluginApi)) {
missing.push("openclaw.compat.pluginApi");
}
if (!getTrimmedString(build?.openclawVersion)) {
missing.push("openclaw.build.openclawVersion");
}
return missing;
}
export function validateExternalCodePluginPackageJson(
packageJson: unknown,
): ExternalCodePluginValidationResult {
const issues = listMissingExternalCodePluginFieldPaths(packageJson).map((fieldPath) => ({
fieldPath,
message: `${fieldPath} is required for external code plugins published to ClawHub.`,
}));
return {
compatibility: normalizeExternalPluginCompatibility(packageJson),
issues,
};
}