#!/usr/bin/env node /** * PaddedCell Plugin Installer v0.2.0 * * Usage: * node install.mjs * node install.mjs --openclaw-profile-path /path/to/.openclaw * node install.mjs --build-only * node install.mjs --skip-check * node install.mjs --uninstall */ import { execSync } from 'child_process'; import { existsSync, mkdirSync, copyFileSync, chmodSync, readdirSync, rmSync } 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 = 'padded-cell'; const SRC_DIST_DIR = join(__dirname, 'dist', PLUGIN_NAME); // Parse arguments 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'), }; // Parse --openclaw-profile-path value const profileIdx = args.indexOf('--openclaw-profile-path'); if (profileIdx !== -1 && args[profileIdx + 1]) { options.openclawProfilePath = resolve(args[profileIdx + 1]); } // Resolve openclaw path: --openclaw-profile-path → $OPENCLAW_PATH → ~/.openclaw function resolveOpenclawPath() { if (options.openclawProfilePath) return options.openclawProfilePath; if (process.env.OPENCLAW_PATH) return resolve(process.env.OPENCLAW_PATH); return join(homedir(), '.openclaw'); } // Colors 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; // skip node_modules entry.isDirectory() ? copyDir(s, d) : copyFileSync(s, d); } } // ── Step 1: Detect ────────────────────────────────────────────────────── function detectEnvironment() { logStep(1, 6, 'Detecting environment...'); const env = { platform: platform(), nodeVersion: null, goVersion: null }; try { env.nodeVersion = exec('node --version', { silent: true }).trim(); logOk(`Node.js ${env.nodeVersion}`); } catch { logErr('Node.js not found'); } try { env.goVersion = exec('go version', { silent: true }).trim(); logOk(`Go: ${env.goVersion}`); } catch { logErr('Go not found'); } try { logOk(`openclaw at ${exec('which openclaw', { silent: true }).trim()}`); } catch { logWarn('openclaw CLI not in PATH'); } return env; } // ── Step 2: Check deps ────────────────────────────────────────────────── function checkDeps(env) { if (options.skipCheck) { logStep(2, 6, 'Skipping dep checks'); return; } logStep(2, 6, 'Checking dependencies...'); let fail = false; if (!env.nodeVersion || parseInt(env.nodeVersion.slice(1)) < 18) { logErr('Node.js 18+ required'); fail = true; } if (!env.goVersion) { logErr('Go 1.22+ required'); fail = true; } if (fail) { log('\nInstall missing deps and retry.', 'red'); process.exit(1); } logOk('All deps OK'); } // ── Step 3: Build ─────────────────────────────────────────────────────── async function build() { logStep(3, 6, 'Building components...'); // pass_mgr (Go) log(' Building pass_mgr...', 'blue'); const pmDir = join(__dirname, 'pass_mgr'); exec('go mod tidy', { cwd: pmDir, silent: !options.verbose }); exec('go build -o dist/pass_mgr src/main.go', { cwd: pmDir, silent: !options.verbose }); chmodSync(join(pmDir, 'dist', 'pass_mgr'), 0o755); logOk('pass_mgr'); // pcguard (Go) log(' Building pcguard...', 'blue'); const pgDir = join(__dirname, 'pcguard'); exec('go mod tidy', { cwd: pgDir, silent: !options.verbose }); exec('go build -o dist/pcguard src/main.go', { cwd: pgDir, silent: !options.verbose }); chmodSync(join(pgDir, 'dist', 'pcguard'), 0o755); logOk('pcguard'); // Plugin (TypeScript) log(' Building plugin...', 'blue'); const pluginDir = join(__dirname, 'plugin'); exec('npm install', { cwd: pluginDir, silent: !options.verbose }); exec('npx tsc', { cwd: pluginDir, silent: !options.verbose }); logOk('plugin'); } // ── Step 4: Install ───────────────────────────────────────────────────── async function install() { if (options.buildOnly) { logStep(4, 6, 'Skipping install (--build-only)'); return null; } logStep(4, 6, 'Installing...'); const openclawPath = resolveOpenclawPath(); const binDir = join(openclawPath, 'bin'); const pluginsDir = join(openclawPath, 'plugins'); const destDir = join(pluginsDir, PLUGIN_NAME); log(` OpenClaw path: ${openclawPath}`, 'blue'); // Copy dist/padded-cell → plugins/padded-cell if (existsSync(destDir)) rmSync(destDir, { recursive: true, force: true }); copyDir(SRC_DIST_DIR, destDir); // Copy openclaw.plugin.json copyFileSync(join(__dirname, 'plugin', 'openclaw.plugin.json'), join(destDir, 'openclaw.plugin.json')); logOk(`Plugin files → ${destDir}`); // Install runtime deps into dest (express, ws) exec('npm install --omit=dev', { cwd: destDir, silent: !options.verbose }); logOk('Runtime deps installed'); // Binaries mkdirSync(binDir, { recursive: true }); const bins = [ { name: 'pass_mgr', src: join(__dirname, 'pass_mgr', 'dist', 'pass_mgr') }, { name: 'pcguard', src: join(__dirname, 'pcguard', 'dist', 'pcguard') }, ]; for (const b of bins) { const dest = join(binDir, b.name); copyFileSync(b.src, dest); chmodSync(dest, 0o755); logOk(`${b.name} → ${dest}`); } return { binDir, destDir }; } // ── Step 5: Configure ─────────────────────────────────────────────────── async function configure() { if (options.buildOnly) { logStep(5, 6, 'Skipping config'); return; } logStep(5, 6, 'Configuring OpenClaw...'); const openclawPath = resolveOpenclawPath(); const destDir = join(openclawPath, 'plugins', PLUGIN_NAME); const passMgrPath = join(openclawPath, 'bin', 'pass_mgr'); try { // plugins.load.paths const paths = getOpenclawConfig('plugins.load.paths', []); if (!paths.includes(destDir)) { paths.push(destDir); setOpenclawConfig('plugins.load.paths', paths); } logOk(`plugins.load.paths includes ${destDir}`); // plugins.allow const allow = getOpenclawConfig('plugins.allow', []); if (!allow.includes(PLUGIN_NAME)) { allow.push(PLUGIN_NAME); setOpenclawConfig('plugins.allow', allow); } logOk(`plugins.allow includes ${PLUGIN_NAME}`); // plugins.entries const plugins = getOpenclawConfig('plugins', {}); plugins.entries = plugins.entries || {}; plugins.entries[PLUGIN_NAME] = { enabled: true, config: { enabled: true, passMgrPath, openclawProfilePath: openclawPath }, }; setOpenclawConfig('plugins', plugins); logOk('Plugin entry configured'); } catch (err) { logWarn(`Config failed: ${err.message}`); } // Check pass_mgr init if (existsSync(join(homedir(), '.pass_mgr', 'config.json'))) { logOk('pass_mgr already initialized'); } else { logWarn(`pass_mgr not initialized — run: ${passMgrPath} admin init`); } } // ── Step 6: Summary ───────────────────────────────────────────────────── function summary(result) { logStep(6, 6, 'Done!'); console.log(''); log('╔══════════════════════════════════════════════╗', 'cyan'); log('║ PaddedCell v0.2.0 Install Complete ║', 'cyan'); log('╚══════════════════════════════════════════════╝', 'cyan'); if (options.buildOnly) { log('\nBuild-only — binaries not installed.', 'yellow'); return; } console.log(''); log('Next steps:', 'blue'); log(' 1. openclaw gateway restart', 'cyan'); const openclawPath = resolveOpenclawPath(); const pmPath = join(openclawPath, 'bin', 'pass_mgr'); if (!existsSync(join(homedir(), '.pass_mgr', 'config.json'))) { log(` 2. ${pmPath} admin init`, 'cyan'); } console.log(''); } // ── Uninstall ─────────────────────────────────────────────────────────── async function uninstall() { log('Uninstalling PaddedCell...', 'cyan'); const openclawPath = resolveOpenclawPath(); // Remove binaries for (const name of ['pass_mgr', 'pcguard']) { const p = join(openclawPath, 'bin', name); if (existsSync(p)) { rmSync(p); logOk(`Removed ${p}`); } } // Remove plugin dir const destDir = join(openclawPath, 'plugins', PLUGIN_NAME); if (existsSync(destDir)) { rmSync(destDir, { recursive: true }); logOk(`Removed ${destDir}`); } // Remove config 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}`); } log('\nRun: openclaw gateway restart', 'yellow'); } // ── Main ──────────────────────────────────────────────────────────────── async function main() { console.log(''); log('╔══════════════════════════════════════════════╗', 'cyan'); log('║ PaddedCell Plugin Installer v0.2.0 ║', 'cyan'); log('╚══════════════════════════════════════════════╝', 'cyan'); console.log(''); try { const env = detectEnvironment(); if (options.uninstall) { await uninstall(); process.exit(0); } checkDeps(env); await build(); const result = await install(); await configure(); summary(result); } catch (err) { log(`\nInstallation failed: ${err.message}`, 'red'); process.exit(1); } } main();