diff --git a/install.mjs b/install.mjs index c2dca37..9b50cae 100755 --- a/install.mjs +++ b/install.mjs @@ -4,32 +4,40 @@ * PaddedCell Plugin Installer * * Usage: - * node install.mjs --install + * node install.mjs + * node install.mjs --prefix /usr/local + * node install.mjs --build-only + * node install.mjs --skip-check * node install.mjs --uninstall */ -import { execSync, spawnSync } from 'child_process'; -import { existsSync, mkdirSync, copyFileSync, writeFileSync, chmodSync, readdirSync, statSync } from 'fs'; +import { execSync } from 'child_process'; +import { existsSync, mkdirSync, copyFileSync, chmodSync } from 'fs'; import { dirname, join, resolve } from 'path'; import { fileURLToPath } from 'url'; import { homedir, platform } from 'os'; +import readline from 'readline'; const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +const __dirname = resolve(dirname(__filename)); // Plugin configuration const PLUGIN_NAME = 'padded-cell'; -const PLUGIN_ENTRY = `plugins.entries.${PLUGIN_NAME}`; -const PLUGIN_ALLOW_PATH = 'plugins.allow'; -const PLUGINS_LOAD_PATHS = 'plugins.load.paths'; // Parse arguments const args = process.argv.slice(2); -const mode = args.includes('--uninstall') ? 'uninstall' : (args.includes('--install') ? 'install' : null); +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'), +}; -if (!mode) { - console.error('Usage: install.mjs --install | --uninstall'); - process.exit(2); +// Parse --prefix value +const prefixIndex = args.indexOf('--prefix'); +if (prefixIndex !== -1 && args[prefixIndex + 1]) { + options.prefix = resolve(args[prefixIndex + 1]); } // Colors for output @@ -46,122 +54,205 @@ 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(`[${PLUGIN_NAME}] ✓ ${message}`, 'green'); + log(` ✓ ${message}`, 'green'); } function logWarning(message) { - log(`[${PLUGIN_NAME}] ⚠ ${message}`, 'yellow'); + log(` ⚠ ${message}`, 'yellow'); } function logError(message) { - log(`[${PLUGIN_NAME}] ✗ ${message}`, 'red'); + log(` ✗ ${message}`, 'red'); } -function logInfo(message) { - log(`[${PLUGIN_NAME}] ${message}`, 'cyan'); +function exec(command, options = {}) { + const defaultOptions = { + cwd: __dirname, + stdio: options.silent ? 'pipe' : 'inherit', + encoding: 'utf8', + }; + return execSync(command, { ...defaultOptions, ...options }); } -// Execute openclaw command -function runOpenclaw(args, { allowFail = false } = {}) { - try { - return execSync('openclaw', args, { encoding: 'utf8' }).trim(); - } catch (e) { - if (allowFail) return null; - throw e; - } -} - -// Get config value as JSON -function getJson(pathKey) { - const out = runOpenclaw(['config', 'get', pathKey, '--json'], { allowFail: true }); - if (out == null || out === '' || out === 'undefined') return undefined; +async function prompt(question) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + return new Promise((resolve) => { + rl.question(question, (answer) => { + rl.close(); + resolve(answer.trim()); + }); + }); +} + +// 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 undefined; + return defaultValue; } } -// Set config value as JSON -function setJson(pathKey, value) { - runOpenclaw(['config', 'set', pathKey, JSON.stringify(value), '--json']); +function setOpenclawConfig(pathKey, value) { + execSync(`openclaw config set ${pathKey} --json '${JSON.stringify(value)}'`, { + cwd: __dirname + }); } -// Unset config path -function unsetPath(pathKey) { - runOpenclaw(['config', 'unset', pathKey], { allowFail: true }); -} - -// Deep clone -function clone(v) { - if (v === undefined) return undefined; - return JSON.parse(JSON.stringify(v)); -} - -// 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); - } +function unsetOpenclawConfig(pathKey) { + try { + execSync(`openclaw config unset ${pathKey}`, { cwd: __dirname }); + } catch { + // Ignore errors } } // ============================================================================ -// Detect Environment +// Step 1: Environment Detection // ============================================================================ function detectEnvironment() { + logStep(1, 'Detecting environment...'); + const env = { - openclawDir: join(homedir(), '.openclaw'), + platform: platform(), + isLinux: platform() === 'linux', + isMacOS: platform() === 'darwin', nodeVersion: null, goVersion: null, + openclawPath: null, + openclawDir: null, }; // Check Node.js try { - env.nodeVersion = execSync('node --version', { encoding: 'utf8' }).trim(); + const version = exec('node --version', { silent: true }).trim(); + env.nodeVersion = version; + logSuccess(`Node.js ${version}`); } catch { logError('Node.js not found'); } // Check Go try { - env.goVersion = execSync('go version', { encoding: 'utf8' }).trim(); + const version = exec('go version', { silent: true }).trim(); + env.goVersion = version; + logSuccess(`Go ${version}`); } catch { logError('Go not found'); } + // Check openclaw + try { + const path = exec('which openclaw', { silent: true }).trim(); + env.openclawPath = path; + logSuccess(`openclaw at ${path}`); + + // Try to find openclaw config dir + const home = homedir(); + const possibleDirs = [ + join(home, '.openclaw'), + join(home, '.config', 'openclaw'), + '/etc/openclaw', + ]; + + for (const dir of possibleDirs) { + if (existsSync(dir)) { + env.openclawDir = dir; + logSuccess(`openclaw config dir: ${dir}`); + break; + } + } + + if (!env.openclawDir) { + env.openclawDir = join(home, '.openclaw'); + logWarning(`openclaw config dir not found, will use: ${env.openclawDir}`); + } + } catch { + logWarning('openclaw CLI not found in PATH'); + env.openclawDir = join(homedir(), '.openclaw'); + } + 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; +} + // ============================================================================ -// Build Components +// Step 3: Build Components // ============================================================================ -function buildComponents(env) { - logInfo('Building 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'); - execSync('go mod tidy', { cwd: passMgrDir, stdio: 'inherit' }); - execSync('go build -o dist/pass_mgr src/main.go', { cwd: passMgrDir, stdio: 'inherit' }); + if (!existsSync(passMgrDir)) { + throw new Error('pass_mgr directory not found'); + } + + 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) { @@ -173,8 +264,13 @@ function buildComponents(env) { log(' Building pcexec (TypeScript)...', 'blue'); try { const pcexecDir = join(__dirname, 'pcexec'); - execSync('npm install', { cwd: pcexecDir, stdio: 'inherit' }); - execSync('npm run build', { cwd: pcexecDir, stdio: 'inherit' }); + if (!existsSync(pcexecDir)) { + throw new Error('pcexec directory not found'); + } + + 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}`); @@ -185,8 +281,13 @@ function buildComponents(env) { log(' Building safe-restart (TypeScript)...', 'blue'); try { const safeRestartDir = join(__dirname, 'safe-restart'); - execSync('npm install', { cwd: safeRestartDir, stdio: 'inherit' }); - execSync('npm run build', { cwd: safeRestartDir, stdio: 'inherit' }); + if (!existsSync(safeRestartDir)) { + throw new Error('safe-restart directory not found'); + } + + 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}`); @@ -195,253 +296,266 @@ function buildComponents(env) { } // ============================================================================ -// Install Plugin +// Step 4: Install Components // ============================================================================ -function installPlugin(env) { - logInfo('Installing plugin...'); +async function installComponents(env) { + if (options.buildOnly) { + logStep(4, 'Skipping installation (--build-only)'); + return null; + } - const pluginDir = join(env.openclawDir, 'plugins', PLUGIN_NAME); - const binDir = join(env.openclawDir, 'bin'); + logStep(4, 'Installing components...'); - // Create directories - mkdirSync(pluginDir, { recursive: true }); + const installDir = options.prefix || env.openclawDir; + const binDir = join(installDir, 'bin'); + + log(` Install directory: ${installDir}`, 'blue'); + log(` Binary directory: ${binDir}`, 'blue'); + log(` Plugin directory: ${__dirname}`, 'blue'); + + // Create bin directory mkdirSync(binDir, { recursive: true }); - // Copy plugin files - log(' Copying plugin files...', 'blue'); - - // Copy pcexec - const pcexecSource = join(__dirname, 'pcexec'); - const pcexecDest = join(pluginDir, 'pcexec'); - copyDir(pcexecSource, pcexecDest); - logSuccess(`Installed pcexec to ${pcexecDest}`); - - // Copy safe-restart - const safeRestartSource = join(__dirname, 'safe-restart'); - const safeRestartDest = join(pluginDir, 'safe-restart'); - copyDir(safeRestartSource, safeRestartDest); - logSuccess(`Installed safe-restart to ${safeRestartDest}`); - // Install pass_mgr binary + 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(`Installed pass_mgr binary to ${passMgrDest}`); + logSuccess(`pass_mgr installed to ${passMgrDest}`); - return { pluginDir, passMgrPath: passMgrDest }; + return { passMgrPath: passMgrDest }; } // ============================================================================ -// Configure OpenClaw +// Step 5: Configuration // ============================================================================ -function configureOpenClaw(env, pluginDir, delta) { - logInfo('Configuring OpenClaw...'); - - // 1. Add plugin to plugins.allow - const allowList = getJson(PLUGIN_ALLOW_PATH) || []; - const oldAllow = clone(allowList); - - if (!allowList.includes(PLUGIN_NAME)) { - allowList.push(PLUGIN_NAME); - setJson(PLUGIN_ALLOW_PATH, allowList); - delta.added[PLUGIN_ALLOW_PATH] = PLUGIN_NAME; - delta._prev = delta._prev || {}; - delta._prev[PLUGIN_ALLOW_PATH] = oldAllow; - logSuccess(`Added '${PLUGIN_NAME}' to plugins.allow`); - } else { - log(' Already in plugins.allow', 'green'); +async function configure(env) { + if (options.buildOnly) { + logStep(5, 'Skipping configuration (--build-only)'); + return; } - // 2. Add plugin entry - const oldEntry = getJson(PLUGIN_ENTRY); - const newEntry = { - enabled: true, - config: { + 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 using openclaw config commands + log('\n Configuring OpenClaw plugin...', 'blue'); + + try { + // 1. Add to plugins.allow + 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'); + } + + // 2. Add plugin entry + const existingEntry = getOpenclawConfig(`plugins.entries.${PLUGIN_NAME}`); + const pluginEntry = { enabled: true, - passMgrPath: join(env.openclawDir, 'bin', 'pass_mgr'), - pluginDir: pluginDir, - }, - }; - - if (oldEntry === undefined) { - delta.added[PLUGIN_ENTRY] = newEntry; - } else { - delta.replaced[PLUGIN_ENTRY] = oldEntry; - } - - // Use set for nested path - const plugins = getJson('plugins') || {}; - plugins.entries = plugins.entries || {}; - plugins.entries[PLUGIN_NAME] = newEntry; - setJson('plugins', plugins); - logSuccess(`Configured ${PLUGIN_ENTRY}`); - - // 3. Add to plugins.load.paths - const pluginsConfig = getJson('plugins') || {}; - const oldPaths = clone(pluginsConfig.load?.paths) || []; - const newPaths = clone(oldPaths); - - if (!newPaths.includes(pluginDir)) { - newPaths.push(pluginDir); - delta.added[PLUGINS_LOAD_PATHS] = pluginDir; - delta._prev = delta._prev || {}; - delta._prev[PLUGINS_LOAD_PATHS] = oldPaths; + config: { + enabled: true, + passMgrPath: passMgrPath, + pluginDir: __dirname, + }, + }; - pluginsConfig.load = pluginsConfig.load || {}; - pluginsConfig.load.paths = newPaths; - setJson('plugins', pluginsConfig); - logSuccess(`Added plugin path to ${PLUGINS_LOAD_PATHS}`); - } else { - log(' Plugin path already in load.paths', 'green'); + const plugins = getOpenclawConfig('plugins', {}); + plugins.entries = plugins.entries || {}; + plugins.entries[PLUGIN_NAME] = pluginEntry; + setOpenclawConfig('plugins', plugins); + + if (existingEntry) { + logSuccess(`Updated ${PLUGIN_NAME} plugin entry`); + } else { + logSuccess(`Added ${PLUGIN_NAME} plugin entry`); + } + + // 3. Add plugin path to plugins.load.paths + const currentPaths = getOpenclawConfig('plugins.load.paths', []); + if (!currentPaths.includes(__dirname)) { + currentPaths.push(__dirname); + setOpenclawConfig('plugins.load.paths', currentPaths); + logSuccess(`Added plugin path to plugins.load.paths`); + } else { + log(' Plugin path already in plugins.load.paths', 'green'); + } + } 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.entries.${PLUGIN_NAME}.enabled --json 'true'`, 'cyan'); + log(` openclaw config set plugins.load.paths --json '[..., "${__dirname}"]'`, 'cyan'); } } // ============================================================================ -// Unconfigure OpenClaw +// Step 6: Print Summary // ============================================================================ -function unconfigureOpenClaw(env, delta) { - logInfo('Removing OpenClaw configuration...'); +function printSummary(env, passMgrPath) { + logStep(6, 'Installation Summary'); - // 1. Remove from plugins.allow first (before removing entry) - if (delta.added?.[PLUGIN_ALLOW_PATH] !== undefined) { - const allowList = getJson(PLUGIN_ALLOW_PATH) || []; - const idx = allowList.indexOf(PLUGIN_NAME); - if (idx !== -1) { - allowList.splice(idx, 1); - setJson(PLUGIN_ALLOW_PATH, allowList); - logSuccess(`Removed '${PLUGIN_NAME}' from plugins.allow`); - } - } + console.log(''); + log('╔════════════════════════════════════════════════════════╗', 'cyan'); + log('║ PaddedCell Installation Complete ║', 'cyan'); + log('╚════════════════════════════════════════════════════════╝', 'cyan'); + console.log(''); - // 2. Remove plugin entry - if (delta.added?.[PLUGIN_ENTRY] !== undefined || delta.replaced?.[PLUGIN_ENTRY] !== undefined) { - unsetPath(PLUGIN_ENTRY); - logSuccess(`Removed ${PLUGIN_ENTRY}`); - } - - // 3. Remove from plugins.load.paths - if (delta.added?.[PLUGINS_LOAD_PATHS] !== undefined) { - const plugins = getJson('plugins') || {}; - const paths = plugins.load?.paths || []; - const pluginDir = join(env.openclawDir, 'plugins', PLUGIN_NAME); - const idx = paths.indexOf(pluginDir); + 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 location: ${__dirname}`, 'reset'); + console.log(''); - if (idx !== -1) { - paths.splice(idx, 1); - plugins.load.paths = paths; - setJson('plugins', plugins); - logSuccess(`Removed plugin path from ${PLUGINS_LOAD_PATHS}`); - } + 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(''); + log('For more information:', 'blue'); + log(' • Documentation: https://git.hangman-lab.top/nav/PaddedCell', 'cyan'); + console.log(''); } // ============================================================================ -// Remove Plugin Files +// Uninstall // ============================================================================ -function removePluginFiles(env) { - logInfo('Removing plugin files...'); +async function uninstall(env) { + logStep(1, 'Uninstalling PaddedCell...'); - const pluginDir = join(env.openclawDir, 'plugins', PLUGIN_NAME); - const passMgrBinary = join(env.openclawDir, 'bin', 'pass_mgr'); - - // Remove plugin directory - if (existsSync(pluginDir)) { - try { - execSync(`rm -rf "${pluginDir}"`, { encoding: 'utf8' }); - logSuccess(`Removed ${pluginDir}`); - } catch (err) { - logError(`Failed to remove ${pluginDir}: ${err.message}`); - } - } + 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}"`, { encoding: 'utf8' }); + execSync(`rm -f "${passMgrBinary}"`, { silent: true }); logSuccess(`Removed ${passMgrBinary}`); } catch (err) { - logError(`Failed to remove ${passMgrBinary}: ${err.message}`); + logError(`Failed to remove ${passMgrBinary}`); } } + + // 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(__dirname); + 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 // ============================================================================ -function main() { - const env = detectEnvironment(); +async function main() { + console.log(''); + log('╔════════════════════════════════════════════════════════╗', 'cyan'); + log('║ PaddedCell Plugin Installer v0.1.0 ║', 'cyan'); + log('╚════════════════════════════════════════════════════════╝', 'cyan'); + console.log(''); - if (mode === 'install') { - logInfo('Starting installation...'); - - try { - // Build components - buildComponents(env); - - // Install plugin files - const { pluginDir, passMgrPath } = installPlugin(env); - - // Configure OpenClaw - const delta = { added: {}, replaced: {}, removed: {} }; - configureOpenClaw(env, pluginDir, delta); - - logInfo('Installation complete!'); - console.log(''); - log('Next steps:', 'cyan'); - 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 to apply changes:', 'yellow'); - log(' openclaw gateway restart', 'cyan'); - - } catch (err) { - logError(`Installation failed: ${err.message}`); - process.exit(1); + try { + const env = detectEnvironment(); + + // Handle uninstall + if (options.uninstall) { + await uninstall(env); + process.exit(0); } - } else { - logInfo('Starting uninstallation...'); - - // For uninstall, we need the delta from install - // For simplicity, we'll track what we added and remove it - const delta = { - added: {}, - replaced: {}, - removed: {} - }; - - // Assume we added these during install - const pluginDir = join(env.openclawDir, 'plugins', PLUGIN_NAME); - delta.added[PLUGIN_ALLOW_PATH] = PLUGIN_NAME; - delta.added[PLUGIN_ENTRY] = { enabled: true }; - delta.added[PLUGINS_LOAD_PATHS] = pluginDir; - - try { - // Unconfigure OpenClaw - unconfigureOpenClaw(env, delta); - - // Remove plugin files - removePluginFiles(env); - - logInfo('Uninstallation complete!'); - console.log(''); - log('Restart OpenClaw gateway to apply changes:', 'yellow'); - log(' openclaw gateway restart', 'cyan'); - - } catch (err) { - logError(`Uninstallation failed: ${err.message}`); - process.exit(1); + + 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'); + if (options.verbose) { + console.log(err.stack); } + process.exit(1); } }