#!/usr/bin/env node /** * HarborForge Monitor Plugin Installer v0.1.0 */ import { execSync } from 'child_process'; import { existsSync, mkdirSync, copyFileSync, readdirSync, rmSync, readFileSync, writeFileSync, } from 'fs'; import { dirname, join, resolve } from 'path'; import { fileURLToPath } from 'url'; import { homedir, platform } from 'os'; const __filename = fileURLToPath(import.meta.url); const __dirname = resolve(dirname(__filename), '..'); const PLUGIN_NAME = 'harborforge-monitor'; const PLUGIN_SRC_DIR = join(__dirname, 'plugin'); const SERVER_SRC_DIR = join(__dirname, 'server'); const SKILLS_SRC_DIR = join(__dirname, 'skills'); const args = process.argv.slice(2); const options = { openclawProfilePath: null, buildOnly: args.includes('--build-only'), skipCheck: args.includes('--skip-check'), verbose: args.includes('--verbose') || args.includes('-v'), uninstall: args.includes('--uninstall'), installOnly: args.includes('--install'), }; const profileIdx = args.indexOf('--openclaw-profile-path'); if (profileIdx !== -1 && args[profileIdx + 1]) { options.openclawProfilePath = resolve(args[profileIdx + 1]); } function resolveOpenclawPath() { if (options.openclawProfilePath) return options.openclawProfilePath; if (process.env.OPENCLAW_PATH) return resolve(process.env.OPENCLAW_PATH); return join(homedir(), '.openclaw'); } const c = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', }; function log(msg, color = 'reset') { console.log(`${c[color]}${msg}${c.reset}`); } function logStep(n, total, msg) { log(`[${n}/${total}] ${msg}`, 'cyan'); } function logOk(msg) { log(` ✓ ${msg}`, 'green'); } function logWarn(msg) { log(` ⚠ ${msg}`, 'yellow'); } function logErr(msg) { log(` ✗ ${msg}`, 'red'); } function exec(command, opts = {}) { return execSync(command, { cwd: __dirname, stdio: opts.silent ? 'pipe' : 'inherit', encoding: 'utf8', ...opts, }); } function getOpenclawConfig(key, def = undefined) { try { const out = exec(`openclaw config get ${key} --json 2>/dev/null || echo "undefined"`, { silent: true }).trim(); if (out === 'undefined' || out === '') return def; return JSON.parse(out); } catch { return def; } } function setOpenclawConfig(key, value) { exec(`openclaw config set ${key} '${JSON.stringify(value)}' --json`, { silent: true }); } function unsetOpenclawConfig(key) { try { exec(`openclaw config unset ${key}`, { silent: true }); } catch {} } function copyDir(src, dest) { mkdirSync(dest, { recursive: true }); for (const entry of readdirSync(src, { withFileTypes: true })) { const s = join(src, entry.name); const d = join(dest, entry.name); if (entry.name === 'node_modules') continue; entry.isDirectory() ? copyDir(s, d) : copyFileSync(s, d); } } function detectEnvironment() { logStep(1, 5, 'Detecting environment...'); const env = { platform: platform(), nodeVersion: null }; try { env.nodeVersion = exec('node --version', { silent: true }).trim(); logOk(`Node.js ${env.nodeVersion}`); } catch { logErr('Node.js not found'); } try { logOk(`openclaw at ${exec('which openclaw', { silent: true }).trim()}`); } catch { logWarn('openclaw CLI not in PATH'); } return env; } function checkDeps(env) { if (options.skipCheck) { logStep(2, 5, 'Skipping dep checks'); return; } logStep(2, 5, 'Checking dependencies...'); let fail = false; if (!env.nodeVersion || parseInt(env.nodeVersion.slice(1)) < 18) { logErr('Node.js 18+ required'); fail = true; } if (fail) { log('\nInstall missing deps and retry.', 'red'); process.exit(1); } logOk('All deps OK'); } async function build() { logStep(3, 5, 'Building plugin...'); log(' Building TypeScript plugin...', 'blue'); exec('npm install', { cwd: PLUGIN_SRC_DIR, silent: !options.verbose }); exec('npm run build', { cwd: PLUGIN_SRC_DIR, silent: !options.verbose }); logOk('plugin compiled'); } function clearInstallTargets(openclawPath) { const destDir = join(openclawPath, 'plugins', PLUGIN_NAME); if (existsSync(destDir)) { rmSync(destDir, { recursive: true, force: true }); logOk(`Removed ${destDir}`); } } function cleanupConfig(openclawPath) { const destDir = join(openclawPath, 'plugins', PLUGIN_NAME); try { const allow = getOpenclawConfig('plugins.allow', []); const idx = allow.indexOf(PLUGIN_NAME); if (idx !== -1) { allow.splice(idx, 1); setOpenclawConfig('plugins.allow', allow); logOk('Removed from allow list'); } unsetOpenclawConfig(`plugins.entries.${PLUGIN_NAME}`); logOk('Removed plugin entry'); const paths = getOpenclawConfig('plugins.load.paths', []); const pidx = paths.indexOf(destDir); if (pidx !== -1) { paths.splice(pidx, 1); setOpenclawConfig('plugins.load.paths', paths); logOk('Removed from load paths'); } } catch (err) { logWarn(`Config cleanup: ${err.message}`); } } async function install() { if (options.buildOnly) { logStep(4, 5, 'Skipping install (--build-only)'); return null; } logStep(4, 5, 'Installing...'); const openclawPath = resolveOpenclawPath(); const pluginsDir = join(openclawPath, 'plugins'); const destDir = join(pluginsDir, PLUGIN_NAME); log(` OpenClaw path: ${openclawPath}`, 'blue'); if (existsSync(destDir)) { logWarn('Existing install detected, uninstalling before install...'); clearInstallTargets(openclawPath); cleanupConfig(openclawPath); } if (existsSync(destDir)) rmSync(destDir, { recursive: true, force: true }); // Copy compiled plugin mkdirSync(destDir, { recursive: true }); copyDir(PLUGIN_SRC_DIR, destDir); // Copy telemetry server const serverDestDir = join(destDir, 'server'); mkdirSync(serverDestDir, { recursive: true }); copyDir(SERVER_SRC_DIR, serverDestDir); logOk(`Server files → ${serverDestDir}`); // Copy skills if (existsSync(SKILLS_SRC_DIR)) { const skillsDestDir = join(openclawPath, 'skills'); mkdirSync(skillsDestDir, { recursive: true }); copyDir(SKILLS_SRC_DIR, skillsDestDir); logOk(`Skills → ${skillsDestDir}`); } // Install runtime deps exec('npm install --omit=dev', { cwd: destDir, silent: !options.verbose }); logOk('Runtime deps installed'); return { destDir }; } async function configure() { if (options.buildOnly) { logStep(5, 5, 'Skipping config'); return; } logStep(5, 5, 'Configuring OpenClaw...'); const openclawPath = resolveOpenclawPath(); const destDir = join(openclawPath, 'plugins', PLUGIN_NAME); try { const paths = getOpenclawConfig('plugins.load.paths', []); if (!paths.includes(destDir)) { paths.push(destDir); setOpenclawConfig('plugins.load.paths', paths); } logOk(`plugins.load.paths includes ${destDir}`); const allow = getOpenclawConfig('plugins.allow', []); if (!allow.includes(PLUGIN_NAME)) { allow.push(PLUGIN_NAME); setOpenclawConfig('plugins.allow', allow); } logOk(`plugins.allow includes ${PLUGIN_NAME}`); // Note: apiKey must be configured manually by user logOk('Plugin configured (remember to set apiKey in plugins.entries.harborforge-monitor.config)'); } catch (err) { logWarn(`Config failed: ${err.message}`); } } function summary() { logStep(5, 5, 'Done!'); console.log(''); log('╔══════════════════════════════════════════════╗', 'cyan'); log('║ HarborForge Monitor v0.1.0 Install Complete ║', 'cyan'); log('╚══════════════════════════════════════════════╝', 'cyan'); if (options.buildOnly) { log('\nBuild-only — plugin not installed.', 'yellow'); return; } console.log(''); log('Next steps:', 'blue'); log(' 1. Register server in HarborForge Monitor to get apiKey', 'cyan'); log(' 2. Edit ~/.openclaw/openclaw.json under plugins.entries.harborforge-monitor.config:', 'cyan'); log(' {', 'cyan'); log(' "plugins": {', 'cyan'); log(' "entries": {', 'cyan'); log(' "harborforge-monitor": {', 'cyan'); log(' "enabled": true,', 'cyan'); log(' "config": {', 'cyan'); log(' "enabled": true,', 'cyan'); log(' "apiKey": "your-api-key"', 'cyan'); log(' }', 'cyan'); log(' }', 'cyan'); log(' }', 'cyan'); log(' }', 'cyan'); log(' }', 'cyan'); log(' 3. openclaw gateway restart', 'cyan'); console.log(''); } async function uninstall() { log('Uninstalling HarborForge Monitor...', 'cyan'); const openclawPath = resolveOpenclawPath(); clearInstallTargets(openclawPath); cleanupConfig(openclawPath); log('\nRun: openclaw gateway restart', 'yellow'); } async function main() { console.log(''); log('╔══════════════════════════════════════════════╗', 'cyan'); log('║ HarborForge Monitor Plugin Installer v0.1.0 ║', 'cyan'); log('╚══════════════════════════════════════════════╝', 'cyan'); console.log(''); try { const env = detectEnvironment(); if (options.uninstall) { await uninstall(); process.exit(0); } checkDeps(env); await build(); if (!options.buildOnly) { await install(); await configure(); } summary(); } catch (err) { log(`\nInstallation failed: ${err.message}`, 'red'); process.exit(1); } } main();