Spaces:
Paused
Paused
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import type { PluginConfigUiHint, PluginKind } from "./types.js"; | |
| import { MANIFEST_KEY } from "../compat/legacy-names.js"; | |
| export const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json"; | |
| export const PLUGIN_MANIFEST_FILENAMES = [PLUGIN_MANIFEST_FILENAME] as const; | |
| export type PluginManifest = { | |
| id: string; | |
| configSchema: Record<string, unknown>; | |
| kind?: PluginKind; | |
| channels?: string[]; | |
| providers?: string[]; | |
| skills?: string[]; | |
| name?: string; | |
| description?: string; | |
| version?: string; | |
| uiHints?: Record<string, PluginConfigUiHint>; | |
| }; | |
| export type PluginManifestLoadResult = | |
| | { ok: true; manifest: PluginManifest; manifestPath: string } | |
| | { ok: false; error: string; manifestPath: string }; | |
| function normalizeStringList(value: unknown): string[] { | |
| if (!Array.isArray(value)) { | |
| return []; | |
| } | |
| return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean); | |
| } | |
| function isRecord(value: unknown): value is Record<string, unknown> { | |
| return Boolean(value && typeof value === "object" && !Array.isArray(value)); | |
| } | |
| export function resolvePluginManifestPath(rootDir: string): string { | |
| for (const filename of PLUGIN_MANIFEST_FILENAMES) { | |
| const candidate = path.join(rootDir, filename); | |
| if (fs.existsSync(candidate)) { | |
| return candidate; | |
| } | |
| } | |
| return path.join(rootDir, PLUGIN_MANIFEST_FILENAME); | |
| } | |
| export function loadPluginManifest(rootDir: string): PluginManifestLoadResult { | |
| const manifestPath = resolvePluginManifestPath(rootDir); | |
| if (!fs.existsSync(manifestPath)) { | |
| return { ok: false, error: `plugin manifest not found: ${manifestPath}`, manifestPath }; | |
| } | |
| let raw: unknown; | |
| try { | |
| raw = JSON.parse(fs.readFileSync(manifestPath, "utf-8")) as unknown; | |
| } catch (err) { | |
| return { | |
| ok: false, | |
| error: `failed to parse plugin manifest: ${String(err)}`, | |
| manifestPath, | |
| }; | |
| } | |
| if (!isRecord(raw)) { | |
| return { ok: false, error: "plugin manifest must be an object", manifestPath }; | |
| } | |
| const id = typeof raw.id === "string" ? raw.id.trim() : ""; | |
| if (!id) { | |
| return { ok: false, error: "plugin manifest requires id", manifestPath }; | |
| } | |
| const configSchema = isRecord(raw.configSchema) ? raw.configSchema : null; | |
| if (!configSchema) { | |
| return { ok: false, error: "plugin manifest requires configSchema", manifestPath }; | |
| } | |
| const kind = typeof raw.kind === "string" ? (raw.kind as PluginKind) : undefined; | |
| const name = typeof raw.name === "string" ? raw.name.trim() : undefined; | |
| const description = typeof raw.description === "string" ? raw.description.trim() : undefined; | |
| const version = typeof raw.version === "string" ? raw.version.trim() : undefined; | |
| const channels = normalizeStringList(raw.channels); | |
| const providers = normalizeStringList(raw.providers); | |
| const skills = normalizeStringList(raw.skills); | |
| let uiHints: Record<string, PluginConfigUiHint> | undefined; | |
| if (isRecord(raw.uiHints)) { | |
| uiHints = raw.uiHints as Record<string, PluginConfigUiHint>; | |
| } | |
| return { | |
| ok: true, | |
| manifest: { | |
| id, | |
| configSchema, | |
| kind, | |
| channels, | |
| providers, | |
| skills, | |
| name, | |
| description, | |
| version, | |
| uiHints, | |
| }, | |
| manifestPath, | |
| }; | |
| } | |
| // package.json "openclaw" metadata (used for onboarding/catalog) | |
| export type PluginPackageChannel = { | |
| id?: string; | |
| label?: string; | |
| selectionLabel?: string; | |
| detailLabel?: string; | |
| docsPath?: string; | |
| docsLabel?: string; | |
| blurb?: string; | |
| order?: number; | |
| aliases?: string[]; | |
| preferOver?: string[]; | |
| systemImage?: string; | |
| selectionDocsPrefix?: string; | |
| selectionDocsOmitLabel?: boolean; | |
| selectionExtras?: string[]; | |
| showConfigured?: boolean; | |
| quickstartAllowFrom?: boolean; | |
| forceAccountBinding?: boolean; | |
| preferSessionLookupForAnnounceTarget?: boolean; | |
| }; | |
| export type PluginPackageInstall = { | |
| npmSpec?: string; | |
| localPath?: string; | |
| defaultChoice?: "npm" | "local"; | |
| }; | |
| export type OpenClawPackageManifest = { | |
| extensions?: string[]; | |
| channel?: PluginPackageChannel; | |
| install?: PluginPackageInstall; | |
| }; | |
| export type ManifestKey = typeof MANIFEST_KEY; | |
| export type PackageManifest = { | |
| name?: string; | |
| version?: string; | |
| description?: string; | |
| } & Partial<Record<ManifestKey, OpenClawPackageManifest>>; | |
| export function getPackageManifestMetadata( | |
| manifest: PackageManifest | undefined, | |
| ): OpenClawPackageManifest | undefined { | |
| if (!manifest) { | |
| return undefined; | |
| } | |
| return manifest[MANIFEST_KEY]; | |
| } | |