Files
PrismFacet/scripts/install.mjs
hzhang 6dca427187 fix: loadRouters preserves API-registered routers; install.mjs chowns root
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>
2026-05-25 10:31:18 +01:00

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;
}