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
JavaScript
;
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;