check: restore bundled channel config metadata gate

This commit is contained in:
Tak Hoffman
2026-03-26 23:56:16 -05:00
parent fbd8990e78
commit ed2798417e
3 changed files with 14799 additions and 4 deletions

View File

@@ -682,17 +682,17 @@
"canon:check:json": "node scripts/canon.mjs check --json",
"canon:enforce": "node scripts/canon.mjs enforce --json",
"canvas:a2ui:bundle": "bash scripts/bundle-a2ui.sh",
"check": "pnpm check:no-conflict-markers && pnpm check:host-env-policy:swift && pnpm check:base-config-schema && pnpm check:bundled-plugin-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:plugins:plugin-sdk-subpaths-exported && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
"check": "pnpm check:no-conflict-markers && pnpm check:host-env-policy:swift && pnpm check:bundled-channel-config-metadata && pnpm check:base-config-schema && pnpm check:bundled-plugin-metadata && pnpm check:bundled-provider-auth-env-vars && pnpm format:check && pnpm tsgo && pnpm plugin-sdk:check-exports && pnpm lint && pnpm lint:tmp:no-random-messaging && pnpm lint:tmp:channel-agnostic-boundaries && pnpm lint:tmp:no-raw-channel-fetch && pnpm lint:agent:ingress-owner && pnpm lint:plugins:no-register-http-handler && pnpm lint:plugins:no-monolithic-plugin-sdk-entry-imports && pnpm lint:plugins:no-extension-src-imports && pnpm lint:plugins:no-extension-test-core-imports && pnpm lint:plugins:no-extension-imports && pnpm lint:plugins:plugin-sdk-subpaths-exported && pnpm lint:extensions:no-src-outside-plugin-sdk && pnpm lint:extensions:no-plugin-sdk-internal && pnpm lint:extensions:no-relative-outside-package && pnpm lint:web-search-provider-boundaries && pnpm lint:webhook:no-low-level-body-read && pnpm lint:auth:no-pairing-store-group && pnpm lint:auth:pairing-account-scope",
"check:base-config-schema": "node --import tsx scripts/generate-base-config-schema.ts --check",
"check:bundled-channel-config-metadata": "node scripts/generate-bundled-plugin-metadata.mjs --check",
"check:bundled-channel-config-metadata": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --check",
"check:bundled-plugin-metadata": "node scripts/generate-bundled-plugin-metadata.mjs --check",
"check:bundled-provider-auth-env-vars": "node scripts/generate-bundled-provider-auth-env-vars.mjs --check",
"check:docs": "pnpm format:docs:check && pnpm lint:docs && pnpm docs:check-i18n-glossary && pnpm docs:check-links",
"check:host-env-policy:swift": "node scripts/generate-host-env-security-policy-swift.mjs --check",
"check:loc": "node --import tsx scripts/check-ts-max-loc.ts --max 500",
"check:no-conflict-markers": "node scripts/check-no-conflict-markers.mjs",
"config:channels:check": "node scripts/generate-bundled-plugin-metadata.mjs --check",
"config:channels:gen": "node scripts/generate-bundled-plugin-metadata.mjs",
"config:channels:check": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --check",
"config:channels:gen": "node --import tsx scripts/generate-bundled-channel-config-metadata.ts --write",
"config:docs:check": "node --import tsx scripts/generate-config-doc-baseline.ts --check",
"config:docs:gen": "node --import tsx scripts/generate-config-doc-baseline.ts --write",
"config:schema:check": "node --import tsx scripts/generate-base-config-schema.ts --check",

View File

@@ -0,0 +1,212 @@
#!/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<string, unknown>;
packageJson?: Record<string, unknown>;
};
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<string, unknown>;
uiHints?: Record<string, unknown>;
};
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<string, unknown> | undefined)
: undefined;
const channelMeta =
openclawMeta &&
typeof openclawMeta.channel === "object" &&
openclawMeta.channel &&
!Array.isArray(openclawMeta.channel)
? (openclawMeta.channel as Record<string, unknown>)
: 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)}`,
);
}
}

File diff suppressed because it is too large Load Diff