import fs from "node:fs"; import path from "node:path"; import { pathToFileURL } from "node:url"; import { removeFileIfExists, writeTextFileIfChanged } from "./runtime-postbuild-shared.mjs"; export function rewritePackageExtensions(entries) { if (!Array.isArray(entries)) { return undefined; } return entries .filter((entry) => typeof entry === "string" && entry.trim().length > 0) .map((entry) => { const normalized = entry.replace(/^\.\//, ""); const rewritten = normalized.replace(/\.[^.]+$/u, ".js"); return `./${rewritten}`; }); } function ensurePathInsideRoot(rootDir, rawPath) { const resolved = path.resolve(rootDir, rawPath); const relative = path.relative(rootDir, resolved); if ( relative === "" || relative === "." || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative)) ) { return resolved; } throw new Error(`path escapes plugin root: ${rawPath}`); } function copyDeclaredPluginSkillPaths(params) { const skills = Array.isArray(params.manifest.skills) ? params.manifest.skills : []; const copiedSkills = []; for (const raw of skills) { if (typeof raw !== "string" || raw.trim().length === 0) { continue; } const normalized = raw.replace(/^\.\//u, ""); const sourcePath = ensurePathInsideRoot(params.pluginDir, raw); if (!fs.existsSync(sourcePath)) { // Some Docker/lightweight builds intentionally omit optional plugin-local // dependencies. Only advertise skill paths that were actually bundled. console.warn( `[bundled-plugin-metadata] skipping missing skill path ${sourcePath} (plugin ${params.manifest.id ?? path.basename(params.pluginDir)})`, ); continue; } const targetPath = ensurePathInsideRoot(params.distPluginDir, normalized); fs.mkdirSync(path.dirname(targetPath), { recursive: true }); fs.cpSync(sourcePath, targetPath, { dereference: true, force: true, recursive: true, }); copiedSkills.push(raw); } return copiedSkills; } export function copyBundledPluginMetadata(params = {}) { const repoRoot = params.cwd ?? params.repoRoot ?? process.cwd(); const extensionsRoot = path.join(repoRoot, "extensions"); const distExtensionsRoot = path.join(repoRoot, "dist", "extensions"); if (!fs.existsSync(extensionsRoot)) { return; } const sourcePluginDirs = new Set(); for (const dirent of fs.readdirSync(extensionsRoot, { withFileTypes: true })) { if (!dirent.isDirectory()) { continue; } sourcePluginDirs.add(dirent.name); const pluginDir = path.join(extensionsRoot, dirent.name); const manifestPath = path.join(pluginDir, "openclaw.plugin.json"); const distPluginDir = path.join(distExtensionsRoot, dirent.name); const distManifestPath = path.join(distPluginDir, "openclaw.plugin.json"); const distPackageJsonPath = path.join(distPluginDir, "package.json"); if (!fs.existsSync(manifestPath)) { removeFileIfExists(distManifestPath); removeFileIfExists(distPackageJsonPath); continue; } const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); const copiedSkills = copyDeclaredPluginSkillPaths({ manifest, pluginDir, distPluginDir }); const bundledManifest = Array.isArray(manifest.skills) ? { ...manifest, skills: copiedSkills } : manifest; writeTextFileIfChanged(distManifestPath, `${JSON.stringify(bundledManifest, null, 2)}\n`); const packageJsonPath = path.join(pluginDir, "package.json"); if (!fs.existsSync(packageJsonPath)) { removeFileIfExists(distPackageJsonPath); continue; } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); if (packageJson.openclaw && "extensions" in packageJson.openclaw) { packageJson.openclaw = { ...packageJson.openclaw, extensions: rewritePackageExtensions(packageJson.openclaw.extensions), }; } writeTextFileIfChanged(distPackageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); } if (!fs.existsSync(distExtensionsRoot)) { return; } for (const dirent of fs.readdirSync(distExtensionsRoot, { withFileTypes: true })) { if (!dirent.isDirectory() || sourcePluginDirs.has(dirent.name)) { continue; } const distPluginDir = path.join(distExtensionsRoot, dirent.name); removeFileIfExists(path.join(distPluginDir, "openclaw.plugin.json")); removeFileIfExists(path.join(distPluginDir, "package.json")); } } if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) { copyBundledPluginMetadata(); }