import fs from "node:fs"; import path from "node:path"; import { discoverOpenClawPlugins } from "../src/plugins/discovery.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_ROUTING_IMPORT_PATTERN = /["']openclaw\/plugin-sdk\/routing["']/; function hasMonolithicRootImport(content: string): boolean { return ROOT_IMPORT_PATTERN.test(content); } function hasLegacyRoutingImport(content: string): boolean { return LEGACY_ROUTING_IMPORT_PATTERN.test(content); } function isSourceFile(filePath: string): boolean { if (filePath.endsWith(".d.ts")) { return false; } return /\.(?:[cm]?ts|[cm]?js|tsx|jsx)$/u.test(filePath); } function collectPluginSourceFiles(rootDir: string): string[] { const srcDir = path.join(rootDir, "src"); if (!fs.existsSync(srcDir)) { return []; } const files: string[] = []; const stack: string[] = [srcDir]; while (stack.length > 0) { const current = stack.pop(); if (!current) { continue; } let entries: fs.Dirent[] = []; try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; } for (const entry of entries) { const fullPath = path.join(current, entry.name); if (entry.isDirectory()) { if ( entry.name === "node_modules" || entry.name === "dist" || entry.name === ".git" || entry.name === "coverage" ) { continue; } stack.push(fullPath); continue; } if (entry.isFile() && isSourceFile(fullPath)) { files.push(fullPath); } } } return files; } function collectSharedExtensionSourceFiles(): string[] { return collectPluginSourceFiles(path.join(process.cwd(), "extensions", "shared")); } 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); } const monolithicOffenders: string[] = []; const legacyRoutingOffenders: string[] = []; for (const entryFile of filesToCheck) { let content = ""; try { content = fs.readFileSync(entryFile, "utf8"); } catch { continue; } if (hasMonolithicRootImport(content)) { monolithicOffenders.push(entryFile); } if (hasLegacyRoutingImport(content)) { legacyRoutingOffenders.push(entryFile); } } if (monolithicOffenders.length > 0 || legacyRoutingOffenders.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()) { const relative = path.relative(process.cwd(), file) || file; console.error(`- ${relative}`); } } if (legacyRoutingOffenders.length > 0) { console.error( "Bundled plugin source files must not import legacy openclaw/plugin-sdk/routing.", ); for (const file of legacyRoutingOffenders.toSorted()) { const relative = path.relative(process.cwd(), file) || file; console.error(`- ${relative}`); } } if (monolithicOffenders.length > 0 || legacyRoutingOffenders.length > 0) { console.error( "Use openclaw/plugin-sdk/ for channel plugins, /core for shared routing and startup surfaces, or /compat for broader internals.", ); } process.exit(1); } console.log( `OK: bundled plugin source files use scoped plugin-sdk subpaths (${filesToCheck.size} checked).`, ); } main();