Files
PaddedCell/install.mjs
zhi 3a495f8d64 fix: create OpenClaw config if not exists and simplify install summary
- Create ~/.openclaw/config.json if it doesn't exist
- Automatically add plugin path to config
- Simplify post-install steps (remove redundant 'add to profile' step)
2026-03-05 11:21:22 +00:00

729 lines
22 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, 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',
].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');
try {
let config = {};
// Load existing config if present
if (existsSync(openclawConfigPath)) {
const configContent = readFileSync(openclawConfigPath, 'utf8');
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}`);
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. Reload your shell or run:', 'yellow');
log(` source ${join(installDir, 'paddedcell.env')}`, 'cyan');
console.log('');
log('2. Initialize pass_mgr (required before first use):', 'yellow');
log(' pass_mgr admin init', 'cyan');
console.log('');
log('3. 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();