Two related fixes: 1. core/router-loader.loadRouters() previously called map.clear() before scanning routersDir, which wiped routers registered via __prismFacet.addRouter (the cross-plugin API). Now: track which entries in the map came from a file vs API (filePath sentinel), only delete file-based ones that disappeared between loads. External routers are never touched. 2. scripts/install.mjs: chown installed plugin files to root when the installer is running as root. openclaw 2026.5+ blocks plugins whose files are owned by non-root; rsync/tar from a developer laptop silently broke prism-facet on the next gateway restart. Matches the Meridian + ClawPrompts install.mjs fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
162 lines
4.9 KiB
JavaScript
162 lines
4.9 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { execSync } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const projRoot = path.resolve(__dirname, "..");
|
|
const pluginSrcDir = path.join(projRoot, "plugin");
|
|
const routersSrcDir = path.join(projRoot, "routers");
|
|
|
|
const PLUGIN_ID = "prism-facet";
|
|
const ocDir = path.join(process.env.HOME || "~", ".openclaw");
|
|
const pluginsDir = path.join(ocDir, "plugins");
|
|
const installDir = path.join(pluginsDir, PLUGIN_ID);
|
|
const configPath = path.join(ocDir, "openclaw.json");
|
|
|
|
const args = process.argv.slice(2);
|
|
const action = args[0];
|
|
|
|
if (!action || !["--install", "--uninstall", "--update"].includes(action)) {
|
|
console.log("Usage: node scripts/install.mjs --install | --uninstall | --update");
|
|
process.exit(1);
|
|
}
|
|
|
|
function readConfig() {
|
|
return JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
}
|
|
|
|
function writeConfig(cfg) {
|
|
fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
}
|
|
|
|
function setIfMissing(obj, key, value) {
|
|
if (obj[key] === undefined || obj[key] === null) {
|
|
obj[key] = value;
|
|
}
|
|
}
|
|
|
|
function buildPlugin() {
|
|
console.log("Building plugin...");
|
|
// Compile TypeScript from plugin/ to plugin/ (in-place, JS output)
|
|
execSync("npx tsc --project tsconfig.plugin.json", { cwd: projRoot, stdio: "inherit" });
|
|
}
|
|
|
|
function install() {
|
|
// 1. Build
|
|
buildPlugin();
|
|
|
|
// 2. Clean and copy plugin to install dir
|
|
if (fs.existsSync(installDir)) {
|
|
fs.rmSync(installDir, { recursive: true });
|
|
}
|
|
fs.mkdirSync(installDir, { recursive: true });
|
|
|
|
// Copy compiled plugin files
|
|
copyDirRecursive(pluginSrcDir, installDir, [".ts"]);
|
|
|
|
// Copy routers alongside plugin
|
|
const routersInstallDir = path.join(installDir, "routers");
|
|
if (fs.existsSync(routersSrcDir)) {
|
|
fs.mkdirSync(routersInstallDir, { recursive: true });
|
|
copyDirRecursive(routersSrcDir, routersInstallDir);
|
|
}
|
|
|
|
// Create empty rules.json if not exists
|
|
const rulesPath = path.join(installDir, "rules.json");
|
|
if (!fs.existsSync(rulesPath)) {
|
|
fs.writeFileSync(rulesPath, "{}\n", "utf8");
|
|
}
|
|
|
|
// 3. Update openclaw.json
|
|
const cfg = readConfig();
|
|
const plugins = cfg.plugins ??= {};
|
|
const allow = plugins.allow ??= [];
|
|
const loadPaths = (plugins.load ??= {}).paths ??= [];
|
|
const entries = plugins.entries ??= {};
|
|
|
|
if (!allow.includes(PLUGIN_ID)) allow.push(PLUGIN_ID);
|
|
if (!loadPaths.includes(installDir)) loadPaths.push(installDir);
|
|
|
|
const entry = entries[PLUGIN_ID] ??= {};
|
|
setIfMissing(entry, "enabled", true);
|
|
const hooks = entry.hooks ??= {};
|
|
setIfMissing(hooks, "allowPromptInjection", true);
|
|
|
|
writeConfig(cfg);
|
|
console.log(`Installed ${PLUGIN_ID} to ${installDir}`);
|
|
console.log("Restart gateway: openclaw gateway restart");
|
|
}
|
|
|
|
function uninstall() {
|
|
const cfg = readConfig();
|
|
const plugins = cfg.plugins ?? {};
|
|
|
|
// Remove from allow
|
|
if (Array.isArray(plugins.allow)) {
|
|
plugins.allow = plugins.allow.filter((id) => id !== PLUGIN_ID);
|
|
}
|
|
|
|
// Remove entry
|
|
if (plugins.entries) delete plugins.entries[PLUGIN_ID];
|
|
|
|
// Remove load path
|
|
if (plugins.load?.paths) {
|
|
plugins.load.paths = plugins.load.paths.filter((p) => !p.includes(PLUGIN_ID));
|
|
}
|
|
|
|
writeConfig(cfg);
|
|
|
|
// Remove install dir
|
|
if (fs.existsSync(installDir)) {
|
|
fs.rmSync(installDir, { recursive: true });
|
|
console.log(`Removed ${installDir}`);
|
|
}
|
|
|
|
console.log(`Uninstalled ${PLUGIN_ID}`);
|
|
}
|
|
|
|
function copyDirRecursive(src, dest, excludeExts = []) {
|
|
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
const srcPath = path.join(src, entry.name);
|
|
const destPath = path.join(dest, entry.name);
|
|
if (entry.isDirectory()) {
|
|
if (entry.name === "node_modules") continue;
|
|
copyDirRecursive(srcPath, destPath, excludeExts);
|
|
} else {
|
|
if (excludeExts.some((ext) => entry.name.endsWith(ext))) continue;
|
|
fs.copyFileSync(srcPath, destPath);
|
|
}
|
|
}
|
|
// openclaw 2026.5+ refuses to load plugins whose files are owned by
|
|
// non-root ("suspicious ownership"). fs.copyFileSync preserves source
|
|
// ownership, so rsync/tar from a developer laptop (uid 1000) breaks
|
|
// the plugin on next gateway restart. Force chown to root when we
|
|
// can (root-only); silently skip otherwise (dev mode under
|
|
// unprivileged user — uid checks don't apply there).
|
|
try {
|
|
if (process.getuid && process.getuid() === 0) chownRecursive(dest);
|
|
} catch {}
|
|
}
|
|
|
|
function chownRecursive(dir) {
|
|
fs.chownSync(dir, 0, 0);
|
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const p = path.join(dir, entry.name);
|
|
if (entry.isDirectory()) chownRecursive(p);
|
|
else fs.chownSync(p, 0, 0);
|
|
}
|
|
}
|
|
|
|
switch (action) {
|
|
case "--install":
|
|
case "--update":
|
|
install();
|
|
break;
|
|
case "--uninstall":
|
|
uninstall();
|
|
break;
|
|
}
|