#!/usr/bin/env node /** * PaddedCell Plugin Installer * * Usage: * node install.mjs * node install.mjs --prefix /usr/local * node install.mjs --build-only * node install.mjs --skip-check * node install.mjs --uninstall * node install.mjs --uninstall --prefix /usr/local */ import { execSync } from 'child_process'; import { existsSync, mkdirSync, copyFileSync, writeFileSync, chmodSync, readdirSync, statSync } 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)); // Plugin configuration - matches directory name in dist/ const PLUGIN_NAME = 'padded-cell'; const DIST_DIR = join(__dirname, 'dist', PLUGIN_NAME); // Parse arguments const args = process.argv.slice(2); const options = { prefix: null, buildOnly: args.includes('--build-only'), skipCheck: args.includes('--skip-check'), verbose: args.includes('--verbose') || args.includes('-v'), uninstall: args.includes('--uninstall'), }; // Parse --prefix value const prefixIndex = args.indexOf('--prefix'); if (prefixIndex !== -1 && args[prefixIndex + 1]) { options.prefix = resolve(args[prefixIndex + 1]); } // Colors for output const colors = { reset: '\x1b[0m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', }; function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } function logStep(step, message) { log(`[${step}/6] ${message}`, 'cyan'); } function logSuccess(message) { log(` ✓ ${message}`, 'green'); } function logWarning(message) { log(` ⚠ ${message}`, 'yellow'); } function logError(message) { log(` ✗ ${message}`, 'red'); } function exec(command, options = {}) { const defaultOptions = { cwd: __dirname, stdio: options.silent ? 'pipe' : 'inherit', encoding: 'utf8', }; return execSync(command, { ...defaultOptions, ...options }); } // OpenClaw config helpers function getOpenclawConfig(pathKey, defaultValue = undefined) { try { const out = execSync(`openclaw config get ${pathKey} --json 2>/dev/null || echo "undefined"`, { encoding: 'utf8', cwd: __dirname }).trim(); if (out === 'undefined' || out === '') return defaultValue; return JSON.parse(out); } catch { return defaultValue; } } function setOpenclawConfig(pathKey, value) { const cmd = `openclaw config set ${pathKey} '${JSON.stringify(value)}' --json`; execSync(cmd, { cwd: __dirname, encoding: 'utf8' }); } function unsetOpenclawConfig(pathKey) { try { execSync(`openclaw config unset ${pathKey}`, { cwd: __dirname, encoding: 'utf8' }); } catch { // Ignore errors } } // Copy directory recursively function copyDir(src, dest) { mkdirSync(dest, { recursive: true }); const entries = readdirSync(src, { withFileTypes: true }); for (const entry of entries) { const srcPath = join(src, entry.name); const destPath = join(dest, entry.name); if (entry.isDirectory()) { copyDir(srcPath, destPath); } else { copyFileSync(srcPath, destPath); } } } // ============================================================================ // Step 1: Environment Detection // ============================================================================ function detectEnvironment() { logStep(1, 'Detecting environment...'); const env = { platform: platform(), nodeVersion: null, goVersion: null, openclawDir: join(homedir(), '.openclaw'), }; // Check Node.js try { env.nodeVersion = exec('node --version', { silent: true }).trim(); logSuccess(`Node.js ${env.nodeVersion}`); } catch { logError('Node.js not found'); } // Check Go try { env.goVersion = exec('go version', { silent: true }).trim(); logSuccess(`Go ${env.goVersion}`); } catch { logError('Go not found'); } // Check openclaw try { const path = exec('which openclaw', { silent: true }).trim(); logSuccess(`openclaw at ${path}`); // Try to find openclaw config dir const home = homedir(); const possibleDirs = [ join(home, '.openclaw'), join(home, '.config', 'openclaw'), ]; for (const dir of possibleDirs) { if (existsSync(dir)) { env.openclawDir = dir; logSuccess(`openclaw config dir: ${dir}`); break; } } } catch { logWarning('openclaw CLI not found in PATH'); } return env; } function checkDependencies(env) { if (options.skipCheck) { logWarning('Skipping dependency checks'); return true; } logStep(2, 'Checking dependencies...'); let hasErrors = false; if (!env.nodeVersion) { logError('Node.js is required. Please install Node.js 18+'); hasErrors = true; } else { const majorVersion = parseInt(env.nodeVersion.slice(1).split('.')[0]); if (majorVersion < 18) { logError(`Node.js 18+ required, found ${env.nodeVersion}`); hasErrors = true; } else { logSuccess(`Node.js version OK`); } } if (!env.goVersion) { logError('Go is required. Please install Go 1.22+'); hasErrors = true; } else { logSuccess(`Go version OK`); } if (hasErrors) { log('\nPlease install missing dependencies and try again.', 'red'); process.exit(1); } return true; } // ============================================================================ // Step 3: Build Components // ============================================================================ async function buildComponents(env) { logStep(3, 'Building components...'); // Build pass_mgr log(' Building pass_mgr (Go)...', 'blue'); try { const passMgrDir = join(__dirname, 'pass_mgr'); exec('go mod tidy', { cwd: passMgrDir, silent: !options.verbose }); exec('go build -o dist/pass_mgr src/main.go', { cwd: passMgrDir, silent: !options.verbose }); const binaryPath = join(passMgrDir, 'dist', 'pass_mgr'); if (!existsSync(binaryPath)) { throw new Error('pass_mgr binary not found after build'); } chmodSync(binaryPath, 0o755); logSuccess('pass_mgr built successfully'); } catch (err) { logError(`Failed to build pass_mgr: ${err.message}`); throw err; } // Build pcexec log(' Building pcexec (TypeScript)...', 'blue'); try { const pcexecDir = join(__dirname, 'pcexec'); exec('npm install', { cwd: pcexecDir, silent: !options.verbose }); exec('npm run build', { cwd: pcexecDir, silent: !options.verbose }); logSuccess('pcexec built successfully'); } catch (err) { logError(`Failed to build pcexec: ${err.message}`); throw err; } // Build safe-restart log(' Building safe-restart (TypeScript)...', 'blue'); try { const safeRestartDir = join(__dirname, 'safe-restart'); exec('npm install', { cwd: safeRestartDir, silent: !options.verbose }); exec('npm run build', { cwd: safeRestartDir, silent: !options.verbose }); logSuccess('safe-restart built successfully'); } catch (err) { logError(`Failed to build safe-restart: ${err.message}`); throw err; } } // ============================================================================ // Step 4: Install Components // ============================================================================ async function installComponents(env) { if (options.buildOnly) { logStep(4, 'Skipping installation (--build-only)'); return null; } logStep(4, 'Installing components...'); const installDir = options.prefix || env.openclawDir; const binDir = join(installDir, 'bin'); log(` Install directory: ${installDir}`, 'blue'); log(` Binary directory: ${binDir}`, 'blue'); log(` Dist directory: ${DIST_DIR}`, 'blue'); // Create dist/padded-cell directory and copy plugin files log(' Copying plugin files to dist/padded-cell...', 'blue'); mkdirSync(DIST_DIR, { recursive: true }); // Copy pcexec copyDir(join(__dirname, 'pcexec'), join(DIST_DIR, 'pcexec')); logSuccess('Copied pcexec to dist/padded-cell/'); // Copy safe-restart copyDir(join(__dirname, 'safe-restart'), join(DIST_DIR, 'safe-restart')); logSuccess('Copied safe-restart to dist/padded-cell/'); // Create root index.js entry point (copy from source) copyFileSync(join(__dirname, 'index.js'), join(DIST_DIR, 'index.js')); logSuccess('Copied index.js entry point'); // Copy openclaw.plugin.json from source copyFileSync(join(__dirname, 'openclaw.plugin.json'), join(DIST_DIR, 'openclaw.plugin.json')); logSuccess('Copied openclaw.plugin.json'); // Create bin directory and install pass_mgr binary mkdirSync(binDir, { recursive: true }); log(' Installing pass_mgr binary...', 'blue'); const passMgrSource = join(__dirname, 'pass_mgr', 'dist', 'pass_mgr'); const passMgrDest = join(binDir, 'pass_mgr'); copyFileSync(passMgrSource, passMgrDest); chmodSync(passMgrDest, 0o755); logSuccess(`pass_mgr installed to ${passMgrDest}`); return { passMgrPath: passMgrDest }; } // ============================================================================ // Step 5: Configuration // ============================================================================ async function configure(env) { if (options.buildOnly) { logStep(5, 'Skipping configuration (--build-only)'); return; } logStep(5, 'Configuration...'); const installDir = options.prefix || env.openclawDir; const passMgrPath = join(installDir, 'bin', 'pass_mgr'); // Check if already initialized const adminKeyDir = join(homedir(), '.pass_mgr'); const configPath = join(adminKeyDir, 'config.json'); if (existsSync(configPath)) { logSuccess('pass_mgr already initialized'); } else { log(' pass_mgr not initialized yet.', 'yellow'); log(` Run "${passMgrPath} admin init" manually after installation.`, 'cyan'); } // Configure OpenClaw log('\n Configuring OpenClaw plugin...', 'blue'); try { // 1. Add plugin path to plugins.load.paths FIRST (required for validation) const currentPaths = getOpenclawConfig('plugins.load.paths', []); log(` Current paths: ${JSON.stringify(currentPaths)}`, 'blue'); log(` DIST_DIR: ${DIST_DIR}`, 'blue'); if (!currentPaths.includes(DIST_DIR)) { currentPaths.push(DIST_DIR); log(` Adding plugin path...`, 'blue'); try { setOpenclawConfig('plugins.load.paths', currentPaths); logSuccess(`Added ${DIST_DIR} to plugins.load.paths`); } catch (err) { logError(`Failed to set paths: ${err.message}`); throw err; } } else { log(' Plugin path already in plugins.load.paths', 'green'); } // 2. Add to plugins.allow (after path is set) const allowList = getOpenclawConfig('plugins.allow', []); if (!allowList.includes(PLUGIN_NAME)) { allowList.push(PLUGIN_NAME); setOpenclawConfig('plugins.allow', allowList); logSuccess(`Added '${PLUGIN_NAME}' to plugins.allow`); } else { log(' Already in plugins.allow', 'green'); } // 3. Add plugin entry const plugins = getOpenclawConfig('plugins', {}); plugins.entries = plugins.entries || {}; plugins.entries[PLUGIN_NAME] = { enabled: true, config: { enabled: true, passMgrPath: passMgrPath, }, }; setOpenclawConfig('plugins', plugins); logSuccess(`Configured ${PLUGIN_NAME} plugin entry`); } catch (err) { logWarning(`Failed to configure OpenClaw: ${err.message}`); log(' Please manually configure:', 'yellow'); log(` openclaw config set plugins.allow --json '[..., "${PLUGIN_NAME}"]'`, 'cyan'); log(` openclaw config set plugins.load.paths --json '[..., "${DIST_DIR}"]'`, 'cyan'); } } // ============================================================================ // Step 6: Print Summary // ============================================================================ function printSummary(env, passMgrPath) { logStep(6, 'Installation Summary'); console.log(''); log('╔════════════════════════════════════════════════════════╗', 'cyan'); log('║ PaddedCell Installation Complete ║', 'cyan'); log('╚════════════════════════════════════════════════════════╝', 'cyan'); console.log(''); if (options.buildOnly) { log('Build-only mode - binaries built but not installed', 'yellow'); console.log(''); log('Built artifacts:', 'blue'); log(` • pass_mgr: ${join(__dirname, 'pass_mgr', 'dist', 'pass_mgr')}`, 'reset'); log(` • pcexec: ${join(__dirname, 'pcexec', 'dist')}`, 'reset'); log(` • safe-restart: ${join(__dirname, 'safe-restart', 'dist')}`, 'reset'); } else { log('Installed components:', 'blue'); log(` • pass_mgr binary: ${passMgrPath}`, 'reset'); log(` • Plugin files: ${DIST_DIR}`, 'reset'); console.log(''); log('Next steps:', 'blue'); console.log(''); log('1. Initialize pass_mgr (required before first use):', 'yellow'); log(` ${passMgrPath} admin init`, 'cyan'); console.log(''); log('2. Test pass_mgr:', 'yellow'); log(` ${passMgrPath} set test_key mypass`, 'cyan'); log(` ${passMgrPath} get test_key`, 'cyan'); console.log(''); log('3. Restart OpenClaw gateway:', 'yellow'); log(' openclaw gateway restart', 'cyan'); } console.log(''); } // ============================================================================ // Uninstall // ============================================================================ async function uninstall(env) { logStep(1, 'Uninstalling PaddedCell...'); const installDir = options.prefix || env.openclawDir || join(homedir(), '.openclaw'); const passMgrBinary = join(installDir, 'bin', 'pass_mgr'); // Remove pass_mgr binary if (existsSync(passMgrBinary)) { try { execSync(`rm -f "${passMgrBinary}"`, { silent: true }); logSuccess(`Removed ${passMgrBinary}`); } catch (err) { logError(`Failed to remove ${passMgrBinary}`); } } // Remove dist/padded-cell directory if (existsSync(DIST_DIR)) { try { execSync(`rm -rf "${DIST_DIR}"`, { silent: true }); logSuccess(`Removed ${DIST_DIR}`); } catch (err) { logError(`Failed to remove ${DIST_DIR}`); } } // Remove OpenClaw configuration log('\n Removing OpenClaw configuration...', 'blue'); try { // Remove from plugins.allow const allowList = getOpenclawConfig('plugins.allow', []); const idx = allowList.indexOf(PLUGIN_NAME); if (idx !== -1) { allowList.splice(idx, 1); setOpenclawConfig('plugins.allow', allowList); logSuccess(`Removed '${PLUGIN_NAME}' from plugins.allow`); } // Remove plugin entry unsetOpenclawConfig(`plugins.entries.${PLUGIN_NAME}`); logSuccess(`Removed ${PLUGIN_NAME} plugin entry`); // Remove from plugins.load.paths const currentPaths = getOpenclawConfig('plugins.load.paths', []); const pathIdx = currentPaths.indexOf(DIST_DIR); if (pathIdx !== -1) { currentPaths.splice(pathIdx, 1); setOpenclawConfig('plugins.load.paths', currentPaths); logSuccess(`Removed plugin path from plugins.load.paths`); } } catch (err) { logWarning(`Failed to update OpenClaw config: ${err.message}`); } // Check for admin key directory const adminKeyDir = join(homedir(), '.pass_mgr'); if (existsSync(adminKeyDir)) { log('\n⚠️ Admin key directory found:', 'yellow'); log(` ${adminKeyDir}`, 'cyan'); log(' This contains your encryption keys. Remove manually if desired.', 'yellow'); } console.log(''); log('╔════════════════════════════════════════════════════════╗', 'cyan'); log('║ PaddedCell Uninstall Complete ║', 'cyan'); log('╚════════════════════════════════════════════════════════╝', 'cyan'); console.log(''); log('Restart OpenClaw gateway:', 'yellow'); log(' openclaw gateway restart', 'cyan'); } // ============================================================================ // Main // ============================================================================ async function main() { console.log(''); log('╔════════════════════════════════════════════════════════╗', 'cyan'); log('║ PaddedCell Plugin Installer v0.1.0 ║', 'cyan'); log('╚════════════════════════════════════════════════════════╝', 'cyan'); console.log(''); try { const env = detectEnvironment(); // Handle uninstall if (options.uninstall) { await uninstall(env); process.exit(0); } checkDependencies(env); await buildComponents(env); const result = await installComponents(env); await configure(env); printSummary(env, result?.passMgrPath); process.exit(0); } catch (err) { console.log(''); log('╔════════════════════════════════════════════════════════╗', 'red'); log('║ Installation Failed ║', 'red'); log('╚════════════════════════════════════════════════════════╝', 'red'); console.log(''); log(`Error: ${err.message}`, 'red'); process.exit(1); } } main();