import fs from "node:fs"; import path from "node:path"; import { discoverOpenClawPlugins } from "../src/plugins/discovery.js"; import { collectFilesSync, isCodeFile, relativeToCwd } from "./check-file-utils.js"; // Match exact monolithic-root specifier in any code path: // imports/exports, require/dynamic import, and test mocks (vi.mock/jest.mock). const ROOT_IMPORT_PATTERN = /["']openclaw\/plugin-sdk["']/; const LEGACY_COMPAT_IMPORT_PATTERN = /["']openclaw\/plugin-sdk\/compat["']/; function hasMonolithicRootImport(content: string): boolean { return ROOT_IMPORT_PATTERN.test(content); } function hasLegacyCompatImport(content: string): boolean { return LEGACY_COMPAT_IMPORT_PATTERN.test(content); } function collectPluginSourceFiles(rootDir: string): string[] { const srcDir = path.join(rootDir, "src"); if (!fs.existsSync(srcDir)) { return []; } return collectFilesSync(srcDir, { includeFile: (filePath) => isCodeFile(filePath), skipDirNames: new Set(["node_modules", "dist", ".git", "coverage"]), }); } function collectSharedExtensionSourceFiles(): string[] { return collectPluginSourceFiles(path.join(process.cwd(), "extensions", "shared")); } function collectBundledExtensionSourceFiles(): string[] { const extensionsDir = path.join(process.cwd(), "extensions"); let entries: fs.Dirent[] = []; try { entries = fs.readdirSync(extensionsDir, { withFileTypes: true }); } catch { return []; } const files: string[] = []; for (const entry of entries) { if (!entry.isDirectory() || entry.name === "shared") { continue; } for (const srcFile of collectPluginSourceFiles(path.join(extensionsDir, entry.name))) { files.push(srcFile); } } return files; } function main() { const discovery = discoverOpenClawPlugins({}); const bundledCandidates = discovery.candidates.filter((c) => c.origin === "bundled"); const filesToCheck = new Set(); for (const candidate of bundledCandidates) { filesToCheck.add(candidate.source); for (const srcFile of collectPluginSourceFiles(candidate.rootDir)) { filesToCheck.add(srcFile); } } for (const sharedFile of collectSharedExtensionSourceFiles()) { filesToCheck.add(sharedFile); } for (const extensionFile of collectBundledExtensionSourceFiles()) { filesToCheck.add(extensionFile); } const monolithicOffenders: string[] = []; const legacyCompatOffenders: string[] = []; for (const entryFile of filesToCheck) { let content = ""; try { content = fs.readFileSync(entryFile, "utf8"); } catch { continue; } if (hasMonolithicRootImport(content)) { monolithicOffenders.push(entryFile); } if (hasLegacyCompatImport(content)) { legacyCompatOffenders.push(entryFile); } } if (monolithicOffenders.length > 0 || legacyCompatOffenders.length > 0) { if (monolithicOffenders.length > 0) { console.error("Bundled plugin source files must not import monolithic openclaw/plugin-sdk."); for (const file of monolithicOffenders.toSorted()) { console.error(`- ${relativeToCwd(file)}`); } } if (legacyCompatOffenders.length > 0) { console.error( "Bundled plugin source files must not import legacy openclaw/plugin-sdk/compat.", ); for (const file of legacyCompatOffenders.toSorted()) { console.error(`- ${relativeToCwd(file)}`); } } if (monolithicOffenders.length > 0 || legacyCompatOffenders.length > 0) { console.error( "Use openclaw/plugin-sdk/ or openclaw/plugin-sdk/ subpaths for bundled plugins; root and compat are legacy surfaces only.", ); } process.exit(1); } console.log( `OK: bundled plugin source files use scoped plugin-sdk subpaths (${filesToCheck.size} checked).`, ); } main();