refactor(installer): replace bash installer logic with node-only implementation
This commit is contained in:
202
scripts/install-whispergate-openclaw.mjs
Executable file
202
scripts/install-whispergate-openclaw.mjs
Executable file
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import { execFileSync } from "node:child_process";
|
||||
|
||||
const modeArg = process.argv[2];
|
||||
if (modeArg !== "--install" && modeArg !== "--uninstall") {
|
||||
console.error("Usage: install-whispergate-openclaw.mjs --install | --uninstall");
|
||||
process.exit(2);
|
||||
}
|
||||
const mode = modeArg === "--install" ? "install" : "uninstall";
|
||||
|
||||
const env = process.env;
|
||||
const OPENCLAW_CONFIG_PATH = env.OPENCLAW_CONFIG_PATH || path.join(os.homedir(), ".openclaw", "openclaw.json");
|
||||
const PLUGIN_PATH = env.PLUGIN_PATH || "/root/.openclaw/workspace-operator/WhisperGate/dist/whispergate";
|
||||
const NO_REPLY_PROVIDER_ID = env.NO_REPLY_PROVIDER_ID || "whisper-gateway";
|
||||
const NO_REPLY_MODEL_ID = env.NO_REPLY_MODEL_ID || "no-reply";
|
||||
const NO_REPLY_BASE_URL = env.NO_REPLY_BASE_URL || "http://127.0.0.1:8787/v1";
|
||||
const NO_REPLY_API_KEY = env.NO_REPLY_API_KEY || "wg-local-test-token";
|
||||
const LIST_MODE = env.LIST_MODE || "human-list";
|
||||
const HUMAN_LIST_JSON = env.HUMAN_LIST_JSON || '["561921120408698910","1474088632750047324"]';
|
||||
const AGENT_LIST_JSON = env.AGENT_LIST_JSON || "[]";
|
||||
const CHANNEL_POLICIES_FILE = (env.CHANNEL_POLICIES_FILE || "~/.openclaw/whispergate-channel-policies.json").replace(/^~(?=$|\/)/, os.homedir());
|
||||
const CHANNEL_POLICIES_JSON = env.CHANNEL_POLICIES_JSON || "{}";
|
||||
const END_SYMBOLS_JSON = env.END_SYMBOLS_JSON || '["🔚"]';
|
||||
|
||||
const STATE_DIR = (env.STATE_DIR || "~/.openclaw/whispergate-install-records").replace(/^~(?=$|\/)/, os.homedir());
|
||||
const LATEST_RECORD_LINK = (env.LATEST_RECORD_LINK || "~/.openclaw/whispergate-install-record-latest.json").replace(/^~(?=$|\/)/, os.homedir());
|
||||
|
||||
const ts = new Date().toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
|
||||
const BACKUP_PATH = `${OPENCLAW_CONFIG_PATH}.bak-whispergate-${mode}-${ts}`;
|
||||
const RECORD_PATH = path.join(STATE_DIR, `whispergate-${ts}.json`);
|
||||
|
||||
const PATH_PLUGINS_LOAD = "plugins.load.paths";
|
||||
const PATH_PLUGIN_ENTRY = "plugins.entries.whispergate";
|
||||
const PROVIDER_PATH = `models.providers["${NO_REPLY_PROVIDER_ID}"]`;
|
||||
|
||||
function runOpenclaw(args, { allowFail = false } = {}) {
|
||||
try {
|
||||
return execFileSync("openclaw", args, { encoding: "utf8" }).trim();
|
||||
} catch (e) {
|
||||
if (allowFail) return null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function getJson(pathKey) {
|
||||
const out = runOpenclaw(["config", "get", pathKey, "--json"], { allowFail: true });
|
||||
if (out == null || out === "") return { exists: false };
|
||||
return { exists: true, value: JSON.parse(out) };
|
||||
}
|
||||
|
||||
function setJson(pathKey, value) {
|
||||
runOpenclaw(["config", "set", pathKey, JSON.stringify(value), "--json"]);
|
||||
}
|
||||
|
||||
function unsetPath(pathKey) {
|
||||
runOpenclaw(["config", "unset", pathKey], { allowFail: true });
|
||||
}
|
||||
|
||||
function writeRecord(modeName, before, after) {
|
||||
fs.mkdirSync(STATE_DIR, { recursive: true });
|
||||
const rec = {
|
||||
mode: modeName,
|
||||
timestamp: ts,
|
||||
openclawConfigPath: OPENCLAW_CONFIG_PATH,
|
||||
backupPath: BACKUP_PATH,
|
||||
paths: before,
|
||||
applied: after,
|
||||
};
|
||||
fs.writeFileSync(RECORD_PATH, JSON.stringify(rec, null, 2));
|
||||
fs.copyFileSync(RECORD_PATH, LATEST_RECORD_LINK);
|
||||
}
|
||||
|
||||
function readRecord(file) {
|
||||
return JSON.parse(fs.readFileSync(file, "utf8"));
|
||||
}
|
||||
|
||||
if (!fs.existsSync(OPENCLAW_CONFIG_PATH)) {
|
||||
console.error(`[whispergate] config not found: ${OPENCLAW_CONFIG_PATH}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (mode === "install") {
|
||||
fs.copyFileSync(OPENCLAW_CONFIG_PATH, BACKUP_PATH);
|
||||
console.log(`[whispergate] backup: ${BACKUP_PATH}`);
|
||||
|
||||
if (!fs.existsSync(CHANNEL_POLICIES_FILE)) {
|
||||
fs.mkdirSync(path.dirname(CHANNEL_POLICIES_FILE), { recursive: true });
|
||||
fs.writeFileSync(CHANNEL_POLICIES_FILE, `${CHANNEL_POLICIES_JSON}\n`);
|
||||
console.log(`[whispergate] initialized channel policies file: ${CHANNEL_POLICIES_FILE}`);
|
||||
}
|
||||
|
||||
const before = {
|
||||
[PATH_PLUGINS_LOAD]: getJson(PATH_PLUGINS_LOAD),
|
||||
[PATH_PLUGIN_ENTRY]: getJson(PATH_PLUGIN_ENTRY),
|
||||
[PROVIDER_PATH]: getJson(PROVIDER_PATH),
|
||||
};
|
||||
|
||||
try {
|
||||
const pluginsNow = getJson("plugins").value || {};
|
||||
const plugins = typeof pluginsNow === "object" ? pluginsNow : {};
|
||||
plugins.load = plugins.load && typeof plugins.load === "object" ? plugins.load : {};
|
||||
const paths = Array.isArray(plugins.load.paths) ? plugins.load.paths : [];
|
||||
if (!paths.includes(PLUGIN_PATH)) paths.push(PLUGIN_PATH);
|
||||
plugins.load.paths = paths;
|
||||
plugins.entries = plugins.entries && typeof plugins.entries === "object" ? plugins.entries : {};
|
||||
plugins.entries.whispergate = {
|
||||
enabled: true,
|
||||
config: {
|
||||
enabled: true,
|
||||
discordOnly: true,
|
||||
listMode: LIST_MODE,
|
||||
humanList: JSON.parse(HUMAN_LIST_JSON),
|
||||
agentList: JSON.parse(AGENT_LIST_JSON),
|
||||
channelPoliciesFile: CHANNEL_POLICIES_FILE,
|
||||
endSymbols: JSON.parse(END_SYMBOLS_JSON),
|
||||
noReplyProvider: NO_REPLY_PROVIDER_ID,
|
||||
noReplyModel: NO_REPLY_MODEL_ID,
|
||||
},
|
||||
};
|
||||
setJson("plugins", plugins);
|
||||
|
||||
setJson(PROVIDER_PATH, {
|
||||
baseUrl: NO_REPLY_BASE_URL,
|
||||
apiKey: NO_REPLY_API_KEY,
|
||||
api: "openai-completions",
|
||||
models: [
|
||||
{
|
||||
id: NO_REPLY_MODEL_ID,
|
||||
name: `${NO_REPLY_MODEL_ID} (Custom Provider)`,
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 4096,
|
||||
maxTokens: 4096,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const after = {
|
||||
[PATH_PLUGINS_LOAD]: getJson(PATH_PLUGINS_LOAD),
|
||||
[PATH_PLUGIN_ENTRY]: getJson(PATH_PLUGIN_ENTRY),
|
||||
[PROVIDER_PATH]: getJson(PROVIDER_PATH),
|
||||
};
|
||||
writeRecord("install", before, after);
|
||||
console.log("[whispergate] install ok");
|
||||
console.log(`[whispergate] record: ${RECORD_PATH}`);
|
||||
} catch (e) {
|
||||
fs.copyFileSync(BACKUP_PATH, OPENCLAW_CONFIG_PATH);
|
||||
console.error(`[whispergate] install failed; rollback complete: ${String(e)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
const recFile = env.RECORD_FILE || (fs.existsSync(LATEST_RECORD_LINK) ? LATEST_RECORD_LINK : "");
|
||||
if (!recFile || !fs.existsSync(recFile)) {
|
||||
console.error("[whispergate] no record found. set RECORD_FILE=<path> or install first.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
fs.copyFileSync(OPENCLAW_CONFIG_PATH, BACKUP_PATH);
|
||||
console.log(`[whispergate] backup before uninstall: ${BACKUP_PATH}`);
|
||||
|
||||
const rec = readRecord(recFile);
|
||||
const before = rec.applied || {};
|
||||
const target = rec.paths || {};
|
||||
|
||||
try {
|
||||
const pluginsNow = getJson("plugins").value || {};
|
||||
const plugins = typeof pluginsNow === "object" ? pluginsNow : {};
|
||||
plugins.load = plugins.load && typeof plugins.load === "object" ? plugins.load : {};
|
||||
plugins.entries = plugins.entries && typeof plugins.entries === "object" ? plugins.entries : {};
|
||||
|
||||
if (target[PATH_PLUGINS_LOAD]?.exists) plugins.load.paths = target[PATH_PLUGINS_LOAD].value;
|
||||
else delete plugins.load.paths;
|
||||
|
||||
if (target[PATH_PLUGIN_ENTRY]?.exists) plugins.entries.whispergate = target[PATH_PLUGIN_ENTRY].value;
|
||||
else delete plugins.entries.whispergate;
|
||||
|
||||
setJson("plugins", plugins);
|
||||
|
||||
for (const k of Object.keys(target)) {
|
||||
if (!k.startsWith("models.providers[")) continue;
|
||||
if (target[k]?.exists) setJson(k, target[k].value);
|
||||
else unsetPath(k);
|
||||
}
|
||||
|
||||
const after = {
|
||||
[PATH_PLUGINS_LOAD]: getJson(PATH_PLUGINS_LOAD),
|
||||
[PATH_PLUGIN_ENTRY]: getJson(PATH_PLUGIN_ENTRY),
|
||||
[PROVIDER_PATH]: getJson(PROVIDER_PATH),
|
||||
};
|
||||
writeRecord("uninstall", before, after);
|
||||
console.log("[whispergate] uninstall ok");
|
||||
console.log(`[whispergate] record: ${RECORD_PATH}`);
|
||||
} catch (e) {
|
||||
fs.copyFileSync(BACKUP_PATH, OPENCLAW_CONFIG_PATH);
|
||||
console.error(`[whispergate] uninstall failed; rollback complete: ${String(e)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user