#!/usr/bin/env node import fs from "node:fs"; import path from "node:path"; import { loadChannelConfigSurfaceModule } from "./load-channel-config-surface.ts"; const GENERATED_BY = "scripts/generate-bundled-channel-config-metadata.ts"; const DEFAULT_OUTPUT_PATH = "src/config/bundled-channel-config-metadata.generated.ts"; type BundledPluginSource = { dirName: string; pluginDir: string; manifestPath: string; manifest: { id: string; channels?: unknown; name?: string; description?: string; } & Record; packageJson?: Record; }; const { collectBundledPluginSources } = (await import( new URL("./lib/bundled-plugin-source-utils.mjs", import.meta.url).href )) as { collectBundledPluginSources: (params?: { repoRoot?: string; requirePackageJson?: boolean; }) => BundledPluginSource[]; }; const { formatGeneratedModule } = (await import( new URL("./lib/format-generated-module.mjs", import.meta.url).href )) as { formatGeneratedModule: ( source: string, options: { repoRoot: string; outputPath: string; errorLabel: string; }, ) => string; }; const { writeGeneratedOutput } = (await import( new URL("./lib/generated-output-utils.mjs", import.meta.url).href )) as { writeGeneratedOutput: (params: { repoRoot: string; outputPath: string; next: string; check?: boolean; }) => { changed: boolean; wrote: boolean; outputPath: string; }; }; type BundledChannelConfigMetadata = { pluginId: string; channelId: string; label?: string; description?: string; schema: Record; uiHints?: Record; }; function resolveChannelConfigSchemaModulePath(rootDir: string): string | null { const candidates = [ path.join(rootDir, "src", "config-schema.ts"), path.join(rootDir, "src", "config-schema.js"), path.join(rootDir, "src", "config-schema.mts"), path.join(rootDir, "src", "config-schema.mjs"), ]; for (const candidate of candidates) { if (fs.existsSync(candidate)) { return candidate; } } return null; } function resolvePackageChannelMeta(source: BundledPluginSource) { const openclawMeta = source.packageJson && typeof source.packageJson === "object" && !Array.isArray(source.packageJson) && "openclaw" in source.packageJson ? (source.packageJson.openclaw as Record | undefined) : undefined; const channelMeta = openclawMeta && typeof openclawMeta.channel === "object" && openclawMeta.channel && !Array.isArray(openclawMeta.channel) ? (openclawMeta.channel as Record) : undefined; return channelMeta; } function resolveRootLabel(source: BundledPluginSource, channelId: string): string | undefined { const channelMeta = resolvePackageChannelMeta(source); if (channelMeta?.id === channelId && typeof channelMeta.label === "string") { return channelMeta.label.trim(); } if (typeof source.manifest?.name === "string" && source.manifest.name.trim()) { return source.manifest.name.trim(); } return undefined; } function resolveRootDescription( source: BundledPluginSource, channelId: string, ): string | undefined { const channelMeta = resolvePackageChannelMeta(source); if (channelMeta?.id === channelId && typeof channelMeta.blurb === "string") { return channelMeta.blurb.trim(); } if (typeof source.manifest?.description === "string" && source.manifest.description.trim()) { return source.manifest.description.trim(); } return undefined; } function formatTypeScriptModule(source: string, outputPath: string, repoRoot: string): string { return formatGeneratedModule(source, { repoRoot, outputPath, errorLabel: "bundled channel config metadata", }); } export async function collectBundledChannelConfigMetadata(params?: { repoRoot?: string }) { const repoRoot = path.resolve(params?.repoRoot ?? process.cwd()); const sources = collectBundledPluginSources({ repoRoot, requirePackageJson: true }); const entries: BundledChannelConfigMetadata[] = []; for (const source of sources) { const channelIds = Array.isArray(source.manifest?.channels) ? source.manifest.channels.filter( (entry: unknown): entry is string => typeof entry === "string" && entry.trim().length > 0, ) : []; if (channelIds.length === 0) { continue; } const modulePath = resolveChannelConfigSchemaModulePath(source.pluginDir); if (!modulePath) { continue; } const surface = await loadChannelConfigSurfaceModule(modulePath, { repoRoot }); if (!surface?.schema) { continue; } for (const channelId of channelIds) { const label = resolveRootLabel(source, channelId); const description = resolveRootDescription(source, channelId); entries.push({ pluginId: String(source.manifest.id), channelId, ...(label ? { label } : {}), ...(description ? { description } : {}), schema: surface.schema, ...(Object.keys(surface.uiHints ?? {}).length > 0 ? { uiHints: surface.uiHints } : {}), }); } } return entries.toSorted((left, right) => left.channelId.localeCompare(right.channelId)); } export async function writeBundledChannelConfigMetadataModule(params?: { repoRoot?: string; outputPath?: string; check?: boolean; }) { const repoRoot = path.resolve(params?.repoRoot ?? process.cwd()); const outputPath = params?.outputPath ?? DEFAULT_OUTPUT_PATH; const entries = await collectBundledChannelConfigMetadata({ repoRoot }); const next = formatTypeScriptModule( `// Auto-generated by ${GENERATED_BY}. Do not edit directly. export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = ${JSON.stringify(entries, null, 2)} as const; `, outputPath, repoRoot, ); return writeGeneratedOutput({ repoRoot, outputPath, next, check: params?.check, }); } if (import.meta.url === new URL(process.argv[1] ?? "", "file://").href) { const check = process.argv.includes("--check"); const result = await writeBundledChannelConfigMetadataModule({ check }); if (!result.changed) { process.exitCode = 0; } else if (check) { console.error( `[bundled-channel-config-metadata] stale generated output at ${path.relative(process.cwd(), result.outputPath)}`, ); process.exitCode = 1; } else { console.log( `[bundled-channel-config-metadata] wrote ${path.relative(process.cwd(), result.outputPath)}`, ); } }