#!/usr/bin/env node /** * Command-line helper for the Plugin Build app packaging, mobile, or * Playwright automation lane. */ import { spawn } from "node:child_process"; import fs from "node:fs"; import path from "node:path"; import process from "node:process"; import { fileURLToPath } from "node:url"; import { CAPACITOR_PLUGIN_NAMES, NATIVE_PLUGINS_ROOT, } from "./capacitor-plugin-names.mjs"; const scriptFile = fileURLToPath(import.meta.url); const __dirname = path.dirname(scriptFile); const verbosePluginBuild = process.env.ELIZA_VERBOSE_PLUGIN_BUILD === "1"; const WORKSPACE_RUNTIME_PACKAGES = new Map([ [ "@elizaos/core", path.resolve(NATIVE_PLUGINS_ROOT, "..", "packages", "core"), ], ]); // Only these values in a plugin's `platforms` array are treated as build-host // gates. Anything else (e.g. "node", "browser") is a runtime hint and does // not block building on the current host. export const OS_PLATFORMS = new Set(["darwin", "linux", "win32"]); /** * Decide whether a plugin should be built on the current host, based on the * `eliza.platforms` / `elizaos.platforms` allowlist in its package.json, or * by detecting Capacitor mobile plugins via their peer dependency. * * Rules (in order): * 1. Explicit `platforms` pure-OS allowlist → build only when host is listed. * 2. `platforms` mixing runtime hints (e.g. "node", "browser") → build everywhere. * 3. No `platforms` but `@capacitor/core` peer dep → mobile-only, skip on desktop. * 4. No signal → build everywhere. * * @param {unknown} pkg — parsed package.json (or undefined) * @param {string} hostPlatform — the current `process.platform` value * @returns {boolean} */ export function shouldBuildPluginForHost(pkg, hostPlatform) { const platforms = (pkg && typeof pkg === "object" && pkg.eliza?.platforms) ?? (pkg && typeof pkg === "object" && pkg.elizaos?.platforms); if (Array.isArray(platforms) && platforms.length > 0) { const isPureOsAllowlist = platforms.every((p) => OS_PLATFORMS.has(p)); if (!isPureOsAllowlist) { return true; } return platforms.includes(hostPlatform); } // No explicit metadata — @capacitor/core peer dep is a reliable mobile-only // signal (every proper Capacitor plugin lists it). Skip on all desktop hosts. const peerDeps = (pkg && typeof pkg === "object" && pkg.peerDependencies) ?? {}; if ("@capacitor/core" in peerDeps) { return false; } return true; } const NATIVE_PLUGIN_DIR_PREFIX = "plugin-native-"; function pluginDirFor(pluginsDir, name) { return path.join(pluginsDir, `${NATIVE_PLUGIN_DIR_PREFIX}${name}`); } function readPluginPackageJson(pluginsDir, name) { const pkgPath = path.join(pluginDirFor(pluginsDir, name), "package.json"); const raw = fs.readFileSync(pkgPath, "utf8"); try { return JSON.parse(raw); } catch (error) { throw new Error( `[plugins] ${pkgPath} is not valid JSON: ${ error instanceof Error ? error.message : String(error) }`, ); } } function hasBuildScript(pkg) { return ( pkg && typeof pkg === "object" && pkg.scripts && typeof pkg.scripts === "object" && typeof pkg.scripts.build === "string" ); } function run(command, args, cwd) { return new Promise((resolve, reject) => { const child = spawn(command, args, { cwd, stdio: "inherit", env: process.env, }); child.on("error", (error) => { reject(new Error(`${command} failed to start: ${error.message}`)); }); child.on("exit", (code, signal) => { if (signal) { reject(new Error(`${command} exited due to signal ${signal}`)); return; } if ((code ?? 1) !== 0) { reject(new Error(`${command} exited with code ${code ?? 1}`)); return; } resolve(); }); }); } function logVerbose(message) { if (verbosePluginBuild) { console.log(message); } } function hasPackageDependency(pkg, packageName) { if (!pkg || typeof pkg !== "object") return false; for (const field of [ "dependencies", "devDependencies", "peerDependencies", "optionalDependencies", ]) { const deps = pkg[field]; if (deps && typeof deps === "object" && packageName in deps) { return true; } } return false; } async function buildWorkspaceRuntimePackagesForPlugins(pluginEntries) { if (process.env.ELIZA_DEV_SOURCE === "1") { logVerbose( "[plugins] skipping workspace runtime package builds (ELIZA_DEV_SOURCE=1)", ); return; } const requiredPackages = new Set(); for (const { pkg } of pluginEntries) { for (const packageName of WORKSPACE_RUNTIME_PACKAGES.keys()) { if (hasPackageDependency(pkg, packageName)) { requiredPackages.add(packageName); } } } for (const packageName of requiredPackages) { const packageDir = WORKSPACE_RUNTIME_PACKAGES.get(packageName); const packageJsonPath = path.join(packageDir, "package.json"); if (!fs.existsSync(packageJsonPath)) { throw new Error( `[plugins] ${packageName} dependency is required but ${packageJsonPath} does not exist`, ); } const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); if (!pkg?.scripts?.build) { throw new Error( `[plugins] ${packageName} dependency is required but has no build script`, ); } console.log(`[plugins] building workspace dependency ${packageName}`); await run("bun", ["run", "build"], packageDir); } } async function main() { const pluginsDir = NATIVE_PLUGINS_ROOT; const pluginNames = CAPACITOR_PLUGIN_NAMES; const skipPlugins = process.env.SKIP_NATIVE_PLUGINS === "1" || process.env.CI === "true"; if (skipPlugins) { console.log( "[plugins] skipping native plugin builds (CI or explicitly disabled)", ); return; } const buildablePlugins = pluginNames .map((name) => ({ name, pkg: readPluginPackageJson(pluginsDir, name) })) .filter(({ name, pkg }) => { if (!hasBuildScript(pkg)) { logVerbose(`[plugin:${name}] skipping — no build script`); return false; } if (shouldBuildPluginForHost(pkg, process.platform)) { return true; } const platforms = pkg?.eliza?.platforms ?? pkg?.elizaos?.platforms; logVerbose( `[plugin:${name}] skipping — declares platforms=${JSON.stringify( platforms, )}, host is ${process.platform}`, ); return false; }); await buildWorkspaceRuntimePackagesForPlugins(buildablePlugins); await Promise.all( buildablePlugins.map(async ({ name }) => { logVerbose(`[plugin:${name}] building...`); await run("bun", ["run", "build"], pluginDirFor(pluginsDir, name)); logVerbose(`[plugin:${name}] done`); }), ); } const isDirectRun = process.argv[1] && path.resolve(process.argv[1]) === path.resolve(scriptFile); if (isDirectRun) { await main(); }