Files
PaddedCell/install.mjs
zhi bb194ef978 refactor: install to dist/padded-cell/ like Dirigent
- Plugin name: padded-cell (matches dist subdirectory)
- Install copies files to dist/padded-cell/
- Plugin path set to dist/padded-cell/ directory
- Follows Dirigent pattern: dist/<plugin-name>/
2026-03-05 12:26:52 +00:00

565 lines
18 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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) {
execSync(`openclaw config set ${pathKey} '${JSON.stringify(value)}' --json`, {
cwd: __dirname
});
}
function unsetOpenclawConfig(pathKey) {
try {
execSync(`openclaw config unset ${pathKey}`, { cwd: __dirname });
} 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 openclaw.plugin.json
const manifest = {
id: PLUGIN_NAME,
name: 'PaddedCell',
version: '0.1.0',
description: 'Secure password management, safe execution, and coordinated restart',
entry: './safe-restart/dist/index.js',
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'
}
]
};
writeFileSync(join(DIST_DIR, 'openclaw.plugin.json'), JSON.stringify(manifest, null, 2));
logSuccess('Created openclaw.plugin.json in dist/padded-cell/');
// 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 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 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`);
// 3. Add plugin path to plugins.load.paths
const currentPaths = getOpenclawConfig('plugins.load.paths', []);
if (!currentPaths.includes(DIST_DIR)) {
currentPaths.push(DIST_DIR);
setOpenclawConfig('plugins.load.paths', currentPaths);
logSuccess(`Added ${DIST_DIR} 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.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();