assh
Version:
Advanced SSH Manager - A modern CLI tool for managing SSH servers and tunnels
1,105 lines (981 loc) • 30.6 kB
JavaScript
import { Command } from "commander";
import chalk from "chalk";
import Table from "cli-table3";
import inquirer from "inquirer";
import fs from "fs-extra";
import { spawn, exec } from "child_process";
import { promisify } from "util";
import path from "path";
import os from "os";
const execAsync = promisify(exec);
// Configuration directory and file
const CONFIG_DIR = path.join(os.homedir(), ".server-manager");
const CONFIG_FILE = path.join(CONFIG_DIR, "servers.json");
// Track temp files for cleanup
const tempFiles = new Set();
// Cleanup function
function cleanupTempFiles() {
tempFiles.forEach((file) => {
try {
if (fs.existsSync(file)) {
fs.removeSync(file);
}
} catch (error) {
// Ignore cleanup errors
}
});
tempFiles.clear();
}
// Add signal handlers for cleanup
process.on("SIGINT", () => {
console.log(chalk.yellow("\n\nReceived SIGINT, cleaning up..."));
cleanupTempFiles();
process.exit(0);
});
process.on("SIGTERM", () => {
console.log(chalk.yellow("\n\nReceived SIGTERM, cleaning up..."));
cleanupTempFiles();
process.exit(0);
});
process.on("exit", () => {
cleanupTempFiles();
});
// Default configuration
const DEFAULT_CONFIG = {
servers: {},
settings: {
defaultLocalPort: 8090,
},
};
// Ensure configuration directory exists
await fs.ensureDir(CONFIG_DIR);
// Load or create configuration
let config = DEFAULT_CONFIG;
if (await fs.pathExists(CONFIG_FILE)) {
try {
config = await fs.readJson(CONFIG_FILE);
} catch (error) {
console.error(
chalk.red("Error reading configuration file:"),
error.message
);
process.exit(1);
}
}
// Save configuration
async function saveConfig() {
try {
await fs.writeJson(CONFIG_FILE, config, { spaces: 2 });
} catch (error) {
console.error(chalk.red("Error saving configuration:"), error.message);
process.exit(1);
}
}
// Get server by name
function getServer(name) {
if (!config.servers[name]) {
console.error(
chalk.red(
`Server '${name}' not found. Use 'bun sm.js ls' to see available servers.`
)
);
process.exit(1);
}
return config.servers[name];
}
// Generate SSH key pair
async function generateSSHKey(serverName) {
const keyPath = path.join(CONFIG_DIR, `${serverName}_key`);
const pubKeyPath = `${keyPath}.pub`;
try {
await execAsync(`ssh-keygen -t rsa -b 2048 -f "${keyPath}" -N "" -q`);
const publicKey = await fs.readFile(pubKeyPath, "utf8");
return {
privateKeyPath: keyPath,
publicKeyPath: pubKeyPath,
publicKey: publicKey.trim(),
};
} catch (error) {
console.error(chalk.red("Error generating SSH key:"), error.message);
throw error;
}
}
// Install SSH key on server
async function installSSHKey(server, keyInfo) {
const { ip, username, password, port } = server;
const { publicKey } = keyInfo;
try {
console.log(chalk.blue("Installing SSH key on server..."));
// Use sshpass if available, otherwise prompt user
const sshCommand = password
? `sshpass -p '${password}' ssh -o StrictHostKeyChecking=no -p ${port} ${username}@${ip}`
: `ssh -o StrictHostKeyChecking=no -p ${port} ${username}@${ip}`;
const installCommand = `echo '${publicKey}' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && chmod 700 ~/.ssh`;
await execAsync(`${sshCommand} "${installCommand}"`);
console.log(chalk.green("SSH key installed successfully!"));
} catch (error) {
console.warn(
chalk.yellow("Warning: Could not automatically install SSH key.")
);
console.log(
chalk.blue("Please manually add this public key to your server:")
);
console.log(chalk.cyan(keyInfo.publicKey));
}
}
// Create SSH connection
function createSSHConnection(server, options = {}) {
const { ip, username, port, keyPath, password } = server;
const { localPort, remotePort, dynamicPort } = options;
let args = ["-o", "StrictHostKeyChecking=no"];
if (keyPath && fs.existsSync(keyPath)) {
args.push("-i", keyPath);
}
args.push("-p", port.toString());
if (dynamicPort) {
args.push("-C", "-N", "-D", `0.0.0.0:${dynamicPort}`);
}
if (localPort && remotePort) {
args.push("-L", `${localPort}:localhost:${remotePort}`);
}
args.push(`${username}@${ip}`);
return spawn("ssh", args, { stdio: "inherit" });
}
// Program setup
const program = new Command();
program
.name("sm")
.description("SSH Server Manager - Manage SSH servers and tunnels")
.version("1.0.0");
// Add server command
program
.command("add")
.description("Add a new SSH server")
.action(async () => {
try {
const answers = await inquirer.prompt([
{
type: "input",
name: "serverName",
message: "Server name:",
validate: (input) => {
if (!input.trim()) return "Server name is required";
if (config.servers[input.trim()])
return "Server name already exists";
return true;
},
},
{
type: "input",
name: "ip",
message: "Server IP:",
validate: (input) => (input.trim() ? true : "Server IP is required"),
},
{
type: "input",
name: "username",
message: "Username:",
default: "root",
},
{
type: "input",
name: "port",
message: "SSH Port:",
default: "22",
validate: (input) => {
const port = parseInt(input);
return port > 0 && port <= 65535
? true
: "Port must be between 1 and 65535";
},
},
{
type: "list",
name: "authMethod",
message: "Authentication method:",
choices: [
{ name: "Generate SSH Key (recommended)", value: "key" },
{ name: "Use existing SSH Key", value: "existing_key" },
{ name: "Password", value: "password" },
],
},
{
type: "input",
name: "keyPath",
message: "SSH key path:",
default: "~/.ssh/id_rsa",
when: (answers) => answers.authMethod === "existing_key",
validate: (input) => {
const resolvedPath = input.startsWith("~")
? path.join(os.homedir(), input.slice(1))
: path.resolve(input);
return fs.existsSync(resolvedPath)
? true
: "SSH key file does not exist";
},
},
{
type: "password",
name: "password",
message: "Password:",
when: (answers) => answers.authMethod === "password",
},
{
type: "password",
name: "singPassword",
message: "Sing-box password (optional, for shadowsocks):",
default: "",
},
{
type: "input",
name: "xrayId",
message: "Xray UUID (optional, for VLESS):",
default: "",
},
]);
const serverName = answers.serverName.trim();
const serverConfig = {
ip: answers.ip.trim(),
username: answers.username.trim(),
port: parseInt(answers.port),
createdAt: new Date().toISOString(),
singPassword: answers.singPassword?.trim() || "",
xrayId: answers.xrayId?.trim() || "",
};
if (answers.authMethod === "key") {
console.log(chalk.blue("Generating SSH key pair..."));
const keyInfo = await generateSSHKey(serverName);
serverConfig.keyPath = keyInfo.privateKeyPath;
serverConfig.publicKeyPath = keyInfo.publicKeyPath;
// Ask if user wants to install key automatically
const { installKey } = await inquirer.prompt([
{
type: "confirm",
name: "installKey",
message:
"Would you like to automatically install the SSH key on the server?",
default: true,
},
]);
if (installKey) {
const { tempPassword } = await inquirer.prompt([
{
type: "password",
name: "tempPassword",
message: "Enter server password for key installation:",
},
]);
await installSSHKey(
{ ...serverConfig, password: tempPassword },
keyInfo
);
}
} else if (answers.authMethod === "existing_key") {
// Resolve the SSH key path
const keyPath = answers.keyPath.startsWith("~")
? path.join(os.homedir(), answers.keyPath.slice(1))
: path.resolve(answers.keyPath);
serverConfig.keyPath = keyPath;
console.log(chalk.green(`Using existing SSH key: ${keyPath}`));
} else {
serverConfig.password = answers.password;
}
config.servers[serverName] = serverConfig;
await saveConfig();
console.log(chalk.green(`✅ Server '${serverName}' added successfully!`));
} catch (error) {
console.error(chalk.red("Error adding server:"), error.message);
process.exit(1);
}
});
// Delete server command
program
.command("delete")
.description("Delete a server")
.action(async () => {
try {
const serverNames = Object.keys(config.servers);
if (serverNames.length === 0) {
console.log(chalk.yellow("No servers found."));
return;
}
const { serverName } = await inquirer.prompt([
{
type: "list",
name: "serverName",
message: "Select server to delete:",
choices: serverNames,
},
]);
const { confirm } = await inquirer.prompt([
{
type: "confirm",
name: "confirm",
message: `Are you sure you want to delete server '${serverName}'?`,
default: false,
},
]);
if (confirm) {
const server = config.servers[serverName];
// Remove SSH key files if they exist
if (server.keyPath && fs.existsSync(server.keyPath)) {
await fs.remove(server.keyPath);
}
if (server.publicKeyPath && fs.existsSync(server.publicKeyPath)) {
await fs.remove(server.publicKeyPath);
}
delete config.servers[serverName];
await saveConfig();
console.log(
chalk.green(`✅ Server '${serverName}' deleted successfully!`)
);
} else {
console.log(chalk.yellow("Delete cancelled."));
}
} catch (error) {
console.error(chalk.red("Error deleting server:"), error.message);
process.exit(1);
}
});
// Update server command
program
.command("update")
.description("Update server configuration")
.action(async () => {
try {
const serverNames = Object.keys(config.servers);
if (serverNames.length === 0) {
console.log(chalk.yellow("No servers found."));
return;
}
const { serverName } = await inquirer.prompt([
{
type: "list",
name: "serverName",
message: "Select server to update:",
choices: serverNames,
},
]);
const server = config.servers[serverName];
// Ask which properties to update
const { propertiesToUpdate } = await inquirer.prompt([
{
type: "checkbox",
name: "propertiesToUpdate",
message: "Select properties to update:",
choices: [
{ name: "IP Address", value: "ip" },
{ name: "Username", value: "username" },
{ name: "SSH Port", value: "port" },
{ name: "Authentication", value: "auth" },
{ name: "Sing-box Password", value: "singPassword" },
{ name: "Xray UUID", value: "xrayId" },
{ name: "Proxy Port", value: "proxyPort" },
],
},
]);
if (propertiesToUpdate.length === 0) {
console.log(chalk.yellow("No properties selected for update."));
return;
}
const updatePrompts = [];
if (propertiesToUpdate.includes("ip")) {
updatePrompts.push({
type: "input",
name: "ip",
message: "New IP address:",
default: server.ip,
validate: (input) => (input.trim() ? true : "IP address is required"),
});
}
if (propertiesToUpdate.includes("username")) {
updatePrompts.push({
type: "input",
name: "username",
message: "New username:",
default: server.username,
});
}
if (propertiesToUpdate.includes("port")) {
updatePrompts.push({
type: "input",
name: "port",
message: "New SSH port:",
default: server.port.toString(),
validate: (input) => {
const port = parseInt(input);
return port > 0 && port <= 65535
? true
: "Port must be between 1 and 65535";
},
});
}
if (propertiesToUpdate.includes("auth")) {
updatePrompts.push({
type: "list",
name: "authMethod",
message: "New authentication method:",
choices: [
{ name: "Generate SSH Key", value: "key" },
{ name: "Use existing SSH Key", value: "existing_key" },
{ name: "Password", value: "password" },
],
});
updatePrompts.push({
type: "input",
name: "keyPath",
message: "SSH key path:",
default: "~/.ssh/id_rsa",
when: (answers) => answers.authMethod === "existing_key",
validate: (input) => {
const resolvedPath = input.startsWith("~")
? path.join(os.homedir(), input.slice(1))
: path.resolve(input);
return fs.existsSync(resolvedPath)
? true
: "SSH key file does not exist";
},
});
updatePrompts.push({
type: "password",
name: "password",
message: "New password:",
when: (answers) => answers.authMethod === "password",
});
}
if (propertiesToUpdate.includes("singPassword")) {
updatePrompts.push({
type: "password",
name: "singPassword",
message: "New Sing-box password:",
default: server.singPassword || "",
});
}
if (propertiesToUpdate.includes("xrayId")) {
updatePrompts.push({
type: "input",
name: "xrayId",
message: "New Xray UUID:",
default: server.xrayId || "",
});
}
if (propertiesToUpdate.includes("proxyPort")) {
updatePrompts.push({
type: "input",
name: "proxyPort",
message: "New proxy port:",
default: server.proxyPort ? server.proxyPort.toString() : "8090",
validate: (input) => {
const port = parseInt(input);
return port > 0 && port <= 65535
? true
: "Port must be between 1 and 65535";
},
});
}
const updates = await inquirer.prompt(updatePrompts);
// Apply updates
if (updates.ip) server.ip = updates.ip.trim();
if (updates.username) server.username = updates.username.trim();
if (updates.port) server.port = parseInt(updates.port);
if (updates.singPassword !== undefined)
server.singPassword = updates.singPassword.trim();
if (updates.xrayId !== undefined) server.xrayId = updates.xrayId.trim();
if (updates.proxyPort) server.proxyPort = parseInt(updates.proxyPort);
if (updates.xrayBasePort) {
server.xrayPorts = {
socks: parseInt(updates.xrayBasePort),
http: parseInt(updates.xrayBasePort) + 1,
};
}
// Handle authentication updates
if (updates.authMethod) {
if (updates.authMethod === "key") {
console.log(chalk.blue("Generating new SSH key pair..."));
const keyInfo = await generateSSHKey(serverName);
server.keyPath = keyInfo.privateKeyPath;
server.publicKeyPath = keyInfo.publicKeyPath;
delete server.password;
const { installKey } = await inquirer.prompt([
{
type: "confirm",
name: "installKey",
message:
"Would you like to automatically install the SSH key on the server?",
default: true,
},
]);
if (installKey) {
const { tempPassword } = await inquirer.prompt([
{
type: "password",
name: "tempPassword",
message: "Enter server password for key installation:",
},
]);
await installSSHKey({ ...server, password: tempPassword }, keyInfo);
}
} else if (updates.authMethod === "existing_key") {
const keyPath = updates.keyPath.startsWith("~")
? path.join(os.homedir(), updates.keyPath.slice(1))
: path.resolve(updates.keyPath);
server.keyPath = keyPath;
delete server.password;
console.log(chalk.green(`Using existing SSH key: ${keyPath}`));
} else if (updates.authMethod === "password") {
server.password = updates.password;
delete server.keyPath;
delete server.publicKeyPath;
}
}
server.updatedAt = new Date().toISOString();
await saveConfig();
console.log(
chalk.green(`✅ Server '${serverName}' updated successfully!`)
);
} catch (error) {
console.error(chalk.red("Error updating server:"), error.message);
process.exit(1);
}
});
// List servers command
program
.command("ls")
.description("List all servers")
.action(() => {
const serverNames = Object.keys(config.servers);
if (serverNames.length === 0) {
console.log(
chalk.yellow('No servers found. Use "bun sm.js add" to add a server.')
);
return;
}
const table = new Table({
head: [
"Name",
"IP",
"Username",
"Port",
"Auth",
"Sing-box",
"Xray",
"Created",
].map((h) => chalk.cyan(h)),
style: { head: [], border: [] },
});
serverNames.forEach((name) => {
const server = config.servers[name];
table.push([
chalk.white(name),
chalk.yellow(server.ip),
chalk.blue(server.username),
chalk.magenta(server.port),
server.keyPath ? chalk.green("SSH Key") : chalk.red("Password"),
server.singPassword ? chalk.green("✓") : chalk.gray("✗"),
server.xrayId ? chalk.green("✓") : chalk.gray("✗"),
server.createdAt
? new Date(server.createdAt).toLocaleDateString()
: "Unknown",
]);
});
console.log(table.toString());
});
// SSH command
program
.command("ssh")
.description("Connect to server via SSH")
.argument("<serverName>", "Server name")
.action((serverName) => {
const server = getServer(serverName);
console.log(
chalk.blue(
`Connecting to ${server.username}@${server.ip}:${server.port}...`
)
);
const sshProcess = createSSHConnection(server);
sshProcess.on("error", (error) => {
console.error(chalk.red("SSH connection error:"), error.message);
process.exit(1);
});
sshProcess.on("close", (code) => {
console.log(chalk.blue(`SSH connection closed with code ${code}`));
});
});
// SOCKS5 command
program
.command("socks5")
.description("Create SOCKS5 proxy tunnel")
.argument("<serverName>", "Server name")
.option("-p, --port <port>", "Local port for SOCKS5 proxy")
.action(async (serverName, options) => {
const server = getServer(serverName);
let localPort;
// Check if port is provided via option
if (options.port) {
localPort = parseInt(options.port);
} else if (server.proxyPort) {
// Use stored port preference
localPort = server.proxyPort;
} else {
// Ask for port and store preference
const { port } = await inquirer.prompt([
{
type: "input",
name: "port",
message: "Enter local port for SOCKS5 proxy:",
default: "8090",
validate: (input) => {
const port = parseInt(input);
return port > 0 && port <= 65535
? true
: "Port must be between 1 and 65535";
},
},
]);
localPort = parseInt(port);
// Store port preference
server.proxyPort = localPort;
await saveConfig();
}
console.log(chalk.blue(`Creating SOCKS5 proxy tunnel...`));
console.log(chalk.green(`SOCKS5 proxy will be available at:`));
console.log(chalk.cyan(` socks5://127.0.0.1:${localPort}`));
// Try to get network IP
try {
const networkInterface = os.networkInterfaces();
const networkIP = Object.values(networkInterface)
.flat()
.find((iface) => iface.family === "IPv4" && !iface.internal)?.address;
if (networkIP) {
console.log(chalk.cyan(` socks5://${networkIP}:${localPort}`));
}
} catch (error) {
// Ignore network IP detection errors
}
const sshProcess = createSSHConnection(server, { dynamicPort: localPort });
sshProcess.on("error", (error) => {
console.error(chalk.red("SOCKS5 tunnel error:"), error.message);
process.exit(1);
});
sshProcess.on("close", (code) => {
console.log(chalk.blue(`SOCKS5 tunnel closed with code ${code}`));
});
});
// Sing-box command
program
.command("sing-box")
.description("Start sing-box with server configuration")
.argument("<serverName>", "Server name")
.action(async (serverName) => {
const server = getServer(serverName);
// Check if sing-box is installed
try {
await execAsync("which sing-box");
} catch (error) {
console.error(chalk.red("Error: sing-box is not installed."));
console.log(chalk.blue("Please install sing-box first:"));
console.log(chalk.cyan(" https://sing-box.sagernet.org/installation/"));
process.exit(1);
}
// Get sing-box password
let singPassword = server.singPassword;
if (!singPassword) {
console.log(chalk.red("No sing-box password found for this server."));
console.log(chalk.blue("Please set it first using: assh update"));
process.exit(1);
}
// Create temporary config file
const tempConfigPath = path.join(
os.tmpdir(),
`assh_${serverName}_sing_${Date.now()}.json`
);
const singConfig = {
experimental: {
clash_api: {
external_controller: "127.0.0.1:9090",
},
},
log: {
level: "error",
},
dns: {
fakeip: {
enabled: true,
inet4_range: "198.18.0.0/15",
inet6_range: "fc00::/18",
},
servers: [
{
tag: "google",
address: "tls://94.140.14.14",
detour: "proxy",
},
{
tag: "tx",
address: "1.1.1.1",
detour: "direct",
},
{
tag: "fakeip",
address: "fakeip",
},
{
tag: "block",
address: "rcode://success",
},
],
rules: [
{
outbound: "any",
server: "google",
},
{
clash_mode: "global",
server: "google",
},
],
final: "google",
independent_cache: false,
strategy: "prefer_ipv4",
},
inbounds: [
{
domain_strategy: "prefer_ipv4",
endpoint_independent_nat: true,
address: ["172.16.0.1/30"],
mtu: 1500,
sniff: true,
sniff_override_destination: true,
auto_route: true,
strict_route: true,
type: "tun",
},
],
outbounds: [
{
type: "direct",
tag: "direct",
},
{
type: "dns",
tag: "dns-out",
},
{
tag: "proxy",
type: "shadowsocks",
server: server.ip,
server_port: 8080,
method: "2022-blake3-aes-128-gcm",
password: singPassword,
},
],
route: {
auto_detect_interface: true,
final: "proxy",
rules: [
{
domain_suffix: [".ir"],
outbound: "direct",
},
{
type: "logical",
mode: "or",
rules: [
{
port: 53,
},
{
protocol: "dns",
},
],
outbound: "dns-out",
},
{
ip_is_private: true,
outbound: "direct",
},
],
},
};
try {
await fs.writeJson(tempConfigPath, singConfig, { spaces: 2 });
// Track temp file for cleanup
tempFiles.add(tempConfigPath);
console.log(chalk.blue("Starting sing-box..."));
console.log(
chalk.yellow("Note: This requires sudo privileges for TUN interface.")
);
const singProcess = spawn(
"sudo",
["sing-box", "run", "-c", tempConfigPath],
{ stdio: "inherit" }
);
singProcess.on("error", (error) => {
console.error(chalk.red("Sing-box error:"), error.message);
// Cleanup temp file on error
tempFiles.delete(tempConfigPath);
try {
fs.removeSync(tempConfigPath);
} catch (e) {
// Ignore cleanup errors
}
process.exit(1);
});
singProcess.on("close", (code) => {
console.log(chalk.blue(`Sing-box closed with code ${code}`));
// Cleanup temp file on close
tempFiles.delete(tempConfigPath);
try {
fs.removeSync(tempConfigPath);
} catch (e) {
// Ignore cleanup errors
}
});
} catch (error) {
console.error(chalk.red("Error starting sing-box:"), error.message);
process.exit(1);
}
});
// Xray command
program
.command("xray")
.description("Start Xray with server configuration")
.argument("<serverName>", "Server name")
.action(async (serverName) => {
const server = getServer(serverName);
// Check if xray is installed
try {
await execAsync("which xray");
} catch (error) {
console.error(chalk.red("Error: xray is not installed."));
console.log(chalk.blue("Please install xray first:"));
console.log(
chalk.cyan(" https://xtls.github.io/en/document/install.html")
);
process.exit(1);
}
// Get xray UUID
let xrayId = server.xrayId;
if (!xrayId) {
console.log(chalk.red("No Xray UUID found for this server."));
console.log(chalk.blue("Please set it first using: assh update"));
process.exit(1);
}
// Get port preferences
let socksPort, httpPort;
if (server.proxyPort) {
socksPort = server.proxyPort;
httpPort = server.proxyPort + 1;
} else {
const { mixedPort } = await inquirer.prompt([
{
type: "input",
name: "mixedPort",
message: "Enter base port for SOCKS5 and HTTP proxy:",
default: "8090",
validate: (input) => {
const port = parseInt(input);
return port > 0 && port <= 65533
? true
: "Port must be between 1 and 65533";
},
},
]);
socksPort = parseInt(mixedPort);
httpPort = socksPort + 1;
// Store port preferences
server.proxyPort = socksPort;
await saveConfig();
}
const xrayConfig = {
log: {
loglevel: "none",
},
outbounds: [
{
protocol: "vless",
settings: {
vnext: [
{
address: server.ip,
port: 443,
users: [
{
id: xrayId,
alterId: 0,
encryption: "none",
},
],
},
],
},
},
],
inbounds: [
{
port: socksPort,
protocol: "socks",
settings: {
auth: "noauth",
udp: true,
},
},
{
port: httpPort,
protocol: "http",
},
],
};
try {
console.log(chalk.blue("Starting Xray..."));
console.log(
chalk.green(
`SOCKS5 proxy available at: socks5://127.0.0.1:${socksPort}`
)
);
console.log(
chalk.green(`HTTP proxy available at: http://127.0.0.1:${httpPort}`)
);
const xrayProcess = spawn("xray", ["run"], {
stdio: ["pipe", "inherit", "inherit"],
input: JSON.stringify(xrayConfig),
});
xrayProcess.stdin.write(JSON.stringify(xrayConfig));
xrayProcess.stdin.end();
xrayProcess.on("error", (error) => {
console.error(chalk.red("Xray error:"), error.message);
process.exit(1);
});
xrayProcess.on("close", (code) => {
console.log(chalk.blue(`Xray closed with code ${code}`));
});
} catch (error) {
console.error(chalk.red("Error starting Xray:"), error.message);
process.exit(1);
}
});
// Ping command
program
.command("ping")
.description("Ping server to check connectivity")
.argument("<serverName>", "Server name")
.action(async (serverName) => {
const server = getServer(serverName);
console.log(chalk.blue(`Pinging ${server.ip}...`));
try {
const pingCommand =
process.platform === "win32"
? `ping -n 4 ${server.ip}`
: `ping -c 4 ${server.ip}`;
const { stdout } = await execAsync(pingCommand);
console.log(stdout);
console.log(chalk.green(`✅ Server ${server.ip} is reachable`));
} catch (error) {
console.error(chalk.red(`❌ Failed to ping ${server.ip}`));
console.error(chalk.red(error.message));
process.exit(1);
}
});
// Parse command line arguments
program.parse();