#!/usr/bin/env zx /** * bundle-openclaw-plugins.mjs * * Build a self-contained mirror of OpenClaw third-party plugins for packaging. * Current plugins: * - @soimy/dingtalk -> build/openclaw-plugins/dingtalk * * The output plugin directory contains: * - plugin source files (index.ts, openclaw.plugin.json, package.json, ...) * - plugin runtime node_modules/ (flattened direct + transitive deps) */ import 'zx/globals'; import fs from 'node:fs'; import path from 'node:path'; const ROOT = path.resolve(__dirname, '..'); const OUTPUT_ROOT = path.join(ROOT, 'build', 'openclaw-plugins'); const NODE_MODULES = path.join(ROOT, 'node_modules'); const PLUGINS = [ { npmName: '@soimy/dingtalk', pluginId: 'dingtalk' }, ]; function getVirtualStoreNodeModules(realPkgPath) { let dir = realPkgPath; while (dir !== path.dirname(dir)) { if (path.basename(dir) === 'node_modules') return dir; dir = path.dirname(dir); } return null; } function listPackages(nodeModulesDir) { const result = []; if (!fs.existsSync(nodeModulesDir)) return result; for (const entry of fs.readdirSync(nodeModulesDir)) { if (entry === '.bin') continue; const entryPath = path.join(nodeModulesDir, entry); const stat = fs.lstatSync(entryPath); if (entry.startsWith('@')) { if (!(stat.isDirectory() || stat.isSymbolicLink())) continue; let scopeEntries = []; try { scopeEntries = fs.readdirSync(entryPath); } catch { continue; } for (const sub of scopeEntries) { result.push({ name: `${entry}/${sub}`, fullPath: path.join(entryPath, sub), }); } } else { result.push({ name: entry, fullPath: entryPath }); } } return result; } function bundleOnePlugin({ npmName, pluginId }) { const pkgPath = path.join(NODE_MODULES, ...npmName.split('/')); if (!fs.existsSync(pkgPath)) { throw new Error(`Missing dependency "${npmName}". Run pnpm install first.`); } const realPluginPath = fs.realpathSync(pkgPath); const outputDir = path.join(OUTPUT_ROOT, pluginId); echo`📦 Bundling plugin ${npmName} -> ${outputDir}`; if (fs.existsSync(outputDir)) { fs.rmSync(outputDir, { recursive: true, force: true }); } fs.mkdirSync(outputDir, { recursive: true }); // 1) Copy plugin package itself fs.cpSync(realPluginPath, outputDir, { recursive: true, dereference: true }); // 2) Collect transitive deps from pnpm virtual store const collected = new Map(); const queue = []; const rootVirtualNM = getVirtualStoreNodeModules(realPluginPath); if (!rootVirtualNM) { throw new Error(`Cannot resolve virtual store node_modules for ${npmName}`); } queue.push({ nodeModulesDir: rootVirtualNM, skipPkg: npmName }); const SKIP_PACKAGES = new Set(['typescript', '@playwright/test']); const SKIP_SCOPES = ['@types/']; while (queue.length > 0) { const { nodeModulesDir, skipPkg } = queue.shift(); for (const { name, fullPath } of listPackages(nodeModulesDir)) { if (name === skipPkg) continue; if (SKIP_PACKAGES.has(name) || SKIP_SCOPES.some((s) => name.startsWith(s))) continue; let realPath; try { realPath = fs.realpathSync(fullPath); } catch { continue; } if (collected.has(realPath)) continue; collected.set(realPath, name); const depVirtualNM = getVirtualStoreNodeModules(realPath); if (depVirtualNM && depVirtualNM !== nodeModulesDir) { queue.push({ nodeModulesDir: depVirtualNM, skipPkg: name }); } } } // 3) Copy flattened deps into plugin/node_modules const outputNodeModules = path.join(outputDir, 'node_modules'); fs.mkdirSync(outputNodeModules, { recursive: true }); let copiedCount = 0; let skippedDupes = 0; const copiedNames = new Set(); for (const [realPath, pkgName] of collected) { if (copiedNames.has(pkgName)) { skippedDupes++; continue; } copiedNames.add(pkgName); const dest = path.join(outputNodeModules, pkgName); try { fs.mkdirSync(path.dirname(dest), { recursive: true }); fs.cpSync(realPath, dest, { recursive: true, dereference: true }); copiedCount++; } catch (err) { echo` ⚠️ Skipped ${pkgName}: ${err.message}`; } } const manifestPath = path.join(outputDir, 'openclaw.plugin.json'); if (!fs.existsSync(manifestPath)) { throw new Error(`Missing openclaw.plugin.json in bundled plugin output: ${pluginId}`); } echo` ✅ ${pluginId}: copied ${copiedCount} deps (skipped dupes: ${skippedDupes})`; } echo`📦 Bundling OpenClaw plugin mirrors...`; fs.mkdirSync(OUTPUT_ROOT, { recursive: true }); for (const plugin of PLUGINS) { bundleOnePlugin(plugin); } echo`✅ Plugin mirrors ready: ${OUTPUT_ROOT}`;