#!/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, spawn } from 'child_process'; import { existsSync, mkdirSync, copyFileSync, writeFileSync, readFileSync, 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); // 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}/7] ${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 }); } 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()); }); }); } async function promptPassword(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); return new Promise((resolve) => { // Hide password input const stdin = process.stdin; stdin.on('data', (char) => { char = char.toString(); switch (char) { case '\n': case '\r': case '\u0004': stdin.pause(); break; default: process.stdout.write('*'); break; } }); rl.question(question, (answer) => { rl.close(); console.log(); // New line after password resolve(answer.trim()); }); }); } // ============================================================================ // Step 1: Environment Detection // ============================================================================ function detectEnvironment() { logStep(1, 'Detecting environment...'); const env = { platform: platform(), isLinux: platform() === 'linux', isMacOS: platform() === 'darwin', nodeVersion: null, goVersion: null, openclawPath: null, openclawDir: null, }; // Check Node.js try { const version = exec('node --version', { silent: true }).trim(); env.nodeVersion = version; logSuccess(`Node.js ${version}`); } catch { logError('Node.js not found'); } // Check Go try { 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; } // ============================================================================ // 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'); 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 }); // Verify binary exists 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'); 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}`); throw err; } // Build safe-restart log(' Building safe-restart (TypeScript)...', 'blue'); try { const safeRestartDir = join(__dirname, 'safe-restart'); 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}`); throw err; } } // ============================================================================ // Step 4: Install Components // ============================================================================ async function installComponents(env) { if (options.buildOnly) { logStep(4, 'Skipping installation (--build-only)'); return; } logStep(4, 'Installing components...'); // Determine install paths const installDir = options.prefix || env.openclawDir; const binDir = options.prefix ? join(installDir, 'bin') : join(installDir, 'bin'); const skillsDir = join(installDir, 'skills', 'paddedcell'); log(` Install directory: ${installDir}`, 'blue'); log(` Binary directory: ${binDir}`, 'blue'); log(` Skills directory: ${skillsDir}`, 'blue'); // Create directories mkdirSync(binDir, { recursive: true }); mkdirSync(skillsDir, { recursive: true }); // 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(`pass_mgr installed to ${passMgrDest}`); // Install pcexec log(' Installing pcexec module...', 'blue'); const pcexecSource = join(__dirname, 'pcexec'); const pcexecDest = join(skillsDir, 'pcexec'); copyModule(pcexecSource, pcexecDest); logSuccess(`pcexec installed to ${pcexecDest}`); // Install safe-restart log(' Installing safe-restart module...', 'blue'); const safeRestartSource = join(__dirname, 'safe-restart'); const safeRestartDest = join(skillsDir, 'safe-restart'); copyModule(safeRestartSource, safeRestartDest); logSuccess(`safe-restart installed to ${safeRestartDest}`); // Create skill manifest log(' Creating skill manifest...', 'blue'); const manifest = { name: 'paddedcell', version: '0.1.0', description: 'Secure password management, safe execution, and coordinated restart', tools: [ { name: 'pcexec', entry: './pcexec/dist/index.js', description: 'Safe exec with password sanitization', }, { name: 'safe_restart', entry: './safe-restart/dist/index.js', description: 'Safe coordinated restart', }, ], binaries: ['pass_mgr'], installedAt: new Date().toISOString(), }; writeFileSync( join(skillsDir, 'manifest.json'), JSON.stringify(manifest, null, 2) ); logSuccess('Skill manifest created'); // Add to PATH if needed if (options.prefix) { log('\n To use pass_mgr, add the following to your shell profile:', 'yellow'); log(` export PATH="${binDir}:$PATH"`, 'cyan'); } } function copyModule(source, dest) { mkdirSync(dest, { recursive: true }); const items = ['package.json', 'tsconfig.json', 'dist', 'src']; for (const item of items) { const srcPath = join(source, item); const destPath = join(dest, item); if (!existsSync(srcPath)) continue; try { const stat = execSync(`stat -c %F "${srcPath}" 2>/dev/null || echo "file"`, { encoding: 'utf8', cwd: __dirname }).trim(); if (stat === 'directory') { execSync(`cp -r "${srcPath}" "${destPath}"`, { cwd: __dirname }); } else { copyFileSync(srcPath, destPath); } } catch (err) { // Fallback: try to copy as file first, then directory try { copyFileSync(srcPath, destPath); } catch { // If file copy fails, try directory copy execSync(`cp -r "${srcPath}" "${destPath}"`, { cwd: __dirname }); } } } } // ============================================================================ // Step 5: Configuration // ============================================================================ async function configure(env) { if (options.buildOnly) { logStep(5, 'Skipping configuration (--build-only)'); return; } logStep(5, 'Configuration...'); const passMgrPath = options.prefix ? join(options.prefix, 'bin', 'pass_mgr') : join(env.openclawDir, '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 "pass_mgr admin init" manually after installation.', 'cyan'); } // Create environment config log(' Creating environment configuration...', 'blue'); const installDir = env.openclawDir || join(homedir(), '.openclaw'); const envConfig = [ '# PaddedCell Environment Configuration', `export PATH="${dirname(passMgrPath)}:$PATH"`, 'export PASS_MGR_PATH="pass_mgr"', '', '# PaddedCell skills', `export PADDEDCELL_SKILLS_DIR="${join(installDir, 'skills', 'paddedcell')}"`, ].join('\n'); const envFile = join(installDir, 'paddedcell.env'); writeFileSync(envFile, envConfig); logSuccess(`Environment config written to ${envFile}`); log('\n Add the following to your shell profile:', 'yellow'); log(` source ${envFile}`, 'cyan'); // Update OpenClaw plugins.load.paths configuration log('\n Updating OpenClaw plugin configuration...', 'blue'); const openclawConfigPath = join(installDir, 'config.json'); if (existsSync(openclawConfigPath)) { try { const configContent = readFileSync(openclawConfigPath, 'utf8'); const config = JSON.parse(configContent); // Ensure plugins.load.paths exists if (!config.plugins) { config.plugins = {}; } if (!config.plugins.load) { config.plugins.load = {}; } if (!config.plugins.load.paths) { config.plugins.load.paths = []; } // Add paddedcell skills path if not already present const skillsPath = join(installDir, 'skills', 'paddedcell'); if (!config.plugins.load.paths.includes(skillsPath)) { config.plugins.load.paths.push(skillsPath); writeFileSync(openclawConfigPath, JSON.stringify(config, null, 2)); logSuccess(`Added plugin path to OpenClaw config: ${skillsPath}`); } else { log(' Plugin path already in OpenClaw config', 'green'); } } catch (err) { logWarning(`Failed to update OpenClaw config: ${err.message}`); } } else { logWarning(`OpenClaw config not found at ${openclawConfigPath}`); log(' Please manually add the following to your OpenClaw config:', 'yellow'); log(` plugins.load.paths: ["${join(installDir, 'skills', 'paddedcell')}"]`, 'cyan'); } } // ============================================================================ // Step 6: Verify Installation // ============================================================================ async function verifyInstallation(env) { if (options.buildOnly) { logStep(6, 'Skipping verification (--build-only)'); return true; } logStep(6, 'Verifying installation...'); let allOk = true; // Check pass_mgr try { const passMgrPath = options.prefix ? join(options.prefix, 'bin', 'pass_mgr') : join(env.openclawDir, 'bin', 'pass_mgr'); if (existsSync(passMgrPath)) { execSync(`"${passMgrPath}" --help`, { silent: true }); logSuccess('pass_mgr is working'); } else { logError('pass_mgr binary not found'); allOk = false; } } catch { logError('pass_mgr test failed'); allOk = false; } // Check pcexec const pcexecPath = join( options.prefix || env.openclawDir, 'skills', 'paddedcell', 'pcexec', 'dist', 'index.js' ); if (existsSync(pcexecPath)) { logSuccess('pcexec module installed'); } else { logError('pcexec module not found'); allOk = false; } // Check safe-restart const safeRestartPath = join( options.prefix || env.openclawDir, 'skills', 'paddedcell', 'safe-restart', 'dist', 'index.js' ); if (existsSync(safeRestartPath)) { logSuccess('safe-restart module installed'); } else { logError('safe-restart module not found'); allOk = false; } return allOk; } // ============================================================================ // Step 7: Print Summary // ============================================================================ function printSummary(env) { logStep(7, 'Installation Summary'); const installDir = options.prefix || env.openclawDir; 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: ${join(installDir, 'bin', 'pass_mgr')}`, 'reset'); log(` • pcexec module: ${join(installDir, 'skills', 'paddedcell', 'pcexec')}`, 'reset'); log(` • safe-restart module: ${join(installDir, 'skills', 'paddedcell', 'safe-restart')}`, 'reset'); console.log(''); log('Next steps:', 'blue'); console.log(''); log('1. Add to your shell profile (~/.bashrc, ~/.zshrc):', 'yellow'); log(` source ${join(installDir, 'paddedcell.env')}`, 'cyan'); console.log(''); log('2. Reload your shell or run:', 'yellow'); log(` source ${join(installDir, 'paddedcell.env')}`, 'cyan'); console.log(''); log('3. Initialize pass_mgr (required before first use):', 'yellow'); log(' pass_mgr admin init', 'cyan'); console.log(''); log('4. Test pass_mgr:', 'yellow'); log(' pass_mgr set test_key mypass # Set a test password', 'cyan'); log(' pass_mgr get test_key # Retrieve password', 'cyan'); } console.log(''); log('For more information:', 'blue'); log(' • Documentation: https://git.hangman-lab.top/nav/PaddedCell', 'cyan'); log(' • Issues: https://git.hangman-lab.top/nav/PaddedCell/issues', 'cyan'); console.log(''); } // ============================================================================ // Uninstall Function // ============================================================================ async function uninstall(env) { logStep(1, 'Uninstalling PaddedCell...'); const installDir = options.prefix || env.openclawDir || join(homedir(), '.openclaw'); const binDir = join(installDir, 'bin'); const skillsDir = join(installDir, 'skills', 'paddedcell'); const envFile = join(installDir, 'paddedcell.env'); const itemsToRemove = []; // Check what exists const passMgrBinary = join(binDir, 'pass_mgr'); if (existsSync(passMgrBinary)) { itemsToRemove.push(passMgrBinary); } if (existsSync(skillsDir)) { itemsToRemove.push(skillsDir); } if (existsSync(envFile)) { itemsToRemove.push(envFile); } if (itemsToRemove.length === 0) { log('No installed components found.', 'yellow'); return; } log('The following items will be removed:', 'yellow'); for (const item of itemsToRemove) { log(` • ${item}`, 'reset'); } // Ask for confirmation const confirm = await prompt('\nAre you sure you want to uninstall? (y/N): '); if (confirm.toLowerCase() !== 'y') { log('Uninstall cancelled.', 'yellow'); return; } // Perform uninstall for (const item of itemsToRemove) { try { if (item === passMgrBinary || item === envFile) { execSync(`rm -f "${item}"`, { silent: true }); logSuccess(`Removed: ${item}`); } else { execSync(`rm -rf "${item}"`, { silent: true }); logSuccess(`Removed: ${item}`); } } catch (err) { logError(`Failed to remove: ${item}`); } } // 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('Remember to remove the following from your shell profile:', 'yellow'); log(` source ${envFile}`, 'cyan'); console.log(''); } 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); await installComponents(env); await configure(env); const verified = await verifyInstallation(env); if (!verified && !options.buildOnly) { log('\nInstallation completed with warnings.', 'yellow'); } printSummary(env); 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); } } main();