Files
PaddedCell/install.mjs
zhi 1c84b4d6a0 refactor: remove env file creation, use full path in examples
- Remove creation of paddedcell.env file
- Update post-install instructions to use full pass_mgr path
- Simplify to 6 steps (remove verification step)
- Update uninstall to not reference env file
2026-03-05 11:34:19 +00:00

624 lines
19 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 } 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}/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 });
}
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());
});
});
}
// ============================================================================
// 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
});
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...');
const installDir = options.prefix || env.openclawDir;
const binDir = join(installDir, 'bin');
const skillsDir = join(installDir, 'skills', 'paddedcell');
log(` Install directory: ${installDir}`, 'blue');
log(` Binary directory: ${binDir}`, 'blue');
log(` Skills directory: ${skillsDir}`, 'blue');
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');
}
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 {
try {
copyFileSync(srcPath, destPath);
} catch {
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 installDir = env.openclawDir || join(homedir(), '.openclaw');
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');
}
// Update OpenClaw plugins.load.paths configuration
log('\n Updating OpenClaw plugin configuration...', 'blue');
try {
const skillsPath = join(installDir, 'skills', 'paddedcell');
// Get current plugins.load.paths using openclaw config
let currentPaths = [];
try {
const result = execSync('openclaw config get plugins.load.paths --json 2>/dev/null || echo "[]"', {
encoding: 'utf8',
cwd: __dirname
}).trim();
currentPaths = JSON.parse(result);
} catch {
currentPaths = [];
}
// Add paddedcell skills path if not already present
if (!currentPaths.includes(skillsPath)) {
currentPaths.push(skillsPath);
execSync(`openclaw config set plugins.load.paths --json '${JSON.stringify(currentPaths)}'`, {
cwd: __dirname
});
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(` openclaw config set plugins.load.paths --json '["${join(installDir, 'skills', 'paddedcell')}"]'`, 'cyan');
}
}
// ============================================================================
// Step 6: Print Summary
// ============================================================================
function printSummary(env) {
logStep(6, 'Installation Summary');
const installDir = options.prefix || env.openclawDir;
const passMgrPath = join(installDir, 'bin', 'pass_mgr');
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(` • 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. 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 # Set a test password`, 'cyan');
log(` ${passMgrPath} 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 itemsToRemove = [];
// Check what exists
const passMgrBinary = join(binDir, 'pass_mgr');
if (existsSync(passMgrBinary)) {
itemsToRemove.push(passMgrBinary);
}
if (existsSync(skillsDir)) {
itemsToRemove.push(skillsDir);
}
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) {
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}`);
}
}
// Remove plugin path from OpenClaw config
log('\n Removing plugin path from OpenClaw config...', 'blue');
try {
const skillsPath = join(installDir, 'skills', 'paddedcell');
// Get current plugins.load.paths
let currentPaths = [];
try {
const result = execSync('openclaw config get plugins.load.paths --json 2>/dev/null || echo "[]"', {
encoding: 'utf8',
cwd: __dirname
}).trim();
currentPaths = JSON.parse(result);
} catch {
currentPaths = [];
}
// Remove paddedcell skills path
const index = currentPaths.indexOf(skillsPath);
if (index > -1) {
currentPaths.splice(index, 1);
execSync(`openclaw config set plugins.load.paths --json '${JSON.stringify(currentPaths)}'`, {
cwd: __dirname
});
logSuccess(`Removed plugin path from OpenClaw config`);
} else {
log(' Plugin path not found in OpenClaw config', 'yellow');
}
} 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('');
}
// ============================================================================
// 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);
await installComponents(env);
await configure(env);
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();