diff --git a/AGENT_TASKS.md b/AGENT_TASKS.md index 1891b5e..c58bb9a 100644 --- a/AGENT_TASKS.md +++ b/AGENT_TASKS.md @@ -1,4 +1,4 @@ -# PaddedCell — Agent Tasks +# PaddedCell - Agent Tasks > 任务拆分 + 依赖关系(基于 PROJECT_PLAN.md) @@ -9,98 +9,113 @@ --- ## 0. 项目准备 -- **T-0001**:确认配置项清单(路径模板、rotate 语义、admin 泄露检测范围) - - Deps: — -- **T-0002**:确定加密库与存储格式(选型与接口约束) +- **T-0001**:确认配置项清单(路径模板、rotate 语义、admin 泄露检测范围) + - Deps: - +- **T-0002**:确定加密库与存储格式(选型与接口约束) - Deps: T-0001 --- ## 1. pass_mgr 二进制 -- **T-1001**:CLI 命令规范与参数校验(get/generate/unset/rotate/admin init/set) +- **T-1001**:CLI 命令规范与参数校验(get/generate/unset/rotate/admin init/set) - Deps: T-0001 -- **T-1002**:初始化与 admin 密码存储/校验机制 +- **T-1002**:初始化与 admin 密码存储/校验机制 - Deps: T-1001, T-0002 -- **T-1003**:加/解密与存取实现(内置加密库) +- **T-1003**:加/解密与存取实现(内置加密库) - Deps: T-1001, T-0002 -- **T-1004**:generate / rotate 实现与语义落地 +- **T-1004**:generate / rotate 实现与语义落地 - Deps: T-1003 -- **T-1005**:admin-only set 实现(环境变量检测 + 禁止 agent 执行) +- **T-1005**:admin-only set 实现(环境变量检测 + 禁止 agent 执行) - Deps: T-1001 -- **T-1006**:未初始化状态阻断逻辑(所有 get/generate/set 报错) +- **T-1006**:未初始化状态阻断逻辑(所有 get/generate/set 报错) - Deps: T-1002 --- ## 2. pcexec 工具(TS) -- **T-2001**:exec 参数/行为兼容设计(与原生 exec 对齐) - - Deps: — -- **T-2002**:pass_mgr get 检测与预执行(不限 `$(...)`) +- **T-2001**:exec 参数/行为兼容设计(与原生 exec 对齐) + - Deps: - +- **T-2002**:pass_mgr get 检测与预执行(不限 `$(...)`) - Deps: T-2001, T-1001 -- **T-2003**:多密码脱敏替换(stdout/stderr) +- **T-2003**:多密码脱敏替换(stdout/stderr) - Deps: T-2002 -- **T-2004**:错误处理/退出码一致性 +- **T-2004**:错误处理/退出码一致性 - Deps: T-2001 --- ## 3. 安全重启(CalmGate 功能并入) -- **T-3001**:状态机与 session tracker(idle/busy/focus/freeze…) - - Deps: — -- **T-3002**:消息生命周期 hooks(start/end)与状态迁移 +- **T-3001**:状态机与 session tracker(idle/busy/focus/freeze…) + - Deps: - +- **T-3002**:消息生命周期 hooks(start/end)与状态迁移 - Deps: T-3001 -- **T-3003**:workflow/focus 机制与“忙碌回复” +- **T-3003**:workflow/focus 机制与"忙碌回复" - Deps: T-3001 -- **T-3004**:query-restart API(OK/NOT_READY/ALREADY_SCHEDULED) +- **T-3004**:query-restart API(OK/NOT_READY/ALREADY_SCHEDULED) - Deps: T-3001, T-3002 -- **T-3005**:safe-restart 工具(轮询/重启/rollback/log) +- **T-3005**:safe-restart 工具(轮询/重启/rollback/log) - Deps: T-3004 -- **T-3006**:重启后恢复与通知(冻结/解冻与回到工作 session) +- **T-3006**:重启后恢复与通知(冻结/解冻与回到工作 session) - Deps: T-3005 -- **T-3007**:持久化(mem+file)与恢复策略 +- **T-3007**:持久化(mem+file)与恢复策略 - Deps: T-3001 --- ## 4. 安全监控与泄露防护 -- **T-4001**:admin 密码泄露检测(message/tool calling) +- **T-4001**:admin 密码泄露检测(message/tool calling) - Deps: T-1002 -- **T-4002**:泄露触发处理(重置未初始化 + 严重漏洞日志) +- **T-4002**:泄露触发处理(重置未初始化 + 严重漏洞日志) - Deps: T-4001 --- ## 4.1 功能开关(Slash Commands) -- **T-4101**:实现 `/padded-cell-ctrl` 命令(status/enable/disable) - - Deps: — -- **T-4102**:开关状态持久化、权限限制(授权用户)与 10 秒冷却 +- **T-4101**:实现 `/padded-cell-ctrl` 命令(status/enable/disable) + - Deps: - +- **T-4102**:开关状态持久化、权限限制(授权用户)与 10 秒冷却 - Deps: T-4101 --- ## 5. 文档 & Skill 指南 -- **T-5001**:Agent 使用指南(如何用 pass_mgr/pcexec) +- **T-5001**:Agent 使用指南(如何用 pass_mgr/pcexec) - Deps: T-1001, T-2001 -- **T-5002**:Skill 文档与示例(正确使用密码相关工具) +- **T-5002**:Skill 文档与示例(正确使用密码相关工具) - Deps: T-5001 --- ## 6. 测试与验收 -- **T-6001**:pass_mgr 单测(get/generate/unset/rotate/admin init) +- **T-6001**:pass_mgr 单测(get/generate/unset/rotate/admin init) - Deps: T-1002, T-1003, T-1004, T-1005, T-1006 -- **T-6002**:pcexec 兼容性测试(参数/管道/多密码替换) +- **T-6002**:pcexec 兼容性测试(参数/管道/多密码替换) - Deps: T-2002, T-2003, T-2004 -- **T-6003**:安全重启回归(并发/冻结/rollback) +- **T-6003**:安全重启回归(并发/冻结/rollback) - Deps: T-3004, T-3005, T-3006, T-3007 -- **T-6004**:admin 泄露触发测试 +- **T-6004**:admin 泄露触发测试 - Deps: T-4002 --- +## 7. 安装脚本 +- **T-7001**:依赖检测(Node.js, Go, openclaw CLI, 平台检测) + - Deps: — +- **T-7002**:自动构建逻辑(Go + TypeScript) + - Deps: T-1001, T-2001, T-3001 +- **T-7003**:安装逻辑(二进制/模块复制、PATH 配置) + - Deps: T-7002 +- **T-7004**:初始化向导(admin 密码设置、配置生成) + - Deps: T-1002, T-7003 +- **T-7005**:安装验证与摘要输出 + - Deps: T-7004 + +--- + ## 依赖关系示意(简化) - 0.* → 1.* / 2.* / 3.* - 1.* → 4.* → 6.* - 2.* → 6.* - 3.* → 6.* - 5.* 可与 1.* / 2.* 并行,但需接口稳定 +- 7.* 依赖 1.* / 2.* / 3.* 完成(可在构建后执行) diff --git a/PROJECT_PLAN.md b/PROJECT_PLAN.md index 2284c72..0136023 100644 --- a/PROJECT_PLAN.md +++ b/PROJECT_PLAN.md @@ -244,6 +244,50 @@ Request: - 任务分配清单 - 测试用例列表 +## 6. 安装脚本 + +### 6.1 需求 +提供统一的插件安装脚本 `install.mjs`,实现以下功能: + +**检测与依赖** +- 检测系统平台 (Linux/macOS) +- 检测必要依赖 (Node.js, Go, openclaw CLI) +- 检测 openclaw 配置目录 + +**构建** +- 自动构建 pass_mgr Go 二进制 +- 自动构建 pcexec TypeScript 模块 +- 自动构建 safe-restart TypeScript 模块 + +**安装** +- 安装 pass_mgr 到系统 PATH (或 openclaw bin 目录) +- 安装 TypeScript 模块到 openclaw skills 目录 +- 创建必要的配置文件和目录 + +**配置** +- 初始化 pass_mgr admin (引导用户设置 admin 密码) +- 配置环境变量 +- 注册 safe-restart API 服务 + +**验证** +- 验证所有组件安装成功 +- 输出安装摘要和下一步操作提示 + +### 6.2 使用方式 +```bash +# 默认安装 +node install.mjs + +# 指定安装路径 +node install.mjs --prefix /usr/local + +# 仅构建不安装 +node install.mjs --build-only + +# 跳过依赖检测 +node install.mjs --skip-check +``` + --- > 更新日志:v0.1(基于当前需求整理) diff --git a/install.mjs b/install.mjs new file mode 100755 index 0000000..9365897 --- /dev/null +++ b/install.mjs @@ -0,0 +1,630 @@ +#!/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 + */ + +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'), +}; + +// 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; + + const stat = execSync(`stat -c %F "${srcPath}" 2>/dev/null || echo "file"`, { + silent: true, + cwd: __dirname + }).trim(); + + if (stat === 'directory') { + exec(`cp -r "${srcPath}" "${destPath}"`, { silent: true }); + } else { + copyFileSync(srcPath, destPath); + } + } +} + +// ============================================================================ +// 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)) { + logWarning('pass_mgr already initialized'); + const reinit = await prompt(' Reinitialize? (y/N): '); + if (reinit.toLowerCase() !== 'y') { + log(' Skipping initialization'); + return; + } + } + + // Initialize pass_mgr + log(' Initializing pass_mgr...', 'blue'); + log(' Please set your admin password for pass_mgr.', 'yellow'); + + const adminPassword = await promptPassword(' Admin password: '); + if (!adminPassword) { + logWarning('Empty password, skipping initialization'); + return; + } + + const confirmPassword = await promptPassword(' Confirm password: '); + if (adminPassword !== confirmPassword) { + logError('Passwords do not match'); + return; + } + + try { + // Write password to temp file and init + const tempKeyFile = join(adminKeyDir, '.tmp_key'); + mkdirSync(adminKeyDir, { recursive: true, mode: 0o700 }); + writeFileSync(tempKeyFile, adminPassword, { mode: 0o600 }); + + exec(`${passMgrPath} admin init --key-path "${tempKeyFile}"`, { silent: !options.verbose }); + + // Remove temp file + execSync(`rm -f "${tempKeyFile}"`); + + logSuccess('pass_mgr initialized successfully'); + } catch (err) { + logError(`Failed to initialize pass_mgr: ${err.message}`); + throw err; + } + + // Create environment config + log(' Creating environment configuration...', 'blue'); + const envConfig = [ + '# PaddedCell Environment Configuration', + `export PATH="${dirname(passMgrPath)}:$PATH"`, + 'export PASS_MGR_PATH="pass_mgr"', + '', + '# PaddedCell skills', + `export PADDEDCELL_SKILLS_DIR="${join(env.openclawDir || homedir(), '.openclaw', 'skills', 'paddedcell')}"`, + ].join('\n'); + + const envFile = join(env.openclawDir || homedir(), '.openclaw', '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'); +} + +// ============================================================================ +// 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. Test pass_mgr:', 'yellow'); + log(' pass_mgr admin init # Initialize (if not done)', 'cyan'); + 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(''); +} + +// ============================================================================ +// Main +// ============================================================================ + +async function main() { + console.log(''); + log('╔════════════════════════════════════════════════════════╗', 'cyan'); + log('║ PaddedCell Plugin Installer v0.1.0 ║', 'cyan'); + log('╚════════════════════════════════════════════════════════╝', 'cyan'); + console.log(''); + + try { + const env = detectEnvironment(); + 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();