UNPKG

caravan-x

Version:

A terminal-based utility for managing Caravan multisig wallets in regtest mode. This tool simplifies development and testing with Caravan by providing an easy-to-use interface

367 lines (366 loc) β€’ 15.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DockerCommands = void 0; /** * Docker Commands for Caravan-X */ const prompts_1 = require("@inquirer/prompts"); const chalk_1 = __importDefault(require("chalk")); // Color utilities const colors = { primary: chalk_1.default.hex("#F7931A"), success: chalk_1.default.hex("#28a745"), warning: chalk_1.default.hex("#ffc107"), error: chalk_1.default.hex("#dc3545"), info: chalk_1.default.hex("#17a2b8"), muted: chalk_1.default.hex("#6c757d"), }; function displayCommandTitle(title) { console.log("\n" + colors.primary(`${"=".repeat(50)}`)); console.log(colors.primary(` ${title}`)); console.log(colors.primary(`${"=".repeat(50)}`) + "\n"); } class DockerCommands { constructor(dockerService) { this.dockerService = dockerService; } async showDockerMenu() { displayCommandTitle("🐳 Docker Management"); const action = await (0, prompts_1.select)({ message: "What would you like to do?", choices: [ { name: "πŸ“Š Container Status", value: "status" }, { name: "πŸš€ Complete Setup (Container + Nginx + Wallet)", value: "complete_setup", }, { name: "πŸ§ͺ Test Connection", value: "test_connection" }, { name: "πŸ“‘ Show Connection Info", value: "connection_info" }, { name: "πŸ“‘ Show Connection Info", value: "connection_info" }, { name: "▢️ Start Container", value: "start" }, { name: "⏸️ Stop Container", value: "stop" }, { name: "πŸ”„ Restart Container", value: "restart" }, { name: "🌐 Setup Nginx Proxy", value: "nginx" }, { name: "πŸ’Ό Create Watch-Only Wallet", value: "wallet" }, { name: "πŸ—‘οΈ Remove Container", value: "remove" }, { name: "πŸ“œ View Logs", value: "logs" }, { name: "🐚 Open Shell", value: "shell" }, { name: "πŸ”§ Troubleshoot Port Issues", value: "troubleshoot" }, { name: "πŸ”™ Back", value: "back" }, ], }); try { switch (action) { case "complete_setup": await this.dockerService.completeSetup(); break; case "test_connection": await this.dockerService.testConnection(); break; case "connection_info": await this.dockerService.displayConnectionInfo(); break; case "nginx": await this.dockerService.setupNginxProxy(); break; case "wallet": const walletName = await (0, prompts_1.input)({ message: "Enter wallet name:", default: "caravan_watcher", }); await this.dockerService.createWatchOnlyWallet(walletName); break; case "status": await this.showStatus(); break; case "start": await this.startContainerWithErrorHandling(); break; case "stop": await this.dockerService.stopContainer(); break; case "restart": await this.dockerService.restartContainer(); break; case "remove": const confirmRemove = await (0, prompts_1.confirm)({ message: "Are you sure you want to remove the container?", default: false, }); if (confirmRemove) { await this.dockerService.removeContainer(); } break; case "logs": await this.showLogs(); break; case "shell": await this.dockerService.openShell(); break; case "troubleshoot": await this.troubleshootPorts(); break; case "back": return; } } catch (error) { await this.displayDockerError(error); } if (action !== "back") { await (0, prompts_1.input)({ message: "\nPress Enter to continue..." }); await this.showDockerMenu(); } } async showStatus() { const status = await this.dockerService.getContainerStatus(); console.log(chalk_1.default.bold("\nπŸ“Š Container Status\n")); if (status.running) { console.log(colors.success("βœ… Status: Running")); console.log(colors.info(`πŸ“¦ Container ID: ${status.containerId}`)); console.log(colors.info(`πŸ”Œ RPC Port: ${status.ports?.rpc}`)); console.log(colors.info(`🌐 P2P Port: ${status.ports?.p2p}`)); console.log(colors.info(`πŸ”— Network: ${status.network}`)); } else { console.log(colors.warning("⏸️ Status: Not Running")); if (status.containerId) { console.log(colors.info(`πŸ“¦ Container ID: ${status.containerId} (stopped)`)); } else { console.log(colors.muted("No container found")); } } } async showLogs() { console.log(chalk_1.default.dim("\n=== Container Logs (Last 50 lines) ===\n")); const logs = await this.dockerService.getLogs(50); console.log(logs); } async startContainerWithErrorHandling() { try { await this.dockerService.startContainer(); } catch (error) { if (error.message.includes("port") && error.message.includes("already in use")) { await this.handlePortConflict(error); } else if (error.message.includes("platform")) { await this.handlePlatformMismatch(error); } else { throw error; } } } async handlePortConflict(error) { console.log("\n" + colors.error("⚠️ Port Conflict Detected")); console.log(colors.warning("\nOne or more ports required by the container are already in use.")); console.log(colors.muted("\nThis usually means:")); console.log(colors.muted(" β€’ Another Bitcoin Core instance is running on the same port")); console.log(colors.muted(" β€’ A previous container wasn't properly cleaned up")); console.log(colors.muted(" β€’ Another application is using ports 18443 or 18444")); const action = await (0, prompts_1.select)({ message: "\nHow would you like to proceed?", choices: [ { name: "πŸ” Check what's using the ports", value: "check", }, { name: "πŸ—‘οΈ Remove any existing Caravan-X containers", value: "cleanup", }, { name: "❌ Cancel", value: "cancel", }, ], }); if (action === "check") { await this.troubleshootPorts(); } else if (action === "cleanup") { await this.cleanupContainers(); } } async handlePlatformMismatch(error) { console.log("\n" + colors.warning("⚠️ Platform Mismatch Detected")); console.log(colors.info("\nYour system is using ARM64 architecture (Apple Silicon/M1/M2/M3),")); console.log(colors.info("but the Docker image is built for AMD64 (Intel/x86).")); console.log(colors.muted("\nThis may cause performance issues or compatibility problems.")); const proceed = await (0, prompts_1.confirm)({ message: "Would you like to continue anyway? (Docker will use emulation)", default: true, }); if (proceed) { console.log(colors.info("\nπŸ’‘ Tip: Consider using a native ARM64 Bitcoin Core image for better performance.")); } } async troubleshootPorts() { displayCommandTitle("πŸ”§ Port Troubleshooting"); console.log(colors.info("Checking ports 18443 and 18444...\n")); try { const { exec } = require("child_process"); const { promisify } = require("util"); const execAsync = promisify(exec); // Check port 18443 (RPC) console.log(colors.primary("Checking port 18443 (Bitcoin RPC):")); try { const { stdout: rpcCheck } = await execAsync("lsof -i :18443 || netstat -an | grep 18443 || echo 'Port appears free'"); if (rpcCheck.includes("Port appears free")) { console.log(colors.success(" βœ… Port 18443 is available\n")); } else { console.log(colors.error(" ❌ Port 18443 is in use:")); console.log(colors.muted(` ${rpcCheck.trim()}\n`)); } } catch (e) { console.log(colors.muted(" Unable to check port status\n")); } // Check port 18444 (P2P) console.log(colors.primary("Checking port 18444 (Bitcoin P2P):")); try { const { stdout: p2pCheck } = await execAsync("lsof -i :18444 || netstat -an | grep 18444 || echo 'Port appears free'"); if (p2pCheck.includes("Port appears free")) { console.log(colors.success(" βœ… Port 18444 is available\n")); } else { console.log(colors.error(" ❌ Port 18444 is in use:")); console.log(colors.muted(` ${p2pCheck.trim()}\n`)); } } catch (e) { console.log(colors.muted(" Unable to check port status\n")); } // Check for Docker containers console.log(colors.primary("Checking for Docker containers:")); try { const { stdout: dockerCheck } = await execAsync("docker ps -a --filter name=caravan"); if (dockerCheck.includes("caravan")) { console.log(colors.warning(" Found Caravan-related containers:")); console.log(colors.muted(` ${dockerCheck}\n`)); } else { console.log(colors.success(" βœ… No Caravan containers found\n")); } } catch (e) { console.log(colors.muted(" Unable to check Docker containers\n")); } } catch (error) { console.log(colors.error("Error during troubleshooting:"), error); } const action = await (0, prompts_1.select)({ message: "What would you like to do?", choices: [ { name: "πŸ—‘οΈ Clean up all Caravan-X containers", value: "cleanup", }, { name: "πŸ”™ Back to Docker menu", value: "back", }, ], }); if (action === "cleanup") { await this.cleanupContainers(); } } async cleanupContainers() { console.log("\n" + colors.info("πŸ—‘οΈ Cleaning up Caravan-X containers...")); try { const { exec } = require("child_process"); const { promisify } = require("util"); const execAsync = promisify(exec); // Stop all caravan containers console.log(colors.muted(" Stopping containers...")); try { await execAsync("docker stop $(docker ps -aq --filter name=caravan) 2>/dev/null"); } catch (e) { // Ignore errors if no containers found } // Remove all caravan containers console.log(colors.muted(" Removing containers...")); try { await execAsync("docker rm $(docker ps -aq --filter name=caravan) 2>/dev/null"); } catch (e) { // Ignore errors if no containers found } console.log(colors.success("\nβœ… Cleanup complete!\n")); const tryStart = await (0, prompts_1.confirm)({ message: "Would you like to try starting the container now?", default: true, }); if (tryStart) { await this.dockerService.startContainer(); } } catch (error) { console.log(colors.error("Error during cleanup:"), error.message); } } async displayDockerError(error) { const errorMessage = error.message || String(error); // Create a nice error box const boxWidth = 80; const lines = [ "⚠️ Docker Error", "", ...this.wrapText(errorMessage, boxWidth - 4), "", ]; // Determine error type and add helpful tips if (errorMessage.includes("port") && errorMessage.includes("in use")) { lines.push("πŸ’‘ Tip: Use 'Troubleshoot Port Issues' to identify what's using the ports"); } else if (errorMessage.includes("platform")) { lines.push("πŸ’‘ Tip: Your system architecture may not match the Docker image"); } else if (errorMessage.includes("docker: command not found")) { lines.push("πŸ’‘ Tip: Docker doesn't appear to be installed on your system"); lines.push(" Visit: https://docs.docker.com/get-docker/"); } else if (errorMessage.includes("Cannot connect to the Docker daemon")) { lines.push("πŸ’‘ Tip: Docker daemon is not running. Start Docker Desktop."); } // Draw the box console.log("\n" + colors.error("β•”" + "═".repeat(boxWidth) + "β•—")); lines.forEach((line) => { const padding = " ".repeat(Math.max(0, boxWidth - this.stripAnsi(line).length)); console.log(colors.error("β•‘") + " " + line + padding + colors.error("β•‘")); }); console.log(colors.error("β•š" + "═".repeat(boxWidth) + "╝\n")); } wrapText(text, maxWidth) { const words = text.split(" "); const lines = []; let currentLine = ""; for (const word of words) { if ((currentLine + word).length <= maxWidth) { currentLine += (currentLine ? " " : "") + word; } else { if (currentLine) lines.push(currentLine); currentLine = word; } } if (currentLine) lines.push(currentLine); return lines; } stripAnsi(text) { return text.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, ""); } } exports.DockerCommands = DockerCommands;